Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs

This commit is contained in:
Bryan Ashby 2017-09-07 20:41:50 -06:00
commit 8acfa609f4
64 changed files with 2804 additions and 896 deletions

View File

@ -1,6 +1,6 @@
# ENiGMA½ BBS Software # ENiGMA½ BBS Software
![alt text](http://i325.photobucket.com/albums/k361/request4spam/enigma.ans_zps05w2ey4s.png "ENiGMA½ BBS") ![alt text](docs/images/enigma-bbs.png "ENiGMA½ BBS")
ENiGMA½ is a modern BBS software with a nostalgic flair! ENiGMA½ is a modern BBS software with a nostalgic flair!
@ -9,26 +9,25 @@ ENiGMA½ is a modern BBS software with a nostalgic flair!
* Multi platform: Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows) * Multi platform: Anywhere [Node.js](https://nodejs.org/) runs likely works (known to work under Linux, FreeBSD, OpenBSD, OS X and Windows)
* Unlimited multi node support (for all those BBS "callers"!) * Unlimited multi node support (for all those BBS "callers"!)
* **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods * **Highly** customizable via [HJSON](http://hjson.org/) based configuration, menus, and themes in addition to JavaScript based mods
* MCI support for lightbars, toggles, input areas, and so on plus many other other bells and whistles * [MCI support](docs/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles
* Telnet & **SSH** access built in. Additional servers are easy to implement * Telnet, **SSH**, and both secure and non-secure [WebSocket](https://en.wikipedia.org/wiki/WebSocket) access built in! Additional servers are easy to implement
* [CP437](http://www.ascii-codes.com/) and UTF-8 output * [CP437](http://www.ascii-codes.com/) and UTF-8 output
* [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior * [SyncTerm](http://syncterm.bbsdev.net/) style font and baud emulation support. Display PC/DOS and Amiga style artwork as it's intended! In general, ANSI-BBS / [cterm.txt](http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt?content-type=text%2Fplain&revision=HEAD) / [bansi.txt](http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt) are followed for expected BBS behavior
* [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support * Full [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support
* Renegade style pipe color codes * Renegade style pipe color codes
* [SQLite](http://sqlite.org/) storage of users, message areas, and so on * [SQLite](http://sqlite.org/) storage of users, message areas, and so on
* Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption
* [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), and [DoorParty](http://forums.throwbackbbs.com/) support! * [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), and [Exodus](https://oddnetwork.org/exodus/) support!
* [Bunyan](https://github.com/trentm/node-bunyan) logging * [Bunyan](https://github.com/trentm/node-bunyan) logging
* [Message networks](docs/msg_networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export * [Message networks](docs/msg_networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export
* [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/web_server.md). Legacy X/Y/Z modem also supported! * [Gazelle](https://github.com/WhatCD/Gazelle) inspirted File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/web_server.md). Legacy X/Y/Z modem also supported!
* Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more! * Upload processor supporting [FILE_ID.DIZ](https://en.wikipedia.org/wiki/FILE_ID.DIZ) and [NFO](https://en.wikipedia.org/wiki/.nfo) extraction, year estimation, and more!
* ANSI support in the Full Screen Editor (FSE), file descriptions, and so on
## In the Works ## In the Works
* More ES6+ usage, and **documentation**! * More ES6+ usage, and **documentation**!
* More ACS support coverage * More ACS support coverage
* SysOp dashboard (ye ol' WFC) * SysOp dashboard (ye ol' WFC)
* Missing functionality such as message FTS, user coloring of messages in the FST, etc.
* String localization
* A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) * A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
## Known Issues ## Known Issues
@ -40,19 +39,20 @@ See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more
* Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) * Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
* **Discussion on a ENiGMA BBS!** (see Boards below) * **Discussion on a ENiGMA BBS!** (see Boards below)
* IRC: **#enigma-bbs** on **chat.freenode.net** * IRC: **#enigma-bbs** on **chat.freenode.net**
* Discussion on [fsxNet](http://bbs.geek.nz/#fsxNet) available on many boards
* Email: bryan -at- l33t.codes * Email: bryan -at- l33t.codes
* [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/) * [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/)
* ENiGMA discussion on [fsxNet](http://bbs.geek.nz/#fsxNet)
## Terminal Clients ## Terminal Clients
ENiGMA has been tested with many terminals. However, the following are suggested for BBSing: ENiGMA has been tested with many terminals. However, the following are suggested for BBSing:
* [VTX](https://github.com/codewar65/VTX_ClientServer) (Try [Xibalba using VTX](https://l33t.codes/vtx/xibalba.html)!)
* [SyncTERM](http://syncterm.bbsdev.net/) * [SyncTERM](http://syncterm.bbsdev.net/)
* [EtherTerm](https://github.com/M-griffin/EtherTerm) * [EtherTerm](https://github.com/M-griffin/EtherTerm)
* [NetRunner](http://mysticbbs.com/downloads.html) * [NetRunner](http://mysticbbs.com/downloads.html)
## Boards ## Boards
* WQH: :skull: Xibalba :skull: (**telnet://xibalba.l33t.codes:44510**) * WQH: :skull: [Xibalba](https://l33t.codes/xibalba-bbs) :skull: (**telnet://xibalba.l33t.codes:44510** or via SSH secure on port 44511)
* Exotica: (**telnet://andrew.homeunix.org:2023**) * [Exotica](https://exoticabbs.com/): (**telnet://exoticabbs.com:8888**)
* [force9](http://bbs.force9.org/): (**telnet://bbs.force9.org**) * [force9](http://bbs.force9.org/): (**telnet://bbs.force9.org**)
@ -60,10 +60,11 @@ ENiGMA has been tested with many terminals. However, the following are suggested
``` ```
curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash
``` ```
<br>
(See the [Quickstart](docs/index.md#quickstart) for more information) Please see the [Quickstart](docs/index.md) for more information.
## Special Thanks ## Special Thanks
* [Daniel Mecklenburg Jr.](https://github.com/codewar65) for the awesome VTX terminal and general coding talk
* [M. Brutman](http://www.brutman.com/), author of [mTCP](http://www.brutman.com/mTCP/mTCP.html) (Interwebs for DOS!) * [M. Brutman](http://www.brutman.com/), author of [mTCP](http://www.brutman.com/mTCP/mTCP.html) (Interwebs for DOS!)
* [M. Griffin](https://github.com/M-griffin), author of [Enthral BBS](https://github.com/M-griffin/Enthral), [Oblivion/2 XRM](https://github.com/M-griffin/Oblivion2-XRM) and [EtherTerm](https://github.com/M-griffin/EtherTerm)! * [M. Griffin](https://github.com/M-griffin), author of [Enthral BBS](https://github.com/M-griffin/Enthral), [Oblivion/2 XRM](https://github.com/M-griffin/Oblivion2-XRM) and [EtherTerm](https://github.com/M-griffin/EtherTerm)!
* [Caphood](http://www.reddit.com/user/Caphood), supreme SysOp of [BLACK ƒlag](http://www.bbsnexus.com/directory/listing/blackflag.html) BBS * [Caphood](http://www.reddit.com/user/Caphood), supreme SysOp of [BLACK ƒlag](http://www.bbsnexus.com/directory/listing/blackflag.html) BBS

View File

@ -44,14 +44,10 @@ function ANSIEscapeParser(options) {
self.row += rows; self.row += rows;
self.column = Math.max(self.column, 1); self.column = Math.max(self.column, 1);
self.column = Math.min(self.column, self.termWidth); self.column = Math.min(self.column, self.termWidth); // can't move past term width
self.row = Math.max(self.row, 1); self.row = Math.max(self.row, 1);
self.row = Math.min(self.row, self.termHeight);
// self.emit('move cursor', self.column, self.row);
self.positionUpdated(); self.positionUpdated();
//self.rowUpdated();
}; };
self.saveCursorPosition = function() { self.saveCursorPosition = function() {
@ -85,22 +81,18 @@ function ANSIEscapeParser(options) {
}; };
function literal(text) { function literal(text) {
let charCode;
let pos;
let start = 0;
const len = text.length; const len = text.length;
let pos = 0;
let start = 0;
let charCode;
function emitLiteral() { while(pos < len) {
self.emit('literal', text.slice(start, pos)); charCode = text.charCodeAt(pos) & 0xff; // 8bit clean
start = pos;
}
for(pos = 0; pos < len; ++pos) {
charCode = text.charCodeAt(pos) & 0xff; // ensure 8bit clean
switch(charCode) { switch(charCode) {
case CR : case CR :
emitLiteral(); self.emit('literal', text.slice(start, pos));
start = pos;
self.column = 1; self.column = 1;
@ -108,7 +100,8 @@ function ANSIEscapeParser(options) {
break; break;
case LF : case LF :
emitLiteral(); self.emit('literal', text.slice(start, pos));
start = pos;
self.row += 1; self.row += 1;
@ -116,73 +109,37 @@ function ANSIEscapeParser(options) {
break; break;
default : default :
if(self.column > self.termWidth) { if(self.column === self.termWidth) {
// self.emit('literal', text.slice(start, pos + 1));
// Emit data up to this point so it can be drawn before the postion update start = pos + 1;
//
emitLiteral();
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} else { } else {
self.column += 1; self.column += 1;
} }
break; break;
} }
++pos;
} }
self.emit('literal', text.slice(start)); //
// Finalize this chunk
//
if(self.column > self.termWidth) { if(self.column > self.termWidth) {
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} }
}
function literal2(text) { const rem = text.slice(start);
var charCode; if(rem) {
self.emit('literal', rem);
var len = text.length;
for(var i = 0; i < len; i++) {
charCode = text.charCodeAt(i) & 0xff; // ensure 8 bit
switch(charCode) {
case CR :
self.column = 1;
break;
case LF :
self.row++;
self.positionUpdated();
//self.rowUpdated();
break;
default :
// wrap
if(self.column > self.termWidth) {
self.column = 1;
self.row++;
//self.rowUpdated();
self.positionUpdated();
} else {
self.column += 1;
}
break;
}
if(self.row === self.termHeight) {
self.scrollBack += 1;
self.row -= 1;
self.positionUpdated();
}
} }
self.emit('literal', text);
} }
function getProcessedMCI(mci) { function getProcessedMCI(mci) {
@ -238,10 +195,10 @@ function ANSIEscapeParser(options) {
}); });
if(self.mciReplaceChar.length > 0) { if(self.mciReplaceChar.length > 0) {
//self.emit('chunk', ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase));
const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase); const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase);
self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[\;m]/).slice(0, 3)); self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[\;m]/).slice(0, 3));
//self.emit('control', ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase));
literal(new Array(match[0].length + 1).join(self.mciReplaceChar)); literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
} else { } else {
literal(match[0]); literal(match[0]);
@ -436,6 +393,8 @@ function ANSIEscapeParser(options) {
// set graphic rendition // set graphic rendition
case 'm' : case 'm' :
self.graphicRendition.reset = false;
for(let i = 0, len = args.length; i < len; ++i) { for(let i = 0, len = args.length; i < len; ++i) {
arg = args[i]; arg = args[i];
@ -453,8 +412,12 @@ function ANSIEscapeParser(options) {
delete self.graphicRendition.negative; delete self.graphicRendition.negative;
delete self.graphicRendition.invisible; delete self.graphicRendition.invisible;
self.graphicRendition.fg = 39; delete self.graphicRendition.fg;
self.graphicRendition.bg = 49; delete self.graphicRendition.bg;
self.graphicRendition.reset = true;
//self.graphicRendition.fg = 39;
//self.graphicRendition.bg = 49;
break; break;
case 1 : case 1 :
@ -490,6 +453,8 @@ function ANSIEscapeParser(options) {
} }
} }
} }
self.emit('sgr update', self.graphicRendition);
break; // m break; // m
// :TODO: s, u, K // :TODO: s, u, K

212
core/ansi_prep.js Normal file
View File

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

View File

@ -17,12 +17,22 @@
// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt
// * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm // * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm
// //
// VTX
// * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
//
// General // General
// * http://en.wikipedia.org/wiki/ANSI_escape_code // * http://en.wikipedia.org/wiki/ANSI_escape_code
// * http://www.inwap.com/pdp10/ansicode.txt // * http://www.inwap.com/pdp10/ansicode.txt
// //
// Other Implementations // Other Implementations
// * https://github.com/chjj/term.js/blob/master/src/term.js // * https://github.com/chjj/term.js/blob/master/src/term.js
//
//
// For a board, we need to support the semi-standard ANSI-BBS "spec" which
// 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½ // ENiGMA½
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
@ -31,6 +41,7 @@ const miscUtil = require('./misc_util.js');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.getFullMatchRegExp = getFullMatchRegExp;
exports.getFGColorValue = getFGColorValue; exports.getFGColorValue = getFGColorValue;
exports.getBGColorValue = getBGColorValue; exports.getBGColorValue = getBGColorValue;
exports.sgr = sgr; exports.sgr = sgr;
@ -172,6 +183,12 @@ const SGRValues = {
whiteBG : 47, 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
}
function getFGColorValue(name) { function getFGColorValue(name) {
return SGRValues[name]; return SGRValues[name];
} }
@ -289,7 +306,6 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = {
'amiga_microknight' : 'microknight', 'amiga_microknight' : 'microknight',
'amiga_microknight+' : 'microknight_plus', 'amiga_microknight+' : 'microknight_plus',
'atari' : 'atari', 'atari' : 'atari',
'atarist' : 'atari', 'atarist' : 'atari',
@ -399,10 +415,6 @@ function getSGRFromGraphicRendition(graphicRendition, initialReset) {
} }
}); });
if(!styleCount) {
sgrSeq.push(0);
}
if(graphicRendition.fg) { if(graphicRendition.fg) {
sgrSeq.push(graphicRendition.fg); sgrSeq.push(graphicRendition.fg);
} }
@ -411,7 +423,7 @@ function getSGRFromGraphicRendition(graphicRendition, initialReset) {
sgrSeq.push(graphicRendition.bg); sgrSeq.push(graphicRendition.bg);
} }
if(initialReset) { if(0 === styleCount || initialReset) {
sgrSeq.unshift(0); sgrSeq.unshift(0);
} }
@ -473,4 +485,3 @@ function setEmulatedBaudRate(rate) {
}[rate] || 0; }[rate] || 0;
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
} }

View File

@ -157,18 +157,19 @@ function getArt(name, options, cb) {
// Ignore anything not allowed in |options.types| // Ignore anything not allowed in |options.types|
// //
const fext = paths.extname(file); const fext = paths.extname(file);
if(options.types.indexOf(fext.toLowerCase()) < 0) { if(!options.types.includes(fext.toLowerCase())) {
return false; return false;
} }
const bn = paths.basename(file, fext).toLowerCase(); const bn = paths.basename(file, fext).toLowerCase();
if(options.random) { if(options.random) {
const suppliedBn = paths.basename(name, fext).toLowerCase(); const suppliedBn = paths.basename(name, fext).toLowerCase();
// //
// Random selection enabled. We'll allow for // Random selection enabled. We'll allow for
// basename1.ext, basename2.ext, ... // basename1.ext, basename2.ext, ...
// //
if(bn.indexOf(suppliedBn) !== 0) { if(!bn.startsWith(suppliedBn)) {
return false; return false;
} }
@ -241,6 +242,10 @@ function display(client, art, options, cb) {
options.mciReplaceChar = options.mciReplaceChar || ' '; options.mciReplaceChar = options.mciReplaceChar || ' ';
options.disableMciCache = options.disableMciCache || false; options.disableMciCache = options.disableMciCache || false;
// :TODO: this is going to be broken into two approaches controlled via options:
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
// 2) CPR driven
if(!_.isBoolean(options.iceColors)) { if(!_.isBoolean(options.iceColors)) {
// try to detect from SAUCE // try to detect from SAUCE
if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) {
@ -282,7 +287,6 @@ function display(client, art, options, cb) {
return cb(null, mciMap, extraInfo); return cb(null, mciMap, extraInfo);
} }
if(!options.disableMciCache) { if(!options.disableMciCache) {
artHash = farmhash.hash32(art); artHash = farmhash.hash32(art);
@ -335,14 +339,14 @@ function display(client, art, options, cb) {
} }
mciCprQueue.push(mapKey); mciCprQueue.push(mapKey);
client.term.write(ansi.queryPos(), false); client.term.rawWrite(ansi.queryPos());
} }
}); });
} }
ansiParser.on('literal', literal => client.term.write(literal, false) ); ansiParser.on('literal', literal => client.term.write(literal, false) );
ansiParser.on('control', control => client.term.write(control, false) ); ansiParser.on('control', control => client.term.rawWrite(control) );
ansiParser.on('complete', () => { ansiParser.on('complete', () => {
parseComplete = true; parseComplete = true;
@ -352,29 +356,35 @@ function display(client, art, options, cb) {
} }
}); });
let ansiFontSeq; let initSeq = '';
if(options.font) { if(options.font) {
ansiFontSeq = ansi.setSyncTermFontWithAlias(options.font); initSeq = ansi.setSyncTermFontWithAlias(options.font);
} else if(options.sauce) { } else if(options.sauce) {
let fontName = getFontNameFromSAUCE(options.sauce); let fontName = getFontNameFromSAUCE(options.sauce);
if(fontName) { if(fontName) {
fontName = ansi.getSyncTERMFontFromAlias(fontName); fontName = ansi.getSyncTERMFontFromAlias(fontName);
} }
// don't set default (CP437) from SAUCE //
if(fontName && 'cp437' !== fontName) { // Set SyncTERM font if we're switching only. Most terminals
ansiFontSeq = ansi.setSyncTERMFont(fontName); // that support this ESC sequence can only show *one* font
// at a time. This applies to detection only (e.g. SAUCE).
// If explicit, we'll set it no matter what (above)
//
if(fontName && client.term.currentSyncFont != fontName) {
client.term.currentSyncFont = fontName;
initSeq = ansi.setSyncTERMFont(fontName);
} }
} }
if(ansiFontSeq) { if(options.iceColors) {
client.term.write(ansiFontSeq, false); initSeq += ansi.blinkToBrightIntensity();
} }
if(options.iceColors) { if(initSeq) {
client.term.write(ansi.blinkToBrightIntensity(), false); client.term.rawWrite(initSeq);
} }
ansiParser.reset(art); ansiParser.reset(art);
ansiParser.parse(); return ansiParser.parse();
} }

View File

@ -9,7 +9,6 @@
const conf = require('./config.js'); const conf = require('./config.js');
const logger = require('./logger.js'); const logger = require('./logger.js');
const database = require('./database.js'); const database = require('./database.js');
const clientConns = require('./client_connections.js');
const resolvePath = require('./misc_util.js').resolvePath; const resolvePath = require('./misc_util.js').resolvePath;
// deps // deps
@ -84,7 +83,7 @@ function main() {
} }
return callback(err); return callback(err);
}); });
}, }
], ],
function complete(err) { function complete(err) {
// note this is escaped: // note this is escaped:
@ -111,11 +110,12 @@ function shutdownSystem() {
async.series( async.series(
[ [
function closeConnections(callback) { function closeConnections(callback) {
const activeConnections = clientConns.getActiveConnections(); const ClientConns = require('./client_connections.js');
const activeConnections = ClientConns.getActiveConnections();
let i = activeConnections.length; let i = activeConnections.length;
while(i--) { while(i--) {
activeConnections[i].term.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); activeConnections[i].term.write('\n\nServer is shutting down NOW! Disconnecting...\n\n');
clientConns.removeClient(activeConnections[i]); ClientConns.removeClient(activeConnections[i]);
} }
callback(null); callback(null);
}, },
@ -179,6 +179,9 @@ function initialize(cb) {
function initDatabases(callback) { function initDatabases(callback) {
return database.initializeDatabases(callback); return database.initializeDatabases(callback);
}, },
function initMimeTypes(callback) {
return require('./mime_util.js').startup(callback);
},
function initStatLog(callback) { function initStatLog(callback) {
return require('./stat_log.js').init(callback); return require('./stat_log.js').init(callback);
}, },
@ -237,6 +240,9 @@ function initialize(cb) {
function readyMessageNetworkSupport(callback) { function readyMessageNetworkSupport(callback) {
return require('./msg_network.js').startup(callback); return require('./msg_network.js').startup(callback);
}, },
function readyEvents(callback) {
return require('./events.js').startup(callback);
},
function listenConnections(callback) { function listenConnections(callback) {
return require('./listening_server.js').startup(callback); return require('./listening_server.js').startup(callback);
}, },

View File

@ -120,6 +120,7 @@ function Client(input, output) {
// * Irssi ConnectBot (Android) // * Irssi ConnectBot (Android)
// //
'63;1;2' : 'arctel', '63;1;2' : 'arctel',
'50;86;84;88' : 'vtx',
}[deviceAttr]; }[deviceAttr];
if(!termClient) { if(!termClient) {
@ -491,3 +492,13 @@ Client.prototype.defaultHandlerMissingMod = function(err) {
return handler; return handler;
}; };
Client.prototype.terminalSupports = function(query) {
switch(query) {
case 'vtx_audio' :
// https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
return this.termClient === 'vtx';
default :
return false;
}
};

View File

@ -3,6 +3,7 @@
// ENiGMA½ // ENiGMA½
const logger = require('./logger.js'); const logger = require('./logger.js');
const Events = require('./events.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -76,6 +77,8 @@ function addNewClient(client, clientSock) {
client.log.info(connInfo, 'Client connected'); client.log.info(connInfo, 'Client connected');
Events.emit('codes.l33t.enigma.system.connected', { client : client, connectionCount : clientConnections.length } );
return id; return id;
} }
@ -93,6 +96,8 @@ function removeClient(client) {
}, },
'Client disconnected' 'Client disconnected'
); );
Events.emit('codes.l33t.enigma.system.disconnected', { client : client, connectionCount : clientConnections.length } );
} }
} }

View File

@ -32,6 +32,8 @@ function ClientTerminal(output) {
var termWidth = 0; var termWidth = 0;
var termClient = 'unknown'; var termClient = 'unknown';
this.currentSyncFont = 'not_set';
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
this.env = {}; this.env = {};
@ -169,7 +171,7 @@ ClientTerminal.prototype.pipeWrite = function(s, spec, cb) {
var conv = { var conv = {
enigma : enigmaToAnsi, enigma : enigmaToAnsi,
renegade : renegadeToAnsi, renegade : renegadeToAnsi,
}[spec] || enigmaToAnsi; }[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|
}; };
@ -183,3 +185,4 @@ ClientTerminal.prototype.encode = function(s, convertLineFeeds) {
return iconv.encode(s, this.outputEncoding); return iconv.encode(s, this.outputEncoding);
}; };

View File

@ -23,6 +23,8 @@ exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi;
// * fromWWIV(): <ctrl-c><0-7> // * fromWWIV(): <ctrl-c><0-7>
// * fromSyncronet(): <ctrl-a><colorCode> // * fromSyncronet(): <ctrl-a><colorCode>
// See http://wiki.synchro.net/custom:colors // See http://wiki.synchro.net/custom:colors
// :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc...
function enigmaToAnsi(s, client) { function enigmaToAnsi(s, client) {
if(-1 == s.indexOf('|')) { if(-1 == s.indexOf('|')) {
return s; // no pipe codes present return s; // no pipe codes present
@ -31,7 +33,7 @@ function enigmaToAnsi(s, client) {
var result = ''; var result = '';
var re = /\|([A-Z\d]{2}|\|)/g; var re = /\|([A-Z\d]{2}|\|)/g;
var m; var m;
var lastIndex = 0; var lastIndex = 0;
while((m = re.exec(s))) { while((m = re.exec(s))) {
var val = m[1]; var val = m[1];
@ -65,18 +67,18 @@ function enigmaToAnsi(s, client) {
} }
result += s.substr(lastIndex, m.index - lastIndex) + attr; result += s.substr(lastIndex, m.index - lastIndex) + attr;
} }
lastIndex = re.lastIndex; lastIndex = re.lastIndex;
} }
result = (0 === result.length ? s : result + s.substr(lastIndex)); result = (0 === result.length ? s : result + s.substr(lastIndex));
return result; return result;
} }
function stripEnigmaCodes(s) { function stripEnigmaCodes(s) {
return s.replace(/\|[A-Z\d]{2}/g, ''); return s.replace(/\|[A-Z\d]{2}/g, '');
} }
function enigmaStrLen(s) { function enigmaStrLen(s) {

View File

@ -100,6 +100,11 @@ function init(configPath, options, cb) {
], ],
function complete(err, mergedConfig) { function complete(err, mergedConfig) {
exports.config = mergedConfig; exports.config = mergedConfig;
exports.config.get = function(path) {
return _.get(exports.config, path);
};
return cb(err); return cb(err);
} }
); );
@ -316,6 +321,24 @@ function getDefaultConfig() {
longDescUtil : 'Exiftool', longDescUtil : 'Exiftool',
}, },
// //
// Video
//
'video/mp4' : {
desc : 'MPEG Video',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'video/x-matroska ' : {
desc : 'Matroska Video',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
'video/x-msvideo' : {
desc : 'Audio Video Interleave',
shortDescUtil : 'Exiftool2Desc',
longDescUtil : 'Exiftool',
},
//
// Images // Images
// //
'image/jpeg' : { 'image/jpeg' : {
@ -347,6 +370,12 @@ function getDefaultConfig() {
offset : 0, offset : 0,
archiveHandler : '7Zip', archiveHandler : '7Zip',
}, },
/*
'application/x-cbr' : {
desc : 'Comic Book Archive',
sig : '504b0304',
},
*/
'application/x-arj' : { 'application/x-arj' : {
desc : 'ARJ Archive', desc : 'ARJ Archive',
sig : '60ea', sig : '60ea',
@ -363,7 +392,7 @@ function getDefaultConfig() {
desc : 'Gzip Archive', desc : 'Gzip Archive',
sig : '1f8b', sig : '1f8b',
offset : 0, offset : 0,
archiveHandler : '7Zip', archiveHandler : 'TarGz',
}, },
// :TODO: application/x-bzip // :TODO: application/x-bzip
'application/x-bzip2' : { 'application/x-bzip2' : {
@ -474,6 +503,22 @@ function getDefaultConfig() {
cmd : 'unrar', cmd : 'unrar',
args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ],
} }
},
TarGz : {
decompress : {
cmd : 'tar',
args : [ '-xf', '{archivePath}', '-C', '{extractPath}', '--strip-components=1' ],
},
list : {
cmd : 'tar',
args : [ '-tvf', '{archivePath}' ],
entryMatch : '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$',
},
extract : {
cmd : 'tar',
args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ],
}
} }
}, },
}, },
@ -582,8 +627,10 @@ function getDefaultConfig() {
// Actual sizes may be slightly larger when we must place a full // Actual sizes may be slightly larger when we must place a full
// PKT contents *somewhere* // PKT contents *somewhere*
// //
packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt
bundleTargetByteSize : 2048000, // 2M, before creating another archive bundleTargetByteSize : 2048000, // 2M, before creating another archive
packetMsgEncoding : 'utf8', // default packet encoding. Override per node if desired.
packetAnsiMsgEncoding : 'cp437', // packet encoding for *ANSI ART* messages
tic : { tic : {
secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected)

View File

@ -3,6 +3,7 @@
// ENiGMA½ // ENiGMA½
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Events = require('./events.js');
// deps // deps
const async = require('async'); const async = require('async');
@ -175,6 +176,9 @@ function connectEntry(client, nextMenu) {
// //
displayBanner(term); displayBanner(term);
// fire event
Events.emit('codes.l33t.enigma.system.term_detected', { client : client } );
setTimeout( () => { setTimeout( () => {
return client.menuStack.goto(nextMenu); return client.menuStack.goto(nextMenu);
}, 500); }, 500);

View File

@ -71,9 +71,13 @@ function initializeDatabases(cb) {
}); });
} }
function enableForeignKeys(db) {
db.run('PRAGMA foreign_keys = ON;');
}
const DB_INIT_TABLE = { const DB_INIT_TABLE = {
system : (cb) => { system : (cb) => {
dbs.system.run('PRAGMA foreign_keys = ON;'); enableForeignKeys(dbs.system);
// Various stat/event logging - see stat_log.js // Various stat/event logging - see stat_log.js
dbs.system.run( dbs.system.run(
@ -110,7 +114,7 @@ const DB_INIT_TABLE = {
}, },
user : (cb) => { user : (cb) => {
dbs.user.run('PRAGMA foreign_keys = ON;'); enableForeignKeys(dbs.user);
dbs.user.run( dbs.user.run(
`CREATE TABLE IF NOT EXISTS user ( `CREATE TABLE IF NOT EXISTS user (
@ -152,7 +156,7 @@ const DB_INIT_TABLE = {
}, },
message : (cb) => { message : (cb) => {
dbs.message.run('PRAGMA foreign_keys = ON;'); enableForeignKeys(dbs.message);
dbs.message.run( dbs.message.run(
`CREATE TABLE IF NOT EXISTS message ( `CREATE TABLE IF NOT EXISTS message (
@ -260,7 +264,7 @@ const DB_INIT_TABLE = {
}, },
file : (cb) => { file : (cb) => {
dbs.file.run('PRAGMA foreign_keys = ON;'); enableForeignKeys(dbs.file);
dbs.file.run( dbs.file.run(
// :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system // :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system

72
core/descript_ion_file.js Normal file
View File

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

View File

@ -97,6 +97,10 @@ exports.getModule = class DoorPartyModule extends MenuModule {
}); });
}); });
sshClient.on('error', err => {
self.client.log.info(`DoorParty SSH client error: ${err.message}`);
});
sshClient.on('close', () => { sshClient.on('close', () => {
restorePipe(); restorePipe();
callback(null); callback(null);

73
core/events.js Normal file
View File

@ -0,0 +1,73 @@
/* jslint node: true */
'use strict';
const paths = require('path');
const events = require('events');
const Log = require('./logger.js').log;
// deps
const _ = require('lodash');
const async = require('async');
const glob = require('glob');
module.exports = new class Events extends events.EventEmitter {
constructor() {
super();
}
addListener(event, listener) {
Log.trace( { event : event }, 'Registering event listener');
return super.addListener(event, listener);
}
emit(event, ...args) {
Log.trace( { event : event }, 'Emitting event');
return super.emit(event, args);
}
on(event, listener) {
Log.trace( { event : event }, 'Registering event listener');
return super.on(event, listener);
}
once(event, listener) {
Log.trace( { event : event }, 'Registering single use event listener');
return super.once(event, listener);
}
removeListener(event, listener) {
Log.trace( { event : event }, 'Removing listener');
return super.removeListener(event, listener);
}
startup(cb) {
async.each(require('./module_util.js').getModulePaths(), (modulePath, nextPath) => {
glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => {
if(err) {
return nextPath(err);
}
async.each(files, (moduleName, nextModule) => {
modulePath = paths.join(modulePath, moduleName);
try {
const mod = require(modulePath);
if(_.isFunction(mod.registerEvents)) {
// :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ?
mod.registerEvents(this);
}
} catch(e) {
}
return nextModule(null);
}, err => {
return nextPath(err);
});
});
}, err => {
return cb(err);
});
}
};

231
core/exodus.js Normal file
View File

@ -0,0 +1,231 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule;
const resetScreen = require('../core/ansi_term.js').resetScreen;
const Config = require('./config.js').config;
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;
/*
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
// optional
caPem: /path/to/cacerts.pem // see https://curl.haxx.se/docs/caextract.html
// required
board: XXXX
key: XXXX
door: some_door
}
}
*/
exports.moduleInfo = {
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');
}
initSequence() {
const self = this;
let clientTerminated = false;
async.waterfall(
[
function validateConfig(callback) {
// very basic validation on optionals
async.each( [ 'board', 'key', 'door' ], (key, next) => {
return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`));
}, callback);
},
function loadCertAuthorities(callback) {
if(!_.isString(self.config.caPem)) {
return callback(null, null);
}
fs.readFile(self.config.caPem, (err, certAuthorities) => {
return callback(err, certAuthorities);
});
},
function getTicket(certAuthorities, callback) {
const now = moment.utc().unix();
const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex');
const token = `${sha256}|${now}`;
const postData = querystring.stringify({
token : token,
board : self.config.board,
user : self.client.user.username,
door : self.config.door,
});
const reqOptions = {
hostname : self.config.ticketHost,
port : self.config.ticketPort,
path : self.config.ticketPath,
rejectUnauthorized : self.config.rejectUnauthorized,
method : 'POST',
headers : {
'Content-Type' : 'application/x-www-form-urlencoded',
'Content-Length' : postData.length,
'User-Agent' : getEnigmaUserAgent(),
}
};
if(certAuthorities) {
reqOptions.ca = certAuthorities;
}
let ticket = '';
const req = https.request(reqOptions, res => {
res.on('data', data => {
ticket += data;
});
res.on('end', () => {
if(ticket.length !== 36) {
return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`));
}
return callback(null, ticket);
});
});
req.on('error', err => {
return callback(Errors.General(`Exodus error: ${err.message}`));
});
req.write(postData);
req.end();
},
function loadPrivateKey(ticket, callback) {
fs.readFile(self.config.sshKeyPem, (err, privateKey) => {
return callback(err, ticket, privateKey);
});
},
function establishSecureConnection(ticket, privateKey, callback) {
let pipeRestored = false;
let pipedStream;
function restorePipe() {
if(pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume();
}
}
self.client.term.write(resetScreen());
self.client.term.write('Connecting to Exodus server, please wait...\n');
const sshClient = new SSHClient();
const window = {
rows : self.client.term.termHeight,
cols : self.client.term.termWidth,
width : 0,
height : 0,
term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :(
};
const options = {
env : {
exodus : ticket,
},
};
sshClient.on('ready', () => {
self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating Exodus connection');
clientTerminated = true;
return sshClient.end();
});
sshClient.shell(window, options, (err, stream) => {
pipedStream = stream; // :TODO: ewwwwwwwww hack
self.client.term.output.pipe(stream);
stream.on('data', d => {
return self.client.term.rawWrite(d);
});
stream.on('close', () => {
restorePipe();
return sshClient.end();
});
stream.on('error', err => {
Log.warn( { error : err.message }, 'Exodus SSH client stream error');
});
});
});
sshClient.on('close', () => {
restorePipe();
return callback(null);
});
sshClient.connect({
host : self.config.sshHost,
port : self.config.sshPort,
username : self.config.sshUser,
privateKey : privateKey,
});
}
],
err => {
if(err) {
self.client.log.warn( { error : err.message }, 'Exodus error');
}
if(!clientTerminated) {
self.prevMenu();
}
}
);
}
};

View File

@ -284,7 +284,7 @@ class FileAreaWebAccess {
resp.on('finish', () => { resp.on('finish', () => {
// transfer completed fully // transfer completed fully
this.updateDownloadStatsForUserId(servedItem.userId, stats.size); this.updateDownloadStatsForUserIdAndSystemAndSystem(servedItem.userId, stats.size);
}); });
const headers = { const headers = {
@ -301,7 +301,7 @@ class FileAreaWebAccess {
}); });
} }
updateDownloadStatsForUserId(userId, dlBytes, cb) { updateDownloadStatsForUserIdAndSystem(userId, dlBytes, cb) {
async.waterfall( async.waterfall(
[ [
function fetchActiveUser(callback) { function fetchActiveUser(callback) {

View File

@ -1,8 +1,9 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const _ = require('lodash'); // deps
const uuidV4 = require('uuid/v4'); const _ = require('lodash');
const uuidV4 = require('uuid/v4');
module.exports = class FileBaseFilters { module.exports = class FileBaseFilters {
constructor(client) { constructor(client) {
@ -65,14 +66,14 @@ module.exports = class FileBaseFilters {
let filtersProperty = this.client.user.properties.file_base_filters; let filtersProperty = this.client.user.properties.file_base_filters;
let defaulted; let defaulted;
if(!filtersProperty) { if(!filtersProperty) {
filtersProperty = JSON.stringify(FileBaseFilters.getDefaultFilters()); filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
defaulted = true; defaulted = true;
} }
try { try {
this.filters = JSON.parse(filtersProperty); this.filters = JSON.parse(filtersProperty);
} catch(e) { } catch(e) {
this.filters = FileBaseFilters.getDefaultFilters(); // something bad happened; reset everything back to defaults :( this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
defaulted = true; defaulted = true;
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' );
} }
@ -107,18 +108,20 @@ module.exports = class FileBaseFilters {
return false; return false;
} }
static getDefaultFilters() { static getBuiltInSystemFilters() {
const filters = {}; const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
const uuid = uuidV4(); const filters = {
filters[uuid] = { [ U_LATEST ] : {
name : 'Default', name : 'By Date Added',
areaTag : '', // all areaTag : '', // all
terms : '', // * terms : '', // *
tags : '', // * tags : '', // *
order : 'descending', order : 'descending',
sort : 'upload_timestamp', sort : 'upload_timestamp',
uuid : uuid, uuid : U_LATEST,
system : true,
}
}; };
return filters; return filters;
@ -127,4 +130,20 @@ module.exports = class FileBaseFilters {
static getActiveFilter(client) { static getActiveFilter(client) {
return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid);
} }
static getFileBaseLastViewedFileIdByUser(user) {
return parseInt((user.properties.user_file_base_last_viewed || 0));
}
static setFileBaseLastViewedFileIdForUser(user, fileId, cb) {
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
if(fileId < current) {
if(cb) {
cb(null);
}
return;
}
return user.persistProperty('user_file_base_last_viewed', fileId, cb);
}
}; };

View File

@ -510,6 +510,14 @@ module.exports = class FileEntry {
); );
} }
if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) {
appendWhereClause(`DATETIME(f.upload_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`);
}
if(_.isNumber(filter.newerThanFileId)) {
appendWhereClause(`f.file_id > ${filter.newerThanFileId}`);
}
sql += `${sqlWhere} ${sqlOrderBy};`; sql += `${sqlWhere} ${sqlOrderBy};`;
const matchingFileIds = []; const matchingFileIds = [];

View File

@ -10,10 +10,11 @@ const Message = require('./message.js');
const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId;
const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
const User = require('./user.js'); const User = require('./user.js');
const cleanControlCodes = require('./string_util.js').cleanControlCodes;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const { isAnsi, cleanControlCodes, insert } = require('./string_util.js');
const Config = require('./config.js').config;
// deps // deps
const async = require('async'); const async = require('async');
@ -168,7 +169,6 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
cb(newFocusViewId); cb(newFocusViewId);
}, },
headerSubmit : function(formData, extraArgs, cb) { headerSubmit : function(formData, extraArgs, cb) {
self.switchToBody(); self.switchToBody();
return cb(null); return cb(null);
@ -210,21 +210,24 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}, },
appendQuoteEntry: function(formData, extraArgs, cb) { appendQuoteEntry: function(formData, extraArgs, cb) {
// :TODO: Dont' use magic # ID's here // :TODO: Dont' use magic # ID's here
var quoteMsgView = self.viewControllers.quoteBuilder.getView(1); const quoteMsgView = self.viewControllers.quoteBuilder.getView(1);
if(self.newQuoteBlock) { if(self.newQuoteBlock) {
self.newQuoteBlock = false; self.newQuoteBlock = false;
// :TODO: If replying to ANSI, add a blank sepration line here
quoteMsgView.addText(self.getQuoteByHeader()); quoteMsgView.addText(self.getQuoteByHeader());
} }
var quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote); const quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote);
quoteMsgView.addText(quoteText); quoteMsgView.addText(quoteText);
// //
// If this is *not* the last item, advance. Otherwise, do nothing as we // If this is *not* the last item, advance. Otherwise, do nothing as we
// don't want to jump back to the top and repeat already quoted lines // don't want to jump back to the top and repeat already quoted lines
// //
var quoteListView = self.viewControllers.quoteBuilder.getView(3); const quoteListView = self.viewControllers.quoteBuilder.getView(3);
if(quoteListView.getData() !== quoteListView.getCount() - 1) { if(quoteListView.getData() !== quoteListView.getCount() - 1) {
quoteListView.focusNext(); quoteListView.focusNext();
} else { } else {
@ -316,22 +319,38 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
} }
buildMessage() { buildMessage(cb) {
const headerValues = this.viewControllers.header.getFormData().value; const headerValues = this.viewControllers.header.getFormData().value;
var msgOpts = { const msgOpts = {
areaTag : this.messageAreaTag, areaTag : this.messageAreaTag,
toUserName : headerValues.to, toUserName : headerValues.to,
fromUserName : this.client.user.username, fromUserName : this.client.user.username,
subject : headerValues.subject, subject : headerValues.subject,
message : this.viewControllers.body.getFormData().value.message, // :TODO: don't hard code 1 here:
message : this.viewControllers.body.getView(1).getData( { forceLineTerms : this.replyIsAnsi } ),
}; };
if(this.isReply()) { 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!)
//
msgOpts.meta = { System : { 'explicit_encoding' : Config.scannerTossers.ftn_bso.packetAnsiMsgEncoding || 'cp437' } };
// :TODO: change to <ansi>\r\nESC[A<message>
//msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}${msgOpts.message}`;
msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`;
}
} }
this.message = new Message(msgOpts); this.message = new Message(msgOpts);
return cb(null);
} }
setMessage(message) { setMessage(message) {
@ -344,9 +363,35 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
this.initHeaderViewMode(); this.initHeaderViewMode();
this.initFooterViewMode(); this.initFooterViewMode();
var bodyMessageView = this.viewControllers.body.getView(1); const bodyMessageView = this.viewControllers.body.getView(1);
let msg = this.message.message;
if(bodyMessageView && _.has(this, 'message.message')) { if(bodyMessageView && _.has(this, 'message.message')) {
bodyMessageView.setText(cleanControlCodes(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
//
if(isAnsi(msg)) {
//
// Find tearline - we want to color it differently.
//
const tearLinePos = this.message.getTearLinePosition(msg);
if(tearLinePos > -1) {
msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text'));
}
bodyMessageView.setAnsi(
msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF
{
prepped : false,
forceLineTerm : true,
}
);
} else {
bodyMessageView.setText(cleanControlCodes(msg));
}
} }
} }
} }
@ -360,9 +405,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
[ [
function buildIfNecessary(callback) { function buildIfNecessary(callback) {
if(self.isEditMode()) { if(self.isEditMode()) {
self.buildMessage(); // creates initial self.message return self.buildMessage(callback); // creates initial self.message
} }
callback(null);
return callback(null);
}, },
function populateLocalUserInfo(callback) { function populateLocalUserInfo(callback) {
if(self.isLocalEmail()) { if(self.isLocalEmail()) {
@ -659,16 +705,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
break; break;
case 'edit' : case 'edit' :
const fromView = self.viewControllers.header.getView(1); {
const area = getMessageAreaByTag(self.messageAreaTag); const fromView = self.viewControllers.header.getView(1);
if(area && area.realNames) { const area = getMessageAreaByTag(self.messageAreaTag);
fromView.setText(self.client.user.properties.real_name || self.client.user.username); if(area && area.realNames) {
} else { fromView.setText(self.client.user.properties.real_name || self.client.user.username);
fromView.setText(self.client.user.username); } else {
} fromView.setText(self.client.user.username);
}
if(self.replyToMessage) { if(self.replyToMessage) {
self.initHeaderReplyEditMode(); self.initHeaderReplyEditMode();
}
} }
break; break;
} }
@ -848,9 +896,31 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
} }
}, },
function loadQuoteLines(callback) { function loadQuoteLines(callback) {
var quoteView = self.viewControllers.quoteBuilder.getView(3); const quoteView = self.viewControllers.quoteBuilder.getView(3);
quoteView.setItems(self.replyToMessage.getQuoteLines(quoteView.dimens.width)); const bodyView = self.viewControllers.body.getView(1);
callback(null);
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,
},
(err, quoteLines, focusQuoteLines, replyIsAnsi) => {
if(err) {
return callback(err);
}
self.replyIsAnsi = replyIsAnsi;
quoteView.setItems(quoteLines);
quoteView.setFocusItems(focusQuoteLines);
return callback(null);
}
);
}, },
function setViewFocus(callback) { function setViewFocus(callback) {
self.viewControllers.quoteBuilder.getView(1).setFocus(false); self.viewControllers.quoteBuilder.getView(1).setFocus(false);
@ -922,14 +992,17 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
quoteBuilderFinalize() { quoteBuilderFinalize() {
// :TODO: fix magic #'s // :TODO: fix magic #'s
var quoteMsgView = this.viewControllers.quoteBuilder.getView(1); const quoteMsgView = this.viewControllers.quoteBuilder.getView(1);
var msgView = this.viewControllers.body.getView(1); const msgView = this.viewControllers.body.getView(1);
var quoteLines = quoteMsgView.getData(); let quoteLines = quoteMsgView.getData();
if(quoteLines.trim().length > 0) { if(quoteLines.trim().length > 0) {
msgView.addText(quoteMsgView.getData() + '\n'); if(this.replyIsAnsi) {
const bodyMessageView = this.viewControllers.body.getView(1);
quoteLines += `${ansi.normal()}${bodyMessageView.getSGRFor('text')}`;
}
msgView.addText(`${quoteLines}\n`);
} }
quoteMsgView.setText(''); quoteMsgView.setText('');

View File

@ -7,6 +7,7 @@ const sauce = require('./sauce.js');
const Address = require('./ftn_address.js'); const Address = require('./ftn_address.js');
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const ansiPrep = require('./ansi_prep.js');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
@ -480,8 +481,8 @@ function Packet(options) {
Log.debug( { encoding : encoding, error : e.toString() }, 'Error decoding. Falling back to ASCII'); Log.debug( { encoding : encoding, error : e.toString() }, 'Error decoding. Falling back to ASCII');
decoded = iconv.decode(messageBodyBuffer, 'ascii'); decoded = iconv.decode(messageBodyBuffer, 'ascii');
} }
//const messageLines = iconv.decode(messageBodyBuffer, encoding).replace(/\xec/g, '').split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
const messageLines = decoded.replace(/\xec/g, '').split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, ''));
let endOfMessage = false; let endOfMessage = false;
messageLines.forEach(line => { messageLines.forEach(line => {
@ -636,108 +637,137 @@ function Packet(options) {
}); });
}; };
this.getMessageEntryBuffer = function(message, options) { this.getMessageEntryBuffer = function(message, options, cb) {
let basicHeader = new Buffer(34);
basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); function getAppendMeta(k, m) {
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); let append = '';
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_network, 8);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12);
const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0');
dateTimeBuffer.copy(basicHeader, 14);
// toUserName & fromUserName: up to 36 bytes in length, NULL term'd
// :TODO: DRY...
let toUserNameBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36);
toUserNameBuf[toUserNameBuf.length - 1] = '\0'; // ensure it's null term'd
let fromUserNameBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36);
fromUserNameBuf[fromUserNameBuf.length - 1] = '\0'; // ensure it's null term'd
// subject: up to 72 bytes in length, NULL term'd
let subjectBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72);
subjectBuf[subjectBuf.length - 1] = '\0'; // ensure it's null term'd
//
// message: unbound length, NULL term'd
//
// We need to build in various special lines - kludges, area,
// seen-by, etc.
//
// :TODO: Put this in it's own method
let msgBody = '';
function appendMeta(k, m) {
if(m) { if(m) {
let a = m; let a = m;
if(!_.isArray(a)) { if(!_.isArray(a)) {
a = [ a ]; a = [ a ];
} }
a.forEach(v => { a.forEach(v => {
msgBody += `${k}: ${v}\r`; append += `${k}: ${v}\r`;
}); });
} }
return append;
} }
// async.waterfall(
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 [
// AREA:CONFERENCE function prepareHeaderAndKludges(callback) {
// Should be first line in a message const basicHeader = new Buffer(34);
//
if(message.meta.FtnProperty.ftn_area) {
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01)
}
Object.keys(message.meta.FtnKludge).forEach(k => { basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0);
// we want PATH to be last basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2);
if('PATH' !== k) { basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4);
appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_network, 8);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10);
basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12);
const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0');
dateTimeBuffer.copy(basicHeader, 14);
//
// 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 } );
//
// message: unbound length, NULL term'd
//
// 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
//
if(message.meta.FtnProperty.ftn_area) {
msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01)
}
Object.keys(message.meta.FtnKludge).forEach(k => {
// we want PATH to be last
if('PATH' !== k) {
msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]);
}
});
return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody);
},
function prepareAnsiMessageBody(basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, callback) {
if(!strUtil.isAnsi(message.message)) {
return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, message.message);
}
ansiPrep(
message.message,
{
cols : 80,
rows : 'auto',
forceLineTerm : true,
exportMode : true,
},
(err, preppedMsg) => {
return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, preppedMsg || message.message);
}
);
},
function addMessageBody(basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, preppedMsg, callback) {
msgBody += preppedMsg + '\r';
//
// 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
//
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
//
msgBody += getAppendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01)
msgBody += getAppendMeta('\x01PATH', message.meta.FtnKludge['PATH']);
let msgBodyEncoded;
try {
msgBodyEncoded = iconv.encode(msgBody + '\0', options.encoding);
} catch(e) {
msgBodyEncoded = iconv.encode(msgBody + '\0', 'ascii');
}
return callback(
null,
Buffer.concat( [
basicHeader,
toUserNameBuf,
fromUserNameBuf,
subjectBuf,
msgBodyEncoded
])
);
}
],
(err, msgEntryBuffer) => {
return cb(err, msgEntryBuffer);
} }
}); );
msgBody += message.message + '\r';
//
// 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
//
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
//
appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01)
appendMeta('\x01PATH', message.meta.FtnKludge['PATH']);
let msgBodyEncoded;
try {
msgBodyEncoded = iconv.encode(msgBody + '\0', options.encoding);
} catch(e) {
msgBodyEncoded = iconv.encode(msgBody + '\0', 'ascii');
}
return Buffer.concat( [
basicHeader,
toUserNameBuf,
fromUserNameBuf,
subjectBuf,
msgBodyEncoded
]);
}; };
this.writeMessage = function(message, ws, options) { this.writeMessage = function(message, ws, options) {

View File

@ -1,17 +1,18 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
let Config = require('./config.js').config; let Config = require('./config.js').config;
let Address = require('./ftn_address.js'); let Address = require('./ftn_address.js');
let FNV1a = require('./fnv1a.js'); let FNV1a = require('./fnv1a.js');
const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion;
let _ = require('lodash'); let _ = require('lodash');
let iconv = require('iconv-lite'); let iconv = require('iconv-lite');
let moment = require('moment'); let moment = require('moment');
//let uuid = require('node-uuid'); //let uuid = require('node-uuid');
let os = require('os'); let os = require('os');
let packageJson = require('../package.json'); let packageJson = require('../package.json');
// :TODO: Remove "Ftn" from most of these -- it's implied in the module // :TODO: Remove "Ftn" from most of these -- it's implied in the module
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
@ -146,11 +147,7 @@ function getMessageIdentifier(message, address) {
// in which (<os>; <arch>; <nodeVer>) is used instead // in which (<os>; <arch>; <nodeVer>) is used instead
// //
function getProductIdentifier() { function getProductIdentifier() {
const version = packageJson.version const version = getCleanEnigmaVersion();
.replace(/\-/g, '.')
.replace(/alpha/,'a')
.replace(/beta/,'b');
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
@ -166,10 +163,23 @@ function getUTCTimeZoneOffset() {
return moment().format('ZZ').replace(/\+/, ''); return moment().format('ZZ').replace(/\+/, '');
} }
// Get a FSC-0032 style quote prefixes //
// Get a FSC-0032 style quote prefix
// http://ftsc.org/docs/fsc-0032.001
//
function getQuotePrefix(name) { function getQuotePrefix(name) {
// :TODO: Add support for real names (e.g. with spaces) -> initials let initials;
return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> ';
const parts = name.split(' ');
if(parts.length > 1) {
// First & Last initials - (Bryan Ashby -> BA)
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase();
} else {
// Just use the first two - (NuSkooler -> Nu)
initials = _.capitalize(name.slice(0, 2));
}
return ` ${initials}> `;
} }
// //

View File

@ -62,7 +62,7 @@ module.exports = class Log {
// Use a regexp -- we don't know how nested fields we want to seek and destroy may be // Use a regexp -- we don't know how nested fields we want to seek and destroy may be
// //
return JSON.parse( return JSON.parse(
JSON.stringify(obj).replace(/"(password)"\s?:\s?"([^"]+)"/, (match, valueName) => { JSON.stringify(obj).replace(/"(password|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
return `"${valueName}":"********"`; return `"${valueName}":"********"`;
}) })
); );

View File

@ -325,11 +325,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
formId : formId, formId : formId,
}; };
return vc.loadFromMenuConfig(loadOpts, cb); return vc.loadFromMenuConfig(loadOpts, err => {
return cb(err, vc);
});
} }
this.viewControllers[name].setFocus(true); this.viewControllers[name].setFocus(true);
return cb(null);
return cb(null, this.viewControllers[name]);
} }
prepViewControllerWithArt(name, formId, options, cb) { prepViewControllerWithArt(name, formId, options, cb) {

View File

@ -113,6 +113,14 @@ MenuView.prototype.focusPrevious = function() {
this.emit('index update', this.focusedItemIndex); this.emit('index update', this.focusedItemIndex);
}; };
MenuView.prototype.focusNextPageItem = function() {
this.emit('index update', this.focusedItemIndex);
};
MenuView.prototype.focusPreviousPageItem = function() {
this.emit('index update', this.focusedItemIndex);
};
MenuView.prototype.setFocusItemIndex = function(index) { MenuView.prototype.setFocusItemIndex = function(index) {
this.focusedItemIndex = index; this.focusedItemIndex = index;
}; };

View File

@ -6,6 +6,16 @@ const wordWrapText = require('./word_wrap.js').wordWrapText;
const ftnUtil = require('./ftn_util.js'); const ftnUtil = require('./ftn_util.js');
const createNamedUUID = require('./uuid_util.js').createNamedUUID; const createNamedUUID = require('./uuid_util.js').createNamedUUID;
const getISOTimestampString = require('./database.js').getISOTimestampString; const getISOTimestampString = require('./database.js').getISOTimestampString;
const Errors = require('./enig_error.js').Errors;
const ANSI = require('./ansi_term.js');
const {
isAnsi, isFormattedLine,
splitTextAtTerms,
renderSubstr
} = require('./string_util.js');
const ansiPrep = require('./ansi_prep.js');
// deps // deps
const uuidParse = require('uuid-parse'); const uuidParse = require('uuid-parse');
@ -82,6 +92,7 @@ Message.SystemMetaNames = {
LocalToUserID : 'local_to_user_id', LocalToUserID : 'local_to_user_id',
LocalFromUserID : 'local_from_user_id', LocalFromUserID : 'local_from_user_id',
StateFlags0 : 'state_flags0', // See Message.StateFlags0 StateFlags0 : 'state_flags0', // See Message.StateFlags0
ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc.
}; };
Message.StateFlags0 = { Message.StateFlags0 = {
@ -429,47 +440,192 @@ Message.prototype.getFTNQuotePrefix = function(source) {
return ftnUtil.getQuotePrefix(this[source]); return ftnUtil.getQuotePrefix(this[source]);
}; };
Message.prototype.getQuoteLines = function(width, options) { Message.prototype.getTearLinePosition = function(input) {
// :TODO: options.maxBlankLines = 1 const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m);
return m ? m.index : -1;
options = options || {}; };
// Message.prototype.getQuoteLines = function(options, cb) {
// Include FSC-0032 style quote prefixes? if(!options.termWidth || !options.termHeight || !options.cols) {
// return cb(Errors.MissingParam());
// See http://ftsc.org/docs/fsc-0032.001 }
//
if(!_.isBoolean(options.includePrefix)) { options.startCol = options.startCol || 1;
options.includePrefix = true; 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 } );
var quoteLines = []; options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting
var origLines = this.message /*
.trim() Some long text that needs to be wrapped and quoted should look right after
.replace(/\b/g, '') doing so, don't ya think? yeah I think so
.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
Nu> Some long text that needs to be wrapped and quoted should look right
var quotePrefix = ''; // we need this init even if blank Nu> after doing so, don't ya think? yeah I think so
if(options.includePrefix) {
quotePrefix = this.getFTNQuotePrefix(options.prefixSource || 'fromUserName'); 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
var wrapOpts = { */
width : width - quotePrefix.length, const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : '';
tabHandling : 'expand',
tabWidth : 4, function getWrapped(text, extraPrefix) {
}; extraPrefix = extraPrefix ? ` ${extraPrefix}` : '';
function addPrefix(l) { const wrapOpts = {
return quotePrefix + l; width : options.cols - (quotePrefix.length + extraPrefix.length),
} tabHandling : 'expand',
tabWidth : 4,
var wrapped; };
for(var i = 0; i < origLines.length; ++i) {
wrapped = wordWrapText(origLines[i], wrapOpts).wrapped; return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => {
Array.prototype.push.apply(quoteLines, _.map(wrapped, addPrefix)); return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`;
} });
}
return quoteLines;
function getFormattedLine(line) {
// for pre-formatted text, we just append a line truncated to fit
let newLen;
const total = line.length + quotePrefix.length;
if(total > options.cols) {
newLen = options.cols - total;
} else {
newLen = total;
}
return `${quotePrefix}${line.slice(0, newLen)}`;
}
if(options.isAnsi) {
ansiPrep(
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,
},
(err, prepped) => {
prepped = prepped || this.message;
let lastSgr = '';
const split = splitTextAtTerms(prepped);
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!
//
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
});
quoteLines[quoteLines.length - 1] += options.ansiResetSgr;
return cb(null, quoteLines, focusQuoteLines, true);
}
);
} else {
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
let tearLinePos = this.getTearLinePosition(input);
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
//
// Also:
// - Detect pre-formatted lines & try to keep them as-is
//
let state;
let buf = '';
let quoteMatch;
if(quoted.length > 0) {
//
// 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.
//
quoted.push(quotePrefix);
}
paragraph.split(/\r?\n/).forEach(line => {
if(0 === line.trim().length) {
// see blank line notes above
return quoted.push(quotePrefix);
}
quoteMatch = line.match(QUOTE_RE);
switch(state) {
case 'line' :
if(quoteMatch) {
if(isFormattedLine(line)) {
quoted.push(getFormattedLine(line.replace(/\s/, '')));
} else {
quoted.push(...getWrapped(buf, quoteMatch[1]));
state = 'quote_line';
buf = line;
}
} else {
buf += ` ${line}`;
}
break;
case 'quote_line' :
if(quoteMatch) {
const rem = line.slice(quoteMatch[0].length);
if(!buf.startsWith(quoteMatch[0])) {
quoted.push(...getWrapped(buf, quoteMatch[1]));
buf = rem;
} else {
buf += ` ${rem}`;
}
} else {
quoted.push(...getWrapped(buf));
buf = line;
state = 'line';
}
break;
default :
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
}
break;
}
});
quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null));
});
input.slice(tearLinePos).split(/\r?\n/).forEach(l => {
quoted.push(...getWrapped(l));
});
return cb(null, quoted, null, false);
}
}; };

View File

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

View File

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

View File

@ -15,6 +15,7 @@ const async = require('async');
exports.loadModuleEx = loadModuleEx; exports.loadModuleEx = loadModuleEx;
exports.loadModule = loadModule; exports.loadModule = loadModule;
exports.loadModulesForCategory = loadModulesForCategory; exports.loadModulesForCategory = loadModulesForCategory;
exports.getModulePaths = getModulePaths;
function loadModuleEx(options, cb) { function loadModuleEx(options, cb) {
assert(_.isObject(options)); assert(_.isObject(options));
@ -97,3 +98,12 @@ function loadModulesForCategory(category, iterator, complete) {
}); });
}); });
} }
function getModulePaths() {
return [
Config.paths.mods,
Config.paths.loginServers,
Config.paths.contentServers,
Config.paths.scannerTossers,
];
}

View File

@ -6,6 +6,7 @@ const strUtil = require('./string_util.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const colorCodes = require('./color_codes.js'); const colorCodes = require('./color_codes.js');
const wordWrapText = require('./word_wrap.js').wordWrapText; const wordWrapText = require('./word_wrap.js').wordWrapText;
const ansiPrep = require('./ansi_prep.js');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
@ -62,7 +63,7 @@ const _ = require('lodash');
// * // *
var SPECIAL_KEY_MAP_DEFAULT = { const SPECIAL_KEY_MAP_DEFAULT = {
'line feed' : [ 'return' ], 'line feed' : [ 'return' ],
exit : [ 'esc' ], exit : [ 'esc' ],
backspace : [ 'backspace' ], backspace : [ 'backspace' ],
@ -165,43 +166,50 @@ function MultiLineEditTextView(options) {
return self.textLines.length; return self.textLines.length;
}; };
this.toggleTextCursor = function(action) {
self.client.term.rawWrite(`${self.getSGRFor('text')}${'hide' === action ? ansi.hideCursor() : ansi.showCursor()}`);
};
this.redrawRows = function(startRow, endRow) { this.redrawRows = function(startRow, endRow) {
self.client.term.rawWrite(self.getSGRFor('text') + ansi.hideCursor()); self.toggleTextCursor('hide');
var startIndex = self.getTextLinesIndex(startRow); const startIndex = self.getTextLinesIndex(startRow);
var endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length); const endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length);
var absPos = self.getAbsolutePosition(startRow, 0); const absPos = self.getAbsolutePosition(startRow, 0);
for(var i = startIndex; i < endIndex; ++i) { for(let i = startIndex; i < endIndex; ++i) {
//${self.getSGRFor('text')}
self.client.term.write( self.client.term.write(
ansi.goto(absPos.row++, absPos.col) + `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`,
self.getRenderText(i), false); false // convertLineFeeds
);
} }
self.client.term.rawWrite(ansi.showCursor()); self.toggleTextCursor('show');
return absPos.row - self.position.row; // row we ended on return absPos.row - self.position.row; // row we ended on
}; };
this.eraseRows = function(startRow, endRow) { this.eraseRows = function(startRow, endRow) {
self.client.term.rawWrite(self.getSGRFor('text') + ansi.hideCursor()); self.toggleTextCursor('hide');
var absPos = self.getAbsolutePosition(startRow, 0); const absPos = self.getAbsolutePosition(startRow, 0);
var absPosEnd = self.getAbsolutePosition(endRow, 0); const absPosEnd = self.getAbsolutePosition(endRow, 0);
var eraseFiller = new Array(self.dimens.width).join(' '); const eraseFiller = ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' ');
while(absPos.row < absPosEnd.row) { while(absPos.row < absPosEnd.row) {
self.client.term.write( self.client.term.write(
ansi.goto(absPos.row++, absPos.col) + `${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`,
eraseFiller, false); false // convertLineFeeds
);
} }
self.client.term.rawWrite(ansi.showCursor()); self.toggleTextCursor('show');
}; };
this.redrawVisibleArea = function() { this.redrawVisibleArea = function() {
assert(self.topVisibleIndex <= self.textLines.length); assert(self.topVisibleIndex <= self.textLines.length);
var lastRow = self.redrawRows(0, self.dimens.height); const lastRow = self.redrawRows(0, self.dimens.height);
self.eraseRows(lastRow, self.dimens.height); self.eraseRows(lastRow, self.dimens.height);
/* /*
@ -255,11 +263,14 @@ function MultiLineEditTextView(options) {
}; };
this.getRenderText = function(index) { this.getRenderText = function(index) {
var text = self.getVisibleText(index); let text = self.getVisibleText(index);
var remain = self.dimens.width - text.length; const remain = self.dimens.width - text.length;
if(remain > 0) { if(remain > 0) {
text += new Array(remain + 1).join(' '); text += ' '.repeat(remain + 1);
// text += new Array(remain + 1).join(' ');
} }
return text; return text;
}; };
@ -273,14 +284,15 @@ function MultiLineEditTextView(options) {
return lines; return lines;
}; };
this.getOutputText = function(startIndex, endIndex, eolMarker) { this.getOutputText = function(startIndex, endIndex, eolMarker, options) {
let lines = self.getTextLines(startIndex, endIndex); const lines = self.getTextLines(startIndex, endIndex);
let text = ''; let text = '';
var re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g');
lines.forEach(line => { lines.forEach(line => {
text += line.text.replace(re, '\t'); text += line.text.replace(re, '\t');
if(eolMarker && line.eol) {
if(options.forceLineTerms || (eolMarker && line.eol)) {
text += eolMarker; text += eolMarker;
} }
}); });
@ -491,25 +503,90 @@ function MultiLineEditTextView(options) {
return new Array(self.getRemainingTabWidth(col)).join(expandChar); return new Array(self.getRemainingTabWidth(col)).join(expandChar);
}; };
this.getStringLength = function(s) {
return self.isPreviewMode() ? colorCodes.enigmaStrLen(s) : s.length;
};
this.wordWrapSingleLine = function(s, tabHandling, width) { this.wordWrapSingleLine = function(s, tabHandling, width) {
if(!_.isNumber(width)) { if(!_.isNumber(width)) {
width = self.dimens.width; width = self.dimens.width;
} }
return wordWrapText( return wordWrapText(
s, { s,
{
width : width, width : width,
tabHandling : tabHandling || 'expand', tabHandling : tabHandling || 'expand',
tabWidth : self.tabWidth, tabWidth : self.tabWidth,
tabChar : '\t', 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
self.textLines = lines.slice(0, -1).map(l => {
return { text : l };
}).concat( { text : lines[lines.length - 1], eol : termWithEol } );
} else {
// insert somewhere in textLines...
if(index > self.textLines.length) {
// fill with empty
self.textLines.splice(
self.textLines.length,
0,
...Array.from( { length : index - self.textLines.length } ).map( () => { return { text : '' }; } )
);
}
const newLines = lines.slice(0, -1).map(l => {
return { text : l };
}).concat( { text : lines[lines.length - 1], eol : termWithEol } );
self.textLines.splice(
index,
0,
...newLines
);
}
};
this.setAnsiWithOptions = function(ansi, options, cb) {
function setLines(text) {
text = strUtil.splitTextAtTerms(text);
let index = 0;
text.forEach(line => {
self.setTextLines( [ line ], index, true); // true=termWithEol
index += 1;
});
self.cursorStartOfDocument();
if(cb) {
return cb(null);
}
}
if(options.prepped) {
return setLines(ansi);
}
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,
},
(err, preppedAnsi) => {
return setLines(err ? ansi : preppedAnsi);
}
);
}; };
// :TODO: rename to insertRawText()
this.insertRawText = function(text, index, col) { this.insertRawText = function(text, index, col) {
// //
// Perform the following on |text|: // Perform the following on |text|:
@ -542,23 +619,19 @@ function MultiLineEditTextView(options) {
index = self.textLines.length; index = self.textLines.length;
} }
text = text text = strUtil.splitTextAtTerms(text);
.replace(/\b/g, '')
.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
let wrapped; let wrapped;
text.forEach(line => {
for(let i = 0; i < text.length; ++i) {
wrapped = self.wordWrapSingleLine( wrapped = self.wordWrapSingleLine(
text[i], // input line, // line to wrap
'expand', // tabHandling 'expand', // tabHandling
self.dimens.width).wrapped; self.dimens.width
).wrapped;
for(let j = 0; j < wrapped.length - 1; ++j) { self.setTextLines(wrapped, index, true); // true=termWithEol
self.textLines.splice(index++, 0, { text : wrapped[j] } ); index += wrapped.length;
} });
self.textLines.splice(index++, 0, { text : wrapped[wrapped.length - 1], eol : true } );
}
}; };
this.getAbsolutePosition = function(row, col) { this.getAbsolutePosition = function(row, col) {
@ -996,8 +1069,6 @@ MultiLineEditTextView.prototype.setFocus = function(focused) {
}; };
MultiLineEditTextView.prototype.setText = function(text) { MultiLineEditTextView.prototype.setText = function(text) {
//text = require('graceful-fs').readFileSync('/home/nuskooler/Downloads/test_text.txt', { encoding : 'utf-8'});
this.textLines = [ ]; this.textLines = [ ];
this.addText(text); this.addText(text);
/*this.insertRawText(text); /*this.insertRawText(text);
@ -1009,6 +1080,11 @@ MultiLineEditTextView.prototype.setText = function(text) {
}*/ }*/
}; };
MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : false }, cb) {
this.textLines = [ ];
return this.setAnsiWithOptions(ansi, options, cb);
};
MultiLineEditTextView.prototype.addText = function(text) { MultiLineEditTextView.prototype.addText = function(text) {
this.insertRawText(text); this.insertRawText(text);
@ -1019,8 +1095,8 @@ MultiLineEditTextView.prototype.addText = function(text) {
} }
}; };
MultiLineEditTextView.prototype.getData = function() { MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms : false } ) {
return this.getOutputText(0, this.textLines.length, '\r\n'); return this.getOutputText(0, this.textLines.length, '\r\n', options);
}; };
MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
@ -1032,7 +1108,9 @@ MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) {
} }
break; break;
case 'autoScroll' : this.autoScroll = value; break; case 'autoScroll' :
this.autoScroll = value;
break;
case 'tabSwitchesView' : case 'tabSwitchesView' :
this.tabSwitchesView = value; this.tabSwitchesView = value;

View File

@ -6,6 +6,9 @@ const msgArea = require('./message_area.js');
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const FileEntry = require('./file_entry.js');
const FileBaseFilters = require('./file_base_filter.js');
const Errors = require('./enig_error.js').Errors;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -30,13 +33,21 @@ const MciCodeIds = {
ScanStatusList : 2, // VM2 (appends) ScanStatusList : 2, // VM2 (appends)
}; };
const Steps = {
MessageConfs : 'messageConferences',
FileBase : 'fileBase',
Finished : 'finished',
};
exports.getModule = class NewScanModule extends MenuModule { exports.getModule = class NewScanModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.newScanFullExit = _.has(options, 'lastMenuResult.fullExit') ? options.lastMenuResult.fullExit : false;
this.currentStep = 'messageConferences'; this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false);
this.currentStep = Steps.MessageConfs;
this.currentScanAux = {}; this.currentScanAux = {};
// :TODO: Make this conf/area specific: // :TODO: Make this conf/area specific:
@ -49,13 +60,6 @@ exports.getModule = class NewScanModule extends MenuModule {
updateScanStatus(statusText) { updateScanStatus(statusText) {
this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText); this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText);
/*
view = vc.getView(MciCodeIds.ScanStatusList);
// :TODO: MenuView needs appendItem()
if(view) {
}
*/
} }
newScanMessageConference(cb) { newScanMessageConference(cb) {
@ -87,32 +91,19 @@ exports.getModule = class NewScanModule extends MenuModule {
this.currentScanAux.area = this.currentScanAux.area || 0; this.currentScanAux.area = this.currentScanAux.area || 0;
} }
const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; const currentConf = this.sortedMessageConfs[this.currentScanAux.conf];
const self = this;
async.series( this.newScanMessageArea(currentConf, () => {
[ if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) {
function scanArea(callback) { this.currentScanAux.conf += 1;
//self.currentScanAux.area = self.currentScanAux.area || 0; this.currentScanAux.area = 0;
self.newScanMessageArea(currentConf, () => { return this.newScanMessageConference(cb); // recursive to next conf
if(self.sortedMessageConfs.length > self.currentScanAux.conf + 1) {
self.currentScanAux.conf += 1;
self.currentScanAux.area = 0;
self.newScanMessageConference(cb); // recursive to next conf
//callback(null);
} else {
self.updateScanStatus(self.scanCompleteMsg);
callback(new Error('No more conferences'));
}
});
}
],
err => {
return cb(err);
} }
);
this.updateScanStatus(this.scanCompleteMsg);
return cb(Errors.DoesNotExist('No more conferences'));
});
} }
newScanMessageArea(conf, cb) { newScanMessageArea(conf, cb) {
@ -134,7 +125,7 @@ exports.getModule = class NewScanModule extends MenuModule {
return callback(null); return callback(null);
} else { } else {
self.updateScanStatus(self.scanCompleteMsg); self.updateScanStatus(self.scanCompleteMsg);
return callback(new Error('No more areas')); // this will stop our scan return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan
} }
}, },
function updateStatusScanStarted(callback) { function updateStatusScanStarted(callback) {
@ -173,6 +164,28 @@ exports.getModule = class NewScanModule extends MenuModule {
); );
} }
newScanFileBase(cb) {
// :TODO: add in steps
FileEntry.findFiles(
{ newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user) },
(err, fileIds) => {
if(err || 0 === fileIds.length) {
return cb(err ? err : Errors.DoesNotExist('No more new files'));
}
FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[0] );
const menuOpts = {
extraArgs : {
fileList : fileIds,
},
};
return this.gotoMenu(this.menuConfig.config.newScanFileBaseList || 'newScanFileBaseList', menuOpts);
}
);
}
getSaveState() { getSaveState() {
return { return {
currentStep : this.currentStep, currentStep : this.currentStep,
@ -185,6 +198,26 @@ exports.getModule = class NewScanModule extends MenuModule {
this.currentScanAux = savedState.currentScanAux; this.currentScanAux = savedState.currentScanAux;
} }
performScanCurrentStep(cb) {
switch(this.currentStep) {
case Steps.MessageConfs :
this.newScanMessageConference( () => {
this.currentStep = Steps.FileBase;
return this.performScanCurrentStep(cb);
});
break;
case Steps.FileBase :
this.newScanFileBase( () => {
this.currentStep = Steps.Finished;
return this.performScanCurrentStep(cb);
});
break;
default : return cb(null);
}
}
mciReady(mciData, cb) { mciReady(mciData, cb) {
if(this.newScanFullExit) { if(this.newScanFullExit) {
// user has canceled the entire scan @ message list view // user has canceled the entire scan @ message list view
@ -213,15 +246,7 @@ exports.getModule = class NewScanModule extends MenuModule {
vc.loadFromMenuConfig(loadOpts, callback); vc.loadFromMenuConfig(loadOpts, callback);
}, },
function performCurrentStepScan(callback) { function performCurrentStepScan(callback) {
switch(self.currentStep) { return self.performScanCurrentStep(callback);
case 'messageConferences' :
self.newScanMessageConference( () => {
callback(null); // finished
});
break;
default : return callback(null);
}
} }
], ],
err => { err => {

View File

@ -34,10 +34,25 @@ exports.handleFileBaseCommand = handleFileBaseCommand;
let fileArea; // required during init let fileArea; // required during init
function finalizeEntryAndPersist(fileEntry, cb) { function finalizeEntryAndPersist(fileEntry, descHandler, cb) {
async.series( async.series(
[ [
function getDescIfNeeded(callback) { function getDescFromHandlerIfNeeded(callback) {
if((fileEntry.desc && fileEntry.desc.length > 0 ) && !argv['desc-file']) {
return callback(null); // we have a desc already and are NOT overriding with desc file
}
if(!descHandler) {
return callback(null); // not much we can do!
}
const desc = descHandler.getDescription(fileEntry.fileName);
if(desc) {
fileEntry.desc = desc;
}
return callback(null);
},
function getDescFromUserIfNeeded(callback) {
if(false === argv.prompt || ( fileEntry.desc && fileEntry.desc.length > 0 ) ) { if(false === argv.prompt || ( fileEntry.desc && fileEntry.desc.length > 0 ) ) {
return callback(null); return callback(null);
} }
@ -70,6 +85,18 @@ function finalizeEntryAndPersist(fileEntry, cb) {
); );
} }
const SCAN_EXCLUDE_FILENAMES = [ 'DESCRIPT.ION', 'FILES.BBS' ];
function loadDescHandler(path, cb) {
const DescIon = require('../../core/descript_ion_file.js');
// :TODO: support FILES.BBS also
DescIon.createFromFile(path, (err, descHandler) => {
return cb(err, descHandler);
});
}
function scanFileAreaForChanges(areaInfo, options, cb) { function scanFileAreaForChanges(areaInfo, options, cb) {
const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => { const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => {
@ -79,9 +106,18 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
}); });
async.eachSeries(storageLocations, (storageLoc, nextLocation) => { async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
async.series( async.waterfall(
[ [
function scanPhysFiles(callback) { function initDescFile(callback) {
if(options.descFileHandler) {
return callback(null, options.descFileHandler); // we're going to use the global handler
}
loadDescHandler(paths.join(storageLoc.dir, 'DESCRIPT.ION'), (err, descHandler) => {
return callback(null, descHandler);
});
},
function scanPhysFiles(descHandler, callback) {
const physDir = storageLoc.dir; const physDir = storageLoc.dir;
fs.readdir(physDir, (err, files) => { fs.readdir(physDir, (err, files) => {
@ -92,6 +128,11 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
async.eachSeries(files, (fileName, nextFile) => { async.eachSeries(files, (fileName, nextFile) => {
const fullPath = paths.join(physDir, fileName); const fullPath = paths.join(physDir, fileName);
if(SCAN_EXCLUDE_FILENAMES.includes(fileName.toUpperCase())) {
console.info(`Excluding ${fullPath}`);
return nextFile(null);
}
fs.stat(fullPath, (err, stats) => { fs.stat(fullPath, (err, stats) => {
if(err) { if(err) {
// :TODO: Log me! // :TODO: Log me!
@ -117,8 +158,6 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
return nextFile(null); // try next anyway return nextFile(null); // try next anyway
} }
if(dupeEntries.length > 0) { if(dupeEntries.length > 0) {
// :TODO: Handle duplidates -- what to do here??? // :TODO: Handle duplidates -- what to do here???
console.info('Dupe'); console.info('Dupe');
@ -131,7 +170,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
}); });
} }
finalizeEntryAndPersist(fileEntry, err => { finalizeEntryAndPersist(fileEntry, descHandler, err => {
return nextFile(err); return nextFile(err);
}); });
} }
@ -304,6 +343,8 @@ function scanFileAreas() {
options.tags = tags.split(','); options.tags = tags.split(',');
} }
options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH
options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2));
async.series( async.series(
@ -311,6 +352,21 @@ function scanFileAreas() {
function init(callback) { function init(callback) {
return initConfigAndDatabases(callback); return initConfigAndDatabases(callback);
}, },
function initGlobalDescHandler(callback) {
//
// If options.descFile is a String, it represents a FILE|PATH. We'll init
// the description handler now. Else, we'll attempt to look for a description
// file in each storage location.
//
if(!_.isString(options.descFile)) {
return callback(null);
}
loadDescHandler(options.descFile, (err, descHandler) => {
options.descFileHandler = descHandler;
return callback(null);
});
},
function scanAreas(callback) { function scanAreas(callback) {
fileArea = require('../../core/file_base_area.js'); fileArea = require('../../core/file_base_area.js');

View File

@ -61,6 +61,10 @@ actions:
scan args: scan args:
--tags TAG1,TAG2,... specify tag(s) to assign to discovered entries --tags TAG1,TAG2,... specify tag(s) to assign to discovered entries
--desc-file [PATH] prefer file descriptions from DESCRIPT.ION file over
other sources such as FILE_ID.DIZ.
if PATH is specified, use DESCRIPT.ION at PATH instead
of looking in specific storage locations
info args: info args:
--show-desc display short description, if any --show-desc display short description, if any

View File

@ -37,7 +37,7 @@ function setNextRandomRumor(cb) {
}); });
} }
function getRatio(client, propA, propB) { function getUserRatio(client, propA, propB) {
const a = StatLog.getUserStatNum(client.user, propA); const a = StatLog.getUserStatNum(client.user, propA);
const b = StatLog.getUserStatNum(client.user, propB); const b = StatLog.getUserStatNum(client.user, propB);
const ratio = ~~((a / b) * 100); const ratio = ~~((a / b) * 100);
@ -48,6 +48,10 @@ function userStatAsString(client, statName, defaultValue) {
return (StatLog.getUserStat(client.user, statName) || defaultValue).toString(); return (StatLog.getUserStat(client.user, statName) || defaultValue).toString();
} }
function sysStatAsString(statName, defaultValue) {
return (StatLog.getSystemStat(statName) || defaultValue).toString();
}
const PREDEFINED_MCI_GENERATORS = { const PREDEFINED_MCI_GENERATORS = {
// //
// Board // Board
@ -84,7 +88,7 @@ const PREDEFINED_MCI_GENERATORS = {
UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); },
UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); },
ND : function connectedNode(client) { return client.node.toString(); }, ND : function connectedNode(client) { return client.node.toString(); },
IP : function clientIpAddress(client) { return client.remoteAddress; }, IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version
ST : function serverName(client) { return client.session.serverName; }, ST : function serverName(client) { return client.session.serverName; },
FN : function activeFileBaseFilterName(client) { FN : function activeFileBaseFilterName(client) {
const activeFilter = FileBaseFilters.getActiveFilter(client); const activeFilter = FileBaseFilters.getActiveFilter(client);
@ -101,15 +105,15 @@ const PREDEFINED_MCI_GENERATORS = {
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 getRatio(client, 'ul_total_count', 'dl_total_count'); return getUserRatio(client, 'ul_total_count', 'dl_total_count');
}, },
KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio
return getRatio(client, 'ul_total_bytes', 'dl_total_bytes'); return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes');
}, },
MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); },
PC : function userPostCallRatio(client) { return getRatio(client, 'post_count', 'login_count'); }, PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); },
MD : function currentMenuDescription(client) { MD : function currentMenuDescription(client) {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
@ -187,7 +191,16 @@ const PREDEFINED_MCI_GENERATORS = {
// //
// :TODO: DD - Today's # of downloads (iNiQUiTY) // :TODO: DD - Today's # of downloads (iNiQUiTY)
// //
// :TODO: System stat log for total ul/dl, total ul/dl bytes SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); },
SO : function systemByteDownload() {
const byteSize = StatLog.getSystemStatNum('dl_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); },
SP : function systemByteUpload() {
const byteSize = StatLog.getSystemStatNum('ul_total_bytes');
return formatByteSize(byteSize, true); // true=withAbbr
},
// :TODO: PT - Messages posted *today* (Obv/2) // :TODO: PT - Messages posted *today* (Obv/2)
// -> Include FTN/etc. // -> Include FTN/etc.

View File

@ -293,14 +293,14 @@ function FTNMessageScanTossModule() {
async.detectSeries(EXT_SUFFIXES, (suffix, callback) => { async.detectSeries(EXT_SUFFIXES, (suffix, callback) => {
const checkFileName = fileName + suffix; const checkFileName = fileName + suffix;
fs.stat(paths.join(basePath, checkFileName), err => { fs.stat(paths.join(basePath, checkFileName), err => {
callback((err && 'ENOENT' === err.code) ? true : false); callback(null, (err && 'ENOENT' === err.code) ? true : false);
}); });
}, finalSuffix => { }, (err, finalSuffix) => {
if(finalSuffix) { if(finalSuffix) {
cb(null, paths.join(basePath, fileName + finalSuffix)); return cb(null, paths.join(basePath, fileName + finalSuffix));
} else {
cb(new Error('Could not acquire a bundle filename!'));
} }
return cb(new Error('Could not acquire a bundle filename!'));
}); });
}; };
@ -390,11 +390,14 @@ function FTNMessageScanTossModule() {
message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier(); message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier();
// //
// Determine CHRS and actual internal encoding name // Determine CHRS and actual internal encoding name. If the message has an
// Try to preserve anything already here // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set.
// //
let encoding = options.nodeConfig.encoding || 'utf8'; let encoding = options.nodeConfig.encoding || Config.scannerTossers.ftn_bso.packetMsgEncoding || 'utf8';
if(message.meta.FtnKludge.CHRS) { const explicitEncoding = _.get(message.meta, 'System.explicit_encoding');
if(explicitEncoding) {
encoding = explicitEncoding;
} else if(message.meta.FtnKludge.CHRS) {
const encFromChars = ftnUtil.getEncodingFromCharacterSetIdentifier(message.meta.FtnKludge.CHRS); const encFromChars = ftnUtil.getEncodingFromCharacterSetIdentifier(message.meta.FtnKludge.CHRS);
if(encFromChars) { if(encFromChars) {
encoding = encFromChars; encoding = encFromChars;
@ -605,16 +608,22 @@ function FTNMessageScanTossModule() {
callback(null); callback(null);
}, },
function appendMessage(callback) { function appendMessage(callback) {
const msgBuf = packet.getMessageEntryBuffer(message, exportOpts); packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => {
currPacketSize += msgBuf.length; if(err) {
return callback(err);
}
if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { currPacketSize += msgBuf.length;
remainMessageBuf = msgBuf; // save for next packet
remainMessageId = message.messageId; if(currPacketSize >= self.moduleConfig.packetTargetByteSize) {
} else { remainMessageBuf = msgBuf; // save for next packet
ws.write(msgBuf); remainMessageId = message.messageId;
} } else {
callback(null); ws.write(msgBuf);
}
return callback(null);
});
}, },
function storeStateFlags0Meta(callback) { function storeStateFlags0Meta(callback) {
message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), err => { message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), err => {

View File

@ -116,7 +116,7 @@ exports.getModule = class WebServerModule extends ServerModule {
// additional options // additional options
Object.assign(options, Config.contentServers.web.https.options || {} ); Object.assign(options, Config.contentServers.web.https.options || {} );
this.httpsServer = https.createServer(options, this.routeRequest); this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) );
} }
} }

View File

@ -6,12 +6,12 @@ const baseClient = require('../../client.js');
const Log = require('../../logger.js').log; const Log = require('../../logger.js').log;
const LoginServerModule = require('../../login_server_module.js'); const LoginServerModule = require('../../login_server_module.js');
const Config = require('../../config.js').config; const Config = require('../../config.js').config;
const EnigAssert = require('../../enigma_assert.js');
// deps // deps
const net = require('net'); const net = require('net');
const buffers = require('buffers'); const buffers = require('buffers');
const binary = require('binary'); const binary = require('binary');
const assert = require('assert');
const util = require('util'); const util = require('util');
//var debug = require('debug')('telnet'); //var debug = require('debug')('telnet');
@ -184,6 +184,10 @@ const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) {
return names; return names;
}, {}); }, {});
function unknownOption(bufs, i, event) {
Log.warn( { bufs : bufs, i : i, event : event }, 'Unknown Telnet option');
}
const OPTION_IMPLS = {}; const OPTION_IMPLS = {};
// :TODO: fill in the rest... // :TODO: fill in the rest...
OPTION_IMPLS.NO_ARGS = OPTION_IMPLS.NO_ARGS =
@ -224,10 +228,10 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) {
.word8('ttype') .word8('ttype')
.word8('is') .word8('is')
.tap(function(vars) { .tap(function(vars) {
assert(vars.iac1 === COMMANDS.IAC); EnigAssert(vars.iac1 === COMMANDS.IAC);
assert(vars.sb === COMMANDS.SB); EnigAssert(vars.sb === COMMANDS.SB);
assert(vars.ttype === OPTIONS.TERMINAL_TYPE); EnigAssert(vars.ttype === OPTIONS.TERMINAL_TYPE);
assert(vars.is === SB_COMMANDS.IS); EnigAssert(vars.is === SB_COMMANDS.IS);
}); });
// eat up the rest // eat up the rest
@ -275,11 +279,11 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) {
.word8('iac2') .word8('iac2')
.word8('se') .word8('se')
.tap(function(vars) { .tap(function(vars) {
assert(vars.iac1 == COMMANDS.IAC); EnigAssert(vars.iac1 == COMMANDS.IAC);
assert(vars.sb == COMMANDS.SB); EnigAssert(vars.sb == COMMANDS.SB);
assert(vars.naws == OPTIONS.WINDOW_SIZE); EnigAssert(vars.naws == OPTIONS.WINDOW_SIZE);
assert(vars.iac2 == COMMANDS.IAC); EnigAssert(vars.iac2 == COMMANDS.IAC);
assert(vars.se == COMMANDS.SE); EnigAssert(vars.se == COMMANDS.SE);
event.cols = event.columns = event.width = vars.width; event.cols = event.columns = event.width = vars.width;
event.rows = event.height = vars.height; event.rows = event.height = vars.height;
@ -322,10 +326,10 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
.word8('newEnv') .word8('newEnv')
.word8('isOrInfo') // initial=IS, updates=INFO .word8('isOrInfo') // initial=IS, updates=INFO
.tap(function(vars) { .tap(function(vars) {
assert(vars.iac1 === COMMANDS.IAC); EnigAssert(vars.iac1 === COMMANDS.IAC);
assert(vars.sb === COMMANDS.SB); EnigAssert(vars.sb === COMMANDS.SB);
assert(vars.newEnv === OPTIONS.NEW_ENVIRONMENT || vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP); EnigAssert(vars.newEnv === OPTIONS.NEW_ENVIRONMENT || vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP);
assert(vars.isOrInfo === SB_COMMANDS.IS || vars.isOrInfo === SB_COMMANDS.INFO); EnigAssert(vars.isOrInfo === SB_COMMANDS.IS || vars.isOrInfo === SB_COMMANDS.INFO);
event.type = vars.isOrInfo; event.type = vars.isOrInfo;
@ -394,8 +398,8 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) {
const MORE_DATA_REQUIRED = 0xfeedface; const MORE_DATA_REQUIRED = 0xfeedface;
function parseBufs(bufs) { function parseBufs(bufs) {
assert(bufs.length >= 2); EnigAssert(bufs.length >= 2);
assert(bufs.get(0) === COMMANDS.IAC); EnigAssert(bufs.get(0) === COMMANDS.IAC);
return parseCommand(bufs, 1, {}); return parseCommand(bufs, 1, {});
} }
@ -421,7 +425,9 @@ function parseOption(bufs, i, event) {
const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same
event.optionCode = option; event.optionCode = option;
event.option = OPTION_NAMES[option]; event.option = OPTION_NAMES[option];
return OPTION_IMPLS[option](bufs, i + 1, event);
const handler = OPTION_IMPLS[option];
return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event);
} }
@ -433,6 +439,8 @@ function TelnetClient(input, output) {
let bufs = buffers(); let bufs = buffers();
this.bufs = bufs; this.bufs = bufs;
this.sentDont = {}; // DON'T's we've already sent
this.setInputOutput(input, output); this.setInputOutput(input, output);
this.negotiationsComplete = false; // are we in the 'negotiation' phase? this.negotiationsComplete = false; // are we in the 'negotiation' phase?
@ -453,6 +461,11 @@ function TelnetClient(input, output) {
}; };
this.dataHandler = function(b) { this.dataHandler = function(b) {
if(!Buffer.isBuffer(b)) {
EnigAssert(false, `Cannot push non-buffer ${typeof b}`);
return;
}
bufs.push(b); bufs.push(b);
let i; let i;
@ -466,7 +479,7 @@ function TelnetClient(input, output) {
break; break;
} }
assert(bufs.length > (i + 1)); EnigAssert(bufs.length > (i + 1));
if(i > 0) { if(i > 0) {
self.emit('data', bufs.splice(0, i).toBuffer()); self.emit('data', bufs.splice(0, i).toBuffer());
@ -476,7 +489,7 @@ function TelnetClient(input, output) {
if(MORE_DATA_REQUIRED === i) { if(MORE_DATA_REQUIRED === i) {
break; break;
} else { } else if(i) {
if(i.option) { if(i.option) {
self.emit(i.option, i); // "transmit binary", "echo", ... self.emit(i.option, i); // "transmit binary", "echo", ...
} }
@ -505,15 +518,26 @@ function TelnetClient(input, output) {
}); });
this.input.on('error', err => { this.input.on('error', err => {
self.log.debug( { err : err }, 'Socket error'); this.connectionDebug( { err : err }, 'Socket error' );
self.emit('end'); return self.emit('end');
}); });
this.connectionDebug = (info, msg) => { this.connectionTrace = (info, msg) => {
if(Config.loginServers.telnet.traceConnections) { if(Config.loginServers.telnet.traceConnections) {
self.log.trace(info, 'Telnet: ' + msg); const logger = self.log || Log;
return logger.trace(info, `Telnet: ${msg}`);
} }
}; };
this.connectionDebug = (info, msg) => {
const logger = self.log || Log;
return logger.debug(info, `Telnet: ${msg}`);
};
this.connectionWarn = (info, msg) => {
const logger = self.log || Log;
return logger.warn(info, `Telnet: ${msg}`);
};
} }
util.inherits(TelnetClient, baseClient.Client); util.inherits(TelnetClient, baseClient.Client);
@ -522,6 +546,11 @@ util.inherits(TelnetClient, baseClient.Client);
// Telnet Command/Option handling // Telnet Command/Option handling
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
TelnetClient.prototype.handleTelnetEvent = function(evt) { TelnetClient.prototype.handleTelnetEvent = function(evt) {
if(!evt.command) {
return this.connectionWarn( { evt : evt }, 'No command for event');
}
// handler name e.g. 'handleWontCommand' // handler name e.g. 'handleWontCommand'
const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`;
@ -547,15 +576,21 @@ TelnetClient.prototype.handleWillCommand = function(evt) {
this.requestNewEnvironment(); this.requestNewEnvironment();
} else { } else {
// :TODO: temporary: // :TODO: temporary:
this.connectionDebug(evt, 'WILL'); this.connectionTrace(evt, 'WILL');
} }
}; };
TelnetClient.prototype.handleWontCommand = function(evt) { TelnetClient.prototype.handleWontCommand = function(evt) {
if(this.sentDont[evt.option]) {
return this.connectionTrace(evt, 'WONT - DON\'T already sent');
}
this.sentDont[evt.option] = true;
if('new environment' === evt.option) { if('new environment' === evt.option) {
this.dont.new_environment(); this.dont.new_environment();
} else { } else {
this.connectionDebug(evt, 'WONT'); this.connectionTrace(evt, 'WONT');
} }
}; };
@ -574,12 +609,12 @@ TelnetClient.prototype.handleDoCommand = function(evt) {
this.wont.encrypt(); this.wont.encrypt();
} else { } else {
// :TODO: temporary: // :TODO: temporary:
this.connectionDebug(evt, 'DO'); this.connectionTrace(evt, 'DO');
} }
}; };
TelnetClient.prototype.handleDontCommand = function(evt) { TelnetClient.prototype.handleDontCommand = function(evt) {
this.connectionDebug(evt, 'DONT'); this.connectionTrace(evt, 'DONT');
}; };
TelnetClient.prototype.handleSbCommand = function(evt) { TelnetClient.prototype.handleSbCommand = function(evt) {
@ -613,24 +648,26 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
} else if('COLUMNS' === name && 0 === self.term.termWidth) { } else if('COLUMNS' === name && 0 === self.term.termWidth) {
self.term.termWidth = parseInt(evt.envVars[name]); self.term.termWidth = parseInt(evt.envVars[name]);
self.clearMciCache(); // term size changes = invalidate cache self.clearMciCache(); // term size changes = invalidate cache
self.log.debug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); self.connectionDebug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated');
} else if('ROWS' === name && 0 === self.term.termHeight) { } else if('ROWS' === name && 0 === self.term.termHeight) {
self.term.termHeight = parseInt(evt.envVars[name]); self.term.termHeight = parseInt(evt.envVars[name]);
self.clearMciCache(); // term size changes = invalidate cache self.clearMciCache(); // term size changes = invalidate cache
self.log.debug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated');
} else { } else {
if(name in self.term.env) { if(name in self.term.env) {
assert(
SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type,
'Unexpected type: ' + evt.type);
self.log.warn( EnigAssert(
SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type,
'Unexpected type: ' + evt.type
);
self.connectionWarn(
{ varName : name, value : evt.envVars[name], existingValue : self.term.env[name] }, { varName : name, value : evt.envVars[name], existingValue : self.term.env[name] },
'Environment variable already exists'); 'Environment variable already exists'
);
} else { } else {
self.term.env[name] = evt.envVars[name]; self.term.env[name] = evt.envVars[name];
self.log.debug( self.connectionDebug( { varName : name, value : evt.envVars[name] }, 'New environment variable' );
{ varName : name, value : evt.envVars[name] }, 'New environment variable');
} }
} }
}); });
@ -653,9 +690,9 @@ TelnetClient.prototype.handleSbCommand = function(evt) {
self.clearMciCache(); // term size changes = invalidate cache self.clearMciCache(); // term size changes = invalidate cache
self.log.debug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated');
} else { } else {
self.log(evt, 'SB'); self.connectionDebug(evt, 'SB');
} }
}; };
@ -666,7 +703,7 @@ const IGNORED_COMMANDS = [];
TelnetClient.prototype.handleMiscCommand = function(evt) { TelnetClient.prototype.handleMiscCommand = function(evt) {
assert(evt.command !== 'undefined' && evt.command.length > 0); EnigAssert(evt.command !== 'undefined' && evt.command.length > 0);
// //
// See: // See:

View File

@ -13,7 +13,7 @@ const WebSocketServer = require('ws').Server;
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const EventEmitter = require('events'); const Writable = require('stream');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'WebSocket', name : 'WebSocket',
@ -34,20 +34,31 @@ function WebSocketClient(ws, req, serverType) {
// This bridge makes accessible various calls that client sub classes // This bridge makes accessible various calls that client sub classes
// want to access on I/O socket // want to access on I/O socket
// //
this.socketBridge = new class SocketBridge extends EventEmitter { this.socketBridge = new class SocketBridge extends Writable {
constructor(ws) { constructor(ws) {
super(); super();
this.ws = ws; this.ws = ws;
} }
end() { end() {
return ws.terminate(); return ws.close();
} }
write(data, cb) { write(data, cb) {
cb = cb || ( () => { /* eat it up */} ); // handle data writes after close
return this.ws.send(data, { binary : true }, cb); return this.ws.send(data, { binary : true }, cb);
} }
// we need to fake some streaming work
unpipe() {
Log.trace('WebSocket SocketBridge unpipe()');
}
resume() {
Log.trace('WebSocket SocketBridge resume()');
}
get remoteAddress() { get remoteAddress() {
// Support X-Forwarded-For and X-Real-IP headers for proxied connections // Support X-Forwarded-For and X-Real-IP headers for proxied connections
return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress;

View File

@ -92,6 +92,10 @@ class StatLog {
getSystemStat(statName) { return this.systemStats[statName]; } getSystemStat(statName) { return this.systemStats[statName]; }
getSystemStatNum(statName) {
return parseInt(this.getSystemStat(statName)) || 0;
}
incrementSystemStat(statName, incrementBy, cb) { incrementSystemStat(statName, incrementBy, cb) {
incrementBy = incrementBy || 1; incrementBy = incrementBy || 1;

View File

@ -8,20 +8,26 @@ const ANSI = require('./ansi_term.js');
// deps // deps
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const _ = require('lodash');
exports.stylizeString = stylizeString; exports.stylizeString = stylizeString;
exports.pad = pad; exports.pad = pad;
exports.insert = insert;
exports.replaceAt = replaceAt; exports.replaceAt = replaceAt;
exports.isPrintable = isPrintable; exports.isPrintable = isPrintable;
exports.stripAllLineFeeds = stripAllLineFeeds; exports.stripAllLineFeeds = stripAllLineFeeds;
exports.debugEscapedString = debugEscapedString; exports.debugEscapedString = debugEscapedString;
exports.stringFromNullTermBuffer = stringFromNullTermBuffer; exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
exports.stringToNullTermBuffer = stringToNullTermBuffer;
exports.renderSubstr = renderSubstr; exports.renderSubstr = renderSubstr;
exports.renderStringLength = renderStringLength; exports.renderStringLength = renderStringLength;
exports.formatByteSizeAbbr = formatByteSizeAbbr; exports.formatByteSizeAbbr = formatByteSizeAbbr;
exports.formatByteSize = formatByteSize; exports.formatByteSize = formatByteSize;
exports.cleanControlCodes = cleanControlCodes; exports.cleanControlCodes = cleanControlCodes;
exports.createCleanAnsi = createCleanAnsi; exports.isAnsi = isAnsi;
exports.isAnsiLine = isAnsiLine;
exports.isFormattedLine = isFormattedLine;
exports.splitTextAtTerms = splitTextAtTerms;
// :TODO: create Unicode verison of this // :TODO: create Unicode verison of this
const VOWELS = [ 'a', 'e', 'i', 'o', 'u' ]; const VOWELS = [ 'a', 'e', 'i', 'o', 'u' ];
@ -168,11 +174,16 @@ function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) {
return stringSGR + s; return stringSGR + s;
} }
function insert(s, index, substr) {
return `${s.slice(0, index)}${substr}${s.slice(index)}`;
}
function replaceAt(s, n, t) { function replaceAt(s, n, t) {
return s.substring(0, n) + t + s.substring(n + 1); return s.substring(0, n) + t + s.substring(n + 1);
} }
const RE_NON_PRINTABLE = /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/; const RE_NON_PRINTABLE =
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/; // eslint-disable-line no-control-regex
function isPrintable(s) { function isPrintable(s) {
// //
@ -207,9 +218,16 @@ function stringFromNullTermBuffer(buf, encoding) {
return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8'); return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8');
} }
function stringToNullTermBuffer(s, options = { encoding : 'utf8', maxBufLen : -1 } ) {
let buf = iconv.encode( `${s}\0`, options.encoding ).slice(0, options.maxBufLen);
buf[buf.length - 1] = '\0'; // make abs sure we null term even if truncated
return buf;
}
const PIPE_REGEXP = /(\|[A-Z\d]{2})/g; const PIPE_REGEXP = /(\|[A-Z\d]{2})/g;
const ANSI_REGEXP = /[\u001b\u009b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/g; //const ANSI_REGEXP = /[\u001b\u009b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/g;
const ANSI_OR_PIPE_REGEXP = new RegExp(PIPE_REGEXP.source + '|' + ANSI_REGEXP.source, 'g'); //const ANSI_OR_PIPE_REGEXP = new RegExp(PIPE_REGEXP.source + '|' + ANSI_REGEXP.source, 'g');
const ANSI_OR_PIPE_REGEXP = new RegExp(PIPE_REGEXP.source + '|' + ANSI.getFullMatchRegExp().source, 'g');
// //
// Similar to substr() but works with ANSI/Pipe code strings // Similar to substr() but works with ANSI/Pipe code strings
@ -322,18 +340,13 @@ function formatByteSize(byteSize, withAbbr, decimals) {
// :TODO: See notes in word_wrap.js about need to consolidate the various ANSI related RegExp's // :TODO: See notes in word_wrap.js about need to consolidate the various ANSI related RegExp's
//const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g; //const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g;
const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([\?=;0-9]*?)([A-ORZcf-npsu=><])/g; const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([\?=;0-9]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex
const ANSI_OPCODES_ALLOWED_CLEAN = [ const ANSI_OPCODES_ALLOWED_CLEAN = [
'A', 'B', // up, down //'A', 'B', // up, down
'C', 'D', // right, left //'C', 'D', // right, left
'm', // color 'm', // color
]; ];
const AnsiSpecialOpCodes = {
positioning : [ 'A', 'B', 'C', 'D' ], // up, down, right, left
style : [ 'm' ] // color
};
function cleanControlCodes(input, options) { function cleanControlCodes(input, options) {
let m; let m;
let pos; let pos;
@ -373,147 +386,249 @@ function cleanControlCodes(input, options) {
return cleaned; return cleaned;
} }
function createCleanAnsi(input, options, cb) { function prepAnsi(input, options, cb) {
if(!input) { if(!input) {
return cb(''); return cb(null, '');
} }
options.width = options.width || 80; options.termWidth = options.termWidth || 80;
options.height = options.height || 25; 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;
const canvas = new Array(options.height); const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) );
for(let i = 0; i < options.height; ++i) { const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } );
canvas[i] = new Array(options.width);
for(let j = 0; j < options.width; ++j) {
canvas[i][j] = {};
}
}
const parserOpts = { const state = {
termHeight : options.height, row : 0,
termWidth : options.width, col : 0,
}; };
const parser = new ANSIEscapeParser(parserOpts); let lastRow = 0;
const canvasPos = { function ensureRow(row) {
col : 0, if(Array.isArray(canvas[row])) {
row : 0, return;
};
let sgr;
function ensureCell() {
// we've pre-allocated a matrix, but allow for > supplied dimens up front. They will be trimmed @ finalize
if(!canvas[canvasPos.row]) {
canvas[canvasPos.row] = new Array(options.width);
for(let j = 0; j < options.width; ++j) {
canvas[canvasPos.row][j] = {};
}
} }
canvas[canvasPos.row][canvasPos.col] = canvas[canvasPos.row][canvasPos.col] || {};
//canvas[canvasPos.row][0].width = Math.max(canvas[canvasPos.row][0].width || 0, canvasPos.col); canvas[row] = Array.from( { length : options.cols}, () => new Object() );
} }
parser.on('position update', (row, col) => {
state.row = row - 1;
state.col = col - 1;
lastRow = Math.max(state.row, lastRow);
});
parser.on('literal', literal => { parser.on('literal', literal => {
// //
// CR/LF are handled for 'position update'; we don't need the chars themselves // CR/LF are handled for 'position update'; we don't need the chars themselves
// //
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
for(let i = 0; i < literal.length; ++i) { for(let c of literal) {
const c = literal.charAt(i); if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) {
ensureRow(state.row);
ensureCell(); canvas[state.row][state.col].char = c;
canvas[canvasPos.row][canvasPos.col].char = c; if(state.sgr) {
canvas[state.row][state.col].sgr = state.sgr;
if(sgr) { state.sgr = null;
canvas[canvasPos.row][canvasPos.col].sgr = sgr; }
sgr = null;
} }
canvasPos.col += 1; state.col += 1;
} }
}); });
parser.on('control', (match, opCode) => { parser.on('control', (match, opCode) => {
if('m' !== opCode) { //
return; // don't care' // Movement is handled via 'position update', so we really only care about
// display opCodes
//
switch(opCode) {
case 'm' :
state.sgr = (state.sgr || '') + match;
break;
default :
break;
} }
sgr = match;
}); });
parser.on('position update', (row, col) => { function getLastPopulatedColumn(row) {
canvasPos.row = row - 1; let col = row.length;
canvasPos.col = Math.min(col - 1, options.width); while(--col > 0) {
}); if(row[col].char || row[col].sgr) {
break;
}
}
return col;
}
parser.on('complete', () => { parser.on('complete', () => {
for(let row = 0; row < options.height; ++row) { let output = '';
let col = 0; let lastSgr = '';
let line;
//while(col <= canvas[row][0].width) { canvas.slice(0, lastRow + 1).forEach(row => {
while(col < options.width) { const lastCol = getLastPopulatedColumn(row) + 1;
if(!canvas[row][col].char) {
canvas[row][col].char = ' '; let i;
if(!canvas[row][col].sgr) { line = '';
// :TODO: fix duplicate SGR's in a row here - we just need one per sequence for(i = 0; i < lastCol; ++i) {
canvas[row][col].sgr = ANSI.reset(); const col = row[i];
if(col.sgr) {
lastSgr = col.sgr;
}
line += `${col.sgr || ''}${col.char || ' '}`;
}
output += line;
if(i < row.length) {
output += `${ANSI.blackBG()}${row.slice(i).map( () => ' ').join('')}${lastSgr}`;
}
//if(options.startCol + options.cols < options.termWidth || options.forceLineTerm) {
if(options.startCol + i < options.termWidth || options.forceLineTerm) {
output += '\r\n';
}
});
if(options.exportMode) {
//
// If we're in export mode, we do some additional hackery:
//
// * Hard wrap ALL lines at <= 79 *characters* (not visible columns)
// if a line must wrap early, we'll place a ESC[A ESC[<N>C where <N>
// represents chars to get back to the position we were previously at
//
// * Replace contig spaces with ESC[<N>C as well to save... space.
//
// :TODO: this would be better to do as part of the processing above, but this will do for now
const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with
let exportOutput = '';
let m;
let afterSeq;
let wantMore;
let renderStart;
splitTextAtTerms(output).forEach(fullLine => {
renderStart = 0;
while(fullLine.length > 0) {
let splitAt;
const ANSI_REGEXP = ANSI.getFullMatchRegExp();
wantMore = true;
while((m = ANSI_REGEXP.exec(fullLine))) {
afterSeq = m.index + m[0].length;
if(afterSeq < MAX_CHARS) {
// after current seq
splitAt = afterSeq;
} else {
if(m.index < MAX_CHARS) {
// before last found seq
splitAt = m.index;
wantMore = false; // can't eat up any more
}
break; // seq's beyond this point are >= MAX_CHARS
}
}
if(splitAt) {
if(wantMore) {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
}
} else {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
}
const part = fullLine.slice(0, splitAt);
fullLine = fullLine.slice(splitAt);
renderStart += renderStringLength(part);
exportOutput += `${part}\r\n`;
if(fullLine.length > 0) { // more to go for this line?
exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`;
} else {
exportOutput += ANSI.up();
} }
} }
});
col += 1; return cb(null, exportOutput);
}
// :TODO: end *all* with CRLF - general usage will be width : 79 - prob update defaults
if(col <= options.width) {
canvas[row][col] = canvas[row][col] || {};
canvas[row][col].char = '\r\n';
canvas[row][col].sgr = ANSI.reset();
// :TODO: don't splice, just reset + fill with ' ' till end
for(let fillCol = col; fillCol <= options.width; ++fillCol) {
canvas[row][fillCol].char = ' ';
}
//canvas[row] = canvas[row].splice(0, col + 1);
//canvas[row][options.width - 1].char = '\r\n';
} else {
canvas[row] = canvas[row].splice(0, options.width + 1);
}
} }
let out = ''; return cb(null, output);
for(let row = 0; row < options.height; ++row) {
out += canvas[row].map( col => {
let c = col.sgr || '';
c += col.char;
return c;
}).join('');
}
// :TODO: finalize: @ any non-char cell, reset sgr & set to ' '
// :TODO: finalize: after sgr established, omit anything > supplied dimens
return cb(out);
}); });
parser.parse(input); parser.parse(input);
} }
/* function isAnsiLine(line) {
const fs = require('graceful-fs'); return isAnsi(line);// || renderStringLength(line) < line.length;
let data = fs.readFileSync('/home/nuskooler/Downloads/art3.ans'); }
data = iconv.decode(data, 'cp437');
createCleanAnsi(data, { width : 79, height : 25 }, (out) => { //
out = iconv.encode(out, 'cp437'); // Returns true if the line is considered "formatted". A line is
fs.writeFileSync('/home/nuskooler/Downloads/art4.ans', out); // considered formatted if it contains:
}); // * ANSI
*/ // * Pipe codes
// * Extended (CP437) ASCII - https://www.ascii-codes.com/
// * Tabs
// * Contigous 3+ spaces before the end of the line
//
function isFormattedLine(line) {
if(renderStringLength(line) < line.length) {
return true; // ANSI or Pipe Codes
}
if(line.match(/[\t\x00-\x1f\x80-\xff]/)) { // eslint-disable-line no-control-regex
return true;
}
if(_.trimEnd(line).match(/[ ]{3,}/)) {
return true;
}
return false;
}
function isAnsi(input) {
//
// * ANSI found - limited, just colors
// * Full ANSI art
// *
//
// FULL ANSI art:
// * SAUCE present & reports as ANSI art
// * ANSI clear screen within first 2-3 codes
// * ANSI movement codes (goto, right, left, etc.)
//
// *
/*
readSAUCE(input, (err, sauce) => {
if(!err && ('ANSi' === sauce.fileType || 'ANSiMation' === sauce.fileType)) {
return cb(null, 'ansi');
}
});
*/
// :TODO: if a similar method is kept, use exec() until threshold
const ANSI_DET_REGEXP = /(?:\x1b\x5b)[\?=;0-9]*?[ABCDEFGHJKLMSTfhlmnprsu]/g; // eslint-disable-line no-control-regex
const m = input.match(ANSI_DET_REGEXP) || [];
return m.length >= 4; // :TODO: do this reasonably, e.g. a percent or soemthing
}
function splitTextAtTerms(s) {
return s.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
}

View File

@ -10,6 +10,7 @@ const getFullConfig = require('./config_util.js').getFullConfig;
const asset = require('./asset.js'); const asset = require('./asset.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const ErrorReasons = require('./enig_error.js').ErrorReasons;
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
@ -80,24 +81,27 @@ function refreshThemeHelpers(theme) {
function loadTheme(themeID, cb) { function loadTheme(themeID, cb) {
var path = paths.join(Config.paths.themes, themeID, 'theme.hjson'); const path = paths.join(Config.paths.themes, themeID, 'theme.hjson');
configCache.getConfigWithOptions( { filePath : path, forceReCache : true }, function loaded(err, theme) { configCache.getConfigWithOptions( { filePath : path, forceReCache : true }, (err, theme) => {
if(err) { if(err) {
cb(err); return cb(err);
} else {
if(!_.isObject(theme.info) ||
!_.isString(theme.info.name) ||
!_.isString(theme.info.author))
{
cb(new Error('Invalid or missing "info" section!'));
return;
}
refreshThemeHelpers(theme);
cb(null, theme, path);
} }
if(!_.isObject(theme.info) ||
!_.isString(theme.info.name) ||
!_.isString(theme.info.author))
{
return cb(Errors.Invalid('Invalid or missing "info" section'));
}
if(false === _.get(theme, 'info.enabled')) {
return cb(Errors.General('Theme is not enalbed', ErrorReasons.ErrNotEnabled));
}
refreshThemeHelpers(theme);
return cb(null, theme, path);
}); });
} }
@ -261,69 +265,72 @@ function getMergedTheme(menuConfig, promptConfig, theme) {
} }
function initAvailableThemes(cb) { function initAvailableThemes(cb) {
var menuConfig;
var promptConfig;
async.waterfall( async.waterfall(
[ [
function loadMenuConfig(callback) { function loadMenuConfig(callback) {
getFullConfig(Config.general.menuFile, function gotConfig(err, mc) { getFullConfig(Config.general.menuFile, (err, menuConfig) => {
menuConfig = mc; return callback(err, menuConfig);
callback(err);
}); });
}, },
function loadPromptConfig(callback) { function loadPromptConfig(menuConfig, callback) {
getFullConfig(Config.general.promptFile, function gotConfig(err, pc) { getFullConfig(Config.general.promptFile, (err, promptConfig) => {
promptConfig = pc; return callback(err, menuConfig, promptConfig);
callback(err);
}); });
}, },
function getDir(callback) { function getThemeDirectories(menuConfig, promptConfig, callback) {
fs.readdir(Config.paths.themes, function dirRead(err, files) { fs.readdir(Config.paths.themes, (err, files) => {
callback(err, files); if(err) {
return callback(err);
}
return callback(
null,
menuConfig,
promptConfig,
files.filter( f => {
// sync normally not allowed -- initAvailableThemes() is a startup-only method, however
return fs.statSync(paths.join(Config.paths.themes, f)).isDirectory();
})
);
}); });
}, },
function filterFiles(files, callback) { function populateAvailable(menuConfig, promptConfig, themeDirectories, callback) {
var filtered = files.filter(function filter(file) { async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID
return fs.statSync(paths.join(Config.paths.themes, file)).isDirectory(); loadTheme(themeId, (err, theme, themePath) => {
}); if(err) {
callback(null, filtered); if(ErrorReasons.NotEnabled !== err.reasonCode) {
}, Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme');
function populateAvailable(filtered, callback) { }
// :TODO: this is a bit broken with callback placement and configCache.on() handler
filtered.forEach(function themeEntry(themeId) { return nextThemeDir(null); // try next
loadTheme(themeId, function themeLoaded(err, theme, themePath) {
if(!err) {
availableThemes[themeId] = getMergedTheme(menuConfig, promptConfig, theme);
configCache.on('recached', function recached(path) {
if(themePath === path) {
loadTheme(themeId, function reloaded(err, reloadedTheme) {
Log.debug( { info : theme.info }, 'Theme recached' );
availableThemes[themeId] = reloadedTheme;
});
}
});
Log.debug( { info : theme.info }, 'Theme loaded');
} else {
Log.warn( { themeId : themeId, error : err.toString() }, 'Failed to load theme');
} }
});
availableThemes[themeId] = getMergedTheme(menuConfig, promptConfig, theme);
configCache.on('recached', recachedPath => {
if(themePath === recachedPath) {
loadTheme(themeId, (err, reloadedTheme) => {
if(!err) {
// :TODO: This is still broken - Need to reapply *latest* menu config and prompt configs to theme at very least
Log.debug( { info : theme.info }, 'Theme recached' );
availableThemes[themeId] = getMergedTheme(menuConfig, promptConfig, reloadedTheme);
} else if(ErrorReasons.NotEnabled === err.reasonCode) {
// :TODO: we need to disable this theme -- users may be using it! We'll need to re-assign them if so
}
});
}
});
return nextThemeDir(null);
});
}, err => {
return callback(err);
}); });
callback(null);
} }
], ],
function onComplete(err) { err => {
if(err) { return cb(err, availableThemes ? availableThemes.length : 0);
cb(err);
return;
}
cb(null, availableThemes.length);
} }
); );
} }
@ -340,17 +347,24 @@ function getRandomTheme() {
} }
function setClientTheme(client, themeId) { function setClientTheme(client, themeId) {
var desc; let logMsg;
try { const availThemes = getAvailableThemes();
client.currentTheme = getAvailableThemes()[themeId];
desc = 'Set client theme'; client.currentTheme = availThemes[themeId];
} catch(e) { if(client.currentTheme) {
client.currentTheme = getAvailableThemes()[Config.defaults.theme]; logMsg = 'Set client theme';
desc = 'Failed setting theme by supplied ID; Using default'; } else {
client.currentTheme = availThemes[Config.defaults.theme];
if(client.currentTheme) {
logMsg = 'Failed setting theme by supplied ID; Using default';
} else {
client.currentTheme = availThemes[Object.keys(availThemes)[0]];
logMsg = 'Failed setting theme by system default ID; Using the first one we can find';
}
} }
client.log.debug( { themeId : themeId, info : client.currentTheme.info }, desc); client.log.debug( { themeId : themeId, info : client.currentTheme.info }, logMsg);
} }
function getThemeArt(options, cb) { function getThemeArt(options, cb) {
@ -375,7 +389,7 @@ function getThemeArt(options, cb) {
// :TODO: Some of these options should only be set if not provided! // :TODO: Some of these options should only be set if not provided!
options.asAnsi = true; // always convert to ANSI options.asAnsi = true; // always convert to ANSI
options.readSauce = true; // read SAUCE, if avail options.readSauce = true; // read SAUCE, if avail
options.random = _.isBoolean(options.random) ? options.random : true; // FILENAME<n>.EXT support options.random = _.get(options, 'random', true); // FILENAME<n>.EXT support
// //
// We look for themed art in the following order: // We look for themed art in the following order:
@ -450,34 +464,20 @@ function displayThemeArt(options, cb) {
assert(_.isObject(options.client)); assert(_.isObject(options.client));
assert(_.isString(options.name)); assert(_.isString(options.name));
getThemeArt(options, function themeArt(err, artInfo) { getThemeArt(options, (err, artInfo) => {
if(err) { if(err) {
cb(err); return cb(err);
} else {
// :TODO: just use simple merge of options -> displayOptions
/*
var dispOptions = {
art : artInfo.data,
sauce : artInfo.sauce,
client : options.client,
font : options.font,
trailingLF : options.trailingLF,
};
art.display(dispOptions, function displayed(err, mciMap, extraInfo) {
cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
});
*/
const displayOpts = {
sauce : artInfo.sauce,
font : options.font,
trailingLF : options.trailingLF,
};
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
});
} }
// :TODO: just use simple merge of options -> displayOptions
const displayOpts = {
sauce : artInfo.sauce,
font : options.font,
trailingLF : options.trailingLF,
};
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
});
}); });
} }
@ -566,8 +566,10 @@ function displayThemedPrompt(name, client, options, cb) {
// //
// If we did *not* clear the screen, don't let the font change // If we did *not* clear the screen, don't let the font change
// as it will mess with the output of the existing art displayed in a terminal // doing so messes things up -- most terminals that support font
// changing can only display a single font at at time.
// //
// :TODO: We can use term detection to do nifty things like avoid this kind of kludge:
const dispOptions = Object.assign( {}, promptConfig.options ); const dispOptions = Object.assign( {}, promptConfig.options );
if(!options.clearScreen) { if(!options.clearScreen) {
dispOptions.font = 'not_really_a_font!'; // kludge :) dispOptions.font = 'not_really_a_font!'; // kludge :)

View File

@ -17,6 +17,7 @@ const crypto = require('crypto');
// //
// Class to read and hold information from a TIC file // Class to read and hold information from a TIC file
// //
// * FTS-5006.001 @ http://www.filegate.net/ftsc/FTS-5006.001
// * FSP-1039.001 @ http://ftsc.org/docs/old/fsp-1039.001 // * FSP-1039.001 @ http://ftsc.org/docs/old/fsp-1039.001
// * FSC-0087.001 @ http://ftsc.org/docs/fsc-0087.001 // * FSC-0087.001 @ http://ftsc.org/docs/fsc-0087.001
// //

View File

@ -37,14 +37,16 @@ function userLogin(client, username, password, cb) {
}); });
if(existingClientConnection) { if(existingClientConnection) {
client.log.info( { client.log.info(
existingClientId : existingClientConnection.session.id, {
username : user.username, existingClientId : existingClientConnection.session.id,
userId : user.userId }, username : user.username,
userId : user.userId
},
'Already logged in' 'Already logged in'
); );
var existingConnError = new Error('Already logged in as supplied user'); const existingConnError = new Error('Already logged in as supplied user');
existingConnError.existingConn = true; existingConnError.existingConn = true;
// :TODO: We should use EnigError & pass existing connection as second param // :TODO: We should use EnigError & pass existing connection as second param
@ -61,24 +63,24 @@ function userLogin(client, username, password, cb) {
[ [
function setTheme(callback) { function setTheme(callback) {
setClientTheme(client, user.properties.theme_id); setClientTheme(client, user.properties.theme_id);
callback(null); return callback(null);
}, },
function updateSystemLoginCount(callback) { function updateSystemLoginCount(callback) {
StatLog.incrementSystemStat('login_count', 1, callback); return StatLog.incrementSystemStat('login_count', 1, callback);
}, },
function recordLastLogin(callback) { function recordLastLogin(callback) {
StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback);
}, },
function updateUserLoginCount(callback) { function updateUserLoginCount(callback) {
StatLog.incrementUserStat(user, 'login_count', 1, callback); return StatLog.incrementUserStat(user, 'login_count', 1, callback);
}, },
function recordLoginHistory(callback) { function recordLoginHistory(callback) {
const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers
StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); return StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback);
} }
], ],
function complete(err) { err => {
cb(err); return cb(err);
} }
); );
}); });

View File

@ -5,10 +5,10 @@
const MenuView = require('./menu_view.js').MenuView; const MenuView = require('./menu_view.js').MenuView;
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
const colorCodes = require('./color_codes.js');
// deps // deps
const util = require('util'); const util = require('util');
const _ = require('lodash');
exports.VerticalMenuView = VerticalMenuView; exports.VerticalMenuView = VerticalMenuView;
@ -20,6 +20,14 @@ function VerticalMenuView(options) {
const self = this; const self = this;
// we want page up/page down by default
if(!_.isObject(options.specialKeyMap)) {
Object.assign(this.specialKeyMap, {
'page up' : [ 'page up' ],
'page down' : [ 'page down' ],
});
}
this.performAutoScale = function() { this.performAutoScale = function() {
if(this.autoScale.height) { if(this.autoScale.height) {
this.dimens.height = (self.items.length * (self.itemSpacing + 1)) - (self.itemSpacing); this.dimens.height = (self.items.length * (self.itemSpacing + 1)) - (self.itemSpacing);
@ -99,7 +107,7 @@ VerticalMenuView.prototype.redraw = function() {
let row = this.position.row + 1; let row = this.position.row + 1;
const endRow = (row + this.oldDimens.height) - 2; const endRow = (row + this.oldDimens.height) - 2;
while(row < endRow) { while(row <= endRow) {
seq += ansi.goto(row, this.position.col) + blank; seq += ansi.goto(row, this.position.col) + blank;
row += 1; row += 1;
} }
@ -160,6 +168,10 @@ VerticalMenuView.prototype.onKeyPress = function(ch, key) {
this.focusPrevious(); this.focusPrevious();
} else if(this.isKeyMapped('down', key.name)) { } else if(this.isKeyMapped('down', key.name)) {
this.focusNext(); this.focusNext();
} else if(this.isKeyMapped('page up', key.name)) {
this.focusPreviousPageItem();
} else if( this.isKeyMapped('page down', key.name)) {
this.focusNextPageItem();
} }
} }
@ -243,6 +255,54 @@ VerticalMenuView.prototype.focusPrevious = function() {
VerticalMenuView.super_.prototype.focusPrevious.call(this); VerticalMenuView.super_.prototype.focusPrevious.call(this);
}; };
VerticalMenuView.prototype.focusPreviousPageItem = function() {
//
// Jump to current - up to page size or top
// If already at the top, jump to bottom
//
if(0 === this.focusedItemIndex) {
return this.focusPrevious(); // will jump to bottom
}
const index = Math.max(this.focusedItemIndex - this.dimens.height, 0);
if(index < this.viewWindow.top) {
this.oldDimens = Object.assign({}, this.dimens);
}
this.setFocusItemIndex(index);
return VerticalMenuView.super_.prototype.focusPreviousPageItem.call(this);
};
VerticalMenuView.prototype.focusNextPageItem = function() {
//
// Jump to current + up to page size or bottom
// If already at the bottom, jump to top
//
if(this.items.length - 1 === this.focusedItemIndex) {
return this.focusNext(); // will jump to top
}
const index = Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length - 1);
if(index > this.viewWindow.bottom) {
this.oldDimens = Object.assign({}, this.dimens);
this.focusedItemIndex = index;
this.viewWindow = {
top : this.focusedItemIndex,
bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1
};
this.redraw();
} else {
this.setFocusItemIndex(index);
}
return VerticalMenuView.super_.prototype.focusNextPageItem.call(this);
};
VerticalMenuView.prototype.setFocusItems = function(items) { VerticalMenuView.prototype.setFocusItems = function(items) {
VerticalMenuView.super_.prototype.setFocusItems.call(this, items); VerticalMenuView.super_.prototype.setFocusItems.call(this, items);

View File

@ -16,50 +16,6 @@ const SPACE_CHARS = [
const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g');
/*
//
// ANSI & pipe codes we indend to strip
//
// See also https://github.com/chalk/ansi-regex/blob/master/index.js
//
// :TODO: Consolidate this, regexp's in ansi_escape_parser, and strutil. Need more complete set that includes common standads, bansi, and cterm.txt
// renderStringLength() from strUtil does not account for ESC[<N>C (e.g. go forward)
const REGEXP_CONTROL_CODES = /(\|[\d]{2})|(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g;
function getRenderLength(s) {
let m;
let pos;
let len = 0;
REGEXP_CONTROL_CODES.lastIndex = 0; // reset
//
// Loop counting only literal (non-control) sequences
// paying special attention to ESC[<N>C which means forward <N>
//
do {
pos = REGEXP_CONTROL_CODES.lastIndex;
m = REGEXP_CONTROL_CODES.exec(s);
if(null !== m) {
if(m.index > pos) {
len += s.slice(pos, m.index).length;
}
if('C' === m[3]) { // ESC[<N>C is foward/right
len += parseInt(m[2], 10) || 0;
}
}
} while(0 !== REGEXP_CONTROL_CODES.lastIndex);
if(pos < s.length) {
len += s.slice(pos).length;
}
return len;
}
*/
function wordWrapText2(text, options) { function wordWrapText2(text, options) {
assert(_.isObject(options)); assert(_.isObject(options));
assert(_.isNumber(options.width)); assert(_.isNumber(options.width));
@ -68,7 +24,13 @@ function wordWrapText2(text, options) {
options.tabWidth = options.tabWidth || 4; options.tabWidth = options.tabWidth || 4;
options.tabChar = options.tabChar || ' '; options.tabChar = options.tabChar || ' ';
const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g'); //const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g');
//
// For a given word, match 0->options.width chars -- alwasy include a full trailing ESC
// sequence if present!
//
// :TODO: Need to create ansi.getMatchRegex or something - this is used all over
const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}\\x1b\\[[\\?=;0-9]*[ABCDEFGHJKLMSTfhlmnprsu]|.{0,${options.width}}`, 'g');
let m; let m;
let word; let word;

BIN
docs/images/enigma-bbs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -2,7 +2,7 @@
ENiGMA½ is a modern from scratch BBS package written in Node.js. ENiGMA½ is a modern from scratch BBS package written in Node.js.
# Quickstart # Quickstart
TL;DR? This should get you started... Unless you have a compelling reason to do otherwise, please use **The Easy Way** below.
## The Easy Way ## The Easy Way
Under most Linux/UNIX like environments (Linux, BSD, OS X, ...) new users can simply execute the `install.sh` script to get everything up and running. Simply cut + paste the following into your terminal: Under most Linux/UNIX like environments (Linux, BSD, OS X, ...) new users can simply execute the `install.sh` script to get everything up and running. Simply cut + paste the following into your terminal:
@ -13,7 +13,7 @@ curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/inst
For other environments such as Windows, see **The Manual Way** below. For other environments such as Windows, see **The Manual Way** below.
## The Manual Way ## The Manual Way (aka Advanced)
For Windows environments or if you simply like to do things manually, read on... For Windows environments or if you simply like to do things manually, read on...
### Prerequisites ### Prerequisites
@ -61,7 +61,7 @@ The main system configuration is handled via `~/.config/enigma-bbs/config.hjson`
`oputil.js` can be utilized to generate your **initial** configuration. **This is the recommended way for all new users**: `oputil.js` can be utilized to generate your **initial** configuration. **This is the recommended way for all new users**:
```bash ```bash
./oputil.js config --new ./oputil.js config new
``` ```
(You wil be asked a series of basic questions) (You wil be asked a series of basic questions)

View File

@ -74,6 +74,11 @@ There are many predefined MCI codes that can be used anywhere on the system (pla
* `AN`: Current active node count * `AN`: Current active node count
* `TC`: Total login/calls to system * `TC`: Total login/calls to system
* `RR`: Displays a random rumor * `RR`: Displays a random rumor
* `SD`: Total downloads, system wide
* `SO`: Total downloaded amount, system wide (formatted to appropriate bytes/megs/etc.)
* `SU`: Total uploads, system wide
* `SP`: Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.)
A special `XY` MCI code may also be utilized for placement identification when creating menus. A special `XY` MCI code may also be utilized for placement identification when creating menus.

27
misc/exodus.id_rsa Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAmpwn/vJ1CAIkVnQGDZumvEDsMyFSHioGO5RM/T2Id6XLX91r
feJI6w48yqLV+HgKLUK7eeTOzb/l4VShH9AzOqTbAxwfZ6fgzV2cI/wZxO0S6QpS
7IrwcVK1Bm7Wu45Kp7LcGHB66nHSb+wqIYkZobIc8Z9arClJxV4AzgaUxjJrk0wT
hW81r5TicbTG7zm+bOMLO/mln+HA/EOtx/yfKDcfkl+mLzzbMpojor+KdwuKJUb1
+r4PhPVl6pZgOuQIl37Qh5SPY3mMjwyXW/tUe+ZmPpfOm3CKf/pTLsA45QzUbNBY
GPLjbEcMJ4R5T3c2LXCKR+Wi9/pCkeZT7/1BbQIDAQABAoIBAQCCasrKIddahAQG
8SPSAsQo9FLJ5oeQbj6Hr1cqHueola/yE6KCs4hyzrW08JqxVwCuoSXncnyHziGp
a2vmnAc6pqkf/G75TwEv+pClQhiyppBXB6Bfa+vai7ury39TAnoy74r9CpSEgrLS
OlJnq3B1lvsXTiZ8Ju/Vjq/7Gk4QyFOVPugbmjhUtuCiyRXV9V2o/HUzZGtaXDp5
n+XOfb90mLtPhtIRC6wmgMkhlRPpGir+NN0DWQ1oBWZO+TockIFusVInOTEXY4ui
V+JJ3KRwfaogzJMnDcqkiCck6bMT8E85ucRScsJjpENsUyEjFAoRV2grbguc/rdx
dgG5BMx5AoGBAMgCDFGwCctHfRvRXIac5goxYuTkVYjEh4yxj8d/Y+0HmDJiH5HM
tiUAtsgq/KYKJKM9U0PJWdPW3DPJa+wDVPQSlIqUOiXEpwLA+yhXuAvTqia9chuI
vaP1Ze/4yfW2eQg+3Ji0vC9VEr1eoRnAwJI+fDE3fRCvoPohlT4+zOhvAoGBAMXk
ksy5DdtUOvt0wss7R030dEtHP/Hs+qheQJOhl+GLlQt5BKP6NsdM3OKXyXYLddOc
xrKSWdjtiWOtap0D7o7cBFv44EmgzSvM2QltYxF4phPaNn2zPC/Mkvs1EaYnMtw4
boKNDWbwixpCapheAE+lfA96DfqU/KyVaXls9MnjAoGAaL+B2ipbBsZ7BF2imrGD
XOU+iOf4z/c1kn7P8UiLefEXSZPQOti+sCRulejFhuQbCg8tE3xZejO2Ab1Es0eP
b4BnoSg+R9d1LGELaLaAIlmJbF6da0QzJbJ437QpeXFGdAYQHD3TrOpeNSVhNA6a
DD2DZ3dLHbkNktKRyhaz1CsCgYBMJbIfOK4OUZEIpVs3XK4JXyFIvjfq3aduFiZ/
KFULIuzNJ1oTxvpBImB0iLeqxqomLVN/7zTHdk/BnT9C//pR2nOK+G9FpayNSBvT
ttXCKUyuou8I22kzc2Kzay5JYxf9CXHspl4b2D+OcTQXQUSZYTIlum+alq3LswqN
ANIIxQKBgHauoT79sViuB/wHcp2W/mek0p9aLkgQKt+riPJ4vKXc8DtapTgQzXkk
6yQCOSD8T9DcVGBcap9n6T21NOyDQwM0gg+DoHVeYqBrAa93jufOi7EY3MFrkjH6
tC0crKBcUkxu43zhY4DkHLxId5btSPH57U+lhrJGjKXdvlJrGGOM
-----END RSA PRIVATE KEY-----

View File

@ -61,7 +61,6 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
this.menuMethods = { this.menuMethods = {
saveFilter : (formData, extraArgs, cb) => { saveFilter : (formData, extraArgs, cb) => {
return this.saveCurrentFilter(formData, cb); return this.saveCurrentFilter(formData, cb);
}, },
prevFilter : (formData, extraArgs, cb) => { prevFilter : (formData, extraArgs, cb) => {
this.currentFilterIndex -= 1; this.currentFilterIndex -= 1;
@ -93,7 +92,15 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
return cb(null); return cb(null);
}, },
deleteFilter : (formData, extraArgs, cb) => { deleteFilter : (formData, extraArgs, cb) => {
const filterUuid = this.filtersArray[this.currentFilterIndex].uuid; const selectedFilter = this.filtersArray[this.currentFilterIndex];
const filterUuid = selectedFilter.uuid;
// cannot delete built-in/system filters
if(true === selectedFilter.system) {
this.showError('Cannot delete built in filters!');
return cb(null);
}
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
@ -144,6 +151,17 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
}; };
} }
showError(errMsg) {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
if(errorView) {
if(errMsg) {
errorView.setText(errMsg);
} else {
errorView.clearText();
}
}
}
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if(err) {

View File

@ -8,7 +8,6 @@ const ansi = require('../core/ansi_term.js');
const theme = require('../core/theme.js'); const theme = require('../core/theme.js');
const FileEntry = require('../core/file_entry.js'); const FileEntry = require('../core/file_entry.js');
const stringFormat = require('../core/string_format.js'); const stringFormat = require('../core/string_format.js');
const createCleanAnsi = require('../core/string_util.js').createCleanAnsi;
const FileArea = require('../core/file_base_area.js'); const FileArea = require('../core/file_base_area.js');
const Errors = require('../core/enig_error.js').Errors; const Errors = require('../core/enig_error.js').Errors;
const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled; const ErrNotEnabled = require('../core/enig_error.js').ErrorReasons.NotEnabled;
@ -18,8 +17,7 @@ const DownloadQueue = require('../core/download_queue.js');
const FileAreaWeb = require('../core/file_area_web.js'); const FileAreaWeb = require('../core/file_area_web.js');
const FileBaseFilters = require('../core/file_base_filter.js'); const FileBaseFilters = require('../core/file_base_filter.js');
const resolveMimeType = require('../core/mime_util.js').resolveMimeType; const resolveMimeType = require('../core/mime_util.js').resolveMimeType;
const isAnsi = require('../core/string_util.js').isAnsi;
const cleanControlCodes = require('../core/string_util.js').cleanControlCodes;
// deps // deps
const async = require('async'); const async = require('async');
@ -74,8 +72,12 @@ exports.getModule = class FileAreaList extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
if(options.extraArgs) { this.filterCriteria = _.get(options, 'extraArgs.filterCriteria');
this.filterCriteria = options.extraArgs.filterCriteria; this.fileList = _.get(options, 'extraArgs.fileList');
if(this.fileList) {
// we'll need to adjust position as well!
this.fileListPosition = 0;
} }
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
@ -116,7 +118,13 @@ exports.getModule = class FileAreaList extends MenuModule {
return this.displayDetailsPage(cb); return this.displayDetailsPage(cb);
}, },
detailsQuit : (formData, extraArgs, cb) => { detailsQuit : (formData, extraArgs, cb) => {
this.viewControllers.details.setFocus(false); [ 'detailsNfo', 'detailsFileList', 'details' ].forEach(n => {
const vc = this.viewControllers[n];
if(vc) {
vc.detachClientEvents();
}
});
return this.displayBrowsePage(true, cb); // true=clearScreen return this.displayBrowsePage(true, cb); // true=clearScreen
}, },
toggleQueue : (formData, extraArgs, cb) => { toggleQueue : (formData, extraArgs, cb) => {
@ -212,8 +220,8 @@ exports.getModule = class FileAreaList extends MenuModule {
const entryInfo = currEntry.entryInfo = { const entryInfo = currEntry.entryInfo = {
fileId : currEntry.fileId, fileId : currEntry.fileId,
areaTag : currEntry.areaTag, areaTag : currEntry.areaTag,
areaName : area.name || 'N/A', areaName : _.get(area, 'name') || 'N/A',
areaDesc : area.desc || 'N/A', areaDesc : _.get(area, 'desc') || 'N/A',
fileSha256 : currEntry.fileSha256, fileSha256 : currEntry.fileSha256,
fileName : currEntry.fileName, fileName : currEntry.fileName,
desc : currEntry.desc || '', desc : currEntry.desc || '',
@ -250,9 +258,9 @@ exports.getModule = class FileAreaList extends MenuModule {
const userRatingTicked = config.userRatingTicked || '*'; const userRatingTicked = config.userRatingTicked || '*';
const userRatingUnticked = config.userRatingUnticked || ''; const userRatingUnticked = config.userRatingUnticked || '';
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
entryInfo.userRatingString = new Array(entryInfo.userRating + 1).join(userRatingTicked); entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating);
if(entryInfo.userRating < 5) { if(entryInfo.userRating < 5) {
entryInfo.userRatingString += new Array( (5 - entryInfo.userRating) + 1).join(userRatingUnticked); entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) );
} }
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => { FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => {
@ -373,31 +381,34 @@ exports.getModule = class FileAreaList extends MenuModule {
return self.populateCurrentEntryInfo(callback); return self.populateCurrentEntryInfo(callback);
}); });
}, },
function populateViews(callback) { function populateDesc(callback) {
if(_.isString(self.currentFileEntry.desc)) { if(_.isString(self.currentFileEntry.desc)) {
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc);
if(descView) { if(descView) {
createCleanAnsi( if(isAnsi(self.currentFileEntry.desc)) {
self.currentFileEntry.desc, descView.setAnsi(
{ height : self.client.termHeight, width : descView.dimens.width }, self.currentFileEntry.desc,
cleanDesc => { {
// :TODO: use cleanDesc -- need to finish createCleanAnsi() !! prepped : false,
//descView.setText(cleanDesc); forceLineTerm : true
descView.setText( self.currentFileEntry.desc ); },
() => {
self.updateQueueIndicator(); return callback(null);
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart); }
);
return callback(null); } else {
} descView.setText(self.currentFileEntry.desc);
); return callback(null);
}
} }
} else { } else {
self.updateQueueIndicator();
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
return callback(null); return callback(null);
} }
},
function populateAdditionalViews(callback) {
self.updateQueueIndicator();
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart);
return callback(null);
} }
], ],
err => { err => {
@ -618,17 +629,37 @@ exports.getModule = class FileAreaList extends MenuModule {
case 'nfo' : case 'nfo' :
{ {
const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo); const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo);
if(nfoView) { if(!nfoView) {
return callback(null);
}
if(isAnsi(self.currentFileEntry.entryInfo.descLong)) {
nfoView.setAnsi(
self.currentFileEntry.entryInfo.descLong,
{
prepped : false,
forceLineTerm : true,
},
() => {
return callback(null);
}
);
} else {
nfoView.setText(self.currentFileEntry.entryInfo.descLong); nfoView.setText(self.currentFileEntry.entryInfo.descLong);
return callback(null);
} }
} }
break; break;
case 'fileList' : case 'fileList' :
self.populateFileListing(); self.populateFileListing();
break; return callback(null);
}
default :
return callback(null);
}
},
function setLabels(callback) {
self.populateCustomLabels(name, MciViewIds[name].customRangeStart); self.populateCustomLabels(name, MciViewIds[name].customRangeStart);
return callback(null); return callback(null);
} }

View File

@ -0,0 +1,84 @@
/* jslint node: true */
'use strict';
// enigma-bbs
const MenuModule = require('../core/menu_module.js').MenuModule;
const Config = require('../core/config.js').config;
const stringFormat = require('../core/string_format.js');
const ViewController = require('../core/view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('../core/file_base_area.js').getSortedAvailableFileAreas;
// deps
const async = require('async');
const _ = require('lodash');
exports.moduleInfo = {
name : 'File Area Selector',
desc : 'Select from available file areas',
author : 'NuSkooler',
};
const MciViewIds = {
areaList : 1,
};
exports.getModule = class FileAreaSelectModule extends MenuModule {
constructor(options) {
super(options);
this.config = this.menuConfig.config || {};
this.loadAvailAreas();
this.menuMethods = {
selectArea : (formData, extraArgs, cb) => {
const area = this.availAreas[formData.value.areaSelect] || 0;
const filterCriteria = {
areaTag : area.areaTag,
};
const menuOpts = {
extraArgs : {
filterCriteria : filterCriteria,
},
menuFlags : [ 'noHistory' ],
};
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb);
}
};
}
loadAvailAreas() {
this.availAreas = getSortedAvailableFileAreas(this.client);
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if(err) {
return cb(err);
}
this.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => {
if(err) {
return cb(err);
}
const areaListView = vc.getView(MciViewIds.areaList);
const areaListFormat = this.config.areaListFormat || '{name}';
areaListView.setItems(this.availAreas.map(a => stringFormat(areaListFormat, a) ) );
if(this.config.areaListFocusFormat) {
areaListView.setFocusItems(this.availAreas.map(a => stringFormat(this.config.areaListFocusFormat, a) ) );
}
areaListView.redraw();
return cb(null);
});
});
}
};

View File

@ -472,7 +472,7 @@
0: { 0: {
mci: { mci: {
TL1: { TL1: {
argName: from argName: from
} }
ET2: { ET2: {
argName: to argName: to
@ -721,7 +721,7 @@
} }
newScanMessageList: { newScanMessageList: {
desc: Viewing New Message List desc: New Messages
module: msg_list module: msg_list
art: NEWMSGS art: NEWMSGS
config: { config: {
@ -761,6 +761,166 @@
} }
} }
newScanFileBaseList: {
module: file_area_list
desc: New Files
config: {
art: {
browse: FNEWBRWSE
details: FDETAIL
detailsGeneral: FDETGEN
detailsNfo: FDETNFO
detailsFileList: FDETLST
help: FBHELP
}
}
form: {
0: {
mci: {
MT1: {
mode: preview
ansiView: true
}
HM2: {
focus: true
submit: true
argName: navSelect
items: [
"prev", "next", "details", "toggle queue", "rate", "help", "quit"
]
focusItemIndex: 1
}
// :TODO: these can be removed once the hack is not required:
TL10: {}
TL11: {}
TL12: {}
TL13: {}
TL14: {}
TL15: {}
TL16: {}
TL17: {}
TL18: {}
}
submit: {
*: [
{
value: { navSelect: 0 }
action: @method:prevFile
}
{
value: { navSelect: 1 }
action: @method:nextFile
}
{
value: { navSelect: 2 }
action: @method:viewDetails
}
{
value: { navSelect: 3 }
action: @method:toggleQueue
}
{
value: { navSelect: 4 }
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
value: { navSelect: 5 }
action: @method:displayHelp
}
{
value: { navSelect: 6 }
action: @systemMethod:prevMenu
}
]
}
actionKeys: [
{
keys: [ "w", "shift + w" ]
action: @method:showWebDownloadLink
}
{
keys: [ "escape", "q", "shift + q" ]
action: @systemMethod:prevMenu
}
{
keys: [ "t", "shift + t" ]
action: @method:toggleQueue
}
{
keys: [ "v", "shift + v" ]
action: @method:viewDetails
}
{
keys: [ "r", "shift + r" ]
action: @menu:fileBaseGetRatingForSelectedEntry
}
{
keys: [ "?" ]
action: @method:displayHelp
}
]
}
1: {
mci: {
HM1: {
focus: true
submit: true
argName: navSelect
items: [
"general", "nfo/readme", "file listing"
]
}
// :TODO: these can be removed once the hack is not required:
TL10: {}
TL11: {}
TL12: {}
TL13: {}
TL14: {}
TL15: {}
TL16: {}
TL17: {}
TL18: {}
}
actionKeys: [
{
keys: [ "escape", "q", "shift + q" ]
action: @method:detailsQuit
}
]
}
2: {
// details - general
mci: {}
}
3: {
// details - nfo/readme
mci: {
MT1: {
mode: preview
}
}
}
4: {
// details - file listing
mci: {
VM1: {
}
}
}
}
}
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
// Main Menu // Main Menu
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
@ -1916,50 +2076,49 @@
} }
} }
submit: { submit: {
*: [ *: [
{
value: { 1: 0 }
action: @method:editModeMenuSave
}
{
value: { 1: 1 }
action: @systemMethod:prevMenu
}
{
value: { 1: 2 },
action: @method:editModeMenuQuote
}
{
value: { 1: 3 }
action: @method:editModeMenuHelp
}
]
}
actionKeys: [
{ {
keys: [ "escape" ] value: { 1: 0 }
action: @method:editModeEscPressed
}
{
keys: [ "s", "shift + s" ]
action: @method:editModeMenuSave action: @method:editModeMenuSave
} }
{ {
keys: [ "d", "shift + d" ] value: { 1: 1 }
action: @systemMethod:prevMenu action: @systemMethod:prevMenu
} }
{ {
keys: [ "q", "shift + q" ] value: { 1: 2 },
action: @method:editModeMenuQuote action: @method:editModeMenuQuote
} }
{ {
keys: [ "?" ] value: { 1: 3 }
action: @method:editModeMenuHelp action: @method:editModeMenuHelp
} }
] ]
} }
actionKeys: [
{
keys: [ "escape" ]
action: @method:editModeEscPressed
}
{
keys: [ "s", "shift + s" ]
action: @method:editModeMenuSave
}
{
keys: [ "d", "shift + d" ]
action: @systemMethod:prevMenu
}
{
keys: [ "q", "shift + q" ]
action: @method:editModeMenuQuote
}
{
keys: [ "?" ]
action: @method:editModeMenuHelp
}
]
} }
// Quote builder // Quote builder
@ -2313,9 +2472,13 @@
prompt: fileMenuCommand prompt: fileMenuCommand
submit: [ submit: [
{ {
value: { menuOption: "B" } value: { menuOption: "L" }
action: @menu:fileBaseListEntries action: @menu:fileBaseListEntries
} }
{
value: { menuOption: "B" }
action: @menu:fileBaseBrowseByAreaSelect
}
{ {
value: { menuOption: "F" } value: { menuOption: "F" }
action: @menu:fileAreaFilterEditor action: @menu:fileAreaFilterEditor
@ -2510,6 +2673,38 @@
} }
} }
fileBaseBrowseByAreaSelect: {
desc: Browsing File Areas
module: file_base_area_select
art: FAREASEL
form: {
0: {
mci: {
VM1: {
focus: true
argName: areaSelect
}
}
submit: {
*: [
{
value: { areaSelect: null }
action: @method:selectArea
}
]
}
actionKeys: [
{
keys: [ "escape" ]
action: @systemMethod:prevMenu
}
]
}
}
}
fileBaseGetRatingForSelectedEntry: { fileBaseGetRatingForSelectedEntry: {
desc: Rating a File desc: Rating a File
prompt: fileBaseRateEntryPrompt prompt: fileBaseRateEntryPrompt

View File

@ -22,8 +22,9 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
this.editorMode = 'view'; this.editorMode = 'view';
if(_.isObject(options.extraArgs)) { if(_.isObject(options.extraArgs)) {
this.messageList = options.extraArgs.messageList; this.messageList = options.extraArgs.messageList;
this.messageIndex = options.extraArgs.messageIndex; this.messageIndex = options.extraArgs.messageIndex;
this.lastMessageNextExit = options.extraArgs.lastMessageNextExit;
} }
this.messageList = this.messageList || []; this.messageList = this.messageList || [];
@ -41,6 +42,12 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb);
} }
// auto-exit if no more to go?
if(self.lastMessageNextExit) {
self.lastMessageReached = true;
return self.prevMenu(cb);
}
return cb(null); return cb(null);
}, },
@ -120,6 +127,9 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
} }
getMenuResult() { getMenuResult() {
return this.messageIndex; return {
messageIndex : this.messageIndex,
lastMessageReached : this.lastMessageReached,
};
} }
}; };

View File

@ -49,6 +49,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
this.messageAreaTag = config.messageAreaTag; this.messageAreaTag = config.messageAreaTag;
this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false);
if(options.extraArgs) { if(options.extraArgs) {
// //
// |extraArgs| can override |messageAreaTag| provided by config // |extraArgs| can override |messageAreaTag| provided by config
@ -73,6 +75,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
messageAreaTag : self.messageAreaTag, messageAreaTag : self.messageAreaTag,
messageList : self.messageList, messageList : self.messageList,
messageIndex : formData.value.message, messageIndex : formData.value.message,
lastMessageNextExit : true,
} }
}; };
@ -107,6 +110,10 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
} }
enter() { enter() {
if(this.lastMessageReachedExit) {
return this.prevMenu();
}
super.enter(); super.enter();
// //

Binary file not shown.

Binary file not shown.

View File

@ -3,6 +3,7 @@
name: Mystery Skull name: Mystery Skull
author: Luciano Ayres author: Luciano Ayres
group: blocktronics group: blocktronics
enabled: true
} }
customization: { customization: {
@ -590,6 +591,124 @@
} }
} }
newScanFileBaseList: {
config: {
hashTagsSep: "|08, |07"
browseInfoFormat10: "|00|10{fileName} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}"
browseInfoFormat11: "|00|15{areaName}"
browseInfoFormat12: "|00|07{hashTags}"
browseInfoFormat13: "|00|07{estReleaseYear}"
browseInfoFormat14: "|00|07{dlCount}"
browseInfoFormat15: "{userRatingString}"
browseInfoFormat16: "{isQueued}"
browseInfoFormat17: "{webDlLink}{webDlExpire}"
webDlExpireTimeFormat: " [|08- |07exp] ddd, MMM Do @ h:mm a"
webDlLinkNeedsGenerated: "|08(|07press |10W |07to generate link|08)"
isQueuedIndicator: "|00|10YES"
isNotQueuedIndicator: "|00|07no"
userRatingTicked: "|00|15*"
userRatingUnticked: "|00|07-"
detailsGeneralInfoFormat10: "{fileName}"
detailsGeneralInfoFormat11: "|00|07{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08(|03{byteSize:,} |11B|08)"
detailsGeneralInfoFormat12: "|00|07{hashTags}"
detailsGeneralInfoFormat13: "{estReleaseYear}"
detailsGeneralInfoFormat14: "{dlCount}"
detailsGeneralInfoFormat15: "{userRatingString}"
detailsGeneralInfoFormat16: "{fileCrc32}"
detailsGeneralInfoFormat17: "{fileMd5}"
detailsGeneralInfoFormat18: "{fileSha1}"
detailsGeneralInfoFormat19: "{fileSha256}"
detailsGeneralInfoFormat20: "{uploadByUsername}"
detailsGeneralInfoFormat21: "{uploadTimestamp}"
detailsGeneralInfoFormat22: "{archiveTypeDesc}"
fileListEntryFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}"
focusFileListEntryFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}"
notAnArchiveFormat: "|00|08( |07{fileName} is not an archive |08)"
}
0: {
mci: {
MT1: {
height: 16
width: 45
}
HM2: {
focusTextStyle: first lower
}
TL11: {
width: 21
textOverflow: ...
}
TL12: {
width: 21
textOverflow: ...
}
TL13: { width: 21 }
TL14: { width: 21 }
TL15: { width: 21 }
TL16: { width: 21 }
TL17: { width: 73 }
}
}
1: {
mci: {
HM1: {
focusTextStyle: first lower
}
}
}
2: {
}
3: {
// details - nfo/readme
mci: {
MT1: {
height: 19
width: 79
}
}
}
4: {
mci: {
VM1: {
height: 17
width: 79
}
}
}
}
fileBaseBrowseByAreaSelect: {
config: {
protListFormat: "|00|03{name}"
protListFocusFormat: "|00|19|15{name}"
}
0: {
mci: {
VM1: {
height: 15
width: 30
focusTextStyle: first lower
}
}
}
}
fileBaseSearch: { fileBaseSearch: {
mci: { mci: {
ET1: { ET1: {

View File

@ -15,7 +15,6 @@ const pathWithTerminatingSeparator = require('../core/file_util.js').pathWithTe
const Log = require('../core/logger.js').log; const Log = require('../core/logger.js').log;
const Errors = require('../core/enig_error.js').Errors; const Errors = require('../core/enig_error.js').Errors;
const FileEntry = require('../core/file_entry.js'); const FileEntry = require('../core/file_entry.js');
const enigmaToAnsi = require('../core/color_codes.js').enigmaToAnsi;
// deps // deps
const async = require('async'); const async = require('async');

View File

@ -1,6 +1,6 @@
{ {
"name": "enigma-bbs", "name": "enigma-bbs",
"version": "0.0.6-alpha", "version": "0.0.7-alpha",
"description": "ENiGMA½ Bulletin Board System", "description": "ENiGMA½ Bulletin Board System",
"author": "Bryan Ashby <bryan@l33t.codes>", "author": "Bryan Ashby <bryan@l33t.codes>",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
@ -39,13 +39,14 @@
"ptyw.js": "NuSkooler/ptyw.js", "ptyw.js": "NuSkooler/ptyw.js",
"sanitize-filename": "^1.6.1", "sanitize-filename": "^1.6.1",
"sqlite3": "^3.1.1", "sqlite3": "^3.1.1",
"ssh2": "^0.5.1", "ssh2": "^0.5.5",
"temptmp": "^1.0.0", "temptmp": "^1.0.0",
"uuid": "^3.0.1", "uuid": "^3.0.1",
"uuid-parse": "^1.0.0", "uuid-parse": "^1.0.0",
"ws" : "^3.0.0", "ws" : "^3.0.0",
"graceful-fs" : "^4.1.11", "graceful-fs" : "^4.1.11",
"exiftool" : "^0.0.3" "exiftool" : "^0.0.3",
"node-glob" : "^1.2.0"
}, },
"devDependencies": {}, "devDependencies": {},
"engines": { "engines": {

View File

@ -17,6 +17,7 @@ const FILETYPE_HANDLERS = {};
[ 'AIFF', 'APE', 'FLAC', 'OGG', 'MP3' ].forEach(ext => FILETYPE_HANDLERS[ext] = audioFile); [ 'AIFF', 'APE', 'FLAC', 'OGG', 'MP3' ].forEach(ext => FILETYPE_HANDLERS[ext] = audioFile);
[ 'PDF', 'DOC', 'DOCX', 'DOCM', 'ODB', 'ODC', 'ODF', 'ODG', 'ODI', 'ODP', 'ODS', 'ODT' ].forEach(ext => FILETYPE_HANDLERS[ext] = documentFile); [ 'PDF', 'DOC', 'DOCX', 'DOCM', 'ODB', 'ODC', 'ODF', 'ODG', 'ODI', 'ODP', 'ODS', 'ODT' ].forEach(ext => FILETYPE_HANDLERS[ext] = documentFile);
[ 'PNG', 'JPEG', 'GIF', 'WEBP', 'XCF' ].forEach(ext => FILETYPE_HANDLERS[ext] = imageFile); [ 'PNG', 'JPEG', 'GIF', 'WEBP', 'XCF' ].forEach(ext => FILETYPE_HANDLERS[ext] = imageFile);
[ 'MP4', 'MOV', 'AVI', 'MKV', 'MPG', 'MPEG', 'M4V', 'WMV' ].forEach(ext => FILETYPE_HANDLERS[ext] = videoFile);
function audioFile(metadata) { function audioFile(metadata) {
// nothing if we don't know at least the author or title // nothing if we don't know at least the author or title
@ -32,6 +33,10 @@ function audioFile(metadata) {
return desc; return desc;
} }
function videoFile(metadata) {
return `${metadata.fileType} video(${metadata.imageSize}px, ${metadata.duration}, ${metadata.audioBitsPerSample}/${metadata.audioSampleRate} audio)`;
}
function documentFile(metadata) { function documentFile(metadata) {
// nothing if we don't know at least the author or title // nothing if we don't know at least the author or title
if(!metadata.author && !metadata.title) { if(!metadata.author && !metadata.title) {