From 90706f87986f42a3e9faf807da3c44f3b0d9e280 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 14 Jan 2018 22:00:00 -0700 Subject: [PATCH 001/569] * Move to Node.js 8.x LTS * Update some packages --- WHATSNEW.md | 4 ++++ package.json | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 667c296f..594418a7 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,6 +1,10 @@ # Whats New This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. +## 0.0.9-alpha +* Development is now against Node.js 8.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! + + ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. * File descriptions (FILE_ID.DIZ, etc.) now support Renegade |## pipe, PCBoard, and other less common color codes found commonly in BBS era scene releases. diff --git a/package.json b/package.json index 678628f1..b44cc894 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enigma-bbs", - "version": "0.0.8-alpha", + "version": "0.0.9-alpha", "description": "ENiGMA½ Bulletin Board System", "author": "Bryan Ashby ", "license": "BSD-2-Clause", @@ -33,12 +33,12 @@ "hashids": "^1.1.1", "hjson": "^3.1.0", "iconv-lite": "^0.4.18", - "inquirer": "^4.0.1", + "inquirer": "^5.0.0", "later": "1.2.0", "lodash": "^4.17.4", "mime-types": "^2.1.17", "minimist": "1.2.x", - "moment": "^2.20.0", + "moment": "^2.20.1", "nodemailer": "^4.4.1", "ptyw.js": "NuSkooler/ptyw.js", "rlogin": "^1.0.0", @@ -50,7 +50,7 @@ "temptmp": "^1.0.0", "uuid": "^3.1.0", "uuid-parse": "^1.0.0", - "ws": "^3.3.3", + "ws": "^4.0.0", "xxhash": "^0.2.4", "yazl": "^2.4.2" }, From a106050ba3c958b7099cc881f31b23517248e272 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 09:41:18 -0700 Subject: [PATCH 002/569] Fix attempts to load bad path --- core/events.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/events.js b/core/events.js index 8e16a374..5febb463 100644 --- a/core/events.js +++ b/core/events.js @@ -48,17 +48,17 @@ module.exports = new class Events extends events.EventEmitter { } async.each(files, (moduleName, nextModule) => { - modulePath = paths.join(modulePath, moduleName); + const fullModulePath = paths.join(modulePath, moduleName); try { - const mod = require(modulePath); - + const mod = require(fullModulePath); + if(_.isFunction(mod.registerEvents)) { // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? mod.registerEvents(this); } } catch(e) { - + Log.warn( { error : e }, 'Exception during module "registerEvents"'); } return nextModule(null); From ac1433e84beda36d89044e7f889b317bf21771ad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 12:22:11 -0700 Subject: [PATCH 003/569] * Code cleanup and eslint since -- remove unused variables, clean up RegExs, so on... --- core/abracadabra.js | 10 +- core/acs.js | 4 +- core/ansi_escape_parser.js | 56 +++-- core/ansi_prep.js | 22 +- core/ansi_term.js | 82 ++++---- core/archive_util.js | 44 ++-- core/art.js | 16 +- core/asset.js | 8 +- core/bbs.js | 2 +- core/bbs_link.js | 10 +- core/bbs_list.js | 16 +- core/button_view.js | 2 +- core/client.js | 46 ++--- core/client_connections.js | 2 +- core/client_term.js | 14 +- core/color_codes.js | 10 +- core/combatnet.js | 92 ++++----- core/conf_area_util.js | 2 +- core/config.js | 4 +- core/config_cache.js | 2 +- core/connect.js | 2 +- core/crc.js | 4 +- core/database.js | 12 +- core/door.js | 10 +- core/door_party.js | 42 ++-- core/download_queue.js | 6 +- core/edit_text_view.js | 4 +- core/enig_error.js | 2 +- core/enigma_assert.js | 4 +- core/erc_client.js | 16 +- core/event_scheduler.js | 82 ++++---- core/exodus.js | 4 +- core/file_area_filter_edit.js | 28 +-- core/file_area_list.js | 54 ++--- core/file_area_web.js | 12 +- core/file_base_area.js | 70 +++---- core/file_base_area_select.js | 2 +- core/file_base_download_manager.js | 8 +- core/file_base_filter.js | 16 +- core/file_base_search.js | 2 +- core/file_base_web_download_manager.js | 27 ++- core/file_entry.js | 24 +-- core/file_transfer.js | 40 ++-- core/file_transfer_protocol_select.js | 4 +- core/file_util.js | 4 +- core/fnv1a.js | 10 +- core/fse.js | 134 ++++++------ core/ftn_address.js | 8 +- core/ftn_mail_packet.js | 146 ++++++------- core/ftn_util.js | 62 +++--- core/horizontal_menu_view.js | 4 +- core/key_entry_view.js | 4 +- core/last_callers.js | 12 +- core/logger.js | 8 +- core/login_server_module.js | 8 +- core/mail_packet.js | 2 +- core/mail_util.js | 2 +- core/mask_edit_text_view.js | 16 +- core/mci_view_factory.js | 25 ++- core/menu_module.js | 32 +-- core/menu_stack.js | 10 +- core/menu_util.js | 44 ++-- core/menu_view.js | 14 +- core/message.js | 112 +++++----- core/message_area.js | 98 ++++----- core/mime_util.js | 2 +- core/misc_util.js | 4 +- core/mod_mixins.js | 6 +- core/msg_area_list.js | 20 +- core/msg_area_post_fse.js | 4 +- core/msg_area_view_fse.js | 8 +- core/msg_conf_list.js | 18 +- core/msg_list.js | 38 ++-- core/msg_network.js | 8 +- core/msg_scan_toss_module.js | 6 +- core/multi_line_edit_text_view.js | 29 ++- core/new_scan.js | 42 ++-- core/nua.js | 16 +- core/onelinerz.js | 26 +-- core/plugin_module.js | 2 +- core/predefined_mci.js | 8 +- core/rumorz.js | 18 +- core/sauce.js | 8 +- core/scanner_tossers/ftn_bso.js | 25 ++- core/set_newscan_date.js | 4 +- core/spinner_menu_view.js | 23 +-- core/stat_log.js | 18 +- core/stats.js | 30 --- core/status_bar_view.js | 64 ------ core/string_format.js | 30 +-- core/string_util.js | 275 ++++--------------------- core/system_menu_method.js | 18 +- core/system_view_validate.js | 8 +- core/telnet_bridge.js | 20 +- core/text_view.js | 50 ++--- core/theme.js | 140 ++++++------- core/tic_file_info.js | 20 +- core/ticker_text_view.js | 94 --------- core/toggle_menu_view.js | 12 +- core/upload.js | 52 ++--- core/user.js | 40 ++-- core/user_config.js | 46 ++--- core/user_group.js | 54 +++-- core/user_list.js | 6 +- core/user_login.js | 8 +- core/uuid_util.js | 10 +- core/vertical_menu_view.js | 16 +- core/view.js | 16 +- core/view_controller.js | 70 +++---- core/web_password_reset.js | 19 +- core/whos_online.js | 2 +- core/word_wrap.js | 166 +++------------ 112 files changed, 1375 insertions(+), 1898 deletions(-) delete mode 100644 core/stats.js delete mode 100644 core/status_bar_view.js delete mode 100644 core/ticker_text_view.js diff --git a/core/abracadabra.js b/core/abracadabra.js index 85d1e205..0ac17887 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -87,11 +87,11 @@ exports.getModule = class AbracadabraModule extends MenuModule { [ function validateNodeCount(callback) { if(self.config.nodeMax > 0 && - _.isNumber(activeDoorNodeInstances[self.config.name]) && + _.isNumber(activeDoorNodeInstances[self.config.name]) && activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) { - self.client.log.info( - { + self.client.log.info( + { name : self.config.name, activeCount : activeDoorNodeInstances[self.config.name] }, @@ -118,11 +118,11 @@ exports.getModule = class AbracadabraModule extends MenuModule { } else { activeDoorNodeInstances[self.config.name] = 1; } - + callback(null); } }, - function generateDropfile(callback) { + function generateDropfile(callback) { self.dropFile = new DropFile(self.client, self.config.dropFileType); var fullPath = self.dropFile.fullPath; diff --git a/core/acs.js b/core/acs.js index f2e04b9f..9532ad78 100644 --- a/core/acs.js +++ b/core/acs.js @@ -13,7 +13,7 @@ class ACS { constructor(client) { this.client = client; } - + check(acs, scope, defaultAcs) { acs = acs ? acs[scope] : defaultAcs; acs = acs || defaultAcs; @@ -22,7 +22,7 @@ class ACS { } catch(e) { Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); return false; - } + } } // diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 7e777618..feb7b164 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -3,7 +3,9 @@ const miscUtil = require('./misc_util.js'); const ansi = require('./ansi_term.js'); +const Log = require('./logger.js').log; +// deps const events = require('events'); const util = require('util'); const _ = require('lodash'); @@ -24,7 +26,7 @@ function ANSIEscapeParser(options) { this.graphicRendition = {}; this.parseState = { - re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex }; options = miscUtil.valueWithDefault(options, { @@ -46,7 +48,7 @@ function ANSIEscapeParser(options) { self.column = Math.max(self.column, 1); self.column = Math.min(self.column, self.termWidth); // can't move past term width self.row = Math.max(self.row, 1); - + self.positionUpdated(); }; @@ -63,7 +65,7 @@ function ANSIEscapeParser(options) { delete self.savedPosition; self.positionUpdated(); -// self.rowUpdated(); + // self.rowUpdated(); }; self.clearScreen = function() { @@ -71,7 +73,7 @@ function ANSIEscapeParser(options) { self.emit('clear screen'); }; -/* + /* self.rowUpdated = function() { self.emit('row update', self.row + self.scrollBack); };*/ @@ -95,7 +97,7 @@ function ANSIEscapeParser(options) { start = pos; self.column = 1; - + self.positionUpdated(); break; @@ -132,7 +134,7 @@ function ANSIEscapeParser(options) { if(self.column > self.termWidth) { self.column = 1; self.row += 1; - + self.positionUpdated(); } @@ -142,17 +144,9 @@ function ANSIEscapeParser(options) { } } - function getProcessedMCI(mci) { - if(self.mciReplaceChar.length > 0) { - return ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + new Array(mci.length + 1).join(self.mciReplaceChar); - } else { - return mci; - } - } - function parseMCI(buffer) { // :TODO: move this to "constants" seciton @ top - var mciRe = /\%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; + var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; var pos = 0; var match; var mciCode; @@ -186,27 +180,23 @@ function ANSIEscapeParser(options) { self.graphicRenditionForErase = _.clone(self.graphicRendition); } - - self.emit('mci', { - mci : mciCode, + + self.emit('mci', { + mci : mciCode, id : id ? parseInt(id, 10) : null, - args : args, + args : args, SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) }); if(self.mciReplaceChar.length > 0) { const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase); - - self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[\;m]/).slice(0, 3)); + + self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)); literal(new Array(match[0].length + 1).join(self.mciReplaceChar)); } else { literal(match[0]); } - - //literal(getProcessedMCI(match[0])); - - //self.emit('chunk', getProcessedMCI(match[0])); } } while(0 !== mciRe.lastIndex); @@ -220,7 +210,7 @@ function ANSIEscapeParser(options) { self.parseState = { // ignore anything past EOF marker, if any buffer : input.split(String.fromCharCode(0x1a), 1)[0], - re : /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex stop : false, }; }; @@ -290,14 +280,14 @@ function ANSIEscapeParser(options) { break; } } - - parseMCI(lastBit) + + parseMCI(lastBit); } self.emit('complete'); }; -/* + /* self.parse = function(buffer, savedRe) { // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. // :TODO: move this to "constants" section @ top @@ -382,12 +372,12 @@ function ANSIEscapeParser(options) { break; // save position - case 's' : + case 's' : self.saveCursorPosition(); break; // restore position - case 'u' : + case 'u' : self.restoreCursorPosition(); break; @@ -422,7 +412,7 @@ function ANSIEscapeParser(options) { case 1 : case 2 : - case 22 : + case 22 : self.graphicRendition.intensity = arg; break; @@ -448,7 +438,7 @@ function ANSIEscapeParser(options) { break; default : - console.log('Unknown attribute: ' + arg); // :TODO: Log properly + Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI'); break; } } diff --git a/core/ansi_prep.js b/core/ansi_prep.js index 45b93d32..a4c894d8 100644 --- a/core/ansi_prep.js +++ b/core/ansi_prep.js @@ -4,7 +4,7 @@ // ENiGMA½ const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSI = require('./ansi_term.js'); -const { +const { splitTextAtTerms, renderStringLength } = require('./string_util.js'); @@ -41,7 +41,7 @@ module.exports = function ansiPrep(input, options, cb) { if(canvas[row]) { return; } - + canvas[row] = Array.from( { length : options.cols}, () => new Object() ); } @@ -113,17 +113,17 @@ module.exports = function ansiPrep(input, options, cb) { const lastCol = getLastPopulatedColumn(row) + 1; let i; - line = options.indent ? + line = options.indent ? output.length > 0 ? ' '.repeat(options.indent) : '' : ''; - + for(i = 0; i < lastCol; ++i) { const col = row[i]; - sgr = !options.asciiMode && 0 === i ? + sgr = !options.asciiMode && 0 === i ? col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : ''; - + if(!options.asciiMode && col.sgr) { sgr += ANSI.getSGRFromGraphicRendition(col.sgr); } @@ -148,7 +148,7 @@ module.exports = function ansiPrep(input, options, cb) { 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[C where // represents chars to get back to the position we were previously at @@ -157,8 +157,8 @@ module.exports = function ansiPrep(input, options, cb) { // // :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 exportOutput = ''; + let m; let afterSeq; let wantMore; @@ -184,7 +184,7 @@ module.exports = function ansiPrep(input, options, cb) { splitAt = m.index; wantMore = false; // can't eat up any more } - + break; // seq's beyond this point are >= MAX_CHARS } } @@ -203,7 +203,7 @@ module.exports = function ansiPrep(input, options, cb) { exportOutput += `${part}\r\n`; if(fullLine.length > 0) { // more to go for this line? - exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; + exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; } else { exportOutput += ANSI.up(); } diff --git a/core/ansi_term.js b/core/ansi_term.js index 7eb10ec2..0a1eaa41 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -3,7 +3,7 @@ // // ANSI Terminal Support Resources -// +// // ANSI-BBS // * http://ansi-bbs.org/ // @@ -31,7 +31,7 @@ // 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. +// with legit oldschool DOS terminals, and so on. // // ENiGMA½ @@ -113,7 +113,7 @@ const CONTROL = { // // Support: // * SyncTERM: Works as expected - // * NetRunner: + // * NetRunner: // // General Notes: // See also notes in bansi.txt and cterm.txt about the various @@ -160,7 +160,7 @@ const SGRValues = { negative : 7, hidden : 8, - normal : 22, // + normal : 22, // steady : 25, positive : 27, @@ -203,7 +203,7 @@ function getBGColorValue(name) { // :TODO: Create mappings for aliases... maybe make this a map to values instead // :TODO: Break this up in to two parts: // 1) FONT_AND_CODE_PAGES (e.g. SyncTERM/cterm) -// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES. +// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES. // ...we can then have getFontFromSAUCEName(sauceFontName) // Also, create a SAUCE_ENCODING_MAP: SAUCE font name -> encodings @@ -215,45 +215,45 @@ function getBGColorValue(name) { // const SYNCTERM_FONT_AND_ENCODING_TABLE = [ 'cp437', - 'cp1251', - 'koi8_r', - 'iso8859_2', - 'iso8859_4', - 'cp866', - 'iso8859_9', - 'haik8', - 'iso8859_8', - 'koi8_u', - 'iso8859_15', + 'cp1251', + 'koi8_r', + 'iso8859_2', 'iso8859_4', - 'koi8_r_b', - 'iso8859_4', - 'iso8859_5', - 'ARMSCII_8', + 'cp866', + 'iso8859_9', + 'haik8', + 'iso8859_8', + 'koi8_u', 'iso8859_15', - 'cp850', - 'cp850', - 'cp885', - 'cp1251', - 'iso8859_7', - 'koi8-r_c', - 'iso8859_4', - 'iso8859_1', - 'cp866', - 'cp437', - 'cp866', + 'iso8859_4', + 'koi8_r_b', + 'iso8859_4', + 'iso8859_5', + 'ARMSCII_8', + 'iso8859_15', + 'cp850', + 'cp850', 'cp885', - 'cp866_u', - 'iso8859_1', - 'cp1131', - 'c64_upper', + 'cp1251', + 'iso8859_7', + 'koi8-r_c', + 'iso8859_4', + 'iso8859_1', + 'cp866', + 'cp437', + 'cp866', + 'cp885', + 'cp866_u', + 'iso8859_1', + 'cp1131', + 'c64_upper', 'c64_lower', - 'c128_upper', - 'c128_lower', - 'atari', - 'pot_noodle', + 'c128_upper', + 'c128_lower', + 'atari', + 'pot_noodle', 'mo_soul', - 'microknight_plus', + 'microknight_plus', 'topaz_plus', 'microknight', 'topaz', @@ -289,7 +289,7 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = { 'topaz' : 'topaz', 'amiga_topaz_1' : 'topaz', 'amiga_topaz_1+' : 'topaz_plus', - 'topazplus' : 'topaz_plus', + 'topazplus' : 'topaz_plus', 'topaz_plus' : 'topaz_plus', 'amiga_topaz_2' : 'topaz', 'amiga_topaz_2+' : 'topaz_plus', @@ -349,7 +349,7 @@ function setCursorStyle(cursorStyle) { return `${ESC_CSI}${ps} q`; } return ''; - + } // Create methods such as up(), nextLine(),... diff --git a/core/archive_util.js b/core/archive_util.js index 6d2644c8..00e48ed8 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -23,7 +23,7 @@ class Archiver { } ok() { - return this.canCompress() && this.canDecompress(); + return this.canCompress() && this.canDecompress(); } can(what) { @@ -41,7 +41,7 @@ class Archiver { } module.exports = class ArchiveUtil { - + constructor() { this.archivers = {}; this.longestSignature = 0; @@ -93,7 +93,7 @@ module.exports = class ArchiveUtil { getArchiver(mimeTypeOrExtension) { mimeTypeOrExtension = resolveMimeType(mimeTypeOrExtension); - + if(!mimeTypeOrExtension) { // lookup returns false on failure return; } @@ -103,21 +103,23 @@ module.exports = class ArchiveUtil { return _.get( Config, [ 'archives', 'archivers', archiveHandler ] ); } } - + haveArchiver(archType) { return this.getArchiver(archType) ? true : false; } - detectTypeWithBuf(buf, cb) { - // :TODO: implement me! + // :TODO: implement me: + /* + detectTypeWithBuf(buf, cb) { } + */ detectType(path, cb) { fs.open(path, 'r', (err, fd) => { if(err) { return cb(err); } - + const buf = new Buffer(this.longestSignature); fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { if(err) { @@ -140,7 +142,7 @@ module.exports = class ArchiveUtil { }); return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); - }); + }); }); } @@ -153,15 +155,15 @@ module.exports = class ArchiveUtil { err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); } }); - + proc.once('exit', exitCode => { return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err); - }); + }); } compressTo(archType, archivePath, files, cb) { const archiver = this.getArchiver(archType); - + if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } @@ -189,13 +191,13 @@ module.exports = class ArchiveUtil { if(!cb && _.isFunction(fileList)) { cb = fileList; fileList = []; - haveFileList = false; + haveFileList = false; } else { haveFileList = true; } const archiver = this.getArchiver(archType); - + if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } @@ -211,7 +213,7 @@ module.exports = class ArchiveUtil { const args = archiver[action].args.map( arg => { return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); }); - + const fileListPos = args.indexOf('{fileList}'); if(fileListPos > -1) { // replace {fileList} with 0:n sep file list arguments @@ -230,9 +232,9 @@ module.exports = class ArchiveUtil { listEntries(archivePath, archType, cb) { const archiver = this.getArchiver(archType); - + if(!archiver) { - return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } const fmtObj = { @@ -240,7 +242,7 @@ module.exports = class ArchiveUtil { }; const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); - + let proc; try { proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); @@ -251,7 +253,7 @@ module.exports = class ArchiveUtil { let output = ''; proc.on('data', data => { // :TODO: hack for: execvp(3) failed.: No such file or directory - + output += data; }); @@ -273,16 +275,16 @@ module.exports = class ArchiveUtil { } return cb(null, entries); - }); + }); } - + getPtyOpts() { return { // :TODO: cwd name : 'enigma-archiver', cols : 80, rows : 24, - env : process.env, + env : process.env, }; } }; diff --git a/core/art.js b/core/art.js index 19e0bafe..546014bd 100644 --- a/core/art.js +++ b/core/art.js @@ -33,7 +33,7 @@ const SUPPORTED_ART_TYPES = { '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, - '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, + '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, // :TODO: extentions for wwiv, renegade, celerity, syncronet, ... // :TODO: extension for atari @@ -93,7 +93,7 @@ function getArtFromPath(path, options, cb) { } return result; - } + } if(options.readSauce === true) { sauce.readSAUCE(data, (err, sauce) => { @@ -164,7 +164,7 @@ function getArt(name, options, cb) { const bn = paths.basename(file, fext).toLowerCase(); if(options.random) { const suppliedBn = paths.basename(name, fext).toLowerCase(); - + // // Random selection enabled. We'll allow for // basename1.ext, basename2.ext, ... @@ -208,7 +208,7 @@ function getArt(name, options, cb) { return getArtFromPath(readPath, options, cb); } - + return cb(new Error(`No matching art for supplied criteria: ${name}`)); }); } @@ -287,7 +287,7 @@ function display(client, art, options, cb) { return cb(null, mciMap, extraInfo); } - if(!options.disableMciCache) { + if(!options.disableMciCache) { artHash = xxhash.hash(new Buffer(art), 0xCAFEBABE); // see if we have a mciMap cached for this art @@ -307,7 +307,7 @@ function display(client, art, options, cb) { if(mciCprQueue.length > 0) { mciMap[mciCprQueue.shift()].position = pos; - if(parseComplete && 0 === mciCprQueue.length) { + if(parseComplete && 0 === mciCprQueue.length) { return completed(); } } @@ -345,7 +345,7 @@ function display(client, art, options, cb) { }); } - ansiParser.on('literal', literal => client.term.write(literal, false) ); + ansiParser.on('literal', literal => client.term.write(literal, false) ); ansiParser.on('control', control => client.term.rawWrite(control) ); ansiParser.on('complete', () => { @@ -353,7 +353,7 @@ function display(client, art, options, cb) { if(0 === mciCprQueue.length) { return completed(); - } + } }); let initSeq = ''; diff --git a/core/asset.js b/core/asset.js index 9f2831b7..3f44a604 100644 --- a/core/asset.js +++ b/core/asset.js @@ -31,7 +31,7 @@ const ALL_ASSETS = [ const ASSET_RE = new RegExp('\\@(' + ALL_ASSETS.join('|') + ')\\:([\\w\\d\\.]*)(?:\\/([\\w\\d\\_]+))*'); -function parseAsset(s) { +function parseAsset(s) { const m = ASSET_RE.exec(s); if(m) { @@ -68,7 +68,7 @@ function getAssetWithShorthand(spec, defaultType) { function getArtAsset(spec) { const asset = getAssetWithShorthand(spec, 'art'); - + if(!asset) { return null; } @@ -79,7 +79,7 @@ function getArtAsset(spec) { function getModuleAsset(spec) { const asset = getAssetWithShorthand(spec, 'systemModule'); - + if(!asset) { return null; } @@ -105,7 +105,7 @@ function resolveConfigAsset(spec) { return conf; } else { return spec; - } + } } function resolveSystemStatAsset(spec) { diff --git a/core/bbs.js b/core/bbs.js index 43bf7cf3..c2f54802 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -182,7 +182,7 @@ function initialize(cb) { return database.initializeDatabases(callback); }, function initMimeTypes(callback) { - return require('./mime_util.js').startup(callback); + return require('./mime_util.js').startup(callback); }, function initStatLog(callback) { return require('./stat_log.js').init(callback); diff --git a/core/bbs_link.js b/core/bbs_link.js index be341115..15416c2e 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -23,10 +23,10 @@ const packageJson = require('../package.json'); authCode: XXXXX schemeCode: XXXX door: lord - + // default hoss: games.bbslink.net host: games.bbslink.net - + // defualt port: 23 port: 23 } @@ -49,7 +49,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { this.config = options.menuConfig.config; this.config.host = this.config.host || 'games.bbslink.net'; this.config.port = this.config.port || 23; - } + } initSequence() { let token; @@ -141,7 +141,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { self.client.once('end', function clientEnd() { self.client.log.info('Connection ended. Terminating BBSLink connection'); clientTerminated = true; - bridgeConnection.end(); + bridgeConnection.end(); }); }); @@ -170,7 +170,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { ], function complete(err) { if(err) { - self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); + self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); } if(!clientTerminated) { diff --git a/core/bbs_list.js b/core/bbs_list.js index 33a7ff59..5c81f478 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -4,7 +4,7 @@ // ENiGMA½ const MenuModule = require('./menu_module.js').MenuModule; -const { +const { getModDatabasePath, getTransactionDatabase } = require('./database.js'); @@ -39,7 +39,7 @@ const MciViewIds = { SelectedBBSLoc : 6, SelectedBBSSoftware : 7, SelectedBBSNotes : 8, - SelectedBBSSubmitter : 9, + SelectedBBSSubmitter : 9, }, add : { BBSName : 1, @@ -49,7 +49,7 @@ const MciViewIds = { Location : 5, Software : 6, Notes : 7, - Error : 8, + Error : 8, } }; @@ -190,12 +190,12 @@ exports.getModule = class BBSListModule extends MenuModule { drawSelectedEntry(entry) { if(!entry) { - Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { this.setViewText('view', MciViewIds.view[mciName], ''); }); } else { const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)'; - + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]]; if(MciViewIds.view[mciName]) { @@ -270,7 +270,7 @@ exports.getModule = class BBSListModule extends MenuModule { (err, row) => { if (!err) { self.entries.push({ - id : row.id, + id : row.id, bbsName : row.bbs_name, sysOp : row.sysop, telnet : row.telnet, @@ -306,9 +306,9 @@ exports.getModule = class BBSListModule extends MenuModule { entriesView.on('index update', idx => { const entry = self.entries[idx]; - + self.drawSelectedEntry(entry); - + if(!entry) { self.selectedBBS = -1; } else { diff --git a/core/button_view.js b/core/button_view.js index 570adc09..d5b858c7 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -22,7 +22,7 @@ ButtonView.prototype.onKeyPress = function(ch, key) { if(this.isKeyMapped('accept', key.name) || ' ' === ch) { this.submitData = 'accept'; this.emit('action', 'accept'); - delete this.submitData; + delete this.submitData; } else { ButtonView.super_.prototype.onKeyPress.call(this, ch, key); } diff --git a/core/client.js b/core/client.js index 424748a6..58700c8f 100644 --- a/core/client.js +++ b/core/client.js @@ -52,8 +52,8 @@ exports.Client = Client; // Resources & Standards: // * http://www.ansi-bbs.org/ansi-bbs-core-server.html // -const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9\;]+)(R)/; -const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[\=\?]([0-9a-zA-Z\;]+)(c)/; +const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; +const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ @@ -64,19 +64,19 @@ const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); const RE_ESC_CODE_ANYWHERE = new RegExp( [ - RE_FUNCTION_KEYCODE_ANYWHERE.source, - RE_META_KEYCODE_ANYWHERE.source, + RE_FUNCTION_KEYCODE_ANYWHERE.source, + RE_META_KEYCODE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source, RE_DEV_ATTR_RESPONSE_ANYWHERE.source, /\u001b./.source ].join('|')); -function Client(input, output) { +function Client(/*input, output*/) { stream.call(this); const self = this; - + this.user = new User(); this.currentTheme = { info : { name : 'N/A', description : 'None' } }; this.lastKeyPressMs = Date.now(); @@ -125,9 +125,9 @@ function Client(input, output) { if(!termClient) { if(_.startsWith(deviceAttr, '67;84;101;114;109')) { - // + // // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt - // + // // Known clients: // * SyncTERM // @@ -139,11 +139,11 @@ function Client(input, output) { }; this.isMouseInput = function(data) { - return /\x1b\[M/.test(data) || - /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || + return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex + /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex /\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || - /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || + /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || /\u001b\[(O|I)/.test(data); }; @@ -163,7 +163,7 @@ function Client(input, output) { 'OE' : { name : 'clear' }, 'OF' : { name : 'end' }, 'OH' : { name : 'home' }, - + // xterm/rxvt '[11~' : { name : 'f1' }, '[12~' : { name : 'f2' }, @@ -290,7 +290,7 @@ function Client(input, output) { if(self.cprOffset) { cprArgs[0] = cprArgs[0] + self.cprOffset; cprArgs[1] = cprArgs[1] + self.cprOffset; - } + } self.emit('cursor position report', cprArgs); } } @@ -299,7 +299,7 @@ function Client(input, output) { var termClient = self.getTermClient(parts[1]); if(termClient) { self.term.termClient = termClient; - } + } } else if('\r' === s) { key.name = 'return'; } else if('\n' === s) { @@ -347,10 +347,10 @@ function Client(input, output) { key.meta = true; key.shift = /^[A-Z]$/.test(parts[1]); } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { - var code = + var code = (parts[1] || '') + (parts[2] || '') + (parts[4] || '') + (parts[9] || ''); - + var modifier = (parts[3] || parts[8] || 1) - 1; key.ctrl = !!(modifier & 4); @@ -375,7 +375,7 @@ function Client(input, output) { // // Adjust name for CTRL/Shift/Meta modifiers // - key.name = + key.name = (key.ctrl ? 'ctrl + ' : '') + (key.meta ? 'meta + ' : '') + (key.shift ? 'shift + ' : '') + @@ -446,7 +446,7 @@ Client.prototype.end = function () { } clearInterval(this.idleCheck); - + try { // // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH @@ -482,7 +482,7 @@ Client.prototype.isLocal = function() { /////////////////////////////////////////////////////////////////////////////// // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something -Client.prototype.defaultHandlerMissingMod = function(err) { +Client.prototype.defaultHandlerMissingMod = function() { var self = this; function handler(err) { @@ -493,12 +493,12 @@ Client.prototype.defaultHandlerMissingMod = function(err) { self.term.write('This has been logged for your SysOp to review.\n'); self.term.write('\nGoodbye!\n'); - + //self.term.write(err); //if(miscUtil.isDevelopment() && err.stack) { // self.term.write('\n' + err.stack + '\n'); - //} + //} self.end(); } @@ -516,8 +516,8 @@ Client.prototype.terminalSupports = function(query) { case 'vtx_hyperlink' : return 'vtx' === termClient; - - default : + + default : return false; } }; diff --git a/core/client_connections.js b/core/client_connections.js index 7e74e29d..d81d0922 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -95,7 +95,7 @@ function removeClient(client) { clientId : client.session.id }, 'Client disconnected' - ); + ); Events.emit('codes.l33t.enigma.system.disconnected', { client : client, connectionCount : clientConnections.length } ); } diff --git a/core/client_term.js b/core/client_term.js index b313841e..b944988d 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -15,8 +15,6 @@ exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { this.output = output; - var self = this; - var outputEncoding = 'cp437'; assert(iconv.encodingExists(outputEncoding)); @@ -56,7 +54,7 @@ function ClientTerminal(output) { }, set : function(ttype) { termType = ttype.toLowerCase(); - + if(this.isANSI()) { this.outputEncoding = 'cp437'; } else { @@ -137,7 +135,7 @@ ClientTerminal.prototype.isANSI = function() { // // syncterm: // * SyncTERM - // + // // xterm: // * PuTTY // @@ -168,7 +166,7 @@ ClientTerminal.prototype.rawWrite = function(s, cb) { if(cb) { return cb(err); } - + if(err) { Log.warn( { error : err.message }, 'Failed writing to socket'); } @@ -178,18 +176,18 @@ ClientTerminal.prototype.rawWrite = function(s, cb) { ClientTerminal.prototype.pipeWrite = function(s, spec, cb) { spec = spec || 'renegade'; - + var conv = { enigma : enigmaToAnsi, renegade : renegadeToAnsi, }[spec] || renegadeToAnsi; - + this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| }; ClientTerminal.prototype.encode = function(s, convertLineFeeds) { convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF; - + if(convertLineFeeds && _.isString(s)) { s = s.replace(/\n/g, '\r\n'); } diff --git a/core/color_codes.js b/core/color_codes.js index 2e368aa3..db9f4fe5 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -68,14 +68,14 @@ function enigmaToAnsi(s, client) { attr = ansi.sgr(['normal', val - 8, 'bold']); } - result += s.substr(lastIndex, m.index - lastIndex) + attr; + result += s.substr(lastIndex, m.index - lastIndex) + attr; } lastIndex = re.lastIndex; } result = (0 === result.length ? s : result + s.substr(lastIndex)); - + return result; } @@ -145,7 +145,7 @@ function renegadeToAnsi(s, client) { } // convert to number - val = parseInt(val, 10); + val = parseInt(val, 10); if(isNaN(val)) { val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal } @@ -160,7 +160,7 @@ function renegadeToAnsi(s, client) { lastIndex = re.lastIndex; } - return (0 === result.length ? s : result + s.substr(lastIndex)); + return (0 === result.length ? s : result + s.substr(lastIndex)); } // @@ -180,7 +180,7 @@ function renegadeToAnsi(s, client) { // * http://wiki.synchro.net/custom:colors // function controlCodesToAnsi(s, client) { - const RE = /(\|([A-Z0-9]{2})|\|)|(\@X([0-9A-F]{2}))|(\@([0-9A-F]{2})\@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex + const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex let m; let result = ''; diff --git a/core/combatnet.js b/core/combatnet.js index 6cde9c7b..217e6d17 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -25,10 +25,10 @@ exports.getModule = class CombatNetModule extends MenuModule { this.config.host = this.config.host || 'bbs.combatnet.us'; this.config.rloginPort = this.config.rloginPort || 4513; } - + initSequence() { const self = this; - + async.series( [ function validateConfig(callback) { @@ -45,59 +45,59 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.term.write('Connecting to CombatNet, please wait...\n'); const restorePipeToNormal = function() { - self.client.term.output.removeListener('data', sendToRloginBuffer); + self.client.term.output.removeListener('data', sendToRloginBuffer); }; - const rlogin = new RLogin( - { 'clientUsername' : self.config.password, - 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, - 'host' : self.config.host, - 'port' : self.config.rloginPort, - 'terminalType' : self.client.term.termClient, - 'terminalSpeed' : 57600 - } - ); + const rlogin = new RLogin( + { 'clientUsername' : self.config.password, + 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, + 'host' : self.config.host, + 'port' : self.config.rloginPort, + 'terminalType' : self.client.term.termClient, + 'terminalSpeed' : 57600 + } + ); - // If there was an error ... - rlogin.on('error', err => { - self.client.log.info(`CombatNet rlogin client error: ${err.message}`); - restorePipeToNormal(); - callback(err); - }); + // If there was an error ... + rlogin.on('error', err => { + self.client.log.info(`CombatNet rlogin client error: ${err.message}`); + restorePipeToNormal(); + return callback(err); + }); - // If we've been disconnected ... - rlogin.on('disconnect', () => { - self.client.log.info(`Disconnected from CombatNet`); - restorePipeToNormal(); - callback(null); - }); + // If we've been disconnected ... + rlogin.on('disconnect', () => { + self.client.log.info('Disconnected from CombatNet'); + restorePipeToNormal(); + return callback(null); + }); - function sendToRloginBuffer(buffer) { - rlogin.send(buffer); - }; + function sendToRloginBuffer(buffer) { + rlogin.send(buffer); + } - rlogin.on("connect", - /* The 'connect' event handler will be supplied with one argument, + rlogin.on('connect', + /* The 'connect' event handler will be supplied with one argument, a boolean indicating whether or not the connection was established. */ - function(state) { - if(state) { - self.client.log.info('Connected to CombatNet'); - self.client.term.output.on('data', sendToRloginBuffer); + function(state) { + if(state) { + self.client.log.info('Connected to CombatNet'); + self.client.term.output.on('data', sendToRloginBuffer); - } else { - return callback(new Error('Failed to establish establish CombatNet connection')); - } - } - ); + } else { + return callback(new Error('Failed to establish establish CombatNet connection')); + } + } + ); - // If data (a Buffer) has been received from the server ... - rlogin.on("data", (data) => { - self.client.term.rawWrite(data); - }); + // If data (a Buffer) has been received from the server ... + rlogin.on('data', (data) => { + self.client.term.rawWrite(data); + }); - // connect... - rlogin.connect(); + // connect... + rlogin.connect(); // note: no explicit callback() until we're finished! } @@ -106,10 +106,10 @@ exports.getModule = class CombatNetModule extends MenuModule { if(err) { self.client.log.warn( { error : err.message }, 'CombatNet error'); } - + // if the client is still here, go to previous self.prevMenu(); } ); - } + } }; diff --git a/core/conf_area_util.js b/core/conf_area_util.js index 5dabfb73..6b71061b 100644 --- a/core/conf_area_util.js +++ b/core/conf_area_util.js @@ -10,7 +10,7 @@ exports.sortAreasOrConfs = sortAreasOrConfs; // Method for sorting message, file, etc. areas and confs // If the sort key is present and is a number, sort in numerical order; // Otherwise, use a locale comparison on the sort key or name as a fallback -// +// function sortAreasOrConfs(areasOrConfs, type) { let entryA; let entryB; diff --git a/core/config.js b/core/config.js index 62ce6032..79824db5 100644 --- a/core/config.js +++ b/core/config.js @@ -653,12 +653,12 @@ function getDefaultConfig() { // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. desc : [ - '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' + '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' // eslint-disable-line no-useless-escape ], // common README filename - https://en.wikipedia.org/wiki/README descLong : [ - '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' + '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' // eslint-disable-line no-useless-escape ], }, diff --git a/core/config_cache.js b/core/config_cache.js index 8b57e125..875e1b2e 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -58,7 +58,7 @@ util.inherits(ConfigCache, events.EventEmitter); ConfigCache.prototype.getConfigWithOptions = function(options, cb) { assert(_.isString(options.filePath)); -// var self = this; + // var self = this; var isCached = (options.filePath in this.cache); if(options.forceReCache || !isCached) { diff --git a/core/connect.js b/core/connect.js index b94fa586..aae1b8c6 100644 --- a/core/connect.js +++ b/core/connect.js @@ -98,7 +98,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) { source : 'ANSI CPR' }, 'Window size updated' - ); + ); return done(null); }; diff --git a/core/crc.js b/core/crc.js index 886dad1d..e4bd8551 100644 --- a/core/crc.js +++ b/core/crc.js @@ -10,7 +10,7 @@ exports.CRC32 = class CRC32 { } update(input) { - input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); + input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); return input.length > 10240 ? this.update_8(input) : this.update_4(input); } @@ -47,7 +47,7 @@ exports.CRC32 = class CRC32 { this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; } } - + finalize() { return (this.crc ^ (-1)) >>> 0; } diff --git a/core/database.js b/core/database.js index 14b3bf95..10331fc0 100644 --- a/core/database.js +++ b/core/database.js @@ -36,12 +36,12 @@ function getModDatabasePath(moduleInfo, suffix) { // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) // We expect that moduleInfo defines packageName which will be the base of the modules // filename. An optional suffix may be supplied as well. - // - const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; + // + const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; assert(_.isObject(moduleInfo)); assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); - + let full = moduleInfo.packageName; if(suffix) { full += `.${suffix}`; @@ -198,7 +198,7 @@ const DB_INIT_TABLE = { DELETE FROM message_fts WHERE docid=old.rowid; END;` ); - + dbs.message.run( `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN DELETE FROM message_fts WHERE docid=old.rowid; @@ -256,14 +256,14 @@ const DB_INIT_TABLE = { UNIQUE(user_id, area_tag) );` ); - + dbs.message.run( `CREATE TABLE IF NOT EXISTS message_area_last_scan ( scan_toss VARCHAR NOT NULL, area_tag VARCHAR NOT NULL, message_id INTEGER NOT NULL, UNIQUE(scan_toss, area_tag) - );` + );` ); return cb(null); diff --git a/core/door.js b/core/door.js index 5670db1e..525a7a02 100644 --- a/core/door.js +++ b/core/door.js @@ -20,7 +20,7 @@ function Door(client, exeInfo) { this.exeInfo = exeInfo; this.exeInfo.encoding = this.exeInfo.encoding || 'cp437'; this.exeInfo.encoding = this.exeInfo.encoding.toLowerCase(); - let restored = false; + let restored = false; // // Members of exeInfo: @@ -52,7 +52,7 @@ function Door(client, exeInfo) { }; this.prepareSocketIoServer = function(cb) { - if('socket' === self.exeInfo.io) { + if('socket' === self.exeInfo.io) { const sockServer = createServer(conn => { sockServer.getConnections( (err, count) => { @@ -60,11 +60,11 @@ function Door(client, exeInfo) { // We expect only one connection from our DOOR/emulator/etc. if(!err && count <= 1) { self.client.term.output.pipe(conn); - + conn.on('data', self.doorDataHandler); conn.once('end', () => { - return self.restoreIo(conn); + return self.restoreIo(conn); }); conn.once('error', err => { @@ -117,7 +117,7 @@ Door.prototype.run = function() { rows : self.client.term.termHeight, // :TODO: cwd env : self.exeInfo.env, - }); + }); if('stdio' === self.exeInfo.io) { self.client.log.debug('Using stdio for door I/O'); diff --git a/core/door_party.js b/core/door_party.js index 762f626b..a64f92c8 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -24,13 +24,13 @@ exports.getModule = class DoorPartyModule extends MenuModule { this.config = options.menuConfig.config; this.config.host = this.config.host || 'dp.throwbackbbs.com'; this.config.sshPort = this.config.sshPort || 2022; - this.config.rloginPort = this.config.rloginPort || 513; + this.config.rloginPort = this.config.rloginPort || 513; } - + initSequence() { let clientTerminated; const self = this; - + async.series( [ function validateConfig(callback) { @@ -48,26 +48,26 @@ exports.getModule = class DoorPartyModule extends MenuModule { function establishSecureConnection(callback) { self.client.term.write(resetScreen()); self.client.term.write('Connecting to DoorParty, please wait...\n'); - + const sshClient = new SSHClient(); - + let pipeRestored = false; let pipedStream; const restorePipe = function() { if(pipedStream && !pipeRestored && !clientTerminated) { - self.client.term.output.unpipe(pipedStream); + self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); - } - }; - + } + }; + sshClient.on('ready', () => { // track client termination so we can clean up early self.client.once('end', () => { self.client.log.info('Connection ended. Terminating DoorParty connection'); clientTerminated = true; - sshClient.end(); + sshClient.end(); }); - + // establish tunnel for rlogin sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { if(err) { @@ -79,17 +79,17 @@ exports.getModule = class DoorPartyModule extends MenuModule { // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. // [XA]nuskooler // - const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; + const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; stream.write(rlogin); - + pipedStream = stream; // :TODO: this is hacky... self.client.term.output.pipe(stream); - + stream.on('data', d => { // :TODO: we should just pipe this... self.client.term.rawWrite(d); }); - + stream.on('close', () => { restorePipe(); sshClient.end(); @@ -100,32 +100,32 @@ exports.getModule = class DoorPartyModule extends MenuModule { sshClient.on('error', err => { self.client.log.info(`DoorParty SSH client error: ${err.message}`); }); - + sshClient.on('close', () => { restorePipe(); callback(null); }); - + sshClient.connect( { host : self.config.host, port : self.config.sshPort, username : self.config.username, password : self.config.password, }); - + // note: no explicit callback() until we're finished! - } + } ], err => { if(err) { self.client.log.warn( { error : err.message }, 'DoorParty error'); } - + // if the client is stil here, go to previous if(!clientTerminated) { self.prevMenu(); } } ); - } + } }; diff --git a/core/download_queue.js b/core/download_queue.js index 6bfbd47f..0f45b04d 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -59,14 +59,14 @@ module.exports = class DownloadQueue { } toProperty() { return JSON.stringify(this.client.user.downloadQueue); } - + loadFromProperty(prop) { try { this.client.user.downloadQueue = JSON.parse(prop); } catch(e) { this.client.user.downloadQueue = []; - this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); + this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); } - } + } }; diff --git a/core/edit_text_view.js b/core/edit_text_view.js index 8e55ae53..0db02638 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -16,7 +16,7 @@ function EditTextView(options) { options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.resizable = false; - + TextView.call(this, options); this.cursorPos = { row : 0, col : 0 }; @@ -44,7 +44,7 @@ EditTextView.prototype.onKeyPress = function(ch, key) { } } } - + return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); } else if(this.isKeyMapped('clearLine', key.name)) { this.text = ''; diff --git a/core/enig_error.js b/core/enig_error.js index 49627b9c..b0dd2335 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -14,7 +14,7 @@ class EnigError extends Error { if(typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor); } else { - this.stack = (new Error(message)).stack; + this.stack = (new Error(message)).stack; } } } diff --git a/core/enigma_assert.js b/core/enigma_assert.js index 2001825d..9217ea49 100644 --- a/core/enigma_assert.js +++ b/core/enigma_assert.js @@ -3,14 +3,14 @@ // ENiGMA½ const Config = require('./config.js').config; -const Log = require('./logger.js').log; +const Log = require('./logger.js').log; // deps const assert = require('assert'); module.exports = function(condition, message) { if(Config.debug.assertsEnabled) { - assert.apply(this, arguments); + assert.apply(this, arguments); } else if(!(condition)) { const stack = new Error().stack; Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' ); diff --git a/core/erc_client.js b/core/erc_client.js index 4fb549f6..ccc70199 100644 --- a/core/erc_client.js +++ b/core/erc_client.js @@ -37,12 +37,12 @@ var MciViewIds = { function ErcClientModule(options) { MenuModule.prototype.ctorShim.call(this, options); - const self = this; + const self = this; this.config = options.menuConfig.config; this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; - this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; - + this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; + this.finishedLoading = function() { async.waterfall( [ @@ -63,12 +63,12 @@ function ErcClientModule(options) { }; const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); - + chatMessageView.setText('Connecting to server...'); chatMessageView.redraw(); - + self.viewControllers.menu.switchFocus(MciViewIds.InputArea); - + // :TODO: Track actual client->enig connection for optional prevMenu @ final CB self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); @@ -98,12 +98,12 @@ function ErcClientModule(options) { } chatMessageView.addText(text); - + if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? chatMessageView.deleteLine(0); chatMessageView.scrollDown(); } - + chatMessageView.redraw(); self.viewControllers.menu.switchFocus(MciViewIds.InputArea); } diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 8b3d3239..0366d5ba 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -24,8 +24,8 @@ exports.moduleInfo = { author : 'NuSkooler', }; -const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:)([^\0]+)?$/; -const ACTION_REGEXP = /\@(method|execute)\:([^\0]+)?$/; +const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; +const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; class ScheduledEvent { constructor(events, name) { @@ -34,32 +34,32 @@ class ScheduledEvent { this.action = this.parseActionSpec(events[name].action); if(this.action) { this.action.args = events[name].args || []; - } + } } - + get isValid() { if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { return false; } - + if('method' === this.action.type && !this.action.location) { return false; } - - return true; + + return true; } - + parseScheduleString(schedStr) { if(!schedStr) { return false; } - + let schedule = {}; - + const m = SCHEDULE_REGEXP.exec(schedStr); if(m) { schedStr = schedStr.substr(0, m.index).trim(); - + if('@watch:' === m[1]) { schedule.watchFile = m[2]; } @@ -69,15 +69,15 @@ class ScheduledEvent { const sched = later.parse.text(schedStr); if(-1 === sched.error) { schedule.sched = sched; - } + } } - + // return undefined if we couldn't parse out anything useful if(!_.isEmpty(schedule)) { return schedule; } } - + parseActionSpec(actionSpec) { if(actionSpec) { if('@' === actionSpec[0]) { @@ -86,7 +86,7 @@ class ScheduledEvent { if(m[2].indexOf(':') > -1) { const parts = m[2].split(':'); return { - type : m[1], + type : m[1], location : parts[0], what : parts[1], }; @@ -98,12 +98,12 @@ class ScheduledEvent { } } } else { - return { + return { type : 'execute', what : actionSpec, }; - } - } + } + } } executeAction(reason, cb) { @@ -119,14 +119,14 @@ class ScheduledEvent { { error : err.toString(), eventName : this.name, action : this.action }, 'Error performing scheduled event action'); } - + return cb(err); }); } catch(e) { Log.warn( { error : e.toString(), eventName : this.name, action : this.action }, 'Failed to perform scheduled event action'); - + return cb(e); } } else if('execute' === this.action.type) { @@ -135,18 +135,18 @@ class ScheduledEvent { name : this.name, cols : 80, rows : 24, - env : process.env, + env : process.env, }; const proc = pty.spawn(this.action.what, this.action.args, opts); - proc.once('exit', exitCode => { + proc.once('exit', exitCode => { if(exitCode) { Log.warn( { eventName : this.name, action : this.action, exitCode : exitCode }, 'Bad exit code while performing scheduled event action'); } - return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); + return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); }); } } @@ -154,58 +154,58 @@ class ScheduledEvent { function EventSchedulerModule(options) { PluginModule.call(this, options); - + if(_.has(Config, 'eventScheduler')) { this.moduleConfig = Config.eventScheduler; } - + const self = this; this.runningActions = new Set(); - + this.performAction = function(schedEvent, reason) { if(self.runningActions.has(schedEvent.name)) { return; // already running - } - + } + self.runningActions.add(schedEvent.name); schedEvent.executeAction(reason, () => { self.runningActions.delete(schedEvent.name); - }); + }); }; } // convienence static method for direct load + start EventSchedulerModule.loadAndStart = function(cb) { const loadModuleEx = require('./module_util.js').loadModuleEx; - + const loadOpts = { name : path.basename(__filename, '.js'), path : __dirname, }; - + loadModuleEx(loadOpts, (err, mod) => { if(err) { return cb(err); } - + const modInst = new mod.getModule(); modInst.startup( err => { return cb(err, modInst); - }); + }); }); }; EventSchedulerModule.prototype.startup = function(cb) { - + this.eventTimers = []; const self = this; - + if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { const events = Object.keys(this.moduleConfig.events).map( name => { return new ScheduledEvent(this.moduleConfig.events, name); }); - + events.forEach( schedEvent => { if(!schedEvent.isValid) { Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); @@ -213,7 +213,7 @@ EventSchedulerModule.prototype.startup = function(cb) { } Log.debug( - { + { eventName : schedEvent.name, schedule : this.moduleConfig.events[schedEvent.name].schedule, action : schedEvent.action, @@ -222,9 +222,9 @@ EventSchedulerModule.prototype.startup = function(cb) { 'Scheduled event loaded' ); - if(schedEvent.schedule.sched) { + if(schedEvent.schedule.sched) { this.eventTimers.push(later.setInterval( () => { - self.performAction(schedEvent, 'Schedule'); + self.performAction(schedEvent, 'Schedule'); }, schedEvent.schedule.sched)); } @@ -255,7 +255,7 @@ EventSchedulerModule.prototype.startup = function(cb) { } }); } - + cb(null); }; @@ -263,6 +263,6 @@ EventSchedulerModule.prototype.shutdown = function(cb) { if(this.eventTimers) { this.eventTimers.forEach( et => et.clear() ); } - + cb(null); }; diff --git a/core/exodus.js b/core/exodus.js index e77183ee..8b3c7548 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -23,7 +23,7 @@ const SSHClient = require('ssh2').Client; /* Configuration block: - + someDoor: { module: exodus config: { @@ -61,7 +61,7 @@ exports.getModule = class ExodusModule extends MenuModule { 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.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; diff --git a/core/file_area_filter_edit.js b/core/file_area_filter_edit.js index 4a53096c..cc4c22c7 100644 --- a/core/file_area_filter_edit.js +++ b/core/file_area_filter_edit.js @@ -65,7 +65,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { prevFilter : (formData, extraArgs, cb) => { this.currentFilterIndex -= 1; if(this.currentFilterIndex < 0) { - this.currentFilterIndex = this.filtersArray.length - 1; + this.currentFilterIndex = this.filtersArray.length - 1; } this.loadDataForFilter(this.currentFilterIndex); return cb(null); @@ -116,21 +116,21 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { if(newActive) { filters.setActive(newActive.uuid); } else { - // nothing to set active to + // nothing to set active to this.client.user.removeProperty('file_base_filter_active_uuid'); } } // update UI this.updateActiveLabel(); - + if(this.filtersArray.length > 0) { this.loadDataForFilter(this.currentFilterIndex); } else { this.clearForm(); } return cb(null); - }); + }); }, viewValidationListener : (err, cb) => { @@ -161,7 +161,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } } } - + mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { @@ -178,7 +178,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { }, function populateAreas(callback) { self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); - + const areasView = vc.getView(MciViewIds.editor.area); if(areasView) { areasView.setItems( self.availAreas.map( a => a.name ) ); @@ -194,7 +194,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { return cb(err); } ); - }); + }); } getCurrentFilter() { @@ -212,7 +212,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { const activeFilter = FileBaseFilters.getActiveFilter(this.client); if(activeFilter) { const activeFormat = this.menuConfig.config.activeFormat || '{name}'; - this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); + this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); } } @@ -256,7 +256,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setAreaIndexFromCurrentFilter() { let index; - const filter = this.getCurrentFilter(); + const filter = this.getCurrentFilter(); if(filter) { // special treatment: areaTag saved as blank ("") if -ALL- index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; @@ -295,7 +295,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setFilterValuesFromFormData(filter, formData) { filter.name = formData.value.name; filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); - filter.terms = formData.value.searchTerms; + filter.terms = formData.value.searchTerms; filter.tags = formData.value.tags; filter.order = this.getOrderBy(formData.value.orderByIndex); filter.sort = this.getSortBy(formData.value.sortByIndex); @@ -304,7 +304,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { saveCurrentFilter(formData, cb) { const filters = new FileBaseFilters(this.client); const selectedFilter = this.filtersArray[this.currentFilterIndex]; - + if(selectedFilter) { // *update* currently selected filter this.setFilterValuesFromFormData(selectedFilter, formData); @@ -316,11 +316,11 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { // set current to what we just saved newFilter.uuid = filters.add(newFilter); - + // add to our array (at current index position) this.filtersArray[this.currentFilterIndex] = newFilter; } - + return filters.persist(cb); } @@ -334,6 +334,6 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { this.setAreaIndexFromCurrentFilter(); this.setSortByFromCurrentFilter(); this.setOrderByFromCurrentFilter(); - } + } } }; diff --git a/core/file_area_list.js b/core/file_area_list.js index 3bcfd7c2..87e794cb 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -96,7 +96,7 @@ exports.getModule = class FileAreaList extends MenuModule { } this.menuMethods = { - nextFile : (formData, extraArgs, cb) => { + nextFile : (formData, extraArgs, cb) => { if(this.fileListPosition + 1 < this.fileList.length) { this.fileListPosition += 1; @@ -131,7 +131,7 @@ exports.getModule = class FileAreaList extends MenuModule { toggleQueue : (formData, extraArgs, cb) => { this.dlQueue.toggle(this.currentFileEntry); this.updateQueueIndicator(); - return cb(null); + return cb(null); }, showWebDownloadLink : (formData, extraArgs, cb) => { return this.fetchAndDisplayWebDownloadLink(cb); @@ -217,7 +217,7 @@ exports.getModule = class FileAreaList extends MenuModule { const hashTagsSep = config.hashTagsSep || ', '; const isQueuedIndicator = config.isQueuedIndicator || 'Y'; const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; - + const entryInfo = currEntry.entryInfo = { fileId : currEntry.fileId, areaTag : currEntry.areaTag, @@ -232,7 +232,7 @@ exports.getModule = class FileAreaList extends MenuModule { hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, webDlLink : '', // :TODO: fetch web any existing web d/l link - webDlExpire : '', // :TODO: fetch web d/l link expire time + webDlExpire : '', // :TODO: fetch web d/l link expire time }; // @@ -257,7 +257,7 @@ exports.getModule = class FileAreaList extends MenuModule { // create a rating string, e.g. "**---" const userRatingTicked = config.userRatingTicked || '*'; - const userRatingUnticked = config.userRatingUnticked || ''; + const userRatingUnticked = config.userRatingUnticked || ''; entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); if(entryInfo.userRating < 5) { @@ -270,7 +270,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(ErrNotEnabled === err.reasonCode) { entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; } else { - entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; + entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; } } else { const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; @@ -339,10 +339,10 @@ exports.getModule = class FileAreaList extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } - + self.viewControllers[name].setFocus(true); return callback(null); - + }, ], err => { @@ -357,7 +357,7 @@ exports.getModule = class FileAreaList extends MenuModule { async.series( [ function fetchEntryData(callback) { - if(self.fileList) { + if(self.fileList) { return callback(null); } return self.loadFileIds(false, callback); // false=do not force @@ -371,14 +371,14 @@ exports.getModule = class FileAreaList extends MenuModule { function prepArtAndViewController(callback) { return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); }, - function loadCurrentFileInfo(callback) { + function loadCurrentFileInfo(callback) { self.currentFileEntry = new FileEntry(); self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { if(err) { return callback(err); } - + return self.populateCurrentEntryInfo(callback); }); }, @@ -422,7 +422,7 @@ exports.getModule = class FileAreaList extends MenuModule { return callback(null); } ], - err => { + err => { if(cb) { return cb(err); } @@ -448,7 +448,7 @@ exports.getModule = class FileAreaList extends MenuModule { function listenNavChanges(callback) { const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu); navMenu.setFocusItemIndex(0); - + navMenu.on('index update', index => { const sectionName = { 0 : 'general', @@ -481,7 +481,7 @@ exports.getModule = class FileAreaList extends MenuModule { } ); } - + fetchAndDisplayWebDownloadLink(cb) { const self = this; @@ -492,11 +492,11 @@ exports.getModule = class FileAreaList extends MenuModule { if(self.currentFileEntry.webDlExpireTime < moment()) { return callback(null); } - + const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); FileAreaWeb.createAndServeTempDownload( - self.client, + self.client, self.currentFileEntry, { expireTime : expireTime }, (err, url) => { @@ -517,8 +517,8 @@ exports.getModule = class FileAreaList extends MenuModule { }, function updateActiveViews(callback) { self.updateCustomViewTextsWithFilter( - 'browse', - MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, + 'browse', + MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, { filter : [ '{webDlLink}', '{webDlExpire}' ] } ); return callback(null); @@ -527,7 +527,7 @@ exports.getModule = class FileAreaList extends MenuModule { err => { return cb(err); } - ); + ); } updateQueueIndicator() { @@ -535,8 +535,8 @@ exports.getModule = class FileAreaList extends MenuModule { const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; this.currentFileEntry.entryInfo.isQueued = stringFormat( - this.dlQueue.isQueued(this.currentFileEntry) ? - isQueuedIndicator : + this.dlQueue.isQueued(this.currentFileEntry) ? + isQueuedIndicator : isNotQueuedIndicator ); @@ -558,7 +558,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(!areaInfo) { return cb(Errors.Invalid('Invalid area tag')); } - + const filePath = this.currentFileEntry.filePath; const archiveUtil = ArchiveUtil.getInstance(); @@ -574,7 +574,7 @@ exports.getModule = class FileAreaList extends MenuModule { populateFileListing() { const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); - + if(this.currentFileEntry.entryInfo.archiveType) { this.cacheArchiveEntries( (err, cacheStatus) => { if(err) { @@ -586,7 +586,7 @@ exports.getModule = class FileAreaList extends MenuModule { if('re-cached' === cacheStatus) { const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; - + fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) ); fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) ); @@ -594,7 +594,7 @@ exports.getModule = class FileAreaList extends MenuModule { } }); } else { - fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] ); + fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] ); } } @@ -608,7 +608,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(self.lastDetailsViewController) { self.lastDetailsViewController.detachClientEvents(); } - return callback(null); + return callback(null); }, function prepArtAndViewController(callback) { @@ -616,7 +616,7 @@ exports.getModule = class FileAreaList extends MenuModule { self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1)); } - gotoTopPos(); + gotoTopPos(); if(clearArea) { self.client.term.rawWrite(ansi.reset()); diff --git a/core/file_area_web.js b/core/file_area_web.js index b12f4f7d..b8a630fc 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -59,7 +59,7 @@ class FileAreaWebAccess { return callback(null); // not enabled, but no error } } - ], + ], err => { return cb(err); } @@ -193,7 +193,7 @@ class FileAreaWebAccess { getExistingTempDownloadServeItem(client, fileEntry, cb) { if(!this.isEnabled()) { return cb(notEnabledError()); - } + } const hashId = this.getSingleFileHashId(client, fileEntry); this.loadServedHashId(hashId, (err, servedItem) => { @@ -201,10 +201,10 @@ class FileAreaWebAccess { return cb(err); } - servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); + servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); return cb(null, servedItem); - }); + }); } _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { @@ -219,7 +219,7 @@ class FileAreaWebAccess { } this.scheduleExpire(hashId, expireTime); - + return cb(null); } ); @@ -476,7 +476,7 @@ class FileAreaWebAccess { StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes); StatLog.incrementSystemStat('dl_total_count', 1); StatLog.incrementSystemStat('dl_total_bytes', dlBytes); - + return callback(null); } ], diff --git a/core/file_base_area.js b/core/file_base_area.js index f3845cc1..6c7318d6 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -61,8 +61,8 @@ function getAvailableFileAreas(client, options) { // perform ACS check per conf & omit internal if desired const allAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); - - return _.omitBy(allAreas, areaInfo => { + + return _.omitBy(allAreas, areaInfo => { if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { return true; } @@ -102,7 +102,7 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { defaultArea = _.findKey(Config.fileBase.areas, (area, areaTag) => { return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area)); }); - + return defaultArea; } @@ -110,7 +110,7 @@ function getFileAreaByTag(areaTag) { const areaInfo = Config.fileBase.areas[areaTag]; if(areaInfo) { areaInfo.areaTag = areaTag; // convienence! - areaInfo.storage = getAreaStorageLocations(areaInfo); + areaInfo.storage = getAreaStorageLocations(areaInfo); return areaInfo; } } @@ -165,13 +165,13 @@ function getAreaDefaultStorageDirectory(areaInfo) { } function getAreaStorageLocations(areaInfo) { - - const storageTags = Array.isArray(areaInfo.storageTags) ? - areaInfo.storageTags : + + const storageTags = Array.isArray(areaInfo.storageTags) ? + areaInfo.storageTags : [ areaInfo.storageTags || '' ]; const avail = Config.fileBase.storageTags; - + return _.compact(storageTags.map(storageTag => { if(avail[storageTag]) { return { @@ -230,7 +230,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); function getMatch(input) { - if(input) { + if(input) { let m; for(let i = 0; i < patterns.length; ++i) { m = patterns[i].exec(input); @@ -249,7 +249,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { // const maxYear = moment().add(2, 'year').year(); const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); - + if(match && match[1]) { let year; if(2 === match[1].length) { @@ -316,7 +316,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { if(err) { return callback(err); - } + } const descFiles = { desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, @@ -327,7 +327,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { }); }); }, - function readDescFiles(descFiles, callback) { + function readDescFiles(descFiles, callback) { async.each(Object.keys(descFiles), (descType, next) => { const path = descFiles[descType]; if(!path) { @@ -341,7 +341,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { // skip entries that are too large const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; - + if(Config.fileBase[maxFileSizeKey] && stats.size > Config.fileBase[maxFileSizeKey]) { logDebug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); return next(null); @@ -353,7 +353,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } // - // Assume FILE_ID.DIZ, NFO files, etc. are CP437. + // Assume FILE_ID.DIZ, NFO files, etc. are CP437. // // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); @@ -389,10 +389,10 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries } const archiveUtil = ArchiveUtil.getInstance(); - + // ensure we only extract one - there should only be one anyway -- we also just need the fileName const extractList = archiveEntries.slice(0, 1).map(entry => entry.fileName); - + archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { if(err) { return callback(err); @@ -540,7 +540,7 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { }); }, () => { return cb(null); - }); + }); } function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb) { @@ -586,10 +586,6 @@ function addNewFileEntry(fileEntry, filePath, cb) { ); } -function updateFileEntry(fileEntry, filePath, cb) { - -} - const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ]; function scanFile(filePath, options, iterator, cb) { @@ -664,7 +660,7 @@ function scanFile(filePath, options, iterator, cb) { return callIter(callback); }); }, - function processPhysicalFileGeneric(callback) { + function processPhysicalFileGeneric(callback) { stepInfo.bytesProcessed = 0; const hashes = {}; @@ -690,7 +686,7 @@ function scanFile(filePath, options, iterator, cb) { stream.on('data', data => { stream.pause(); // until iterator compeltes - stepInfo.bytesProcessed += data.length; + stepInfo.bytesProcessed += data.length; stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); // @@ -710,13 +706,13 @@ function scanFile(filePath, options, iterator, cb) { updateHashes(data); }); - } + } }); stream.on('end', () => { fileEntry.meta.byte_size = stepInfo.bytesProcessed; - async.each(hashesToCalc, (hashName, nextHash) => { + async.each(hashesToCalc, (hashName, nextHash) => { if('sha256' === hashName) { stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); } else if('sha1' === hashName || 'md5' === hashName) { @@ -747,7 +743,9 @@ function scanFile(filePath, options, iterator, cb) { populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { if(err) { populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - // :TODO: log err + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } return callback(null); // ignore err }); } else { @@ -756,7 +754,9 @@ function scanFile(filePath, options, iterator, cb) { }); } else { populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - // :TODO: log err + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } return callback(null); // ignore err }); } @@ -773,7 +773,7 @@ function scanFile(filePath, options, iterator, cb) { return callback(null, dupeEntries); }); } - ], + ], (err, dupeEntries) => { if(err) { return cb(err); @@ -858,12 +858,12 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { // :TODO: Look @ db entries for area that were *not* processed above return callback(null); } - ], + ], err => { return nextLocation(err); } ); - }, + }, err => { return cb(err); }); @@ -874,14 +874,14 @@ function getDescFromFileName(fileName) { const ext = paths.extname(fileName); const name = paths.basename(fileName, ext); - return _.upperFirst(name.replace(/[\-_.+]/g, ' ').replace(/\s+/g, ' ')); + return _.upperFirst(name.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' ')); } // // Return an object of stats about an area(s) // // { -// +// // totalFiles : , // totalBytes : , // areas : { @@ -892,7 +892,7 @@ function getDescFromFileName(fileName) { // } // } // -function getAreaStats(cb) { +function getAreaStats(cb) { FileDb.all( `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size FROM file f, file_meta m @@ -928,9 +928,9 @@ function getAreaStats(cb) { // method exposed for event scheduler function updateAreaStatsScheduledEvent(args, cb) { - getAreaStats( (err, stats) => { + getAreaStats( (err, stats) => { if(!err) { - StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); } return cb(err); diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index 5ec266fd..87dfe9f4 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -38,7 +38,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { const menuOpts = { extraArgs : { - filterCriteria : filterCriteria, + filterCriteria : filterCriteria, }, menuFlags : [ 'popParent' ], }; diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index 7444af56..88ed2ddd 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -59,8 +59,6 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); }, - viewItemInfo : (formData, extraArgs, cb) => { - }, removeItem : (formData, extraArgs, cb) => { const selectedItem = this.dlQueue.items[formData.value.queueItem]; if(!selectedItem) { @@ -74,7 +72,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); - + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView('all', cb); } @@ -230,10 +228,10 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } - + self.viewControllers[name].setFocus(true); return callback(null); - + }, ], err => { diff --git a/core/file_base_filter.js b/core/file_base_filter.js index 320d36d3..d8b566b7 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -8,7 +8,7 @@ const uuidV4 = require('uuid/v4'); module.exports = class FileBaseFilters { constructor(client) { this.client = client; - + this.load(); } @@ -25,7 +25,7 @@ module.exports = class FileBaseFilters { 'est_release_year', 'byte_size', 'file_name', - ]; + ]; } toArray() { @@ -40,11 +40,11 @@ module.exports = class FileBaseFilters { add(filterInfo) { const filterUuid = uuidV4(); - + filterInfo.tags = this.cleanTags(filterInfo.tags); - + this.filters[filterUuid] = filterInfo; - + return filterUuid; } @@ -94,18 +94,18 @@ module.exports = class FileBaseFilters { } cleanTags(tags) { - return tags.toLowerCase().replace(/,?\s+|\,/g, ' ').trim(); + return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim(); } setActive(filterUuid) { const activeFilter = this.get(filterUuid); - + if(activeFilter) { this.activeFilter = activeFilter; this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid); return true; } - + return false; } diff --git a/core/file_base_search.js b/core/file_base_search.js index 27656123..06dac204 100644 --- a/core/file_base_search.js +++ b/core/file_base_search.js @@ -110,7 +110,7 @@ exports.getModule = class FileBaseSearch extends MenuModule { const menuOpts = { extraArgs : { - filterCriteria : filterCriteria, + filterCriteria : filterCriteria, }, menuFlags : [ 'popParent' ], }; diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index dea7c5a8..13b7de33 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -32,13 +32,13 @@ const MciViewIds = { queueManager : { queue : 1, navMenu : 2, - + customRangeStart : 10, } }; exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { - + constructor(options) { super(options); @@ -58,7 +58,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); - + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView('all', cb); }, @@ -109,7 +109,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { displayFileInfoForFileEntry(fileEntry) { this.updateCustomViewTextsWithFilter( - 'queueManager', + 'queueManager', MciViewIds.queueManager.customRangeStart, fileEntry, { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... ); @@ -142,7 +142,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); FileAreaWeb.createAndServeTempBatchDownload( - this.client, + this.client, this.dlQueue.items, { expireTime : expireTime @@ -162,7 +162,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { this.updateCustomViewTextsWithFilter( 'queueManager', - MciViewIds.queueManager.customRangeStart, + MciViewIds.queueManager.customRangeStart, formatObj, { filter : Object.keys(formatObj).map(k => '{' + k + '}' ) } ); @@ -187,13 +187,13 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { if(err) { if(ErrNotEnabled === err.reasonCode) { - return nextFileEntry(err); // we should have caught this prior + return nextFileEntry(err); // we should have caught this prior } const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); - + FileAreaWeb.createAndServeTempDownload( - self.client, + self.client, fileEntry, { expireTime : expireTime }, (err, url) => { @@ -202,13 +202,13 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { } fileEntry.webDlLinkRaw = url; - fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); return nextFileEntry(null); } ); - } else { + } else { fileEntry.webDlLinkRaw = serveItem.url; fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); @@ -272,10 +272,10 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } - + self.viewControllers[name].setFocus(true); return callback(null); - + }, ], err => { @@ -284,4 +284,3 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { ); } }; - \ No newline at end of file diff --git a/core/file_entry.js b/core/file_entry.js index 8bf7a69d..861b9d79 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -15,7 +15,7 @@ const { unlink, readFile } = require('graceful-fs'); const crypto = require('crypto'); const moment = require('moment'); -const FILE_TABLE_MEMBERS = [ +const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', 'desc', 'desc_long', 'upload_timestamp' ]; @@ -47,7 +47,7 @@ module.exports = class FileEntry { // values we always want dl_count : 0, }; - + this.hashTags = options.hashTags || new Set(); this.fileName = options.fileName; this.storageTag = options.storageTag; @@ -173,7 +173,7 @@ module.exports = class FileEntry { async.each(Object.keys(self.meta), (n, next) => { const v = self.meta[n]; return FileEntry.persistMetaValue(self.fileId, n, v, trans, next); - }, + }, err => { return callback(err, trans); }); @@ -185,7 +185,7 @@ module.exports = class FileEntry { }, err => { return callback(err, trans); - }); + }); } ], (err, trans) => { @@ -203,10 +203,10 @@ module.exports = class FileEntry { static getAreaStorageDirectoryByTag(storageTag) { const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]); - + // absolute paths as-is if(storageLocation && '/' === storageLocation.charAt(0)) { - return storageLocation; + return storageLocation; } // relative to |areaStoragePrefix| @@ -283,7 +283,7 @@ module.exports = class FileEntry { transOrDb.serialize( () => { transOrDb.run( `INSERT OR IGNORE INTO hash_tag (hash_tag) - VALUES (?);`, + VALUES (?);`, [ hashTag ] ); @@ -321,7 +321,7 @@ module.exports = class FileEntry { err => { return cb(err); } - ); + ); } loadRating(cb) { @@ -352,7 +352,7 @@ module.exports = class FileEntry { } static get WellKnownMetaValues() { - return Object.keys(FILE_WELL_KNOWN_META); + return Object.keys(FILE_WELL_KNOWN_META); } static findFileBySha(sha, cb) { @@ -469,7 +469,7 @@ module.exports = class FileEntry { sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; } else { - sql = + sql = `SELECT DISTINCT f.file_id, f.${filter.sort} FROM file f`; @@ -531,7 +531,7 @@ module.exports = class FileEntry { )` ); } - + if(filter.tags && filter.tags.length > 0) { // build list of quoted tags; filter.tags comes in as a space and/or comma separated values const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${tag}"` ).join(','); @@ -617,7 +617,7 @@ module.exports = class FileEntry { const srcPath = srcFileEntry.filePath; const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); - + if(!dstDir) { return cb(Errors.Invalid('Invalid storage tag')); } diff --git a/core/file_transfer.js b/core/file_transfer.js index 4e81bf72..bb3d362c 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -65,7 +65,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } if(options.extraArgs.sendQueue) { - this.sendQueue = options.extraArgs.sendQueue; + this.sendQueue = options.extraArgs.sendQueue; } if(options.extraArgs.recvFileName) { @@ -107,7 +107,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return { path : item }; } else { return item; - } + } }); this.sentFileIds = []; @@ -137,7 +137,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.sendQueue.forEach(f => { f.sent = true; sentFiles.push(f.path); - + }); this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); @@ -160,7 +160,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.sendQueue.forEach(f => { f.sent = true; sentFiles.push(f.path); - + }); this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); @@ -180,16 +180,16 @@ exports.getModule = class TransferFileModule extends MenuModule { } return next(err); }); - }, err => { + }, err => { return cb(err); }); - } + } } */ moveFileWithCollisionHandling(src, dst, cb) { // - // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. + // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // in the case of collisions. // const dstPath = paths.dirname(dst); @@ -283,7 +283,7 @@ exports.getModule = class TransferFileModule extends MenuModule { }); }, () => { return cb(null); - }); + }); }); } }); @@ -309,7 +309,7 @@ exports.getModule = class TransferFileModule extends MenuModule { temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { if(err) { - return callback(err); // failed to create it + return callback(err); // failed to create it } fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); @@ -334,7 +334,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return callback(null, args); } - ], + ], (err, args) => { return cb(err, args); } @@ -364,7 +364,7 @@ exports.getModule = class TransferFileModule extends MenuModule { const externalProc = pty.spawn(cmd, args, { cols : this.client.term.termWidth, rows : this.client.term.termHeight, - cwd : this.recvDirectory, + cwd : this.recvDirectory, }); this.client.setTemporaryDirectDataHandler(data => { @@ -376,7 +376,7 @@ exports.getModule = class TransferFileModule extends MenuModule { externalProc.write(data); } }); - + externalProc.on('data', data => { // needed for things like sz/rz if(external.escapeTelnet) { @@ -393,12 +393,12 @@ exports.getModule = class TransferFileModule extends MenuModule { externalProc.once('exit', (exitCode) => { this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); - + this.restorePipeAfterExternalProc(); externalProc.removeAllListeners(); return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null); - }); + }); } executeExternalProtocolHandlerForSend(filePaths, cb) { @@ -413,7 +413,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.executeExternalProtocolHandler(args, err => { return cb(err); - }); + }); }); } @@ -434,7 +434,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return { sentFileIds : this.sentFileIds }; } else { return { recvFilePaths : this.recvFilePaths }; - } + } } updateSendStats(cb) { @@ -478,11 +478,11 @@ exports.getModule = class TransferFileModule extends MenuModule { fileIds.forEach(fileId => { FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); }); - + return cb(null); }); } - + updateRecvStats(cb) { let uploadBytes = 0; let uploadCount = 0; @@ -519,7 +519,7 @@ exports.getModule = class TransferFileModule extends MenuModule { function validateConfig(callback) { if(self.isSending()) { if(!Array.isArray(self.sendQueue)) { - self.sendQueue = [ self.sendQueue ]; + self.sendQueue = [ self.sendQueue ]; } } @@ -555,7 +555,7 @@ exports.getModule = class TransferFileModule extends MenuModule { }); } }, - function cleanupTempFiles(callback) { + function cleanupTempFiles(callback) { temptmp.cleanup( paths => { Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); }); diff --git a/core/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js index f1b3dbed..299c8af2 100644 --- a/core/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -64,7 +64,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb); } else { return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb); - } + } }, }; } @@ -118,7 +118,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { protListView.redraw(); - return callback(null); + return callback(null); } ], err => { diff --git a/core/file_util.js b/core/file_util.js index 9452b23f..0f91e71a 100644 --- a/core/file_util.js +++ b/core/file_util.js @@ -66,11 +66,11 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { (err, finalPath) => { return cb(err, finalPath); } - ); + ); } // -// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. +// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // in the case of collisions. // function moveFileWithCollisionHandling(src, dst, cb) { diff --git a/core/fnv1a.js b/core/fnv1a.js index f7714936..743986d6 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -7,7 +7,7 @@ let _ = require('lodash'); module.exports = class FNV1a { constructor(data) { this.hash = 0x811c9dc5; - + if(!_.isUndefined(data)) { this.update(data); } @@ -17,7 +17,7 @@ module.exports = class FNV1a { if(_.isNumber(data)) { data = data.toString(); } - + if(_.isString(data)) { data = new Buffer(data); } @@ -28,8 +28,8 @@ module.exports = class FNV1a { for(let b of data) { this.hash = this.hash ^ b; - this.hash += - (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + + this.hash += + (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + (this.hash << 4) + (this.hash << 1); } @@ -46,5 +46,5 @@ module.exports = class FNV1a { get value() { return this.hash & 0xffffffff; } -} +}; diff --git a/core/fse.js b/core/fse.js index b266a9c9..02134f31 100644 --- a/core/fse.js +++ b/core/fse.js @@ -39,7 +39,7 @@ exports.moduleInfo = { TL2 - To TL3 - Subject TL4 - Area name - + TL5 - Date/Time (TODO: format) TL6 - Message number TL7 - Mesage total (in area) @@ -50,7 +50,7 @@ exports.moduleInfo = { TL12 - User1 TL13 - User2 - + Footer - Viewing HM1 - Menu (prev/next/etc.) @@ -61,14 +61,14 @@ exports.moduleInfo = { TL12 - User1 (fmt message object) TL13 - User2 - + */ const MciCodeIds = { ViewModeHeader : { From : 1, To : 2, Subject : 3, - + DateTime : 5, MsgNum : 6, MsgTotal : 7, @@ -78,9 +78,9 @@ const MciCodeIds = { ReplyToMsgID : 11, // :TODO: ConfName - + }, - + ViewModeFooter : { MsgNum : 6, MsgTotal : 7, @@ -90,7 +90,7 @@ const MciCodeIds = { From : 1, To : 2, Subject : 3, - + ErrorMsg : 13, }, }; @@ -116,12 +116,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // toUserId // this.editorType = config.editorType; - this.editorMode = config.editorMode; - + this.editorMode = config.editorMode; + if(config.messageAreaTag) { this.messageAreaTag = config.messageAreaTag; } - + this.messageIndex = config.messageIndex || 0; this.messageTotal = config.messageTotal || 0; this.toUserId = config.toUserId || 0; @@ -160,7 +160,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(errMsgView) { if(err) { errMsgView.setText(err.message); - + if(MciCodeIds.ViewModeHeader.Subject === err.view.getId()) { // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) } @@ -179,26 +179,24 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul self.switchFooter(function next(err) { if(err) { - // :TODO:... what now? - console.log(err) - } else { - switch(self.footerMode) { - case 'editor' : - if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { - //self.viewControllers.footerEditorMenu.setFocus(false); - self.viewControllers.footerEditorMenu.detachClientEvents(); - } - self.viewControllers.body.switchFocus(1); - self.observeEditorEvents(); - break; + return cb(err); + } - case 'editorMenu' : - self.viewControllers.body.setFocus(false); - self.viewControllers.footerEditorMenu.switchFocus(1); - break; + switch(self.footerMode) { + case 'editor' : + if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { + self.viewControllers.footerEditorMenu.detachClientEvents(); + } + self.viewControllers.body.switchFocus(1); + self.observeEditorEvents(); + break; - default : throw new Error('Unexpected mode'); - } + case 'editorMenu' : + self.viewControllers.body.setFocus(false); + self.viewControllers.footerEditorMenu.switchFocus(1); + break; + + default : throw new Error('Unexpected mode'); } return cb(null); @@ -210,9 +208,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return cb(null); }, appendQuoteEntry: function(formData, extraArgs, cb) { - // :TODO: Dont' use magic # ID's here + // :TODO: Dont' use magic # ID's here const quoteMsgView = self.viewControllers.quoteBuilder.getView(1); - + if(self.newQuoteBlock) { self.newQuoteBlock = false; @@ -220,7 +218,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul quoteMsgView.addText(self.getQuoteByHeader()); } - + const quoteText = self.viewControllers.quoteBuilder.getView(3).getItem(formData.value.quote); quoteMsgView.addText(quoteText); @@ -339,7 +337,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // // 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 + // 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' } }; @@ -351,7 +349,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return cb(null); } - + setMessage(message) { this.message = message; @@ -495,7 +493,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // :TODO: We'd like to delete up to N rows, but this does not work // in NetRunner: self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); - + self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)); } callback(null); @@ -534,7 +532,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul art[n], self.client, { font : self.menuConfig.font }, - function displayed(err, artData) { + function displayed(err) { next(err); } ); @@ -561,7 +559,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul function complete(err) { cb(err); } - ); + ); } switchFooter(cb) { @@ -645,14 +643,13 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul ], function complete(err) { if(err) { - // :TODO: This needs properly handled! - console.log(err) + self.client.log.warn( { error : err.message }, 'FSE init error'); } else { self.isReady = true; self.finishedLoading(); } } - ); + ); } createInitialViews(mciData, cb) { @@ -666,7 +663,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul menuLoadOpts.mciMap = mciData.header.mciMap; self.addViewController( - 'header', + 'header', new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) ).loadFromMenuConfig(menuLoadOpts, function headerReady(err) { callback(err); @@ -713,7 +710,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }, function setInitialData(callback) { - switch(self.editorMode) { + switch(self.editorMode) { case 'view' : if(self.message) { self.initHeaderViewMode(); @@ -726,7 +723,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } } break; - + case 'edit' : { const fromView = self.viewControllers.header.getView(1); @@ -747,9 +744,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul callback(null); }, function setInitialFocus(callback) { - + switch(self.editorMode) { - case 'edit' : + case 'edit' : self.switchToHeader(); break; @@ -763,10 +760,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } ], function complete(err) { - if(err) { - console.error(err) - } - cb(err); + return cb(err); } ); } @@ -774,7 +768,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul mciReadyHandler(mciData, cb) { this.createInitialViews(mciData, err => { - // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in + // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in // place - if this is for existing usernames else validate spec /* @@ -787,7 +781,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul view.clearText(); self.viewControllers.headers.switchFocus(2); } - }); + }); } });*/ @@ -813,7 +807,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(modeView) { this.client.term.rawWrite(ansi.savePos()); modeView.setText('insert' === mode ? 'INS' : 'OVR'); - this.client.term.rawWrite(ansi.restorePos()); + this.client.term.rawWrite(ansi.restorePos()); } } } @@ -824,7 +818,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul initHeaderViewMode() { assert(_.isObject(this.message)); - + this.setHeaderText(MciCodeIds.ViewModeHeader.From, this.message.fromUserName); this.setHeaderText(MciCodeIds.ViewModeHeader.To, this.message.toUserName); this.setHeaderText(MciCodeIds.ViewModeHeader.Subject, this.message.subject); @@ -881,7 +875,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // this.newQuoteBlock = true; const self = this; - + async.waterfall( [ function clearAndDisplayArt(callback) { @@ -892,23 +886,23 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul self.client.term.rawWrite( ansi.goto(self.header.height + 1, 1) + ansi.deleteLine(24 - self.header.height)); - + theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) { callback(err, artData); }); }, function createViewsIfNecessary(artData, callback) { var formId = self.getFormId('quoteBuilder'); - + if(_.isUndefined(self.viewControllers.quoteBuilder)) { var menuLoadOpts = { callingMenu : self, formId : formId, - mciMap : artData.mciMap, + mciMap : artData.mciMap, }; - + self.addViewController( - 'quoteBuilder', + 'quoteBuilder', new ViewController( { client : self.client, formId : formId } ) ).loadFromMenuConfig(menuLoadOpts, function quoteViewsReady(err) { callback(err); @@ -954,10 +948,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul ], function complete(err) { if(err) { - console.log(err) // :TODO: needs real impl. + self.client.log.warn( { error : err.message }, 'Error displaying quote builder'); } } - ); + ); } observeEditorEvents() { @@ -1004,22 +998,22 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul var body = this.viewControllers.body.getView(1); body.redraw(); this.viewControllers.body.switchFocus(1); - + // :TODO: create method (DRY) - + this.updateTextEditMode(body.getTextEditMode()); this.updateEditModePosition(body.getEditPosition()); this.observeEditorEvents(); } - + quoteBuilderFinalize() { // :TODO: fix magic #'s const quoteMsgView = this.viewControllers.quoteBuilder.getView(1); const msgView = this.viewControllers.body.getView(1); - + let quoteLines = quoteMsgView.getData().trim(); - + if(quoteLines.length > 0) { if(this.replyIsAnsi) { const bodyMessageView = this.viewControllers.body.getView(1); @@ -1027,7 +1021,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } msgView.addText(`${quoteLines}\n\n`); } - + quoteMsgView.setText(''); this.footerMode = 'editor'; @@ -1040,14 +1034,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul getQuoteByHeader() { let quoteFormat = this.menuConfig.config.quoteFormats; - if(Array.isArray(quoteFormat)) { + if(Array.isArray(quoteFormat)) { quoteFormat = quoteFormat[ Math.floor(Math.random() * quoteFormat.length) ]; } else if(!_.isString(quoteFormat)) { quoteFormat = 'On {dateTime} {userName} said...'; } - const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); - return stringFormat(quoteFormat, { + const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); + return stringFormat(quoteFormat, { dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), userName : this.replyToMessage.fromUserName, }); diff --git a/core/ftn_address.js b/core/ftn_address.js index f0936e1d..6b1e57e0 100644 --- a/core/ftn_address.js +++ b/core/ftn_address.js @@ -3,8 +3,8 @@ const _ = require('lodash'); -const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-\.]+)?$/i; -const FTN_PATTERN_REGEXP = /^([0-9\*]+:)?([0-9\*]+)(\/[0-9\*]+)?(\.[0-9\*]+)?(@[a-z0-9\-\.\*]+)?$/i; +const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i; +const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; module.exports = class Address { constructor(addr) { @@ -133,7 +133,7 @@ module.exports = class Address { static fromString(addrStr) { const m = FTN_ADDRESS_REGEXP.exec(addrStr); - + if(m) { // start with a 2D let addr = { @@ -165,7 +165,7 @@ module.exports = class Address { let addrStr = `${this.zone}:${this.net}`; - // allow for e.g. '4D' or 5 + // allow for e.g. '4D' or 5 const dim = parseInt(dimensions.toString()[0]); if(dim >= 3) { diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index d84b4a69..d2f003e9 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -56,7 +56,7 @@ class PacketHeader { this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap this.prodCodeHi = 0xfe; // see above - this.prodRevHi = 0; + this.prodRevHi = 0; } get origAddress() { @@ -84,9 +84,9 @@ class PacketHeader { // See FSC-48 // :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2 - /*if(address.point) { + /*if(address.point) { this.auxNet = address.origNet; - this.origNet = -1; + this.origNet = -1; } else { this.origNet = address.net; this.auxNet = 0; @@ -158,16 +158,16 @@ exports.PacketHeader = PacketHeader; // // * Type 2 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete) // * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001 -// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 +// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 // and http://ftsc.org/docs/fsc-0048.002 -// +// // Additional resources: // * Writeup on differences between type 2, 2.2, and 2+: // http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt // function Packet(options) { var self = this; - + this.options = options || {}; this.parsePacketHeader = function(packetBuffer, cb) { @@ -240,11 +240,11 @@ function Packet(options) { // // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" // - const capWordValidateSwapped = + const capWordValidateSwapped = ((packetHeader.capWordValidate & 0xff) << 8) | ((packetHeader.capWordValidate >> 8) & 0xff); - if(capWordValidateSwapped === packetHeader.capWord && + if(capWordValidateSwapped === packetHeader.capWord && 0 != packetHeader.capWord && packetHeader.capWord & 0x0001) { @@ -260,7 +260,7 @@ function Packet(options) { // :TODO: should fill bytes be 0? } } - + packetHeader.created = moment({ year : packetHeader.year, month : packetHeader.month - 1, // moment uses 0 indexed months @@ -269,36 +269,36 @@ function Packet(options) { minute : packetHeader.minute, second : packetHeader.second }); - + let ph = new PacketHeader(); _.assign(ph, packetHeader); cb(null, ph); }); }; - + this.getPacketHeaderBuffer = function(packetHeader) { let buffer = new Buffer(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); buffer.writeUInt16LE(packetHeader.year, 4); - buffer.writeUInt16LE(packetHeader.month, 6); + buffer.writeUInt16LE(packetHeader.month, 6); buffer.writeUInt16LE(packetHeader.day, 8); buffer.writeUInt16LE(packetHeader.hour, 10); buffer.writeUInt16LE(packetHeader.minute, 12); buffer.writeUInt16LE(packetHeader.second, 14); - + buffer.writeUInt16LE(packetHeader.baud, 16); buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); buffer.writeUInt16LE(packetHeader.destNet, 22); buffer.writeUInt8(packetHeader.prodCodeLo, 24); buffer.writeUInt8(packetHeader.prodRevHi, 25); - + const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); pass.copy(buffer, 26); - + buffer.writeUInt16LE(packetHeader.origZone, 34); buffer.writeUInt16LE(packetHeader.destZone, 36); buffer.writeUInt16LE(packetHeader.auxNet, 38); @@ -311,7 +311,7 @@ function Packet(options) { buffer.writeUInt16LE(packetHeader.origPoint, 50); buffer.writeUInt16LE(packetHeader.destPoint, 52); buffer.writeUInt32LE(packetHeader.prodData, 54); - + return buffer; }; @@ -321,22 +321,22 @@ function Packet(options) { buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); buffer.writeUInt16LE(packetHeader.year, 4); - buffer.writeUInt16LE(packetHeader.month, 6); + buffer.writeUInt16LE(packetHeader.month, 6); buffer.writeUInt16LE(packetHeader.day, 8); buffer.writeUInt16LE(packetHeader.hour, 10); buffer.writeUInt16LE(packetHeader.minute, 12); buffer.writeUInt16LE(packetHeader.second, 14); - + buffer.writeUInt16LE(packetHeader.baud, 16); buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); buffer.writeUInt16LE(packetHeader.destNet, 22); buffer.writeUInt8(packetHeader.prodCodeLo, 24); buffer.writeUInt8(packetHeader.prodRevHi, 25); - + const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); pass.copy(buffer, 26); - + buffer.writeUInt16LE(packetHeader.origZone, 34); buffer.writeUInt16LE(packetHeader.destZone, 36); buffer.writeUInt16LE(packetHeader.auxNet, 38); @@ -376,9 +376,9 @@ function Packet(options) { // likely need to re-decode as the specified encoding // * SAUCE is binary-ish data, so we need to inspect for it before any // decoding occurs - // + // let messageBodyData = { - message : [], + message : [], kludgeLines : {}, // KLUDGE:[value1, value2, ...] map seenBy : [], }; @@ -411,7 +411,7 @@ function Packet(options) { messageBodyData.kludgeLines[key] = value; } } - + let encoding = 'cp437'; async.series( @@ -426,12 +426,12 @@ function Packet(options) { if(!err) { // we read some SAUCE - don't re-process that portion into the body messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); -// messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); + // messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); messageBodyData.sauce = theSauce; } else { - console.log(err) + Log.warn( { error : err.message }, 'Found what looks like to be a SAUCE record, but failed to read'); } - callback(null); // failure to read SAUCE is OK + return callback(null); // failure to read SAUCE is OK }); } else { callback(null); @@ -482,7 +482,7 @@ function Packet(options) { Log.debug( { encoding : encoding, error : e.toString() }, 'Error decoding. Falling back to ASCII'); decoded = iconv.decode(messageBodyBuffer, 'ascii'); } - + const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, '')); let endOfMessage = false; @@ -491,13 +491,13 @@ function Packet(options) { messageBodyData.message.push(''); return; } - + if(line.startsWith('AREA:')) { messageBodyData.area = line.substring(line.indexOf(':') + 1).trim(); } else if(line.startsWith('--- ')) { // Tear Lines are tracked allowing for specialized display/etc. messageBodyData.tearLine = line; - } else if(/^[ ]{1,2}\* Origin\: /.test(line)) { // To spec is " * Origin: ..." + } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." messageBodyData.originLine = line; endOfMessage = true; // Anything past origin is not part of the message body } else if(line.startsWith('SEEN-BY:')) { @@ -523,7 +523,7 @@ function Packet(options) { } ); }; - + this.parsePacketMessages = function(packetBuffer, iterator, cb) { binary.parse(packetBuffer) .word16lu('messageType') @@ -540,22 +540,22 @@ function Packet(options) { .scan('message', NULL_TERM_BUFFER) .tap(function tapped(msgData) { // no arrow function; want classic this if(!msgData.messageType) { - // end marker -- no more messages + // end marker -- no more messages return cb(null); } - + if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { return cb(new Error('Unsupported message type: ' + msgData.messageType)); } - - const read = + + const read = 14 + // fixed header size msgData.modDateTime.length + 1 + msgData.toUserName.length + 1 + msgData.fromUserName.length + 1 + msgData.subject.length + 1 + msgData.message.length + 1; - + // // Convert null terminated arrays to strings // @@ -575,7 +575,7 @@ function Packet(options) { subject : convMsgData.subject, modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime), }); - + msg.meta.FtnProperty = {}; msg.meta.FtnProperty.ftn_orig_node = msgData.ftn_orig_node; msg.meta.FtnProperty.ftn_dest_node = msgData.ftn_dest_node; @@ -587,31 +587,31 @@ function Packet(options) { self.processMessageBody(msgData.message, messageBodyData => { msg.message = messageBodyData.message; msg.meta.FtnKludge = messageBodyData.kludgeLines; - + if(messageBodyData.tearLine) { msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; - + if(self.options.keepTearAndOrigin) { msg.message += `\r\n${messageBodyData.tearLine}\r\n`; } } - + if(messageBodyData.seenBy.length > 0) { msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; } - + if(messageBodyData.area) { msg.meta.FtnProperty.ftn_area = messageBodyData.area; } - + if(messageBodyData.originLine) { msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; - + if(self.options.keepTearAndOrigin) { msg.message += `${messageBodyData.originLine}\r\n`; } } - + // // If we have a UTC offset kludge (e.g. TZUTC) then update // modDateTime with it @@ -619,7 +619,7 @@ function Packet(options) { if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); } - + const nextBuf = packetBuffer.slice(read); if(nextBuf.length > 0) { let next = function(e) { @@ -629,12 +629,12 @@ function Packet(options) { self.parsePacketMessages(nextBuf, iterator, cb); } }; - + iterator('message', msg, next); } else { cb(null); } - }); + }); }); }; @@ -702,7 +702,7 @@ function Packet(options) { // // message: unbound length, NULL term'd - // + // // We need to build in various special lines - kludges, area, // seen-by, etc. // @@ -716,7 +716,7 @@ function Packet(options) { if(message.meta.FtnProperty.ftn_area) { msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) } - + // :TODO: DRY with similar function in this file! Object.keys(message.meta.FtnKludge).forEach(k => { switch(k) { @@ -731,7 +731,7 @@ function Packet(options) { break; default : - msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); + msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; } }); @@ -780,22 +780,22 @@ function Packet(options) { // 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, + null, + Buffer.concat( [ + basicHeader, + toUserNameBuf, + fromUserNameBuf, subjectBuf, - msgBodyEncoded + msgBodyEncoded ]) ); } @@ -808,7 +808,7 @@ function Packet(options) { this.writeMessage = function(message, ws, options) { let basicHeader = new Buffer(34); - + basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4); @@ -827,7 +827,7 @@ function Packet(options) { let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36); encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); - + encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36); encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); @@ -839,7 +839,7 @@ function Packet(options) { // // message: unbound length, NULL term'd - // + // // We need to build in various special lines - kludges, area, // seen-by, etc. // @@ -866,7 +866,7 @@ function Packet(options) { 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 => { switch(k) { case 'PATH' : break; // skip & save for last @@ -889,8 +889,8 @@ function Packet(options) { 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) { @@ -918,11 +918,11 @@ function Packet(options) { if(err) { return callback(err); } - + let next = function(e) { callback(e); }; - + iterator('header', header, next); }); }, @@ -934,7 +934,7 @@ function Packet(options) { } ], cb // complete - ); + ); }; } @@ -954,7 +954,7 @@ Packet.Attribute = { InTransit : 0x0020, Orphan : 0x0040, KillSent : 0x0080, - Local : 0x0100, // Message is from *this* system + Local : 0x0100, // Message is from *this* system Hold : 0x0200, Reserved0 : 0x0400, FileRequest : 0x0800, @@ -998,7 +998,7 @@ Packet.prototype.writeHeader = function(ws, packetHeader) { Packet.prototype.writeMessageEntry = function(ws, msgEntry) { ws.write(msgEntry); - return msgEntry.length; + return msgEntry.length; }; Packet.prototype.writeTerminator = function(ws) { @@ -1014,11 +1014,11 @@ Packet.prototype.writeStream = function(ws, messages, options) { if(!_.isBoolean(options.terminatePacket)) { options.terminatePacket = true; } - + if(_.isObject(options.packetHeader)) { this.writePacketHeader(options.packetHeader, ws); } - + options.encoding = options.encoding || 'utf8'; messages.forEach(msg => { @@ -1034,12 +1034,12 @@ Packet.prototype.write = function(path, packetHeader, messages, options) { if(!_.isArray(messages)) { messages = [ messages ]; } - + options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4' this.writeStream( fs.createWriteStream(path), // :TODO: specify mode/etc. messages, - { packetHeader : packetHeader, terminatePacket : true } - ); + Object.assign( { packetHeader : packetHeader, terminatePacket : true }, options) + ); }; diff --git a/core/ftn_util.js b/core/ftn_util.js index 39093fab..03040484 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -45,7 +45,7 @@ exports.getQuotePrefix = getQuotePrefix; // See list here: https://github.com/Mithgol/node-fidonet-jam -function stringToNullPaddedBuffer(s, bufLen) { +function stringToNullPaddedBuffer(s, bufLen) { let buffer = new Buffer(bufLen).fill(0x00); let enc = iconv.encode(s, 'CP437').slice(0, bufLen); for(let i = 0; i < enc.length; ++i) { @@ -56,7 +56,7 @@ function stringToNullPaddedBuffer(s, bufLen) { // // Convert a FTN style DateTime string to a Date object -// +// // :TODO: Name the next couple methods better - for FTN *packets* function getDateFromFtnDateTime(dateTime) { // @@ -103,7 +103,7 @@ function getMessageSerialNumber(messageId) { // // Return a FTS-0009.001 compliant MSGID value given a message // See http://ftsc.org/docs/fts-0009.001 -// +// // "A MSGID line consists of the string "^AMSGID:" (where ^A is a // control-A (hex 01) and the double-quotes are not part of the // string), followed by a space, the address of the originating @@ -113,9 +113,9 @@ function getMessageSerialNumber(messageId) { // ^AMSGID: origaddr serialno // // The originating address should be specified in a form that -// constitutes a valid return address for the originating network. +// constitutes a valid return address for the originating network. // If the originating address is enclosed in double-quotes, the -// entire string between the beginning and ending double-quotes is +// entire string between the beginning and ending double-quotes is // considered to be the orginating address. A double-quote character // within a quoted address is represented by by two consecutive // double-quote characters. The serial number may be any eight @@ -123,13 +123,13 @@ function getMessageSerialNumber(messageId) { // messages from a given system may have the same serial number // within a three years. The manner in which this serial number is // generated is left to the implementor." -// +// // // Examples & Implementations // // Synchronet: .@ // 2606.agora-agn_tst@46:1/142 19609217 -// +// // Mystic: // 46:3/102 46686263 // @@ -145,10 +145,10 @@ function getMessageSerialNumber(messageId) { // function getMessageIdentifier(message, address, isNetMail = false) { const addrStr = new Address(address).toString('5D'); - return isNetMail ? + return isNetMail ? `${addrStr} ${getMessageSerialNumber(message.messageId)}` : `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}` - ; + ; } // @@ -166,7 +166,7 @@ function getProductIdentifier() { } // -// Return a FRL-1004 style time zone offset for a +// Return a FRL-1004 style time zone offset for a // 'TZUTC' kludge line // // http://ftsc.org/docs/frl-1004.002 @@ -178,10 +178,10 @@ function getUTCTimeZoneOffset() { // // Get a FSC-0032 style quote prefix // http://ftsc.org/docs/fsc-0032.001 -// +// function getQuotePrefix(name) { let initials; - + const parts = name.split(' '); if(parts.length > 1) { // First & Last initials - (Bryan Ashby -> BA) @@ -199,7 +199,7 @@ function getQuotePrefix(name) { // http://ftsc.org/docs/fts-0004.001 // function getOrigin(address) { - const origin = _.has(Config, 'messageNetworks.originLine') ? + const origin = _.has(Config, 'messageNetworks.originLine') ? Config.messageNetworks.originLine : Config.general.boardName; @@ -220,16 +220,12 @@ function getVia(address) { /* FRL-1005.001 states teh following format: - ^AVia: @YYYYMMDD.HHMMSS[.Precise][.Time Zone] + ^AVia: @YYYYMMDD.HHMMSS[.Precise][.Time Zone] [Serial Number] */ const addrStr = new Address(address).toString('5D'); const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); - - const version = packageJson.version - .replace(/\-/g, '.') - .replace(/alpha/,'a') - .replace(/beta/,'b'); + const version = getCleanEnigmaVersion(); return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; } @@ -272,7 +268,7 @@ function parseAbbreviatedNetNodeList(netNodes) { const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g; let net; let m; - let results = []; + let results = []; while(null !== (m = re.exec(netNodes))) { if(m[1] && m[2]) { net = parseInt(m[1]); @@ -288,7 +284,7 @@ function parseAbbreviatedNetNodeList(netNodes) { // // Return a FTS-0004.001 SEEN-BY entry(s) that include // all pre-existing SEEN-BY entries with the addition -// of |additions|. +// of |additions|. // // See http://ftsc.org/docs/fts-0004.001 // and notes at http://ftsc.org/docs/fsc-0043.002. @@ -324,9 +320,9 @@ function getUpdatedSeenByEntries(existingEntries, additions) { if(!_.isArray(existingEntries)) { existingEntries = [ existingEntries ]; } - + if(!_.isString(additions)) { - additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); + additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); } additions = additions.sort(Address.getComparator()); @@ -361,13 +357,13 @@ const ENCODING_TO_FTS_5003_001_CHARS = { // level 1 - generally should not be used ascii : [ 'ASCII', 1 ], 'us-ascii' : [ 'ASCII', 1 ], - + // level 2 - 8 bit, ASCII based cp437 : [ 'CP437', 2 ], cp850 : [ 'CP850', 2 ], - + // level 3 - reserved - + // level 4 utf8 : [ 'UTF-8', 4 ], 'utf-8' : [ 'UTF-8', 4 ], @@ -381,7 +377,7 @@ function getCharacterSetIdentifierByEncoding(encodingName) { function getEncodingFromCharacterSetIdentifier(chrs) { const ident = chrs.split(' ')[0].toUpperCase(); - + // :TODO: fill in the rest!!! return { // level 1 @@ -399,7 +395,7 @@ function getEncodingFromCharacterSetIdentifier(chrs) { 'SWISS' : 'iso-646', 'UK' : 'iso-646', 'ISO-10' : 'iso-646-10', - + // level 2 'CP437' : 'cp437', 'CP850' : 'cp850', @@ -414,15 +410,15 @@ function getEncodingFromCharacterSetIdentifier(chrs) { 'LATIN-2' : 'iso-8859-2', 'LATIN-5' : 'iso-8859-9', 'LATIN-9' : 'iso-8859-15', - + // level 4 'UTF-8' : 'utf8', - + // deprecated stuff - 'IBMPC' : 'cp1250', // :TODO: validate + 'IBMPC' : 'cp1250', // :TODO: validate '+7_FIDO' : 'cp866', - '+7' : 'cp866', + '+7' : 'cp866', 'MAC' : 'macroman', // :TODO: validate - + }[ident]; } \ No newline at end of file diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 28f4c29d..81d477ad 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -63,7 +63,7 @@ function HorizontalMenuView(options) { } var text = strUtil.stylizeString( - item.text, + item.text, this.hasFocus && item.focused ? self.focusTextStyle : self.textStyle); var drawWidth = text.length + self.getSpacer().length * 2; // * 2 = sides @@ -72,7 +72,7 @@ function HorizontalMenuView(options) { ansi.goto(self.position.row, item.col) + (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()) + strUtil.pad(text, drawWidth, self.fillChar, 'center') - ); + ); }; } diff --git a/core/key_entry_view.js b/core/key_entry_view.js index cf1ba008..304f8ef3 100644 --- a/core/key_entry_view.js +++ b/core/key_entry_view.js @@ -44,7 +44,7 @@ module.exports = class KeyEntryView extends View { if(key && 'tab' === key.name && !this.eatTabKey) { return this.emit('action', 'next', key); } - + this.emit('action', 'accept'); // NOTE: we don't call super here. KeyEntryView is a special snowflake. } @@ -69,7 +69,7 @@ module.exports = class KeyEntryView extends View { } break; } - + super.setPropertyValue(propName, propValue); } diff --git a/core/last_callers.js b/core/last_callers.js index 3a889468..1bc1f422 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -20,7 +20,7 @@ const _ = require('lodash'); location affiliation ts - + */ exports.moduleInfo = { @@ -65,7 +65,7 @@ exports.getModule = class LastCallersModule extends MenuModule { function fetchHistory(callback) { callersView = vc.getView(MciCodeIds.CallerList); - // fetch up + // fetch up StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { loginHistory = lh; @@ -82,12 +82,12 @@ exports.getModule = class LastCallersModule extends MenuModule { loginHistory = noOpLoginHistory; } } - + // // Finally, we need to trim up the list to the needed size // loginHistory = loginHistory.slice(0, callersView.dimens.height); - + return callback(err); }); }, @@ -99,10 +99,10 @@ exports.getModule = class LastCallersModule extends MenuModule { const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; async.each( - loginHistory, + loginHistory, (item, next) => { item.userId = parseInt(item.log_value); - item.ts = moment(item.timestamp).format(dateTimeFormat); + item.ts = moment(item.timestamp).format(dateTimeFormat); User.getUserName(item.userId, (err, userName) => { if(err) { diff --git a/core/logger.js b/core/logger.js index f90aec41..1f1efde2 100644 --- a/core/logger.js +++ b/core/logger.js @@ -12,7 +12,7 @@ module.exports = class Log { static init() { const Config = require('./config.js').config; const logPath = Config.paths.logs; - + const err = this.checkLogPath(logPath); if(err) { console.error(err.message); // eslint-disable-line no-console @@ -29,9 +29,9 @@ module.exports = class Log { err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. }; - // try to remove sensitive info by default, e.g. 'password' fields + // try to remove sensitive info by default, e.g. 'password' fields [ 'formData', 'formValue' ].forEach(keyName => { - serializers[keyName] = (fd) => Log.hideSensitive(fd); + serializers[keyName] = (fd) => Log.hideSensitive(fd); }); this.log = bunyan.createLogger({ @@ -46,7 +46,7 @@ module.exports = class Log { if(!fs.statSync(logPath).isDirectory()) { return new Error(`${logPath} is not a directory`); } - + return null; } catch(e) { if('ENOENT' === e.code) { diff --git a/core/login_server_module.js b/core/login_server_module.js index 212d2e27..72958a0c 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -28,7 +28,7 @@ module.exports = class LoginServerModule extends ServerModule { } else { client.user.properties.theme_id = conf.config.preLoginTheme; } - + theme.setClientTheme(client, client.user.properties.theme_id); return cb(null); // note: currently useless to use cb here - but this may change...again... } @@ -37,7 +37,7 @@ module.exports = class LoginServerModule extends ServerModule { // // Start tracking the client. We'll assign it an ID which is // just the index in our connections array. - // + // if(_.isUndefined(client.session)) { client.session = {}; } @@ -68,7 +68,7 @@ module.exports = class LoginServerModule extends ServerModule { client.on('close', err => { const logFunc = err ? logger.log.info : logger.log.debug; logFunc( { clientId : client.session.id }, 'Connection closed'); - + clientConns.removeClient(client); }); @@ -80,7 +80,7 @@ module.exports = class LoginServerModule extends ServerModule { // likely just doesn't exist client.term.write('\nIdle timeout expired. Goodbye!\n'); client.end(); - } + } }); }); } diff --git a/core/mail_packet.js b/core/mail_packet.js index 3fb8b2d2..fbbb3e76 100644 --- a/core/mail_packet.js +++ b/core/mail_packet.js @@ -33,4 +33,4 @@ MailPacket.prototype.write = function(options) { // emits 'packet' event per packet constructed // assert(_.isArray(options.messages)); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/core/mail_util.js b/core/mail_util.js index 654b1617..4e959389 100644 --- a/core/mail_util.js +++ b/core/mail_util.js @@ -6,7 +6,7 @@ const Message = require('./message.js'); exports.getAddressedToInfo = getAddressedToInfo; -const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /* Input Output diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index f99774e6..2c0b6021 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -25,7 +25,7 @@ exports.MaskEditTextView = MaskEditTextView; // :TODO: // * Hint, e.g. YYYY/MM/DD // * Return values with literals in place -// +// function MaskEditTextView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); @@ -49,7 +49,7 @@ function MaskEditTextView(options) { this.drawText = function(s) { var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - + assert(textToDraw.length <= self.patternArray.length); // draw out the text we have so far @@ -105,7 +105,7 @@ MaskEditTextView.maskPatternCharacterRegEx = { MaskEditTextView.prototype.setText = function(text) { MaskEditTextView.super_.prototype.setText.call(this, text); - + if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() this.patternArrayPos = this.patternArray.length; } @@ -130,14 +130,14 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { this.clientBackspace(); } else { while(this.patternArrayPos > 0) { - if(_.isRegExp(this.patternArray[this.patternArrayPos])) { + if(_.isRegExp(this.patternArray[this.patternArrayPos])) { this.text = this.text.substr(0, this.text.length - 1); this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); this.clientBackspace(); break; } this.patternArrayPos--; - } + } } } @@ -162,7 +162,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { this.text += ch; this.patternArrayPos++; - while(this.patternArrayPos < this.patternArray.length && + while(this.patternArrayPos < this.patternArray.length && !_.isRegExp(this.patternArray[this.patternArrayPos])) { this.patternArrayPos++; @@ -186,11 +186,11 @@ MaskEditTextView.prototype.setPropertyValue = function(propName, value) { MaskEditTextView.prototype.getData = function() { var rawData = MaskEditTextView.super_.prototype.getData.call(this); - + if(!rawData || 0 === rawData.length) { return rawData; } - + var data = ''; assert(rawData.length <= this.patternArray.length); diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index c5b95bb3..eb783d41 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -4,13 +4,12 @@ // ENiGMA½ const TextView = require('./text_view.js').TextView; const EditTextView = require('./edit_text_view.js').EditTextView; -const ButtonView = require('./button_view.js').ButtonView; +const ButtonView = require('./button_view.js').ButtonView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; -const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; -const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; -const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; +const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; +const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; -//const StatusBarView = require('./status_bar_view.js').StatusBarView; const KeyEntryView = require('./key_entry_view.js'); const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; @@ -37,7 +36,7 @@ MCIViewFactory.UserViewCodes = [ 'XY', ]; -MCIViewFactory.prototype.createFromMCI = function(mci, cb) { +MCIViewFactory.prototype.createFromMCI = function(mci) { assert(mci.code); assert(mci.id > 0); assert(mci.position); @@ -78,7 +77,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { // switch(mci.code) { // Text Label (Text View) - case 'TL' : + case 'TL' : setOption(0, 'textStyle'); setOption(1, 'justify'); setWidth(2); @@ -105,14 +104,14 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { break; // Multi Line Edit Text - case 'MT' : + case 'MT' : // :TODO: apply params view = new MultiLineEditTextView(options); break; // Pre-defined Label (Text View) // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove - case 'PL' : + case 'PL' : if(mci.args.length > 0) { options.text = getPredefinedMCIValue(this.client, mci.args[0]); if(options.text) { @@ -126,7 +125,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { break; // Button - case 'BT' : + case 'BT' : if(mci.args.length > 0) { options.dimens = { width : parseInt(mci.args[0], 10) }; } @@ -144,14 +143,14 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { setOption(0, 'itemSpacing'); setOption(1, 'justify'); setOption(2, 'textStyle'); - + setFocusOption(0, 'focusTextStyle'); view = new VerticalMenuView(options); break; // Horizontal Menu - case 'HM' : + case 'HM' : setOption(0, 'itemSpacing'); setOption(1, 'textStyle'); @@ -165,7 +164,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci, cb) { setOption(1, 'justify'); setFocusOption(0, 'focusTextStyle'); - + view = new SpinnerMenuView(options); break; diff --git a/core/menu_module.js b/core/menu_module.js index 884389ca..783cc40b 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -17,17 +17,17 @@ const assert = require('assert'); const _ = require('lodash'); exports.MenuModule = class MenuModule extends PluginModule { - + constructor(options) { - super(options); + super(options); this.menuName = options.menuName; this.menuConfig = options.menuConfig; this.client = options.client; this.menuConfig.options = options.menuConfig.options || {}; - this.menuMethods = {}; // methods called from @method's + this.menuMethods = {}; // methods called from @method's this.menuConfig.config = this.menuConfig.config || {}; - + this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config.menus.cls; this.viewControllers = {}; @@ -70,7 +70,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } ); }, - function moveToPromptLocation(callback) { + function moveToPromptLocation(callback) { if(self.menuConfig.prompt) { // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements } @@ -171,10 +171,10 @@ exports.MenuModule = class MenuModule extends PluginModule { } nextMenu(cb) { - if(!this.haveNext()) { + if(!this.haveNext()) { return this.prevMenu(cb); // no next, go to prev } - + return this.client.menuStack.next(cb); } @@ -210,7 +210,7 @@ exports.MenuModule = class MenuModule extends PluginModule { haveNext() { return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); } - + autoNextMenu(cb) { const self = this; @@ -221,8 +221,8 @@ exports.MenuModule = class MenuModule extends PluginModule { return self.prevMenu(cb); } } - - if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { + + if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { if(this.hasNextTimeout()) { setTimeout( () => { return gotoNextMenu(); @@ -297,10 +297,10 @@ exports.MenuModule = class MenuModule extends PluginModule { if(options.clearScreen) { this.client.term.rawWrite(ansi.resetScreen()); } - + return theme.displayThemedAsset( - name, - this.client, + name, + this.client, Object.assign( { font : this.menuConfig.config.font }, options ), (err, artData) => { if(cb) { @@ -361,7 +361,7 @@ exports.MenuModule = class MenuModule extends PluginModule { pausePrompt(position, cb) { if(!cb && _.isFunction(position)) { cb = position; - position = null; + position = null; } this.optionalMoveToPosition(position); @@ -390,7 +390,7 @@ exports.MenuModule = class MenuModule extends PluginModule { if(!view) { return; } - + if(appendMultiLine && (view instanceof MultiLineEditTextView)) { view.addText(text); } else { @@ -401,7 +401,7 @@ exports.MenuModule = class MenuModule extends PluginModule { updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) { options = options || {}; - let textView; + let textView; let customMciId = startId; const config = this.menuConfig.config; const endId = options.endId || 99; // we'll fail to get a view before 99 diff --git a/core/menu_stack.js b/core/menu_stack.js index b4bebea6..26e88cc5 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -78,19 +78,19 @@ module.exports = class MenuStack { // :TODO: leave() should really take a cb... this.pop().instance.leave(); // leave & remove current - + const previousModuleInfo = this.pop(); // get previous if(previousModuleInfo) { const opts = { - extraArgs : previousModuleInfo.extraArgs, + extraArgs : previousModuleInfo.extraArgs, savedState : previousModuleInfo.savedState, lastMenuResult : menuResult, }; return this.goto(previousModuleInfo.name, opts, cb); } - + return cb(Errors.MenuStack('No previous menu available', 'NOPREV')); } @@ -106,14 +106,14 @@ module.exports = class MenuStack { if(currentModuleInfo && name === currentModuleInfo.name) { if(cb) { - cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); + cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); } return; } const loadOpts = { name : name, - client : self.client, + client : self.client, }; if(_.isObject(options)) { diff --git a/core/menu_util.js b/core/menu_util.js index d9e5a1a6..a86690e5 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -42,7 +42,7 @@ function getMenuConfig(client, name, cb) { } else { callback(null); } - } + } ], function complete(err) { cb(err, menuConfig); @@ -53,7 +53,7 @@ function getMenuConfig(client, name, cb) { function loadMenu(options, cb) { assert(_.isObject(options)); assert(_.isString(options.name)); - assert(_.isObject(options.client)); + assert(_.isObject(options.client)); async.waterfall( [ @@ -88,7 +88,7 @@ function loadMenu(options, cb) { return callback(err, modData); }); - }, + }, function createModuleInstance(modData, callback) { Log.trace( { moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, @@ -98,11 +98,11 @@ function loadMenu(options, cb) { try { moduleInstance = new modData.mod.getModule({ menuName : options.name, - menuConfig : modData.config, + menuConfig : modData.config, extraArgs : options.extraArgs, client : options.client, lastMenuResult : options.lastMenuResult, - }); + }); } catch(e) { return callback(e); } @@ -143,7 +143,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); cb(null, formForId[mciReqKey]); return; - } + } // // Generic match @@ -184,24 +184,24 @@ function handleAction(client, formData, conf, cb) { switch(actionAsset.type) { case 'method' : - case 'systemMethod' : + case 'systemMethod' : if(_.isString(actionAsset.location)) { return callModuleMenuMethod( - client, - actionAsset, - paths.join(Config.paths.mods, actionAsset.location), - formData, - conf.extraArgs, + client, + actionAsset, + paths.join(Config.paths.mods, actionAsset.location), + formData, + conf.extraArgs, cb); } else if('systemMethod' === actionAsset.type) { // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () // :TODO: Probably better as system_method.js return callModuleMenuMethod( - client, - actionAsset, - paths.join(__dirname, 'system_menu_method.js'), - formData, - conf.extraArgs, + client, + actionAsset, + paths.join(__dirname, 'system_menu_method.js'), + formData, + conf.extraArgs, cb); } else { // local to current module @@ -209,7 +209,7 @@ function handleAction(client, formData, conf, cb) { if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); } - + const err = new Error('Method does not exist'); client.log.warn( { method : actionAsset.asset }, err.message); return cb(err); @@ -222,14 +222,14 @@ function handleAction(client, formData, conf, cb) { function handleNext(client, nextSpec, conf, cb) { assert(_.isString(nextSpec) || _.isArray(nextSpec)); - + if(_.isArray(nextSpec)) { nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); } - + const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); // :TODO: getAssetWithShorthand() can return undefined - handle it! - + conf = conf || {}; const extraArgs = conf.extraArgs || {}; @@ -252,7 +252,7 @@ function handleNext(client, nextSpec, conf, cb) { const err = new Error('Method does not exist'); client.log.warn( { method : nextAsset.asset }, err.message); - return cb(err); + return cb(err); } case 'menu' : diff --git a/core/menu_view.js b/core/menu_view.js index 41f1302f..5aed54ca 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -16,7 +16,7 @@ exports.MenuView = MenuView; function MenuView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - + View.call(this, options); this.disablePipe = options.disablePipe || false; @@ -65,11 +65,11 @@ util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { const self = this; - if(items) { + if(items) { this.items = []; items.forEach( itemText => { this.items.push( - { + { text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) } ); @@ -79,7 +79,7 @@ MenuView.prototype.setItems = function(items) { MenuView.prototype.removeItem = function(index) { this.items.splice(index, 1); - + if(this.focusItems) { this.focusItems.splice(index, 1); } @@ -95,7 +95,7 @@ MenuView.prototype.getCount = function() { return this.items.length; }; -MenuView.prototype.getItems = function() { +MenuView.prototype.getItems = function() { return this.items.map( item => { return item.text; }); @@ -140,7 +140,7 @@ MenuView.prototype.onKeyPress = function(ch, key) { MenuView.prototype.setFocusItems = function(items) { const self = this; - + if(items) { this.focusItems = []; items.forEach( itemText => { @@ -183,7 +183,7 @@ MenuView.prototype.setHotKeys = function(hotKeys) { this.hotKeys[key.toLowerCase()] = hotKeys[key]; } } else { - this.hotKeys = hotKeys; + this.hotKeys = hotKeys; } } }; diff --git a/core/message.js b/core/message.js index 5d3c8db9..7cac5c79 100644 --- a/core/message.js +++ b/core/message.js @@ -9,9 +9,9 @@ const getISOTimestampString = require('./database.js').getISOTimestampString; const Errors = require('./enig_error.js').Errors; const ANSI = require('./ansi_term.js'); -const { +const { isAnsi, isFormattedLine, - splitTextAtTerms, + splitTextAtTerms, renderSubstr } = require('./string_util.js'); @@ -45,7 +45,7 @@ function Message(options) { this.fromUserName = options.fromUserName || ''; this.subject = options.subject || ''; this.message = options.message || ''; - + if(_.isDate(options.modTimestamp) || moment.isMoment(options.modTimestamp)) { this.modTimestamp = moment(options.modTimestamp); } else if(_.isString(options.modTimestamp)) { @@ -115,7 +115,7 @@ Message.StateFlags0 = { Exported : 0x00000002, // exported to foreign system }; -Message.FtnPropertyNames = { +Message.FtnPropertyNames = { FtnOrigNode : 'ftn_orig_node', FtnDestNode : 'ftn_dest_node', FtnOrigNetwork : 'ftn_orig_network', @@ -166,12 +166,12 @@ Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { if(!moment.isMoment(modTimestamp)) { modTimestamp = moment(modTimestamp); } - + areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); - + return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); }; @@ -180,8 +180,8 @@ Message.getMessageIdByUuid = function(uuid, cb) { `SELECT message_id FROM message WHERE message_uuid = ? - LIMIT 1;`, - [ uuid ], + LIMIT 1;`, + [ uuid ], (err, row) => { if(err) { cb(err); @@ -210,30 +210,30 @@ Message.getMessageIdsByMetaValue = function(category, name, value, cb) { }; Message.getMetaValuesByMessageId = function(messageId, category, name, cb) { - const sql = + const sql = `SELECT meta_value FROM message_meta WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; - + msgDb.all(sql, [ messageId, category, name ], (err, rows) => { if(err) { return cb(err); } - + if(0 === rows.length) { return cb(new Error('No value for category/name')); } - + // single values are returned without an array if(1 === rows.length) { return cb(null, rows[0].meta_value); } - + cb(null, rows.map(r => r.meta_value)); // map to array of values only }); }; -Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { +Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { async.waterfall( [ function getMessageId(callback) { @@ -256,22 +256,22 @@ Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { Message.prototype.loadMeta = function(cb) { /* Example of loaded this.meta: - + meta: { System: { - local_to_user_id: 1234, + local_to_user_id: 1234, }, FtnProperty: { ftn_seen_by: [ "1/102 103", "2/42 52 65" ] } - } - */ - - const sql = + } + */ + + const sql = `SELECT meta_category, meta_name, meta_value FROM message_meta WHERE message_id = ?;`; - + let self = this; msgDb.each(sql, [ this.messageId ], (err, row) => { if(!(row.meta_category in self.meta)) { @@ -279,12 +279,12 @@ Message.prototype.loadMeta = function(cb) { self.meta[row.meta_category][row.meta_name] = row.meta_value; } else { if(!(row.meta_name in self.meta[row.meta_category])) { - self.meta[row.meta_category][row.meta_name] = row.meta_value; + self.meta[row.meta_category][row.meta_name] = row.meta_value; } else { if(_.isString(self.meta[row.meta_category][row.meta_name])) { - self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; + self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; } - + self.meta[row.meta_category][row.meta_name].push(row.meta_value); } } @@ -315,7 +315,7 @@ Message.prototype.load = function(options, cb) { if(!msgRow) { return callback(new Error('Message (no longer) available')); } - + self.messageId = msgRow.message_id; self.areaTag = msgRow.area_tag; self.messageUuid = msgRow.message_uuid; @@ -356,13 +356,13 @@ Message.prototype.persistMetaValue = function(category, name, value, transOrDb, const metaStmt = transOrDb.prepare( `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) VALUES (?, ?, ?, ?);`); - + if(!_.isArray(value)) { value = [ value ]; } - + let self = this; - + async.each(value, (v, next) => { metaStmt.run(self.messageId, category, name, v, err => { next(err); @@ -379,7 +379,7 @@ Message.prototype.persist = function(cb) { } const self = this; - + async.waterfall( [ function beginTransaction(callback) { @@ -398,7 +398,7 @@ Message.prototype.persist = function(cb) { trans.run( `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) - VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], function inserted(err) { // use non-arrow function for 'this' scope if(!err) { @@ -415,15 +415,15 @@ Message.prototype.persist = function(cb) { } /* Example of self.meta: - + meta: { System: { - local_to_user_id: 1234, + local_to_user_id: 1234, }, FtnProperty: { ftn_seen_by: [ "1/102 103", "2/42 52 65" ] } - } + } */ async.each(Object.keys(self.meta), (category, nextCat) => { async.each(Object.keys(self.meta[category]), (name, nextName) => { @@ -433,10 +433,10 @@ Message.prototype.persist = function(cb) { }, err => { nextCat(err); }); - + }, err => { callback(err, trans); - }); + }); }, function storeHashTags(trans, callback) { // :TODO: hash tag support @@ -470,21 +470,21 @@ Message.prototype.getQuoteLines = function(options, cb) { if(!options.termWidth || !options.termHeight || !options.cols) { return cb(Errors.MissingParam()); } - + options.startCol = options.startCol || 1; options.includePrefix = _.get(options, 'includePrefix', true); options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting - + /* Some long text that needs to be wrapped and quoted should look right after - doing so, don't ya think? yeah I think so + doing so, don't ya think? yeah I think so - Nu> Some long text that needs to be wrapped and quoted should look right + Nu> Some long text that needs to be wrapped and quoted should look right Nu> after doing so, don't ya think? yeah I think so - Ot> Nu> Some long text that needs to be wrapped and quoted should look + Ot> Nu> Some long text that needs to be wrapped and quoted should look Ot> Nu> right after doing so, don't ya think? yeah I think so */ @@ -498,7 +498,7 @@ Message.prototype.getQuoteLines = function(options, cb) { tabHandling : 'expand', tabWidth : 4, }; - + return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; }); @@ -527,44 +527,44 @@ Message.prototype.getQuoteLines = function(options, cb) { cols : options.cols, rows : 'auto', startCol : options.startCol, - forceLineTerm : true, + 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 + // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do // the trick and allow them to leave them alone! // split.forEach(l => { quoteLines.push(`${lastSgr}${l}`); - + focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`); - lastSgr = (l.match(/(?:\x1b\x5b)[\?=;0-9]*m(?!.*(?:\x1b\x5b)[\?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex + lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex }); quoteLines[quoteLines.length - 1] += options.ansiResetSgr; - + return cb(null, quoteLines, focusQuoteLines, true); } ); } else { - const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}\> )+(?:[A-Za-z0-9]{2}\>)*) */; + const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; const quoted = []; const 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: @@ -612,7 +612,7 @@ Message.prototype.getQuoteLines = function(options, cb) { buf += ` ${line}`; } break; - + case 'quote_line' : if(quoteMatch) { const rem = line.slice(quoteMatch[0].length); @@ -628,7 +628,7 @@ Message.prototype.getQuoteLines = function(options, cb) { state = 'line'; } break; - + default : if(isFormattedLine(line)) { quoted.push(getFormattedLine(line)); @@ -637,12 +637,12 @@ Message.prototype.getQuoteLines = function(options, cb) { 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)); }); diff --git a/core/message_area.js b/core/message_area.js index 53dd3086..3c42e23f 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -40,9 +40,9 @@ function getAvailableMessageConferences(client, options) { options = options || { includeSystemInternal : false }; assert(client || true === options.noClient); - - // perform ACS check per conf & omit system_internal if desired - return _.omitBy(Config.messageConferences, (conf, confTag) => { + + // perform ACS check per conf & omit system_internal if desired + return _.omitBy(Config.messageConferences, (conf, confTag) => { if(!options.includeSystemInternal && 'system_internal' === confTag) { return true; } @@ -60,15 +60,15 @@ function getSortedAvailMessageConferences(client, options) { }); sortAreasOrConfs(confs, 'conf'); - + return confs; } // Return an *object* of available areas within |confTag| function getAvailableMessageAreasByConfTag(confTag, options) { options = options || {}; - - // :TODO: confTag === "" then find default + + // :TODO: confTag === "" then find default if(_.has(Config.messageConferences, [ confTag, 'areas' ])) { const areas = Config.messageConferences[confTag].areas; @@ -92,9 +92,9 @@ function getSortedAvailMessageAreasByConfTag(confTag, options) { area : v, }; }); - + sortAreasOrConfs(areas, 'area'); - + return areas; } @@ -103,7 +103,7 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) { // Find the first conference marked 'default'. If found, // inspect |client| against *read* ACS using defaults if not // specified. - // + // // If the above fails, just go down the list until we get one // that passes. // @@ -116,14 +116,14 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) { const conf = Config.messageConferences[defaultConf]; if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { return defaultConf; - } + } } // just use anything we can defaultConf = _.findKey(Config.messageConferences, (conf, confTag) => { return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)); }); - + return defaultConf; } @@ -138,19 +138,19 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { confTag = confTag || getDefaultMessageConferenceTag(client); if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) { - const areaPool = Config.messageConferences[confTag].areas; + const areaPool = Config.messageConferences[confTag].areas; let defaultArea = _.findKey(areaPool, o => o.default); if(defaultArea) { const area = areaPool[defaultArea]; if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) { return defaultArea; - } + } } - + defaultArea = _.findKey(areaPool, (area) => { - return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); + return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); }); - + return defaultArea; } } @@ -159,18 +159,6 @@ function getMessageConferenceByTag(confTag) { return Config.messageConferences[confTag]; } -function getMessageConfByAreaTag(areaTag) { - const confs = Config.messageConferences; - let conf; - _.forEach(confs, (v) => { - if(_.has(v, [ 'areas', areaTag ])) { - conf = v; - return false; // stop iteration - } - }); - return conf; -} - function getMessageConfTagByAreaTag(areaTag) { const confs = Config.messageConferences; return Object.keys(confs).find( (confTag) => { @@ -194,9 +182,9 @@ function getMessageAreaByTag(areaTag, optionalConfTag) { if(_.has(v, [ 'areas', areaTag ])) { area = v.areas[areaTag]; return false; // stop iteration - } + } }); - + return area; } } @@ -206,7 +194,7 @@ function changeMessageConference(client, confTag, cb) { [ function getConf(callback) { const conf = getMessageConferenceByTag(confTag); - + if(conf) { callback(null, conf); } else { @@ -216,7 +204,7 @@ function changeMessageConference(client, confTag, cb) { function getDefaultAreaInConf(conf, callback) { const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); const area = getMessageAreaByTag(areaTag, confTag); - + if(area) { callback(null, conf, { areaTag : areaTag, area : area } ); } else { @@ -229,7 +217,7 @@ function changeMessageConference(client, confTag, cb) { } else { return callback(null, conf, areaInfo); } - }, + }, function changeConferenceAndArea(conf, areaInfo, callback) { const newProps = { message_conf_tag : confTag, @@ -258,12 +246,12 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { [ function getArea(callback) { const area = getMessageAreaByTag(areaTag); - return callback(area ? null : new Error('Invalid message areaTag'), area); + return callback(area ? null : new Error('Invalid message areaTag'), area); }, function validateAccess(area, callback) { - // - // Need at least *read* to access the area - // + // + // Need at least *read* to access the area + // if(!client.acs.hasMessageAreaRead(area)) { return callback(new Error('Access denied to message area')); } else { @@ -294,7 +282,7 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { } // -// Temporairly -- e.g. non-persisted -- change to an area and it's +// Temporairly -- e.g. non-persisted -- change to an area and it's // associated underlying conference. ACS is checked for both. // // This is useful for example when doing a new scan @@ -312,7 +300,7 @@ function tempChangeMessageConfAndArea(client, areaTag) { if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { return false; } - + client.user.properties.message_conf_tag = confTag; client.user.properties.message_area_tag = areaTag; @@ -324,7 +312,7 @@ function changeMessageArea(client, areaTag, cb) { } function getMessageFromRow(row) { - return { + return { messageId : row.message_id, messageUuid : row.message_uuid, replyToMsgId : row.reply_to_message_id, @@ -346,8 +334,8 @@ function getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, what) // // * Only messages > |lastMessageId| should be returned/counted // - const selectWhat = ('count' === what) ? - 'COUNT() AS count' : + const selectWhat = ('count' === what) ? + 'COUNT() AS count' : 'message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count'; let sql = @@ -386,7 +374,7 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) { msgDb.get(sql, (err, row) => { return callback(err, row ? row.count : 0); }); - } + } ], cb ); @@ -421,7 +409,7 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { function complete(err) { cb(err, msgList); } - ); + ); } function getMessageListForArea(options, areaTag, cb) { @@ -435,7 +423,7 @@ function getMessageListForArea(options, areaTag, cb) { /* [ - { + { messageId, messageUuid, replyToId, toUserName, fromUserName, subject, modTimestamp, status(new|old), viewCount @@ -448,13 +436,13 @@ function getMessageListForArea(options, areaTag, cb) { async.series( [ function fetchMessages(callback) { - let sql = + let sql = `SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count FROM message WHERE area_tag = ?`; if(Message.isPrivateAreaTag(areaTag)) { - sql += + sql += ` AND message_id IN ( SELECT message_id FROM message_meta @@ -462,7 +450,7 @@ function getMessageListForArea(options, areaTag, cb) { )`; } - sql += ' ORDER BY message_id;'; + sql += ' ORDER BY message_id;'; msgDb.each( sql, @@ -551,12 +539,12 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) ], function complete(err, didUpdate) { if(err) { - Log.debug( - { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, + Log.debug( + { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, 'Failed updating area last read ID'); } else { if(true === didUpdate) { - Log.trace( + Log.trace( { userId : userId, areaTag : areaTag, messageId : messageId }, 'Area last read ID updated'); } @@ -574,7 +562,7 @@ function persistMessage(message, cb) { }, function recordToMessageNetworks(callback) { return msgNetRecord(message, callback); - } + } ], cb ); @@ -582,7 +570,7 @@ function persistMessage(message, cb) { // method exposed for event scheduler function trimMessageAreasScheduledEvent(args, cb) { - + function trimMessageAreaByMaxMessages(areaInfo, cb) { if(0 === areaInfo.maxMessages) { return cb(null); @@ -605,7 +593,7 @@ function trimMessageAreasScheduledEvent(args, cb) { Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully'); } return cb(err); - } + } ); } @@ -690,7 +678,7 @@ function trimMessageAreasScheduledEvent(args, cb) { trimMessageAreaByMaxAgeDays(areaInfo, err => { return next(err); - }); + }); }); }, callback diff --git a/core/mime_util.js b/core/mime_util.js index 1e2bd32c..a110572c 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -36,6 +36,6 @@ function resolveMimeType(query) { if(mimeTypes.extensions[query]) { return query; // alreaed a mime-type } - + return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined } \ No newline at end of file diff --git a/core/misc_util.js b/core/misc_util.js index afe33dee..70bbb5e2 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -36,10 +36,10 @@ function resolvePath(path) { function getCleanEnigmaVersion() { return packageJson.version - .replace(/\-/g, '.') + .replace(/-/g, '.') .replace(/alpha/,'a') .replace(/beta/,'b') - ; + ; } // See also ftn_util.js getTearLine() & getProductIdentifier() diff --git a/core/mod_mixins.js b/core/mod_mixins.js index 291e0cc9..48546825 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -5,7 +5,7 @@ const messageArea = require('../core/message_area.js'); exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { - + tempMessageConfAndAreaSwitch(messageAreaTag) { messageAreaTag = messageAreaTag || this.messageAreaTag; if(!messageAreaTag) { @@ -14,7 +14,7 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { this.prevMessageConfAndArea = { confTag : this.client.user.properties.message_conf_tag, - areaTag : this.client.user.properties.message_area_tag, + areaTag : this.client.user.properties.message_area_tag, }; if(!messageArea.tempChangeMessageConfAndArea(this.client, this.messageAreaTag)) { @@ -25,7 +25,7 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { tempMessageConfAndAreaRestore() { if(this.prevMessageConfAndArea) { this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; - this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; + this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; } } }; diff --git a/core/msg_area_list.js b/core/msg_area_list.js index eaedbef8..96b0a51c 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -36,7 +36,7 @@ exports.moduleInfo = { const MciViewIds = { AreaList : 1, SelAreaInfo1 : 2, - SelAreaInfo2 : 3, + SelAreaInfo2 : 3, }; exports.getModule = class MessageAreaListModule extends MenuModule { @@ -61,7 +61,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); self.prevMenuOnTimeout(1000, cb); - } else { + } else { if(_.isString(area.art)) { const dispOptions = { client : self.client, @@ -72,7 +72,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { displayThemeArt(dispOptions, () => { // pause by default, unless explicitly told not to - if(_.has(area, 'options.pause') && false === area.options.pause) { + if(_.has(area, 'options.pause') && false === area.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { self.pausePrompt( () => { @@ -98,9 +98,9 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }, timeout); } - updateGeneralAreaInfoViews(areaIndex) { - // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! - /* experimental: not yet avail + // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! + /* + updateGeneralAreaInfoViews(areaIndex) { const areaInfo = self.messageAreas[areaIndex]; [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { @@ -109,8 +109,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule { v.setFormatObject(areaInfo.area); } }); - */ } + */ mciReady(mciData, cb) { super.mciReady(mciData, err => { @@ -137,7 +137,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { function populateAreaListView(callback) { const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; - + const areaListView = vc.getView(MciViewIds.AreaList); let i = 1; areaListView.setItems(_.map(self.messageAreas, v => { @@ -145,7 +145,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { index : i++, areaTag : v.area.areaTag, name : v.area.name, - desc : v.area.desc, + desc : v.area.desc, }); })); @@ -155,7 +155,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { index : i++, areaTag : v.area.areaTag, name : v.area.name, - desc : v.area.desc, + desc : v.area.desc, }); })); diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index c13f39a6..3ffef698 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -48,9 +48,9 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { self.client.log.info( { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, 'Message persisted' - ); + ); } - + return self.nextMenu(cb); } ); diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index 02915f79..0f25c63f 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -69,7 +69,7 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'up arrow' : bodyView.scrollDocumentDown(); break; case 'page up' : bodyView.keyPressPageUp(); break; - case 'page down' : bodyView.keyPressPageDown(); break; + case 'page down' : bodyView.keyPressPageDown(); break; } // :TODO: need to stop down/page down if doing so would push the last @@ -83,13 +83,13 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { const modOpts = { extraArgs : { messageAreaTag : self.messageAreaTag, - replyToMessage : self.message, - } + replyToMessage : self.message, + } }; return self.gotoMenu(extraArgs.menu, modOpts, cb); } - + self.client.log(extraArgs, 'Missing extraArgs.menu'); return cb(null); } diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index 6f42cf36..43e57820 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -21,10 +21,10 @@ exports.moduleInfo = { const MciViewIds = { ConfList : 1, - + // :TODO: // # areas in conf .... see Obv/2, iNiQ, ... - // + // }; exports.getModule = class MessageConfListModule extends MenuModule { @@ -33,16 +33,16 @@ exports.getModule = class MessageConfListModule extends MenuModule { this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client); const self = this; - + this.menuMethods = { changeConference : function(formData, extraArgs, cb) { if(1 === formData.submitId) { let conf = self.messageConfs[formData.value.conf]; const confTag = conf.confTag; - conf = conf.conf; // what we want is embedded + conf = conf.conf; // what we want is embedded messageArea.changeMessageConference(self.client, confTag, err => { - if(err) { + if(err) { self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); setTimeout( () => { @@ -59,7 +59,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { displayThemeArt(dispOptions, () => { // pause by default, unless explicitly told not to - if(_.has(conf, 'options.pause') && false === conf.options.pause) { + if(_.has(conf, 'options.pause') && false === conf.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { self.pausePrompt( () => { @@ -108,7 +108,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { function populateConfListView(callback) { const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; - + const confListView = vc.getView(MciViewIds.ConfList); let i = 1; confListView.setItems(_.map(self.messageConfs, v => { @@ -116,7 +116,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { index : i++, confTag : v.conf.confTag, name : v.conf.name, - desc : v.conf.desc, + desc : v.conf.desc, }); })); @@ -126,7 +126,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { index : i++, confTag : v.conf.confTag, name : v.conf.name, - desc : v.conf.desc, + desc : v.conf.desc, }); })); diff --git a/core/msg_list.js b/core/msg_list.js index e5a69e80..19ef30cd 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -26,7 +26,7 @@ const moment = require('moment'); MCI codes: VM1 : Message list - TL2 : Message info 1: { msgNumSelected, msgNumTotal } + TL2 : Message info 1: { msgNumSelected, msgNumTotal } */ exports.moduleInfo = { @@ -84,9 +84,9 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 // modOpts.extraArgs.toJSON = function() { - const logMsgList = (this.messageList.length <= 4) ? - this.messageList : - this.messageList.slice(0, 2).concat(this.messageList.slice(-2)); + const logMsgList = (this.messageList.length <= 4) ? + this.messageList : + this.messageList.slice(0, 2).concat(this.messageList.slice(-2)); return { messageAreaTag : this.messageAreaTag, @@ -158,14 +158,14 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( if(_.isArray(self.messageList)) { return callback(0 === self.messageList.length ? new Error('No messages in area') : null); } - + messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) { if(!msgList || 0 === msgList.length) { return callback(new Error('No messages in area')); } - + self.messageList = msgList; - return callback(err); + return callback(err); }); }, function getLastReadMesageId(callback) { @@ -187,15 +187,15 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { self.initialFocusIndex = index; - } + } }); return callback(null); }, function populateList(callback) { - const msgListView = vc.getView(MCICodesIDs.MsgList); - const listFormat = self.menuConfig.config.listFormat || '{msgNum} - {subject} - {toUserName}'; + const msgListView = vc.getView(MCICodesIDs.MsgList); + const listFormat = self.menuConfig.config.listFormat || '{msgNum} - {subject} - {toUserName}'; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default change color here - const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; + const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; // :TODO: This can take a very long time to load large lists. What we need is to implement the "owner draw" concept in // which items are requested (e.g. their format at least) *as-needed* vs trying to get the format for all of them at once @@ -211,10 +211,10 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( msgListView.on('index update', idx => { self.setViewText( 'allViews', - MCICodesIDs.MsgInfo1, + MCICodesIDs.MsgInfo1, stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.messageList.length } )); }); - + if(self.initialFocusIndex > 0) { // note: causes redraw() msgListView.setFocusItemIndex(self.initialFocusIndex); @@ -228,29 +228,29 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; self.setViewText( 'allViews', - MCICodesIDs.MsgInfo1, + MCICodesIDs.MsgInfo1, stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.messageList.length } )); return callback(null); }, - ], + ], err => { if(err) { - self.client.log.error( { error : err.message }, 'Error loading message list'); + self.client.log.error( { error : err.message }, 'Error loading message list'); } return cb(err); } ); - }); + }); } getSaveState() { - return { initialFocusIndex : this.initialFocusIndex }; + return { initialFocusIndex : this.initialFocusIndex }; } restoreSavedState(savedState) { if(savedState) { this.initialFocusIndex = savedState.initialFocusIndex; - } + } } getMenuResult() { diff --git a/core/msg_network.js b/core/msg_network.js index 9e0813f4..890d1bcf 100644 --- a/core/msg_network.js +++ b/core/msg_network.js @@ -38,17 +38,17 @@ function startup(cb) { function shutdown(cb) { async.each( - msgNetworkModules, + msgNetworkModules, (msgNetModule, next) => { msgNetModule.shutdown( () => { return next(); }); - }, + }, () => { msgNetworkModules = []; return cb(null); } - ); + ); } function recordMessage(message, cb) { @@ -59,7 +59,7 @@ function recordMessage(message, cb) { // async.each(msgNetworkModules, (modInst, next) => { modInst.record(message); - next(); + next(); }, err => { cb(err); }); diff --git a/core/msg_scan_toss_module.js b/core/msg_scan_toss_module.js index 8172d77f..9b3598c1 100644 --- a/core/msg_scan_toss_module.js +++ b/core/msg_scan_toss_module.js @@ -13,12 +13,12 @@ function MessageScanTossModule() { require('util').inherits(MessageScanTossModule, PluginModule); MessageScanTossModule.prototype.startup = function(cb) { - cb(null); + return cb(null); }; MessageScanTossModule.prototype.shutdown = function(cb) { - cb(null); + return cb(null); }; -MessageScanTossModule.prototype.record = function(message) { +MessageScanTossModule.prototype.record = function(/*message*/) { }; \ No newline at end of file diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 68d8b3d6..bc73e903 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -4,7 +4,6 @@ const View = require('./view.js').View; const strUtil = require('./string_util.js'); const ansi = require('./ansi_term.js'); -const colorCodes = require('./color_codes.js'); const wordWrapText = require('./word_wrap.js').wordWrapText; const ansiPrep = require('./ansi_prep.js'); @@ -12,11 +11,11 @@ const assert = require('assert'); const _ = require('lodash'); // :TODO: Determine CTRL-* keys for various things - // See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt - // http://wiki.synchro.net/howto:editor:slyedit#edit_mode - // http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html +// See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt +// http://wiki.synchro.net/howto:editor:slyedit#edit_mode +// http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html - /* Mystic +/* Mystic [^B] Reformat Paragraph [^O] Show this help file [^I] Insert tab space [^Q] Enter quote mode [^K] Cut current line of text [^V] Toggle insert/overwrite @@ -179,8 +178,8 @@ function MultiLineEditTextView(options) { for(let i = startIndex; i < endIndex; ++i) { //${self.getSGRFor('text')} - self.client.term.write( - `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, + self.client.term.write( + `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, false // convertLineFeeds ); } @@ -268,7 +267,7 @@ function MultiLineEditTextView(options) { if(remain > 0) { text += ' '.repeat(remain + 1); -// text += new Array(remain + 1).join(' '); + // text += new Array(remain + 1).join(' '); } return text; @@ -291,7 +290,7 @@ function MultiLineEditTextView(options) { lines.forEach(line => { text += line.text.replace(re, '\t'); - + if(options.forceLineTerms || (eolMarker && line.eol)) { text += eolMarker; } @@ -459,7 +458,7 @@ function MultiLineEditTextView(options) { self.getRenderText(index).slice(self.cursorPos.col - c.length) + ansi.goto(absPos.row, absPos.col) + ansi.showCursor(), false - ); + ); } }; @@ -502,7 +501,7 @@ function MultiLineEditTextView(options) { } return wordWrapText( - s, + s, { width : width, tabHandling : tabHandling || 'expand', @@ -1122,19 +1121,19 @@ MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms : MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'mode' : + case 'mode' : this.mode = value; if('preview' === value && !this.specialKeyMap.next) { this.specialKeyMap.next = [ 'tab' ]; - } + } break; - case 'autoScroll' : + case 'autoScroll' : this.autoScroll = value; break; case 'tabSwitchesView' : - this.tabSwitchesView = value; + this.tabSwitchesView = value; this.specialKeyMap.next = this.specialKeyMap.next || []; this.specialKeyMap.next.push('tab'); break; diff --git a/core/new_scan.js b/core/new_scan.js index f3e851fb..b088e47f 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -25,8 +25,8 @@ exports.moduleInfo = { * :TODO: * * User configurable new scan: Area selection (avail from messages area) (sep module) * * Add status TL/VM (either/both should update if present) - * * - + * * + */ const MciCodeIds = { @@ -37,7 +37,7 @@ const MciCodeIds = { const Steps = { MessageConfs : 'messageConferences', FileBase : 'fileBase', - + Finished : 'finished', }; @@ -53,7 +53,7 @@ exports.getModule = class NewScanModule extends MenuModule { // :TODO: Make this conf/area specific: const config = this.menuConfig.config; - this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; + this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; @@ -62,16 +62,16 @@ exports.getModule = class NewScanModule extends MenuModule { updateScanStatus(statusText) { this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText); } - + newScanMessageConference(cb) { - // lazy init + // lazy init if(!this.sortedMessageConfs) { - const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. + const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => { return { confTag : k, - conf : v, + conf : v, }; }); @@ -91,27 +91,27 @@ exports.getModule = class NewScanModule extends MenuModule { this.currentScanAux.conf = this.currentScanAux.conf || 0; this.currentScanAux.area = this.currentScanAux.area || 0; } - + const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; this.newScanMessageArea(currentConf, () => { if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) { - this.currentScanAux.conf += 1; + this.currentScanAux.conf += 1; this.currentScanAux.area = 0; - + return this.newScanMessageConference(cb); // recursive to next conf } this.updateScanStatus(this.scanCompleteMsg); return cb(Errors.DoesNotExist('No more conferences')); - }); + }); } - + newScanMessageArea(conf, cb) { - // :TODO: it would be nice to cache this - must be done by conf! + // :TODO: it would be nice to cache this - must be done by conf! const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); const currentArea = sortedAreas[this.currentScanAux.area]; - + // // Scan and update index until we find something. If results are found, // we'll goto the list module & show them. @@ -207,20 +207,20 @@ exports.getModule = class NewScanModule extends MenuModule { performScanCurrentStep(cb) { switch(this.currentStep) { - case Steps.MessageConfs : - this.newScanMessageConference( () => { + 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); + return this.performScanCurrentStep(cb); }); break; - + default : return cb(null); } } @@ -241,7 +241,7 @@ exports.getModule = class NewScanModule extends MenuModule { // :TODO: display scan step/etc. - async.series( + async.series( [ function loadFromConfig(callback) { const loadOpts = { diff --git a/core/nua.js b/core/nua.js index 7939e739..61dbeb62 100644 --- a/core/nua.js +++ b/core/nua.js @@ -22,10 +22,10 @@ const MciViewIds = { }; exports.getModule = class NewUserAppModule extends MenuModule { - + constructor(options) { super(options); - + const self = this; this.menuMethods = { @@ -40,7 +40,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { viewValidationListener : function(err, cb) { const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); let newFocusId; - + if(err) { errMsgView.setText(err.message); err.view.clearText(); @@ -67,14 +67,14 @@ exports.getModule = class NewUserAppModule extends MenuModule { // // We have to disable ACS checks for initial default areas as the user is not yet ready - // + // let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck // can't store undefined! confTag = confTag || ''; areaTag = areaTag || ''; - + newUser.properties = { real_name : formData.value.realName, birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format @@ -84,12 +84,12 @@ exports.getModule = class NewUserAppModule extends MenuModule { email_address : formData.value.email, web_address : formData.value.web, account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format - + message_conf_tag : confTag, message_area_tag : areaTag, term_height : self.client.term.termHeight, - term_width : self.client.term.termWidth, + term_width : self.client.term.termWidth, // :TODO: Other defaults // :TODO: should probably have a place to create defaults/etc. @@ -100,7 +100,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { } else { newUser.properties.theme_id = Config.defaults.theme; } - + // :TODO: User.create() should validate email uniqueness! newUser.create(formData.value.password, err => { if(err) { diff --git a/core/onelinerz.js b/core/onelinerz.js index 9e89addf..65599ba9 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -20,7 +20,7 @@ const async = require('async'); const _ = require('lodash'); const moment = require('moment'); -/* +/* Module :TODO: * Add pipe code support - override max length & monitor *display* len as user types in order to allow for actual display len with color @@ -73,7 +73,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { self.client.log.warn( { error : err.message }, 'Failed saving oneliner'); } - self.clearAddForm(); + self.clearAddForm(); return self.displayViewScreen(true, cb); // true=cls }); @@ -89,7 +89,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { } }; } - + initSequence() { const self = this; async.series( @@ -136,7 +136,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'view', + 'view', new ViewController( { client : self.client, formId : FormIds.View } ) ); @@ -149,7 +149,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } else { self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); + self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); return callback(null); } }, @@ -216,7 +216,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { [ function clearAndDisplayArt(callback) { self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(ansi.resetScreen()); + self.client.term.rawWrite(ansi.resetScreen()); theme.displayThemedAsset( self.menuConfig.config.art.add, @@ -230,7 +230,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'add', + 'add', new ViewController( { client : self.client, formId : FormIds.Add } ) ); @@ -269,7 +269,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { [ function openDatabase(callback) { self.db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(exports.moduleInfo), + getModDatabasePath(exports.moduleInfo), err => { return callback(err); } @@ -284,10 +284,10 @@ exports.getModule = class OnelinerzModule extends MenuModule { oneliner VARCHAR NOT NULL, timestamp DATETIME NOT NULL );` - , - err => { - return callback(err); - }); + , + err => { + return callback(err); + }); } ], err => { @@ -327,7 +327,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { err => { return cb(err); } - ); + ); } beforeArt(cb) { diff --git a/core/plugin_module.js b/core/plugin_module.js index 31ba6f01..da9410b0 100644 --- a/core/plugin_module.js +++ b/core/plugin_module.js @@ -3,5 +3,5 @@ exports.PluginModule = PluginModule; -function PluginModule(options) { +function PluginModule(/*options*/) { } diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 7fe921b3..47370a66 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -92,7 +92,7 @@ const PREDEFINED_MCI_GENERATORS = { ST : function serverName(client) { return client.session.serverName; }, FN : function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); - return activeFilter ? activeFilter.name : ''; + return activeFilter ? activeFilter.name : ''; }, DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes @@ -160,7 +160,7 @@ const PREDEFINED_MCI_GENERATORS = { }, OA : function systemArchitecture() { return os.arch(); }, - + SC : function systemCpuModel() { // // Clean up CPU strings a bit for better display @@ -190,7 +190,7 @@ const PREDEFINED_MCI_GENERATORS = { // System File Base, Up/Download Info // // :TODO: DD - Today's # of downloads (iNiQUiTY) - // + // SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, SO : function systemByteDownload() { const byteSize = StatLog.getSystemStatNum('dl_total_bytes'); @@ -221,7 +221,7 @@ const PREDEFINED_MCI_GENERATORS = { // -> Include FTN/etc. // :TODO: LC - name of last caller to system (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) - + // // Special handling for XY diff --git a/core/rumorz.js b/core/rumorz.js index b83853f0..da1ab5f6 100644 --- a/core/rumorz.js +++ b/core/rumorz.js @@ -52,9 +52,9 @@ exports.getModule = class RumorzModule extends MenuModule { addEntry : (formData, extraArgs, cb) => { if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { const rumor = formData.value.rumor.trim(); // remove any trailing ws - + StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { - this.clearAddForm(); + this.clearAddForm(); return this.displayViewScreen(true, cb); // true=cls }); } else { @@ -77,7 +77,7 @@ exports.getModule = class RumorzModule extends MenuModule { const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); newEntryView.setText(''); - + // preview is optional if(previewView) { previewView.setText(''); @@ -130,7 +130,7 @@ exports.getModule = class RumorzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'view', + 'view', new ViewController( { client : self.client, formId : FormIds.View } ) ); @@ -143,7 +143,7 @@ exports.getModule = class RumorzModule extends MenuModule { return vc.loadFromMenuConfig(loadOpts, callback); } else { self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); + self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); return callback(null); } }, @@ -186,7 +186,7 @@ exports.getModule = class RumorzModule extends MenuModule { [ function clearAndDisplayArt(callback) { self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(resetScreen()); + self.client.term.rawWrite(resetScreen()); theme.displayThemedAsset( self.config.art.add, @@ -200,7 +200,7 @@ exports.getModule = class RumorzModule extends MenuModule { function initOrRedrawViewController(artData, callback) { if(_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( - 'add', + 'add', new ViewController( { client : self.client, formId : FormIds.Add } ) ); @@ -220,7 +220,7 @@ exports.getModule = class RumorzModule extends MenuModule { }, function initPreviewUpdates(callback) { const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); - const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); + const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); if(previewView) { let timerId; entryView.on('key press', () => { @@ -230,7 +230,7 @@ exports.getModule = class RumorzModule extends MenuModule { if(focused === entryView) { previewView.setText(entryView.getData()); focused.setFocus(true); - } + } }, 500); }); } diff --git a/core/sauce.js b/core/sauce.js index 295a6069..b976450b 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -8,7 +8,9 @@ exports.readSAUCE = readSAUCE; const SAUCE_SIZE = 128; const SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' -const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' + +// :TODO read comments +//const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' exports.SAUCE_SIZE = SAUCE_SIZE; // :TODO: SAUCE should be a class @@ -51,7 +53,7 @@ function readSAUCE(data, cb) { if(!SAUCE_ID.equals(vars.id)) { return cb(new Error('No SAUCE record present')); - } + } var ver = iconv.decode(vars.version, 'cp437'); @@ -137,7 +139,7 @@ var SAUCE_FONT_TO_ENCODING_HINT = { }; ['437', '720', '737', '775', '819', '850', '852', '855', '857', '858', -'860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) { + '860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) { var codec = 'cp' + page; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 8da71d02..20ab435f 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1213,7 +1213,30 @@ function FTNMessageScanTossModule() { User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { if(err) { - return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); + // + // Couldn't find a local username. If the toUserName itself is a FTN address + // we can only assume the message is to the +op, else we'll have to fail. + // + const toUserNameAsAddress = Address.fromString(message.toUserName); + if(toUserNameAsAddress.isValid()) { + + Log.info( + { toUserName : message.toUserName, fromUserName : message.fromUserName }, + 'No local "to" username for FTN message. Appears to be a FTN address only; assuming addressed to SysOp' + ); + + User.getUserName(User.RootUserID, (err, sysOpUserName) => { + if(err) { + return callback(Errors.UnexpectedState('Failed to get SysOp user information')); + } + + message.meta.System[Message.SystemMetaNames.LocalToUserID] = User.RootUserID; + message.toUserName = sysOpUserName; + return callback(null); + }); + } else { + return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); + } } // we do this after such that error cases can be preseved above diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js index 0e29e999..efcb1f19 100644 --- a/core/set_newscan_date.js +++ b/core/set_newscan_date.js @@ -43,7 +43,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { const config = this.menuConfig.config; this.target = config.target || 'message'; - this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; + this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; this.menuMethods = { scanDateSubmit : (formData, extraArgs, cb) => { @@ -232,7 +232,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { const scanDateView = vc.getView(MciViewIds.main.scanDate); // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now - const scanDateFormat = self.scanDateFormat.replace(/[\/\-. ]/g, ''); + const scanDateFormat = self.scanDateFormat.replace(/[/\-. ]/g, ''); scanDateView.setText(today.format(scanDateFormat)); if('message' === self.target) { diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index 65ac10af..e9145662 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -1,13 +1,12 @@ /* jslint node: true */ 'use strict'; -var MenuView = require('./menu_view.js').MenuView; -var ansi = require('./ansi_term.js'); -var strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); -var util = require('util'); -var assert = require('assert'); -var _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); exports.SpinnerMenuView = SpinnerMenuView; @@ -16,7 +15,7 @@ function SpinnerMenuView(options) { options.cursor = options.cursor || 'hide'; MenuView.call(this, options); - + var self = this; /* @@ -29,7 +28,7 @@ function SpinnerMenuView(options) { //assert(!self.positionCacheExpired); assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); - + self.drawItem(this.focusedItemIndex); }; @@ -66,19 +65,19 @@ SpinnerMenuView.prototype.setFocus = function(focused) { SpinnerMenuView.prototype.setFocusItemIndex = function(index) { SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - + this.updateSelection(); // will redraw }; SpinnerMenuView.prototype.onKeyPress = function(ch, key) { if(key) { - if(this.isKeyMapped('up', key.name)) { + if(this.isKeyMapped('up', key.name)) { if(0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; } else { this.focusedItemIndex--; } - + this.updateSelection(); return; } else if(this.isKeyMapped('down', key.name)) { @@ -87,7 +86,7 @@ SpinnerMenuView.prototype.onKeyPress = function(ch, key) { } else { this.focusedItemIndex++; } - + this.updateSelection(); return; } diff --git a/core/stat_log.js b/core/stat_log.js index d6a53d28..edb2d98c 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -10,14 +10,14 @@ const moment = require('moment'); /* System Event Log & Stats ------------------------ - + System & user specific: * Events for generating various statistics, logs such as last callers, etc. * Stats such as counters User specific stats are simply an alternate interface to user properties, while system wide entries are handled on their own. Both are read accessible non-blocking - making them easily available for MCI codes for example. + making them easily available for MCI codes for example. */ class StatLog { constructor() { @@ -66,7 +66,7 @@ class StatLog { TimestampDesc : 'timestamp_desc', Random : 'random', }; - } + } setNonPeristentSystemStat(statName, statValue) { this.systemStats[statName] = statValue; @@ -139,7 +139,7 @@ class StatLog { return cb(new Error(`Value for ${statName} is not a number!`)); } - newValue += incrementBy; + newValue += incrementBy; } else { newValue = incrementBy; } @@ -201,19 +201,19 @@ class StatLog { } } ); - break; + break; case 'forever' : default : // nop break; - } + } } ); } getSystemLogEntries(logName, order, limit, cb) { - let sql = + let sql = `SELECT timestamp, log_value FROM system_event_log WHERE log_name = ?`; @@ -228,7 +228,7 @@ class StatLog { sql += ' ORDER BY timestamp DESC'; break; - case 'random' : + case 'random' : sql += ' ORDER BY RANDOM()'; } @@ -279,7 +279,7 @@ class StatLog { ); } ); - } + } } module.exports = new StatLog(); diff --git a/core/stats.js b/core/stats.js deleted file mode 100644 index ecc472de..00000000 --- a/core/stats.js +++ /dev/null @@ -1,30 +0,0 @@ -/* jslint node: true */ -'use strict'; - -var userDb = require('./database.js').dbs.user; - -exports.getSystemLoginHistory = getSystemLoginHistory; - -function getSystemLoginHistory(numRequested, cb) { - - numRequested = Math.max(1, numRequested); - - var loginHistory = []; - - userDb.each( - 'SELECT user_id, user_name, timestamp ' + - 'FROM user_login_history ' + - 'ORDER BY timestamp DESC ' + - 'LIMIT ' + numRequested + ';', - function historyRow(err, histEntry) { - loginHistory.push( { - userId : histEntry.user_id, - userName : histEntry.user_name, - timestamp : histEntry.timestamp, - } ); - }, - function complete(err, recCount) { - cb(err, loginHistory); - } - ); -} diff --git a/core/status_bar_view.js b/core/status_bar_view.js deleted file mode 100644 index ed47ca7d..00000000 --- a/core/status_bar_view.js +++ /dev/null @@ -1,64 +0,0 @@ -/* jslint node: true */ -'use strict'; - -var View = require('./view.js').View; -var TextView = require('./text_view.js').TextView; - -var assert = require('assert'); -var _ = require('lodash'); - -function StatusBarView(options) { - View.call(this, options); - - var self = this; - - -} - -require('util').inherits(StatusBarView, View); - -StatusBarView.prototype.redraw = function() { - - StatusBarView.super_.prototype.redraw.call(this); - -}; - -StatusBarView.prototype.setPanels = function(panels) { - -/* - "panels" : [ - { - "text" : "things and stuff", - "width" 20, - ... - }, - { - "width" : 40 // no text, etc... = spacer - } - ] - - |---------------------------------------------| - | stuff | -*/ - assert(_.isArray(panels)); - - this.panels = []; - - var tvOpts = { - cursor : 'hide', - position : { row : this.position.row, col : 0 }, - }; - - panels.forEach(function panel(p) { - assert(_.isObject(p)); - assert(_.has(p, 'width')); - - if(p.text) { - this.panels.push( new TextView( { })) - } else { - this.panels.push( { width : p.width } ); - } - }); - -}; - diff --git a/core/string_format.js b/core/string_format.js index 7fb7109a..eba715d5 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -5,7 +5,7 @@ const EnigError = require('./enig_error.js').EnigError; const { pad, - stylizeString, + stylizeString, renderStringLength, renderSubstr, formatByteSize, formatByteSizeAbbr, @@ -172,15 +172,15 @@ function formatNumberHelper(n, precision, type) { case 'b' : return n.toString(2); case 'o' : return n.toString(8); case 'x' : return n.toString(16); - case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0'); - case 'f' : return n.toFixed(precision); + case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0'); + case 'f' : return n.toFixed(precision); case 'g' : // we don't want useless trailing zeros. parseFloat -> back to string fixes this for us return parseFloat(n.toPrecision(precision || 1)).toString(); case '%' : return formatNumberHelper(n * 100, precision, 'f') + '%'; case '' : return formatNumberHelper(n, precision, 'd'); - + default : throw new ValueError(`Unknown format code "${type}" for object of type 'float'`); } @@ -207,7 +207,7 @@ function formatNumber(value, tokens) { if('' !== tokens.precision) { throw new ValueError('Precision not allowed in integer format specifier'); - } + } } else if( [ 'e', 'E', 'f', 'F', 'g', 'G', '%' ].indexOf(type) > - 1) { if(tokens['#']) { throw new ValueError('Alternate form (#) not allowed in float format specifier'); @@ -215,7 +215,7 @@ function formatNumber(value, tokens) { } const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type); - const sign = value < 0 || 1 / value < 0 ? + const sign = value < 0 || 1 / value < 0 ? '-' : '-' === tokens.sign ? '' : tokens.sign; @@ -223,7 +223,7 @@ function formatNumber(value, tokens) { if(tokens[',']) { const match = /^(\d*)(.*)$/.exec(s); - const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; + const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; if('=' !== align) { return pad(sign + separated, width, fill, getPadAlign(align)); @@ -246,7 +246,7 @@ function formatNumber(value, tokens) { if(0 === width) { return sign + prefix + s; - } + } if('=' === align) { return sign + prefix + pad(s, width - sign.length - prefix.length, fill, getPadAlign('>')); @@ -272,9 +272,9 @@ const transformers = { styleL33t : (s) => stylizeString(s, 'l33t'), // :TODO: - // toMegs(), toKilobytes(), ... - // toList(), toCommaList(), - + // toMegs(), toKilobytes(), ... + // toList(), toCommaList(), + sizeWithAbbr : (n) => formatByteSize(n, true, 2), sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), sizeAbbr : (n) => formatByteSizeAbbr(n), @@ -293,14 +293,14 @@ function transformValue(transformerName, value) { } // :TODO: Use explicit set of chars for paths & function/transforms such that } is allowed as fill/etc. -const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:\!([^:}]+))?(?:\:([^}]+))?}/g; +const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:!([^:}]+))?(?::([^}]+))?}/g; function getValue(obj, path) { const value = _.get(obj, path); if(!_.isUndefined(value)) { return _.isFunction(value) ? value() : value; } - + throw new KeyError(quote(path)); } @@ -350,7 +350,7 @@ module.exports = function format(fmt, obj) { // remainder if(pos < fmt.length) { out += fmt.slice(pos); - } + } - return out; + return out; }; diff --git a/core/string_util.js b/core/string_util.js index 238aeeee..a4544754 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -3,7 +3,6 @@ // ENiGMA½ const miscUtil = require('./misc_util.js'); -const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSI = require('./ansi_term.js'); // deps @@ -53,12 +52,12 @@ function stylizeString(s, style) { switch(style) { // None/normal case 'normal' : - case 'N' : + case 'N' : return s; // UPPERCASE - case 'upper' : - case 'U' : + case 'upper' : + case 'U' : return s.toUpperCase(); // lowercase @@ -107,8 +106,8 @@ function stylizeString(s, style) { return stylized; // Small i's: DEMENTiA - case 'small i' : - case 'i' : + case 'small i' : + case 'i' : return s.toUpperCase().replace(/I/g, 'i'); // mIxeD CaSE (random upper/lower) @@ -128,7 +127,7 @@ function stylizeString(s, style) { case '3' : for(i = 0; i < len; ++i) { c = SIMPLE_ELITE_MAP[s[i].toLowerCase()]; - stylized += c || s[i]; + stylized += c || s[i]; } return stylized; } @@ -147,11 +146,11 @@ function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { useRenderLen = miscUtil.valueWithDefault(useRenderLen, true); const renderLen = useRenderLen ? renderStringLength(s) : s.length; - const padlen = len >= renderLen ? len - renderLen : 0; + const padlen = len >= renderLen ? len - renderLen : 0; switch(dir) { case 'L' : - case 'left' : + case 'left' : s = padSGR + new Array(padlen).join(padChar) + stringSGR + s; break; @@ -162,10 +161,10 @@ function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { const right = Math.ceil(padlen / 2); const left = padlen - right; s = padSGR + new Array(left + 1).join(padChar) + stringSGR + s + padSGR + new Array(right + 1).join(padChar); - } + } break; - case 'R' : + case 'R' : case 'right' : s = stringSGR + s + padSGR + new Array(padlen).join(padChar); break; @@ -184,7 +183,7 @@ function replaceAt(s, n, t) { return s.substring(0, n) + t + s.substring(n + 1); } -const RE_NON_PRINTABLE = +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) { @@ -198,11 +197,6 @@ function isPrintable(s) { return !RE_NON_PRINTABLE.test(s); } -function stringLength(s) { - // :TODO: See https://mathiasbynens.be/notes/javascript-unicode - return s.length; -} - function stripAllLineFeeds(s) { return s.replace(/\r?\n|[\r\u2028\u2029]/g, ''); } @@ -256,7 +250,7 @@ function renderSubstr(str, start, length) { match = re.exec(str); if(match) { - if(match.index > pos) { + if(match.index > pos) { s = str.slice(pos + start, Math.min(match.index, pos + (length - renderLen))); start = 0; // start offset applies only once out += s; @@ -269,7 +263,7 @@ function renderSubstr(str, start, length) { // remainder if(pos + start < str.length && renderLen < length) { - out += str.slice(pos + start, (pos + start + (length - renderLen))); + out += str.slice(pos + start, (pos + start + (length - renderLen))); //out += str.slice(pos + start, Math.max(1, pos + (length - renderLen - 1))); } @@ -277,7 +271,7 @@ function renderSubstr(str, start, length) { } // -// Method to return the "rendered" length taking into account Pipe and ANSI color codes. +// Method to return the "rendered" length taking into account Pipe and ANSI color codes. // // We additionally account for ANSI *forward* movement ESC sequences // in the form of ESC[C where is the "go forward" character count. @@ -291,40 +285,40 @@ function renderStringLength(s) { const re = ANSI_OR_PIPE_REGEXP; re.lastIndex = 0; // we recycle the rege; reset - + // // Loop counting only literal (non-control) sequences // paying special attention to ESC[C which means forward - // + // do { pos = re.lastIndex; m = re.exec(s); - + if(m) { if(m.index > pos) { len += s.slice(pos, m.index).length; } - + if('C' === m[3]) { // ESC[C is foward/right len += parseInt(m[2], 10) || 0; } - } + } } while(0 !== re.lastIndex); - + if(pos < s.length) { len += s.slice(pos).length; } - + return len; } -const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) +const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) function formatByteSizeAbbr(byteSize) { if(0 === byteSize) { return BYTE_SIZE_ABBRS[0]; // B } - + return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; } @@ -332,7 +326,7 @@ function formatByteSize(byteSize, withAbbr = false, decimals = 2) { const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024)); let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); if(withAbbr) { - result += ` ${BYTE_SIZE_ABBRS[i]}`; + result += ` ${BYTE_SIZE_ABBRS[i]}`; } return result; } @@ -351,7 +345,7 @@ function formatCount(count, withAbbr = false, decimals = 2) { const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); if(withAbbr) { - result += `${COUNT_ABBRS[i]}`; + result += `${COUNT_ABBRS[i]}`; } return result; } @@ -359,7 +353,7 @@ function formatCount(count, withAbbr = false, decimals = 2) { // :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]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex +const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([?=;0-9]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex const ANSI_OPCODES_ALLOWED_CLEAN = [ //'A', 'B', // up, down //'C', 'D', // right, left @@ -370,17 +364,17 @@ function cleanControlCodes(input, options) { let m; let pos; let cleaned = ''; - + options = options || {}; - + // // Loop through |input| adding only allowed ESC // sequences and literals to |cleaned| - // + // do { pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; m = REGEXP_ANSI_CONTROL_CODES.exec(input); - + if(m) { if(m.index > pos) { cleaned += input.slice(pos, m.index); @@ -394,205 +388,17 @@ function cleanControlCodes(input, options) { cleaned += m[0]; } } - + } while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex); - + // remainder if(pos < input.length) { cleaned += input.slice(pos); } - + return cleaned; } -function prepAnsi(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; - - 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(Array.isArray(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; - - 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); - - canvas[state.row][state.col].char = c; - - if(state.sgr) { - canvas[state.row][state.col].sgr = state.sgr; - state.sgr = null; - } - } - - state.col += 1; - } - }); - - parser.on('control', (match, opCode) => { - // - // 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; - } - }); - - 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 lastSgr = ''; - let line; - - 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]; - 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[C where - // represents chars to get back to the position we were previously at - // - // * Replace contig spaces with ESC[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); -} - function isAnsiLine(line) { return isAnsi(line);// || renderStringLength(line) < line.length; } @@ -622,22 +428,23 @@ function isFormattedLine(line) { return false; } +// :TODO: rename to containsAnsi() function isAnsi(input) { if(!input || 0 === input.length) { return false; } - + // // * 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)) { @@ -647,8 +454,8 @@ function isAnsi(input) { */ // :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) || []; + 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 } diff --git a/core/system_menu_method.js b/core/system_menu_method.js index f968a493..8a20af02 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -33,7 +33,7 @@ function login(callingMenu, formData, extraArgs, cb) { return callingMenu.prevMenu(cb); } } - + // success! return callingMenu.nextMenu(cb); }); @@ -72,7 +72,7 @@ function prevMenu(callingMenu, formData, extraArgs, cb) { callingMenu.prevMenu( err => { if(err) { - callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!'); + callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!'); } return cb(err); }); @@ -119,7 +119,7 @@ function nextConf(callingMenu, formData, extraArgs, cb) { if(err) { return cb(err); // logged within changeMessageConference() } - + return reloadMenu(callingMenu, cb); }); } @@ -132,7 +132,7 @@ function prevArea(callingMenu, formData, extraArgs, cb) { if(err) { return cb(err); // logged within changeMessageArea() } - + return reloadMenu(callingMenu, cb); }); } @@ -155,10 +155,10 @@ function nextArea(callingMenu, formData, extraArgs, cb) { } function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { - const username = formData.value.username || callingMenu.client.user.username; + const username = formData.value.username || callingMenu.client.user.username; const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; - + WebPasswordReset.sendForgotPasswordEmail(username, err => { if(err) { callingMenu.client.log.warn( { err : err.message }, 'Failed sending forgot password email'); @@ -166,8 +166,8 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { if(extraArgs.next) { return callingMenu.gotoMenu(extraArgs.next, cb); - } - - return logoff(callingMenu, formData, extraArgs, cb); + } + + return logoff(callingMenu, formData, extraArgs, cb); }); } diff --git a/core/system_view_validate.js b/core/system_view_validate.js index e2a5b2e0..beb6bcce 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -99,7 +99,7 @@ function validateGeneralMailAddressedTo(data, cb) { function validateEmailAvail(data, cb) { // // This particular method allows empty data - e.g. no email entered - // + // if(!data || 0 === data.length) { return cb(null); } @@ -110,7 +110,7 @@ function validateEmailAvail(data, cb) { // // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation // - const emailRegExp = /[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; + const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; if(!emailRegExp.test(data)) { return cb(new Error('Invalid email address')); } @@ -121,8 +121,8 @@ function validateEmailAvail(data, cb) { } else if(uids.length > 0) { return cb(new Error('Email address not unique')); } - - return cb(null); + + return cb(null); }); } diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index fa1754a5..30db1207 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -42,7 +42,7 @@ class TelnetClientConnection extends EventEmitter { this.client = client; } - + restorePipe() { if(!this.pipeRestored) { this.pipeRestored = true; @@ -68,14 +68,14 @@ class TelnetClientConnection extends EventEmitter { this.bridgeConnection.on('data', data => { this.client.term.rawWrite(data); - // + // // Wait for a terminal type request, and send it eactly once. // This is enough (in additional to other negotiations handled in telnet.js) // to get us in on most systems // if(!this.termSent && data.indexOf(IAC_DO_TERM_TYPE) > -1) { this.termSent = true; - this.bridgeConnection.write(this.getTermTypeNegotiationBuffer()); + this.bridgeConnection.write(this.getTermTypeNegotiationBuffer()); } }); @@ -102,9 +102,9 @@ class TelnetClientConnection extends EventEmitter { // actual/current terminal type. // let bufs = buffers(); - + bufs.push(new Buffer( - [ + [ 255, // IAC 250, // SB 24, // TERMINAL-TYPE @@ -113,9 +113,9 @@ class TelnetClientConnection extends EventEmitter { )); bufs.push( - new Buffer(this.client.term.termType), // e.g. "ansi" + new Buffer(this.client.term.termType), // e.g. "ansi" new Buffer( [ 255, 240 ] ) // IAC, SE - ); + ); return bufs.toBuffer(); } @@ -128,9 +128,9 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { this.config = options.menuConfig.config; // defaults - this.config.port = this.config.port || 23; + this.config.port = this.config.port || 23; } - + initSequence() { let clientTerminated; const self = this; @@ -158,7 +158,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { self.client.term.write(` Connecting to ${connectOpts.host}, please wait...\n`); const telnetConnection = new TelnetClientConnection(self.client); - + telnetConnection.on('connected', () => { self.client.log.info(connectOpts, 'Telnet bridge connection established'); diff --git a/core/text_view.js b/core/text_view.js index f1b3ee7e..8bd38213 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -35,7 +35,7 @@ function TextView(options) { this.justify = options.justify || 'right'; this.resizable = miscUtil.valueWithDefault(options.resizable, true); this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); - + if(_.isString(options.textOverflow)) { this.textOverflow = options.textOverflow; } @@ -44,19 +44,19 @@ function TextView(options) { this.textMaskChar = options.textMaskChar; } -/* + /* this.drawText = function(s) { - // + // // |<- this.maxLength // ABCDEFGHIJK // |ABCDEFG| ^_ this.text.length // ^-- this.dimens.width // - let textToDraw = _.isString(this.textMaskChar) ? - new Array(s.length + 1).join(this.textMaskChar) : + let textToDraw = _.isString(this.textMaskChar) ? + new Array(s.length + 1).join(this.textMaskChar) : stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - + if(textToDraw.length > this.dimens.width) { if(this.hasFocus) { if(this.horizScroll) { @@ -64,7 +64,7 @@ function TextView(options) { } } else { if(textToDraw.length > this.dimens.width) { - if(this.textOverflow && + if(this.textOverflow && this.dimens.width > this.textOverflow.length && textToDraw.length - this.textOverflow.length >= this.textOverflow.length) { @@ -72,7 +72,7 @@ function TextView(options) { } else { textToDraw = textToDraw.substr(0, this.dimens.width); } - } + } } } @@ -89,7 +89,7 @@ function TextView(options) { this.drawText = function(s) { - // + // // |<- this.maxLength // ABCDEFGHIJK // |ABCDEFG| ^_ this.text.length @@ -97,26 +97,26 @@ function TextView(options) { // let renderLength = renderStringLength(s); // initial; may be adjusted below: - let textToDraw = _.isString(this.textMaskChar) ? - new Array(renderLength + 1).join(this.textMaskChar) : + let textToDraw = _.isString(this.textMaskChar) ? + new Array(renderLength + 1).join(this.textMaskChar) : stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - + renderLength = renderStringLength(textToDraw); - + if(renderLength >= this.dimens.width) { if(this.hasFocus) { if(this.horizScroll) { textToDraw = renderSubstr(textToDraw, renderLength - this.dimens.width, renderLength); } } else { - if(this.textOverflow && + if(this.textOverflow && this.dimens.width > this.textOverflow.length && renderLength - this.textOverflow.length >= this.textOverflow.length) { - textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; + textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; } else { textToDraw = renderSubstr(textToDraw, 0, this.dimens.width); - } + } } } @@ -128,7 +128,7 @@ function TextView(options) { this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), this.getStyleSGR(1) || this.getSGR() - ), + ), false // no converting CRLF needed ); }; @@ -136,7 +136,7 @@ function TextView(options) { this.getEndOfTextColumn = function() { var offset = Math.min(this.text.length, this.dimens.width); - return this.position.col + offset; + return this.position.col + offset; }; this.setText(options.text || '', false); // false=do not redraw now @@ -168,7 +168,7 @@ TextView.prototype.setFocus = function(focused) { TextView.super_.prototype.setFocus.call(this, focused); this.redraw(); - + this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); this.client.term.write(this.getFocusSGR()); }; @@ -184,7 +184,7 @@ TextView.prototype.setText = function(text, redraw) { text = text.toString(); } - text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. + text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. var widthDelta = 0; if(this.text && this.text !== text) { @@ -199,7 +199,7 @@ TextView.prototype.setText = function(text, redraw) { } // :TODO: it would be nice to be able to stylize strings with MCI and {special} MCI syntax, e.g. "|BN {UN!toUpper}" - this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); if(this.autoScale.width) { this.dimens.width = renderStringLength(this.text) + widthDelta; @@ -214,7 +214,7 @@ TextView.prototype.setText = function(text, redraw) { TextView.prototype.setText = function(text) { if(!_.isString(text)) { text = text.toString(); - } + } var widthDelta = 0; if(this.text && this.text !== text) { @@ -227,7 +227,7 @@ TextView.prototype.setText = function(text) { this.text = this.text.substr(0, this.maxLength); } - this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); //if(this.resizable) { // this.dimens.width = this.text.length + widthDelta; @@ -254,9 +254,9 @@ TextView.prototype.setPropertyValue = function(propName, value) { if(true === value) { this.textMaskChar = this.client.currentTheme.helpers.getPasswordChar(); } - break; + break; } - + TextView.super_.prototype.setPropertyValue.call(this, propName, value); }; diff --git a/core/theme.js b/core/theme.js index 0796b8ab..a1045a18 100644 --- a/core/theme.js +++ b/core/theme.js @@ -87,8 +87,8 @@ function loadTheme(themeID, cb) { if(err) { return cb(err); } - - if(!_.isObject(theme.info) || + + if(!_.isObject(theme.info) || !_.isString(theme.info.name) || !_.isString(theme.info.author)) { @@ -114,16 +114,16 @@ const IMMUTABLE_MCI_PROPERTIES = [ function getMergedTheme(menuConfig, promptConfig, theme) { assert(_.isObject(menuConfig)); assert(_.isObject(theme)); - - // :TODO: merge in defaults (customization.defaults{} ) - // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") - - // - // Create a *clone* of menuConfig (menu.hjson) then bring in - // promptConfig (prompt.hjson) - // + + // :TODO: merge in defaults (customization.defaults{} ) + // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") + + // + // Create a *clone* of menuConfig (menu.hjson) then bring in + // promptConfig (prompt.hjson) + // var mergedTheme = _.cloneDeep(menuConfig); - + if(_.isObject(promptConfig.prompts)) { mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); } @@ -136,8 +136,8 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // // merge customizer to disallow immutable MCI properties - // - var mciCustomizer = function(objVal, srcVal, key) { + // + var mciCustomizer = function(objVal, srcVal, key) { return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; }; @@ -159,69 +159,69 @@ function getMergedTheme(menuConfig, promptConfig, theme) { } else { if(_.has(src, [ formKey, 'mci' ])) { mergeMciProperties(dest, src[formKey].mci); - } + } } } - - // - // menu.hjson can have a couple different structures: - // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block - // (this allows multiple layout types defined by one menu for example) - // - // 2) Non-explicit declaration: 'mci' directly under 'form:' - // - // theme.hjson has it's own mix: - // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) - // - // 2) Non-explicit: 'mci' directly under an entry - // - // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up - // with menu.hjson in #1. - // - // * When theming an explicit menu.hjson entry (1), we will use a matching explicit - // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" - // and fall back to generic if a match is not found. - // - // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming - // there is a generic 'mci' block. - // - function applyToForm(form, menuTheme, formKey) { + + // + // menu.hjson can have a couple different structures: + // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block + // (this allows multiple layout types defined by one menu for example) + // + // 2) Non-explicit declaration: 'mci' directly under 'form:' + // + // theme.hjson has it's own mix: + // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) + // + // 2) Non-explicit: 'mci' directly under an entry + // + // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up + // with menu.hjson in #1. + // + // * When theming an explicit menu.hjson entry (1), we will use a matching explicit + // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" + // and fall back to generic if a match is not found. + // + // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming + // there is a generic 'mci' block. + // + function applyToForm(form, menuTheme, formKey) { if(_.isObject(form.mci)) { // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID applyThemeMciBlock(form.mci, menuTheme, formKey); - + } else { var menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { - return k === k.toUpperCase(); // remove anything not uppercase + return k === k.toUpperCase(); // remove anything not uppercase }); - + menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - var applyFrom; + var applyFrom; if(_.has(menuTheme, [ mciKey, 'mci' ])) { applyFrom = menuTheme[mciKey]; } else { applyFrom = menuTheme; } - + applyThemeMciBlock(form[mciKey].mci, applyFrom); }); } } - + [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { var createdFormSection = false; var mergedThemeMenu = mergedTheme[sectionName][menuName]; - + if(_.has(theme, [ 'customization', sectionName, menuName ])) { var menuTheme = theme.customization[sectionName][menuName]; - + // config block is direct assign/overwrite // :TODO: should probably be _.merge() if(menuTheme.config) { mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); } - + if('menus' === sectionName) { if(_.isObject(mergedThemeMenu.form)) { getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { @@ -232,7 +232,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // // Not specified at menu level means we apply anything from the // theme to form.0.mci{} - // + // mergedThemeMenu.form = { 0 : { mci : { } } }; mergeMciProperties(mergedThemeMenu.form[0], menuTheme); createdFormSection = true; @@ -241,9 +241,9 @@ function getMergedTheme(menuConfig, promptConfig, theme) { } else if('prompts' === sectionName) { // no 'form' or form keys for prompts -- direct to mci applyToForm(mergedThemeMenu, menuTheme); - } + } } - + // // Finished merging for this menu/prompt // @@ -259,13 +259,13 @@ function getMergedTheme(menuConfig, promptConfig, theme) { } }); }); - + return mergedTheme; } function initAvailableThemes(cb) { - + async.waterfall( [ function loadMenuConfig(callback) { @@ -285,9 +285,9 @@ function initAvailableThemes(cb) { } return callback( - null, - menuConfig, - promptConfig, + 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(); @@ -363,7 +363,7 @@ function setClientTheme(client, themeId) { logMsg = 'Failed setting theme by system default ID; Using the first one we can find'; } } - + client.log.debug( { themeId : themeId, info : client.currentTheme.info }, logMsg); } @@ -371,7 +371,7 @@ function getThemeArt(options, cb) { // // options - required: // name - // + // // options - optional // client - needed for user's theme/etc. // themeId @@ -388,7 +388,7 @@ function getThemeArt(options, cb) { // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... // :TODO: Some of these options should only be set if not provided! options.asAnsi = true; // always convert to ANSI - options.readSauce = true; // read SAUCE, if avail + options.readSauce = true; // read SAUCE, if avail options.random = _.get(options, 'random', true); // FILENAME.EXT support // @@ -406,7 +406,7 @@ function getThemeArt(options, cb) { // if('/' === options.name.charAt(0)) { // just take the path as-is - options.basePath = paths.dirname(options.name); + options.basePath = paths.dirname(options.name); } else if(options.name.indexOf('/') > -1) { // make relative to base BBS dir options.basePath = paths.join(__dirname, '../', paths.dirname(options.name)); @@ -432,7 +432,7 @@ function getThemeArt(options, cb) { if(artInfo || Config.defaults.theme === options.themeId) { return callback(null, artInfo); } - + options.basePath = paths.join(Config.paths.themes, Config.defaults.theme); art.getArt(options.name, options, (err, artInfo) => { return callback(null, artInfo); @@ -442,11 +442,11 @@ function getThemeArt(options, cb) { if(artInfo) { return callback(null, artInfo); } - + options.basePath = Config.paths.art; art.getArt(options.name, options, (err, artInfo) => { return callback(err, artInfo); - }); + }); } ], function complete(err, artInfo) { @@ -483,7 +483,7 @@ function displayThemeArt(options, cb) { /* function displayThemedPrompt(name, client, options, cb) { - + async.waterfall( [ function loadConfig(callback) { @@ -511,14 +511,14 @@ function displayThemedPrompt(name, client, options, cb) { // // If we did not clear the screen, don't let the font change - // + // const dispOptions = Object.assign( {}, promptConfig.options ); if(!options.clearScreen) { dispOptions.font = 'not_really_a_font!'; } displayThemedAsset( - promptConfig.art, + promptConfig.art, client, dispOptions, (err, artData) => { @@ -576,7 +576,7 @@ function displayThemedPrompt(name, client, options, cb) { } displayThemedAsset( - promptConfig.art, + promptConfig.art, client, dispOptions, (err, artInfo) => { @@ -593,7 +593,7 @@ function displayThemedPrompt(name, client, options, cb) { // no need to query cursor - we're not gonna use it return callback(null, promptConfig, artInfo); } - + client.once('cursor position report', pos => { artInfo.startRow = pos[0] - artInfo.height; return callback(null, promptConfig, artInfo); @@ -627,7 +627,7 @@ function displayThemedPrompt(name, client, options, cb) { if(options.clearPrompt) { if(artInfo.startRow && artInfo.height) { client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); - + // Note: Does not work properly in NetRunner < 2.0b17: client.term.rawWrite(ansi.deleteLine(artInfo.height)); } else { @@ -654,7 +654,7 @@ function displayThemedPrompt(name, client, options, cb) { // // Pause prompts are a special prompt by the name 'pause'. -// +// function displayThemedPause(client, options, cb) { if(!cb && _.isFunction(options)) { @@ -699,7 +699,7 @@ function displayThemedAsset(assetSpec, client, options, cb) { }); break; - case 'method' : + case 'method' : // :TODO: fetch & render via method break; diff --git a/core/tic_file_info.js b/core/tic_file_info.js index d2216d66..27e31e4c 100644 --- a/core/tic_file_info.js +++ b/core/tic_file_info.js @@ -28,7 +28,7 @@ module.exports = class TicFileInfo { static get requiredFields() { return [ - 'Area', 'Origin', 'From', 'File', 'Crc', + 'Area', 'Origin', 'From', 'File', 'Crc', // :TODO: validate this: //'Path', 'Seenby' // these two are questionable; some systems don't send them? ]; @@ -43,16 +43,16 @@ module.exports = class TicFileInfo { if(value) { // // We call toString() on values to ensure numbers, addresses, etc. are converted - // + // joinWith = joinWith || ''; if(Array.isArray(value)) { return value.map(v => v.toString() ).join(joinWith); } - + return value.toString(); } } - + get filePath() { return paths.join(paths.dirname(this.path), this.getAsString('File')); } @@ -86,7 +86,7 @@ module.exports = class TicFileInfo { const localInfo = { areaTag : config.localAreaTags.find( areaTag => areaTag.toUpperCase() === area ), }; - + if(!localInfo.areaTag) { return callback(Errors.Invalid(`No local area for "Area" of ${area}`)); } @@ -112,7 +112,7 @@ module.exports = class TicFileInfo { return callback(null, localInfo); }, function checksumAndSize(localInfo, callback) { - const crcTic = self.get('Crc'); + const crcTic = self.get('Crc'); const stream = fs.createReadStream(self.filePath); const crc = new CRC32(); let sizeActual = 0; @@ -193,7 +193,7 @@ module.exports = class TicFileInfo { // This is an optional keyword." // const to = this.get('To'); - + if(!to) { return allowNonExplicit; } @@ -219,10 +219,10 @@ module.exports = class TicFileInfo { let key; let value; let entry; - + lines.forEach(line => { keyEnd = line.search(/\s/); - + if(keyEnd < 0) { keyEnd = line.length; } @@ -253,7 +253,7 @@ module.exports = class TicFileInfo { value = parseInt(value, 16); break; - case 'size' : + case 'size' : value = parseInt(value, 10); break; diff --git a/core/ticker_text_view.js b/core/ticker_text_view.js deleted file mode 100644 index 6574880b..00000000 --- a/core/ticker_text_view.js +++ /dev/null @@ -1,94 +0,0 @@ -/* jslint node: true */ -'use strict'; - -var View = require('./view.js').View; -var miscUtil = require('./misc_util.js'); -var strUtil = require('./string_util.js'); -var ansi = require('./ansi_term.js'); -var util = require('util'); -var assert = require('assert'); - -exports.TickerTextView = TickerTextView; - -function TickerTextView(options) { - View.call(this, options); - - var self = this; - - this.text = options.text || ''; - this.tickerStyle = options.tickerStyle || 'rightToLeft'; - assert(this.tickerStyle in TickerTextView.TickerStyles); - - // :TODO: Ticker |text| should have ANSI stripped before calculating any lengths/etc. - // strUtil.ansiTextLength(s) - // strUtil.pad(..., ignoreAnsi) - // strUtil.stylizeString(..., ignoreAnsi) - - this.tickerState = {}; - switch(this.tickerStyle) { - case 'rightToLeft' : - this.tickerState.pos = this.position.row + this.dimens.width; - break; - } - - - self.onTickerInterval = function() { - switch(self.tickerStyle) { - case 'rightToLeft' : self.updateRightToLeftTicker(); break; - } - }; - - self.updateRightToLeftTicker = function() { - // if pos < start - // drawRemain() - // if pos + remain > end - // drawRemain(0, spaceFor) - // else - // drawString() + remainPading - }; - -} - -util.inherits(TickerTextView, View); - -TickerTextView.TickerStyles = { - leftToRight : 1, - rightToLeft : 2, - bounce : 3, - slamLeft : 4, - slamRight : 5, - slamBounce : 6, - decrypt : 7, - typewriter : 8, -}; -Object.freeze(TickerTextView.TickerStyles); - -/* -TickerTextView.TICKER_STYLES = [ - 'leftToRight', - 'rightToLeft', - 'bounce', - 'slamLeft', - 'slamRight', - 'slamBounce', - 'decrypt', - 'typewriter', -]; -*/ - -TickerTextView.prototype.controllerAttached = function() { - // :TODO: call super -}; - -TickerTextView.prototype.controllerDetached = function() { - // :TODO: call super - -}; - -TickerTextView.prototype.setText = function(text) { - this.text = strUtil.stylizeString(text, this.textStyle); - - if(!this.dimens || !this.dimens.width) { - this.dimens.width = Math.ceil(this.text.length / 2); - } -}; \ No newline at end of file diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index 35676193..27ae2169 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -1,13 +1,11 @@ /* jslint node: true */ 'use strict'; -var MenuView = require('./menu_view.js').MenuView; -var ansi = require('./ansi_term.js'); -var strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); -var util = require('util'); -var assert = require('assert'); -var _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); exports.ToggleMenuView = ToggleMenuView; @@ -44,7 +42,7 @@ ToggleMenuView.prototype.redraw = function() { var item = this.items[i]; var text = strUtil.stylizeString( item.text, i === this.focusedItemIndex && this.hasFocus ? this.focusTextStyle : this.textStyle); - + if(1 === i) { //console.log(this.styleColor1) //var sepColor = this.getANSIColor(this.styleColor1 || this.getColor()); diff --git a/core/upload.js b/core/upload.js index 5a49a0ca..b4130433 100644 --- a/core/upload.js +++ b/core/upload.js @@ -85,7 +85,7 @@ exports.getModule = class UploadModule extends MenuModule { fileDetailsContinue : (formData, extraArgs, cb) => { // see displayFileDetailsPageForUploadEntry() for this hackery: - cb(null); + cb(null); return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any }, @@ -119,7 +119,7 @@ exports.getModule = class UploadModule extends MenuModule { return cb(null); } - }; + }; } getSaveState() { @@ -143,12 +143,12 @@ exports.getModule = class UploadModule extends MenuModule { isBlindUpload() { return 'blind' === this.uploadType; } isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); } - + initSequence() { const self = this; if(0 === this.availAreas.length) { - // + // return this.gotoMenu(this.menuConfig.config.noUploadAreasAvailMenu || 'fileBaseNoUploadAreasAvail'); } @@ -185,7 +185,7 @@ exports.getModule = class UploadModule extends MenuModule { // need a terminator for various external protocols this.tempRecvDirectory = pathWithTerminatingSeparator(tempRecvDirectory); - + const modOpts = { extraArgs : { recvDirectory : this.tempRecvDirectory, // we'll move files from here to their area container once processed/confirmed @@ -203,8 +203,8 @@ exports.getModule = class UploadModule extends MenuModule { // Upon completion, we'll re-enter the module with some file paths handed to us // return this.gotoMenu( - this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', - modOpts, + this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', + modOpts, cb ); }); @@ -219,7 +219,7 @@ exports.getModule = class UploadModule extends MenuModule { const fmtObj = Object.assign( {}, stepInfo); let stepIndicatorFmt = ''; - let logStepFmt; + let logStepFmt; const fmtConfig = this.menuConfig.config; @@ -228,7 +228,7 @@ exports.getModule = class UploadModule extends MenuModule { const indicator = { }; const self = this; - + function updateIndicator(mci, isFinished) { indicator.mci = mci; @@ -253,7 +253,7 @@ exports.getModule = class UploadModule extends MenuModule { updateIndicator(MciViewIds.processing.calcHashIndicator); break; - case 'hash_finish' : + case 'hash_finish' : stepIndicatorFmt = fmtConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; updateIndicator(MciViewIds.processing.calcHashIndicator, true); break; @@ -263,7 +263,7 @@ exports.getModule = class UploadModule extends MenuModule { updateIndicator(MciViewIds.processing.archiveListIndicator); break; - case 'archive_list_finish' : + case 'archive_list_finish' : fmtObj.archivedFileCount = stepInfo.archiveEntries.length; stepIndicatorFmt = fmtConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; updateIndicator(MciViewIds.processing.archiveListIndicator, true); @@ -273,7 +273,7 @@ exports.getModule = class UploadModule extends MenuModule { stepIndicatorFmt = fmtConfig.extractArchiveListFailedFormat || 'Archive list extraction failed'; break; - case 'desc_files_start' : + case 'desc_files_start' : stepIndicatorFmt = fmtConfig.processingDescFilesFormat || 'Processing description files'; updateIndicator(MciViewIds.processing.descFileIndicator); break; @@ -289,7 +289,7 @@ exports.getModule = class UploadModule extends MenuModule { } fmtObj.stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj); - + if(this.hasProcessingArt) { this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj, { appendMultiLine : true } ); @@ -339,7 +339,7 @@ exports.getModule = class UploadModule extends MenuModule { return nextScanStep(null); } - self.client.log.debug('Scanning file', { filePath : filePath } ); + self.client.log.debug('Scanning file', { filePath : filePath } ); scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => { if(err) { @@ -389,7 +389,7 @@ exports.getModule = class UploadModule extends MenuModule { // name changed; ajust before persist newEntry.fileName = paths.basename(finalPath); } - + return nextEntry(null); // still try next file } @@ -474,7 +474,7 @@ exports.getModule = class UploadModule extends MenuModule { if(err) { return nextDupe(err); } - + const areaInfo = getFileAreaByTag(dupe.areaTag); if(areaInfo) { dupe.areaName = areaInfo.name; @@ -553,12 +553,12 @@ exports.getModule = class UploadModule extends MenuModule { return callback(null, scanResults); } - return self.displayDupesPage(scanResults.dupes, () => { + return self.displayDupesPage(scanResults.dupes, () => { return callback(null, scanResults); }); }, function prepDetails(scanResults, callback) { - return self.prepDetailsForUpload(scanResults, callback); + return self.prepDetailsForUpload(scanResults, callback); }, function startMovingAndPersistingToDatabase(scanResults, callback) { // @@ -583,14 +583,14 @@ exports.getModule = class UploadModule extends MenuModule { displayOptionsPage(cb) { const self = this; - + async.series( [ function prepArtAndViewController(callback) { return self.prepViewControllerWithArt( - 'options', - FormIds.options, - { clearScreen : true, trailingLF : false }, + 'options', + FormIds.options, + { clearScreen : true, trailingLF : false }, callback ); }, @@ -621,7 +621,7 @@ exports.getModule = class UploadModule extends MenuModule { fileNameView.setText(sanatizeFilename(fileNameView.getData())); } }); - + self.uploadType = 'blind'; uploadTypeView.setFocusItemIndex(0); // default to blind fileNameView.setText(blindFileNameText); @@ -658,14 +658,14 @@ exports.getModule = class UploadModule extends MenuModule { displayFileDetailsPageForUploadEntry(fileEntry, cb) { const self = this; - + async.waterfall( [ function prepArtAndViewController(callback) { return self.prepViewControllerWithArt( - 'fileDetails', + 'fileDetails', FormIds.fileDetails, - { clearScreen : true, trailingLF : false }, + { clearScreen : true, trailingLF : false }, err => { return callback(err); } diff --git a/core/user.js b/core/user.js index 09d26163..76c493ea 100644 --- a/core/user.js +++ b/core/user.js @@ -49,7 +49,7 @@ module.exports = class User { active : 2, }; } - + isAuthenticated() { return true === this.authenticated; } @@ -83,7 +83,7 @@ module.exports = class User { groupNames = [ groupNames ]; } - const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); + const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); return isMember; } @@ -91,11 +91,11 @@ module.exports = class User { if(this.isRoot() || this.isGroupMember('sysops')) { return 100; } - + if(this.isGroupMember('users')) { return 30; } - + return 10; // :TODO: Is this what we want? } @@ -203,14 +203,14 @@ module.exports = class User { if(err) { return callback(err); } - + self.userId = this.lastID; // Do not require activation for userId 1 (root/admin) if(User.RootUserID === self.userId) { self.properties.account_status = User.AccountStatus.active; } - + return callback(null, trans); } ); @@ -220,7 +220,7 @@ module.exports = class User { if(err) { return callback(err); } - + self.properties.pw_pbkdf2_salt = info.salt; self.properties.pw_pbkdf2_dk = info.dk; return callback(null, trans); @@ -283,8 +283,8 @@ module.exports = class User { userDb.run( `REPLACE INTO user_property (user_id, prop_name, prop_value) - VALUES (?, ?, ?);`, - [ this.userId, propName, propValue ], + VALUES (?, ?, ?);`, + [ this.userId, propName, propValue ], err => { if(cb) { return cb(err); @@ -334,7 +334,7 @@ module.exports = class User { if(err) { return cb(err); } - + stmt.finalize( () => { return cb(null); }); @@ -346,7 +346,7 @@ module.exports = class User { if(err) { return cb(err); } - + const newProperties = { pw_pbkdf2_salt : info.salt, pw_pbkdf2_dk : info.dk, @@ -395,7 +395,7 @@ module.exports = class User { } ); } - + static isRootUserId(userId) { return (User.RootUserID === userId); } @@ -466,11 +466,11 @@ module.exports = class User { if(err) { return cb(err); } - + if(row) { return cb(null, row.user_name); } - + return cb(Errors.DoesNotExist('No matching user ID')); } ); @@ -498,7 +498,7 @@ module.exports = class User { if(err) { return cb(err); } - properties[row.prop_name] = row.prop_value; + properties[row.prop_name] = row.prop_value; }, (err) => { return cb(err, err ? null : properties); }); @@ -512,12 +512,12 @@ module.exports = class User { `SELECT user_id FROM user_property WHERE prop_name = ? AND prop_value = ?;`, - [ propName, propValue ], + [ propName, propValue ], (err, row) => { if(row) { userIds.push(row.user_id); } - }, + }, () => { return cb(null, userIds); } @@ -557,7 +557,7 @@ module.exports = class User { return nextUser(err, user); } ); - }, + }, (err, transformed) => { return cb(err, transformed); }); @@ -594,14 +594,14 @@ module.exports = class User { }); } - static generatePasswordDerivedKey(password, salt, cb) { + static generatePasswordDerivedKey(password, salt, cb) { password = new Buffer(password).toString('hex'); crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', (err, dk) => { if(err) { return cb(err); } - + return cb(null, dk.toString('hex')); }); } diff --git a/core/user_config.js b/core/user_config.js index 432cdade..70e1f068 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -28,10 +28,10 @@ const MciCodeIds = { TermHeight : 8, Theme : 9, Password : 10, - PassConfirm : 11, + PassConfirm : 11, ThemeInfo : 20, ErrorMsg : 21, - + SaveCancel : 25, }; @@ -52,11 +52,11 @@ exports.getModule = class UserConfigModule extends MenuModule { if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { return cb(null); } - + // Otherwise we can use the standard system method return sysValidate.validateEmailAvail(data, cb); }, - + validatePassword : function(data, cb) { // // Blank is OK - this means we won't be changing it @@ -64,23 +64,23 @@ exports.getModule = class UserConfigModule extends MenuModule { if(!data || 0 === data.length) { return cb(null); } - + // Otherwise we can use the standard system method return sysValidate.validatePasswordSpec(data, cb); }, - + validatePassConfirmMatch : function(data, cb) { var passwordView = self.getView(MciCodeIds.Password); cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); }, - + viewValidationListener : function(err, cb) { var errMsgView = self.getView(MciCodeIds.ErrorMsg); var newFocusId; if(errMsgView) { if(err) { errMsgView.setText(err.message); - + if(err.view.getId() === MciCodeIds.PassConfirm) { newFocusId = MciCodeIds.Password; var passwordView = self.getView(MciCodeIds.Password); @@ -93,13 +93,13 @@ exports.getModule = class UserConfigModule extends MenuModule { } cb(newFocusId); }, - + // // Handlers // saveChanges : function(formData, extraArgs, cb) { assert(formData.value.password === formData.value.passwordConfirm); - + const newProperties = { real_name : formData.value.realName, birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), @@ -108,15 +108,15 @@ exports.getModule = class UserConfigModule extends MenuModule { affiliation : formData.value.affils, email_address : formData.value.email, web_address : formData.value.web, - term_height : formData.value.termHeight.toString(), + term_height : formData.value.termHeight.toString(), theme_id : self.availThemeInfo[formData.value.theme].themeId, }; - + // runtime set theme theme.setClientTheme(self.client, newProperties.theme_id); - + // persist all changes - self.client.user.persistProperties(newProperties, err => { + self.client.user.persistProperties(newProperties, err => { if(err) { self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); // :TODO: warn end user! @@ -126,7 +126,7 @@ exports.getModule = class UserConfigModule extends MenuModule { // New password if it's not empty // self.client.log.info('User updated properties'); - + if(formData.value.password.length > 0) { self.client.user.setNewAuthCredentials(formData.value.password, err => { if(err) { @@ -155,7 +155,7 @@ exports.getModule = class UserConfigModule extends MenuModule { } const self = this; - const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); + const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); let currentThemeIdIndex = 0; async.series( @@ -164,7 +164,7 @@ exports.getModule = class UserConfigModule extends MenuModule { vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); }, function prepareAvailableThemes(callback) { - self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) { + self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) { return { themeId : themeId, name : t.info.name, @@ -173,11 +173,11 @@ exports.getModule = class UserConfigModule extends MenuModule { group : _.isString(t.info.group) ? t.info.group : '', }; }), 'name'); - + currentThemeIdIndex = _.findIndex(self.availThemeInfo, function cmp(ti) { return ti.themeId === self.client.user.properties.theme_id; }); - + callback(null); }, function populateViews(callback) { @@ -191,19 +191,19 @@ exports.getModule = class UserConfigModule extends MenuModule { self.setViewText('menu', MciCodeIds.Email, user.properties.email_address); self.setViewText('menu', MciCodeIds.Web, user.properties.web_address); self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString()); - - + + var themeView = self.getView(MciCodeIds.Theme); if(themeView) { themeView.setItems(_.map(self.availThemeInfo, 'name')); themeView.setFocusItemIndex(currentThemeIdIndex); } - + var realNameView = self.getView(MciCodeIds.RealName); if(realNameView) { realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! } - + callback(null); } ], diff --git a/core/user_group.js b/core/user_group.js index 3903f2c3..db444296 100644 --- a/core/user_group.js +++ b/core/user_group.js @@ -1,11 +1,10 @@ /* jslint node: true */ 'use strict'; -var userDb = require('./database.js').dbs.user; -var Config = require('./config.js').config; +const userDb = require('./database.js').dbs.user; -var async = require('async'); -var _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); exports.getGroupsForUser = getGroupsForUser; exports.addUserToGroup = addUserToGroup; @@ -13,23 +12,22 @@ exports.addUserToGroups = addUserToGroups; exports.removeUserFromGroup = removeUserFromGroup; function getGroupsForUser(userId, cb) { - var sql = - 'SELECT group_name ' + - 'FROM user_group_member ' + - 'WHERE user_id=?;'; + const sql = + `SELECT group_name + FROM user_group_member + WHERE user_id=?;`; - var groups = []; + const groups = []; - userDb.each(sql, [ userId ], function rowData(err, row) { + userDb.each(sql, [ userId ], (err, row) => { if(err) { - cb(err); - return; - } else { - groups.push(row.group_name); + return cb(err); } + + groups.push(row.group_name); }, - function complete() { - cb(null, groups); + () => { + return cb(null, groups); }); } @@ -40,31 +38,31 @@ function addUserToGroup(userId, groupName, transOrDb, cb) { } transOrDb.run( - 'REPLACE INTO user_group_member (group_name, user_id) ' + - 'VALUES(?, ?);', + `REPLACE INTO user_group_member (group_name, user_id) + VALUES(?, ?);`, [ groupName, userId ], - function complete(err) { - cb(err); + err => { + return cb(err); } ); } function addUserToGroups(userId, groups, transOrDb, cb) { - async.each(groups, function item(groupName, next) { - addUserToGroup(userId, groupName, transOrDb, next); - }, function complete(err) { - cb(err); + async.each(groups, (groupName, nextGroupName) => { + return addUserToGroup(userId, groupName, transOrDb, nextGroupName); + }, err => { + return cb(err); }); } function removeUserFromGroup(userId, groupName, cb) { userDb.run( - 'DELETE FROM user_group_member ' + - 'WHERE group_name=? AND user_id=?;', + `DELETE FROM user_group_member + WHERE group_name=? AND user_id=?;`, [ groupName, userId ], - function complete(err) { - cb(err); + err => { + return cb(err); } ); } diff --git a/core/user_list.js b/core/user_list.js index be85c586..30313a28 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -12,7 +12,7 @@ const _ = require('lodash'); /* Available listFormat/focusListFormat object members: - + userId : User ID userName : User name/handle lastLoginTs : Last login timestamp @@ -99,7 +99,7 @@ exports.getModule = class UserListModule extends MenuModule { userListView.redraw(); callback(null); } - ], + ], function complete(err) { if(err) { self.client.log.error( { error : err.toString() }, 'Error loading user list'); @@ -108,5 +108,5 @@ exports.getModule = class UserListModule extends MenuModule { } ); }); - } + } }; diff --git a/core/user_login.js b/core/user_login.js index 4bd9176c..37c3b306 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -39,8 +39,8 @@ function userLogin(client, username, password, cb) { if(existingClientConnection) { client.log.info( { - existingClientId : existingClientConnection.session.id, - username : user.username, + existingClientId : existingClientConnection.session.id, + username : user.username, userId : user.userId }, 'Already logged in' @@ -57,7 +57,7 @@ function userLogin(client, username, password, cb) { // update client logger with addition of username client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username }); - client.log.info('Successful login'); + client.log.info('Successful login'); async.parallel( [ @@ -72,7 +72,7 @@ function userLogin(client, username, password, cb) { return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); }, function updateUserLoginCount(callback) { - return StatLog.incrementUserStat(user, 'login_count', 1, callback); + return StatLog.incrementUserStat(user, 'login_count', 1, callback); }, function recordLoginHistory(callback) { const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers diff --git a/core/uuid_util.js b/core/uuid_util.js index d8023f95..de64ec30 100644 --- a/core/uuid_util.js +++ b/core/uuid_util.js @@ -9,15 +9,15 @@ function createNamedUUID(namespaceUuid, key) { // // v5 UUID generation code based on the work here: // https://github.com/download13/uuidv5/blob/master/uuid.js - // + // if(!Buffer.isBuffer(namespaceUuid)) { namespaceUuid = new Buffer(namespaceUuid); } - + if(!Buffer.isBuffer(key)) { key = new Buffer(key); } - + let digest = createHash('sha1').update( Buffer.concat( [ namespaceUuid, key ] )).digest(); @@ -31,8 +31,8 @@ function createNamedUUID(namespaceUuid, key) { u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 u[9] = digest[9]; - + digest.copy(u, 10, 10, 16); - + return u; } \ No newline at end of file diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 2cc2ad7a..847b9407 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -15,7 +15,7 @@ exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { options.cursor = options.cursor || 'hide'; options.justify = options.justify || 'right'; // :TODO: default to center - + MenuView.call(this, options); const self = this; @@ -80,7 +80,7 @@ function VerticalMenuView(options) { self.client.term.write( ansi.goto(item.row, self.position.col) + - sgr + + sgr + strUtil.pad(text, this.dimens.width, this.fillChar, this.justify) ); }; @@ -89,7 +89,7 @@ function VerticalMenuView(options) { util.inherits(VerticalMenuView, MenuView); VerticalMenuView.prototype.redraw = function() { - VerticalMenuView.super_.prototype.redraw.call(this); + VerticalMenuView.super_.prototype.redraw.call(this); // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such if(this.positionCacheExpired) { @@ -106,14 +106,14 @@ VerticalMenuView.prototype.redraw = function() { let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; let row = this.position.row + 1; const endRow = (row + this.oldDimens.height) - 2; - + while(row <= endRow) { seq += ansi.goto(row, this.position.col) + blank; row += 1; } this.client.term.write(seq); delete this.oldDimens; - } + } if(this.items.length) { let row = this.position.row; @@ -206,7 +206,7 @@ VerticalMenuView.prototype.removeItem = function(index) { VerticalMenuView.prototype.focusNext = function() { if(this.items.length - 1 === this.focusedItemIndex) { this.focusedItemIndex = 0; - + this.viewWindow = { top : 0, bottom : Math.min(this.maxVisibleItems, this.items.length) - 1 @@ -228,7 +228,7 @@ VerticalMenuView.prototype.focusNext = function() { VerticalMenuView.prototype.focusPrevious = function() { if(0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; - + this.viewWindow = { //top : this.items.length - this.maxVisibleItems, top : Math.max(this.items.length - this.maxVisibleItems, 0), @@ -279,7 +279,7 @@ 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 } diff --git a/core/view.js b/core/view.js index fccca541..1829f26d 100644 --- a/core/view.js +++ b/core/view.js @@ -39,7 +39,7 @@ function View(options) { var self = this; this.client = options.client; - + this.cursor = options.cursor || 'show'; this.cursorStyle = options.cursorStyle || 'default'; @@ -72,7 +72,7 @@ function View(options) { } else { this.dimens = { width : options.width || 0, - height : 0 + height : 0 }; } @@ -106,7 +106,7 @@ function View(options) { this.restoreCursor = function() { //this.client.term.write(ansi.setCursorStyle(this.cursorStyle)); this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor()); - }; + }; } util.inherits(View, events.EventEmitter); @@ -150,7 +150,7 @@ View.prototype.setDimension = function(dimens) { View.prototype.setHeight = function(height) { height = parseInt(height) || 1; - height = Math.min(height, this.client.term.termHeight); + height = Math.min(height, this.client.term.termHeight); this.dimens.height = height; this.autoScale.height = false; @@ -182,9 +182,9 @@ View.prototype.setPropertyValue = function(propName, value) { case 'height' : this.setHeight(value); break; case 'width' : this.setWidth(value); break; case 'focus' : this.setFocus(value); break; - - case 'text' : - if('setText' in this) { + + case 'text' : + if('setText' in this) { this.setText(value); } break; @@ -248,7 +248,7 @@ View.prototype.setFocus = function(focused) { this.restoreCursor(); }; -View.prototype.onKeyPress = function(ch, key) { +View.prototype.onKeyPress = function(ch, key) { enigAssert(this.hasFocus, 'View does not have focus'); enigAssert(this.acceptsInput, 'View does not accept input'); diff --git a/core/view_controller.js b/core/view_controller.js index ecf36be5..55b1bd1c 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -22,7 +22,7 @@ var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/; function ViewController(options) { assert(_.isObject(options)); assert(_.isObject(options.client)); - + events.EventEmitter.call(this); var self = this; @@ -54,14 +54,14 @@ function ViewController(options) { self.client.log.warn( { err : err }, 'Error during handleAction()'); } } - + self.waitActionCompletion = false; }); }; this.clientKeyPressHandler = function(ch, key) { // - // Process key presses treating form submit mapped keys special. + // Process key presses treating form submit mapped keys special. // Everything else is forwarded on to the focused View, if any. // var actionForKey = key ? self.actionKeyMap[key.name] : self.actionKeyMap[ch]; @@ -92,7 +92,7 @@ function ViewController(options) { self.nextFocus(); break; - case 'accept' : + case 'accept' : if(self.focusedView && self.focusedView.submit) { // :TODO: need to do validation here!!! var focusedView = self.focusedView; @@ -166,37 +166,23 @@ function ViewController(options) { var propAsset; var propValue; - function callModuleMethod(path) { - if('' === paths.extname(path)) { - path += '.js'; - } - - try { - var methodMod = require(path); - // :TODO: fix formData & extraArgs - return methodMod[propAsset.asset](self.client.currentMenuModule, {}, {} ); - } catch(e) { - self.client.log.error( { error : e.toString(), methodName : propAsset.asset }, 'Failed to execute asset method'); - } - } - - for(var propName in conf) { + for(var propName in conf) { propAsset = asset.getViewPropertyAsset(conf[propName]); if(propAsset) { switch(propAsset.type) { case 'config' : - propValue = asset.resolveConfigAsset(conf[propName]); + propValue = asset.resolveConfigAsset(conf[propName]); break; - + case 'sysStat' : propValue = asset.resolveSystemStatAsset(conf[propName]); break; // :TODO: handle @art (e.g. text : @art ...) - case 'method' : + case 'method' : case 'systemMethod' : - if('validate' === propName) { + if('validate' === propName) { // :TODO: handle propAsset.location for @method script specification if('systemMethod' === propAsset.type) { // :TODO: implementation validation @systemMethod handling! @@ -211,7 +197,7 @@ function ViewController(options) { } } else { if(_.isString(propAsset.location)) { - + // :TODO: clean this code up! } else { if('systemMethod' === propAsset.type) { // :TODO: @@ -227,7 +213,7 @@ function ViewController(options) { } break; - default : + default : propValue = propValue = conf[propName]; break; } @@ -238,7 +224,7 @@ function ViewController(options) { if(!_.isUndefined(propValue)) { view.setPropertyValue(propName, propValue); } - } + } }; this.applyViewConfig = function(config, cb) { @@ -251,7 +237,7 @@ function ViewController(options) { if(null === mciMatch) { self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); return; - } + } var viewId = parseInt(mciMatch[2]); assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used @@ -261,7 +247,7 @@ function ViewController(options) { } var view = self.getView(viewId); - + if(!view) { self.client.log.warn( { viewId : viewId }, 'Cannot find view'); nextItem(null); @@ -278,7 +264,7 @@ function ViewController(options) { nextItem(null); }, - function complete(err) { + function complete(err) { // default to highest ID if no 'submit' entry present if(!submitId) { var highestIdView = self.getView(highestId); @@ -310,7 +296,7 @@ function ViewController(options) { if(_.isUndefined(actionValue)) { return false; } - + if(_.isNumber(actionValue) || _.isString(actionValue)) { if(_.isUndefined(formValue[actionValue])) { return false; @@ -337,10 +323,10 @@ function ViewController(options) { } self.client.log.trace( - { + { formValue : formValue, actionValue : actionValue - }, + }, 'Action match' ); @@ -412,7 +398,7 @@ ViewController.prototype.detachClientEvents = function() { if(!this.attached) { return; } - + this.client.removeListener('key press', this.clientKeyPressHandler); for(var id in this.views) { @@ -465,7 +451,7 @@ ViewController.prototype.switchFocus = function(id) { self.validateView(focusedView, function validated(err, newFocusedViewId) { if(err) { - var newFocusedView = self.getView(newFocusedViewId) || focusedView; + var newFocusedView = self.getView(newFocusedViewId) || focusedView; self.setViewFocusWithEvents(newFocusedView, true); } else { self.attachClientEvents(); @@ -524,7 +510,7 @@ ViewController.prototype.setViewOrder = function(order) { ViewController.prototype.redrawAll = function(initialFocusId) { this.client.term.rawWrite(ansi.hideCursor()); - + for(var id in this.views) { if(initialFocusId === id) { continue; // will draw @ focus @@ -538,7 +524,7 @@ ViewController.prototype.redrawAll = function(initialFocusId) { ViewController.prototype.loadFromPromptConfig = function(options, cb) { assert(_.isObject(options)); assert(_.isObject(options.mciMap)); - + var self = this; var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig; var initialFocusId = 1; // default to first @@ -560,7 +546,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { callback(null); } }, - function prepareFormSubmission(callback) { + function prepareFormSubmission(callback) { if(false === self.noInput) { self.on('submit', function promptSubmit(formData) { @@ -610,7 +596,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { // // * 'keys' must be present and be an array of key names // * If 'viewId' is present, key(s) will focus & submit on behalf - // of the specified view. + // of the specified view. // * If 'action' is present, that action will be procesed when // triggered by key(s) // @@ -681,7 +667,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { callback(err); }); }, - /* + /* function applyThemeCustomization(callback) { formConfig = formConfig || {}; formConfig.mci = formConfig.mci || {}; @@ -701,7 +687,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { //console.log('after theme...') //console.log(self.client.currentMenuModule.menuConfig.config) - + callback(null); }, */ @@ -764,7 +750,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { // // * 'keys' must be present and be an array of key names // * If 'viewId' is present, key(s) will focus & submit on behalf - // of the specified view. + // of the specified view. // * If 'action' is present, that action will be procesed when // triggered by key(s) // @@ -807,7 +793,7 @@ ViewController.prototype.formatMCIString = function(format) { return format.replace(/{(\d+)}/g, function replacer(match, number) { view = self.getView(number); - + if(!view) { return match; } diff --git a/core/web_password_reset.js b/core/web_password_reset.js index c2d6852d..6ea1e6f8 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -13,13 +13,12 @@ const Log = require('./logger.js').log; // deps const async = require('async'); -const _ = require('lodash'); const crypto = require('crypto'); const fs = require('graceful-fs'); const url = require('url'); const querystring = require('querystring'); -const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = +const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = `%USERNAME%: a password reset has been requested for your account on %BOARDNAME%. @@ -46,7 +45,7 @@ class WebPasswordReset { } async.waterfall( - [ + [ function getEmailAddress(callback) { if(!username) { return callback(Errors.MissingParam('Missing "username"')); @@ -81,7 +80,7 @@ class WebPasswordReset { email_password_reset_token : token, email_password_reset_token_ts : getISOTimestampString(), }; - + // we simply place the reset token in the user's properties user.persistProperties(newProperties, err => { return callback(err, user); @@ -111,13 +110,13 @@ class WebPasswordReset { .replace(/%USERNAME%/g, user.username) .replace(/%TOKEN%/g, user.properties.email_password_reset_token) .replace(/%RESET_URL%/g, resetUrl) - ; + ; } textTemplate = replaceTokens(textTemplate); if(htmlTemplate) { htmlTemplate = replaceTokens(htmlTemplate); - } + } const message = { to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, @@ -128,7 +127,11 @@ class WebPasswordReset { }; sendMail(message, (err, info) => { - // :TODO: Log me! + if(err) { + Log.warn( { error : err.message }, 'Failed sending password reset email' ); + } else { + Log.debug( { info : info }, 'Successfully sent password reset email'); + } return callback(err); }); @@ -162,7 +165,7 @@ class WebPasswordReset { path : '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate handler : WebPasswordReset.routeResetPasswordGet, }, - // POST handler for performing the actual reset + // POST handler for performing the actual reset { method : 'POST', path : '^\\/reset_password$', diff --git a/core/whos_online.js b/core/whos_online.js index 6abd76ef..f832bc10 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -53,7 +53,7 @@ exports.getModule = class WhosOnlineModule extends MenuModule { const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In'; const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A'; const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height); - + onlineListView.setItems(_.map(onlineList, oe => { if(oe.authenticated) { oe.timeOn = _.upperFirst(oe.timeOn.humanize()); diff --git a/core/word_wrap.js b/core/word_wrap.js index 0a4b122d..f246ad23 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -1,29 +1,31 @@ /* jslint node: true */ 'use strict'; -var assert = require('assert'); -var _ = require('lodash'); -const renderStringLength = require('./string_util.js').renderStringLength; +const renderStringLength = require('./string_util.js').renderStringLength; -exports.wordWrapText = wordWrapText2; +// deps +const assert = require('assert'); +const _ = require('lodash'); + +exports.wordWrapText = wordWrapText; const SPACE_CHARS = [ - ' ', '\f', '\n', '\r', '\v', + ' ', '\f', '\n', '\r', '\v', '​\u00a0', '\u1680', '​\u180e', '\u2000​', '\u2001', '\u2002', '​\u2003', '\u2004', - '\u2005', '\u2006​', '\u2007', '\u2008​', '\u2009', '\u200a​', '\u2028', '\u2029​', + '\u2005', '\u2006​', '\u2007', '\u2008​', '\u2009', '\u200a​', '\u2028', '\u2029​', '\u202f', '\u205f​', '\u3000', ]; -const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); +const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); -function wordWrapText2(text, options) { +function wordWrapText(text, options) { assert(_.isObject(options)); assert(_.isNumber(options.width)); - + options.tabHandling = options.tabHandling || 'expand'; options.tabWidth = options.tabWidth || 4; - options.tabChar = options.tabChar || ' '; - + options.tabChar = options.tabChar || ' '; + //const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g'); // // For a given word, match 0->options.width chars -- alwasy include a full trailing ESC @@ -31,7 +33,7 @@ function wordWrapText2(text, options) { // // :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 word; let c; @@ -39,30 +41,30 @@ function wordWrapText2(text, options) { let i = 0; let wordStart = 0; let result = { wrapped : [ '' ], renderLen : [] }; - + function expandTab(column) { const remainWidth = options.tabWidth - (column % options.tabWidth); return new Array(remainWidth).join(options.tabChar); } - + function appendWord() { word.match(REGEXP_GOBBLE).forEach( w => { renderLen = renderStringLength(w); - + if(result.renderLen[i] + renderLen > options.width) { if(0 === i) { result.firstWrapRange = { start : wordStart, end : wordStart + w.length }; } - + result.wrapped[++i] = w; result.renderLen[i] = renderLen; } else { - result.wrapped[i] += w; - result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; + result.wrapped[i] += w; + result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; } }); } - + // // Some of the way we word wrap is modeled after Sublime Test 3: // @@ -74,10 +76,10 @@ function wordWrapText2(text, options) { // "\t" may resolve to " " and must fit within the space. // // * If a word is ultimately too long to fit, break it up until it does. - // + // while(null !== (m = REGEXP_WORD_WRAP.exec(text))) { word = text.substring(wordStart, REGEXP_WORD_WRAP.lastIndex - 1); - + c = m[0].charAt(0); if(SPACE_CHARS.indexOf(c) > -1) { word += m[0]; @@ -89,129 +91,13 @@ function wordWrapText2(text, options) { word += m[0]; } } - + appendWord(); wordStart = REGEXP_WORD_WRAP.lastIndex + m[0].length - 1; } - + word = text.substring(wordStart); appendWord(); - + return result; } - -function wordWrapText(text, options) { - // - // options.*: - // width : word wrap width - // tabHandling : expand (default=expand) - // tabWidth : tab width if tabHandling is 'expand' (default=4) - // tabChar : character to use for tab expansion - // - assert(_.isObject(options), 'Missing options!'); - assert(_.isNumber(options.width), 'Missing options.width!'); - - options.tabHandling = options.tabHandling || 'expand'; - - if(!_.isNumber(options.tabWidth)) { - options.tabWidth = 4; - } - - options.tabChar = options.tabChar || ' '; - - // - // Notes - // * Sublime Text 3 for example considers spaces after a word - // part of said word. For example, "word " would be wraped - // in it's entirity. - // - // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. - // "\t" may resolve to " " and must fit within the space. - // - // * If a word is ultimately too long to fit, break it up until it does. - // - // RegExp below is JavaScript '\s' minus the '\t' - // - var re = new RegExp( - '\t|[ \f\n\r\v​\u00a0\u1680​\u180e\u2000​\u2001\u2002​\u2003\u2004\u2005\u2006​' + - '\u2007\u2008​\u2009\u200a​\u2028\u2029​\u202f\u205f​\u3000]', 'g'); - var m; - var wordStart = 0; - var results = { wrapped : [ '' ] }; - var i = 0; - var word; - var wordLen; - - function expandTab(col) { - var remainWidth = options.tabWidth - (col % options.tabWidth); - return new Array(remainWidth).join(options.tabChar); - } - - // :TODO: support wrapping pipe code text (e.g. ignore color codes, expand MCI codes) - - function addWord() { - word.match(new RegExp('.{0,' + options.width + '}', 'g')).forEach(function wrd(w) { - //wordLen = self.getStringLength(w); - - if(results.wrapped[i].length + w.length > options.width) { - //if(results.wrapped[i].length + wordLen > width) { - if(0 === i) { - results.firstWrapRange = { start : wordStart, end : wordStart + w.length }; - //results.firstWrapRange = { start : wordStart, end : wordStart + wordLen }; - } - // :TODO: Must handle len of |w| itself > options.width & split how ever many times required (e.g. handle paste) - results.wrapped[++i] = w; - } else { - results.wrapped[i] += w; - } - }); - } - - while((m = re.exec(text)) !== null) { - word = text.substring(wordStart, re.lastIndex - 1); - - switch(m[0].charAt(0)) { - case ' ' : - word += m[0]; - break; - - case '\t' : - // - // Expand tab given position - // - // Nice info here: http://c-for-dummies.com/blog/?p=424 - // - if('expand' === options.tabHandling) { - word += expandTab(results.wrapped[i].length + word.length) + options.tabChar; - } else { - word += m[0]; - } - break; - } - - addWord(); - wordStart = re.lastIndex + m[0].length - 1; - } - - // - // Remainder - // - word = text.substring(wordStart); - addWord(); - - return results; -} - -//const input = 'Hello, |04World! This |08i|02s a test it is \x1b[20Conly a test of the emergency broadcast system. What you see is not a joke!'; -//const input = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five enturies, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; - -/* -const iconv = require('iconv-lite'); -const input = iconv.decode(require('graceful-fs').readFileSync('/home/nuskooler/Downloads/msg_out.txt'), 'cp437'); - -const opts = { - width : 80, -}; - -console.log(wordWrapText2(input, opts).wrapped, 'utf8') -*/ \ No newline at end of file From a8d5e8477982ace10a8e0047f0244aeab605439d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 16:08:35 -0700 Subject: [PATCH 004/569] * Fix justification 'right' vs 'left': They were flipped (durp!). Right aligned is now really that, etc. You may need to update your theme.hjson/similar! --- art/themes/luciano_blocktronics/theme.hjson | 14 ++++++------ core/string_format.js | 6 +++--- core/string_util.js | 24 ++++++++++----------- core/text_view.js | 2 +- core/vertical_menu_view.js | 2 +- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 19f63194..433f8884 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -722,15 +722,15 @@ } SM4: { width: 14 - justify: right + justify: left } SM5: { width: 14 - justify: right + justify: left } SM6: { width: 14 - justify: right + justify: left } BT7: { focusTextStyle: first lower @@ -748,15 +748,15 @@ } SM3: { width: 14 - justify: right + justify: left } SM4: { width: 14 - justify: right + justify: left } SM5: { width: 14 - justify: right + justify: left } ET6: { width: 26 @@ -826,7 +826,7 @@ mci: { SM1: { width: 14 - justify: right + justify: left focusTextStyle: first lower } diff --git a/core/string_format.js b/core/string_format.js index eba715d5..38c2047f 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -122,10 +122,10 @@ function quote(s) { function getPadAlign(align) { return { - '<' : 'right', - '>' : 'left', + '<' : 'left', + '>' : 'right', '^' : 'center', - }[align] || '<'; + }[align] || '>'; } function formatString(value, tokens) { diff --git a/core/string_util.js b/core/string_util.js index a4544754..c846fc38 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -135,23 +135,21 @@ function stylizeString(s, style) { return s; } -// Based on http://www.webtoolkit.info/ -// :TODO: Look into lodash padLeft, padRight, etc. -function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { - len = miscUtil.valueWithDefault(len, 0); - padChar = miscUtil.valueWithDefault(padChar, ' '); - dir = miscUtil.valueWithDefault(dir, 'right'); - stringSGR = miscUtil.valueWithDefault(stringSGR, ''); - padSGR = miscUtil.valueWithDefault(padSGR, ''); - useRenderLen = miscUtil.valueWithDefault(useRenderLen, true); +function pad(s, len, padChar, justify, stringSGR, padSGR, useRenderLen) { + len = len || 0; + padChar = padChar || ' '; + justify = justify || 'left'; + stringSGR = stringSGR || ''; + padSGR = padSGR || ''; + useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; const renderLen = useRenderLen ? renderStringLength(s) : s.length; const padlen = len >= renderLen ? len - renderLen : 0; - switch(dir) { + switch(justify) { case 'L' : case 'left' : - s = padSGR + new Array(padlen).join(padChar) + stringSGR + s; + s = `${stringSGR}${s}${padSGR}${Array(padlen).join(padChar)}`; break; case 'C' : @@ -160,13 +158,13 @@ function pad(s, len, padChar, dir, stringSGR, padSGR, useRenderLen) { { const right = Math.ceil(padlen / 2); const left = padlen - right; - s = padSGR + new Array(left + 1).join(padChar) + stringSGR + s + padSGR + new Array(right + 1).join(padChar); + s = `${padSGR}${Array(left + 1).join(padChar)}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; } break; case 'R' : case 'right' : - s = stringSGR + s + padSGR + new Array(padlen).join(padChar); + s = `${padSGR}${Array(padlen).join(padChar)}${stringSGR}${s}`; break; default : break; diff --git a/core/text_view.js b/core/text_view.js index 8bd38213..ea9c352a 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -32,7 +32,7 @@ function TextView(options) { } this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'right'; + this.justify = options.justify || 'left'; this.resizable = miscUtil.valueWithDefault(options.resizable, true); this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 847b9407..9fe4cc7d 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -14,7 +14,7 @@ exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { options.cursor = options.cursor || 'hide'; - options.justify = options.justify || 'right'; // :TODO: default to center + options.justify = options.justify || 'left'; MenuView.call(this, options); From d1593ed159316a26b9df8768a100f8485e0449da Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 20:30:55 -0700 Subject: [PATCH 005/569] * Fix bug where 'submit' property was ignored in favor of highest MCI ID always; Will now properly set view with 'submit' to true else rely on highest ID --- core/view_controller.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/core/view_controller.js b/core/view_controller.js index 55b1bd1c..71911f28 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -228,25 +228,25 @@ function ViewController(options) { }; this.applyViewConfig = function(config, cb) { - var highestId = 1; - var submitId; - var initialFocusId = 1; + let highestId = 1; + let submitId; + let initialFocusId = 1; async.each(Object.keys(config.mci || {}), function entry(mci, nextItem) { - var mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? + const mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? if(null === mciMatch) { self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); return; } - var viewId = parseInt(mciMatch[2]); + const viewId = parseInt(mciMatch[2]); assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used if(viewId > highestId) { highestId = viewId; } - var view = self.getView(viewId); + const view = self.getView(viewId); if(!view) { self.client.log.warn( { viewId : viewId }, 'Cannot find view'); @@ -254,7 +254,7 @@ function ViewController(options) { return; } - var mciConf = config.mci[mci]; + const mciConf = config.mci[mci]; self.setViewPropertiesFromMCIConf(view, mciConf); @@ -262,9 +262,13 @@ function ViewController(options) { initialFocusId = viewId; } + if(true === view.submit) { + submitId = viewId; + } + nextItem(null); }, - function complete(err) { + err => { // default to highest ID if no 'submit' entry present if(!submitId) { var highestIdView = self.getView(highestId); @@ -275,7 +279,7 @@ function ViewController(options) { } } - cb(err, { initialFocusId : initialFocusId } ); + return cb(err, { initialFocusId : initialFocusId } ); }); }; From 827d793a2dae8ef5db725af4ac57017330443c20 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 20:31:37 -0700 Subject: [PATCH 006/569] Add notes about left/right justify --- UPGRADE.md | 5 +++++ WHATSNEW.md | 1 + 2 files changed, 6 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index c7651fcd..5975b96e 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -37,6 +37,11 @@ npm install Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). +# 0.0.8-alpha to 0.0.9-alpha +* Development is now against Node.js 8.x LTS. Follow your standard upgrade path to update to Node 8.x before using 0.0.9-alpha. +* The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well. + + # 0.0.7-alpha to 0.0.8-alpha ENiGMA 0.0.8-alpha comes with some structure changes: * Configuration files are defaulted to `./config`. Related, the `--config` option now points to a configuration **directory** diff --git a/WHATSNEW.md b/WHATSNEW.md index 594418a7..d5d5fee2 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -3,6 +3,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.9-alpha * Development is now against Node.js 8.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! +* Fixed `justify` properties: `left` and `right` values were formerly swapped (oops!) ## 0.0.8-alpha From 05a93cae891157d26aa72ff0fb58242ef37e6472 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 20:31:55 -0700 Subject: [PATCH 007/569] Default to left justification --- core/spinner_menu_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index e9145662..72b8b2f7 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -11,7 +11,7 @@ const assert = require('assert'); exports.SpinnerMenuView = SpinnerMenuView; function SpinnerMenuView(options) { - options.justify = options.justify || 'center'; + options.justify = options.justify || 'left'; options.cursor = options.cursor || 'hide'; MenuView.call(this, options); From 23e77dcb319e50c16a6d821c03f12d13d8eb8749 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 21:05:55 -0700 Subject: [PATCH 008/569] Uncommeng out a deprecated function - will fix later; need for now --- core/msg_area_list.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 96b0a51c..fcc44b23 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -99,8 +99,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule { } // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! - /* - updateGeneralAreaInfoViews(areaIndex) { + updateGeneralAreaInfoViews(areaIndex) { + /* const areaInfo = self.messageAreas[areaIndex]; [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { @@ -109,8 +109,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule { v.setFormatObject(areaInfo.area); } }); + */ } - */ mciReady(mciData, cb) { super.mciReady(mciData, err => { From 78ca1e9c4f8dedfd16d0b0577bbb02889ed202de Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 21:06:16 -0700 Subject: [PATCH 009/569] * Ensure explicit by-MCI key forms are properly themed, e.g. form: { 3: { HM1: { ... }}} --- core/theme.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/theme.js b/core/theme.js index a1045a18..5c056c49 100644 --- a/core/theme.js +++ b/core/theme.js @@ -122,7 +122,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // Create a *clone* of menuConfig (menu.hjson) then bring in // promptConfig (prompt.hjson) // - var mergedTheme = _.cloneDeep(menuConfig); + const mergedTheme = _.cloneDeep(menuConfig); if(_.isObject(promptConfig.prompts)) { mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); @@ -137,7 +137,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // // merge customizer to disallow immutable MCI properties // - var mciCustomizer = function(objVal, srcVal, key) { + const mciCustomizer = function(objVal, srcVal, key) { return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; }; @@ -191,30 +191,30 @@ function getMergedTheme(menuConfig, promptConfig, theme) { applyThemeMciBlock(form.mci, menuTheme, formKey); } else { - var menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { + const menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { return k === k.toUpperCase(); // remove anything not uppercase }); menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - var applyFrom; + let applyFrom; if(_.has(menuTheme, [ mciKey, 'mci' ])) { applyFrom = menuTheme[mciKey]; } else { applyFrom = menuTheme; } - applyThemeMciBlock(form[mciKey].mci, applyFrom); + applyThemeMciBlock(form[mciKey].mci, applyFrom, formKey); }); } } [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { - var createdFormSection = false; - var mergedThemeMenu = mergedTheme[sectionName][menuName]; + let createdFormSection = false; + const mergedThemeMenu = mergedTheme[sectionName][menuName]; if(_.has(theme, [ 'customization', sectionName, menuName ])) { - var menuTheme = theme.customization[sectionName][menuName]; + const menuTheme = theme.customization[sectionName][menuName]; // config block is direct assign/overwrite // :TODO: should probably be _.merge() From 16c8fd0afc57e2bd876e06df198e9807ee9f39ed Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 15 Jan 2018 21:40:13 -0700 Subject: [PATCH 010/569] Fix focusTextStyle for VerticalMenuView (lightbar) --- core/vertical_menu_view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 9fe4cc7d..a7e566eb 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -68,11 +68,11 @@ function VerticalMenuView(options) { const focusItem = self.focusItems[index]; text = strUtil.stylizeString( focusItem ? focusItem.text : item.text, - self.textStyle + self.focusTextStyle ); sgr = ''; } else { - text = strUtil.stylizeString(item.text, self.textStyle); + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } From b1cea5edd72868771e30aed37b729863501022af Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 15:16:10 -0700 Subject: [PATCH 011/569] Add in reason if available, to error message --- core/enig_error.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/enig_error.js b/core/enig_error.js index b0dd2335..c6eb8097 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -11,6 +11,10 @@ class EnigError extends Error { this.reason = reason; this.reasonCode = reasonCode; + if(this.reason) { + this.message += `: ${this.reason}`; + } + if(typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor); } else { From afe0c88cfc14a515ed1c3aee7efd73910b0056ad Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 15:16:35 -0700 Subject: [PATCH 012/569] NetMail non-HUB fixes * Properly separate FTN *packet* header vs *message* header DST/SRC information * Change routes{} handling: These are now *require* for out-of-HUB routing such that Enig will know where to send messages --- core/ftn_mail_packet.js | 97 ++++++++++++++++-------------- core/message.js | 8 +++ core/scanner_tossers/ftn_bso.js | 101 ++++++++++++++++---------------- 3 files changed, 113 insertions(+), 93 deletions(-) diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index d2f003e9..1c60e1f0 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -524,13 +524,13 @@ function Packet(options) { ); }; - this.parsePacketMessages = function(packetBuffer, iterator, cb) { + this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { binary.parse(packetBuffer) .word16lu('messageType') - .word16lu('ftn_orig_node') - .word16lu('ftn_dest_node') - .word16lu('ftn_orig_network') - .word16lu('ftn_dest_network') + .word16lu('ftn_msg_orig_node') + .word16lu('ftn_msg_dest_node') + .word16lu('ftn_msg_orig_net') + .word16lu('ftn_msg_dest_net') .word16lu('ftn_attr_flags') .word16lu('ftn_cost') .scan('modDateTime', NULL_TERM_BUFFER) // :TODO: 20 bytes max @@ -569,20 +569,28 @@ function Packet(options) { // contain an origin line, kludges, SAUCE in the case // of ANSI files, etc. // - let msg = new Message( { + const msg = new Message( { toUserName : convMsgData.toUserName, fromUserName : convMsgData.fromUserName, subject : convMsgData.subject, modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime), }); - msg.meta.FtnProperty = {}; - msg.meta.FtnProperty.ftn_orig_node = msgData.ftn_orig_node; - msg.meta.FtnProperty.ftn_dest_node = msgData.ftn_dest_node; - msg.meta.FtnProperty.ftn_orig_network = msgData.ftn_orig_network; - msg.meta.FtnProperty.ftn_dest_network = msgData.ftn_dest_network; - msg.meta.FtnProperty.ftn_attr_flags = msgData.ftn_attr_flags; - msg.meta.FtnProperty.ftn_cost = msgData.ftn_cost; + // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) + msg.meta.FtnProperty = { + ftn_orig_node : header.origNode, + ftn_dest_node : header.destNode, + ftn_orig_network : header.origNet, + ftn_dest_network : header.destNet, + + ftn_attr_flags : msgData.ftn_attr_flags, + ftn_cost : msgData.ftn_cost, + + ftn_msg_orig_node : msgData.ftn_msg_orig_node, + ftn_msg_dest_node : msgData.ftn_msg_dest_node, + ftn_msg_orig_net : msgData.ftn_msg_orig_net, + ftn_msg_dest_net : msgData.ftn_msg_dest_net, + }; self.processMessageBody(msgData.message, messageBodyData => { msg.message = messageBodyData.message; @@ -622,11 +630,11 @@ function Packet(options) { const nextBuf = packetBuffer.slice(read); if(nextBuf.length > 0) { - let next = function(e) { + const next = function(e) { if(e) { cb(e); } else { - self.parsePacketMessages(nextBuf, iterator, cb); + self.parsePacketMessages(header, nextBuf, iterator, cb); } }; @@ -651,6 +659,10 @@ function Packet(options) { Message.FtnPropertyNames.FtnOrigPoint, Message.FtnPropertyNames.FtnDestPoint, Message.FtnPropertyNames.FtnAttribute, + Message.FtnPropertyNames.FtnMsgOrigNode, + Message.FtnPropertyNames.FtnMsgDestNode, + Message.FtnPropertyNames.FtnMsgOrigNet, + Message.FtnPropertyNames.FtnMsgDestNet, ].forEach( propName => { if(message.meta.FtnProperty[propName]) { message.meta.FtnProperty[propName] = parseInt(message.meta.FtnProperty[propName]) || 0; @@ -658,6 +670,25 @@ function Packet(options) { }); }; + this.writeMessageHeader = function(message, buf) { + // ensure address FtnProperties are numbers + self.sanatizeFtnProperties(message); + + const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node; + const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network; + + buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); + buf.writeUInt16LE(destNode, 4); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6); + buf.writeUInt16LE(destNet, 8); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); + + const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0'); + dateTimeBuffer.copy(buf, 14); + }; + this.getMessageEntryBuffer = function(message, options, cb) { function getAppendMeta(k, m, sepChar=':') { @@ -678,20 +709,7 @@ function Packet(options) { [ function prepareHeaderAndKludges(callback) { const basicHeader = new Buffer(34); - - // ensure address FtnProperties are numbers - self.sanatizeFtnProperties(message); - - basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); - 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); + self.writeMessageHeader(message, basicHeader); // // To, from, and subject must be NULL term'd and have max lengths as per spec. @@ -808,17 +826,7 @@ function Packet(options) { this.writeMessage = function(message, ws, options) { let basicHeader = new Buffer(34); - - basicHeader.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); - basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); - 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); + self.writeMessageHeader(message, basicHeader); ws.write(basicHeader); @@ -911,7 +919,7 @@ function Packet(options) { }; this.parsePacketBuffer = function(packetBuffer, iterator, cb) { - async.series( + async.waterfall( [ function processHeader(callback) { self.parsePacketHeader(packetBuffer, (err, header) => { @@ -919,15 +927,16 @@ function Packet(options) { return callback(err); } - let next = function(e) { - callback(e); + const next = function(e) { + return callback(e, header); }; iterator('header', header, next); }); }, - function processMessages(callback) { + function processMessages(header, callback) { self.parsePacketMessages( + header, packetBuffer.slice(FTN_PACKET_HEADER_SIZE), iterator, callback); diff --git a/core/message.js b/core/message.js index 7cac5c79..f99efa8a 100644 --- a/core/message.js +++ b/core/message.js @@ -116,8 +116,10 @@ Message.StateFlags0 = { }; Message.FtnPropertyNames = { + // packet header oriented FtnOrigNode : 'ftn_orig_node', FtnDestNode : 'ftn_dest_node', + // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping FtnOrigNetwork : 'ftn_orig_network', FtnDestNetwork : 'ftn_dest_network', FtnAttrFlags : 'ftn_attr_flags', @@ -127,6 +129,12 @@ Message.FtnPropertyNames = { FtnOrigPoint : 'ftn_orig_point', FtnDestPoint : 'ftn_dest_point', + // message header oriented + FtnMsgOrigNode : 'ftn_msg_orig_node', + FtnMsgDestNode : 'ftn_msg_dest_node', + FtnMsgOrigNet : 'ftn_msg_orig_net', + FtnMsgDestNet : 'ftn_msg_dest_net', + FtnAttribute : 'ftn_attribute', FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 20ab435f..36ee815f 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -306,12 +306,26 @@ function FTNMessageScanTossModule() { // const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version + // :TODO: create Address.toMeta() / similar message.meta.FtnProperty = message.meta.FtnProperty || {}; message.meta.FtnKludge = message.meta.FtnKludge || {}; - message.meta.FtnProperty.ftn_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_orig_network = localAddress.net; - message.meta.FtnProperty.ftn_cost = 0; + message.meta.FtnProperty.ftn_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_orig_network = localAddress.net; + message.meta.FtnProperty.ftn_cost = 0; + message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; + + const destAddress = options.routeAddress || options.destAddress; + message.meta.FtnProperty.ftn_dest_node = destAddress.node; + message.meta.FtnProperty.ftn_dest_network = destAddress.net; + + if(destAddress.zone) { + message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; + } + if(destAddress.point) { + message.meta.FtnProperty.ftn_dest_point = destAddress.point; + } // tear line and origin can both go in EchoMail & NetMail message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); @@ -320,9 +334,11 @@ function FTNMessageScanTossModule() { let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system if(self.isNetMailMessage(message)) { - // These should be set for Private/NetMail already - assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_node))); - assert(_.isNumber(parseInt(message.meta.FtnProperty.ftn_dest_network))); + // + // Set route and message destination properties -- they may differ + // + message.meta.FtnProperty.ftn_msg_dest_node = options.destAddress.node; + message.meta.FtnProperty.ftn_msg_dest_net = options.destAddress.net; ftnAttribute |= ftnMailPacket.Packet.Attribute.Private; @@ -353,10 +369,6 @@ function FTNMessageScanTossModule() { message.meta.FtnKludge.TOPT = options.destAddress.point; } } else { - // We need to set some destination info for EchoMail - message.meta.FtnProperty.ftn_dest_node = options.destAddress.node; - message.meta.FtnProperty.ftn_dest_network = options.destAddress.net; - // // Set appropriate attribute flag for export type // @@ -573,7 +585,7 @@ function FTNMessageScanTossModule() { const packetHeader = new ftnMailPacket.PacketHeader( exportOpts.network.localAddress, - exportOpts.destAddress, + exportOpts.routeAddress, exportOpts.nodeConfig.packetType ); @@ -801,57 +813,44 @@ function FTNMessageScanTossModule() { return _.find(routes, (route, addrWildcard) => { return dstAddr.isPatternMatch(addrWildcard); }); - - /* - const route = _.find(routes, (route, addrWildcard) => { - return dstAddr.isPatternMatch(addrWildcard); - }); - - if(route && route.address) { - return Address.fromString(route.address); - } - */ }; - this.getAcceptableNetMailNetworkInfoFromAddress = function(dstAddr, cb) { + this.getNetMailRouteInfoFromAddress = function(destAddress, cb) { // - // Attempt to find an acceptable network configuration using the following - // lookup order (most to least explicit config): + // Attempt to find route information for |destAddress|: // // 1) Routes: messageNetworks.ftn.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config - // - Where we send may not be where dstAddress is (it's routed!) + // - Where we send may not be where destAddress is (it's routed!) // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config - // - Where we send is direct to dstAddr + // - Where we send is direct to destAddress // // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address // falling back to Config.scannerTossers.ftn_bso.defaultNetwork // - const route = this.getNetMailRoute(dstAddr); + const route = this.getNetMailRoute(destAddress); let routeAddress; let networkName; + let isRouted; if(route) { routeAddress = Address.fromString(route.address); networkName = route.network; + isRouted = true; } else { - routeAddress = dstAddr; + routeAddress = destAddress; + isRouted = false; } - networkName = networkName || - this.getNetworkNameByAddressPattern(`${routeAddress.zone}:${routeAddress.net}/*`) || - Config.scannerTossers.ftn_bso.defaultNetwork - ; + networkName = networkName || this.getNetworkNameByAddress(routeAddress); const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { return routeAddress.isPatternMatch(nodeAddrWildcard); - }) || { - packetType : '2+', - encoding : Config.scannerTossers.ftn_bso.packetMsgEncoding, - }; + }) || { packetType : '2+', encoding : Config.scannerTossers.ftn_bso.packetMsgEncoding }; + // we should never be failing here; we may just be using defaults. return cb( - config ? null : Errors.DoesNotExist(`No configuration found for ${dstAddr.toString()}`), - config, routeAddress, networkName + networkName ? null : Errors.DoesNotExist(`No NetMail route for ${destAddress.toString()}`), + { destAddress, routeAddress, networkName, config, isRouted } ); }; @@ -876,21 +875,22 @@ function FTNMessageScanTossModule() { function discoverUplink(callback) { const dstAddr = new Address(message.meta.System[Message.SystemMetaNames.RemoteToUser]); - return self.getAcceptableNetMailNetworkInfoFromAddress(dstAddr, (err, config, routeAddress, networkName) => { + self.getNetMailRouteInfoFromAddress(dstAddr, (err, routeInfo) => { if(err) { return callback(err); } - exportOpts.nodeConfig = config; - exportOpts.destAddress = routeAddress; - exportOpts.fileCase = config.fileCase || 'lower'; - exportOpts.network = Config.messageNetworks.ftn.networks[networkName]; - exportOpts.networkName = networkName; + exportOpts.nodeConfig = routeInfo.config; + exportOpts.destAddress = dstAddr; + exportOpts.routeAddress = routeInfo.routeAddress; + exportOpts.fileCase = routeInfo.config.fileCase || 'lower'; + exportOpts.network = Config.messageNetworks.ftn.networks[routeInfo.networkName]; + exportOpts.networkName = routeInfo.networkName; exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); - exportOpts.exportType = self.getExportType(config); + exportOpts.exportType = self.getExportType(routeInfo.config); if(!exportOpts.network) { - return callback(Errors.DoesNotExist(`No configuration found for network ${networkName}`)); + return callback(Errors.DoesNotExist(`No configuration found for network ${routeInfo.networkName}`)); } return callback(null); @@ -937,12 +937,15 @@ function FTNMessageScanTossModule() { ], err => { if(err) { - Log.warn( { error :err.message }, 'Error exporting message' ); + Log.warn( { error : err.message }, 'Error exporting message' ); } return nextMessageOrUuid(null); } ); }, err => { + if(err) { + Log.warn( { error : err.message }, 'Error(s) during NetMail export'); + } return cb(err); }); }; @@ -962,6 +965,7 @@ function FTNMessageScanTossModule() { fileCase : self.moduleConfig.nodes[nodeConfigKey].fileCase || 'lower', }; + if(_.isString(exportOpts.network.localAddress)) { exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); } @@ -2031,8 +2035,7 @@ function FTNMessageScanTossModule() { this.isNetMailMessage = function(message) { return message.isPrivate() && null === _.get(message, 'meta.System.LocalToUserID', null) && - Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null) - ; + Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null); }; } From 70a2bc5160a6c25a0f14f50fdbb9ed16738f08ff Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 18:32:15 -0700 Subject: [PATCH 013/569] Rework BSO-style flow file generation * Add point address NNNNnnnn.pnt sub dir support * Use *route* address in case of non-direct destinations --- core/scanner_tossers/ftn_bso.js | 50 ++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 36ee815f..6a5c0c60 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -221,7 +221,13 @@ function FTNMessageScanTossModule() { }; this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) { - let basename; + // + // Refs + // * http://ftsc.org/docs/fts-5005.003 + // * http://wiki.synchro.net/ref:fidonet_files#flow_files + // + let controlFileBaseName; + let pointDir; const ext = self.getOutgoingFlowFileExtension( destAddress, @@ -230,32 +236,50 @@ function FTNMessageScanTossModule() { fileCase ); - if(destAddress.point) { + const netComponent = `0000${destAddress.net.toString(16)}`.substr(-4); + const nodeComponent = `0000${destAddress.node.toString(16)}`.substr(-4); + if(destAddress.point) { + // point's go in an extra subdir, e.g. outbound/NNNNnnnn.pnt/00000001.pnt (for a point of 1) + pointDir = `${netComponent}${nodeComponent}.pnt`; + controlFileBaseName = `00000000${destAddress.point.toString(16)}`.substr(-8); } else { + pointDir = ''; + // // Use |destAddress| nnnnNNNN.??? where nnnn is dest net and NNNN is dest // node. This seems to match what Mystic does // - basename = - `0000${destAddress.net.toString(16)}`.substr(-4) + - `0000${destAddress.node.toString(16)}`.substr(-4); + controlFileBaseName = `${netComponent}${nodeComponent}`; } + // + // From FTS-5005.003: "Lower case filenames are prefered if supported by the file system." + // ...but we let the user override. + // if('upper' === fileCase) { - basename = basename.toUpperCase(); + controlFileBaseName = controlFileBaseName.toUpperCase(); + pointDir = pointDir.toUpperCase(); } - return paths.join(basePath, `${basename}.${ext}`); + return paths.join(basePath, pointDir, `${controlFileBaseName}.${ext}`); }; this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) { - const appendLines = fileRefs.reduce( (content, ref) => { - return content + `${directive}${ref}\n`; - }, ''); + // + // We have to ensure the *directory* of |filePath| exists here esp. + // for cases such as point destinations where a subdir may be + // present in the path that doesn't yet exist. + // + const flowFileDir = paths.dirname(filePath); + fse.mkdirs(flowFileDir, () => { // note not checking err; let's try appendFile + const appendLines = fileRefs.reduce( (content, ref) => { + return content + `${directive}${ref}\n`; + }, ''); - fs.appendFile(filePath, appendLines, err => { - cb(err); + fs.appendFile(filePath, appendLines, err => { + return cb(err); + }); }); }; @@ -915,7 +939,7 @@ function FTNMessageScanTossModule() { function prepareFloFile(callback) { const flowFilePath = self.getOutgoingFlowFileName( exportOpts.outgoingDir, - exportOpts.destAddress, + exportOpts.routeAddress, 'ref', exportOpts.exportType, exportOpts.fileCase From 5caf7a9fce3a3b3fcc5585e7cac71a3a8c34f3e2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 18:47:19 -0700 Subject: [PATCH 014/569] Move NetMail routes to scannerTossers: { ftn_bso: { ... } } where it belongs in config.hjson --- core/scanner_tossers/ftn_bso.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 6a5c0c60..a24024ee 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -827,9 +827,9 @@ function FTNMessageScanTossModule() { this.getNetMailRoute = function(dstAddr) { // - // messageNetworks.ftn.netMail.routes{} full|wildcard -> full adddress lookup + // Route full|wildcard -> full adddress/network lookup // - const routes = _.get(Config, 'messageNetworks.ftn.netMail.routes'); + const routes = _.get(Config, 'scannerTossers.ftn_bso.netMail.routes'); if(!routes) { return; } @@ -843,7 +843,7 @@ function FTNMessageScanTossModule() { // // Attempt to find route information for |destAddress|: // - // 1) Routes: messageNetworks.ftn.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config + // 1) Routes: scannerTossers.ftn_bso.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config // - Where we send may not be where destAddress is (it's routed!) // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config // - Where we send is direct to destAddress From bc55317a4b979517c2640377b3588c3873ea2fa4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 19:30:10 -0700 Subject: [PATCH 015/569] Fix drawing when focus items set - we should not attempt to stylize! --- core/vertical_menu_view.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index a7e566eb..5b27c36d 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -66,10 +66,7 @@ function VerticalMenuView(options) { let sgr; if(item.focused && self.hasFocusItems()) { const focusItem = self.focusItems[index]; - text = strUtil.stylizeString( - focusItem ? focusItem.text : item.text, - self.focusTextStyle - ); + text = focusItem ? focusItem.text : item.text; sgr = ''; } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); From c1f971d2d997a2664229fdb02c5c72b460084acd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 20 Jan 2018 19:30:21 -0700 Subject: [PATCH 016/569] Code readability --- core/bbs_list.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/bbs_list.js b/core/bbs_list.js index 5c81f478..74c792c6 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -150,7 +150,10 @@ exports.getModule = class BBSListModule extends MenuModule { self.database.run( `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, - [ formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes ], + [ + formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, + formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes + ], err => { if(err) { self.client.log.error( { err : err }, 'Error adding to BBS list'); From 8bfad971a15ff2a348216e8a74439f4b37f9ca9d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 21 Jan 2018 11:58:19 -0700 Subject: [PATCH 017/569] Finish conversion from 'binary' -> 'binary-parser' * FTN packets * SAUCE --- core/ftn_mail_packet.js | 475 +++++++++++++++++++---------------- core/sauce.js | 180 ++++++------- core/servers/login/telnet.js | 271 +++++++++++--------- core/string_util.js | 2 +- package.json | 2 +- 5 files changed, 506 insertions(+), 424 deletions(-) diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 1c60e1f0..bb72c40a 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -8,10 +8,11 @@ const Address = require('./ftn_address.js'); const strUtil = require('./string_util.js'); const Log = require('./logger.js').log; const ansiPrep = require('./ansi_prep.js'); +const Errors = require('./enig_error.js').Errors; const _ = require('lodash'); const assert = require('assert'); -const binary = require('binary'); +const { Parser } = require('binary-parser'); const fs = require('graceful-fs'); const async = require('async'); const iconv = require('iconv-lite'); @@ -23,7 +24,6 @@ const FTN_PACKET_HEADER_SIZE = 58; // fixed header size const FTN_PACKET_HEADER_TYPE = 2; const FTN_PACKET_MESSAGE_TYPE = 2; const FTN_PACKET_BAUD_TYPE_2_2 = 2; -const NULL_TERM_BUFFER = new Buffer( [ 0x00 ] ); // SAUCE magic header + version ("00") const FTN_MESSAGE_SAUCE_HEADER = new Buffer('SAUCE00'); @@ -173,108 +173,103 @@ function Packet(options) { this.parsePacketHeader = function(packetBuffer, cb) { assert(Buffer.isBuffer(packetBuffer)); - if(packetBuffer.length < FTN_PACKET_HEADER_SIZE) { - cb(new Error('Buffer too small')); - return; + let packetHeader; + try { + packetHeader = new Parser() + .uint16le('origNode') + .uint16le('destNode') + .uint16le('year') + .uint16le('month') + .uint16le('day') + .uint16le('hour') + .uint16le('minute') + .uint16le('second') + .uint16le('baud') + .uint16le('packetType') + .uint16le('origNet') + .uint16le('destNet') + .int8('prodCodeLo') + .int8('prodRevLo') // aka serialNo + .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 + .uint16le('origZone') + .uint16le('destZone') + // + // The following is "filler" in FTS-0001, specifics in + // FSC-0045 and FSC-0048 + // + .uint16le('auxNet') + .uint16le('capWordValidate') + .int8('prodCodeHi') + .int8('prodRevHi') + .uint16le('capWord') + .uint16le('origZone2') + .uint16le('destZone2') + .uint16le('origPoint') + .uint16le('destPoint') + .uint32le('prodData') + .parse(packetBuffer); + } catch(e) { + return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`); + } + + // Convert password from NULL padded array to string + packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); + + if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { + return cb(Errors.Invalid(`Unsupported FTN packet header type: ${packetHeader.packetType}`)); } // - // Start out reading as if this is a FSC-0048 2+ packet + // What kind of packet do we really have here? // - binary.parse(packetBuffer) - .word16lu('origNode') - .word16lu('destNode') - .word16lu('year') - .word16lu('month') - .word16lu('day') - .word16lu('hour') - .word16lu('minute') - .word16lu('second') - .word16lu('baud') - .word16lu('packetType') - .word16lu('origNet') - .word16lu('destNet') - .word8('prodCodeLo') - .word8('prodRevLo') // aka serialNo - .buffer('password', 8) // null padded C style string - .word16lu('origZone') - .word16lu('destZone') + // :TODO: adjust values based on version discovered + if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { + packetHeader.version = '2.2'; + + // See FSC-0045 + packetHeader.origPoint = packetHeader.year; + packetHeader.destPoint = packetHeader.month; + + packetHeader.destDomain = packetHeader.origZone2; + packetHeader.origDomain = packetHeader.auxNet; + } else { // - // The following is "filler" in FTS-0001, specifics in - // FSC-0045 and FSC-0048 + // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" // - .word16lu('auxNet') - .word16lu('capWordValidate') - .word8('prodCodeHi') - .word8('prodRevHi') - .word16lu('capWord') - .word16lu('origZone2') - .word16lu('destZone2') - .word16lu('origPoint') - .word16lu('destPoint') - .word32lu('prodData') - .tap(packetHeader => { - // Convert password from NULL padded array to string - //packetHeader.password = ftn.stringFromFTN(packetHeader.password); - packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); + const capWordValidateSwapped = + ((packetHeader.capWordValidate & 0xff) << 8) | + ((packetHeader.capWordValidate >> 8) & 0xff); - if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { - cb(new Error('Unsupported header type: ' + packetHeader.packetType)); - return; + if(capWordValidateSwapped === packetHeader.capWord && + 0 != packetHeader.capWord && + packetHeader.capWord & 0x0001) + { + packetHeader.version = '2+'; + + // See FSC-0048 + if(-1 === packetHeader.origNet) { + packetHeader.origNet = packetHeader.auxNet; } + } else { + packetHeader.version = '2'; - // - // What kind of packet do we really have here? - // - // :TODO: adjust values based on version discovered - if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { - packetHeader.version = '2.2'; + // :TODO: should fill bytes be 0? + } + } - // See FSC-0045 - packetHeader.origPoint = packetHeader.year; - packetHeader.destPoint = packetHeader.month; + packetHeader.created = moment({ + year : packetHeader.year, + month : packetHeader.month - 1, // moment uses 0 indexed months + date : packetHeader.day, + hour : packetHeader.hour, + minute : packetHeader.minute, + second : packetHeader.second + }); - packetHeader.destDomain = packetHeader.origZone2; - packetHeader.origDomain = packetHeader.auxNet; - } else { - // - // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" - // - const capWordValidateSwapped = - ((packetHeader.capWordValidate & 0xff) << 8) | - ((packetHeader.capWordValidate >> 8) & 0xff); + const ph = new PacketHeader(); + _.assign(ph, packetHeader); - if(capWordValidateSwapped === packetHeader.capWord && - 0 != packetHeader.capWord && - packetHeader.capWord & 0x0001) - { - packetHeader.version = '2+'; - - // See FSC-0048 - if(-1 === packetHeader.origNet) { - packetHeader.origNet = packetHeader.auxNet; - } - } else { - packetHeader.version = '2'; - - // :TODO: should fill bytes be 0? - } - } - - packetHeader.created = moment({ - year : packetHeader.year, - month : packetHeader.month - 1, // moment uses 0 indexed months - date : packetHeader.day, - hour : packetHeader.hour, - minute : packetHeader.minute, - second : packetHeader.second - }); - - let ph = new PacketHeader(); - _.assign(ph, packetHeader); - - cb(null, ph); - }); + return cb(null, ph); }; this.getPacketHeaderBuffer = function(packetHeader) { @@ -454,21 +449,30 @@ function Packet(options) { // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam const FTN_CHRS_PREFIX = new Buffer( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" const FTN_CHRS_SUFFIX = new Buffer( [ 0x0d ] ); - binary.parse(messageBodyBuffer) - .scan('prefix', FTN_CHRS_PREFIX) - .scan('content', FTN_CHRS_SUFFIX) - .tap(chrsData => { - if(chrsData.prefix && chrsData.content && chrsData.content.length > 0) { - const chrs = iconv.decode(chrsData.content, 'CP437'); - const chrsEncoding = ftn.getEncodingFromCharacterSetIdentifier(chrs); - if(chrsEncoding) { - encoding = chrsEncoding; - } - callback(null); - } else { - callback(null); - } - }); + + let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); + if(chrsPrefixIndex < 0) { + return callback(null); + } + + chrsPrefixIndex += FTN_CHRS_PREFIX.length; + + const chrsEndIndex = messageBodyBuffer.indexOf(FTN_CHRS_SUFFIX, chrsPrefixIndex); + if(chrsEndIndex < 0) { + return callback(null); + } + + let chrsContent = messageBodyBuffer.slice(chrsPrefixIndex, chrsEndIndex); + if(0 === chrsContent.length) { + return callback(null); + } + + chrsContent = iconv.decode(chrsContent, 'CP437'); + const chrsEncoding = ftn.getEncodingFromCharacterSetIdentifier(chrsContent); + if(chrsEncoding) { + encoding = chrsEncoding; + } + return callback(null); }, function extractMessageData(callback) { // @@ -525,125 +529,160 @@ function Packet(options) { }; this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { - binary.parse(packetBuffer) - .word16lu('messageType') - .word16lu('ftn_msg_orig_node') - .word16lu('ftn_msg_dest_node') - .word16lu('ftn_msg_orig_net') - .word16lu('ftn_msg_dest_net') - .word16lu('ftn_attr_flags') - .word16lu('ftn_cost') - .scan('modDateTime', NULL_TERM_BUFFER) // :TODO: 20 bytes max - .scan('toUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max - .scan('fromUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max - .scan('subject', NULL_TERM_BUFFER) // :TODO: 72 bytes max6 - .scan('message', NULL_TERM_BUFFER) - .tap(function tapped(msgData) { // no arrow function; want classic this - if(!msgData.messageType) { - // end marker -- no more messages - return cb(null); + // + // Check for end-of-messages marker up front before parse so we can easily + // tell the difference between end and bad header + // + if(packetBuffer.length < 3) { + const peek = packetBuffer.slice(0, 2); + if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) { + // end marker - no more messages + return cb(null); + } + // else fall through & hit exception below to log error + } + + let msgData; + try { + msgData = new Parser() + .uint16le('messageType') + .uint16le('ftn_msg_orig_node') + .uint16le('ftn_msg_dest_node') + .uint16le('ftn_msg_orig_net') + .uint16le('ftn_msg_dest_net') + .uint16le('ftn_attr_flags') + .uint16le('ftn_cost') + // :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved + .array('modDateTime', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('toUserName', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('fromUserName', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('subject', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('message', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .parse(packetBuffer); + } catch(e) { + return cb(Errors.Invalid(`Failed to parse FTN message header: ${e.message}`)); + } + + if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { + return cb(Errors.Invalid(`Unsupported FTN message type: ${msgData.messageType}`)); + } + + // + // Convert null terminated arrays to strings + // + [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { + msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437'); + }); + + // Technically the following fields have length limits as per fts-0001.016: + // * modDateTime : 20 bytes + // * toUserName : 36 bytes + // * fromUserName : 36 bytes + // * subject : 72 bytes + + // + // The message body itself is a special beast as it may + // contain an origin line, kludges, SAUCE in the case + // of ANSI files, etc. + // + const msg = new Message( { + toUserName : msgData.toUserName, + fromUserName : msgData.fromUserName, + subject : msgData.subject, + modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), + }); + + // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) + msg.meta.FtnProperty = { + ftn_orig_node : header.origNode, + ftn_dest_node : header.destNode, + ftn_orig_network : header.origNet, + ftn_dest_network : header.destNet, + + ftn_attr_flags : msgData.ftn_attr_flags, + ftn_cost : msgData.ftn_cost, + + ftn_msg_orig_node : msgData.ftn_msg_orig_node, + ftn_msg_dest_node : msgData.ftn_msg_dest_node, + ftn_msg_orig_net : msgData.ftn_msg_orig_net, + ftn_msg_dest_net : msgData.ftn_msg_dest_net, + }; + + self.processMessageBody(msgData.message, messageBodyData => { + msg.message = messageBodyData.message; + msg.meta.FtnKludge = messageBodyData.kludgeLines; + + if(messageBodyData.tearLine) { + msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; + + if(self.options.keepTearAndOrigin) { + msg.message += `\r\n${messageBodyData.tearLine}\r\n`; } + } - if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { - return cb(new Error('Unsupported message type: ' + msgData.messageType)); + if(messageBodyData.seenBy.length > 0) { + msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; + } + + if(messageBodyData.area) { + msg.meta.FtnProperty.ftn_area = messageBodyData.area; + } + + if(messageBodyData.originLine) { + msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; + + if(self.options.keepTearAndOrigin) { + msg.message += `${messageBodyData.originLine}\r\n`; } + } - const read = - 14 + // fixed header size - msgData.modDateTime.length + 1 + - msgData.toUserName.length + 1 + - msgData.fromUserName.length + 1 + - msgData.subject.length + 1 + - msgData.message.length + 1; + // + // If we have a UTC offset kludge (e.g. TZUTC) then update + // modDateTime with it + // + if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { + msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); + } - // - // Convert null terminated arrays to strings - // - let convMsgData = {}; - [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { - convMsgData[k] = iconv.decode(msgData[k], 'CP437'); - }); + // :TODO: Parser should give is this info: + const bytesRead = + 14 + // fixed header size + msgData.modDateTime.length + 1 + // +1 = NULL + msgData.toUserName.length + 1 + // +1 = NULL + msgData.fromUserName.length + 1 + // +1 = NULL + msgData.subject.length + 1 + // +1 = NULL + msgData.message.length; // includes NULL - // - // The message body itself is a special beast as it may - // contain an origin line, kludges, SAUCE in the case - // of ANSI files, etc. - // - const msg = new Message( { - toUserName : convMsgData.toUserName, - fromUserName : convMsgData.fromUserName, - subject : convMsgData.subject, - modTimestamp : ftn.getDateFromFtnDateTime(convMsgData.modDateTime), - }); - - // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) - msg.meta.FtnProperty = { - ftn_orig_node : header.origNode, - ftn_dest_node : header.destNode, - ftn_orig_network : header.origNet, - ftn_dest_network : header.destNet, - - ftn_attr_flags : msgData.ftn_attr_flags, - ftn_cost : msgData.ftn_cost, - - ftn_msg_orig_node : msgData.ftn_msg_orig_node, - ftn_msg_dest_node : msgData.ftn_msg_dest_node, - ftn_msg_orig_net : msgData.ftn_msg_orig_net, - ftn_msg_dest_net : msgData.ftn_msg_dest_net, + const nextBuf = packetBuffer.slice(bytesRead); + if(nextBuf.length > 0) { + const next = function(e) { + if(e) { + cb(e); + } else { + self.parsePacketMessages(header, nextBuf, iterator, cb); + } }; - self.processMessageBody(msgData.message, messageBodyData => { - msg.message = messageBodyData.message; - msg.meta.FtnKludge = messageBodyData.kludgeLines; - - if(messageBodyData.tearLine) { - msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; - - if(self.options.keepTearAndOrigin) { - msg.message += `\r\n${messageBodyData.tearLine}\r\n`; - } - } - - if(messageBodyData.seenBy.length > 0) { - msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; - } - - if(messageBodyData.area) { - msg.meta.FtnProperty.ftn_area = messageBodyData.area; - } - - if(messageBodyData.originLine) { - msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; - - if(self.options.keepTearAndOrigin) { - msg.message += `${messageBodyData.originLine}\r\n`; - } - } - - // - // If we have a UTC offset kludge (e.g. TZUTC) then update - // modDateTime with it - // - if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { - msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); - } - - const nextBuf = packetBuffer.slice(read); - if(nextBuf.length > 0) { - const next = function(e) { - if(e) { - cb(e); - } else { - self.parsePacketMessages(header, nextBuf, iterator, cb); - } - }; - - iterator('message', msg, next); - } else { - cb(null); - } - }); - }); + iterator('message', msg, next); + } else { + cb(null); + } + }); }; this.sanatizeFtnProperties = function(message) { diff --git a/core/sauce.js b/core/sauce.js index b976450b..9ee75b47 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -1,8 +1,11 @@ /* jslint node: true */ 'use strict'; -var binary = require('binary'); -var iconv = require('iconv-lite'); +const Errors = require('./enig_error.js').Errors; + +// deps +const iconv = require('iconv-lite'); +const { Parser } = require('binary-parser'); exports.readSAUCE = readSAUCE; @@ -25,103 +28,107 @@ const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; function readSAUCE(data, cb) { if(data.length < SAUCE_SIZE) { - cb(new Error('No SAUCE record present')); - return; + return cb(Errors.DoesNotExist('No SAUCE record present')); } - var offset = data.length - SAUCE_SIZE; - var sauceRec = data.slice(offset); + let sauceRec; + try { + sauceRec = new Parser() + .buffer('id', { length : 5 } ) + .buffer('version', { length : 2 } ) + .buffer('title', { length: 35 } ) + .buffer('author', { length : 20 } ) + .buffer('group', { length: 20 } ) + .buffer('date', { length: 8 } ) + .uint32le('fileSize') + .int8('dataType') + .int8('fileType') + .uint16le('tinfo1') + .uint16le('tinfo2') + .uint16le('tinfo3') + .uint16le('tinfo4') + .int8('numComments') + .int8('flags') + // :TODO: does this need to be optional? + .buffer('tinfos', { length: 22 } ) // SAUCE 00.5 + .parse(data.slice(data.length - SAUCE_SIZE)); + } catch(e) { + return cb(Errors.Invalid('Invalid SAUCE record')); + } - binary.parse(sauceRec) - .buffer('id', 5) - .buffer('version', 2) - .buffer('title', 35) - .buffer('author', 20) - .buffer('group', 20) - .buffer('date', 8) - .word32lu('fileSize') - .word8('dataType') - .word8('fileType') - .word16lu('tinfo1') - .word16lu('tinfo2') - .word16lu('tinfo3') - .word16lu('tinfo4') - .word8('numComments') - .word8('flags') - .buffer('tinfos', 22) // SAUCE 00.5 - .tap(function onVars(vars) { - if(!SAUCE_ID.equals(vars.id)) { - return cb(new Error('No SAUCE record present')); - } + if(!SAUCE_ID.equals(sauceRec.id)) { + return cb(Errors.DoesNotExist('No SAUCE record present')); + } - var ver = iconv.decode(vars.version, 'cp437'); + const ver = iconv.decode(sauceRec.version, 'cp437'); - if('00' !== ver) { - return cb(new Error('Unsupported SAUCE version: ' + ver)); - } + if('00' !== ver) { + return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`)); + } - if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(vars.dataType)) { - return cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType)); - } + if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) { + return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`)); + } - var sauce = { - id : iconv.decode(vars.id, 'cp437'), - version : iconv.decode(vars.version, 'cp437').trim(), - title : iconv.decode(vars.title, 'cp437').trim(), - author : iconv.decode(vars.author, 'cp437').trim(), - group : iconv.decode(vars.group, 'cp437').trim(), - date : iconv.decode(vars.date, 'cp437').trim(), - fileSize : vars.fileSize, - dataType : vars.dataType, - fileType : vars.fileType, - tinfo1 : vars.tinfo1, - tinfo2 : vars.tinfo2, - tinfo3 : vars.tinfo3, - tinfo4 : vars.tinfo4, - numComments : vars.numComments, - flags : vars.flags, - tinfos : vars.tinfos, - }; + const sauce = { + id : iconv.decode(sauceRec.id, 'cp437'), + version : iconv.decode(sauceRec.version, 'cp437').trim(), + title : iconv.decode(sauceRec.title, 'cp437').trim(), + author : iconv.decode(sauceRec.author, 'cp437').trim(), + group : iconv.decode(sauceRec.group, 'cp437').trim(), + date : iconv.decode(sauceRec.date, 'cp437').trim(), + fileSize : sauceRec.fileSize, + dataType : sauceRec.dataType, + fileType : sauceRec.fileType, + tinfo1 : sauceRec.tinfo1, + tinfo2 : sauceRec.tinfo2, + tinfo3 : sauceRec.tinfo3, + tinfo4 : sauceRec.tinfo4, + numComments : sauceRec.numComments, + flags : sauceRec.flags, + tinfos : sauceRec.tinfos, + }; - var dt = SAUCE_DATA_TYPES[sauce.dataType]; - if(dt && dt.parser) { - sauce[dt.name] = dt.parser(sauce); - } + const dt = SAUCE_DATA_TYPES[sauce.dataType]; + if(dt && dt.parser) { + sauce[dt.name] = dt.parser(sauce); + } - cb(null, sauce); - }); + return cb(null, sauce); } // :TODO: These need completed: -var SAUCE_DATA_TYPES = {}; -SAUCE_DATA_TYPES[0] = { name : 'None' }; -SAUCE_DATA_TYPES[1] = { name : 'Character', parser : parseCharacterSAUCE }; -SAUCE_DATA_TYPES[2] = 'Bitmap'; -SAUCE_DATA_TYPES[3] = 'Vector'; -SAUCE_DATA_TYPES[4] = 'Audio'; -SAUCE_DATA_TYPES[5] = 'BinaryText'; -SAUCE_DATA_TYPES[6] = 'XBin'; -SAUCE_DATA_TYPES[7] = 'Archive'; -SAUCE_DATA_TYPES[8] = 'Executable'; +const SAUCE_DATA_TYPES = { + 0 : { name : 'None' }, + 1 : { name : 'Character', parser : parseCharacterSAUCE }, + 2 : 'Bitmap', + 3 : 'Vector', + 4 : 'Audio', + 5 : 'BinaryText', + 6 : 'XBin', + 7 : 'Archive', + 8 : 'Executable', +}; -var SAUCE_CHARACTER_FILE_TYPES = {}; -SAUCE_CHARACTER_FILE_TYPES[0] = 'ASCII'; -SAUCE_CHARACTER_FILE_TYPES[1] = 'ANSi'; -SAUCE_CHARACTER_FILE_TYPES[2] = 'ANSiMation'; -SAUCE_CHARACTER_FILE_TYPES[3] = 'RIP script'; -SAUCE_CHARACTER_FILE_TYPES[4] = 'PCBoard'; -SAUCE_CHARACTER_FILE_TYPES[5] = 'Avatar'; -SAUCE_CHARACTER_FILE_TYPES[6] = 'HTML'; -SAUCE_CHARACTER_FILE_TYPES[7] = 'Source'; -SAUCE_CHARACTER_FILE_TYPES[8] = 'TundraDraw'; +const SAUCE_CHARACTER_FILE_TYPES = { + 0 : 'ASCII', + 1 : 'ANSi', + 2 : 'ANSiMation', + 3 : 'RIP script', + 4 : 'PCBoard', + 5 : 'Avatar', + 6 : 'HTML', + 7 : 'Source', + 8 : 'TundraDraw', +}; // // Map of SAUCE font -> encoding hint // // Note that this is the same mapping that x84 uses. Be compatible! // -var SAUCE_FONT_TO_ENCODING_HINT = { +const SAUCE_FONT_TO_ENCODING_HINT = { 'Amiga MicroKnight' : 'amiga', 'Amiga MicroKnight+' : 'amiga', 'Amiga mOsOul' : 'amiga', @@ -138,9 +145,11 @@ var SAUCE_FONT_TO_ENCODING_HINT = { 'IBM VGA' : 'cp437', }; -['437', '720', '737', '775', '819', '850', '852', '855', '857', '858', - '860', '861', '862', '863', '864', '865', '866', '869', '872'].forEach(function onPage(page) { - var codec = 'cp' + page; +[ + '437', '720', '737', '775', '819', '850', '852', '855', '857', '858', + '860', '861', '862', '863', '864', '865', '866', '869', '872' +].forEach( page => { + const codec = 'cp' + page; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; @@ -149,7 +158,7 @@ var SAUCE_FONT_TO_ENCODING_HINT = { }); function parseCharacterSAUCE(sauce) { - var result = {}; + const result = {}; result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; @@ -157,11 +166,12 @@ function parseCharacterSAUCE(sauce) { // convience: create ansiFlags sauce.ansiFlags = sauce.flags; - var i = 0; + let i = 0; while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { ++i; } - var fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); + + const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); if(fontName.length > 0) { result.fontName = fontName; } diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index a6fa0deb..6ffb49a9 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -2,16 +2,17 @@ 'use strict'; // ENiGMA½ -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const LoginServerModule = require('../../login_server_module.js'); -const Config = require('../../config.js').config; -const EnigAssert = require('../../enigma_assert.js'); +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); +const Config = require('../../config.js').config; +const EnigAssert = require('../../enigma_assert.js'); +const { stringFromNullTermBuffer } = require('../../string_util.js'); // deps const net = require('net'); const buffers = require('buffers'); -const binary = require('binary'); +const { Parser } = require('binary-parser'); const util = require('util'); //var debug = require('debug')('telnet'); @@ -218,46 +219,42 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { return MORE_DATA_REQUIRED; } - let end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes + const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes if(-1 === end) { return MORE_DATA_REQUIRED; } - // eat up and process the header - let buf = bufs.splice(0, 4).toBuffer(); - binary.parse(buf) - .word8('iac1') - .word8('sb') - .word8('ttype') - .word8('is') - .tap(function(vars) { - EnigAssert(vars.iac1 === COMMANDS.IAC); - EnigAssert(vars.sb === COMMANDS.SB); - EnigAssert(vars.ttype === OPTIONS.TERMINAL_TYPE); - EnigAssert(vars.is === SB_COMMANDS.IS); - }); - - // eat up the rest - end -= 4; - buf = bufs.splice(0, end).toBuffer(); - - // - // From this point -> |end| is our ttype - // - // Look for trailing NULL(s). Clients such as NetRunner do this. - // If none is found, we take the entire buffer - // - let trimAt = 0; - for(; trimAt < buf.length; ++trimAt) { - if(0x00 === buf[trimAt]) { - break; - } + let ttypeCmd; + try { + ttypeCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint8('is') + .array('ttype', { + type : 'uint8', + readUntil : b => 255 === b, // 255=COMMANDS.IAC + }) + // note we read iac2 above + .uint8('se') + .parse(bufs.toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing TTYP telnet command'); + return event; } - event.ttype = buf.toString('ascii', 0, trimAt); + EnigAssert(COMMANDS.IAC === ttypeCmd.iac1); + EnigAssert(COMMANDS.SB === ttypeCmd.sb); + EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); + EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); + EnigAssert(ttypeCmd.ttype.length > 0); + // note we found IAC_SE above - // pop off the terminating IAC SE - bufs.splice(0, 2); + // some terminals such as NetRunner provide a NULL-terminated buffer + // slice to remove IAC + event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); + + bufs.splice(0, end); } return event; @@ -272,25 +269,30 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { return MORE_DATA_REQUIRED; } - event.buf = bufs.splice(0, 9).toBuffer(); - binary.parse(event.buf) - .word8('iac1') - .word8('sb') - .word8('naws') - .word16bu('width') - .word16bu('height') - .word8('iac2') - .word8('se') - .tap(function(vars) { - EnigAssert(vars.iac1 == COMMANDS.IAC); - EnigAssert(vars.sb == COMMANDS.SB); - EnigAssert(vars.naws == OPTIONS.WINDOW_SIZE); - EnigAssert(vars.iac2 == COMMANDS.IAC); - EnigAssert(vars.se == COMMANDS.SE); + let nawsCmd; + try { + nawsCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint16be('width') + .uint16be('height') + .uint8('iac2') + .uint8('se') + .parse(bufs.splice(0, 9).toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing NAWS telnet command'); + return event; + } - event.cols = event.columns = event.width = vars.width; - event.rows = event.height = vars.height; - }); + EnigAssert(COMMANDS.IAC === nawsCmd.iac1); + EnigAssert(COMMANDS.SB === nawsCmd.sb); + EnigAssert(OPTIONS.WINDOW_SIZE === nawsCmd.opt); + EnigAssert(COMMANDS.IAC === nawsCmd.iac2); + EnigAssert(COMMANDS.SE === nawsCmd.se); + + event.cols = event.columns = event.width = nawsCmd.width; + event.rows = event.height = nawsCmd.height; } return event; }; @@ -321,78 +323,109 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { return MORE_DATA_REQUIRED; } - // eat up and process the header - let buf = bufs.splice(0, 4).toBuffer(); - binary.parse(buf) - .word8('iac1') - .word8('sb') - .word8('newEnv') - .word8('isOrInfo') // initial=IS, updates=INFO - .tap(function(vars) { - EnigAssert(vars.iac1 === COMMANDS.IAC); - EnigAssert(vars.sb === COMMANDS.SB); - EnigAssert(vars.newEnv === OPTIONS.NEW_ENVIRONMENT || vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP); - EnigAssert(vars.isOrInfo === SB_COMMANDS.IS || vars.isOrInfo === SB_COMMANDS.INFO); + // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. - event.type = vars.isOrInfo; - - if(vars.newEnv === OPTIONS.NEW_ENVIRONMENT_DEP) { - // :TODO: bring all this into Telnet class - Log.log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); - } - }); - - // eat up the rest - end -= 4; - buf = bufs.splice(0, end).toBuffer(); - - // - // This part can become messy. The basic spec is: - // IAC SB NEW-ENVIRON IS type ... [ VALUE ... ] [ type ... [ VALUE ... ] [ ... ] ] IAC SE - // - // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html - // - // Start by splitting up the remaining buffer. Keep the delimiters - // as prefixes we can use for processing. - // - // :TODO: Currently not supporting ESCaped values (ESC + ). Probably not really in the wild, but we should be compliant - // :TODO: Could probably just convert this to use a regex & handle delims + escaped values... in any case, this is sloppy... - const params = []; - let p = 0; - let j; - let l; - for(j = 0, l = buf.length; j < l; ++j) { - if(NEW_ENVIRONMENT_DELIMITERS.indexOf(buf[j]) === -1) { - continue; - } - - params.push(buf.slice(p, j)); - p = j; + let envCmd; + try { + envCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint8('isOrInfo') // IS=initial, INFO=updates + .array('envBlock', { + type : 'uint8', + readUntil : b => 255 === b, // 255=COMMANDS.IAC + }) + // note we consume IAC above + .uint8('se') + .parse(bufs.splice(0, bufs.length).toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing NEW-ENVIRON telnet command'); + return event; } - // remainder - if(p < l) { - params.push(buf.slice(p, l)); + EnigAssert(COMMANDS.IAC === envCmd.iac1); + EnigAssert(COMMANDS.SB === envCmd.sb); + EnigAssert(OPTIONS.NEW_ENVIRONMENT === envCmd.opt || OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt); + EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); + + if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { + // :TODO: we should probably support this for legacy clients? + Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); } + const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC + + if(envBuf.length < 4) { // TYPE + single char name + sep + single char value + // empty env block + return event; + } + + const States = { + Name : 1, + Value : 2, + }; + + let state = States.Name; + const setVars = {}; + const delVars = []; let varName; - event.envVars = {}; - // :TODO: handle cases where a variable was present in a previous exchange, but missing here...e.g removed - for(j = 0; j < params.length; ++j) { - if(params[j].length < 2) { - continue; - } + // :TODO: handle ESC type!!! + while(envBuf.length) { + switch(state) { + case States.Name : + { + const type = parseInt(envBuf.splice(0, 1)); + if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { + return event; // fail :( + } - let cmd = params[j].readUInt8(); - if(cmd === NEW_ENVIRONMENT_COMMANDS.VAR || cmd === NEW_ENVIRONMENT_COMMANDS.USERVAR) { - varName = params[j].slice(1).toString('utf8'); // :TODO: what encoding should this really be? - } else { - event.envVars[varName] = params[j].slice(1).toString('utf8'); // :TODO: again, what encoding? + let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); + if(-1 === nameEnd) { + nameEnd = envBuf.length; + } + + varName = envBuf.splice(0, nameEnd); + if(!varName) { + return event; // something is wrong. + } + + varName = Buffer.from(varName).toString('ascii'); + + const next = parseInt(envBuf.splice(0, 1)); + if(NEW_ENVIRONMENT_COMMANDS.VALUE === next) { + state = States.Value; + } else { + state = States.Name; + delVars.push(varName); // no value; del this var + } + } + break; + + case States.Value : + { + let valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VAR); + if(-1 === valueEnd) { + valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.USERVAR); + } + if(-1 === valueEnd) { + valueEnd = envBuf.length; + } + + let value = envBuf.splice(0, valueEnd); + if(value) { + value = Buffer.from(value).toString('ascii'); + setVars[varName] = value; + } + state = States.Name; + } + break; } } - // pop off remaining IAC SE - bufs.splice(0, 2); + // :TODO: Handle deleting previously set vars via delVars + event.type = envCmd.isOrInfo; + event.envVars = setVars; } return event; diff --git a/core/string_util.js b/core/string_util.js index c846fc38..f47bd6b5 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -204,7 +204,7 @@ function debugEscapedString(s) { } function stringFromNullTermBuffer(buf, encoding) { - let nullPos = buf.indexOf(new Buffer( [ 0x00 ] )); + let nullPos = buf.indexOf( 0x00 ); if(-1 === nullPos) { nullPos = buf.length; } diff --git a/package.json b/package.json index b44cc894..8c7a7a5d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ ], "dependencies": { "async": "^2.5.0", - "binary": "0.3.x", + "binary-parser": "^1.3.2", "buffers": "NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", From 94f3721bf8f753c9af358ae36b0fd191414fa708 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 21 Jan 2018 20:49:38 -0700 Subject: [PATCH 018/569] Prompt when already logged in --- core/servers/login/ssh.js | 76 ++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index cd9ca1a9..d14bdbbd 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -41,14 +41,19 @@ function SSHClient(clientConn) { clientConn.on('authentication', function authAttempt(ctx) { const username = ctx.username || ''; const password = ctx.password || ''; - + self.isNewUser = (Config.users.newUserNames || []).indexOf(username) > -1; self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); function terminateConnection() { ctx.reject(); - clientConn.end(); + return clientConn.end(); + } + + function alreadyLoggedIn(username) { + ctx.prompt(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); + return terminateConnection(); } // @@ -65,15 +70,13 @@ function SSHClient(clientConn) { userLogin(self, ctx.username, ctx.password, function authResult(err) { if(err) { if(err.existingConn) { - // :TODO: Can we display somthing here? - terminateConnection(); - return; - } else { - return ctx.reject(SSHClient.ValidAuthMethods); + return alreadyLoggedIn(username); } - } else { - ctx.accept(); + + return ctx.reject(SSHClient.ValidAuthMethods); } + + ctx.accept(); }); } else { if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { @@ -85,7 +88,7 @@ function SSHClient(clientConn) { return ctx.reject(); } - let interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; + const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; ctx.prompt(interactivePrompt, function retryPrompt(answers) { loginAttempts += 1; @@ -93,37 +96,36 @@ function SSHClient(clientConn) { userLogin(self, username, (answers[0] || ''), err => { if(err) { if(err.existingConn) { - // :TODO: can we display something here? - terminateConnection(); - } else { - if(loginAttempts >= Config.general.loginAttempts) { - terminateConnection(); - } else { - const artOpts = { - client : self, - name : 'SSHPMPT.ASC', - readSauce : false, - }; - - theme.getThemeArt(artOpts, (err, artInfo) => { - if(err) { - interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; - } else { - const newUserNameList = _.has(Config, 'users.newUserNames') && Config.users.newUserNames.length > 0 ? - Config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : - '(No new user names enabled!)'; - - interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; - } - return ctx.prompt(interactivePrompt, retryPrompt); - }); - } + return alreadyLoggedIn(username); } + + if(loginAttempts >= Config.general.loginAttempts) { + return terminateConnection(); + } + + const artOpts = { + client : self, + name : 'SSHPMPT.ASC', + readSauce : false, + }; + + theme.getThemeArt(artOpts, (err, artInfo) => { + if(err) { + interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; + } else { + const newUserNameList = _.has(Config, 'users.newUserNames') && Config.users.newUserNames.length > 0 ? + Config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : + '(No new user names enabled!)'; + + interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; + } + return ctx.prompt(interactivePrompt, retryPrompt); + }); } else { ctx.accept(); } - }); - }); + }); + }); } }); From 50074d77656a1e03b93374e985bcf0e641462fd9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 21 Jan 2018 20:49:49 -0700 Subject: [PATCH 019/569] Remove unused require --- core/string_util.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/string_util.js b/core/string_util.js index f47bd6b5..4539ea38 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -2,7 +2,6 @@ 'use strict'; // ENiGMA½ -const miscUtil = require('./misc_util.js'); const ANSI = require('./ansi_term.js'); // deps From cc74616a93f390273695762f6b8a35b9b3960424 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:34:10 -0700 Subject: [PATCH 020/569] Next at end of list goes to previous menu by default --- core/file_area_list.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/file_area_list.js b/core/file_area_list.js index 87e794cb..f42bf93e 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -75,6 +75,7 @@ exports.getModule = class FileAreaList extends MenuModule { this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); this.fileList = _.get(options, 'extraArgs.fileList'); + this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); if(this.fileList) { // we'll need to adjust position as well! @@ -103,6 +104,10 @@ exports.getModule = class FileAreaList extends MenuModule { return this.displayBrowsePage(true, cb); // true=clerarScreen } + if(this.lastFileNextExit) { + return this.prevMenu(cb); + } + return cb(null); }, prevFile : (formData, extraArgs, cb) => { From ec1876084cc4e079daab67cabd162b21eb4bdde9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:34:32 -0700 Subject: [PATCH 021/569] Add sanatizeString() method --- core/database.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/database.js b/core/database.js index 10331fc0..2717a545 100644 --- a/core/database.js +++ b/core/database.js @@ -19,6 +19,7 @@ const dbs = {}; exports.getTransactionDatabase = getTransactionDatabase; exports.getModDatabasePath = getModDatabasePath; exports.getISOTimestampString = getISOTimestampString; +exports.sanatizeString = sanatizeString; exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; @@ -59,6 +60,25 @@ function getISOTimestampString(ts) { return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } +function sanatizeString(s) { + return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex + switch (c) { + case '\0' : return '\\0'; + case '\x08' : return '\\b'; + case '\x09' : return '\\t'; + case '\x1a' : return '\\z'; + case '\n' : return '\\n'; + case '\r' : return '\\r'; + + case '"' : + case '\'' : + case '\\' : + case '%' : + return `\\${c}`; + } + }); +} + function initializeDatabases(cb) { async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { From 70b5d7a124b50447a99aa3709880d27bb77931c2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:36:16 -0700 Subject: [PATCH 022/569] MAJOR refactor of Message class * ES6 class vs old style * Add findMessages(filter, ...) similar to FileEntry.findFiles() allowing many filter types used throughout the system --- core/message.js | 1258 ++++++++++++++++++++++++++--------------------- 1 file changed, 711 insertions(+), 547 deletions(-) diff --git a/core/message.js b/core/message.js index f99efa8a..6c47a934 100644 --- a/core/message.js +++ b/core/message.js @@ -5,9 +5,11 @@ const msgDb = require('./database.js').dbs.message; const wordWrapText = require('./word_wrap.js').wordWrapText; const ftnUtil = require('./ftn_util.js'); const createNamedUUID = require('./uuid_util.js').createNamedUUID; -const getISOTimestampString = require('./database.js').getISOTimestampString; const Errors = require('./enig_error.js').Errors; const ANSI = require('./ansi_term.js'); +const { + sanatizeString, + getISOTimestampString } = require('./database.js'); const { isAnsi, isFormattedLine, @@ -25,74 +27,15 @@ const assert = require('assert'); const moment = require('moment'); const iconvEncode = require('iconv-lite').encode; -module.exports = Message; - const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); -function Message(options) { - options = options || {}; - - this.messageId = options.messageId || 0; // always generated @ persist - this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid; - - if(options.uuid) { - // note: new messages have UUID generated @ time of persist. See also Message.createMessageUUID() - this.uuid = options.uuid; - } - - this.replyToMsgId = options.replyToMsgId || 0; - this.toUserName = options.toUserName || ''; - this.fromUserName = options.fromUserName || ''; - this.subject = options.subject || ''; - this.message = options.message || ''; - - if(_.isDate(options.modTimestamp) || moment.isMoment(options.modTimestamp)) { - this.modTimestamp = moment(options.modTimestamp); - } else if(_.isString(options.modTimestamp)) { - this.modTimestamp = moment(options.modTimestamp); - } - - this.viewCount = options.viewCount || 0; - - this.meta = { - System : {}, // we'll always have this one - }; - - if(_.isObject(options.meta)) { - _.defaultsDeep(this.meta, options.meta); - } - - if(options.meta) { - this.meta = options.meta; - } - - this.hashTags = options.hashTags || []; - - this.isValid = function() { - // :TODO: validate as much as possible - return true; - }; - - this.isPrivate = function() { - return Message.isPrivateAreaTag(this.areaTag); - }; - - this.isFromRemoteUser = function() { - return null !== _.get(this, 'meta.System.remote_from_user', null); - }; -} - -Message.WellKnownAreaTags = { +const WELL_KNOWN_AREA_TAGS = { Invalid : '', Private : 'private_mail', Bulletin : 'local_bulletin', }; -Message.isPrivateAreaTag = function(areaTag) { - return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; -}; - -Message.SystemMetaNames = { +const SYSTEM_META_NAMES = { LocalToUserID : 'local_to_user_id', LocalFromUserID : 'local_from_user_id', StateFlags0 : 'state_flags0', // See Message.StateFlags0 @@ -103,19 +46,20 @@ Message.SystemMetaNames = { }; // Types for Message.SystemMetaNames.ExternalFlavor meta -Message.AddressFlavor = { +const ADDRESS_FLAVOR = { Local : 'local', // local / non-remote addressing FTN : 'ftn', // FTN style Email : 'email', }; -Message.StateFlags0 = { +const STATE_FLAGS0 = { None : 0x00000000, Imported : 0x00000001, // imported from foreign system Exported : 0x00000002, // exported to foreign system }; -Message.FtnPropertyNames = { +// :TODO: these should really live elsewhere... +const FTN_PROPERTY_NAMES = { // packet header oriented FtnOrigNode : 'ftn_orig_node', FtnDestNode : 'ftn_dest_node', @@ -143,518 +87,738 @@ Message.FtnPropertyNames = { FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 }; -// Note: kludges are stored with their names as-is +module.exports = class Message { + constructor( + { + messageId = 0, areaTag = Message.WellKnownAreaTags.Invalid, uuid, replyToMsgId = 0, + toUserName = '', fromUserName = '', subject = '', message = '', modTimestamp = moment(), + meta, hashTags = [], + } = { } + ) + { + this.messageId = messageId; + this.areaTag = areaTag; + this.uuid = uuid; + this.replyToMsgId = replyToMsgId; + this.toUserName = toUserName; + this.fromUserName = fromUserName; + this.subject = subject; + this.message = message; -Message.prototype.setLocalToUserId = function(userId) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; -}; + if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { + modTimestamp = moment(modTimestamp); + } -Message.prototype.setLocalFromUserId = function(userId) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; -}; + this.modTimestamp = modTimestamp; -Message.prototype.setRemoteToUser = function(remoteTo) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo; -}; + this.meta = {}; + _.defaultsDeep(this.meta, { System : {} }, meta); -Message.prototype.setExternalFlavor = function(flavor) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor; -}; - -Message.createMessageUUID = function(areaTag, modTimestamp, subject, body) { - assert(_.isString(areaTag)); - assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp)); - assert(_.isString(subject)); - assert(_.isString(body)); - - if(!moment.isMoment(modTimestamp)) { - modTimestamp = moment(modTimestamp); + this.hashTags = hashTags; } - areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); - modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); - subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); - body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); + isValid() { return true; } // :TODO: obviously useless; look into this or remove it - return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); -}; + static isPrivateAreaTag(areaTag) { + return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; + } -Message.getMessageIdByUuid = function(uuid, cb) { - msgDb.get( - `SELECT message_id - FROM message - WHERE message_uuid = ? - LIMIT 1;`, - [ uuid ], - (err, row) => { - if(err) { - cb(err); + isPrivate() { + return Message.isPrivateAreaTag(this.areaTag); + } + + isFromRemoteUser() { + return null !== _.get(this, 'meta.System.remote_from_user', null); + } + + static get WellKnownAreaTags() { + return WELL_KNOWN_AREA_TAGS; + } + + static get SystemMetaNames() { + return SYSTEM_META_NAMES; + } + + static get AddressFlavor() { + return ADDRESS_FLAVOR; + } + + static get StateFlags0() { + return STATE_FLAGS0; + } + + static get FtnPropertyNames() { + return FTN_PROPERTY_NAMES; + } + + setLocalToUserId(userId) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; + } + + setLocalFromUserId(userId) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; + } + + setRemoteToUser(remoteTo) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo; + } + + setExternalFlavor(flavor) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor; + } + + static createMessageUUID(areaTag, modTimestamp, subject, body) { + assert(_.isString(areaTag)); + assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp)); + assert(_.isString(subject)); + assert(_.isString(body)); + + if(!moment.isMoment(modTimestamp)) { + modTimestamp = moment(modTimestamp); + } + + areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); + modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); + subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); + body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); + + return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); + } + + /* + Find message IDs or UUIDs by filter. Available filters/options: + + filter.uuids - use with resultType='id' + filter.ids - use with resultType='uuid' + filter.toUserName + filter.fromUserName + filter.replyToMesageId + filter.newerThanTimestamp + filter.newerThanMessageId + *filter.confTag - all area tags in confTag + filter.areaTag + *filter.metaTuples - {category, name, value} + + *filter.terms - FTS search + + filter.sort = modTimestamp | messageId + filter.order = ascending | (descending) + + filter.limit + filter.resultType = (id) | uuid | count + filter.extraFields = [] + + filter.privateTagUserId = - if set, only private messages belonging to are processed + (any other areaTag or confTag filters will be ignored) + + *=NYI + */ + static findMessages(filter, cb) { + filter = filter || {}; + + filter.resultType = filter.resultType || 'id'; + filter.extraFields = filter.extraFields || []; + + const field = 'id' === filter.resultType ? 'message_id' : 'message_uuid'; + + if(moment.isMoment(filter.newerThanTimestamp)) { + filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); + } + + let sql; + if('count' === filter.resultType) { + sql = + `SELECT COUNT() AS count + FROM message m`; + + } else { + sql = + `SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} + FROM message m`; + } + + const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; + let sqlOrderBy; + let sqlWhere = ''; + + function appendWhereClause(clause) { + if(sqlWhere) { + sqlWhere += ' AND '; } else { + sqlWhere += ' WHERE '; + } + sqlWhere += clause; + } + + // currently only avail sort + if('modTimestamp' === filter.sort) { + sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`; + } else { + sqlOrderBy = `ORDER BY m.message_id ${sqlOrderDir}`; + } + + if(Array.isArray(filter.ids)) { + appendWhereClause(`m.message_id IN (${filter.ids.join(', ')})`); + } + + if(Array.isArray(filter.uuids)) { + const uuidList = filter.uuids.map(u => `"${u}"`).join(', '); + appendWhereClause(`m.message_id IN (${uuidList})`); + } + + + if(_.isNumber(filter.privateTagUserId)) { + appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); + appendWhereClause( + `m.message_id IN ( + SELECT message_id + FROM message_meta + WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} + )`); + } else { + let areaTags = []; + if(filter.confTag && filter.confTag.length > 0) { + // :TODO: grab areas from conf -> add to areaTags[] + } + + if(areaTags.length > 0 || filter.areaTag && filter.areaTag.length > 0) { + if(Array.isArray(filter.areaTag)) { + areaTags = areaTags.concat(filter.areaTag); + } else if(_.isString(filter.areaTag)) { + areaTags.push(filter.areaTag); + } + + areaTags = _.uniq(areaTags); // remove any dupes + + if(areaTags.length > 1) { + const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); + appendWhereClause(`m.area_tag IN(${areaList})`); + } else { + appendWhereClause(`m.area_tag = "${areaTags[0]}"`); + } + } + } + + [ 'toUserName', 'fromUserName', 'replyToMessageId' ].forEach(field => { + if(_.isString(filter[field]) && filter[field].length > 0) { + appendWhereClause(`m.${_.snakeCase(field)} = "${sanatizeString(filter[field])}"`); + } + }); + + if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { + appendWhereClause(`DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); + } + + if(_.isNumber(filter.newerThanMessageId)) { + appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); + } + + sql += `${sqlWhere} ${sqlOrderBy}`; + + if(_.isNumber(filter.limit)) { + sql += ` LIMIT ${filter.limit}`; + } + + sql += ';'; + + if('count' === filter.resultType) { + msgDb.get(sql, (err, row) => { + return cb(err, row ? row.count : 0); + }); + } else { + const matches = []; + const extra = filter.extraFields.length > 0; + msgDb.each(sql, (err, row) => { + if(_.isObject(row)) { + matches.push(extra ? row : row[field]); + } + }, err => { + return cb(err, matches); + }); + } + } + + // :TODO: use findMessages, by uuid, limit=1 + static getMessageIdByUuid(uuid, cb) { + msgDb.get( + `SELECT message_id + FROM message + WHERE message_uuid = ? + LIMIT 1;`, + [ uuid ], + (err, row) => { + if(err) { + return cb(err); + } + const success = (row && row.message_id); - cb(success ? null : new Error('No match'), success ? row.message_id : null); - } - } - ); -}; - -Message.getMessageIdsByMetaValue = function(category, name, value, cb) { - msgDb.all( - `SELECT message_id - FROM message_meta - WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, - [ category, name, value ], - (err, rows) => { - if(err) { - cb(err); - } else { - cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) - } - } - ); -}; - -Message.getMetaValuesByMessageId = function(messageId, category, name, cb) { - const sql = - `SELECT meta_value - FROM message_meta - WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; - - msgDb.all(sql, [ messageId, category, name ], (err, rows) => { - if(err) { - return cb(err); - } - - if(0 === rows.length) { - return cb(new Error('No value for category/name')); - } - - // single values are returned without an array - if(1 === rows.length) { - return cb(null, rows[0].meta_value); - } - - cb(null, rows.map(r => r.meta_value)); // map to array of values only - }); -}; - -Message.getMetaValuesByMessageUuid = function(uuid, category, name, cb) { - async.waterfall( - [ - function getMessageId(callback) { - Message.getMessageIdByUuid(uuid, (err, messageId) => { - callback(err, messageId); - }); - }, - function getMetaValues(messageId, callback) { - Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { - callback(err, values); - }); - } - ], - (err, values) => { - cb(err, values); - } - ); -}; - -Message.prototype.loadMeta = function(cb) { - /* - Example of loaded this.meta: - - meta: { - System: { - local_to_user_id: 1234, - }, - FtnProperty: { - ftn_seen_by: [ "1/102 103", "2/42 52 65" ] - } - } - */ - - const sql = - `SELECT meta_category, meta_name, meta_value - FROM message_meta - WHERE message_id = ?;`; - - let self = this; - msgDb.each(sql, [ this.messageId ], (err, row) => { - if(!(row.meta_category in self.meta)) { - self.meta[row.meta_category] = { }; - self.meta[row.meta_category][row.meta_name] = row.meta_value; - } else { - if(!(row.meta_name in self.meta[row.meta_category])) { - self.meta[row.meta_category][row.meta_name] = row.meta_value; - } else { - if(_.isString(self.meta[row.meta_category][row.meta_name])) { - self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; - } - - self.meta[row.meta_category][row.meta_name].push(row.meta_value); - } - } - }, err => { - cb(err); - }); -}; - -Message.prototype.load = function(options, cb) { - assert(_.isString(options.uuid)); - - var self = this; - - async.series( - [ - function loadMessage(callback) { - msgDb.get( - 'SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, ' + - 'message, modified_timestamp, view_count ' + - 'FROM message ' + - 'WHERE message_uuid=? ' + - 'LIMIT 1;', - [ options.uuid ], - (err, msgRow) => { - if(err) { - return callback(err); - } - if(!msgRow) { - return callback(new Error('Message (no longer) available')); - } - - self.messageId = msgRow.message_id; - self.areaTag = msgRow.area_tag; - self.messageUuid = msgRow.message_uuid; - self.replyToMsgId = msgRow.reply_to_message_id; - self.toUserName = msgRow.to_user_name; - self.fromUserName = msgRow.from_user_name; - self.subject = msgRow.subject; - self.message = msgRow.message; - self.modTimestamp = moment(msgRow.modified_timestamp); - self.viewCount = msgRow.view_count; - - callback(err); - } + return cb( + success ? null : Errors.DoesNotExist(`No message for UUID ${uuid}`), + success ? row.message_id : null ); - }, - function loadMessageMeta(callback) { - self.loadMeta(err => { - callback(err); - }); - }, - function loadHashTags(callback) { - // :TODO: - callback(null); - } - ], - function complete(err) { - cb(err); - } - ); -}; - -Message.prototype.persistMetaValue = function(category, name, value, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = msgDb; - } - - const metaStmt = transOrDb.prepare( - `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) - VALUES (?, ?, ?, ?);`); - - if(!_.isArray(value)) { - value = [ value ]; - } - - let self = this; - - async.each(value, (v, next) => { - metaStmt.run(self.messageId, category, name, v, err => { - next(err); - }); - }, err => { - cb(err); - }); -}; - -Message.prototype.persist = function(cb) { - - if(!this.isValid()) { - return cb(new Error('Cannot persist invalid message!')); - } - - const self = this; - - async.waterfall( - [ - function beginTransaction(callback) { - return msgDb.beginTransaction(callback); - }, - function storeMessage(trans, callback) { - // generate a UUID for this message if required (general case) - const msgTimestamp = moment(); - if(!self.uuid) { - self.uuid = Message.createMessageUUID( - self.areaTag, - msgTimestamp, - self.subject, - self.message); - } - - trans.run( - `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) - VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, - [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], - function inserted(err) { // use non-arrow function for 'this' scope - if(!err) { - self.messageId = this.lastID; - } - - return callback(err, trans); - } - ); - }, - function storeMeta(trans, callback) { - if(!self.meta) { - return callback(null, trans); - } - /* - Example of self.meta: - - meta: { - System: { - local_to_user_id: 1234, - }, - FtnProperty: { - ftn_seen_by: [ "1/102 103", "2/42 52 65" ] - } - } - */ - async.each(Object.keys(self.meta), (category, nextCat) => { - async.each(Object.keys(self.meta[category]), (name, nextName) => { - self.persistMetaValue(category, name, self.meta[category][name], trans, err => { - nextName(err); - }); - }, err => { - nextCat(err); - }); - - }, err => { - callback(err, trans); - }); - }, - function storeHashTags(trans, callback) { - // :TODO: hash tag support - return callback(null, trans); - } - ], - (err, trans) => { - if(trans) { - trans[err ? 'rollback' : 'commit'](transErr => { - return cb(err ? err : transErr, self.messageId); - }); - } else { - return cb(err); - } - } - ); -}; - -Message.prototype.getFTNQuotePrefix = function(source) { - source = source || 'fromUserName'; - - return ftnUtil.getQuotePrefix(this[source]); -}; - -Message.prototype.getTearLinePosition = function(input) { - const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); - return m ? m.index : -1; -}; - -Message.prototype.getQuoteLines = function(options, cb) { - if(!options.termWidth || !options.termHeight || !options.cols) { - return cb(Errors.MissingParam()); - } - - options.startCol = options.startCol || 1; - options.includePrefix = _.get(options, 'includePrefix', true); - options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); - options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); - options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting - - /* - Some long text that needs to be wrapped and quoted should look right after - doing so, don't ya think? yeah I think so - - Nu> Some long text that needs to be wrapped and quoted should look right - Nu> after doing so, don't ya think? yeah I think so - - Ot> Nu> Some long text that needs to be wrapped and quoted should look - Ot> Nu> right after doing so, don't ya think? yeah I think so - - */ - const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; - - function getWrapped(text, extraPrefix) { - extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; - - const wrapOpts = { - width : options.cols - (quotePrefix.length + extraPrefix.length), - tabHandling : 'expand', - tabWidth : 4, - }; - - return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { - return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; - }); - } - - 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 + // :TODO: use findMessages + static getMessageIdsByMetaValue(category, name, value, cb) { + msgDb.all( + `SELECT message_id + FROM message_meta + WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, + [ category, name, value ], + (err, rows) => { + if(err) { + return cb(err); + } + return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) + } + ); + } - 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; + static getMetaValuesByMessageId(messageId, category, name, cb) { + const sql = + `SELECT meta_value + FROM message_meta + WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; - 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); + msgDb.all(sql, [ messageId, category, name ], (err, rows) => { + if(err) { + return cb(err); } - paragraph.split(/\r?\n/).forEach(line => { - if(0 === line.trim().length) { - // see blank line notes above - return quoted.push(quotePrefix); + if(0 === rows.length) { + return cb(Errors.DoesNotExist('No value for category/name')); + } + + // single values are returned without an array + if(1 === rows.length) { + return cb(null, rows[0].meta_value); + } + + return cb(null, rows.map(r => r.meta_value)); // map to array of values only + }); + } + + static getMetaValuesByMessageUuid(uuid, category, name, cb) { + async.waterfall( + [ + function getMessageId(callback) { + Message.getMessageIdByUuid(uuid, (err, messageId) => { + return callback(err, messageId); + }); + }, + function getMetaValues(messageId, callback) { + Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { + return callback(err, values); + }); + } + ], + (err, values) => { + return cb(err, values); + } + ); + } + + loadMeta(cb) { + /* + Example of loaded this.meta: + + meta: { + System: { + local_to_user_id: 1234, + }, + FtnProperty: { + ftn_seen_by: [ "1/102 103", "2/42 52 65" ] + } + } + */ + const sql = + `SELECT meta_category, meta_name, meta_value + FROM message_meta + WHERE message_id = ?;`; + + const self = this; // :TODO: not required - arrow functions below: + msgDb.each(sql, [ this.messageId ], (err, row) => { + if(!(row.meta_category in self.meta)) { + self.meta[row.meta_category] = { }; + self.meta[row.meta_category][row.meta_name] = row.meta_value; + } else { + if(!(row.meta_name in self.meta[row.meta_category])) { + self.meta[row.meta_category][row.meta_name] = row.meta_value; + } else { + if(_.isString(self.meta[row.meta_category][row.meta_name])) { + self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; + } + + self.meta[row.meta_category][row.meta_name].push(row.meta_value); + } + } + }, err => { + return cb(err); + }); + } + + load(options, cb) { + assert(_.isString(options.uuid)); + + const self = this; + + async.series( + [ + function loadMessage(callback) { + msgDb.get( + `SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, + message, modified_timestamp, view_count + FROM message + WHERE message_uuid=? + LIMIT 1;`, + [ options.uuid ], + (err, msgRow) => { + if(err) { + return callback(err); + } + + if(!msgRow) { + return callback(Errors.DoesNotExist('Message (no longer) available')); + } + + self.messageId = msgRow.message_id; + self.areaTag = msgRow.area_tag; + self.messageUuid = msgRow.message_uuid; + self.replyToMsgId = msgRow.reply_to_message_id; + self.toUserName = msgRow.to_user_name; + self.fromUserName = msgRow.from_user_name; + self.subject = msgRow.subject; + self.message = msgRow.message; + self.modTimestamp = moment(msgRow.modified_timestamp); + + return callback(err); + } + ); + }, + function loadMessageMeta(callback) { + self.loadMeta(err => { + return callback(err); + }); + }, + function loadHashTags(callback) { + // :TODO: + return callback(null); + } + ], + err => { + return cb(err); + } + ); + } + + persistMetaValue(category, name, value, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = msgDb; + } + + const metaStmt = transOrDb.prepare( + `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) + VALUES (?, ?, ?, ?);`); + + if(!_.isArray(value)) { + value = [ value ]; + } + + const self = this; + + async.each(value, (v, next) => { + metaStmt.run(self.messageId, category, name, v, err => { + return next(err); + }); + }, err => { + return cb(err); + }); + } + + persist(cb) { + if(!this.isValid()) { + return cb(Errors.Invalid('Cannot persist invalid message!')); + } + + const self = this; + + async.waterfall( + [ + function beginTransaction(callback) { + return msgDb.beginTransaction(callback); + }, + function storeMessage(trans, callback) { + // generate a UUID for this message if required (general case) + const msgTimestamp = moment(); + if(!self.uuid) { + self.uuid = Message.createMessageUUID( + self.areaTag, + msgTimestamp, + self.subject, + self.message + ); + } + + trans.run( + `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) + VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, + [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], + function inserted(err) { // use non-arrow function for 'this' scope + if(!err) { + self.messageId = this.lastID; + } + + return callback(err, trans); + } + ); + }, + function storeMeta(trans, callback) { + if(!self.meta) { + return callback(null, trans); + } + /* + Example of self.meta: + + meta: { + System: { + local_to_user_id: 1234, + }, + FtnProperty: { + ftn_seen_by: [ "1/102 103", "2/42 52 65" ] + } + } + */ + async.each(Object.keys(self.meta), (category, nextCat) => { + async.each(Object.keys(self.meta[category]), (name, nextName) => { + self.persistMetaValue(category, name, self.meta[category][name], trans, err => { + return nextName(err); + }); + }, err => { + return nextCat(err); + }); + + }, err => { + return callback(err, trans); + }); + }, + function storeHashTags(trans, callback) { + // :TODO: hash tag support + return callback(null, trans); + } + ], + (err, trans) => { + if(trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(err ? err : transErr, self.messageId); + }); + } else { + return cb(err); + } + } + ); + } + + // :TODO: FTN stuff doesn't have any business here + getFTNQuotePrefix(source) { + source = source || 'fromUserName'; + + return ftnUtil.getQuotePrefix(this[source]); + } + + getTearLinePosition(input) { + const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); + return m ? m.index : -1; + } + + getQuoteLines(options, cb) { + if(!options.termWidth || !options.termHeight || !options.cols) { + return cb(Errors.MissingParam()); + } + + options.startCol = options.startCol || 1; + options.includePrefix = _.get(options, 'includePrefix', true); + options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); + options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); + options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting + + /* + Some long text that needs to be wrapped and quoted should look right after + doing so, don't ya think? yeah I think so + + Nu> Some long text that needs to be wrapped and quoted should look right + Nu> after doing so, don't ya think? yeah I think so + + Ot> Nu> Some long text that needs to be wrapped and quoted should look + Ot> Nu> right after doing so, don't ya think? yeah I think so + + */ + const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; + + function getWrapped(text, extraPrefix) { + extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; + + const wrapOpts = { + width : options.cols - (quotePrefix.length + extraPrefix.length), + tabHandling : 'expand', + tabWidth : 4, + }; + + return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { + return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; + }); + } + + 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); } - quoteMatch = line.match(QUOTE_RE); + paragraph.split(/\r?\n/).forEach(line => { + if(0 === line.trim().length) { + // see blank line notes above + return quoted.push(quotePrefix); + } - switch(state) { - case 'line' : - if(quoteMatch) { - if(isFormattedLine(line)) { - quoted.push(getFormattedLine(line.replace(/\s/, ''))); + 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 { - quoted.push(...getWrapped(buf, quoteMatch[1])); - state = 'quote_line'; + 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'; } - } else { - buf += ` ${line}`; - } - break; + 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; + default : + if(isFormattedLine(line)) { + quoted.push(getFormattedLine(line)); } else { - buf += ` ${rem}`; + state = quoteMatch ? 'quote_line' : 'line'; + buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any } - } else { - quoted.push(...getWrapped(buf)); - buf = line; - state = 'line'; - } - break; + 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)); }); - quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); - }); + input.slice(tearLinePos).split(/\r?\n/).forEach(l => { + quoted.push(...getWrapped(l)); + }); - input.slice(tearLinePos).split(/\r?\n/).forEach(l => { - quoted.push(...getWrapped(l)); - }); - - return cb(null, quoted, null, false); + return cb(null, quoted, null, false); + } } }; From 3d575f764534234ecc999d86694cc5bf4b307434 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:37:26 -0700 Subject: [PATCH 023/569] Default renderLen array --- core/word_wrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/word_wrap.js b/core/word_wrap.js index f246ad23..ecb728a5 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -40,7 +40,7 @@ function wordWrapText(text, options) { let renderLen; let i = 0; let wordStart = 0; - let result = { wrapped : [ '' ], renderLen : [] }; + let result = { wrapped : [ '' ], renderLen : [ 0 ] }; function expandTab(column) { const remainWidth = options.tabWidth - (column % options.tabWidth); From b6bda7f45f459bd241249282b7331083f742eb7b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:38:50 -0700 Subject: [PATCH 024/569] much cleaner code --- core/theme.js | 56 ++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/core/theme.js b/core/theme.js index 5c056c49..2c984ae0 100644 --- a/core/theme.js +++ b/core/theme.js @@ -34,47 +34,31 @@ function refreshThemeHelpers(theme) { // theme.helpers = { getPasswordChar : function() { - var pwChar = Config.defaults.passwordChar; - if(_.has(theme, 'customization.defaults.general')) { - var themePasswordChar = theme.customization.defaults.general.passwordChar; - if(_.isString(themePasswordChar)) { - pwChar = themePasswordChar.substr(0, 1); - } else if(_.isNumber(themePasswordChar)) { - pwChar = String.fromCharCode(themePasswordChar); - } + let pwChar = _.get( + theme, + 'customization.defaults.general.passwordChar', + Config.defaults.passwordChar + ); + + if(_.isString(pwChar)) { + pwChar = pwChar.substr(0, 1); + } else if(_.isNumber(pwChar)) { + pwChar = String.fromCharCode(pwChar); } + return pwChar; }, - getDateFormat : function(style) { - style = style || 'short'; - - var format = Config.defaults.dateFormat[style] || 'MM/DD/YYYY'; - - if(_.has(theme, 'customization.defaults.dateFormat')) { - return theme.customization.defaults.dateFormat[style] || format; - } - return format; + getDateFormat : function(style = 'short') { + const format = Config.defaults.dateFormat[style] || 'MM/DD/YYYY'; + return _.get(theme, `customization.defaults.dateFormat.${style}`, format); }, - getTimeFormat : function(style) { - style = style || 'short'; - - var format = Config.defaults.timeFormat[style] || 'h:mm a'; - - if(_.has(theme, 'customization.defaults.timeFormat')) { - return theme.customization.defaults.timeFormat[style] || format; - } - return format; + getTimeFormat : function(style = 'short') { + const format = Config.defaults.timeFormat[style] || 'h:mm a'; + return _.get(theme, `customization.defaults.timeFormat.${style}`, format); }, - getDateTimeFormat : function(style) { - style = style || 'short'; - - var format = Config.defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; - - if(_.has(theme, 'customization.defaults.dateTimeFormat')) { - return theme.customization.defaults.dateTimeFormat[style] || format; - } - - return format; + getDateTimeFormat : function(style = 'short') { + const format = Config.defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); } }; } From cc119297e83f33107ede69b85261f5878c376620 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:39:53 -0700 Subject: [PATCH 025/569] wcValue -> wildcards (readability) --- core/file_entry.js | 2 +- core/scanner_tossers/ftn_bso.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 861b9d79..b95c7acb 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -496,7 +496,7 @@ module.exports = class FileEntry { if(filter.metaPairs && filter.metaPairs.length > 0) { filter.metaPairs.forEach(mp => { - if(mp.wcValue) { + if(mp.wildcards) { // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); appendWhereClause( diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index a24024ee..8c87b61f 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1718,9 +1718,9 @@ function FTNMessageScanTossModule() { const metaPairs = [ { - name : 'short_file_name', - value : replaces.toUpperCase(), // we store upper as well - wcValue : true, // value may contain wildcards + name : 'short_file_name', + value : replaces.toUpperCase(), // we store upper as well + wildcards : true, // value may contain wildcards }, { name : 'tic_origin', From f350e3d446c81e1bd797f42f4bfd317a55dfeb73 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:41:53 -0700 Subject: [PATCH 026/569] Use private message list header for 'inbox' --- art/themes/luciano_blocktronics/PRVMSGLIST.ANS | Bin 0 -> 2291 bytes config/menu.hjson | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 art/themes/luciano_blocktronics/PRVMSGLIST.ANS diff --git a/art/themes/luciano_blocktronics/PRVMSGLIST.ANS b/art/themes/luciano_blocktronics/PRVMSGLIST.ANS new file mode 100644 index 0000000000000000000000000000000000000000..911f1f2030dd91fe3cdf1e638635457a2565362e GIT binary patch literal 2291 zcmb_e%Wl&^6is)iTQ-zP;7!)pPT~l%Dp7@ysDvm~psWgtc!+5QrSK8_Ox175U)Yj= zg>%lmGj$pO5p94VOh|>h5Wqwizz-0DG~(4?(bX0DQFs zWf_0%ow>_2dT2*J&3;zb7H~F++jUJVa*xY!_H}iJk32l$x(cJ0&r0Mifn^orF+Kpx z!sUA}N5GQ630?B=zz{G@=|oN}+p_#QcNNaBkO4YW?o)=f)_S`&TwcBXw2fM?6A+*H zE&|`_iBYf0BaBXui{WxQpOlowR+@m} zvQpb5SHx5K8I>>0oLQJHbM~+17Sqx029en-PbtT<#H_{iY;PBsQVx~#9fcHQ(ubx9sJ8=*i~x%l>P43E z9zftST&_N^-hGpkQux_VV)ttibKOYPT03l+i6GXJ4Yb;16OP^6fQdT-L^-3PD4`RS zHCQZ3P*a~y1MKO%T7Q4rBqre8z>jl=qNtW%v}L$+aPPk4NMeMG?<~$!PIRJzX9#2k!>@@!WtIXQUu8i2~wg^4qFq z2ijONXNlJij9aD#_au?dEdtq%&fVpUpocN3{Rz Date: Fri, 26 Jan 2018 21:42:43 -0700 Subject: [PATCH 027/569] Use new Message.findMessages() functionality --- core/message_area.js | 225 +++++++++++++------------------------------ 1 file changed, 69 insertions(+), 156 deletions(-) diff --git a/core/message_area.js b/core/message_area.js index 3c42e23f..fb068e13 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -320,177 +320,90 @@ function getMessageFromRow(row) { fromUserName : row.from_user_name, subject : row.subject, modTimestamp : row.modified_timestamp, - viewCount : row.view_count, }; } -function getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, what) { - // - // Helper for building SQL to fetch either a full message list or simply - // a count of new messages based on |what|. - // - // * If |areaTag| is Message.WellKnownAreaTags.Private, - // only messages addressed to |userId| should be returned/counted. - // - // * Only messages > |lastMessageId| should be returned/counted - // - const selectWhat = ('count' === what) ? - 'COUNT() AS count' : - 'message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count'; - - let sql = - `SELECT ${selectWhat} - FROM message - WHERE area_tag = "${areaTag}" AND message_id > ${lastMessageId}`; - - if(Message.isPrivateAreaTag(areaTag)) { - sql += - ` AND message_id in ( - SELECT message_id - FROM message_meta - WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${userId} - )`; - } - - if('count' === what) { - sql += ';'; - } else { - sql += ' ORDER BY message_id;'; - } - - return sql; -} - function getNewMessageCountInAreaForUser(userId, areaTag, cb) { - async.waterfall( - [ - function getLastMessageId(callback) { - getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) { - callback(null, lastMessageId || 0); // note: willingly ignoring any errors here! - }); - }, - function getCount(lastMessageId, callback) { - const sql = getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, 'count'); - msgDb.get(sql, (err, row) => { - return callback(err, row ? row.count : 0); - }); - } - ], - cb - ); + getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { + lastMessageId = lastMessageId || 0; + + const filter = { + areaTag, + newerThanMessageId : lastMessageId, + resultType : 'count', + }; + + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = userId; + } + + Message.findMessages(filter, (err, count) => { + return cb(err, count); + }); + }); } function getNewMessagesInAreaForUser(userId, areaTag, cb) { - // - // If |areaTag| is Message.WellKnownAreaTags.Private, - // only messages addressed to |userId| should be returned. - // - // Only messages > lastMessageId should be returned - // - let msgList = []; + getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { + lastMessageId = lastMessageId || 0; - async.waterfall( - [ - function getLastMessageId(callback) { - getMessageAreaLastReadId(userId, areaTag, function fetched(err, lastMessageId) { - callback(null, lastMessageId || 0); // note: willingly ignoring any errors here! - }); - }, - function getMessages(lastMessageId, callback) { - const sql = getNewMessageDataInAreaForUserSql(userId, areaTag, lastMessageId, 'messages'); + const filter = { + areaTag, + newerThanMessageId : lastMessageId, + sort : 'messageId', + order : 'ascending', + extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + }; - msgDb.each(sql, function msgRow(err, row) { - if(!err) { - msgList.push(getMessageFromRow(row)); - } - }, callback); - } - ], - function complete(err) { - cb(err, msgList); + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = userId; } - ); -} -function getMessageListForArea(options, areaTag, cb) { - // - // options.client (required) - // - - options.client.log.debug( { areaTag : areaTag }, 'Fetching available messages'); - - assert(_.isObject(options.client)); - - /* - [ - { - messageId, messageUuid, replyToId, toUserName, fromUserName, subject, modTimestamp, - status(new|old), - viewCount - } - ] - */ - - let msgList = []; - - async.series( - [ - function fetchMessages(callback) { - let sql = - `SELECT message_id, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, modified_timestamp, view_count - FROM message - WHERE area_tag = ?`; - - if(Message.isPrivateAreaTag(areaTag)) { - sql += - ` AND message_id IN ( - SELECT message_id - FROM message_meta - WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${options.client.user.userId} - )`; - } - - sql += ' ORDER BY message_id;'; - - msgDb.each( - sql, - [ areaTag.toLowerCase() ], - (err, row) => { - if(!err) { - msgList.push(getMessageFromRow(row)); - } - }, - callback - ); - }, - function fetchStatus(callback) { - callback(null);// :TODO: fixmeh. - } - ], - function complete(err) { - cb(err, msgList); - } - ); -} - -function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { - if(moment.isMoment(newerThanTimestamp)) { - newerThanTimestamp = getISOTimestampString(newerThanTimestamp); - } - - msgDb.get( - `SELECT message_id - FROM message - WHERE area_tag = ? AND DATETIME(modified_timestamp) > DATETIME("${newerThanTimestamp}", "+1 seconds") - ORDER BY modified_timestamp ASC - LIMIT 1;`, - [ areaTag ], - (err, row) => { + Message.findMessages(filter, (err, messages) => { if(err) { return cb(err); } - return cb(null, row ? row.message_id : null); + return cb(null, messages.map(msg => getMessageFromRow(msg))); + }); + }); +} + +function getMessageListForArea(client, areaTag, cb) { + const filter = { + areaTag, + sort : 'messageId', + order : 'ascending', + extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + }; + + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = client.user.userId; + } + + Message.findMessages(filter, (err, messages) => { + if(err) { + return cb(err); + } + + return cb(null, messages.map(msg => getMessageFromRow(msg))); + }); +} + +function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { + Message.findMessages( + { + areaTag, + newerThanTimestamp, + sort : 'modTimestamp', + order : 'ascending', + limit : 1, + }, + (err, id) => { + if(err) { + return cb(err); + } + return cb(null, id ? id[0] : null); } ); } From 303259841fc5fb69d0638ab46ae01846097398d3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:43:08 -0700 Subject: [PATCH 028/569] options -> client, since client was only option ;) --- core/msg_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/msg_list.js b/core/msg_list.js index 19ef30cd..72ee20f5 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -159,7 +159,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( return callback(0 === self.messageList.length ? new Error('No messages in area') : null); } - messageArea.getMessageListForArea( { client : self.client }, self.messageAreaTag, function msgs(err, msgList) { + messageArea.getMessageListForArea(self.client, self.messageAreaTag, function msgs(err, msgList) { if(!msgList || 0 === msgList.length) { return callback(new Error('No messages in area')); } From a3e257aee3b086a2e150e525a9d06b9dd8df05f2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:44:07 -0700 Subject: [PATCH 029/569] Fix FSE word wrap bug when no barriers could be located in a > width string --- core/multi_line_edit_text_view.js | 63 ++++++++++++------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index bc73e903..1be0591f 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -56,9 +56,9 @@ const _ = require('lodash'); // To-Do // // * Index pos % for emit scroll events -// * Some of this shoudl be async'd where there is lots of processing (e.g. word wrap) +// * Some of this should be async'd where there is lots of processing (e.g. word wrap) // * Fix backspace when col=0 (e.g. bs to prev line) -// * Add back word delete +// * Add word delete (CTRL+????) // * @@ -336,13 +336,10 @@ function MultiLineEditTextView(options) { */ this.updateTextWordWrap = function(index) { - var nextEolIndex = self.getNextEndOfLineIndex(index); - var wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); - var newLines = wrapped.wrapped; + const nextEolIndex = self.getNextEndOfLineIndex(index); + const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); + const newLines = wrapped.wrapped.map(l => { return { text : l }; } ); - for(var i = 0; i < newLines.length; ++i) { - newLines[i] = { text : newLines[i] }; - } newLines[newLines.length - 1].eol = true; Array.prototype.splice.apply( @@ -420,44 +417,40 @@ function MultiLineEditTextView(options) { self.textLines[index].text.slice(col) ].join(''); - //self.cursorPos.col++; self.cursorPos.col += c.length; - var cursorOffset; - var absPos; + let cursorOffset; + let absPos; if(self.getTextLength(index) > self.dimens.width) { // // Update word wrapping and |cursorOffset| if the cursor // was within the bounds of the wrapped text // - var lastCol = self.cursorPos.col - c.length; - var firstWrapRange = self.updateTextWordWrap(index); + const lastCol = self.cursorPos.col - c.length; + const firstWrapRange = self.updateTextWordWrap(index); if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { cursorOffset = self.cursorPos.col - firstWrapRange.start; + } else { + cursorOffset = firstWrapRange.end; } // redraw from current row to end of visible area self.redrawRows(self.cursorPos.row, self.dimens.height); - if(!_.isUndefined(cursorOffset)) { - self.cursorBeginOfNextLine(); - self.cursorPos.col += cursorOffset; - self.client.term.rawWrite(ansi.right(cursorOffset)); - } else { - self.moveClientCursorToCursorPos(); - } + self.cursorBeginOfNextLine(); + self.cursorPos.col += cursorOffset; + self.client.term.rawWrite(ansi.right(cursorOffset)); } else { // // We must only redraw from col -> end of current visible line // absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length); + self.client.term.write( - ansi.hideCursor() + - self.getSGRFor('text') + - self.getRenderText(index).slice(self.cursorPos.col - c.length) + - ansi.goto(absPos.row, absPos.col) + - ansi.showCursor(), false + `${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto(absPos.row, absPos.col)}${ansi.showCursor()}`, + false // convertLineFeeds ); } }; @@ -495,16 +488,12 @@ function MultiLineEditTextView(options) { return new Array(self.getRemainingTabWidth(col)).join(expandChar); }; - this.wordWrapSingleLine = function(s, tabHandling, width) { - if(!_.isNumber(width)) { - width = self.dimens.width; - } - + this.wordWrapSingleLine = function(line, tabHandling = 'expand') { return wordWrapText( - s, + line, { - width : width, - tabHandling : tabHandling || 'expand', + width : self.dimens.width, + tabHandling : tabHandling, tabWidth : self.tabWidth, tabChar : '\t', } @@ -615,11 +604,7 @@ function MultiLineEditTextView(options) { let wrapped; text.forEach(line => { - wrapped = self.wordWrapSingleLine( - line, // line to wrap - 'expand', // tabHandling - self.dimens.width - ).wrapped; + wrapped = self.wordWrapSingleLine(line, 'expand').wrapped; self.setTextLines(wrapped, index, true); // true=termWithEol index += wrapped.length; @@ -784,7 +769,7 @@ function MultiLineEditTextView(options) { var index = self.getTextLinesIndex(); var nextEolIndex = self.getNextEndOfLineIndex(index); var text = self.getContiguousText(index, nextEolIndex); - var newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; + const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); for(var i = 1; i < newLines.length; ++i) { From 974ee1b389768d5a1337626e7a9b443efad86485 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 26 Jan 2018 21:45:08 -0700 Subject: [PATCH 030/569] MAJOR *POSSIBLY BREAKING* changes in FSE * WIP on cleanup to use 'standard' MCI formatting / theming used elsewhere in system * Some MCI ID changes (e.g. FSE in edit mode %TL13 -> %TL4); update your theme.hjson / artwork! --- art/themes/luciano_blocktronics/MSGEHDR.ANS | Bin 1578 -> 1578 bytes core/fse.js | 216 +++++++++----------- 2 files changed, 100 insertions(+), 116 deletions(-) diff --git a/art/themes/luciano_blocktronics/MSGEHDR.ANS b/art/themes/luciano_blocktronics/MSGEHDR.ANS index b2ed34e7eb397451cec927bfb56d220142458478..c455a9a3eca82f881c2e882359903293b244c516 100644 GIT binary patch delta 15 WcmZ3*vx;Yf8Y`2D!e$LtCPn}whXa8C delta 15 WcmZ3*vx;Yf8Y`2b@n#KHCPn}w 0) { self.message.setLocalToUserId(self.toUserId); return callback(null); @@ -695,12 +686,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }, function prepareViewStates(callback) { var header = self.viewControllers.header; - var from = header.getView(1); + var from = header.getView(MciViewIds.header.from); from.acceptsFocus = false; //from.setText(self.client.user.username); // :TODO: make this a method - var body = self.viewControllers.body.getView(1); + var body = self.viewControllers.body.getView(MciViewIds.body.message); self.updateTextEditMode(body.getTextEditMode()); self.updateEditModePosition(body.getEditPosition()); @@ -716,7 +707,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul self.initHeaderViewMode(); self.initFooterViewMode(); - var bodyMessageView = self.viewControllers.body.getView(1); + var bodyMessageView = self.viewControllers.body.getView(MciViewIds.body.message); if(bodyMessageView && _.has(self, 'message.message')) { //self.setBodyMessageViewText(); bodyMessageView.setText(cleanControlCodes(self.message.message)); @@ -726,7 +717,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul case 'edit' : { - const fromView = self.viewControllers.header.getView(1); + const fromView = self.viewControllers.header.getView(MciViewIds.header.from); const area = getMessageAreaByTag(self.messageAreaTag); if(area && area.realNames) { fromView.setText(self.client.user.properties.real_name || self.client.user.username); @@ -817,24 +808,20 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } initHeaderViewMode() { - assert(_.isObject(this.message)); + this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + this.setHeaderText(MciViewIds.header.to, this.message.toUserName); + this.setHeaderText(MciViewIds.header.subject, this.message.subject); + this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); + this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); + this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); - this.setHeaderText(MciCodeIds.ViewModeHeader.From, this.message.fromUserName); - this.setHeaderText(MciCodeIds.ViewModeHeader.To, this.message.toUserName); - this.setHeaderText(MciCodeIds.ViewModeHeader.Subject, this.message.subject); - this.setHeaderText(MciCodeIds.ViewModeHeader.DateTime, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); - this.setHeaderText(MciCodeIds.ViewModeHeader.MsgNum, (this.messageIndex + 1).toString()); - this.setHeaderText(MciCodeIds.ViewModeHeader.MsgTotal, this.messageTotal.toString()); - this.setHeaderText(MciCodeIds.ViewModeHeader.ViewCount, this.message.viewCount); - this.setHeaderText(MciCodeIds.ViewModeHeader.HashTags, 'TODO hash tags'); - this.setHeaderText(MciCodeIds.ViewModeHeader.MessageID, this.message.messageId); - this.setHeaderText(MciCodeIds.ViewModeHeader.ReplyToMsgID, this.message.replyToMessageId); + this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); } initHeaderReplyEditMode() { assert(_.isObject(this.replyToMessage)); - this.setHeaderText(MciCodeIds.ReplyEditModeHeader.To, this.replyToMessage.fromUserName); + this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName); // // We want to prefix the subject with "RE: " only if it's not already @@ -845,12 +832,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul newSubj = `RE: ${newSubj}`; } - this.setHeaderText(MciCodeIds.ReplyEditModeHeader.Subject, newSubj); + this.setHeaderText(MciViewIds.header.subject, newSubj); } initFooterViewMode() { - this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgNum, (this.messageIndex + 1).toString() ); - this.setViewText('footerView', MciCodeIds.ViewModeFooter.MsgTotal, this.messageTotal.toString() ); + this.setViewText('footerView', MciViewIds.ViewModeFooter.msgNum, (this.messageIndex + 1).toString() ); + this.setViewText('footerView', MciViewIds.ViewModeFooter.msgTotal, this.messageTotal.toString() ); } displayHelp(cb) { @@ -913,8 +900,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } }, function loadQuoteLines(callback) { - const quoteView = self.viewControllers.quoteBuilder.getView(3); - const bodyView = self.viewControllers.body.getView(1); + const quoteView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); + const bodyView = self.viewControllers.body.getView(MciViewIds.body.message); self.replyToMessage.getQuoteLines( { @@ -935,16 +922,13 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul quoteView.setItems(quoteLines); quoteView.setFocusItems(focusQuoteLines); + self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg).setFocus(false); + self.viewControllers.quoteBuilder.switchFocus(MciViewIds.quoteBuilder.quoteLines); + return callback(null); } ); }, - function setViewFocus(callback) { - self.viewControllers.quoteBuilder.getView(1).setFocus(false); - self.viewControllers.quoteBuilder.switchFocus(3); - - callback(null); - } ], function complete(err) { if(err) { @@ -955,7 +939,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } observeEditorEvents() { - const bodyView = this.viewControllers.body.getView(1); + const bodyView = this.viewControllers.body.getView(MciViewIds.body.message); bodyView.on('edit position', pos => { this.updateEditModePosition(pos); @@ -968,7 +952,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul /* this.observeViewPosition = function() { - self.viewControllers.body.getView(1).on('edit position', function positionUpdate(pos) { + self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) { console.log(pos.percent + ' / ' + pos.below) }); }; @@ -995,7 +979,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul switchFromQuoteBuilderToBody() { this.viewControllers.quoteBuilder.setFocus(false); - var body = this.viewControllers.body.getView(1); + var body = this.viewControllers.body.getView(MciViewIds.body.message); body.redraw(); this.viewControllers.body.switchFocus(1); @@ -1009,14 +993,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul quoteBuilderFinalize() { // :TODO: fix magic #'s - const quoteMsgView = this.viewControllers.quoteBuilder.getView(1); - const msgView = this.viewControllers.body.getView(1); + const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); + const msgView = this.viewControllers.body.getView(MciViewIds.body.message); let quoteLines = quoteMsgView.getData().trim(); if(quoteLines.length > 0) { if(this.replyIsAnsi) { - const bodyMessageView = this.viewControllers.body.getView(1); + const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); quoteLines += `${ansi.normal()}${bodyMessageView.getSGRFor('text')}`; } msgView.addText(`${quoteLines}\n\n`); From 7a2df5685516c6dc89de8d3858b33fd5d6c1bdb9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 27 Jan 2018 22:21:48 -0700 Subject: [PATCH 031/569] Menu items can now be arrays of objects * Allows custom members of each item * 'data' overrides selection (vs returning the index) * 'text' is the default member for text if no formatters are supplied * formatters: 'itemFormat' and 'focusItemFormat', e.g. "{member1} - {member2}" --- core/horizontal_menu_view.js | 36 ++++++++++++++++--------- core/menu_view.js | 51 +++++++++++++++++++++++++++++------- core/toggle_menu_view.js | 6 ++--- core/vertical_menu_view.js | 15 ++++++----- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 81d477ad..ee1d5318 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -1,12 +1,14 @@ /* jslint node: true */ 'use strict'; -var MenuView = require('./menu_view.js').MenuView; -var ansi = require('./ansi_term.js'); -var strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const { pipeToAnsi } = require('./color_codes.js'); +const { goto } = require('./ansi_term.js'); -var assert = require('assert'); -var _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); exports.HorizontalMenuView = HorizontalMenuView; @@ -57,21 +59,29 @@ function HorizontalMenuView(options) { this.drawItem = function(index) { assert(!this.positionCacheExpired); - var item = self.items[index]; + const item = self.items[index]; if(!item) { return; } - var text = strUtil.stylizeString( - item.text, - this.hasFocus && item.focused ? self.focusTextStyle : self.textStyle); + let text; + let sgr; + if(item.focused && self.hasFocusItems()) { + const focusItem = self.focusItems[index]; + text = focusItem ? focusItem.text : item.text; + sgr = ''; + } else if(this.complexItems) { + text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } - var drawWidth = text.length + self.getSpacer().length * 2; // * 2 = sides + const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2); self.client.term.write( - ansi.goto(self.position.row, item.col) + - (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()) + - strUtil.pad(text, drawWidth, self.fillChar, 'center') + `${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}` ); }; } diff --git a/core/menu_view.js b/core/menu_view.js index 5aed54ca..56899077 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -63,17 +63,37 @@ function MenuView(options) { util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { - const self = this; + if(Array.isArray(items)) { + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if(stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - if(items) { - this.items = []; - items.forEach( itemText => { - this.items.push( - { - text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others }); + + if(this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; + this.focusItemFormat = this.focusItemFormat || this.itemFormat; + } } }; @@ -96,12 +116,20 @@ MenuView.prototype.getCount = function() { }; MenuView.prototype.getItems = function() { + if(this.complexItems) { + return this.items; + } + return this.items.map( item => { return item.text; }); }; MenuView.prototype.getItem = function(index) { + if(this.complexItems) { + return this.items[index]; + } + return this.items[index].text; }; @@ -170,6 +198,11 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'hotKeySubmit' : this.hotKeySubmit = value; break; case 'justify' : this.justify = value; break; case 'focusItemIndex' : this.focusedItemIndex = value; break; + + case 'itemFormat' : + case 'focusItemFormat' : + this[propName] = value; + break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index 27ae2169..39d7ef95 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -113,9 +113,9 @@ ToggleMenuView.prototype.getData = function() { }; ToggleMenuView.prototype.setItems = function(items) { + items = items.slice(0, 2); // switch/toggle only works with two elements + ToggleMenuView.super_.prototype.setItems.call(this, items); - this.items = this.items.splice(0, 2); // switch/toggle only works with two elements - - this.dimens.width = this.items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) + this.dimens.width = items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) }; diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 5b27c36d..e1d6cd17 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -5,6 +5,8 @@ const MenuView = require('./menu_view.js').MenuView; const ansi = require('./ansi_term.js'); const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps const util = require('util'); @@ -68,17 +70,16 @@ function VerticalMenuView(options) { const focusItem = self.focusItems[index]; text = focusItem ? focusItem.text : item.text; sgr = ''; + } else if(this.complexItems) { + text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } - text += self.getSGR(); - self.client.term.write( - ansi.goto(item.row, self.position.col) + - sgr + - strUtil.pad(text, this.dimens.width, this.fillChar, this.justify) + `${ansi.goto(item.row, self.position.col)}${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}` ); }; } @@ -176,7 +177,9 @@ VerticalMenuView.prototype.onKeyPress = function(ch, key) { }; VerticalMenuView.prototype.getData = function() { - return this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return item.data ? item.data : this.focusedItemIndex; + //return this.focusedItemIndex; }; VerticalMenuView.prototype.setItems = function(items) { From 342c37b38828c0573417cf8a91adebe51e96a75b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 12:56:35 -0700 Subject: [PATCH 032/569] Allow extraArgs such that we can launch from menu items easier --- core/telnet_bridge.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 30db1207..6fb81d21 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -126,8 +126,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { constructor(options) { super(options); - this.config = options.menuConfig.config; - // defaults + this.config = Object.assign({}, _.get(options, 'menuConf.config'), options.extraArgs); this.config.port = this.config.port || 23; } @@ -152,10 +151,10 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { host : self.config.host, }; - let clientTerminated; - self.client.term.write(resetScreen()); - self.client.term.write(` Connecting to ${connectOpts.host}, please wait...\n`); + self.client.term.write( + ` Connecting to ${connectOpts.host}, please wait...\n` + ); const telnetConnection = new TelnetClientConnection(self.client); From 90427ac89d0fb7978882bddd17d3f39f92afcd8a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 12:56:58 -0700 Subject: [PATCH 033/569] Notes on changes --- UPGRADE.md | 1 + WHATSNEW.md | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 5975b96e..bec189ed 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -40,6 +40,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or # 0.0.8-alpha to 0.0.9-alpha * Development is now against Node.js 8.x LTS. Follow your standard upgrade path to update to Node 8.x before using 0.0.9-alpha. * The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well. +* Possible breaking changes in FSE: The MCI code `%TL13` for error indicator is now `%TL4`. This is part of a cleanup and standardization on "custom ranges". You may need to update your `theme.hjson` and related artwork. # 0.0.7-alpha to 0.0.8-alpha diff --git a/WHATSNEW.md b/WHATSNEW.md index d5d5fee2..99c47786 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -4,6 +4,12 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 0.0.9-alpha * Development is now against Node.js 8.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! * Fixed `justify` properties: `left` and `right` values were formerly swapped (oops!) +* Menu items can now be arrays of *objects* not just arrays of strings. + * The properties `itemFormat` and `focusItemFormat` allow you to supply the string format for items. For example if a menu object is `{ "userName" : "Bob", "age" : 35 }`, a `itemFormat` might be `|04{userName} |08- |14{age}`. + * If no `itemFormat` is supplied, the default formatter is `{text}`. + * Setting the `data` member of an object will cause form submissions to use this value instead of the selected items index. + * See the default `luciano_blocktronics` `matrix` menu for example usage. +* You can now set the `sort` property on a menu to sort items. If `true` items are sorted by `text`. If the value is a string, it represents the key in menu objects to sort by. ## 0.0.8-alpha From 70eefc008a997f367c5c6aa44cc83f6b08290f64 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 12:59:20 -0700 Subject: [PATCH 034/569] Update matrix example to show item formatting --- art/themes/luciano_blocktronics/theme.hjson | 3 +- config/menu.hjson | 33 +++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 433f8884..27bd44ca 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -22,7 +22,8 @@ matrix: { mci: { VM1: { - focusTextStyle: first lower + itemFormat: "|03{text}" + focusItemFormat: "|11{text!styleFirstLower}" } } } diff --git a/config/menu.hjson b/config/menu.hjson index b8d26521..3fb9c229 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -67,26 +67,47 @@ submit: true focus: true argName: navSelect - // :TODO: need a good way to localize these ... Standard Orig->Lookup seems good. - items: [ "login", "apply", "forgot pw", "log off" ] + // + // To enable forgot password, you will need to have the web server + // enabled and mail/SMTP configured. Once that is in place, swap out + // the commented lines below as well as in the submit block + // + items: [ + { + text: login + data: login + } + { + text: apply + data: apply + } + { + text: forgot pass + data: forgot + } + { + text: log off + data: logoff + } + ] } } submit: { *: [ { - value: { navSelect: 0 } + value: { navSelect: "login" } action: @menu:login } { - value: { navSelect: 1 }, + value: { navSelect: "apply" } action: @menu:newUserApplicationPre } { - value: { navSelect: 2 } + value: { navSelect: "forgot" } action: @menu:forgotPassword } { - value: { navSelect: 3 }, + value: { navSelect: "logoff" } action: @menu:logoff } ] From b6317e05415dcaaeb8763d9d08d114b438faf9ac Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 13:02:24 -0700 Subject: [PATCH 035/569] File Base area selection using new simplified formatting --- config/menu.hjson | 4 ++-- core/file_base_area_select.js | 35 +++++++++-------------------------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index 3fb9c229..cf2c71dd 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -2782,14 +2782,14 @@ mci: { VM1: { focus: true - argName: areaSelect + argName: areaTag } } submit: { *: [ { - value: { areaSelect: null } + value: { areaTag: null } action: @method:selectArea } ] diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index 87dfe9f4..f762ab1d 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -3,8 +3,7 @@ // enigma-bbs const MenuModule = require('./menu_module.js').MenuModule; -const stringFormat = require('./string_format.js'); -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const { getSortedAvailableFileAreas } = require('./file_base_area.js'); const StatLog = require('./stat_log.js'); // deps @@ -24,16 +23,10 @@ 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, + areaTag : formData.value.areaTag, }; const menuOpts = { @@ -48,10 +41,6 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { }; } - loadAvailAreas() { - this.availAreas = getSortedAvailableFileAreas(this.client); - } - mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { @@ -60,35 +49,29 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { const self = this; - async.series( + async.waterfall( [ function mergeAreaStats(callback) { const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; - self.availAreas.forEach(area => { + // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override + const availAreas = getSortedAvailableFileAreas(self.client); + availAreas.forEach(area => { const stats = areaStats.areas[area.areaTag]; area.totalFiles = stats ? stats.files : 0; area.totalBytes = stats ? stats.bytes : 0; }); - return callback(null); + return callback(null, availAreas); }, - function prepView(callback) { + function prepView(availAreas, callback) { self.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => { if(err) { return callback(err); } const areaListView = vc.getView(MciViewIds.areaList); - - const areaListFormat = self.config.areaListFormat || '{name}'; - - areaListView.setItems(self.availAreas.map(a => stringFormat(areaListFormat, a) ) ); - - if(self.config.areaListFocusFormat) { - areaListView.setFocusItems(self.availAreas.map(a => stringFormat(self.config.areaListFocusFormat, a) ) ); - } - + areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } ))); areaListView.redraw(); return callback(null); From 999033ec154848adf5d261e187b18825fd93d28c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 13:03:11 -0700 Subject: [PATCH 036/569] New menu sorting, fix up default SGR --- art/themes/luciano_blocktronics/MATRIX.ANS | Bin 4797 -> 5117 bytes core/horizontal_menu_view.js | 2 +- core/menu_view.js | 33 +++++++++++++++++++-- core/vertical_menu_view.js | 2 +- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/art/themes/luciano_blocktronics/MATRIX.ANS b/art/themes/luciano_blocktronics/MATRIX.ANS index 4e18372379762657be552e9bb01e2e8df65401ee..14219543d7a87cf9e911adada5c27c3a5f07c8bd 100644 GIT binary patch delta 884 zcmZ8fyG{Z@6b%sMO#~8~MKTEzO$>3D#RbGht+hdoLP-1or?J4qM>N(pdTmH(EUat< zYU5W}?IzpV8xlLkxihoL4$aQq$2oV-nfqS)U7puEdb6rG%^Hns9aE`%(&4VbWXKZ>NN6-6pIlSm!Ukw1dkbEdX;DJF zu~qm=786)9BttIMF7a5d2fIMELK#5k>&1R;d8a|@mXo45gR({7Fcbpi^hXLKbPlbc z<4L*@%yVI!*P`m?SQH8wu_)cl3deF<#KL`M{|4El^R^u)u5H_{LzL}foMJPjIBvQ~ hdN#|H<6tA1$+~i}C7eGIgNp}P&7KCIa?sB%{R4tV2Ot0d delta 567 zcmYjOJxjx25XQ7k+w@CQ5F~!QpjEdfO`En<#rkd)CyQu<;Gk2@;vk49qPQ011{EB2 zaaM#srjGA2! zLjU<-Y`LsEnqB2YHcO5dP+aCQ#ykuzrifV_R;9jF;qW6hVLdX67$zUIqO~!?da)A; z8EJW&zPj?BT=yHor~D#Ug7hrDk){-f^hpK`uLUdVwSm{#_s z8i!luDP6Jt{K)_-8no)CH%5n-jTLyB%*R0~g z7XE`|rubLUhgRlvhETtPG2)ZV5Wvwe%*-pkTZUg9&320ZJS#L1jsEAP(Cl)_!9Hle zzcmvAZn9SseofUuQ&BawhpF60JUqkTJ2xw3Xid}|8b7Bt;YeK!D7f~6+TILzD>*_a o%o9R*F7wY66b9EL{d7Y#h>lNSkbB{I$p;U*`D3ZA!dHIb7Z850UjP6A diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index ee1d5318..02f9c06e 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -71,7 +71,7 @@ function HorizontalMenuView(options) { text = focusItem ? focusItem.text : item.text; sgr = ''; } else if(this.complexItems) { - text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); diff --git a/core/menu_view.js b/core/menu_view.js index 56899077..f15491cf 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -64,6 +64,8 @@ util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { + this.sorted = false; + // // Items can be an array of strings or an array of objects. // @@ -91,13 +93,38 @@ MenuView.prototype.setItems = function(items) { }); if(this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; - this.focusItemFormat = this.focusItemFormat || this.itemFormat; + this.itemFormat = this.itemFormat || '{text}'; } } }; +MenuView.prototype.setSort = function(sort) { + if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; + } + + const key = true === sort ? 'text' : sort; + if('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } + + this.items.sort( (a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if(!a1) { + return -1; + } + if(!b1) { + return 1; + } + return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); + }); + + this.sorted = true; +}; + MenuView.prototype.removeItem = function(index) { + this.sorted = false; this.items.splice(index, 1); if(this.focusItems) { @@ -203,6 +230,8 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'focusItemFormat' : this[propName] = value; break; + + case 'sort' : this.setSort(value); break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index e1d6cd17..6f083193 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -71,7 +71,7 @@ function VerticalMenuView(options) { text = focusItem ? focusItem.text : item.text; sgr = ''; } else if(this.complexItems) { - text = pipeToAnsi(formatString(item.focused ? this.focusItemFormat : this.itemFormat, item)); + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } else { text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); From c81aa001f47fe868b2217c9682e8469ba6c61088 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 28 Jan 2018 13:22:47 -0700 Subject: [PATCH 037/569] Fix typo --- core/telnet_bridge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 6fb81d21..447efe4c 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -126,7 +126,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConf.config'), options.extraArgs); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config.port = this.config.port || 23; } From ec87d11c31ae430194d4aacbe8c7d702de094940 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:36:31 -0700 Subject: [PATCH 038/569] Fix FileEntry.findFiles() terms MATCH expr --- core/file_entry.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index b95c7acb..8738fcbd 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -3,7 +3,10 @@ const fileDb = require('./database.js').dbs.file; const Errors = require('./enig_error.js').Errors; -const getISOTimestampString = require('./database.js').getISOTimestampString; +const { + getISOTimestampString, + sanatizeString +} = require('./database.js'); const Config = require('./config.js').config; // deps @@ -523,11 +526,12 @@ module.exports = class FileEntry { } if(filter.terms && filter.terms.length > 0) { + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex appendWhereClause( `f.file_id IN ( SELECT rowid FROM file_fts - WHERE file_fts MATCH "${filter.terms.replace(/"/g,'""')}" + WHERE file_fts MATCH ":${sanatizeString(filter.terms)}" )` ); } From cb8d331415418e55b1946bab97d178a494eb0a6b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:37:03 -0700 Subject: [PATCH 039/569] Add 'data' member support to getData() --- core/horizontal_menu_view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 02f9c06e..d9921c96 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -163,5 +163,6 @@ HorizontalMenuView.prototype.onKeyPress = function(ch, key) { }; HorizontalMenuView.prototype.getData = function() { - return this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; \ No newline at end of file From d244cd25fa4324947f6bb019e80240599473eb75 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:38:02 -0700 Subject: [PATCH 040/569] Add getViewsByMciCode() * Store MCI code in View when created from MCI * Allow retrieval by MCI code --- core/mci_view_factory.js | 4 ++++ core/view_controller.js | 26 ++++++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index eb783d41..eab2787c 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -199,5 +199,9 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { break; } + if(view) { + view.mciCode = mci.code; + } + return view; }; diff --git a/core/view_controller.js b/core/view_controller.js index 71911f28..65a5a1b3 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -140,9 +140,9 @@ function ViewController(options) { }; this.createViewsFromMCI = function(mciMap, cb) { - async.each(Object.keys(mciMap), function entry(name, nextItem) { - var mci = mciMap[name]; - var view = self.mciViewFactory.createFromMCI(mci); + async.each(Object.keys(mciMap), (name, nextItem) => { + const mci = mciMap[name]; + const view = self.mciViewFactory.createFromMCI(mci); if(view) { if(false === self.noInput) { @@ -152,11 +152,11 @@ function ViewController(options) { self.addView(view); } - nextItem(null); + return nextItem(null); }, - function complete(err) { + err => { self.setViewOrder(); - cb(err); + return cb(err); }); }; @@ -426,6 +426,20 @@ ViewController.prototype.getView = function(id) { return this.views[id]; }; +ViewController.prototype.getViewsByMciCode = function(mciCode) { + if(!Array.isArray(mciCode)) { + mciCode = [ mciCode ]; + } + + const views = []; + _.each(this.views, v => { + if(mciCode.includes(v.mciCode)) { + views.push(v); + } + }); + return views; +}; + ViewController.prototype.getFocusedView = function() { return this.focusedView; }; From 783f142e20facfd1a3b35f93e38eea64192e3c9e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:41:13 -0700 Subject: [PATCH 041/569] Add refreshPredefinedMciViewsByCode() --- core/menu_module.js | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 783cc40b..a6fb3b4d 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -1,15 +1,16 @@ /* jslint node: true */ 'use strict'; -const PluginModule = require('./plugin_module.js').PluginModule; -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const ViewController = require('./view_controller.js').ViewController; -const menuUtil = require('./menu_util.js'); -const Config = require('./config.js').config; -const stringFormat = require('../core/string_format.js'); -const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; -const Errors = require('../core/enig_error.js').Errors; +const PluginModule = require('./plugin_module.js').PluginModule; +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const ViewController = require('./view_controller.js').ViewController; +const menuUtil = require('./menu_util.js'); +const Config = require('./config.js').config; +const stringFormat = require('../core/string_format.js'); +const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; +const Errors = require('../core/enig_error.js').Errors; +const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); // deps const async = require('async'); @@ -423,4 +424,17 @@ exports.MenuModule = class MenuModule extends PluginModule { ++customMciId; } } + + refreshPredefinedMciViewsByCode(formName, mciCodes) { + const form = _.get(this, [ 'viewControllers', formName] ); + if(form) { + form.getViewsByMciCode(mciCodes).forEach(v => { + if(!v.setText) { + return; + } + + v.setText(getPredefinedMCIValue(this.client, v.mciCode)); + }); + } + } }; From 0eee701bf6ee457ae9b82b515d0b123e241bff6d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:42:20 -0700 Subject: [PATCH 042/569] Add 'data' member support to getData() --- core/spinner_menu_view.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index 72b8b2f7..829255ca 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -7,6 +7,7 @@ const strUtil = require('./string_util.js'); const util = require('util'); const assert = require('assert'); +const _ = require('lodash'); exports.SpinnerMenuView = SpinnerMenuView; @@ -29,7 +30,8 @@ function SpinnerMenuView(options) { assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); - self.drawItem(this.focusedItemIndex); + this.drawItem(this.focusedItemIndex); + this.emit('index update', this.focusedItemIndex); }; this.drawItem = function() { @@ -96,7 +98,8 @@ SpinnerMenuView.prototype.onKeyPress = function(ch, key) { }; SpinnerMenuView.prototype.getData = function() { - return this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; SpinnerMenuView.prototype.setItems = function(items) { From cc2ee9c586607e3fbcd3e0d51a24624b21eea3dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:42:43 -0700 Subject: [PATCH 043/569] Add ESC support - WIP, not fully functional --- core/telnet_bridge.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 447efe4c..95ace6c6 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -158,7 +158,17 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { const telnetConnection = new TelnetClientConnection(self.client); + const connectionKeyPressHandler = (ch, key) => { + if('escape' === key.name) { + self.client.removeListener('key press', connectionKeyPressHandler); + telnetConnection.disconnect(); + } + }; + + self.client.on('key press', connectionKeyPressHandler); + telnetConnection.on('connected', () => { + self.client.removeListener('key press', connectionKeyPressHandler); self.client.log.info(connectOpts, 'Telnet bridge connection established'); if(self.config.font) { @@ -173,6 +183,8 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { }); telnetConnection.on('end', err => { + self.client.removeListener('key press', connectionKeyPressHandler); + if(err) { self.client.log.info(`Telnet bridge connection error: ${err.message}`); } From 837326e15ac41b02ccdbf10d951e1ba9c7985e70 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 22:45:03 -0700 Subject: [PATCH 044/569] MANY changes around message listing / viewing * If messageList is used, alwasy require items to contain areaTag * Standardize messageList a bit - still WIP, needs cleaned up * Lof of changes around area/conf tracking in relation to messages and message listings * Work for message searching * Clean up of various code, much to do... --- core/fse.js | 76 ++++++++++-------- core/menu_stack.js | 2 +- core/message.js | 72 ++++++++++++------ core/message_area.js | 35 ++------- core/message_base_search.js | 148 ++++++++++++++++++++++++++++++++++++ core/mod_mixins.js | 17 +++-- core/msg_area_view_fse.js | 10 +++ core/msg_list.js | 112 ++++++++++++++------------- core/vertical_menu_view.js | 3 +- 9 files changed, 329 insertions(+), 146 deletions(-) create mode 100644 core/message_base_search.js diff --git a/core/fse.js b/core/fse.js index de8fb1f9..c0135919 100644 --- a/core/fse.js +++ b/core/fse.js @@ -104,6 +104,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.editorMode = config.editorMode; if(config.messageAreaTag) { + // :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs this.messageAreaTag = config.messageAreaTag; } @@ -127,6 +128,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } } + this.noUpdateLastReadId = _.get(options, 'extraArgs.noUpdateLastReadId', config.noUpdateLastReadId) || false; + console.log(this.noUpdateLastReadId); + this.isReady = false; if(_.has(options, 'extraArgs.message')) { @@ -342,49 +346,56 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return cb(null); } + updateLastReadId(cb) { + if(this.noUpdateLastReadId) { + return cb(null); + } + + return updateMessageAreaLastReadId( + this.client.user.userId, this.messageAreaTag, this.message.messageId, cb + ); + } + setMessage(message) { this.message = message; - updateMessageAreaLastReadId( - this.client.user.userId, this.messageAreaTag, this.message.messageId, () => { + this.updateLastReadId( () => { + if(this.isReady) { + this.initHeaderViewMode(); + this.initFooterViewMode(); - if(this.isReady) { - this.initHeaderViewMode(); - this.initFooterViewMode(); + const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); + let msg = this.message.message; - const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); - let msg = this.message.message; - - if(bodyMessageView && _.has(this, 'message.message')) { + if(bodyMessageView && _.has(this, 'message.message')) { + // + // We handle ANSI messages differently than standard messages -- this is required as + // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted + // how the author wanted it + // + if(isAnsi(msg)) { // - // 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 + // Find tearline - we want to color it differently. // - if(isAnsi(msg)) { - // - // Find tearline - we want to color it differently. - // - const tearLinePos = this.message.getTearLinePosition(msg); + 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)); + 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)); } } } - ); + }); } getMessage(cb) { @@ -816,6 +827,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); + + // if we changed conf/area we need to update any related standard MCI view + this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] ); } initHeaderReplyEditMode() { diff --git a/core/menu_stack.js b/core/menu_stack.js index 26e88cc5..073dece5 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -117,7 +117,7 @@ module.exports = class MenuStack { }; if(_.isObject(options)) { - loadOpts.extraArgs = options.extraArgs; + loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); loadOpts.lastMenuResult = options.lastMenuResult; } diff --git a/core/message.js b/core/message.js index 6c47a934..f7f980f7 100644 --- a/core/message.js +++ b/core/message.js @@ -87,6 +87,12 @@ const FTN_PROPERTY_NAMES = { FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 }; +// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! +const MESSAGE_ROW_MAP = { + reply_to_message_id : 'replyToMsgId', + modified_timestamp : 'modTimestamp' +}; + module.exports = class Message { constructor( { @@ -189,6 +195,16 @@ module.exports = class Message { return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); } + static getMessageFromRow(row) { + const msg = {}; + _.each(row, (v, k) => { + // :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()! + k = MESSAGE_ROW_MAP[k] || _.camelCase(k); + msg[k] = v; + }); + return msg; + } + /* Find message IDs or UUIDs by filter. Available filters/options: @@ -199,11 +215,10 @@ module.exports = class Message { filter.replyToMesageId filter.newerThanTimestamp filter.newerThanMessageId - *filter.confTag - all area tags in confTag - filter.areaTag + filter.areaTag - note if you want by conf, send in all areas for a conf *filter.metaTuples - {category, name, value} - *filter.terms - FTS search + filter.terms - FTS search filter.sort = modTimestamp | messageId filter.order = ascending | (descending) @@ -223,7 +238,13 @@ module.exports = class Message { filter.resultType = filter.resultType || 'id'; filter.extraFields = filter.extraFields || []; - const field = 'id' === filter.resultType ? 'message_id' : 'message_uuid'; + if('messageList' === filter.resultType) { + filter.extraFields = _.uniq(filter.extraFields.concat( + [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ] + )); + } + + const field = 'uuid' === filter.resultType ? 'message_uuid' : 'message_id'; if(moment.isMoment(filter.newerThanTimestamp)) { filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); @@ -280,32 +301,23 @@ module.exports = class Message { WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} )`); } else { - let areaTags = []; - if(filter.confTag && filter.confTag.length > 0) { - // :TODO: grab areas from conf -> add to areaTags[] - } - - if(areaTags.length > 0 || filter.areaTag && filter.areaTag.length > 0) { + if(filter.areaTag && filter.areaTag.length > 0) { if(Array.isArray(filter.areaTag)) { - areaTags = areaTags.concat(filter.areaTag); - } else if(_.isString(filter.areaTag)) { - areaTags.push(filter.areaTag); - } - - areaTags = _.uniq(areaTags); // remove any dupes - - if(areaTags.length > 1) { const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); appendWhereClause(`m.area_tag IN(${areaList})`); - } else { - appendWhereClause(`m.area_tag = "${areaTags[0]}"`); + } else if(_.isString(filter.areaTag)) { + appendWhereClause(`m.area_tag = "${filter.areaTag}"`); } } } - [ 'toUserName', 'fromUserName', 'replyToMessageId' ].forEach(field => { + if(_.isNumber(filter.replyToMessageId)) { + appendWhereClause(`m.reply_to_message_id=${filter.replyToMessageId}`); + } + + [ 'toUserName', 'fromUserName' ].forEach(field => { if(_.isString(filter[field]) && filter[field].length > 0) { - appendWhereClause(`m.${_.snakeCase(field)} = "${sanatizeString(filter[field])}"`); + appendWhereClause(`m.${_.snakeCase(field)} LIKE "${sanatizeString(filter[field])}"`); } }); @@ -317,6 +329,17 @@ module.exports = class Message { appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); } + if(filter.terms && filter.terms.length > 0) { + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + appendWhereClause( + `m.message_id IN ( + SELECT rowid + FROM message_fts + WHERE message_fts MATCH ":${sanatizeString(filter.terms)}" + )` + ); + } + sql += `${sqlWhere} ${sqlOrderBy}`; if(_.isNumber(filter.limit)) { @@ -332,9 +355,12 @@ module.exports = class Message { } else { const matches = []; const extra = filter.extraFields.length > 0; + + const rowConv = 'messageList' === filter.resultType ? Message.getMessageFromRow : row => row; + msgDb.each(sql, (err, row) => { if(_.isObject(row)) { - matches.push(extra ? row : row[field]); + matches.push(extra ? rowConv(row) : row[field]); } }, err => { return cb(err, matches); diff --git a/core/message_area.js b/core/message_area.js index fb068e13..985da046 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -8,13 +8,11 @@ const Message = require('./message.js'); const Log = require('./logger.js').log; const msgNetRecord = require('./msg_network.js').recordMessage; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; -const { getISOTimestampString } = require('./database.js'); // deps const async = require('async'); const _ = require('lodash'); const assert = require('assert'); -const moment = require('moment'); exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; @@ -169,6 +167,7 @@ function getMessageConfTagByAreaTag(areaTag) { function getMessageAreaByTag(areaTag, optionalConfTag) { const confs = Config.messageConferences; + // :TODO: this could be cached if(_.isString(optionalConfTag)) { if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { return confs[optionalConfTag].areas[areaTag]; @@ -311,18 +310,6 @@ function changeMessageArea(client, areaTag, cb) { changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); } -function getMessageFromRow(row) { - return { - messageId : row.message_id, - messageUuid : row.message_uuid, - replyToMsgId : row.reply_to_message_id, - toUserName : row.to_user_name, - fromUserName : row.from_user_name, - subject : row.subject, - modTimestamp : row.modified_timestamp, - }; -} - function getNewMessageCountInAreaForUser(userId, areaTag, cb) { getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { lastMessageId = lastMessageId || 0; @@ -349,45 +336,33 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { const filter = { areaTag, + resultType : 'messageList', newerThanMessageId : lastMessageId, sort : 'messageId', order : 'ascending', - extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], }; if(Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = userId; } - Message.findMessages(filter, (err, messages) => { - if(err) { - return cb(err); - } - - return cb(null, messages.map(msg => getMessageFromRow(msg))); - }); + return Message.findMessages(filter, cb); }); } function getMessageListForArea(client, areaTag, cb) { const filter = { areaTag, + resultType : 'messageList', sort : 'messageId', order : 'ascending', - extraFields : [ 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], }; if(Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = client.user.userId; } - Message.findMessages(filter, (err, messages) => { - if(err) { - return cb(err); - } - - return cb(null, messages.map(msg => getMessageFromRow(msg))); - }); + return Message.findMessages(filter, cb); } function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { diff --git a/core/message_base_search.js b/core/message_base_search.js new file mode 100644 index 00000000..b0259490 --- /dev/null +++ b/core/message_base_search.js @@ -0,0 +1,148 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const { + getSortedAvailMessageConferences, + getAvailableMessageAreasByConfTag, + getSortedAvailMessageAreasByConfTag, +} = require('./message_area.js'); +const Errors = require('./enig_error.js').Errors; +const Message = require('./message.js'); + +// deps +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'Message Base Search', + desc : 'Module for quickly searching the message base', + author : 'NuSkooler', +}; + +const MciViewIds = { + search : { + searchTerms : 1, + search : 2, + conf : 3, + area : 4, + to : 5, + from : 6, + advSearch : 7, + } +}; + +exports.getModule = class MessageBaseSearch extends MenuModule { + constructor(options) { + super(options); + + this.menuMethods = { + search : (formData, extraArgs, cb) => { + return this.searchNow(formData, cb); + } + }; + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + this.prepViewController('search', 0, { mciMap : mciData.menu }, (err, vc) => { + if(err) { + return cb(err); + } + + const confView = vc.getView(MciViewIds.search.conf); + const areaView = vc.getView(MciViewIds.search.area); + + if(!confView || !areaView) { + return cb(Errors.DoesNotExist('Missing one or more required views')); + } + + const availConfs = [ { text : '-ALL-', data : '' } ].concat( + getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || [] + ); + + let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL + + confView.setItems(availConfs); + areaView.setItems(availAreas); + + confView.setFocusItemIndex(0); + areaView.setFocusItemIndex(0); + + confView.on('index update', idx => { + availAreas = [ { text : '-ALL-', data : '' } ].concat( + getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map( + area => Object.assign(area, { text : area.area.name, data : area.areaTag } ) + ) + ); + areaView.setItems(availAreas); + areaView.setFocusItemIndex(0); + }); + + vc.switchFocus(MciViewIds.search.searchTerms); + return cb(null); + }); + }); + } + + searchNow(formData, cb) { + const isAdvanced = formData.submitId === MciViewIds.search.advSearch; + const value = formData.value; + + const filter = { + resultType : 'messageList', + sort : 'modTimestamp', + terms : value.searchTerms, + //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned + }; + + if(isAdvanced) { + filter.toUserName = value.toUserName; + filter.fromUserName = value.fromUserName; + + if(value.confTag && !value.areaTag) { + // areaTag may be a string or array of strings + // getAvailableMessageAreasByConfTag() returns a obj - we only need tags + filter.areaTag = _.map( + getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ), + (area, areaTag) => areaTag + ); + } else if(value.areaTag) { + filter.areaTag = value.areaTag; // specific conf + area + } + } + + Message.findMessages(filter, (err, messageList) => { + if(err) { + return cb(err); + } + + if(0 === messageList.length) { + return this.gotoMenu( + this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', + { menuFlags : [ 'popParent' ] }, + cb + ); + } + + const menuOpts = { + extraArgs : { + messageList, + noUpdateLastReadId : true + }, + menuFlags : [ 'popParent' ], + }; + + return this.gotoMenu( + this.menuConfig.config.messageListMenu || 'messageAreaMessageList', + menuOpts, + cb + ); + }); + } +}; diff --git a/core/mod_mixins.js b/core/mod_mixins.js index 48546825..c830813a 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -2,22 +2,25 @@ 'use strict'; const messageArea = require('../core/message_area.js'); +const { get } = require('lodash'); exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { - tempMessageConfAndAreaSwitch(messageAreaTag) { - messageAreaTag = messageAreaTag || this.messageAreaTag; + tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { + messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); if(!messageAreaTag) { return; // nothing to do! } - this.prevMessageConfAndArea = { - confTag : this.client.user.properties.message_conf_tag, - areaTag : this.client.user.properties.message_area_tag, - }; + if(recordPrevious) { + this.prevMessageConfAndArea = { + confTag : this.client.user.properties.message_conf_tag, + areaTag : this.client.user.properties.message_area_tag, + }; + } - if(!messageArea.tempChangeMessageConfAndArea(this.client, this.messageAreaTag)) { + if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) { this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch'); } } diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index 0f25c63f..7452d9d2 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -31,6 +31,10 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { this.messageIndex = this.messageIndex || 0; this.messageTotal = this.messageList.length; + if(this.messageList.length > 0) { + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + } + const self = this; // assign *additional* menuMethods @@ -39,6 +43,9 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { if(self.messageIndex + 1 < self.messageList.length) { self.messageIndex++; + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); } @@ -55,6 +62,9 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { if(self.messageIndex > 0) { self.messageIndex--; + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); } diff --git a/core/msg_list.js b/core/msg_list.js index 72ee20f5..d48c1588 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -35,8 +35,8 @@ exports.moduleInfo = { author : 'NuSkooler', }; -const MCICodesIDs = { - MsgList : 1, // VM1 +const MciViewIds = { + msgList : 1, // VM1 MsgInfo1 : 2, // TL2 }; @@ -44,71 +44,68 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( constructor(options) { super(options); - const self = this; - const config = this.menuConfig.config; + // :TODO: consider this pattern in base MenuModule - clean up code all over + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.messageAreaTag = config.messageAreaTag; + // :TODO: Ugg, this is needed for MessageAreaConfTempSwitcher, which wants |this.messageAreaTag| explicitly + //this.messageAreaTag = this.config.messageAreaTag; this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); - if(options.extraArgs) { - // - // |extraArgs| can override |messageAreaTag| provided by config - // as well as supply a pre-defined message list - // - if(options.extraArgs.messageAreaTag) { - this.messageAreaTag = options.extraArgs.messageAreaTag; - } - - if(options.extraArgs.messageList) { - this.messageList = options.extraArgs.messageList; - } - } - this.menuMethods = { - selectMessage : function(formData, extraArgs, cb) { - if(1 === formData.submitId) { - self.initialFocusIndex = formData.value.message; + selectMessage : (formData, extraArgs, cb) => { + if(MciViewIds.msgList === formData.submitId) { + this.initialFocusIndex = formData.value.message; const modOpts = { extraArgs : { - messageAreaTag : self.messageAreaTag, - messageList : self.messageList, + messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, + messageList : this.config.messageList, messageIndex : formData.value.message, - lastMessageNextExit : true, + lastMessageNextExit : true, } }; + if(_.isBoolean(this.config.noUpdateLastReadId)) { + modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId; + } + // // Provide a serializer so we don't dump *huge* bits of information to the log // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 // + const self = this; modOpts.extraArgs.toJSON = function() { - const logMsgList = (this.messageList.length <= 4) ? - this.messageList : - this.messageList.slice(0, 2).concat(this.messageList.slice(-2)); + const logMsgList = (self.config.messageList.length <= 4) ? + self.config.messageList : + self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); return { + // note |this| is scope of toJSON()! messageAreaTag : this.messageAreaTag, apprevMessageList : logMsgList, messageCount : this.messageList.length, - messageIndex : formData.value.message, + messageIndex : this.messageIndex, }; }; - return self.gotoMenu(config.menuViewPost || 'messageAreaViewPost', modOpts, cb); + return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb); } else { return cb(null); } }, - fullExit : function(formData, extraArgs, cb) { - self.menuResult = { fullExit : true }; - return self.prevMenu(cb); + fullExit : (formData, extraArgs, cb) => { + this.menuResult = { fullExit : true }; + return this.prevMenu(cb); } }; } + getSelectedAreaTag(listIndex) { + return this.config.messageList[listIndex].areaTag || this.config.messageAreaTag; + } + enter() { if(this.lastMessageReachedExit) { return this.prevMenu(); @@ -118,12 +115,16 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // // Config can specify |messageAreaTag| else it comes from - // the user's current area + // the user's current area. If |messageList| is supplied, + // each item is expected to contain |areaTag|, so we use that + // instead in those cases. // - if(this.messageAreaTag) { - this.tempMessageConfAndAreaSwitch(this.messageAreaTag); - } else { - this.messageAreaTag = this.client.user.properties.message_area_tag; + if(!Array.isArray(this.config.messageList)) { + if(this.config.messageAreaTag) { + this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); + } else { + this.config.messageAreaTag = this.client.user.properties.message_area_tag; + } } } @@ -155,21 +156,27 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // // Config can supply messages else we'll need to populate the list now // - if(_.isArray(self.messageList)) { - return callback(0 === self.messageList.length ? new Error('No messages in area') : null); + if(_.isArray(self.config.messageList)) { + return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); } - messageArea.getMessageListForArea(self.client, self.messageAreaTag, function msgs(err, msgList) { + messageArea.getMessageListForArea(self.client, self.config.messageAreaTag, function msgs(err, msgList) { if(!msgList || 0 === msgList.length) { return callback(new Error('No messages in area')); } - self.messageList = msgList; + self.config.messageList = msgList; return callback(err); }); }, function getLastReadMesageId(callback) { - messageArea.getMessageAreaLastReadId(self.client.user.userId, self.messageAreaTag, function lastRead(err, lastReadId) { + // messageList entries can contain |isNew| if they want to be considered new + if(Array.isArray(self.config.messageList)) { + self.lastReadId = 0; + return callback(null); + } + + messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { self.lastReadId = lastReadId || 0; return callback(null); // ignore any errors, e.g. missing value }); @@ -180,10 +187,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues let msgNum = 1; - self.messageList.forEach( (listItem, index) => { + self.config.messageList.forEach( (listItem, index) => { listItem.msgNum = msgNum++; listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); - listItem.newIndicator = listItem.messageId > self.lastReadId ? newIndicator : regIndicator; + const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; + listItem.newIndicator = isNew ? newIndicator : regIndicator; if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { self.initialFocusIndex = index; @@ -192,7 +200,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( return callback(null); }, function populateList(callback) { - const msgListView = vc.getView(MCICodesIDs.MsgList); + const msgListView = vc.getView(MciViewIds.msgList); const listFormat = self.menuConfig.config.listFormat || '{msgNum} - {subject} - {toUserName}'; const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default change color here const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; @@ -200,19 +208,19 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // :TODO: This can take a very long time to load large lists. What we need is to implement the "owner draw" concept in // which items are requested (e.g. their format at least) *as-needed* vs trying to get the format for all of them at once - msgListView.setItems(_.map(self.messageList, listEntry => { + msgListView.setItems(_.map(self.config.messageList, listEntry => { return stringFormat(listFormat, listEntry); })); - msgListView.setFocusItems(_.map(self.messageList, listEntry => { + msgListView.setFocusItems(_.map(self.config.messageList, listEntry => { return stringFormat(focusListFormat, listEntry); })); msgListView.on('index update', idx => { self.setViewText( 'allViews', - MCICodesIDs.MsgInfo1, - stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.messageList.length } )); + MciViewIds.msgInfo1, + stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.config.messageList.length } )); }); if(self.initialFocusIndex > 0) { @@ -228,8 +236,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; self.setViewText( 'allViews', - MCICodesIDs.MsgInfo1, - stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.messageList.length } )); + MciViewIds.msgInfo1, + stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.config.messageList.length } )); return callback(null); }, ], diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 6f083193..423ffc00 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -178,8 +178,7 @@ VerticalMenuView.prototype.onKeyPress = function(ch, key) { VerticalMenuView.prototype.getData = function() { const item = this.getItem(this.focusedItemIndex); - return item.data ? item.data : this.focusedItemIndex; - //return this.focusedItemIndex; + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; VerticalMenuView.prototype.setItems = function(items) { From 5c580c1ecd698a46bc85da399dd9934e31fe3fbe Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 31 Jan 2018 23:01:42 -0700 Subject: [PATCH 045/569] Prevent private mail in message search results --- core/message.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/core/message.js b/core/message.js index f7f980f7..d7204e13 100644 --- a/core/message.js +++ b/core/message.js @@ -228,7 +228,8 @@ module.exports = class Message { filter.extraFields = [] filter.privateTagUserId = - if set, only private messages belonging to are processed - (any other areaTag or confTag filters will be ignored) + - any other areaTag or confTag filters will be ignored + - if NOT present, private areas are skipped *=NYI */ @@ -301,14 +302,21 @@ module.exports = class Message { WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} )`); } else { - if(filter.areaTag && filter.areaTag.length > 0) { + if(filter.areaTag && filter.areaTag.length > 0) { if(Array.isArray(filter.areaTag)) { - const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); - appendWhereClause(`m.area_tag IN(${areaList})`); - } else if(_.isString(filter.areaTag)) { + const areaList = filter.areaTag + .filter(t => t != Message.WellKnownAreaTags.Private) + .map(t => `"${t}"`).join(', '); + if(areaList.length > 0) { + appendWhereClause(`m.area_tag IN(${areaList})`); + } + } else if(_.isString(filter.areaTag) && Message.WellKnownAreaTags.Private !== filter.areaTag) { appendWhereClause(`m.area_tag = "${filter.areaTag}"`); } } + + // explicit exclude of Private + appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`); } if(_.isNumber(filter.replyToMessageId)) { From a121d60c1b72e60c9712d959ae30348e3a7ef98a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 1 Feb 2018 19:34:14 -0700 Subject: [PATCH 046/569] Fix lastReadId logic --- core/fse.js | 1 - core/msg_list.js | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/fse.js b/core/fse.js index c0135919..8a409589 100644 --- a/core/fse.js +++ b/core/fse.js @@ -129,7 +129,6 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } this.noUpdateLastReadId = _.get(options, 'extraArgs.noUpdateLastReadId', config.noUpdateLastReadId) || false; - console.log(this.noUpdateLastReadId); this.isReady = false; diff --git a/core/msg_list.js b/core/msg_list.js index d48c1588..ab0ab108 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -141,6 +141,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const self = this; const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + let configProvidedMessageList = false; async.series( [ @@ -157,6 +158,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // Config can supply messages else we'll need to populate the list now // if(_.isArray(self.config.messageList)) { + configProvidedMessageList = true; return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); } @@ -171,7 +173,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( }, function getLastReadMesageId(callback) { // messageList entries can contain |isNew| if they want to be considered new - if(Array.isArray(self.config.messageList)) { + if(configProvidedMessageList) { self.lastReadId = 0; return callback(null); } From 548ff41467d60bb649ad2d9b31001f60825eb0e9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 1 Feb 2018 20:29:26 -0700 Subject: [PATCH 047/569] Conceptual MenuItem caching - WIP for testing, will impl. in others if it seems good --- core/menu_view.js | 13 +++++++++++++ core/vertical_menu_view.js | 11 ++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/menu_view.js b/core/menu_view.js index f15491cf..dc2f5c81 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -29,6 +29,8 @@ function MenuView(options) { this.items = []; } + this.renderCache = {}; + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); this.setHotKeys(options.hotKeys); @@ -65,6 +67,7 @@ util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { this.sorted = false; + this.renderCache = {}; // // Items can be an array of strings or an array of objects. @@ -98,6 +101,16 @@ MenuView.prototype.setItems = function(items) { } }; +MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; +}; + +MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; +}; + MenuView.prototype.setSort = function(sort) { if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { return; diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 423ffc00..57570e16 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -64,6 +64,11 @@ function VerticalMenuView(options) { return; } + const cached = this.getRenderCacheItem(index, item.focused); + if(cached) { + return self.client.term.write(`${ansi.goto(item.row, self.position.col)}${cached}`); + } + let text; let sgr; if(item.focused && self.hasFocusItems()) { @@ -78,9 +83,9 @@ function VerticalMenuView(options) { sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); } - self.client.term.write( - `${ansi.goto(item.row, self.position.col)}${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}` - ); + text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; + self.client.term.write(`${ansi.goto(item.row, self.position.col)}${text}`); + this.setRenderCacheItem(index, text, item.focused); }; } From 1b58b85b1f38a344edc2c1852a33c515cb3011c9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 2 Feb 2018 21:22:47 -0700 Subject: [PATCH 048/569] Code cleanup + provide default 'text' member --- core/msg_list.js | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/core/msg_list.js b/core/msg_list.js index ab0ab108..90c5095a 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -47,9 +47,6 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // :TODO: consider this pattern in base MenuModule - clean up code all over this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - // :TODO: Ugg, this is needed for MessageAreaConfTempSwitcher, which wants |this.messageAreaTag| explicitly - //this.messageAreaTag = this.config.messageAreaTag; - this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); this.menuMethods = { @@ -184,12 +181,12 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( }); }, function updateMessageListObjects(callback) { - const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM Do'; + const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); const newIndicator = self.menuConfig.config.newIndicator || '*'; const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues let msgNum = 1; - self.config.messageList.forEach( (listItem, index) => { + self.config.messageList.forEach( (listItem, index) => { listItem.msgNum = msgNum++; listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; @@ -198,25 +195,17 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { self.initialFocusIndex = index; } + + listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text }); return callback(null); }, function populateList(callback) { const msgListView = vc.getView(MciViewIds.msgList); - const listFormat = self.menuConfig.config.listFormat || '{msgNum} - {subject} - {toUserName}'; - const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default change color here + // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; - // :TODO: This can take a very long time to load large lists. What we need is to implement the "owner draw" concept in - // which items are requested (e.g. their format at least) *as-needed* vs trying to get the format for all of them at once - - msgListView.setItems(_.map(self.config.messageList, listEntry => { - return stringFormat(listFormat, listEntry); - })); - - msgListView.setFocusItems(_.map(self.config.messageList, listEntry => { - return stringFormat(focusListFormat, listEntry); - })); + msgListView.setItems(self.config.messageList); msgListView.on('index update', idx => { self.setViewText( From 7e82f754e1d4f30efc563155e601b94d3d9cf795 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 2 Feb 2018 21:31:22 -0700 Subject: [PATCH 049/569] Use new itemFormat/focusItemFormat for default theme msg list --- art/themes/luciano_blocktronics/theme.hjson | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 27bd44ca..9cdf3838 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -207,14 +207,14 @@ } messageAreaMessageList: { - config: { - listFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}" - focusListFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" + config: { dateTimeFormat: ddd MMM Do } mci: { VM1: { height: 14 + itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}" + focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" } } } @@ -468,14 +468,14 @@ } newScanMessageList: { - config: { - listFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}" - focusListFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}" + config: { dateTimeFormat: ddd MMM Do } mci: { VM1: { height: 14 + itemFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}" + focusItemFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}" } } } From 0a486d290f5e30445aaf88e94ef97668ecd24277 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Feb 2018 10:27:32 -0700 Subject: [PATCH 050/569] Fix word wrap crash reported by user when pipe codes are in play --- core/string_util.js | 17 +++++++++++++---- core/word_wrap.js | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/string_util.js b/core/string_util.js index 4539ea38..20ff3511 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -218,8 +218,6 @@ function stringToNullTermBuffer(s, options = { encoding : 'utf8', maxBufLen : -1 } 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_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'); // @@ -275,12 +273,23 @@ function renderSubstr(str, start, length) { // // See also https://github.com/chalk/ansi-regex/blob/master/index.js // -function renderStringLength(s) { +function renderStringLength(s, options = { pipe : true, ansi : true } ) { let m; let pos; let len = 0; - const re = ANSI_OR_PIPE_REGEXP; + let re; + if(options.pipe && options.ansi) { + re = ANSI_OR_PIPE_REGEXP; + } else if(options.pipe) { + re = PIPE_REGEXP; + } else if(options.ansi) { + re = ANSI.getFullMatchRegExp(); + } else { + // no options - just return string length. + return s.length; + } + re.lastIndex = 0; // we recycle the rege; reset // diff --git a/core/word_wrap.js b/core/word_wrap.js index ecb728a5..d3e78b79 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -49,7 +49,7 @@ function wordWrapText(text, options) { function appendWord() { word.match(REGEXP_GOBBLE).forEach( w => { - renderLen = renderStringLength(w); + renderLen = renderStringLength(w, { ansi : true, pipe : false } ); if(result.renderLen[i] + renderLen > options.width) { if(0 === i) { From 1261af00c31e0916aba6a726a3a09cd90f04c680 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Feb 2018 10:38:52 -0700 Subject: [PATCH 051/569] Update Node.version for 0.0.9-alpha branch --- misc/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/install.sh b/misc/install.sh index 44554da1..36ab8202 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -2,7 +2,7 @@ { # this ensures the entire script is downloaded before execution -ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=6} +ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=8} ENIGMA_INSTALL_DIR=${ENIGMA_INSTALL_DIR:=$HOME/enigma-bbs} ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` @@ -24,7 +24,7 @@ ENiGMA½ will be installed to ${ENIGMA_INSTALL_DIR}, from source ${ENIGMA_SOURCE ENiGMA½ requires Node.js. Version ${ENIGMA_NODE_VERSION}.x current will be installed via nvm. If you already have nvm installed, this install script will update it to the latest version. -If this isn't what you were expecting, hit ctrl-c now. Installation will continue in ${WAIT_BEFORE_INSTALL} seconds... +If this isn't what you were expecting, hit CTRL-C now. Installation will continue in ${WAIT_BEFORE_INSTALL} seconds... EndOfMessage sleep ${WAIT_BEFORE_INSTALL} From aecc24079f512c09d6f4096eff6ecb9386e1fa2c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Feb 2018 10:39:14 -0700 Subject: [PATCH 052/569] Revert "Fix word wrap crash reported by user when pipe codes are in play" This reverts commit 0a486d290f5e30445aaf88e94ef97668ecd24277. --- core/string_util.js | 17 ++++------------- core/word_wrap.js | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/core/string_util.js b/core/string_util.js index 20ff3511..4539ea38 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -218,6 +218,8 @@ function stringToNullTermBuffer(s, options = { encoding : 'utf8', maxBufLen : -1 } 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_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'); // @@ -273,23 +275,12 @@ function renderSubstr(str, start, length) { // // See also https://github.com/chalk/ansi-regex/blob/master/index.js // -function renderStringLength(s, options = { pipe : true, ansi : true } ) { +function renderStringLength(s) { let m; let pos; let len = 0; - let re; - if(options.pipe && options.ansi) { - re = ANSI_OR_PIPE_REGEXP; - } else if(options.pipe) { - re = PIPE_REGEXP; - } else if(options.ansi) { - re = ANSI.getFullMatchRegExp(); - } else { - // no options - just return string length. - return s.length; - } - + const re = ANSI_OR_PIPE_REGEXP; re.lastIndex = 0; // we recycle the rege; reset // diff --git a/core/word_wrap.js b/core/word_wrap.js index d3e78b79..ecb728a5 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -49,7 +49,7 @@ function wordWrapText(text, options) { function appendWord() { word.match(REGEXP_GOBBLE).forEach( w => { - renderLen = renderStringLength(w, { ansi : true, pipe : false } ); + renderLen = renderStringLength(w); if(result.renderLen[i] + renderLen > options.width) { if(0 === i) { From 7555233ac769495e7415cc0733bab9e23929005d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Feb 2018 21:01:19 -0700 Subject: [PATCH 053/569] Fix some word wrap bugs previously introduced --- core/multi_line_edit_text_view.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 1be0591f..5757e708 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -411,6 +411,9 @@ function MultiLineEditTextView(options) { }; this.insertCharactersInText = function(c, index, col) { + const prevTextLength = self.getTextLength(index); + let editingEol = self.cursorPos.col === prevTextLength; + self.textLines[index].text = [ self.textLines[index].text.slice(0, col), c, @@ -419,18 +422,17 @@ function MultiLineEditTextView(options) { self.cursorPos.col += c.length; - let cursorOffset; - let absPos; - if(self.getTextLength(index) > self.dimens.width) { // // Update word wrapping and |cursorOffset| if the cursor // was within the bounds of the wrapped text // + let cursorOffset; const lastCol = self.cursorPos.col - c.length; const firstWrapRange = self.updateTextWordWrap(index); if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { cursorOffset = self.cursorPos.col - firstWrapRange.start; + editingEol = true; //override } else { cursorOffset = firstWrapRange.end; } @@ -438,14 +440,22 @@ function MultiLineEditTextView(options) { // redraw from current row to end of visible area self.redrawRows(self.cursorPos.row, self.dimens.height); - self.cursorBeginOfNextLine(); - self.cursorPos.col += cursorOffset; - self.client.term.rawWrite(ansi.right(cursorOffset)); + // If we're editing mid, we're done here. Else, we need to + // move the cursor to the new editing position after a wrap + if(editingEol) { + self.cursorBeginOfNextLine(); + self.cursorPos.col += cursorOffset; + self.client.term.rawWrite(ansi.right(cursorOffset)); + } else { + // adjust cursor after drawing new rows + const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col)); + } } else { // // We must only redraw from col -> end of current visible line // - absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length); self.client.term.write( From ced943867e97fafed4c202188aac7cf9387b018c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Feb 2018 18:52:24 -0700 Subject: [PATCH 054/569] #146 Fix color codes --- core/color_codes.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index db9f4fe5..10e7a2c3 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -116,14 +116,14 @@ function ansiSgrFromRenegadeColorCode(cc) { 22 : [ 'yellowBG' ], 23 : [ 'whiteBG' ], - 24 : [ 'bold', 'blackBG' ], - 25 : [ 'bold', 'blueBG' ], - 26 : [ 'bold', 'greenBG' ], - 27 : [ 'bold', 'cyanBG' ], - 28 : [ 'bold', 'redBG' ], - 29 : [ 'bold', 'magentaBG' ], - 30 : [ 'bold', 'yellowBG' ], - 31 : [ 'bold', 'whiteBG' ], + 24 : [ 'blink', 'blackBG' ], + 25 : [ 'blink', 'blueBG' ], + 26 : [ 'blink', 'greenBG' ], + 27 : [ 'blink', 'cyanBG' ], + 28 : [ 'blink', 'redBG' ], + 29 : [ 'blink', 'magentaBG' ], + 30 : [ 'blink', 'yellowBG' ], + 31 : [ 'blink', 'whiteBG' ], }[cc] || 'normal'); } From a611870dbfce8af659243fe9454bca73764b5cf9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Feb 2018 19:54:10 -0700 Subject: [PATCH 055/569] Update issue template for 0.0.9-alpha --- .github/ISSUE_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 5d3f54be..d53a6435 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,8 +3,8 @@ For :bug: bug reports, please fill out the information below plus any additional **Short problem description** **Environment** -- [ ] I am using Node.js v6.x or higher -- [ ] `npm install` reports success +- [ ] I am using Node.js v8.x LTS or higher +- [ ] `npm install` or `yarn` reports success - Actual Node.js version (`node --version`): - Operating system (`uname -a` on *nix systems): - Revision (`git rev-parse --short HEAD`): From 3db50816989c118a9ceb6b0c9f7685238809f604 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Feb 2018 20:13:29 -0700 Subject: [PATCH 056/569] Allow wildcards such as "21:*" for node configuraiton keys * Resolves TODO * Aligns with docs --- core/scanner_tossers/ftn_bso.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 8c87b61f..a98c6f4d 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -584,15 +584,6 @@ function FTNMessageScanTossModule() { }); }; - // :TODO: deprecate this in favor of getNodeConfigByAddress() - this.getNodeConfigKeyByAddress = function(uplink) { - const nodeKey = _.filter(Object.keys(this.moduleConfig.nodes), addr => { - return Address.fromString(addr).isPatternMatch(uplink); - })[0]; - - return nodeKey; - }; - this.exportNetMailMessagePacket = function(message, exportOpts, cb) { // // For NetMail, we always create a *single* packet per message. @@ -976,20 +967,19 @@ function FTNMessageScanTossModule() { this.exportEchoMailMessagesToUplinks = function(messageUuids, areaConfig, cb) { async.each(areaConfig.uplinks, (uplink, nextUplink) => { - const nodeConfigKey = self.getNodeConfigKeyByAddress(uplink); - if(!nodeConfigKey) { + const nodeConfig = self.getNodeConfigByAddress(uplink); + if(!nodeConfig) { return nextUplink(); } const exportOpts = { - nodeConfig : self.moduleConfig.nodes[nodeConfigKey], + nodeConfig, network : Config.messageNetworks.ftn.networks[areaConfig.network], destAddress : Address.fromString(uplink), networkName : areaConfig.network, - fileCase : self.moduleConfig.nodes[nodeConfigKey].fileCase || 'lower', + fileCase : nodeConfig.fileCase || 'lower', }; - if(_.isString(exportOpts.network.localAddress)) { exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); } From 8c7c20862c6f5194490bc41e8ffeef82f6e76053 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 7 Feb 2018 20:26:29 -0700 Subject: [PATCH 057/569] * Implement some missing placeholder ACS checks * Add some new ACS checks * Add documentation on new ACS --- core/acs_parser.js | 97 ++++++++++++++++++++++++++++----------- docs/configuration/acs.md | 11 ++++- misc/acs_parser.pegjs | 97 ++++++++++++++++++++++++++++----------- 3 files changed, 150 insertions(+), 55 deletions(-) diff --git a/core/acs_parser.js b/core/acs_parser.js index 36b9372a..3025f654 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -844,11 +844,10 @@ function peg$parse(input, options) { } - var client = options.client; - var user = options.client.user; + const client = options.client; + const user = options.client.user; - var _ = require('lodash'); - var assert = require('assert'); + const moment = require('moment'); function checkAccess(acsCode, value) { try { @@ -860,41 +859,85 @@ function peg$parse(input, options) { return !isNaN(value) && user.getAge() >= value; }, AS : function accountStatus() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { value = [ value ]; } const userAccountStatus = parseInt(user.properties.account_status, 10); - value = value.map(n => parseInt(n, 10)); // ensure we have integers - return value.indexOf(userAccountStatus) > -1; + return value.map(n => parseInt(n, 10)).includes(userAccountStatus); }, EC : function isEncoding() { + const encoding = client.term.outputEncoding.toLowerCase(); switch(value) { - case 0 : return 'cp437' === client.term.outputEncoding.toLowerCase(); - case 1 : return 'utf-8' === client.term.outputEncoding.toLowerCase(); + case 0 : return 'cp437' === encoding; + case 1 : return 'utf-8' === encoding; default : return false; } }, GM : function isOneOfGroups() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { return false; } - return _.findIndex(value, function cmp(groupName) { - return user.isGroupMember(groupName); - }) > - 1; + return value.some(groupName => user.isGroupMember(groupName)); }, NN : function isNode() { - return client.node === value; + if(!Array.isArray(value)) { + value = [ value ]; + } + return value.map(n => parseInt(n, 10)).includes(client.node); }, NP : function numberOfPosts() { - const postCount = parseInt(user.properties.post_count, 10); + const postCount = parseInt(user.properties.post_count, 10) || 0; return !isNaN(value) && postCount >= value; }, NC : function numberOfCalls() { const loginCount = parseInt(user.properties.login_count, 10); return !isNaN(value) && loginCount >= value; }, + AA : function accountAge() { + const accountCreated = moment(user.properties.account_created); + const now = moment(); + const daysOld = accountCreated.diff(moment(), 'days'); + return !isNaN(value) && + accountCreated.isValid() && + now.isAfter(accountCreated) && + daysOld >= value; + }, + BU : function bytesUploaded() { + const bytesUp = parseInt(user.properties.ul_total_bytes, 10) || 0; + return !isNaN(value) && bytesUp >= value; + }, + UP : function uploads() { + const uls = parseInt(user.properties.ul_total_count, 10) || 0; + return !isNaN(value) && uls >= value; + }, + BD : function bytesDownloaded() { + const bytesDown = parseInt(user.properties.dl_total_bytes, 10) || 0; + return !isNaN(value) && bytesDown >= value; + }, + DL : function downloads() { + const dls = parseInt(user.properties.dl_total_count, 10) || 0; + return !isNaN(value) && dls >= value; + }, + NR : function uploadDownloadRatioGreaterThan() { + const ulCount = parseInt(user.properties.ul_total_count, 10) || 0; + const dlCount = parseInt(user.properties.dl_total_count, 10) || 0; + const ratio = ~~((ulCount / dlCount) * 100); + return !isNaN(value) && ratio >= value; + }, + KR : function uploadDownloadByteRatioGreaterThan() { + const ulBytes = parseInt(user.properties.ul_total_bytes, 10) || 0; + const dlBytes = parseInt(user.properties.dl_total_bytes, 10) || 0; + const ratio = ~~((ulBytes / dlBytes) * 100); + return !isNaN(value) && ratio >= value; + }, + PC : function postCallRatio() { + const postCount = parseInt(user.properties.post_count, 10) || 0; + const loginCount = parseInt(user.properties.login_count, 10); + const ratio = ~~((postCount / loginCount) * 100); + return !isNaN(value) && ratio >= value; + }, SC : function isSecureConnection() { return client.session.isSecure; }, @@ -906,41 +949,41 @@ function peg$parse(input, options) { return !isNaN(value) && client.term.termHeight >= value; }, TM : function isOneOfThemes() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { return false; } - return value.indexOf(client.currentTheme.name) > -1; + return value.includes(client.currentTheme.name); }, TT : function isOneOfTermTypes() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { return false; } - return value.indexOf(client.term.termType) > -1; + return value.includes(client.term.termType); }, TW : function termWidth() { return !isNaN(value) && client.term.termWidth >= value; }, ID : function isUserId(value) { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { value = [ value ]; } - value = value.map(n => parseInt(n, 10)); // ensure we have integers - return value.indexOf(user.userId) > -1; + return value.map(n => parseInt(n, 10)).includes(user.userId); }, WD : function isOneOfDayOfWeek() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { value = [ value ]; } - value = value.map(n => parseInt(n, 10)); // ensure we have integers - return value.indexOf(new Date().getDay()) > -1; + return value.map(n => parseInt(n, 10)).includes(new Date().getDay()); }, MM : function isMinutesPastMidnight() { - // :TODO: return true if value is >= minutes past midnight sys time - return false; + const now = moment(); + const midnight = now.clone().startOf('day') + const minutesPastMidnight = now.diff(midnight, 'minutes'); + return !isNaN(value) && minutesPastMidnight >= value; } }[acsCode](value); } catch (e) { diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index 827050c4..3b7acbc0 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -16,7 +16,7 @@ The following are ACS codes available as of this writing: | ASstatus, AS[_status_,...] | User's account status is _group_ or one of [_group_,...] | | ECencoding | Terminal encoding is set to _encoding_ where `0` is `CP437` and `1` is `UTF-8` | | GM[_group_,...] | User belongs to one of [_group_,...] | -| NNnode | Current node is _node_ | +| NNnode, NN[_node_,...] | Current node is _node_ or one of [_node_,...] | | NPposts | User's number of message posts is >= _posts_ | | NCcalls | User's number of calls is >= _calls_ | | SC | Connection is considered secure (SSL, secure WebSockets, etc.) | @@ -26,6 +26,15 @@ The following are ACS codes available as of this writing: | TT[_termType_,...] | User's current terminal type is one of [_termType_,...] (`ANSI-BBS`, `utf8`, `xterm`, etc.) | | IDid, ID[_id_,...] | User's ID is _id_ or oen of [_id_,...] | | WDweekDay, WD[_weekDay_,...] | Current day of week is _weekDay_ or one of [_weekDay_,...] where `0` is Sunday, `1` is Monday, and so on. | +| AAdays | Account is >= _days_ old | +| BUbytes | User has uploaded >= _bytes_ | +| UPuploads | User has uploaded >= _uploads_ files | +| BDbytes | User has downloaded >= _bytes_ | +| DLdownloads | User has downloaded >= _downloads_ files | +| NRratio | User has upload/download count ratio >= _ratio_ | +| KRratio | User has a upload/download byte ratio >= _ratio_ | +| PCratio | User has a post/call ratio >= _ratio_ | +| MMminutes | It is currently >= _minutes_ past midnight (system time) \* Many more ACS codes are planned for the near future. diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs index b381ccef..4c9bc37b 100644 --- a/misc/acs_parser.pegjs +++ b/misc/acs_parser.pegjs @@ -1,10 +1,9 @@ { - var client = options.client; - var user = options.client.user; + const client = options.client; + const user = options.client.user; - var _ = require('lodash'); - var assert = require('assert'); + const moment = require('moment'); function checkAccess(acsCode, value) { try { @@ -16,41 +15,85 @@ return !isNaN(value) && user.getAge() >= value; }, AS : function accountStatus() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { value = [ value ]; } const userAccountStatus = parseInt(user.properties.account_status, 10); - value = value.map(n => parseInt(n, 10)); // ensure we have integers - return value.indexOf(userAccountStatus) > -1; + return value.map(n => parseInt(n, 10)).includes(userAccountStatus); }, EC : function isEncoding() { + const encoding = client.term.outputEncoding.toLowerCase(); switch(value) { - case 0 : return 'cp437' === client.term.outputEncoding.toLowerCase(); - case 1 : return 'utf-8' === client.term.outputEncoding.toLowerCase(); + case 0 : return 'cp437' === encoding; + case 1 : return 'utf-8' === encoding; default : return false; } }, GM : function isOneOfGroups() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { return false; } - return _.findIndex(value, function cmp(groupName) { - return user.isGroupMember(groupName); - }) > - 1; + return value.some(groupName => user.isGroupMember(groupName)); }, NN : function isNode() { - return client.node === value; + if(!Array.isArray(value)) { + value = [ value ]; + } + return value.map(n => parseInt(n, 10)).includes(client.node); }, NP : function numberOfPosts() { - const postCount = parseInt(user.properties.post_count, 10); + const postCount = parseInt(user.properties.post_count, 10) || 0; return !isNaN(value) && postCount >= value; }, NC : function numberOfCalls() { const loginCount = parseInt(user.properties.login_count, 10); return !isNaN(value) && loginCount >= value; }, + AA : function accountAge() { + const accountCreated = moment(user.properties.account_created); + const now = moment(); + const daysOld = accountCreated.diff(moment(), 'days'); + return !isNaN(value) && + accountCreated.isValid() && + now.isAfter(accountCreated) && + daysOld >= value; + }, + BU : function bytesUploaded() { + const bytesUp = parseInt(user.properties.ul_total_bytes, 10) || 0; + return !isNaN(value) && bytesUp >= value; + }, + UP : function uploads() { + const uls = parseInt(user.properties.ul_total_count, 10) || 0; + return !isNaN(value) && uls >= value; + }, + BD : function bytesDownloaded() { + const bytesDown = parseInt(user.properties.dl_total_bytes, 10) || 0; + return !isNaN(value) && bytesDown >= value; + }, + DL : function downloads() { + const dls = parseInt(user.properties.dl_total_count, 10) || 0; + return !isNaN(value) && dls >= value; + }, + NR : function uploadDownloadRatioGreaterThan() { + const ulCount = parseInt(user.properties.ul_total_count, 10) || 0; + const dlCount = parseInt(user.properties.dl_total_count, 10) || 0; + const ratio = ~~((ulCount / dlCount) * 100); + return !isNaN(value) && ratio >= value; + }, + KR : function uploadDownloadByteRatioGreaterThan() { + const ulBytes = parseInt(user.properties.ul_total_bytes, 10) || 0; + const dlBytes = parseInt(user.properties.dl_total_bytes, 10) || 0; + const ratio = ~~((ulBytes / dlBytes) * 100); + return !isNaN(value) && ratio >= value; + }, + PC : function postCallRatio() { + const postCount = parseInt(user.properties.post_count, 10) || 0; + const loginCount = parseInt(user.properties.login_count, 10); + const ratio = ~~((postCount / loginCount) * 100); + return !isNaN(value) && ratio >= value; + }, SC : function isSecureConnection() { return client.session.isSecure; }, @@ -62,41 +105,41 @@ return !isNaN(value) && client.term.termHeight >= value; }, TM : function isOneOfThemes() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { return false; } - return value.indexOf(client.currentTheme.name) > -1; + return value.includes(client.currentTheme.name); }, TT : function isOneOfTermTypes() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { return false; } - return value.indexOf(client.term.termType) > -1; + return value.includes(client.term.termType); }, TW : function termWidth() { return !isNaN(value) && client.term.termWidth >= value; }, ID : function isUserId(value) { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { value = [ value ]; } - value = value.map(n => parseInt(n, 10)); // ensure we have integers - return value.indexOf(user.userId) > -1; + return value.map(n => parseInt(n, 10)).includes(user.userId); }, WD : function isOneOfDayOfWeek() { - if(!_.isArray(value)) { + if(!Array.isArray(value)) { value = [ value ]; } - value = value.map(n => parseInt(n, 10)); // ensure we have integers - return value.indexOf(new Date().getDay()) > -1; + return value.map(n => parseInt(n, 10)).includes(new Date().getDay()); }, MM : function isMinutesPastMidnight() { - // :TODO: return true if value is >= minutes past midnight sys time - return false; + const now = moment(); + const midnight = now.clone().startOf('day') + const minutesPastMidnight = now.diff(midnight, 'minutes'); + return !isNaN(value) && minutesPastMidnight >= value; } }[acsCode](value); } catch (e) { From c26a8872e61f3beca512d102a6ea5fd0d0b37d4d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Feb 2018 10:54:04 -0700 Subject: [PATCH 058/569] Fix TIC node config lookup when wildcards are present --- core/tic_file_info.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/tic_file_info.js b/core/tic_file_info.js index 27e31e4c..124bbca1 100644 --- a/core/tic_file_info.js +++ b/core/tic_file_info.js @@ -91,8 +91,13 @@ module.exports = class TicFileInfo { return callback(Errors.Invalid(`No local area for "Area" of ${area}`)); } - const from = self.getAsString('From'); - localInfo.node = Object.keys(config.nodes).find( nodeAddr => Address.fromString(nodeAddr).isPatternMatch(from) ); + const from = Address.fromString(self.getAsString('From')); + if(!from.isValid()) { + return callback(Errors.Invalid(`Invalid "From" address: ${self.getAsString('From')}`)); + } + + // note that our config may have wildcards, such as "80:774/*" + localInfo.node = Object.keys(config.nodes).find( nodeAddrWildcard => from.isPatternMatch(nodeAddrWildcard) ); if(!localInfo.node) { return callback(Errors.Invalid('TIC is not from a known node')); From b5ad6ef1eec2b6c379ed74fb397c49256a0dcc18 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Mon, 12 Feb 2018 21:17:55 +0000 Subject: [PATCH 059/569] Update MMENU.ANS to include rumours and private mail --- art/themes/luciano_blocktronics/MMENU.ANS | Bin 3429 -> 3492 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index 995a5db59590a759b09502a341a97ae8ed2fb8f8..4a058fc910136e6839ec43a91e6b84fb2c5ac686 100644 GIT binary patch delta 123 zcmaDVwM2Tu87`?*1?gx5Yh$zAf}+f_#FA8n+{DZr>1bo4+{w?l9x0nTOGg{#mcta4 zXXFWYA7 Date: Mon, 12 Feb 2018 21:45:56 +0000 Subject: [PATCH 060/569] Remove unused view to clear warn from logs --- config/menu.hjson | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index cf2c71dd..0a57414c 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1914,9 +1914,6 @@ submit: true argName: message } - TL6: { - // theme me! - } } submit: { *: [ From 26e8e0f6d045168f6327c58846aec19086bcea26 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Mon, 12 Feb 2018 21:53:15 +0000 Subject: [PATCH 061/569] Fix message counter and message area display in message listings --- art/themes/luciano_blocktronics/MSGLIST.ANS | Bin 2291 -> 2249 bytes art/themes/luciano_blocktronics/NEWMSGS.ANS | Bin 2325 -> 2312 bytes .../luciano_blocktronics/PRVMSGLIST.ANS | Bin 2291 -> 2249 bytes core/msg_list.js | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) diff --git a/art/themes/luciano_blocktronics/MSGLIST.ANS b/art/themes/luciano_blocktronics/MSGLIST.ANS index 911f1f2030dd91fe3cdf1e638635457a2565362e..e1e0ddf86c304f43a5d013de1c28e9a6b0bbad54 100644 GIT binary patch delta 316 zcmew?cv5h}Ty9@S1?gymTYSs4>2pW{#k0Jr*IssI20 delta 370 zcmX>p_*rnmT-y*IBk5>EBWLMogIwuoV`Cu0*eF*aw>TXrY;A0iYo!1dQ4R4i0kI5n z^&u*N0;U^J=rPuV6iG*$nF4hh=H9-|rEvcaP@w@t*c@aaR9FG4umXj`^||-&0PVyy z1uTs26l3#Tpew*0yU(R?8(rEEY#7KBAbjUGnv0Qi1MR$XA0*9>YSs4>2pW{#k0Jr*IssI20 delta 370 zcmX>p_*rnmT-y*IBk5>EBWLMogIwuoV`Cu0*eF*aw>TXrY;A0iYo!1dQ4R4i0kI5n z^&u*N0;U^J=rPuV6iG*$nF4hh=H9-|rEvcaP@w@t*c@aaR9FG4umXj`^||-&0PVyy z1uTs26l3#Tpew*0yU(R?8(rEEY#7KBAbjUGnv0Qi1MR$XA0*9> Date: Mon, 12 Feb 2018 21:56:36 +0000 Subject: [PATCH 062/569] Correctly theme private message list --- art/themes/luciano_blocktronics/theme.hjson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 9cdf3838..3b6e91aa 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -262,13 +262,13 @@ mailMenuInbox: { config: { - listFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |01|31{newIndicator}" - focusListFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" dateTimeFormat: ddd MMM Do } mci: { VM1: { height: 14 + itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}" + focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" } } } From 9ad38f84a7775e011e440ae09cc11c206de2c217 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 16 Feb 2018 23:00:15 -0700 Subject: [PATCH 063/569] Add --quick option to fb scan ... --- core/file_entry.js | 13 +++ core/oputil/oputil_file_base.js | 144 +++++++++++++++++++++++++++----- 2 files changed, 134 insertions(+), 23 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 8738fcbd..467e0d3a 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -221,6 +221,19 @@ module.exports = class FileEntry { return paths.join(storageDir, this.fileName); } + static quickCheckExistsByPath(fullPath, cb) { + fileDb.get( + `SELECT COUNT() AS count + FROM file + WHERE file_name = ? + LIMIT 1;`, + [ paths.basename(fullPath) ], + (err, rows) => { + return err ? cb(err) : cb(null, rows.count > 0 ? true : false); + } + ); + } + static persistUserRating(fileId, userId, rating, cb) { return fileDb.run( `REPLACE INTO file_user_rating (file_id, user_id, rating) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index d6a18026..b1d1d97b 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -24,7 +24,7 @@ exports.handleFileBaseCommand = handleFileBaseCommand; Global options: --yes: assume yes - --no-prompt: try to avoid user input + --no-prompt: try to avoid user input Prompt for import and description before scan * Only after finding duplicate-by-path @@ -59,7 +59,7 @@ function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName; const descFromFile = getDescFromFileName(fileEntry.fileName); - + if(false === argv.prompt) { fileEntry.desc = descFromFile; return callback(null); @@ -116,7 +116,9 @@ function scanFileAreaForChanges(areaInfo, options, cb) { fe.hashTags = new Set(options.tags); } } - + + const FileEntry = require('../file_entry.js'); + async.eachSeries(storageLocations, (storageLoc, nextLocation) => { async.waterfall( [ @@ -157,6 +159,100 @@ function scanFileAreaForChanges(areaInfo, options, cb) { process.stdout.write(`Scanning ${fullPath}... `); + async.series( + [ + function quickCheck(next) { + if(!options.quick) { + return next(null); + } + + FileEntry.quickCheckExistsByPath(fullPath, (err, exists) => { + if(exists) { + console.info('Dupe'); + return nextFile(null); + } + + return next(null); + }); + }, + function fullScan() { + fileArea.scanFile( + fullPath, + { + areaTag : areaInfo.areaTag, + storageTag : storageLoc.storageTag + }, + (err, fileEntry, dupeEntries) => { + if(err) { + console.info(`Error: ${err.message}`); + return nextFile(null); // try next anyway + } + + // + // We'll update the entry if the following conditions are met: + // * We have a single duplicate, and: + // * --update was passed or the existing entry's desc, + // longDesc, or est_release_year meta are blank/empty + // + if(argv.update && 1 === dupeEntries.length) { + const FileEntry = require('../../core/file_entry.js'); + const existingEntry = new FileEntry(); + + return existingEntry.load(dupeEntries[0].fileId, err => { + if(err) { + console.info('Dupe (cannot update)'); + return nextFile(null); + } + + // + // Update only if tags or desc changed + // + const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; + const tagsEq = _.isEqual(optTags, existingEntry.hashTags); + + if( tagsEq && + fileEntry.desc === existingEntry.desc && + fileEntry.descLong == existingEntry.descLong && + fileEntry.meta.est_release_year == existingEntry.meta.est_release_year) + { + console.info('Dupe'); + return nextFile(null); + } + + console.info('Dupe (updating)'); + + // don't allow overwrite of values if new version is blank + existingEntry.desc = fileEntry.desc || existingEntry.desc; + existingEntry.descLong = fileEntry.descLong || existingEntry.descLong; + + if(fileEntry.meta.est_release_year) { + existingEntry.meta.est_release_year = fileEntry.meta.est_release_year; + } + + updateTags(existingEntry); + + finalizeEntryAndPersist(true, existingEntry, descHandler, err => { + return nextFile(err); + }); + }); + } else if(dupeEntries.length > 0) { + console.info('Dupe'); + return nextFile(null); + } + + console.info('Done!'); + updateTags(fileEntry); + + finalizeEntryAndPersist(false, fileEntry, descHandler, err => { + return nextFile(err); + }); + } + ); + } + ] + ); + + /* fileArea.scanFile( fullPath, { @@ -165,7 +261,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) { }, (err, fileEntry, dupeEntries) => { if(err) { - console.info(`Error: ${err.message}`); + console.info(`Error: ${err.message}`); return nextFile(null); // try next anyway } @@ -191,8 +287,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) { const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; const tagsEq = _.isEqual(optTags, existingEntry.hashTags); - if( tagsEq && - fileEntry.desc === existingEntry.desc && + if( tagsEq && + fileEntry.desc === existingEntry.desc && fileEntry.descLong == existingEntry.descLong && fileEntry.meta.est_release_year == existingEntry.meta.est_release_year) { @@ -220,15 +316,16 @@ function scanFileAreaForChanges(areaInfo, options, cb) { console.info('Dupe'); return nextFile(null); } - + console.info('Done!'); updateTags(fileEntry); - + finalizeEntryAndPersist(false, fileEntry, descHandler, err => { return nextFile(err); }); } ); + */ }); }, err => { return callback(err); @@ -239,12 +336,12 @@ function scanFileAreaForChanges(areaInfo, options, cb) { // :TODO: Look @ db entries for area that were *not* processed above return callback(null); } - ], + ], err => { return nextLocation(err); } ); - }, + }, err => { return cb(err); }); @@ -259,7 +356,7 @@ function dumpAreaInfo(areaInfo, areaAndStorageInfo, cb) { console.info(`storageTag: ${si.storageTag} => ${si.dir}`); }); console.info(''); - + return cb(null); } @@ -325,7 +422,7 @@ function dumpFileInfo(shaOrFileId, cb) { console.info(`path: ${fullPath}`); console.info(`hashTags: ${Array.from(fileEntry.hashTags).join(', ')}`); console.info(`uploaded: ${moment(fileEntry.uploadTimestamp).format()}`); - + _.each(fileEntry.meta, (metaValue, metaName) => { console.info(`${metaName}: ${metaValue}`); }); @@ -354,7 +451,7 @@ function displayFileAreaInfo() { [ function init(callback) { return initConfigAndDatabases(callback); - }, + }, function dumpInfo(callback) { const Config = require('../../core/config.js').config; let suppliedAreas = argv._.slice(2); @@ -396,8 +493,9 @@ function scanFileAreas() { options.tags = tags.split(','); } - options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH - + options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH + options.quick = argv.quick; + options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); async.series( @@ -405,10 +503,10 @@ function scanFileAreas() { function init(callback) { return initConfigAndDatabases(callback); }, - function initGlobalDescHandler(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 + // the description handler now. Else, we'll attempt to look for a description // file in each storage location. // if(!_.isString(options.descFile)) { @@ -546,14 +644,14 @@ function moveFiles() { }); }, function moveEntries(srcEntries, callback) { - + if(!dst.storageTag) { dst.storageTag = dst.areaInfo.storageTags[0]; } - + const destDir = FileEntry.getAreaStorageDirectoryByTag(dst.storageTag); - - async.eachSeries(srcEntries, (entry, nextEntry) => { + + async.eachSeries(srcEntries, (entry, nextEntry) => { const srcPath = entry.filePath; const dstPath = paths.join(destDir, entry.fileName); @@ -566,7 +664,7 @@ function moveFiles() { console.info('Done'); } return nextEntry(null); // always try next - }); + }); }, err => { return callback(err); @@ -652,7 +750,7 @@ function handleFileBaseCommand() { function errUsage() { return printUsageAndSetExitCode( - getHelpFor('FileBase') + getHelpFor('FileOpsInfo'), + getHelpFor('FileBase') + getHelpFor('FileOpsInfo'), ExitCodes.ERROR ); } From 95f4cd3fe25387c354fcfeeec1df49a0a21ca9ed Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 3 Mar 2018 21:16:01 -0700 Subject: [PATCH 064/569] Fix emit args --- core/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/events.js b/core/events.js index 5febb463..1838e087 100644 --- a/core/events.js +++ b/core/events.js @@ -22,7 +22,7 @@ module.exports = new class Events extends events.EventEmitter { emit(event, ...args) { Log.trace( { event : event }, 'Emitting event'); - return super.emit(event, args); + return super.emit(event, ...args); } on(event, listener) { From c3b62ac6086882b198055871b38e05b6ff485680 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 3 Mar 2018 21:16:21 -0700 Subject: [PATCH 065/569] Some default long formats for theme helpers --- core/config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/config.js b/core/config.js index 79824db5..6fcadd5b 100644 --- a/core/config.js +++ b/core/config.js @@ -173,12 +173,14 @@ function getDefaultConfig() { passwordChar : '*', // TODO: move to user ? dateFormat : { short : 'MM/DD/YYYY', + long : 'ddd, MMMM Do, YYYY', }, timeFormat : { short : 'h:mm a', }, dateTimeFormat : { short : 'MM/DD/YYYY h:mm a', + long : 'ddd, MMMM Do, YYYY, h:mm a', } }, From bb605d8781d416dd19b95beb466983e3c9029138 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 3 Mar 2018 21:40:28 -0700 Subject: [PATCH 066/569] * Add new well known meta for temp session d/ls * Better meta assign in ctor --- core/file_entry.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 467e0d3a..2ec03c17 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -38,6 +38,7 @@ const FILE_WELL_KNOWN_META = { tic_origin : null, // TIC "Origin" tic_desc : null, // TIC "Desc" tic_ldesc : null, // TIC "Ldesc" joined by '\n' + session_temp_dl : (v) => parseInt(v) ? true : false, }; module.exports = class FileEntry { @@ -46,11 +47,7 @@ module.exports = class FileEntry { this.fileId = options.fileId || 0; this.areaTag = options.areaTag || ''; - this.meta = options.meta || { - // values we always want - dl_count : 0, - }; - + this.meta = Object.assign( { dl_count : 0 }, options.meta); this.hashTags = options.hashTags || new Set(); this.fileName = options.fileName; this.storageTag = options.storageTag; From 4ccb059d614ef5b4f13f28b87f7fd47a1108ca6d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 3 Mar 2018 21:41:17 -0700 Subject: [PATCH 067/569] Add --quick to help --- core/oputil/oputil_help.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index b4c48267..baa0138c 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -67,7 +67,8 @@ scan args: other sources such as FILE_ID.DIZ. if PATH is specified, use DESCRIPT.ION at PATH instead of looking in specific storage locations - --update attempt to update information for existing entries + --update attempt to update information for existing entries + --quick perform quick scan info args: --show-desc display short description, if any From d3d8268df8e9875c8121a9e4495e4f2f4a091a42 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 3 Mar 2018 21:46:41 -0700 Subject: [PATCH 068/569] + New file base list export functionality (early beta!) * File base area startup() and cleanup * Better prepViewController() signature --- core/bbs.js | 3 + core/file_base_area_select.js | 2 +- core/file_base_list_export.js | 343 ++++++++++++++++++++++++++++++++++ core/menu_module.js | 6 +- core/message_base_search.js | 2 +- 5 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 core/file_base_list_export.js diff --git a/core/bbs.js b/core/bbs.js index c2f54802..33301f70 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -259,6 +259,9 @@ function initialize(cb) { function listenConnections(callback) { return require('./listening_server.js').startup(callback); }, + function readyFileBaseArea(callback) { + return require('./file_base_area.js').startup(callback); + }, function readyFileAreaWeb(callback) { return require('./file_area_web.js').startup(callback); }, diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index f762ab1d..df859bea 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -65,7 +65,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { return callback(null, availAreas); }, function prepView(availAreas, callback) { - self.prepViewController('allViews', 0, { mciMap : mciData.menu }, (err, vc) => { + self.prepViewController('allViews', 0, mciData.menu, (err, vc) => { if(err) { return callback(err); } diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js new file mode 100644 index 00000000..f3f185cd --- /dev/null +++ b/core/file_base_list_export.js @@ -0,0 +1,343 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const stringFormat = require('./string_format.js'); +const FileEntry = require('./file_entry.js'); +const FileArea = require('./file_base_area.js'); +const Config = require('./config.js').config; +const { Errors } = require('./enig_error.js'); +const { + splitTextAtTerms, + isAnsi +} = require('./string_util.js'); +const AnsiPrep = require('./ansi_prep.js'); +const Events = require('./events.js'); +const Log = require('./logger.js').log; +const DownloadQueue = require('./download_queue.js'); + +// deps +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const fse = require('fs-extra'); +const paths = require('path'); +const iconv = require('iconv-lite'); +const moment = require('moment'); +const uuidv4 = require('uuid/v4'); + +/* + :TODO: document: + - config options + - header template + - entry template + - header format + - entry format +*/ + +// :TODO: compress lists > N + +exports.moduleInfo = { + name : 'File Base List Export', + desc : 'Exports file base listings for download', + author : 'NuSkooler', +}; + +const FormIds = { + main : 0, +}; + +const MciViewIds = { + main : { + status : 1, + progressBar : 2, + + customRangeStart : 10, + } +}; + +const TEMPLATE_KEYS = [ // config.templates.* + 'header', 'entry', +]; + +exports.getModule = class FileBaseListExport extends MenuModule { + + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + + this.config.templateEncoding = this.config.templateEncoding || 'utf8'; + this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); + this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ + this.config.progBarChar = (this.config.progBarChar || '▒').charAt(0); + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + (callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), + (callback) => this.prepareList(callback), + ], + err => { + return cb(err); + } + ); + }); + } + + finishedLoading() { + this.prevMenu(); + } + + prepareList(cb) { + const self = this; + + const statusView = self.viewControllers.main.getView(MciViewIds.main.status); + const updateStatus = (status) => { + if(statusView) { + statusView.setText(status); + } + }; + + const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); + const updateProgressBar = (curr, total) => { + if(progBarView) { + const prog = Math.floor( (curr / total) * progBarView.dimens.width ); + progBarView.setText(self.config.progBarChar.repeat(prog)); + } + }; + + async.waterfall( + [ + function readTemplateFiles(callback) { + updateStatus('Preparing'); + + async.map(TEMPLATE_KEYS, (templateKey, nextKey) => { + let templatePath = _.get(self.config, [ 'templates', templateKey ]); + templatePath = templatePath || `file_list_${templateKey}.asc`; + templatePath = paths.isAbsolute(templatePath) ? templatePath : paths.join(Config.paths.misc, templatePath); + + fs.readFile(templatePath, (err, data) => { + return nextKey(err, data); + }); + }, (err, templates) => { + if(err) { + return Errors.General(err.message); + } + + // decode + ensure DOS style CRLF + templates = templates.map(tmp => iconv.decode(tmp, self.config.templateEncoding).replace(/\r?\n/g, '\r\n') ); + + // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements + let descIndent = 0; + splitTextAtTerms(templates[1]).some(line => { + const pos = line.indexOf('{fileDesc}'); + if(pos > -1) { + descIndent = pos; + return true; // found it! + } + return false; // keep looking + }); + + return callback(null, templates[0], templates[1], descIndent); + }); + }, + function findFiles(headerTemplate, entryTemplate, descIndent, callback) { + const filterCriteria = Object.assign({}, self.config.filterCriteria); + if(!filterCriteria.areaTag) { + filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); + } + + updateStatus('Gathering files for supplied criteria'); + + FileEntry.findFiles(filterCriteria, (err, fileIds) => { + // :TODO: handle empty file IDs -- bail early. + return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); + }); + }, + function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { + const formatObj = { + totalFileCount : fileIds.length, + }; + + const fileInfo = new FileEntry(); + let current = 0; + let listBody = ''; + const totals = { fileCount : fileIds.length, bytes : 0 }; + + async.eachSeries(fileIds, (fileId, nextFileId) => { + current += 1; + + fileInfo.load(fileId, err => { + if(err) { + return nextFileId(null); // failed, but try the next + } + + updateStatus(`Processing ${fileInfo.fileName}`); + + totals.bytes += fileInfo.meta.byte_size; + + updateProgressBar(current, fileIds.length); + + const appendFileInfo = () => { + listBody += stringFormat(entryTemplate, formatObj); + + self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, formatObj); + + return nextFileId(null); + }; + + const area = FileArea.getFileAreaByTag(fileInfo.areaTag); + + formatObj.fileId = fileId; + formatObj.areaName = _.get(area, 'name') || 'N/A'; + formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; + formatObj.userRating = fileInfo.userRating || 0; + formatObj.fileName = fileInfo.fileName; + formatObj.fileSize = fileInfo.meta.byte_size; + formatObj.fileDesc = fileInfo.desc || ''; + formatObj.fileDescShort = formatObj.fileDesc.slice(0, self.config.descWidth); + formatObj.fileSha256 = fileInfo.fileSha256; + formatObj.fileCrc32 = fileInfo.meta.file_crc32; + formatObj.fileMd5 = fileInfo.meta.file_md5; + formatObj.fileSha1 = fileInfo.meta.file_sha1; + formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; + formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(self.config.tsFormat); + formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; + formatObj.currentFile = current; + formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); + + if(isAnsi(fileInfo.desc)) { + AnsiPrep( + fileInfo.desc, + { + cols : Math.min(self.config.descWidth, 79 - descIndent), + forceLineTerm : true, // ensure each line is term'd + asciiMode : true, // export to ASCII + fillLines : false, // don't fill up to |cols| + indent : descIndent, + }, + (err, desc) => { + if(desc) { + formatObj.fileDesc = desc; + } + return appendFileInfo(); + } + ); + } else { + const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; + formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; + return appendFileInfo(); + } + }); + }, err => { + return callback(err, listBody, headerTemplate, totals); + }); + }, + function buildHeader(listBody, headerTemplate, totals, callback) { + // header is built last such that we can have totals/etc. + + let filterAreaName; + let filterAreaDesc; + if(self.config.filterCriteria.areaTag) { + const area = FileArea.getFileAreaByTag(self.config.filterCriteria.areaTag); + filterAreaName = _.get(area, 'name') || 'N/A'; + filterAreaDesc = _.get(area, 'desc') || 'N/A'; + } else { + filterAreaName = '-ALL-'; + filterAreaDesc = 'All areas'; + } + + const headerFormatObj = { + nowTs : moment().format(self.config.tsFormat), + boardName : Config.general.boardName, + totalFileCount : totals.fileCount, + totalFileSize : totals.bytes, + filterAreaTag : self.config.filterCriteria.areaTag || '-ALL-', + filterAreaName : filterAreaName, + filterAreaDesc : filterAreaDesc, + filterTerms : self.config.filterCriteria.terms || '(none)', + filterHashTags : self.config.filterCriteria.tags || '(none)', + }; + + listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; + return callback(null, listBody); + }, + function persistList(listBody, callback) { + + updateStatus('Persisting list'); + + const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); + const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); + + fse.mkdirs(sysTempDownloadDir, err => { + if(err) { + return callback(err); + } + + const outputFileName = paths.join( + sysTempDownloadDir, + `file_list_${uuidv4()}.txt` + ); + + fs.writeFile(outputFileName, listBody, 'utf8', err => { + return callback(err, outputFileName, sysTempDownloadArea); + }); + }); + }, + function persistFileEntry(outputFileName, sysTempDownloadArea, callback) { + fse.stat(outputFileName, (err, stats) => { + const newEntry = new FileEntry({ + areaTag : sysTempDownloadArea.areaTag, + fileName : paths.basename(outputFileName), + storageTag : sysTempDownloadArea.storageTags[0], + meta : { + upload_by_username : self.client.user.username, + upload_by_user_id : self.client.user.userId, + byte_size : stats.size, + session_temp_dl : 1, // download is valid until session is over + } + }); + + newEntry.desc = 'File List Export'; + + newEntry.persist(err => { + if(!err) { + // queue it! + const dlQueue = new DownloadQueue(self.client); + dlQueue.add(newEntry); + + // clean up after ourselves when the session ends + const thisClientId = self.client.session.id; + Events.once('codes.l33t.enigma.system.disconnected', evt => { // :TODO: Make a enum for system events/etc. + if(thisClientId === _.get(evt, 'client.session.id')) { + FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { + if(err) { + Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); + } else { + Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); + } + }); + } + }); + } + return callback(err); + }); + }); + }, + function done(callback) { + updateStatus('Exported list has been added to your download queue'); + return callback(null); + } + ], err => { + return cb(err); + } + ); + } +}; \ No newline at end of file diff --git a/core/menu_module.js b/core/menu_module.js index a6fb3b4d..97536bc7 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -311,7 +311,7 @@ exports.MenuModule = class MenuModule extends PluginModule { ); } - prepViewController(name, formId, artData, cb) { + prepViewController(name, formId, mciMap, cb) { if(_.isUndefined(this.viewControllers[name])) { const vcOpts = { client : this.client, @@ -322,7 +322,7 @@ exports.MenuModule = class MenuModule extends PluginModule { const loadOpts = { callingMenu : this, - mciMap : artData.mciMap, + mciMap : mciMap, formId : formId, }; @@ -345,7 +345,7 @@ exports.MenuModule = class MenuModule extends PluginModule { return cb(err); } - return this.prepViewController(name, formId, artData, cb); + return this.prepViewController(name, formId, artData.mciMap, cb); } ); } diff --git a/core/message_base_search.js b/core/message_base_search.js index b0259490..fdb17859 100644 --- a/core/message_base_search.js +++ b/core/message_base_search.js @@ -49,7 +49,7 @@ exports.getModule = class MessageBaseSearch extends MenuModule { return cb(err); } - this.prepViewController('search', 0, { mciMap : mciData.menu }, (err, vc) => { + this.prepViewController('search', 0, mciData.menu, (err, vc) => { if(err) { return cb(err); } From d260011ce8a4f5e1fa293a736cb418890a93eed8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 3 Mar 2018 21:47:04 -0700 Subject: [PATCH 069/569] + New file base list export functionality (early beta!) * File base area startup() and cleanup * Better prepViewController() signature --- core/file_base_area.js | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/file_base_area.js b/core/file_base_area.js index 6c7318d6..5f3b974a 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -26,6 +26,7 @@ const iconv = require('iconv-lite'); const execFile = require('child_process').execFile; const moment = require('moment'); +exports.startup = startup; exports.isInternalArea = isInternalArea; exports.getAvailableFileAreas = getAvailableFileAreas; exports.getAvailableFileAreaTags = getAvailableFileAreaTags; @@ -42,6 +43,7 @@ exports.scanFile = scanFile; exports.scanFileAreaForChanges = scanFileAreaForChanges; exports.getDescFromFileName = getDescFromFileName; exports.getAreaStats = getAreaStats; +exports.cleanUpTempSessionItems = cleanUpTempSessionItems; // for scheduler: exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; @@ -52,6 +54,10 @@ const WellKnownAreaTags = exports.WellKnownAreaTags = { TempDownloads : 'system_temporary_download', }; +function startup(cb) { + return cleanUpTempSessionItems(cb); +} + function isInternalArea(areaTag) { return [ WellKnownAreaTags.MessageAreaAttach, WellKnownAreaTags.TempDownloads ].includes(areaTag); } @@ -935,4 +941,42 @@ function updateAreaStatsScheduledEvent(args, cb) { return cb(err); }); +} + +function cleanUpTempSessionItems(cb) { + // find (old) temporary session items and nuke 'em + const filter = { + areaTag : WellKnownAreaTags.TempDownloads, + metaPairs : [ + { + name : 'session_temp_dl', + value : 1 + } + ] + }; + + FileEntry.findFiles(filter, (err, fileIds) => { + if(err) { + return cb(err); + } + + async.each(fileIds, (fileId, nextFileId) => { + const fileEntry = new FileEntry(); + fileEntry.load(fileId, err => { + if(err) { + Log.warn( { fileId }, 'Failed loading temporary session download item for cleanup'); + return nextFileId(null); + } + + FileEntry.removeEntry(fileEntry, { removePhysFile : true }, err => { + if(err) { + Log.warn( { fileId : fileEntry.fileId, filePath : fileEntry.filePath }, 'Failed to clean up temporary session download item'); + } + return nextFileId(null); + }); + }); + }, () => { + return cb(null); + }); + }); } \ No newline at end of file From 44a4a4aeb45a4b8a9b285d418f3191b77dd69e23 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Mar 2018 09:17:27 -0700 Subject: [PATCH 070/569] Updates to idle monitor inc. ability to disable --- core/client.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/core/client.js b/core/client.js index 58700c8f..24c8b1d1 100644 --- a/core/client.js +++ b/core/client.js @@ -414,26 +414,28 @@ Client.prototype.setTermType = function(termType) { }; Client.prototype.startIdleMonitor = function() { - var self = this; - - self.lastKeyPressMs = Date.now(); + this.lastKeyPressMs = Date.now(); // // Every 1m, check for idle. // - self.idleCheck = setInterval(function checkForIdle() { + this.idleCheck = setInterval( () => { const nowMs = Date.now(); - const idleLogoutSeconds = self.user.isAuthenticated() ? + const idleLogoutSeconds = this.user.isAuthenticated() ? Config.misc.idleLogoutSeconds : Config.misc.preAuthIdleLogoutSeconds; - if(nowMs - self.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { - self.emit('idle timeout'); + if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { + this.emit('idle timeout'); } }, 1000 * 60); }; +Client.prototype.stopIdleMonitor = function() { + clearInterval(this.idleCheck); +}; + Client.prototype.end = function () { if(this.term) { this.term.disconnect(); @@ -445,7 +447,7 @@ Client.prototype.end = function () { currentModule.leave(); } - clearInterval(this.idleCheck); + this.stopIdleMonitor(); try { // From 74b9d587c9cb3e25349510001e9ee0d2d6a4ecd9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Mar 2018 09:17:49 -0700 Subject: [PATCH 071/569] + Add compression for larger exports * Temp disable of idle monitor while building large lists * Fix hash tags * Handle no results & other errors --- core/file_base_list_export.js | 135 ++++++++++++++++++++++++---------- 1 file changed, 96 insertions(+), 39 deletions(-) diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index f3f185cd..f1f87aab 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -26,6 +26,7 @@ const paths = require('path'); const iconv = require('iconv-lite'); const moment = require('moment'); const uuidv4 = require('uuid/v4'); +const yazl = require('yazl'); /* :TODO: document: @@ -71,6 +72,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ this.config.progBarChar = (this.config.progBarChar || '▒').charAt(0); + this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) } mciReady(mciData, cb) { @@ -85,6 +87,13 @@ exports.getModule = class FileBaseListExport extends MenuModule { (callback) => this.prepareList(callback), ], err => { + if(err) { + if('NORESULTS' === err.reasonCode) { + return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults'); + } + + return this.prevMenu(); + } return cb(err); } ); @@ -157,7 +166,10 @@ exports.getModule = class FileBaseListExport extends MenuModule { updateStatus('Gathering files for supplied criteria'); FileEntry.findFiles(filterCriteria, (err, fileIds) => { - // :TODO: handle empty file IDs -- bail early. + if(0 === fileIds.length) { + return callback(Errors.General('No results for criteria', 'NORESULTS')); + } + return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); }); }, @@ -166,12 +178,15 @@ exports.getModule = class FileBaseListExport extends MenuModule { totalFileCount : fileIds.length, }; - const fileInfo = new FileEntry(); let current = 0; let listBody = ''; const totals = { fileCount : fileIds.length, bytes : 0 }; + // this may take quite a while; temp disable of idle monitor + self.client.stopIdleMonitor(); + async.eachSeries(fileIds, (fileId, nextFileId) => { + const fileInfo = new FileEntry(); current += 1; fileInfo.load(fileId, err => { @@ -237,6 +252,9 @@ exports.getModule = class FileBaseListExport extends MenuModule { } }); }, err => { + // re-enable idle monitor + self.client.startIdleMonitor(); + return callback(err, listBody, headerTemplate, totals); }); }, @@ -283,52 +301,56 @@ exports.getModule = class FileBaseListExport extends MenuModule { const outputFileName = paths.join( sysTempDownloadDir, - `file_list_${uuidv4()}.txt` + `file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` ); fs.writeFile(outputFileName, listBody, 'utf8', err => { - return callback(err, outputFileName, sysTempDownloadArea); + if(err) { + return callback(err); + } + + self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { + return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); + }); }); }); }, - function persistFileEntry(outputFileName, sysTempDownloadArea, callback) { - fse.stat(outputFileName, (err, stats) => { - const newEntry = new FileEntry({ - areaTag : sysTempDownloadArea.areaTag, - fileName : paths.basename(outputFileName), - storageTag : sysTempDownloadArea.storageTags[0], - meta : { - upload_by_username : self.client.user.username, - upload_by_user_id : self.client.user.userId, - byte_size : stats.size, - session_temp_dl : 1, // download is valid until session is over - } - }); + function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { + const newEntry = new FileEntry({ + areaTag : sysTempDownloadArea.areaTag, + fileName : paths.basename(outputFileName), + storageTag : sysTempDownloadArea.storageTags[0], + meta : { + upload_by_username : self.client.user.username, + upload_by_user_id : self.client.user.userId, + byte_size : fileSize, + session_temp_dl : 1, // download is valid until session is over + } + }); - newEntry.desc = 'File List Export'; + newEntry.desc = 'File List Export'; - newEntry.persist(err => { - if(!err) { - // queue it! - const dlQueue = new DownloadQueue(self.client); - dlQueue.add(newEntry); + newEntry.persist(err => { + if(!err) { + // queue it! + const dlQueue = new DownloadQueue(self.client); + dlQueue.add(newEntry); - // clean up after ourselves when the session ends - const thisClientId = self.client.session.id; - Events.once('codes.l33t.enigma.system.disconnected', evt => { // :TODO: Make a enum for system events/etc. - if(thisClientId === _.get(evt, 'client.session.id')) { - FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { - if(err) { - Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); - } else { - Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); - } - }); - } - }); - } - return callback(err); - }); + // clean up after ourselves when the session ends + const thisClientId = self.client.session.id; + Events.once('codes.l33t.enigma.system.disconnected', evt => { // :TODO: Make a enum for system events/etc. + if(thisClientId === _.get(evt, 'client.session.id')) { + FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { + if(err) { + Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); + } else { + Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); + } + }); + } + }); + } + return callback(err); }); }, function done(callback) { @@ -340,4 +362,39 @@ exports.getModule = class FileBaseListExport extends MenuModule { } ); } + + getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { + fse.stat(filePath, (err, stats) => { + if(err) { + return cb(err); + } + + if(stats.size < this.config.compressThreshold) { + // small enough, keep orig + return cb(null, filePath, stats.size); + } + + const zipFilePath = `${filePath}.zip`; + + const zipFile = new yazl.ZipFile(); + zipFile.addFile(filePath, paths.basename(filePath)); + zipFile.end( () => { + const outZipFile = fs.createWriteStream(zipFilePath); + zipFile.outputStream.pipe(outZipFile); + zipFile.outputStream.on('finish', () => { + // delete the original + fse.unlink(filePath, err => { + if(err) { + return cb(err); + } + + // finally stat the new output + fse.stat(zipFilePath, (err, stats) => { + return cb(err, zipFilePath, stats ? stats.size : 0); + }); + }); + }); + }); + }); + } }; \ No newline at end of file From 1482d0b78ffcc5118d27e84048b16a3a270946bf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Mar 2018 13:34:35 -0700 Subject: [PATCH 072/569] Add known system events enum - many more to come --- core/events.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/events.js b/core/events.js index 1838e087..b1e15c21 100644 --- a/core/events.js +++ b/core/events.js @@ -10,11 +10,21 @@ const _ = require('lodash'); const async = require('async'); const glob = require('glob'); +const SYSTEM_EVENTS = { + ClientConnected : 'codes.l33t.enigma.system.connected', + ClientDisconnected : 'codes.l33t.enigma.system.disconnected', + TermDetected : 'codes.l33t.enigma.term_detected', +}; + module.exports = new class Events extends events.EventEmitter { constructor() { super(); } + getSystemEvents() { + return SYSTEM_EVENTS; + } + addListener(event, listener) { Log.trace( { event : event }, 'Registering event listener'); return super.addListener(event, listener); From e7fb56946646573cfbc88705fc90d0511eaf1fd0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Mar 2018 13:35:05 -0700 Subject: [PATCH 073/569] Docs, some minor updates --- core/file_base_list_export.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index f1f87aab..9c63ab2d 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -10,7 +10,8 @@ const Config = require('./config.js').config; const { Errors } = require('./enig_error.js'); const { splitTextAtTerms, - isAnsi + isAnsi, + renderSubstr } = require('./string_util.js'); const AnsiPrep = require('./ansi_prep.js'); const Events = require('./events.js'); @@ -29,15 +30,27 @@ const uuidv4 = require('uuid/v4'); const yazl = require('yazl'); /* - :TODO: document: - - config options - - header template - - entry template - - header format - - entry format -*/ + Module config block can contain the following: + templateEncoding - encoding of template files (utf8) + tsFormat - timestamp format (theme 'short') + descWidth - max desc width (45) + progBarChar - progress bar character (▒) + compressThreshold - threshold to kick in comrpession for lists (1.44 MiB) + templates - object containing: + header - filename of header template (misc/file_list_header.asc) + entry - filename of entry template (misc/file_list_entry.asc) -// :TODO: compress lists > N + Header template variables: + nowTs, boardName, totalFileCount, totalFileSize, + filterAreaTag, filterAreaName, filterAreaDesc, + filterTerms, filterHashTags + + Entry template variables: + fileId, areaName, areaDesc, userRating, fileName, + fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32, + fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags, + currentFile, progress, +*/ exports.moduleInfo = { name : 'File Base List Export', @@ -71,7 +84,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { this.config.templateEncoding = this.config.templateEncoding || 'utf8'; this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ - this.config.progBarChar = (this.config.progBarChar || '▒').charAt(0); + this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) } @@ -338,7 +351,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { // clean up after ourselves when the session ends const thisClientId = self.client.session.id; - Events.once('codes.l33t.enigma.system.disconnected', evt => { // :TODO: Make a enum for system events/etc. + Events.once(Events.getSystemEvents().ClientDisconnected, evt => { if(thisClientId === _.get(evt, 'client.session.id')) { FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { if(err) { From cac3e0ceae445915955206efb93d43f43a6f7bbb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Mar 2018 13:35:36 -0700 Subject: [PATCH 074/569] Better fillChar handling, specialKeyMapOverride() --- core/view.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/core/view.js b/core/view.js index 1829f26d..1333ca24 100644 --- a/core/view.js +++ b/core/view.js @@ -2,11 +2,12 @@ 'use strict'; // ENiGMA½ -const events = require('events'); -const util = require('util'); -const ansi = require('./ansi_term.js'); -const colorCodes = require('./color_codes.js'); -const enigAssert = require('./enigma_assert.js'); +const events = require('events'); +const util = require('util'); +const ansi = require('./ansi_term.js'); +const colorCodes = require('./color_codes.js'); +const enigAssert = require('./enigma_assert.js'); +const { renderSubstr } = require('./string_util.js'); // deps const _ = require('lodash'); @@ -85,6 +86,10 @@ function View(options) { if(this.acceptsInput) { this.specialKeyMap = options.specialKeyMap || VIEW_SPECIAL_KEY_MAP_DEFAULT; + + if(_.isObject(options.specialKeyMapOverride)) { + this.setSpecialKeyMapOverride(options.specialKeyMapOverride); + } } this.isKeyMapped = function(keySet, keyName) { @@ -177,6 +182,10 @@ View.prototype.getFocusSGR = function() { return this.ansiFocusSGR; }; +View.prototype.setSpecialKeyMapOverride = function(specialKeyMapOverride) { + this.specialKeyMap = Object.assign(this.specialKeyMap, specialKeyMapOverride); +}; + View.prototype.setPropertyValue = function(propName, value) { switch(propName) { case 'height' : this.setHeight(value); break; @@ -199,7 +208,7 @@ View.prototype.setPropertyValue = function(propName, value) { if(_.isNumber(value)) { this.fillChar = String.fromCharCode(value); } else if(_.isString(value)) { - this.fillChar = value.substr(0, 1); + this.fillChar = renderSubstr(value, 0, 1); } } break; From 17cebdebce43c3a5101d25e099f6536603f8d451 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Mar 2018 13:35:57 -0700 Subject: [PATCH 075/569] Better fillChar handling --- core/text_view.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/text_view.js b/core/text_view.js index ea9c352a..8de04484 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -31,7 +31,7 @@ function TextView(options) { this.maxLength = this.client.term.termWidth - this.position.col; } - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + this.fillChar = renderSubstr(miscUtil.valueWithDefault(options.fillChar, ' '), 0, 1); this.justify = options.justify || 'left'; this.resizable = miscUtil.valueWithDefault(options.resizable, true); this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); @@ -120,11 +120,13 @@ function TextView(options) { } } + const renderedFillChar = pipeToAnsi(this.fillChar); + this.client.term.write( padStr( textToDraw, this.dimens.width + 1, - this.fillChar, + renderedFillChar, //this.fillChar, this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), this.getStyleSGR(1) || this.getSGR() From 281bfbc2aa563c0f1b4601148e934a73d76fa527 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Mar 2018 20:47:25 -0700 Subject: [PATCH 076/569] Implement isLocal() for 'LC' ACS --- core/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/client.js b/core/client.js index 24c8b1d1..a6c6b78a 100644 --- a/core/client.js +++ b/core/client.js @@ -475,8 +475,8 @@ Client.prototype.waitForKeyPress = function(cb) { }; Client.prototype.isLocal = function() { - // :TODO: return rather client is a local connection or not - return false; + // :TODO: Handle ipv6 better + return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); }; /////////////////////////////////////////////////////////////////////////////// From 63ff2a60573f3760b2b00b970ab030059c4f85ab Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 8 Mar 2018 19:56:00 -0700 Subject: [PATCH 077/569] Templates for new export --- misc/file_list_entry.asc | 7 +++++++ misc/file_list_header.asc | 11 +++++++++++ 2 files changed, 18 insertions(+) create mode 100644 misc/file_list_entry.asc create mode 100644 misc/file_list_header.asc diff --git a/misc/file_list_entry.asc b/misc/file_list_entry.asc new file mode 100644 index 00000000..fe1785df --- /dev/null +++ b/misc/file_list_entry.asc @@ -0,0 +1,7 @@ +{fileName:<32.33} {fileSize!sizeWithAbbr:<8.7} {fileUploadTs} + + {fileDesc} + +tags: {fileHashTags} +sha1: {fileSha1} + diff --git a/misc/file_list_header.asc b/misc/file_list_header.asc new file mode 100644 index 00000000..4e307ca7 --- /dev/null +++ b/misc/file_list_header.asc @@ -0,0 +1,11 @@ +------------------------------------------------------------------------------- +{boardName} File Base List Export - Generated {nowTs} + +Search Criteria: + Area : {filterAreaName} + Terms: {filterTerms} + Tags : {filterHashTags} + +Total Files: {totalFileCount} / {totalFileSize!sizeWithAbbr} +------------------------------------------------------------------------------- + From f6f1de4bd884444019193cd06aae5fa28074ab8d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 8 Mar 2018 21:39:42 -0700 Subject: [PATCH 078/569] Move to pty-node over custom pty2.js --- core/archive_util.js | 2 +- core/door.js | 18 +++++++----------- core/event_scheduler.js | 2 +- core/file_transfer.js | 15 +++++++++------ package.json | 2 +- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index 00e48ed8..8a4c388a 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -10,7 +10,7 @@ const resolveMimeType = require('./mime_util.js').resolveMimeType; // base/modules const fs = require('graceful-fs'); const _ = require('lodash'); -const pty = require('ptyw.js'); +const pty = require('node-pty'); let archiveUtil; diff --git a/core/door.js b/core/door.js index 525a7a02..58c8effa 100644 --- a/core/door.js +++ b/core/door.js @@ -6,7 +6,7 @@ const stringFormat = require('./string_format.js'); const events = require('events'); const _ = require('lodash'); -const pty = require('ptyw.js'); +const pty = require('node-pty'); const decode = require('iconv-lite').decode; const createServer = require('net').createServer; @@ -18,8 +18,7 @@ function Door(client, exeInfo) { const self = this; this.client = client; this.exeInfo = exeInfo; - this.exeInfo.encoding = this.exeInfo.encoding || 'cp437'; - this.exeInfo.encoding = this.exeInfo.encoding.toLowerCase(); + this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); let restored = false; // @@ -36,11 +35,7 @@ function Door(client, exeInfo) { // this.doorDataHandler = function(data) { - if(self.client.term.outputEncoding === self.exeInfo.encoding) { - self.client.term.rawWrite(data); - } else { - self.client.term.write(decode(data, self.exeInfo.encoding)); - } + self.client.term.write(decode(data, self.exeInfo.encoding)); }; this.restoreIo = function(piped) { @@ -113,10 +108,11 @@ Door.prototype.run = function() { } const door = pty.spawn(self.exeInfo.cmd, args, { - cols : self.client.term.termWidth, - rows : self.client.term.termHeight, + cols : self.client.term.termWidth, + rows : self.client.term.termHeight, // :TODO: cwd - env : self.exeInfo.env, + env : self.exeInfo.env, + encoding : null, // we want to handle all encoding ourself }); if('stdio' === self.exeInfo.io) { diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 0366d5ba..10c59f72 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -9,7 +9,7 @@ const Log = require('./logger.js').log; const _ = require('lodash'); const later = require('later'); const path = require('path'); -const pty = require('ptyw.js'); +const pty = require('node-pty'); const sane = require('sane'); const moment = require('moment'); const paths = require('path'); diff --git a/core/file_transfer.js b/core/file_transfer.js index bb3d362c..757eff8a 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -14,7 +14,7 @@ const Log = require('./logger.js').log; // deps const async = require('async'); const _ = require('lodash'); -const pty = require('ptyw.js'); +const pty = require('node-pty'); const temptmp = require('temptmp').createTrackedSession('transfer_file'); const paths = require('path'); const fs = require('graceful-fs'); @@ -361,11 +361,14 @@ exports.getModule = class TransferFileModule extends MenuModule { 'Executing external protocol' ); - const externalProc = pty.spawn(cmd, args, { - cols : this.client.term.termWidth, - rows : this.client.term.termHeight, - cwd : this.recvDirectory, - }); + const spawnOpts = { + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, + cwd : this.recvDirectory, + encoding : null, // don't bork our data! + }; + + const externalProc = pty.spawn(cmd, args, spawnOpts); this.client.setTemporaryDirectDataHandler(data => { // needed for things like sz/rz diff --git a/package.json b/package.json index 8c7a7a5d..88844455 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "minimist": "1.2.x", "moment": "^2.20.1", "nodemailer": "^4.4.1", - "ptyw.js": "NuSkooler/ptyw.js", + "node-pty": "^0.7.4", "rlogin": "^1.0.0", "sane": "^2.2.0", "sanitize-filename": "^1.6.1", From 9c87d4543364193f45db97dd5b47b7768899c546 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Mar 2018 11:37:23 -0700 Subject: [PATCH 079/569] Add GLOB support to oputil fb scan... --- core/oputil/oputil_common.js | 10 ++++++++++ core/oputil/oputil_file_base.js | 22 ++++++++++++++++++++-- core/oputil/oputil_help.js | 2 ++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index e175a166..d03bb82f 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -15,6 +15,7 @@ exports.getDefaultConfigPath = getDefaultConfigPath; exports.getConfigPath = getConfigPath; exports.initConfigAndDatabases = initConfigAndDatabases; exports.getAreaAndStorage = getAreaAndStorage; +exports.looksLikePattern = looksLikePattern; const exitCodes = exports.ExitCodes = { SUCCESS : 0, @@ -87,4 +88,13 @@ function getAreaAndStorage(tags) { } return entry; }); +} + +function looksLikePattern(tag) { + // globs can start with @ + if(tag.indexOf('@') > 0) { + return false; + } + + return /[*?[\]!()+|^]/.test(tag); } \ No newline at end of file diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index b1d1d97b..a2abbe99 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -7,7 +7,10 @@ const ExitCodes = require('./oputil_common.js').ExitCodes; const argv = require('./oputil_common.js').argv; const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; const getHelpFor = require('./oputil_help.js').getHelpFor; -const getAreaAndStorage = require('./oputil_common.js').getAreaAndStorage; +const { + getAreaAndStorage, + looksLikePattern +} = require('./oputil_common.js'); const Errors = require('../enig_error.js').Errors; const async = require('async'); @@ -16,6 +19,7 @@ const paths = require('path'); const _ = require('lodash'); const moment = require('moment'); const inq = require('inquirer'); +const glob = require('glob'); exports.handleFileBaseCommand = handleFileBaseCommand; @@ -119,6 +123,14 @@ function scanFileAreaForChanges(areaInfo, options, cb) { const FileEntry = require('../file_entry.js'); + const readDir = options.glob ? + (dir, next) => { + return glob(options.glob, { cwd : dir, nodir : true }, next); + } : + (dir, next) => { + return fs.readdir(dir, next); + }; + async.eachSeries(storageLocations, (storageLoc, nextLocation) => { async.waterfall( [ @@ -134,7 +146,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) { function scanPhysFiles(descHandler, callback) { const physDir = storageLoc.dir; - fs.readdir(physDir, (err, files) => { + readDir(physDir, (err, files) => { if(err) { return callback(err); } @@ -498,6 +510,12 @@ function scanFileAreas() { options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); + const last = argv._[argv._.length - 1]; + if(options.areaAndStorageInfo.length > 1 && looksLikePattern(last)) { + options.glob = last; + options.areaAndStorageInfo.length -= 1; + } + async.series( [ function init(callback) { diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index baa0138c..4875d382 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -49,6 +49,8 @@ import-areas args: actions: scan AREA_TAG[@STORAGE_TAG] scan specified area + may also contain optional GLOB as last parameter, + for examle: scan some_area *.zip info AREA_TAG|SHA|FILE_ID display information about areas and/or files SHA may be a full or partial SHA-256 From edc0bf5e068ade0d123a41422af5a20478e3d536 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 11 Mar 2018 21:23:23 -0600 Subject: [PATCH 080/569] Split up code a bit in prep for DESCRIPT.ION generator --- core/file_base_user_list_export.js | 292 +++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 core/file_base_user_list_export.js diff --git a/core/file_base_user_list_export.js b/core/file_base_user_list_export.js new file mode 100644 index 00000000..4a2d0c4b --- /dev/null +++ b/core/file_base_user_list_export.js @@ -0,0 +1,292 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const FileEntry = require('./file_entry.js'); +const FileArea = require('./file_base_area.js'); +const { renderSubstr } = require('./string_util.js'); +const { Errors } = require('./enig_error.js'); +const Events = require('./events.js'); +const Log = require('./logger.js').log; +const DownloadQueue = require('./download_queue.js'); +const exportFileList = require('./file_base_list_export.js'); + +// deps +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const fse = require('fs-extra'); +const paths = require('path'); +const moment = require('moment'); +const uuidv4 = require('uuid/v4'); +const yazl = require('yazl'); + +/* + Module config block can contain the following: + templateEncoding - encoding of template files (utf8) + tsFormat - timestamp format (theme 'short') + descWidth - max desc width (45) + progBarChar - progress bar character (▒) + compressThreshold - threshold to kick in comrpession for lists (1.44 MiB) + templates - object containing: + header - filename of header template (misc/file_list_header.asc) + entry - filename of entry template (misc/file_list_entry.asc) + + Header template variables: + nowTs, boardName, totalFileCount, totalFileSize, + filterAreaTag, filterAreaName, filterAreaDesc, + filterTerms, filterHashTags + + Entry template variables: + fileId, areaName, areaDesc, userRating, fileName, + fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32, + fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags, + currentFile, progress, +*/ + +exports.moduleInfo = { + name : 'File Base List Export', + desc : 'Exports file base listings for download', + author : 'NuSkooler', +}; + +const FormIds = { + main : 0, +}; + +const MciViewIds = { + main : { + status : 1, + progressBar : 2, + + customRangeStart : 10, + } +}; + +exports.getModule = class FileBaseListExport extends MenuModule { + + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + + this.config.templateEncoding = this.config.templateEncoding || 'utf8'; + this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); + this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ + this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); + this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + (callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), + (callback) => this.prepareList(callback), + ], + err => { + if(err) { + if('NORESULTS' === err.reasonCode) { + return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults'); + } + + return this.prevMenu(); + } + return cb(err); + } + ); + }); + } + + finishedLoading() { + this.prevMenu(); + } + + prepareList(cb) { + const self = this; + + const statusView = self.viewControllers.main.getView(MciViewIds.main.status); + const updateStatus = (status) => { + if(statusView) { + statusView.setText(status); + } + }; + + const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); + const updateProgressBar = (curr, total) => { + if(progBarView) { + const prog = Math.floor( (curr / total) * progBarView.dimens.width ); + progBarView.setText(self.config.progBarChar.repeat(prog)); + } + }; + + let cancel = false; + + const exportListProgress = (state, progNext) => { + switch(state.step) { + case 'preparing' : + case 'gathering' : + updateStatus(state.status); + break; + case 'file' : + updateStatus(state.status); + updateProgressBar(state.current, state.total); + self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo); + break; + default : + break; + } + + return progNext(cancel ? Errors.General('User canceled') : null); + }; + + const keyPressHandler = (ch, key) => { + if('escape' === key.name) { + cancel = true; + self.client.removeListener('key press', keyPressHandler); + } + }; + + async.waterfall( + [ + function buildList(callback) { + // this may take quite a while; temp disable of idle monitor + self.client.stopIdleMonitor(); + + const filterCriteria = Object.assign({}, self.config.filterCriteria); + if(!filterCriteria.areaTag) { + filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); + } + + const opts = { + templateEncoding : self.config.templateEncoding, + headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), + entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), + tsFormat : self.config.tsFormat, + descWidth : self.config.descWidth, + progress : exportListProgress, + }; + + exportFileList(filterCriteria, opts, (err, listBody) => { + return callback(err, listBody); + }); + }, + function persistList(listBody, callback) { + updateStatus('Persisting list'); + + const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); + const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); + + fse.mkdirs(sysTempDownloadDir, err => { + if(err) { + return callback(err); + } + + const outputFileName = paths.join( + sysTempDownloadDir, + `file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` + ); + + fs.writeFile(outputFileName, listBody, 'utf8', err => { + if(err) { + return callback(err); + } + + self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { + return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); + }); + }); + }); + }, + function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { + const newEntry = new FileEntry({ + areaTag : sysTempDownloadArea.areaTag, + fileName : paths.basename(outputFileName), + storageTag : sysTempDownloadArea.storageTags[0], + meta : { + upload_by_username : self.client.user.username, + upload_by_user_id : self.client.user.userId, + byte_size : fileSize, + session_temp_dl : 1, // download is valid until session is over + } + }); + + newEntry.desc = 'File List Export'; + + newEntry.persist(err => { + if(!err) { + // queue it! + const dlQueue = new DownloadQueue(self.client); + dlQueue.add(newEntry); + + // clean up after ourselves when the session ends + const thisClientId = self.client.session.id; + Events.once(Events.getSystemEvents().ClientDisconnected, evt => { + if(thisClientId === _.get(evt, 'client.session.id')) { + FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { + if(err) { + Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); + } else { + Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); + } + }); + } + }); + } + return callback(err); + }); + }, + function done(callback) { + // re-enable idle monitor + self.client.startIdleMonitor(); + + updateStatus('Exported list has been added to your download queue'); + return callback(null); + } + ], + err => { + self.client.removeListener('key press', keyPressHandler); + return cb(err); + } + ); + } + + getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { + fse.stat(filePath, (err, stats) => { + if(err) { + return cb(err); + } + + if(stats.size < this.config.compressThreshold) { + // small enough, keep orig + return cb(null, filePath, stats.size); + } + + const zipFilePath = `${filePath}.zip`; + + const zipFile = new yazl.ZipFile(); + zipFile.addFile(filePath, paths.basename(filePath)); + zipFile.end( () => { + const outZipFile = fs.createWriteStream(zipFilePath); + zipFile.outputStream.pipe(outZipFile); + zipFile.outputStream.on('finish', () => { + // delete the original + fse.unlink(filePath, err => { + if(err) { + return cb(err); + } + + // finally stat the new output + fse.stat(zipFilePath, (err, stats) => { + return cb(err, zipFilePath, stats ? stats.size : 0); + }); + }); + }); + }); + }); + } +}; \ No newline at end of file From 7bf49d973d1ff36a161b8a2cce59fd21e54d976a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 11 Mar 2018 21:23:35 -0600 Subject: [PATCH 081/569] Split up code a bit in prep for DESCRIPT.ION generation --- core/file_base_list_export.js | 516 +++++++++++----------------------- 1 file changed, 164 insertions(+), 352 deletions(-) diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index 9c63ab2d..2ae4699d 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -2,7 +2,6 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); const stringFormat = require('./string_format.js'); const FileEntry = require('./file_entry.js'); const FileArea = require('./file_base_area.js'); @@ -11,142 +10,54 @@ const { Errors } = require('./enig_error.js'); const { splitTextAtTerms, isAnsi, - renderSubstr } = require('./string_util.js'); const AnsiPrep = require('./ansi_prep.js'); -const Events = require('./events.js'); -const Log = require('./logger.js').log; -const DownloadQueue = require('./download_queue.js'); // deps const _ = require('lodash'); const async = require('async'); const fs = require('graceful-fs'); -const fse = require('fs-extra'); const paths = require('path'); const iconv = require('iconv-lite'); const moment = require('moment'); -const uuidv4 = require('uuid/v4'); -const yazl = require('yazl'); -/* - Module config block can contain the following: - templateEncoding - encoding of template files (utf8) - tsFormat - timestamp format (theme 'short') - descWidth - max desc width (45) - progBarChar - progress bar character (▒) - compressThreshold - threshold to kick in comrpession for lists (1.44 MiB) - templates - object containing: - header - filename of header template (misc/file_list_header.asc) - entry - filename of entry template (misc/file_list_entry.asc) +module.exports = function exportFileList(filterCriteria, options, cb) { + options.templateEncoding = options.templateEncoding || 'utf8'; + options.headerTemplate = options.headerTemplate || 'description_export_header_template.asc'; + options.entryTemplate = options.entryTemplate || 'descripion_export_entry_template.asc'; + options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; + options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec - Header template variables: - nowTs, boardName, totalFileCount, totalFileSize, - filterAreaTag, filterAreaName, filterAreaDesc, - filterTerms, filterHashTags + const state = { + total : 0, + current : 0, + step : 'preparing', + status : 'Preparing', + }; - Entry template variables: - fileId, areaName, areaDesc, userRating, fileName, - fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32, - fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags, - currentFile, progress, -*/ + const updateProgress = _.isFunction(options.progress) ? + progCb => { + return options.progress(state, progCb); + } : + progCb => { + return progCb(null); + } + ; -exports.moduleInfo = { - name : 'File Base List Export', - desc : 'Exports file base listings for download', - author : 'NuSkooler', -}; - -const FormIds = { - main : 0, -}; - -const MciViewIds = { - main : { - status : 1, - progressBar : 2, - - customRangeStart : 10, - } -}; - -const TEMPLATE_KEYS = [ // config.templates.* - 'header', 'entry', -]; - -exports.getModule = class FileBaseListExport extends MenuModule { - - constructor(options) { - super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - - this.config.templateEncoding = this.config.templateEncoding || 'utf8'; - this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); - this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ - this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); - this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) - } - - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } - - async.series( - [ - (callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), - (callback) => this.prepareList(callback), - ], - err => { + async.waterfall( + [ + function readTemplateFiles(callback) { + updateProgress(err => { if(err) { - if('NORESULTS' === err.reasonCode) { - return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults'); - } - - return this.prevMenu(); + return callback(err); } - return cb(err); - } - ); - }); - } - finishedLoading() { - this.prevMenu(); - } + const templateFiles = [ options.headerTemplate, options.entryTemplate ]; + async.map(templateFiles, (template, nextTemplate) => { + template = paths.isAbsolute(template) ? template : paths.join(Config.paths.misc, template); - prepareList(cb) { - const self = this; - - const statusView = self.viewControllers.main.getView(MciViewIds.main.status); - const updateStatus = (status) => { - if(statusView) { - statusView.setText(status); - } - }; - - const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); - const updateProgressBar = (curr, total) => { - if(progBarView) { - const prog = Math.floor( (curr / total) * progBarView.dimens.width ); - progBarView.setText(self.config.progBarChar.repeat(prog)); - } - }; - - async.waterfall( - [ - function readTemplateFiles(callback) { - updateStatus('Preparing'); - - async.map(TEMPLATE_KEYS, (templateKey, nextKey) => { - let templatePath = _.get(self.config, [ 'templates', templateKey ]); - templatePath = templatePath || `file_list_${templateKey}.asc`; - templatePath = paths.isAbsolute(templatePath) ? templatePath : paths.join(Config.paths.misc, templatePath); - - fs.readFile(templatePath, (err, data) => { - return nextKey(err, data); + fs.readFile(template, (err, data) => { + return nextTemplate(err, data); }); }, (err, templates) => { if(err) { @@ -154,7 +65,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { } // decode + ensure DOS style CRLF - templates = templates.map(tmp => iconv.decode(tmp, self.config.templateEncoding).replace(/\r?\n/g, '\r\n') ); + templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') ); // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements let descIndent = 0; @@ -169,15 +80,16 @@ exports.getModule = class FileBaseListExport extends MenuModule { return callback(null, templates[0], templates[1], descIndent); }); - }, - function findFiles(headerTemplate, entryTemplate, descIndent, callback) { - const filterCriteria = Object.assign({}, self.config.filterCriteria); - if(!filterCriteria.areaTag) { - filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); + }); + }, + function findFiles(headerTemplate, entryTemplate, descIndent, callback) { + state.step = 'gathering'; + state.status = 'Gathering files for supplied criteria'; + updateProgress(err => { + if(err) { + return callback(err); } - updateStatus('Gathering files for supplied criteria'); - FileEntry.findFiles(filterCriteria, (err, fileIds) => { if(0 === fileIds.length) { return callback(Errors.General('No results for criteria', 'NORESULTS')); @@ -185,229 +97,129 @@ exports.getModule = class FileBaseListExport extends MenuModule { return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); }); - }, - function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { - const formatObj = { - totalFileCount : fileIds.length, - }; - - let current = 0; - let listBody = ''; - const totals = { fileCount : fileIds.length, bytes : 0 }; - - // this may take quite a while; temp disable of idle monitor - self.client.stopIdleMonitor(); - - async.eachSeries(fileIds, (fileId, nextFileId) => { - const fileInfo = new FileEntry(); - current += 1; - - fileInfo.load(fileId, err => { - if(err) { - return nextFileId(null); // failed, but try the next - } - - updateStatus(`Processing ${fileInfo.fileName}`); - - totals.bytes += fileInfo.meta.byte_size; - - updateProgressBar(current, fileIds.length); - - const appendFileInfo = () => { - listBody += stringFormat(entryTemplate, formatObj); - - self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, formatObj); - - return nextFileId(null); - }; - - const area = FileArea.getFileAreaByTag(fileInfo.areaTag); - - formatObj.fileId = fileId; - formatObj.areaName = _.get(area, 'name') || 'N/A'; - formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; - formatObj.userRating = fileInfo.userRating || 0; - formatObj.fileName = fileInfo.fileName; - formatObj.fileSize = fileInfo.meta.byte_size; - formatObj.fileDesc = fileInfo.desc || ''; - formatObj.fileDescShort = formatObj.fileDesc.slice(0, self.config.descWidth); - formatObj.fileSha256 = fileInfo.fileSha256; - formatObj.fileCrc32 = fileInfo.meta.file_crc32; - formatObj.fileMd5 = fileInfo.meta.file_md5; - formatObj.fileSha1 = fileInfo.meta.file_sha1; - formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; - formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(self.config.tsFormat); - formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; - formatObj.currentFile = current; - formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); - - if(isAnsi(fileInfo.desc)) { - AnsiPrep( - fileInfo.desc, - { - cols : Math.min(self.config.descWidth, 79 - descIndent), - forceLineTerm : true, // ensure each line is term'd - asciiMode : true, // export to ASCII - fillLines : false, // don't fill up to |cols| - indent : descIndent, - }, - (err, desc) => { - if(desc) { - formatObj.fileDesc = desc; - } - return appendFileInfo(); - } - ); - } else { - const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; - formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; - return appendFileInfo(); - } - }); - }, err => { - // re-enable idle monitor - self.client.startIdleMonitor(); - - return callback(err, listBody, headerTemplate, totals); - }); - }, - function buildHeader(listBody, headerTemplate, totals, callback) { - // header is built last such that we can have totals/etc. - - let filterAreaName; - let filterAreaDesc; - if(self.config.filterCriteria.areaTag) { - const area = FileArea.getFileAreaByTag(self.config.filterCriteria.areaTag); - filterAreaName = _.get(area, 'name') || 'N/A'; - filterAreaDesc = _.get(area, 'desc') || 'N/A'; - } else { - filterAreaName = '-ALL-'; - filterAreaDesc = 'All areas'; - } - - const headerFormatObj = { - nowTs : moment().format(self.config.tsFormat), - boardName : Config.general.boardName, - totalFileCount : totals.fileCount, - totalFileSize : totals.bytes, - filterAreaTag : self.config.filterCriteria.areaTag || '-ALL-', - filterAreaName : filterAreaName, - filterAreaDesc : filterAreaDesc, - filterTerms : self.config.filterCriteria.terms || '(none)', - filterHashTags : self.config.filterCriteria.tags || '(none)', - }; - - listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; - return callback(null, listBody); - }, - function persistList(listBody, callback) { - - updateStatus('Persisting list'); - - const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); - const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); - - fse.mkdirs(sysTempDownloadDir, err => { - if(err) { - return callback(err); - } - - const outputFileName = paths.join( - sysTempDownloadDir, - `file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` - ); - - fs.writeFile(outputFileName, listBody, 'utf8', err => { - if(err) { - return callback(err); - } - - self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { - return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); - }); - }); - }); - }, - function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { - const newEntry = new FileEntry({ - areaTag : sysTempDownloadArea.areaTag, - fileName : paths.basename(outputFileName), - storageTag : sysTempDownloadArea.storageTags[0], - meta : { - upload_by_username : self.client.user.username, - upload_by_user_id : self.client.user.userId, - byte_size : fileSize, - session_temp_dl : 1, // download is valid until session is over - } - }); - - newEntry.desc = 'File List Export'; - - newEntry.persist(err => { - if(!err) { - // queue it! - const dlQueue = new DownloadQueue(self.client); - dlQueue.add(newEntry); - - // clean up after ourselves when the session ends - const thisClientId = self.client.session.id; - Events.once(Events.getSystemEvents().ClientDisconnected, evt => { - if(thisClientId === _.get(evt, 'client.session.id')) { - FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { - if(err) { - Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); - } else { - Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); - } - }); - } - }); - } - return callback(err); - }); - }, - function done(callback) { - updateStatus('Exported list has been added to your download queue'); - return callback(null); - } - ], err => { - return cb(err); - } - ); - } - - getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { - fse.stat(filePath, (err, stats) => { - if(err) { - return cb(err); - } - - if(stats.size < this.config.compressThreshold) { - // small enough, keep orig - return cb(null, filePath, stats.size); - } - - const zipFilePath = `${filePath}.zip`; - - const zipFile = new yazl.ZipFile(); - zipFile.addFile(filePath, paths.basename(filePath)); - zipFile.end( () => { - const outZipFile = fs.createWriteStream(zipFilePath); - zipFile.outputStream.pipe(outZipFile); - zipFile.outputStream.on('finish', () => { - // delete the original - fse.unlink(filePath, err => { - if(err) { - return cb(err); - } - - // finally stat the new output - fse.stat(zipFilePath, (err, stats) => { - return cb(err, zipFilePath, stats ? stats.size : 0); - }); - }); }); - }); - }); - } -}; \ No newline at end of file + }, + function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { + const formatObj = { + totalFileCount : fileIds.length, + }; + + let current = 0; + let listBody = ''; + const totals = { fileCount : fileIds.length, bytes : 0 }; + state.total = fileIds.length; + + state.step = 'file'; + + async.eachSeries(fileIds, (fileId, nextFileId) => { + const fileInfo = new FileEntry(); + current += 1; + + fileInfo.load(fileId, err => { + if(err) { + return nextFileId(null); // failed, but try the next + } + + totals.bytes += fileInfo.meta.byte_size; + + const appendFileInfo = () => { + listBody += stringFormat(entryTemplate, formatObj); + + state.current = current; + state.status = `Processing ${fileInfo.fileName}`; + state.fileInfo = formatObj; + + updateProgress(err => { + return nextFileId(err); + }); + }; + + const area = FileArea.getFileAreaByTag(fileInfo.areaTag); + + formatObj.fileId = fileId; + formatObj.areaName = _.get(area, 'name') || 'N/A'; + formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; + formatObj.userRating = fileInfo.userRating || 0; + formatObj.fileName = fileInfo.fileName; + formatObj.fileSize = fileInfo.meta.byte_size; + formatObj.fileDesc = fileInfo.desc || ''; + formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); + formatObj.fileSha256 = fileInfo.fileSha256; + formatObj.fileCrc32 = fileInfo.meta.file_crc32; + formatObj.fileMd5 = fileInfo.meta.file_md5; + formatObj.fileSha1 = fileInfo.meta.file_sha1; + formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; + formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); + formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; + formatObj.currentFile = current; + formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); + + if(isAnsi(fileInfo.desc)) { + AnsiPrep( + fileInfo.desc, + { + cols : Math.min(options.descWidth, 79 - descIndent), + forceLineTerm : true, // ensure each line is term'd + asciiMode : true, // export to ASCII + fillLines : false, // don't fill up to |cols| + indent : descIndent, + }, + (err, desc) => { + if(desc) { + formatObj.fileDesc = desc; + } + return appendFileInfo(); + } + ); + } else { + const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; + formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; + return appendFileInfo(); + } + }); + }, err => { + return callback(err, listBody, headerTemplate, totals); + }); + }, + function buildHeader(listBody, headerTemplate, totals, callback) { + // header is built last such that we can have totals/etc. + + let filterAreaName; + let filterAreaDesc; + if(filterCriteria.areaTag) { + const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); + filterAreaName = _.get(area, 'name') || 'N/A'; + filterAreaDesc = _.get(area, 'desc') || 'N/A'; + } else { + filterAreaName = '-ALL-'; + filterAreaDesc = 'All areas'; + } + + const headerFormatObj = { + nowTs : moment().format(options.tsFormat), + boardName : Config.general.boardName, + totalFileCount : totals.fileCount, + totalFileSize : totals.bytes, + filterAreaTag : filterCriteria.areaTag || '-ALL-', + filterAreaName : filterAreaName, + filterAreaDesc : filterAreaDesc, + filterTerms : filterCriteria.terms || '(none)', + filterHashTags : filterCriteria.tags || '(none)', + }; + + listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; + return callback(null, listBody); + }, + function done(listBody, callback) { + delete state.fileInfo; + state.step = 'finished'; + state.status = 'Finished processing'; + updateProgress( () => { + return callback(null, listBody); + }); + } + ], (err, listBody) => { + return cb(err, listBody); + } + ); +}; From 0de98a673f41a0885c4f368de047e4a5cd8b4d5c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Mar 2018 22:18:09 -0600 Subject: [PATCH 082/569] Add DESCRIPT.ION export ability * 4DOS style DESCRIPT.ION generated in storage areas @ weekly schedule by default * Format can be controlled via templates; schedule can be changed or disabled, etc. --- core/config.js | 5 + core/file_base_list_export.js | 106 +++++++++++++++++--- core/file_base_user_list_export.js | 18 ++-- misc/descript_ion_export_entry_template.asc | 1 + 4 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 misc/descript_ion_export_entry_template.asc diff --git a/core/config.js b/core/config.js index 6fcadd5b..cd87c771 100644 --- a/core/config.js +++ b/core/config.js @@ -738,6 +738,11 @@ function getDefaultConfig() { schedule : 'every 24 hours', action : '@method:core/web_password_reset.js:performMaintenanceTask', args : [ '24 hours' ] // items older than this will be removed + }, + + updateDescriptIonFiles : { + schedule : 'every 168 hours', // once a week + action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', } } }, diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index 2ae4699d..442946e3 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -12,6 +12,7 @@ const { isAnsi, } = require('./string_util.js'); const AnsiPrep = require('./ansi_prep.js'); +const Log = require('./logger.js').log; // deps const _ = require('lodash'); @@ -21,12 +22,19 @@ const paths = require('path'); const iconv = require('iconv-lite'); const moment = require('moment'); -module.exports = function exportFileList(filterCriteria, options, cb) { +exports.exportFileList = exportFileList; +exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; + +function exportFileList(filterCriteria, options, cb) { options.templateEncoding = options.templateEncoding || 'utf8'; - options.headerTemplate = options.headerTemplate || 'description_export_header_template.asc'; - options.entryTemplate = options.entryTemplate || 'descripion_export_entry_template.asc'; + options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec + options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? + + if(true === options.escapeDesc) { + options.escapeDesc = '\\n'; + } const state = { total : 0, @@ -52,16 +60,22 @@ module.exports = function exportFileList(filterCriteria, options, cb) { return callback(err); } - const templateFiles = [ options.headerTemplate, options.entryTemplate ]; + const templateFiles = [ + { name : options.headerTemplate, req : false }, + { name : options.entryTemplate, req : true } + ]; async.map(templateFiles, (template, nextTemplate) => { - template = paths.isAbsolute(template) ? template : paths.join(Config.paths.misc, template); + if(!template.name && !template.req) { + return nextTemplate(null, Buffer.from([])); + } - fs.readFile(template, (err, data) => { + template.name = paths.isAbsolute(template.name) ? template.name : paths.join(Config.paths.misc, template.name); + fs.readFile(template.name, (err, data) => { return nextTemplate(err, data); }); }, (err, templates) => { if(err) { - return Errors.General(err.message); + return callback(Errors.General(err.message)); } // decode + ensure DOS style CRLF @@ -69,14 +83,16 @@ module.exports = function exportFileList(filterCriteria, options, cb) { // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements let descIndent = 0; - splitTextAtTerms(templates[1]).some(line => { - const pos = line.indexOf('{fileDesc}'); - if(pos > -1) { - descIndent = pos; - return true; // found it! - } - return false; // keep looking - }); + if(!options.escapeDesc) { + splitTextAtTerms(templates[1]).some(line => { + const pos = line.indexOf('{fileDesc}'); + if(pos > -1) { + descIndent = pos; + return true; // found it! + } + return false; // keep looking + }); + } return callback(null, templates[0], templates[1], descIndent); }); @@ -123,6 +139,14 @@ module.exports = function exportFileList(filterCriteria, options, cb) { totals.bytes += fileInfo.meta.byte_size; const appendFileInfo = () => { + if(options.escapeDesc) { + formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc); + } + + if(options.maxDescLen) { + formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen); + } + listBody += stringFormat(entryTemplate, formatObj); state.current = current; @@ -222,4 +246,54 @@ module.exports = function exportFileList(filterCriteria, options, cb) { return cb(err, listBody); } ); -}; +} + +function updateFileBaseDescFilesScheduledEvent(args, cb) { + // + // For each area, loop over storage locations and build + // DESCRIPT.ION file to store in the same directory. + // + // Standard-ish 4DOS spec is as such: + // * Entry: [0x04]\r\n + // * Multi line descriptions are stored with *escaped* \r\n pairs + // * Default template uses 0x2c for as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec + // + const entryTemplate = args[0]; + const headerTemplate = args[1]; + + const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true }); + async.each(areas, (area, nextArea) => { + const storageLocations = FileArea.getAreaStorageLocations(area); + + async.each(storageLocations, (storageLoc, nextStorageLoc) => { + const filterCriteria = { + areaTag : area.areaTag, + storageTag : storageLoc.storageTag, + }; + + const exportOpts = { + headerTemplate : headerTemplate, + entryTemplate : entryTemplate, + escapeDesc : true, // escape CRLF's + maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes" + }; + + exportFileList(filterCriteria, exportOpts, (err, listBody) => { + + const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION'); + fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => { + if(err) { + Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION'); + } else { + Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION'); + } + return nextStorageLoc(null); + }); + }); + }, () => { + return nextArea(null); + }); + }, () => { + return cb(null); + }); +} diff --git a/core/file_base_user_list_export.js b/core/file_base_user_list_export.js index 4a2d0c4b..686833e5 100644 --- a/core/file_base_user_list_export.js +++ b/core/file_base_user_list_export.js @@ -2,15 +2,15 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const FileEntry = require('./file_entry.js'); -const FileArea = require('./file_base_area.js'); -const { renderSubstr } = require('./string_util.js'); -const { Errors } = require('./enig_error.js'); -const Events = require('./events.js'); -const Log = require('./logger.js').log; -const DownloadQueue = require('./download_queue.js'); -const exportFileList = require('./file_base_list_export.js'); +const { MenuModule } = require('./menu_module.js'); +const FileEntry = require('./file_entry.js'); +const FileArea = require('./file_base_area.js'); +const { renderSubstr } = require('./string_util.js'); +const { Errors } = require('./enig_error.js'); +const Events = require('./events.js'); +const Log = require('./logger.js').log; +const DownloadQueue = require('./download_queue.js'); +const { exportFileList } = require('./file_base_list_export.js'); // deps const _ = require('lodash'); diff --git a/misc/descript_ion_export_entry_template.asc b/misc/descript_ion_export_entry_template.asc new file mode 100644 index 00000000..e7618280 --- /dev/null +++ b/misc/descript_ion_export_entry_template.asc @@ -0,0 +1 @@ +"{fileName}" {fileDesc} From 6311198f4d54938a0493dedff33838c863ae41ae Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Mar 2018 22:55:10 -0600 Subject: [PATCH 083/569] Schedule that actually is valid... --- core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index cd87c771..d153d4cb 100644 --- a/core/config.js +++ b/core/config.js @@ -741,7 +741,7 @@ function getDefaultConfig() { }, updateDescriptIonFiles : { - schedule : 'every 168 hours', // once a week + schedule : 'on the last day of the week', action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', } } From 91bbc2e5fea4426b39724d6155aa057d3552413e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Mar 2018 18:59:47 -0600 Subject: [PATCH 084/569] DESCRIPT.ION generation disabled by default --- core/config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/config.js b/core/config.js index d153d4cb..7e5d92a1 100644 --- a/core/config.js +++ b/core/config.js @@ -740,10 +740,16 @@ function getDefaultConfig() { args : [ '24 hours' ] // items older than this will be removed }, + // + // Enable the following entry in your config.hjson to periodically create/update + // DESCRIPT.ION files for your file base + // + /* updateDescriptIonFiles : { schedule : 'on the last day of the week', action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', } + */ } }, From 534b51933a369e31ea769c61e93dc19972cd673c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Mar 2018 19:00:34 -0600 Subject: [PATCH 085/569] Register key press handler - oops! --- core/file_base_user_list_export.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/file_base_user_list_export.js b/core/file_base_user_list_export.js index 686833e5..2dba52ad 100644 --- a/core/file_base_user_list_export.js +++ b/core/file_base_user_list_export.js @@ -157,6 +157,8 @@ exports.getModule = class FileBaseListExport extends MenuModule { // this may take quite a while; temp disable of idle monitor self.client.stopIdleMonitor(); + self.client.on('key press', keyPressHandler); + const filterCriteria = Object.assign({}, self.config.filterCriteria); if(!filterCriteria.areaTag) { filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); From 66423068b1b9c42244b4f41fc9f56bc18cc1b68a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Mar 2018 19:10:20 -0600 Subject: [PATCH 086/569] Crash on delete in BBS List with zero entries #156 --- core/bbs_list.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/bbs_list.js b/core/bbs_list.js index 74c792c6..abff376f 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -99,6 +99,10 @@ exports.getModule = class BBSListModule extends MenuModule { self.displayAddScreen(cb); }, deleteBBS : function(formData, extraArgs, cb) { + if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) { + return cb(null); + } + const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { @@ -323,6 +327,7 @@ exports.getModule = class BBSListModule extends MenuModule { entriesView.setFocusItemIndex(self.selectedBBS); self.drawSelectedEntry(self.entries[self.selectedBBS]); } else if (self.entries.length > 0) { + self.selectedBBS = 0; entriesView.setFocusItemIndex(0); self.drawSelectedEntry(self.entries[0]); } From 4acbae86e36b4d44091734f7ab87326cab30375e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Mar 2018 19:22:26 -0600 Subject: [PATCH 087/569] Crash on configuration when theme no longer exists #157 --- core/user_config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/user_config.js b/core/user_config.js index 70e1f068..f60d2155 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -174,9 +174,9 @@ exports.getModule = class UserConfigModule extends MenuModule { }; }), 'name'); - currentThemeIdIndex = _.findIndex(self.availThemeInfo, function cmp(ti) { + currentThemeIdIndex = Math.max(0, _.findIndex(self.availThemeInfo, function cmp(ti) { return ti.themeId === self.client.user.properties.theme_id; - }); + })); callback(null); }, From 290b391bf84921dc4079e2c6be42bf74d1f92040 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Mar 2018 20:26:40 -0600 Subject: [PATCH 088/569] Fix TypeError: cb is not a function during ping --- core/servers/login/websocket.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index 378af7ef..cc0270b1 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -77,7 +77,7 @@ function WebSocketClient(ws, req, serverType) { // // Montior connection status with ping/pong // - ws.on('pong', () => { + ws.on('pong', () => { Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); ws.isConnectionAlive = true; }); @@ -187,13 +187,13 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { } ws.isConnectionAlive = false; // pong will reset this - + Log.trace('Ping to remote WebSocket client'); - return ws.ping('', false, true); + return ws.ping('', false); // false=don't mask }); } }); - }, 30000); + }, 30000); return true; } From 299d524b794bb02ec400a0eb757a2fb691d16f15 Mon Sep 17 00:00:00 2001 From: Jason Windisch Date: Wed, 14 Mar 2018 22:51:08 -0400 Subject: [PATCH 089/569] Added widths to keep text within theme borders --- art/themes/luciano_blocktronics/theme.hjson | 52 ++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 3b6e91aa..e6662c2c 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -88,11 +88,11 @@ fullLoginSequenceOnelinerz: { config: { - listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.58}" + listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.59}" } 0: { mci: { - VM1: { height: 10 } + VM1: { height: 10, width: 20 } TM2: { focusTextStyle: first lower } @@ -111,14 +111,14 @@ mainMenuUserStats: { mci: { - UN1: { width: 17 } - UR2: { width: 17 } - LO3: { width: 17 } - UF4: { width: 17 } - UG5: { width: 17 } - UT6: { width: 17 } - UC7: { width: 17 } - ST8: { width: 17 } + UN1: { width: 15 } + UR2: { width: 15 } + LO3: { width: 15 } + UF4: { width: 15 } + UG5: { width: 15 } + UT6: { width: 15 } + UC7: { width: 15 } + ST8: { width: 15 } } } @@ -143,7 +143,7 @@ dateTimeFormat: MMM Do h:mma } mci: { - VM1: { height: 10 } + VM1: { height: 10, width: 20 } } } @@ -154,7 +154,7 @@ dateTimeFormat: MMM Do h:mma } mci: { - VM1: { height: 15 } + VM1: { height: 15, width: 50} } } @@ -163,7 +163,7 @@ listFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}" } mci: { - VM1: { height: 10 } + VM1: { height: 10, width: 20 } } } @@ -185,11 +185,11 @@ mainMenuOnelinerz: { // :TODO: Need way to just duplicate entry here & in menu.hjson, e.g. use: someName + must supply next/etc. in menu config: { - listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.58}" + listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.59}" } 0: { mci: { - VM1: { height: 10 } + VM1: { height: 10, width: 10 } TM2: { focusTextStyle: first lower } @@ -280,7 +280,7 @@ } 0: { mci: { - VM1: { height: 14 } + VM1: { height: 14, width: 60} TM2: { focusTextStyle: upper items: [ "yes", "no" ] @@ -415,7 +415,7 @@ dateTimeFormat: MMM Do h:mma } mci: { - VM1: { height: 10 } + VM1: { height: 10, width: 20 } } } @@ -424,7 +424,7 @@ listFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}" } mci: { - VM1: { height: 10 } + VM1: { height: 10, width: 20 } } } @@ -434,14 +434,14 @@ fullLoginSequenceUserStats: { mci: { - UN1: { width: 17 } - UR2: { width: 17 } - LO3: { width: 17 } - UF4: { width: 17 } - UG5: { width: 17 } - UT6: { width: 17 } - UC7: { width: 17 } - ST8: { width: 17 } + UN1: { width: 15 } + UR2: { width: 15 } + LO3: { width: 15 } + UF4: { width: 15 } + UG5: { width: 15 } + UT6: { width: 15 } + UC7: { width: 15 } + ST8: { width: 15 } } } From 18284d80386f838494278e3e4d3c0503594a2719 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Mar 2018 21:33:58 -0600 Subject: [PATCH 090/569] Merge minor theme stuff --- art/themes/luciano_blocktronics/theme.hjson | 2 +- core/text_view.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index e6662c2c..9a8d3b6c 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -280,7 +280,7 @@ } 0: { mci: { - VM1: { height: 14, width: 60} + VM1: { height: 14, width: 70 } TM2: { focusTextStyle: upper items: [ "yes", "no" ] diff --git a/core/text_view.js b/core/text_view.js index 8de04484..9497a16b 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -129,7 +129,8 @@ function TextView(options) { renderedFillChar, //this.fillChar, this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), - this.getStyleSGR(1) || this.getSGR() + this.getStyleSGR(1) || this.getSGR(), + true // use render len ), false // no converting CRLF needed ); From f7639c90e51c98ee1122dddc3c2e34e12ae2afd8 Mon Sep 17 00:00:00 2001 From: Jason Windisch Date: Thu, 15 Mar 2018 00:22:33 -0400 Subject: [PATCH 091/569] fixed mispelling of "reset" --- .../luciano_blocktronics/FORGOTPWSENT.ANS | Bin 311 -> 313 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/FORGOTPWSENT.ANS b/art/themes/luciano_blocktronics/FORGOTPWSENT.ANS index 99219c10e33321748bba3da1002be96392e7857e..c0ebbfd96b08ca66364d88a07c32798d38e8ad89 100644 GIT binary patch delta 42 ycmdnaw3BIq9AoN4x#?VbTvEY~q0X)b1``i@@F*xK7#SFv8yFdx?Vc>es0sl7hzlG5 delta 37 tcmdnVw4G^!+(h~5oLo}Dj-k%31_l!kdT}Wj85o)y7#W%EoGikq3INf73P=C| From 66f444d4fb4b9d9b462204f49b033789414075d6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Mar 2018 13:48:11 -0600 Subject: [PATCH 092/569] Slight findFiles optimization --- core/file_entry.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 2ec03c17..169bbb74 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -483,7 +483,7 @@ module.exports = class FileEntry { sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; } else { sql = - `SELECT DISTINCT f.file_id, f.${filter.sort} + `SELECT DISTINCT f.file_id FROM file f`; sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; @@ -579,13 +579,14 @@ module.exports = class FileEntry { sql += ';'; - const matchingFileIds = []; - fileDb.each(sql, (err, fileId) => { - if(fileId) { - matchingFileIds.push(fileId.file_id); + fileDb.all(sql, (err, rows) => { + if(err) { + return cb(err); } - }, err => { - return cb(err, matchingFileIds); + if(!rows || 0 === rows.length) { + return cb(null, []); // no matches + } + return cb(null, rows.map(r => r.file_id)); }); } From 7bd980c8864e0db5acc12b3e6ba062cfbe6ed27c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 22 Mar 2018 20:48:31 -0600 Subject: [PATCH 093/569] Crash with color differences in same variable #164 --- core/msg_area_list.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/msg_area_list.js b/core/msg_area_list.js index fcc44b23..8238e4b6 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -8,6 +8,7 @@ const messageArea = require('./message_area.js'); const displayThemeArt = require('./theme.js').displayThemeArt; const resetScreen = require('./ansi_term.js').resetScreen; const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; // deps const async = require('async'); @@ -139,6 +140,9 @@ exports.getModule = class MessageAreaListModule extends MenuModule { const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const areaListView = vc.getView(MciViewIds.AreaList); + if(!areaListView) { + return callback(Errors.MissingMci('A MenuView compatible MCI code is required')); + } let i = 1; areaListView.setItems(_.map(self.messageAreas, v => { return stringFormat(listFormat, { From f08d6efb97f8dcf8a6714d458cb348ac6604dd54 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 28 Mar 2018 19:16:10 -0600 Subject: [PATCH 094/569] WIP work on door fixes, updates, etc. --- core/abracadabra.js | 66 +++++++++++---- core/door.js | 120 +++++++++++++++++++++++++- core/dropfile.js | 201 +++++++++++++++++++++++--------------------- core/enig_error.js | 1 + package.json | 3 +- 5 files changed, 273 insertions(+), 118 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 0ac17887..603eab84 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -2,16 +2,15 @@ 'use strict'; const MenuModule = require('./menu_module.js').MenuModule; -const DropFile = require('./dropfile.js').DropFile; -const door = require('./door.js'); +const DropFile = require('./dropfile.js'); +const Door = require('./door.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); const async = require('async'); const assert = require('assert'); -const paths = require('path'); const _ = require('lodash'); -const mkdirs = require('fs-extra').mkdirs; +const getSockHandle = require('getsockethandleaddress'); // black magic // :TODO: This should really be a system module... needs a little work to allow for such @@ -122,19 +121,18 @@ exports.getModule = class AbracadabraModule extends MenuModule { callback(null); } }, + function prepareDoor(callback) { + self.doorInstance = new Door(self.client); + return self.doorInstance.prepare(self.config.io || 'stdio', callback); + }, function generateDropfile(callback) { - self.dropFile = new DropFile(self.client, self.config.dropFileType); - var fullPath = self.dropFile.fullPath; + const dropFileOpts = { + fileType : self.config.dropFileType, + socketDescriptor : self.getSocketFd(), + }; - mkdirs(paths.dirname(fullPath), function dirCreated(err) { - if(err) { - callback(err); - } else { - self.dropFile.createFile(function created(err) { - callback(err); - }); - } - }); + self.dropFile = new DropFile(self.client, dropFileOpts); + return self.dropFile.createFile(callback); } ], function complete(err) { @@ -150,7 +148,38 @@ exports.getModule = class AbracadabraModule extends MenuModule { } runDoor() { + this.client.term.write(ansi.resetScreen()); + const exeInfo = { + cmd : this.config.cmd, + args : this.config.args, + io : this.config.io || 'stdio', + encoding : this.config.encoding || this.client.term.outputEncoding, + dropFile : this.dropFile.fileName, + dropFilePath : this.dropFile.fullPath, + node : this.client.node, + }; + + if('socketfd' === this.config.io) { + exeInfo.srvSocketFd = this.getSocketFd(); + } + + this.doorInstance.run(exeInfo, () => { + // + // Try to clean up various settings such as scroll regions that may + // have been set within the door + // + this.client.term.rawWrite( + ansi.normal() + + ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + + ansi.setScrollRegion() + + ansi.goto(this.client.term.termHeight, 0) + + '\r\n\r\n' + ); + + this.prevMenu(); + }); + /* const exeInfo = { cmd : this.config.cmd, args : this.config.args, @@ -178,10 +207,11 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.prevMenu(); }); - + this.client.term.write(ansi.resetScreen()); doorInstance.run(); + */ } leave() { @@ -194,4 +224,8 @@ exports.getModule = class AbracadabraModule extends MenuModule { finishedLoading() { this.runDoor(); } + + getSocketFd() { + return getSockHandle.getAddress(this.client.output._handle); // seriously... black magic :( + } }; diff --git a/core/door.js b/core/door.js index 58c8effa..ecf858f0 100644 --- a/core/door.js +++ b/core/door.js @@ -1,17 +1,128 @@ /* jslint node: true */ 'use strict'; - const stringFormat = require('./string_format.js'); +const { Errors } = require('./enig_error.js'); -const events = require('events'); -const _ = require('lodash'); const pty = require('node-pty'); const decode = require('iconv-lite').decode; const createServer = require('net').createServer; -exports.Door = Door; +module.exports = class Door { + constructor(client) { + this.client = client; + this.restored = false; + } + prepare(ioType, cb) { + this.io = ioType; + + // we currently only have to do any real setup for 'socket' + if('socket' !== ioType) { + return cb(null); + } + + this.sockServer = createServer(conn => { + this.sockServer.getConnections( (err, count) => { + + // We expect only one connection from our DOOR/emulator/etc. + if(!this && count <= 1) { + this.client.term.output.pipe(conn); + + conn.on('data', this.doorDataHandler.bind(this)); + + conn.once('end', () => { + return this.restoreIo(conn); + }); + + conn.once('error', err => { + this.client.log.info( { error : err.message }, 'Door socket server connection'); + return this.restoreIo(conn); + }); + } + }); + }); + + this.sockServer.listen(0, () => { + return cb(null); + }); + } + + run(exeInfo, cb) { + this.encoding = (exeInfo.encoding || 'cp437').toLowerCase(); + + if('socket' === this.io && !this.sockServer) { + return cb(Errors.UnexpectedState('Socket server is not running')); + } + + const formatObj = { + dropFile : exeInfo.dropFile, + dropFilePath : exeInfo.dropFilePath, + node : exeInfo.node.toString(), + srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', + userId : this.client.user.userId.toString(), + srvSocketFd : exeInfo.srvSocketFd ? exeInfo.srvSocketFd.toString() : '-1', + }; + + const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); + + const door = pty.spawn(exeInfo.cmd, args, { + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, + // :TODO: cwd + env : exeInfo.env, + encoding : null, // we want to handle all encoding ourself + }); + + if('stdio' === this.io) { + this.client.log.debug('Using stdio for door I/O'); + + this.client.term.output.pipe(door); + + door.on('data', this.doorDataHandler.bind(this)); + + door.once('close', () => { + return this.restoreIo(door); + }); + } else if('socket' === this.io) { + this.client.log.debug( + { srvPort : this.sockServer.address().port, srvSocketFd : this.sockServerSocket }, + 'Using temporary socket server for door I/O' + ); + } + + door.once('exit', exitCode => { + this.client.log.info( { exitCode : exitCode }, 'Door exited'); + + if(this.sockServer) { + this.sockServer.close(); + } + + // we may not get a close + if('stdio' === this.io) { + this.restoreIo(door); + } + + door.removeAllListeners(); + + return cb(null); + }); + } + + doorDataHandler(data) { + this.client.term.write(decode(data, this.encoding)); + } + + restoreIo(piped) { + if(!this.restored && this.client.term.output) { + this.client.term.output.unpipe(piped); + this.client.term.output.resume(); + this.restored = true; + } + } +}; + +/* function Door(client, exeInfo) { events.EventEmitter.call(this); @@ -147,3 +258,4 @@ Door.prototype.run = function() { }); }); }; +*/ \ No newline at end of file diff --git a/core/dropfile.js b/core/dropfile.js index 20c027e3..9aea759a 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -1,16 +1,15 @@ /* jslint node: true */ 'use strict'; -var Config = require('./config.js').config; +const Config = require('./config.js').config; const StatLog = require('./stat_log.js'); -var fs = require('graceful-fs'); -var paths = require('path'); -var _ = require('lodash'); -var moment = require('moment'); -var iconv = require('iconv-lite'); - -exports.DropFile = DropFile; +const fs = require('graceful-fs'); +const paths = require('path'); +const _ = require('lodash'); +const moment = require('moment'); +const iconv = require('iconv-lite'); +const { mkdirs } = require('fs-extra'); // // Resources @@ -20,53 +19,56 @@ exports.DropFile = DropFile; // * http://thebbs.org/bbsfaq/ch06.02.htm // http://lord.lordlegacy.com/dosemu/ +module.exports = class DropFile { + constructor(client, { fileType = 'DORINFO', baseDir = Config.paths.dropFiles, socketDescriptor = null } = {} ) { + this.client = client; + this.fileType = fileType.toUpperCase(); + this.baseDir = baseDir; + this.socketDescriptor = socketDescriptor; + } -function DropFile(client, fileType) { + get fullPath() { + return paths.join(this.baseDir, ('node' + this.client.node), this.fileName); + } - var self = this; - this.client = client; - this.fileType = (fileType || 'DORINFO').toUpperCase(); + get fileName() { + return { + DOOR : 'DOOR.SYS', // GAP BBS, many others + DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... + CALLINFO : 'CALLINFO.BBS', // Citadel? + DORINFO : this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... + CHAIN : 'CHAIN.TXT', // WWIV + CURRUSER : 'CURRUSER.BBS', // RyBBS + SFDOORS : 'SFDOORS.DAT', // Spitfire + PCBOARD : 'PCBOARD.SYS', // PCBoard + TRIBBS : 'TRIBBS.SYS', // TriBBS + USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ + JUMPER : 'JUMPER.DAT', // 2AM BBS + SXDOOR : 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE + INFO : 'INFO.BBS', // Phoenix BBS + }[this.fileType]; + } - Object.defineProperty(this, 'fullPath', { - get : function() { - return paths.join(Config.paths.dropFiles, ('node' + self.client.node), self.fileName); - } - }); + isSupported() { + return this.getHandler() ? true : false; + } - Object.defineProperty(this, 'fileName', { - get : function() { - return { - DOOR : 'DOOR.SYS', // GAP BBS, many others - DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... - CALLINFO : 'CALLINFO.BBS', // Citadel? - DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... - CHAIN : 'CHAIN.TXT', // WWIV - CURRUSER : 'CURRUSER.BBS', // RyBBS - SFDOORS : 'SFDOORS.DAT', // Spitfire - PCBOARD : 'PCBOARD.SYS', // PCBoard - TRIBBS : 'TRIBBS.SYS', // TriBBS - USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ - JUMPER : 'JUMPER.DAT', // 2AM BBS - SXDOOR : // System/X, dESiRE - 'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'), - INFO : 'INFO.BBS', // Phoenix BBS - }[self.fileType]; - } - }); + getHandler() { + return { + DOOR : this.getDoorSysBuffer, + DOOR32 : this.getDoor32Buffer, + DORINFO : this.getDoorInfoDefBuffer, + }[this.fileType]; + } - Object.defineProperty(this, 'dropFileContents', { - get : function() { - return { - DOOR : self.getDoorSysBuffer(), - DOOR32 : self.getDoor32Buffer(), - DORINFO : self.getDoorInfoDefBuffer(), - }[self.fileType]; - } - }); + getContents() { + const handler = this.getHandler().bind(this); + return handler(); + } - this.getDoorInfoFileName = function() { - var x; - var node = self.client.node; + getDoorInfoFileName() { + let x; + const node = this.client.node; if(10 === node) { x = 0; } else if(node < 10) { @@ -75,54 +77,53 @@ function DropFile(client, fileType) { x = String.fromCharCode('a'.charCodeAt(0) + (node - 11)); } return 'DORINFO' + x + '.DEF'; - }; + } - this.getDoorSysBuffer = function() { - var up = self.client.user.properties; - var now = moment(); - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + getDoorSysBuffer() { + const prop = this.client.user.properties; + const now = moment(); + const secLevel = this.client.user.getLegacySecurityLevel().toString(); // :TODO: fix time remaining // :TODO: fix default protocol -- user prop: transfer_protocol - return iconv.encode( [ 'COM1:', // "Comm Port - COM0: = LOCAL MODE" '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) '8', // "Parity - 7 or 8" - self.client.node.toString(), // "Node Number - 1 to 99" + this.client.node.toString(), // "Node Number - 1 to 99" '57600', // "DTE Rate. Actual BPS rate to use. (kg)" 'Y', // "Screen Display - Y=On N=Off (Default to Y)" 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" 'Y', // "Page Bell - Y=On N=Off (Default to Y)" 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" - up.real_name || self.client.user.username, // "User Full Name" - up.location || 'Anywhere', // "Calling From" + prop.real_name || this.client.user.username, // "User Full Name" + prop.location || 'Anywhere', // "Calling From" '123-456-7890', // "Home Phone" '123-456-7890', // "Work/Data Phone" 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) secLevel, // "Security Level" - up.login_count.toString(), // "Total Times On" + prop.login_count.toString(), // "Total Times On" now.format('MM/DD/YY'), // "Last Date Called" '15360', // "Seconds Remaining THIS call (for those that particular)" '256', // "Minutes Remaining THIS call" 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" - self.client.term.termHeight.toString(), // "Page Length" + this.client.term.termHeight.toString(), // "Page Length" 'N', // "User Mode - Y = Expert, N = Novice" '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" '1', // "Conference Exited To DOOR From (G)" '01/01/99', // "User Expiration Date (mm/dd/yy)" - self.client.user.userId.toString(), // "User File's Record Number" + this.client.user.userId.toString(), // "User File's Record Number" 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." // :TODO: fix up, down, etc. form user properties '0', // "Total Uploads" '0', // "Total Downloads" '0', // "Daily Download "K" Total" '999999', // "Daily Download Max. "K" Limit" - moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" + moment(prop.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" 'X:\\GEN\\', // "Path to the GEN directory" StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" - self.client.user.username, // "Alias name" + this.client.user.username, // "Alias name" '00:05', // "Event time (hh:mm)" (note: wat?) 'Y', // "If its an error correcting connection (Y/N)" 'Y', // "ANSI supported & caller using NG mode (Y/N)" @@ -136,40 +137,47 @@ function DropFile(client, fileType) { now.format('hh:mm'), // "Time of Last Call (hh:mm)" '9999', // "Maximum daily files available" // :TODO: fix these stats: - '0', // "Files d/led so far today" + '0', // "Files d/led so far today" '0', // "Total "K" Bytes Uploaded" '0', // "Total "K" Bytes Downloaded" - up.user_comment || 'None', // "User Comment" + prop.user_comment || 'None', // "User Comment" '0', // "Total Doors Opened" '0', // "Total Messages Left" ].join('\r\n') + '\r\n', 'cp437'); - }; + } - this.getDoor32Buffer = function() { + getDoor32Buffer() { // // Resources: // * http://wiki.bbses.info/index.php/DOOR32.SYS // // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! + const Door32CommTypes = { + Local : 0, + Serial : 1, + Telnet : 2, + }; + + const socketFd = _.isNumber(this.socketDescriptor) ? this.socketDescriptor : -1; + const commType = socketFd > -1 ? Door32CommTypes.Telnet : Door32CommTypes.Local; + return iconv.encode([ - '2', // :TODO: This needs to be configurable! - // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely - '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! - '57600', + commType.toString(), + socketFd.toString(), + '115200', Config.general.boardName, - self.client.user.userId.toString(), - self.client.user.properties.real_name || self.client.user.username, - self.client.user.username, - self.client.user.getLegacySecurityLevel().toString(), + this.client.user.userId.toString(), + this.client.user.properties.real_name || this.client.user.username, + this.client.user.username, + this.client.user.getLegacySecurityLevel().toString(), '546', // :TODO: Minutes left! '1', // ANSI - self.client.node.toString(), + this.client.node.toString(), ].join('\r\n') + '\r\n', 'cp437'); + } - }; - - this.getDoorInfoDefBuffer = function() { + getDoorInfoDefBuffer() { // :TODO: fix time remaining // @@ -178,34 +186,33 @@ function DropFile(client, fileType) { // // Note that usernames are just used for first/last names here // - var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; - var un = /[^\s]*/.exec(self.client.user.username)[0]; - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + const opUserName = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; + const userName = /[^\s]*/.exec(this.client.user.username)[0]; + const secLevel = this.client.user.getLegacySecurityLevel().toString(); return iconv.encode( [ Config.general.boardName, // "The name of the system." - opUn, // "The sysop's name up to the first space." - opUn, // "The sysop's name following the first space." + opUserName, // "The sysop's name up to the first space." + opUserName, // "The sysop's name following the first space." 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." '57600', // "The current port (DTE) rate." '0', // "The number "0"" - un, // "The current user's name, up to the first space." - un, // "The current user's name, following the first space." - self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." + userName, // "The current user's name, up to the first space." + userName, // "The current user's name, following the first space." + this.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." '1', // "The number "0" if TTY, or "1" if ANSI." secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." ].join('\r\n') + '\r\n', 'cp437'); - }; + } -} - -DropFile.fileTypes = [ 'DORINFO' ]; - -DropFile.prototype.createFile = function(cb) { - fs.writeFile(this.fullPath, this.dropFileContents, function written(err) { - cb(err); - }); + createFile(cb) { + mkdirs(paths.dirname(this.fullPath), err => { + if(err) { + return cb(err); + } + return fs.writeFile(this.fullPath, this.getContents(), cb); + }); + } }; - diff --git a/core/enig_error.js b/core/enig_error.js index c6eb8097..4dc9047c 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -35,6 +35,7 @@ exports.Errors = { MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), + MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), }; exports.ErrorReasons = { diff --git a/package.json b/package.json index 88844455..b6d19715 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "uuid-parse": "^1.0.0", "ws": "^4.0.0", "xxhash": "^0.2.4", - "yazl": "^2.4.2" + "yazl": "^2.4.2", + "getsockethandleaddress" : "^1.1.0" }, "devDependencies": {}, "engines": { From 6ec3619815f21017128ec18bac433382f8621e61 Mon Sep 17 00:00:00 2001 From: Jason Windisch Date: Thu, 29 Mar 2018 22:16:45 -0400 Subject: [PATCH 095/569] Added arrows to theme selection --- art/themes/luciano_blocktronics/CONFSCR.ANS | Bin 2529 -> 2556 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/CONFSCR.ANS b/art/themes/luciano_blocktronics/CONFSCR.ANS index 0bf72430fa56c7339d52bed57a21c0c0277846bb..e3db0cbb04fc67a019784ec9991e2906dfa68410 100644 GIT binary patch delta 108 zcmaDT{6~1hPBvy^^W4qb**F+1WFQ@FW Date: Tue, 10 Apr 2018 19:53:04 -0600 Subject: [PATCH 096/569] Pull copyright from license - easier to maintain single place --- core/bbs.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 33301f70..a7246b8b 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -25,9 +25,12 @@ exports.main = main; // object with various services we want to de-init/shutdown cleanly if possible const initServices = {}; -const ENIGMA_COPYRIGHT = 'ENiGMA½ Copyright (c) 2014-2017 Bryan Ashby'; +// only include bbs.js once @ startup; this should be fine +const COPYRIGHT = fs.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8').split(/\r?\n/g)[0]; + +const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`; const HELP = -`${ENIGMA_COPYRIGHT} +`${FULL_COPYRIGHT} usage: main.js eg : main.js --config /enigma_install_path/config/ @@ -90,7 +93,7 @@ function main() { function complete(err) { // note this is escaped: fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { - console.info(ENIGMA_COPYRIGHT); + console.info(FULL_COPYRIGHT); if(!err) { console.info(banner); } From 8ee573fb9d5bc104a52bcaca944ea750417588d5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 15 Apr 2018 20:25:56 -0600 Subject: [PATCH 097/569] Initial commit of Gopher contnet server --- core/servers/content/gopher.js | 316 +++++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 core/servers/content/gopher.js diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js new file mode 100644 index 00000000..0a20b0aa --- /dev/null +++ b/core/servers/content/gopher.js @@ -0,0 +1,316 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').config; +const { + splitTextAtTerms, + isAnsi, + cleanControlCodes +} = require('../../string_util.js'); +const { + getMessageConferenceByTag, + getMessageAreaByTag, + getMessageListForArea, +} = require('../../message_area.js'); +const { sortAreasOrConfs } = require('../../conf_area_util.js'); +const AnsiPrep = require('../../ansi_prep.js'); + +// deps +const net = require('net'); +const _ = require('lodash'); +const fs = require('graceful-fs'); +const paths = require('path'); +const moment = require('moment'); + +const ModuleInfo = exports.moduleInfo = { + name : 'Gopher', + desc : 'Gopher Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.gopher.server', +}; + +const Message = require('../../message.js'); + +const ItemTypes = { + Invalid : '', // not really a type, of course! + + // Canonical, RFC-1436 + TextFile : '0', + SubMenu : '1', + CCSONameserver : '2', + Error : '3', + BinHexFile : '4', + DOSFile : '5', + UuEncodedFile : '6', + FullTextSearch : '7', + Telnet : '8', + BinaryFile : '9', + AltServer : '+', + GIFFile : 'g', + ImageFile : 'I', + Telnet3270 : 'T', + + // Non-canonical + HtmlFile : 'h', + InfoMessage : 'i', + SoundFile : 's', +}; + +exports.getModule = class GopherModule extends ServerModule { + + constructor() { + super(); + + this.routes = new Map(); // selector->generator => gopher item + } + + createServer() { + if(!this.enabled) { + return; + } + + this.publicHostname = Config.contentServers.gopher.publicHostname; + this.publicPort = Config.contentServers.gopher.publicPort; + + this.addRoute(/^\/?\r\n$/, this.defaultGenerator); + this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); + + this.server = net.createServer( socket => { + socket.setEncoding('ascii'); + + socket.on('data', data => { + this.routeRequest(data, socket); + }); + + socket.on('error', err => { + if('ECONNRESET' !== err.code) { // normal + Log.trace( { error : err.message }, 'Socket error'); + } + }); + }); + } + + listen() { + if(!this.enabled) { + return true; // nothing to do, but not an error + } + + const port = parseInt(Config.contentServers.gopher.port); + if(isNaN(port)) { + Log.warn( { port : Config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); + return false; + } + + return this.server.listen(port); + } + + get enabled() { + return _.get(Config, 'contentServers.gopher.enabled', false) && this.isConfigured(); + } + + isConfigured() { + // public hostname & port must be set; responses contain them! + return _.isString(_.get(Config, 'contentServers.gopher.publicHostname')) && + _.isNumber(_.get(Config, 'contentServers.gopher.publicPort')); + } + + addRoute(selectorRegExp, generatorHandler) { + if(_.isString(selectorRegExp)) { + try { + selectorRegExp = new RegExp(`${selectorRegExp}\r\n`); + } catch(e) { + Log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' ); + return false; + } + } + this.routes.set(selectorRegExp, generatorHandler.bind(this)); + } + + routeRequest(selector, socket) { + let generator; + let match; + for(let [regex, gen] of this.routes) { + match = selector.match(regex); + if(match) { + generator = gen; + break; + } + } + generator = generator || this.notFoundGenerator; + generator(match, res => { + socket.end(`${res}.\r\n`); // includes RFC-1436 'Lastline' + }); + } + + makeItem(itemType, text, selector, hostname, port) { + selector = selector || ''; // e.g. for info + hostname = hostname || this.publicHostname; + port = port || this.publicPort; + return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; + } + + defaultGenerator(selectorMatch, cb) { + let bannerFile = _.get(Config, 'contentServers.gopher.banner', 'startup_banner.asc'); + bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); + fs.readFile(bannerFile, 'utf8', (err, banner) => { + if(err) { + return cb('You have reached an ENiGMA½ Gopher server!'); + } + + banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join(''); + banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); + return cb(banner); + }); + } + + notFoundGenerator(selectorMatch, cb) { + return cb('Not found'); + } + + isAreaAndConfExposed(confTag, areaTag) { + const conf = _.get(Config, [ 'contentServers', 'gopher', 'messageConferences', confTag ]); + return Array.isArray(conf) && conf.includes(areaTag); + } + + prepareMessageBody(body, cb) { + if(isAnsi(body)) { + AnsiPrep( + body, + { + cols : 79, // Gopher std. wants 70, but we'll have to deal with it. + forceLineTerm : true, // ensure each line is term'd + asciiMode : true, // export to ASCII + fillLines : false, // don't fill up to |cols| + }, + (err, prepped) => { + return cb(prepped || body); + } + ); + } else { + return cb(cleanControlCodes(body, { all : true } )); + } + } + + messageAreaGenerator(selectorMatch, cb) { + // + // Selector should be: + // /msgarea - list confs + // /msgarea/conftag - list areas in conf + // /msgarea/conftag/areatag - list messages in area + // /msgarea/conftag/areatag/ - message as text + // /msgarea/conftag/areatag/_raw - full message as text + headers + // + if(selectorMatch[3] || selectorMatch[4]) { + // message + //const raw = selectorMatch[4] ? true : false; + // :TODO: support 'raw' + const msgUuid = selectorMatch[3].replace(/\r\n|\//g, ''); + const confTag = selectorMatch[1].substr(1).split('/')[0]; + const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); + const message = new Message(); + + return message.load( { uuid : msgUuid }, err => { + if(err) { + return this.notFoundGenerator(selectorMatch, cb); + } + + if(message.areaTag !== areaTag || !this.isAreaAndConfExposed(confTag, areaTag)) { + return this.notFoundGenerator(selectorMatch, cb); + } + + this.prepareMessageBody(message.message, msgBody => { + // :TODO: create DRY for subject trimming... + const response = `${'-'.repeat(70)} +To : ${message.toUserName} +From : ${message.fromUserName} +When : ${moment(message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')} +Subject: ${message.subject} +ID : ${message.messageUuid} (${message.messageId}) +${'-'.repeat(70)} +${msgBody} + `; + return cb(response); + }); + }); + } else if(selectorMatch[2]) { + // list messages in area + const confTag = selectorMatch[1].substr(1).split('/')[0]; + const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); + const area = getMessageAreaByTag(areaTag); + + if(Message.isPrivateAreaTag(areaTag)) { + return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private')); + } + + if(!area || !this.isAreaAndConfExposed(confTag, areaTag)) { + return this.notFoundGenerator(selectorMatch, cb); + } + + return getMessageListForArea(null, areaTag, (err, msgList) => { + const response = [ + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`), + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + ...msgList.map(msg => this.makeItem( + ItemTypes.TextFile, + // :TODO: reasonably trim string + `${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')} ${msg.subject} (${msg.fromUserName} to ${msg.toUserName})`, + `/msgarea/${confTag}/${areaTag}/${msg.messageUuid}` + )) + ].join(''); + + return cb(response); + }); + } else if(selectorMatch[1]) { + // list areas in conf + const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); + const conf = _.get(Config, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); + if(!conf) { + return this.notFoundGenerator(selectorMatch, cb); + } + + const areas = _.get(Config, [ 'contentServers', 'gopher', 'messageConferences', confTag ], {}) + .map(areaTag => Object.assign( { areaTag }, getMessageAreaByTag(areaTag))) + .filter(area => area && !Message.isPrivateAreaTag(area.areaTag)); + + if(0 === areas.length) { + return cb(this.makeIItem(ItemTypes.InfoMessage, 'No message areas available')); + } + + sortAreasOrConfs(areas); + + const response = [ + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`), + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + ...areas.map(area => this.makeItem(ItemTypes.SubMenu, area.name, `/msgarea/${confTag}/${area.areaTag}`)) + ].join(''); + + return cb(response); + } else { + // message area base (list confs) + const confs = Object.keys(_.get(Config, 'contentServers.gopher.messageConferences', {})) + .map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag))) + .filter(conf => conf); // remove any baddies + + if(0 === confs.length) { + return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available')); + } + + sortAreasOrConfs(confs); + + const response = [ + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'), + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, ''), + ...confs.map(conf => this.makeItem(ItemTypes.SubMenu, conf.name, `/msgarea/${conf.confTag}`)) + ].join(''); + + return cb(response); + } + } +}; \ No newline at end of file From f557e5b6e0d3616a517ef530d4562cf311e3f7e3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 16 Apr 2018 17:10:19 -0600 Subject: [PATCH 098/569] Minor Gopher updates --- core/message.js | 1 + core/servers/content/gopher.js | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/message.js b/core/message.js index d7204e13..f1fa2db8 100644 --- a/core/message.js +++ b/core/message.js @@ -497,6 +497,7 @@ module.exports = class Message { }); } + // :TODO: this should only take a UUID... load(options, cb) { assert(_.isString(options.uuid)); diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 0a20b0aa..4d1e3f6a 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -153,7 +153,7 @@ exports.getModule = class GopherModule extends ServerModule { } defaultGenerator(selectorMatch, cb) { - let bannerFile = _.get(Config, 'contentServers.gopher.banner', 'startup_banner.asc'); + let bannerFile = _.get(Config, 'contentServers.gopher.bannerFile', 'startup_banner.asc'); bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); fs.readFile(bannerFile, 'utf8', (err, banner) => { if(err) { @@ -194,6 +194,10 @@ exports.getModule = class GopherModule extends ServerModule { } } + shortenSubject(subject) { + return _.truncate(subject, { length : 30 } ); + } + messageAreaGenerator(selectorMatch, cb) { // // Selector should be: @@ -222,7 +226,6 @@ exports.getModule = class GopherModule extends ServerModule { } this.prepareMessageBody(message.message, msgBody => { - // :TODO: create DRY for subject trimming... const response = `${'-'.repeat(70)} To : ${message.toUserName} From : ${message.fromUserName} @@ -256,8 +259,7 @@ ${msgBody} this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), ...msgList.map(msg => this.makeItem( ItemTypes.TextFile, - // :TODO: reasonably trim string - `${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')} ${msg.subject} (${msg.fromUserName} to ${msg.toUserName})`, + `${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(msg.subject)} (${msg.fromUserName} to ${msg.toUserName})`, `/msgarea/${confTag}/${areaTag}/${msg.messageUuid}` )) ].join(''); From 695e84e16f8b7d8419d3401e7679567adfa7c224 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 16 Apr 2018 19:29:25 -0600 Subject: [PATCH 099/569] * Remove "LastLine" indicator - does not seem to be used in practice/is not required * Add logging to Gopher --- core/servers/content/gopher.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 4d1e3f6a..f4fc3797 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -65,6 +65,7 @@ exports.getModule = class GopherModule extends ServerModule { super(); this.routes = new Map(); // selector->generator => gopher item + this.log = Log.child( { server : 'Gopher' } ); } createServer() { @@ -87,7 +88,7 @@ exports.getModule = class GopherModule extends ServerModule { socket.on('error', err => { if('ECONNRESET' !== err.code) { // normal - Log.trace( { error : err.message }, 'Socket error'); + this.log.trace( { error : err.message }, 'Socket error'); } }); }); @@ -100,7 +101,7 @@ exports.getModule = class GopherModule extends ServerModule { const port = parseInt(Config.contentServers.gopher.port); if(isNaN(port)) { - Log.warn( { port : Config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); + this.log.warn( { port : Config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); return false; } @@ -122,7 +123,7 @@ exports.getModule = class GopherModule extends ServerModule { try { selectorRegExp = new RegExp(`${selectorRegExp}\r\n`); } catch(e) { - Log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' ); + this.log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' ); return false; } } @@ -141,7 +142,7 @@ exports.getModule = class GopherModule extends ServerModule { } generator = generator || this.notFoundGenerator; generator(match, res => { - socket.end(`${res}.\r\n`); // includes RFC-1436 'Lastline' + socket.end(`${res}`); }); } @@ -153,6 +154,8 @@ exports.getModule = class GopherModule extends ServerModule { } defaultGenerator(selectorMatch, cb) { + this.log.trace( { selector : selectorMatch[0] }, 'Serving default content'); + let bannerFile = _.get(Config, 'contentServers.gopher.bannerFile', 'startup_banner.asc'); bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); fs.readFile(bannerFile, 'utf8', (err, banner) => { @@ -167,6 +170,7 @@ exports.getModule = class GopherModule extends ServerModule { } notFoundGenerator(selectorMatch, cb) { + this.log.trace( { selector : selectorMatch[0] }, 'Serving not found content'); return cb('Not found'); } @@ -199,13 +203,14 @@ exports.getModule = class GopherModule extends ServerModule { } messageAreaGenerator(selectorMatch, cb) { + this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content'); // // Selector should be: // /msgarea - list confs // /msgarea/conftag - list areas in conf // /msgarea/conftag/areatag - list messages in area - // /msgarea/conftag/areatag/ - message as text - // /msgarea/conftag/areatag/_raw - full message as text + headers + // /msgarea/conftag/areatag/ - message as text + // /msgarea/conftag/areatag/_raw - full message as text + headers // if(selectorMatch[3] || selectorMatch[4]) { // message @@ -218,10 +223,17 @@ exports.getModule = class GopherModule extends ServerModule { return message.load( { uuid : msgUuid }, err => { if(err) { + this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existant message UUID!'); return this.notFoundGenerator(selectorMatch, cb); } if(message.areaTag !== areaTag || !this.isAreaAndConfExposed(confTag, areaTag)) { + this.log.warn( { areaTag }, 'Attempted access to non-exposed conference and/or area!'); + return this.notFoundGenerator(selectorMatch, cb); + } + + if(Message.isPrivateAreaTag(areaTag)) { + this.log.warn( { areaTag }, 'Attempted access to message in private area!'); return this.notFoundGenerator(selectorMatch, cb); } @@ -245,10 +257,12 @@ ${msgBody} const area = getMessageAreaByTag(areaTag); if(Message.isPrivateAreaTag(areaTag)) { + this.log.warn( { areaTag }, 'Attempted access to private area!'); return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private')); } if(!area || !this.isAreaAndConfExposed(confTag, areaTag)) { + this.log.warn( { confTag, areaTag }, 'Attempted access to non-exposed conference and/or area!'); return this.notFoundGenerator(selectorMatch, cb); } @@ -279,7 +293,7 @@ ${msgBody} .filter(area => area && !Message.isPrivateAreaTag(area.areaTag)); if(0 === areas.length) { - return cb(this.makeIItem(ItemTypes.InfoMessage, 'No message areas available')); + return cb(this.makeItem(ItemTypes.InfoMessage, 'No message areas available')); } sortAreasOrConfs(areas); From 78feb2695849181df36c10ecc1c40c14742677bd Mon Sep 17 00:00:00 2001 From: Jason Kendall Date: Mon, 23 Apr 2018 20:20:31 -0400 Subject: [PATCH 100/569] Invalid JSON - Missing Comma Was missing the ending comma on the wsConnect line. This just adds it. --- docs/servers/websocket.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/servers/websocket.md b/docs/servers/websocket.md index 62df02d7..435e4482 100644 --- a/docs/servers/websocket.md +++ b/docs/servers/websocket.md @@ -64,7 +64,7 @@ webserver, and unpack it to a temporary directory. ````javascript var vtxdata = { sysName: "Your Awesome BBS", - wsConnect: "wss://your-hostname.here:8811" + wsConnect: "wss://your-hostname.here:8811", term: "ansi-bbs", codePage: "CP437", fontName: "UVGA16", @@ -88,4 +88,4 @@ otherwise. 9. If you navigate to http://your-hostname.here/vtx.html, you should see a splash screen like the following: ![VTXClient](../assets/images/vtxclient.png "VTXClient") - \ No newline at end of file + From a1f55e5ad51f82bb47a335d5758784c20f6d8021 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 23 Apr 2018 18:41:12 -0600 Subject: [PATCH 101/569] Webserver Crashes accessing Directory #177 --- core/servers/content/web.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 8a9903b4..d1edb221 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -218,7 +218,7 @@ exports.getModule = class WebServerModule extends ServerModule { const self = this; fs.stat(filePath, (err, stats) => { - if(err) { + if(err || !stats.isFile()) { return self.fileNotFound(resp); } From 389e52dcb9f184d11b8608550377ca75574d0257 Mon Sep 17 00:00:00 2001 From: Jason Kendall Date: Mon, 23 Apr 2018 20:53:55 -0400 Subject: [PATCH 102/569] Missing mb command in oputil.js help The `mb` sub-command was missing from the general help output from oputil.js - this add it. --- core/oputil/oputil_help.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 4875d382..c5e096b5 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -19,6 +19,7 @@ commands: user user utilities config config file management fb file base management + mb message base management `, User : `usage: optutil.js user --user USERNAME @@ -100,4 +101,4 @@ general information: function getHelpFor(command) { return usageHelp[command]; -} \ No newline at end of file +} From a0cd8fed83483efb127b378985c93e4c9fad6808 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 23 Apr 2018 19:03:35 -0600 Subject: [PATCH 103/569] Websocket config should be similar to web #176 --- core/config.js | 18 +++++++++++++----- core/servers/login/websocket.js | 17 ++++++++++------- docs/servers/websocket.md | 21 ++++++++++++++++----- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/core/config.js b/core/config.js index 7e5d92a1..2564c256 100644 --- a/core/config.js +++ b/core/config.js @@ -230,11 +230,19 @@ function getDefaultConfig() { firstMenuNewUser : 'sshConnectedNewUser', }, webSocket : { - port : 8810, // ws:// - enabled : false, - securePort : 8811, // wss:// - must provide certPem and keyPem - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + ws : { + // non-secure ws:// + enabled : false, + port : 8810, + }, + wss : { + // secure ws:// + // must provide valid certPem and keyPem + enabled : false, + port : 8811, + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + }, }, }, diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index cc0270b1..9e480ac9 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -118,12 +118,15 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { // * insecure websocket (ws://) // * secure (tls) websocket (wss://) // - const config = _.get(Config, 'loginServers.webSocket') || { enabled : false }; - if(!config || true !== config.enabled || !(config.port || config.securePort)) { + const config = _.get(Config, 'loginServers.webSocket'); + if(!_.isObject(config)) { return; } - if(config.port) { + const wsPort = _.get(config, 'ws.port'); + const wssPort = _.get(config, 'wss.port'); + + if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) { const httpServer = http.createServer( (req, resp) => { // dummy handler resp.writeHead(200); @@ -136,10 +139,10 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { }; } - if(config.securePort) { + if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) { const httpServer = https.createServer({ - key : fs.readFileSync(Config.loginServers.webSocket.keyPem), - cert : fs.readFileSync(Config.loginServers.webSocket.certPem), + key : fs.readFileSync(config.wss.keyPem), + cert : fs.readFileSync(config.wss.certPem), }); this.secure = { @@ -157,7 +160,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { } const serverName = `${ModuleInfo.name} (${serverType})`; - const port = parseInt(_.get(Config, [ 'loginServers', 'webSocket', 'secure' === serverType ? 'securePort' : 'port' ] )); + const port = parseInt(_.get(Config, [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); if(isNaN(port)) { Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); diff --git a/docs/servers/websocket.md b/docs/servers/websocket.md index 435e4482..be5eb739 100644 --- a/docs/servers/websocket.md +++ b/docs/servers/websocket.md @@ -27,11 +27,22 @@ don't already have it defined). ````hjson loginServers: { webSocket : { - port: 8810 - enabled: true - securePort: 8811 - certPem: /path/to/https_cert.pem - keyPem: /path/to/https_cert_key.pem + ws: { + // non-secure ws:// + port: 8810 + enabled: true + } + wss: { + // secure-over-tls wss:// + port: 8811 + enabled: true + certPem: /path/to/https_cert.pem + keyPem: /path/to/https_cert_key.pem + } + // set proxied to true to allow TLS-terminated proxied connections + // containing the "X-Forwarded-Proto: https" header to be treated + // as secure + proxied: true } } ```` From 7ac388c30d96087b725265424049bd3a89f19158 Mon Sep 17 00:00:00 2001 From: Jason Kendall Date: Tue, 24 Apr 2018 08:30:14 -0400 Subject: [PATCH 104/569] Force no compression It appears as tho there is a problem with compression and the upstream library. This PR forces no compression mode, making a work around for #181. This work around is derived from https://github.com/mscdex/ssh2/issues/594 which may be the cause for #181 as well. --- core/servers/login/ssh.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index d14bdbbd..80a99c7e 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -248,6 +248,7 @@ exports.getModule = class SSHServerModule extends LoginServerModule { Log.trace(`SSH: ${sshDebugLine}`); } }, + algorithms: { compress: ['none'] }, }; this.server = ssh2.Server(serverConf); From 69ced917f38dabc968e206bc00fc7bf157e23acc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 24 Apr 2018 19:58:59 -0600 Subject: [PATCH 105/569] Bind notFoundGenerator --- core/servers/content/gopher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index f4fc3797..aa2e4be0 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -140,7 +140,7 @@ exports.getModule = class GopherModule extends ServerModule { break; } } - generator = generator || this.notFoundGenerator; + generator = generator || this.notFoundGenerator.bind(this); generator(match, res => { socket.end(`${res}`); }); From 0b77c1f79edba73fb986f1b34bdd37ce418f349d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 24 Apr 2018 20:07:02 -0600 Subject: [PATCH 106/569] Better notFoundGenerator --- core/servers/content/gopher.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index aa2e4be0..5e0ee42f 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -131,18 +131,17 @@ exports.getModule = class GopherModule extends ServerModule { } routeRequest(selector, socket) { - let generator; let match; for(let [regex, gen] of this.routes) { match = selector.match(regex); if(match) { - generator = gen; - break; + return gen(match, res => { + return socket.end(`${res}`); + }); } } - generator = generator || this.notFoundGenerator.bind(this); - generator(match, res => { - socket.end(`${res}`); + this.notFoundGenerator(selector, res => { + return socket.end(`${res}`); }); } @@ -169,8 +168,8 @@ exports.getModule = class GopherModule extends ServerModule { }); } - notFoundGenerator(selectorMatch, cb) { - this.log.trace( { selector : selectorMatch[0] }, 'Serving not found content'); + notFoundGenerator(selector, cb) { + this.log.trace( { selector }, 'Serving not found content'); return cb('Not found'); } From 476e8f2f0c9fbae70ce0b8aed015cb6ea3b389b4 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 28 Apr 2018 00:33:34 +0100 Subject: [PATCH 107/569] Upgrade to node.js 10 --- misc/install.sh | 2 +- package-lock.json | 2663 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 8 +- 3 files changed, 2668 insertions(+), 5 deletions(-) create mode 100644 package-lock.json diff --git a/misc/install.sh b/misc/install.sh index 36ab8202..b50ca8c7 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -2,7 +2,7 @@ { # this ensures the entire script is downloaded before execution -ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=8} +ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=10} ENIGMA_INSTALL_DIR=${ENIGMA_INSTALL_DIR:=$HOME/enigma-bbs} ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..3e52f2b9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2663 @@ +{ + "name": "enigma-bbs", + "version": "0.0.9-alpha", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "requires": { + "lodash": "4.17.10" + } + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "binary-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-1.3.2.tgz", + "integrity": "sha512-VDhHcpeF1/ZZy1XvDmYD67bBjRNm1gacw+772xNd5BnTH6ax5TzlDV5dl7216/UlQXQoN9vug07ehk7e0PhNUw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "bser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", + "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "requires": { + "node-int64": "0.4.0" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffers": { + "version": "github:NuSkooler/node-buffers#cd0855598f7048b02f0a51c90e22573973e9e2c2" + }, + "bunyan": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", + "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", + "requires": { + "dtrace-provider": "0.8.6", + "moment": "2.22.1", + "mv": "2.1.1", + "safe-json-stringify": "1.1.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.1", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.4.5" + } + }, + "dtrace-provider": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz", + "integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=", + "optional": true, + "requires": { + "nan": "2.10.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "exec-sh": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", + "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "requires": { + "merge": "1.2.0" + } + }, + "exiftool": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/exiftool/-/exiftool-0.0.3.tgz", + "integrity": "sha1-9YqSvXcnCtxU8xUc7WGko6tp1wc=" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.21", + "tmp": "0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "fb-watchman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", + "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "requires": { + "bser": "2.0.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "0.2.2" + } + }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.3.tgz", + "integrity": "sha512-X+57O5YkDTiEQGiw8i7wYc2nQgweIekqkepI8Q3y4wVlurgBt2SuwxTeYUYMZIGpLZH3r/TsMjczCMXE5ZOt7Q==", + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.9.1" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.9.1", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.6", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.6", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hashids": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.1.4.tgz", + "integrity": "sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w==" + }, + "hjson": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hjson/-/hjson-3.1.1.tgz", + "integrity": "sha512-1oGkOq4sssz7HFZ8Is9HuTR47r8gSC46qAzQxVlAkj0lNKpS+W5Lv2eci+c5+fFqL+Idtj5EvprFreUwH29a8A==" + }, + "iconv-lite": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "requires": { + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.2.0", + "figures": "2.0.0", + "lodash": "4.17.10", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rxjs": "5.5.10", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "later": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/later/-/later-1.2.0.tgz", + "integrity": "sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8=" + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "requires": { + "tmpl": "1.0.4" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "1.0.1" + } + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true + } + } + }, + "moment": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", + "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" + } + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + }, + "node-pty": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.7.4.tgz", + "integrity": "sha512-WxMY1BsGcHJ2Z2qWpYL7QbfOSnkkCzV0H/9+dJ7uQEIJyz0A4fVBLymswBCTc7RoweY5ingib2gNvf87KvJxuA==", + "requires": { + "nan": "2.10.0" + } + }, + "nodemailer": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz", + "integrity": "sha512-SD4uuX7NMzZ5f5m1XHDd13J4UC3SmdJk8DsmU1g6Nrs5h3x9LcXr6EBPZIqXRJ3LrF7RdklzGhZRF/TuylTcLg==" + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "3.0.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "1.2.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "requires": { + "glob": "6.0.4" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "rlogin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rlogin/-/rlogin-1.0.0.tgz", + "integrity": "sha1-2wcyKzEhkSZiXZ0KqYctfr6KxAM=" + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "2.1.0" + } + }, + "rxjs": { + "version": "5.5.10", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", + "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", + "requires": { + "symbol-observable": "1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-json-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz", + "integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==", + "optional": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "0.1.15" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.0.tgz", + "integrity": "sha512-glfKd7YH4UCrh/7dD+UESsr8ylKWRE7UQPoXuz28FgmcF0ViJQhCTCCZHICRKxf8G8O1KdLEn20dcICK54c7ew==", + "requires": { + "anymatch": "2.0.0", + "exec-sh": "0.2.1", + "fb-watchman": "2.0.0", + "fsevents": "1.2.3", + "micromatch": "3.1.10", + "minimist": "1.2.0", + "walker": "1.0.7", + "watch": "0.18.0" + } + }, + "sanitize-filename": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", + "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", + "requires": { + "truncate-utf8-bytes": "1.0.2" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "requires": { + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "3.0.2" + } + }, + "sqlite3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.0.tgz", + "integrity": "sha512-6OlcAQNGaRSBLK1CuaRbKwlMFBb9DEhzmZyQP+fltNRF6XcIMpVIfXCBEcXPe1d4v9LnhkQUYkknDbA5JReqJg==", + "requires": { + "nan": "2.9.2", + "node-pre-gyp": "0.9.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.5" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "requires": { + "minipass": "2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "iconv-lite": { + "version": "0.4.19", + "bundled": true + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "minipass": { + "version": "2.2.1", + "bundled": true, + "requires": { + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "requires": { + "minipass": "2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "nan": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz", + "integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==" + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.19", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.9.0", + "bundled": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.6", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true + }, + "rc": { + "version": "1.2.6", + "bundled": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "readable-stream": { + "version": "2.3.5", + "bundled": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "sax": { + "version": "1.2.4", + "bundled": true + }, + "semver": { + "version": "5.5.0", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "4.4.0", + "bundled": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.1", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true + } + } + }, + "sqlite3-trans": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sqlite3-trans/-/sqlite3-trans-1.2.0.tgz", + "integrity": "sha1-E8/K2wk+1I5m+U7IlWq3RU3clgg=", + "requires": { + "lodash": "4.17.10" + } + }, + "ssh2": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.5.5.tgz", + "integrity": "sha1-x3gezS7OcwSiU89iD6taXCK7IjU=", + "requires": { + "ssh2-streams": "0.1.20" + } + }, + "ssh2-streams": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.1.20.tgz", + "integrity": "sha1-URGNFUVV31Rp7h9n4M8efoosDjo=", + "requires": { + "asn1": "0.2.3", + "semver": "5.5.0", + "streamsearch": "0.1.2" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "3.0.0" + } + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + }, + "temptmp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.0.0.tgz", + "integrity": "sha1-M7Djbh8nMXyKKBIO6Wufj+tw2UM=", + "requires": { + "del": "2.2.2" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "requires": { + "utf8-byte-length": "1.0.4" + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "requires": { + "kind-of": "6.0.2" + } + }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "uuid-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.0.0.tgz", + "integrity": "sha1-9GV3F2JLDkuIrzb5jYlYmlu+5Wk=" + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "requires": { + "makeerror": "1.0.11" + } + }, + "watch": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", + "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", + "requires": { + "exec-sh": "0.2.1", + "minimist": "1.2.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.2" + } + }, + "xxhash": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/xxhash/-/xxhash-0.2.4.tgz", + "integrity": "sha1-i4pIFiz8zCG5IPpQAmEYfUAhbDk=", + "requires": { + "nan": "2.10.0" + } + }, + "yazl": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.4.3.tgz", + "integrity": "sha1-7CblzIfVYBud+EMtvdPNLlFzoHE=", + "requires": { + "buffer-crc32": "0.2.13" + } + } + } +} diff --git a/package.json b/package.json index 88844455..27c5332a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dependencies": { "async": "^2.5.0", "binary-parser": "^1.3.2", - "buffers": "NuSkooler/node-buffers", + "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", "fs-extra": "^5.0.0", @@ -39,12 +39,12 @@ "mime-types": "^2.1.17", "minimist": "1.2.x", "moment": "^2.20.1", - "nodemailer": "^4.4.1", "node-pty": "^0.7.4", + "nodemailer": "^4.4.1", "rlogin": "^1.0.0", "sane": "^2.2.0", "sanitize-filename": "^1.6.1", - "sqlite3": "^3.1.9", + "sqlite3": "^4.0.0", "sqlite3-trans": "^1.2.0", "ssh2": "^0.5.5", "temptmp": "^1.0.0", @@ -56,6 +56,6 @@ }, "devDependencies": {}, "engines": { - "node": ">=6.9.2" + "node": ">=10" } } From f16eb6f3e698f570acc3e461b552bfa214d2f1bb Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 28 Apr 2018 13:59:07 +0100 Subject: [PATCH 108/569] Fix Node.js 10 deprecation warnings --- .gitignore | 1 + core/archive_util.js | 4 ++-- core/art.js | 2 +- core/file_transfer.js | 4 ++-- core/fnv1a.js | 4 ++-- core/ftn_mail_packet.js | 20 ++++++++++---------- core/ftn_util.js | 2 +- core/sauce.js | 4 ++-- core/servers/login/telnet.js | 22 +++++++++++----------- core/telnet_bridge.js | 8 ++++---- core/user.js | 6 +++--- core/uuid_util.js | 6 +++--- 12 files changed, 42 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 5a58c717..28a19883 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.pem # Various directories +config/config.hjson logs/ db/ dropfiles/ diff --git a/core/archive_util.js b/core/archive_util.js index 8a4c388a..8ce2abc2 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -78,7 +78,7 @@ module.exports = class ArchiveUtil { Object.keys(Config.fileTypes).forEach(mimeType => { const fileType = Config.fileTypes[mimeType]; if(fileType.sig) { - fileType.sig = new Buffer(fileType.sig, 'hex'); + fileType.sig = Buffer.alloc(fileType.sig, 'hex'); fileType.offset = fileType.offset || 0; // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well @@ -120,7 +120,7 @@ module.exports = class ArchiveUtil { return cb(err); } - const buf = new Buffer(this.longestSignature); + const buf = Buffer.from(this.longestSignature); fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { if(err) { return cb(err); diff --git a/core/art.js b/core/art.js index 546014bd..9234542d 100644 --- a/core/art.js +++ b/core/art.js @@ -288,7 +288,7 @@ function display(client, art, options, cb) { } if(!options.disableMciCache) { - artHash = xxhash.hash(new Buffer(art), 0xCAFEBABE); + artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE); // see if we have a mciMap cached for this art if(client.mciCache) { diff --git a/core/file_transfer.js b/core/file_transfer.js index 757eff8a..19388bab 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -374,7 +374,7 @@ exports.getModule = class TransferFileModule extends MenuModule { // needed for things like sz/rz if(external.escapeTelnet) { const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape - externalProc.write(new Buffer(tmp, 'binary')); + externalProc.write(Buffer.from(tmp, 'binary')); } else { externalProc.write(data); } @@ -384,7 +384,7 @@ exports.getModule = class TransferFileModule extends MenuModule { // needed for things like sz/rz if(external.escapeTelnet) { const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape - this.client.term.rawWrite(new Buffer(tmp, 'binary')); + this.client.term.rawWrite(Buffer.from(tmp, 'binary')); } else { this.client.term.rawWrite(data); } diff --git a/core/fnv1a.js b/core/fnv1a.js index 743986d6..ce1fe105 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -19,7 +19,7 @@ module.exports = class FNV1a { } if(_.isString(data)) { - data = new Buffer(data); + data = Buffer.from(data); } if(!Buffer.isBuffer(data)) { @@ -38,7 +38,7 @@ module.exports = class FNV1a { digest(encoding) { encoding = encoding || 'binary'; - let buf = new Buffer(4); + let buf = Buffer.alloc(4); buf.writeInt32BE(this.hash & 0xffffffff, 0); return buf.toString(encoding); } diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index bb72c40a..5db02227 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -26,7 +26,7 @@ const FTN_PACKET_MESSAGE_TYPE = 2; const FTN_PACKET_BAUD_TYPE_2_2 = 2; // SAUCE magic header + version ("00") -const FTN_MESSAGE_SAUCE_HEADER = new Buffer('SAUCE00'); +const FTN_MESSAGE_SAUCE_HEADER = Buffer.from('SAUCE00'); const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; @@ -273,7 +273,7 @@ function Packet(options) { }; this.getPacketHeaderBuffer = function(packetHeader) { - let buffer = new Buffer(FTN_PACKET_HEADER_SIZE); + let buffer = Buffer.from(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); @@ -311,7 +311,7 @@ function Packet(options) { }; this.writePacketHeader = function(packetHeader, ws) { - let buffer = new Buffer(FTN_PACKET_HEADER_SIZE); + let buffer = Buffer.from(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); @@ -447,8 +447,8 @@ function Packet(options) { // Also according to the spec, the deprecated "CHARSET" value may be used // :TODO: Look into CHARSET more - should we bother supporting it? // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam - const FTN_CHRS_PREFIX = new Buffer( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" - const FTN_CHRS_SUFFIX = new Buffer( [ 0x0d ] ); + const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" + const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] ); let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); if(chrsPrefixIndex < 0) { @@ -724,7 +724,7 @@ function Packet(options) { buf.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); buf.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); - const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0'); + const dateTimeBuffer = Buffer.from(ftn.getDateTimeString(message.modTimestamp) + '\0'); dateTimeBuffer.copy(buf, 14); }; @@ -747,7 +747,7 @@ function Packet(options) { async.waterfall( [ function prepareHeaderAndKludges(callback) { - const basicHeader = new Buffer(34); + const basicHeader = Buffer.from(34); self.writeMessageHeader(message, basicHeader); // @@ -864,7 +864,7 @@ function Packet(options) { }; this.writeMessage = function(message, ws, options) { - let basicHeader = new Buffer(34); + let basicHeader = Buffer.from(34); self.writeMessageHeader(message, basicHeader); ws.write(basicHeader); @@ -1054,7 +1054,7 @@ Packet.prototype.writeTerminator = function(ws) { // From FTS-0001.016: // "A pseudo-message beginning with the word 0000H signifies the end of the packet." // - ws.write(new Buffer( [ 0x00, 0x00 ] )); // final extra null term + ws.write(Buffer.from( [ 0x00, 0x00 ] )); // final extra null term return 2; }; @@ -1074,7 +1074,7 @@ Packet.prototype.writeStream = function(ws, messages, options) { }); if(true === options.terminatePacket) { - ws.write(new Buffer( [ 0 ] )); // final extra null term + ws.write(Buffer.from( [ 0 ] )); // final extra null term } }; diff --git a/core/ftn_util.js b/core/ftn_util.js index 03040484..e40d2687 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -46,7 +46,7 @@ exports.getQuotePrefix = getQuotePrefix; // See list here: https://github.com/Mithgol/node-fidonet-jam function stringToNullPaddedBuffer(s, bufLen) { - let buffer = new Buffer(bufLen).fill(0x00); + let buffer = Buffer.alloc(bufLen).fill(0x00); let enc = iconv.encode(s, 'CP437').slice(0, bufLen); for(let i = 0; i < enc.length; ++i) { buffer[i] = enc[i]; diff --git a/core/sauce.js b/core/sauce.js index 9ee75b47..e0182ae7 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -10,10 +10,10 @@ const { Parser } = require('binary-parser'); exports.readSAUCE = readSAUCE; const SAUCE_SIZE = 128; -const SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' +const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' // :TODO read comments -//const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' +//const COMNT_ID = Buffer.from([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' exports.SAUCE_SIZE = SAUCE_SIZE; // :TODO: SAUCE should be a class diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 6ffb49a9..3c585c84 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -159,8 +159,8 @@ const NEW_ENVIRONMENT_COMMANDS = { USERVAR : 3, }; -const IAC_BUF = new Buffer([ COMMANDS.IAC ]); -const IAC_SE_BUF = new Buffer([ COMMANDS.IAC, COMMANDS.SE ]); +const IAC_BUF = Buffer.from([ COMMANDS.IAC ]); +const IAC_SE_BUF = Buffer.from([ COMMANDS.IAC, COMMANDS.SE ]); const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { names[COMMANDS[name]] = name.toLowerCase(); @@ -766,7 +766,7 @@ TelnetClient.prototype.handleMiscCommand = function(evt) { }; TelnetClient.prototype.requestTerminalType = function() { - const buf = new Buffer( [ + const buf = Buffer.from( [ COMMANDS.IAC, COMMANDS.SB, OPTIONS.TERMINAL_TYPE, @@ -777,10 +777,10 @@ TelnetClient.prototype.requestTerminalType = function() { }; const WANTED_ENVIRONMENT_VAR_BUFS = [ - new Buffer( 'LINES' ), - new Buffer( 'COLUMNS' ), - new Buffer( 'TERM' ), - new Buffer( 'TERM_PROGRAM' ) + Buffer.from( 'LINES' ), + Buffer.from( 'COLUMNS' ), + Buffer.from( 'TERM' ), + Buffer.from( 'TERM_PROGRAM' ) ]; TelnetClient.prototype.requestNewEnvironment = function() { @@ -793,7 +793,7 @@ TelnetClient.prototype.requestNewEnvironment = function() { const self = this; const bufs = buffers(); - bufs.push(new Buffer( [ + bufs.push(Buffer.from( [ COMMANDS.IAC, COMMANDS.SB, OPTIONS.NEW_ENVIRONMENT, @@ -801,10 +801,10 @@ TelnetClient.prototype.requestNewEnvironment = function() { )); for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { - bufs.push(new Buffer( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); + bufs.push(Buffer.from( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); } - bufs.push(new Buffer([ NEW_ENVIRONMENT_COMMANDS.USERVAR, COMMANDS.IAC, COMMANDS.SE ])); + bufs.push(Buffer.from([ NEW_ENVIRONMENT_COMMANDS.USERVAR, COMMANDS.IAC, COMMANDS.SE ])); self.output.write(bufs.toBuffer()); @@ -836,7 +836,7 @@ Object.keys(OPTIONS).forEach(function(name) { const code = OPTIONS[name]; Command.prototype[name.toLowerCase()] = function() { - const buf = new Buffer(3); + const buf = Buffer.alloc(3); buf[0] = COMMANDS.IAC; buf[1] = this.command; buf[2] = code; diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 95ace6c6..150996dd 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -33,7 +33,7 @@ exports.moduleInfo = { author : 'Andrew Pamment', }; -const IAC_DO_TERM_TYPE = new Buffer( [ 255, 253, 24 ] ); +const IAC_DO_TERM_TYPE = Buffer.from( [ 255, 253, 24 ] ); class TelnetClientConnection extends EventEmitter { constructor(client) { @@ -103,7 +103,7 @@ class TelnetClientConnection extends EventEmitter { // let bufs = buffers(); - bufs.push(new Buffer( + bufs.push(Buffer.from( [ 255, // IAC 250, // SB @@ -113,8 +113,8 @@ class TelnetClientConnection extends EventEmitter { )); bufs.push( - new Buffer(this.client.term.termType), // e.g. "ansi" - new Buffer( [ 255, 240 ] ) // IAC, SE + Buffer.from(this.client.term.termType), // e.g. "ansi" + Buffer.from( [ 255, 240 ] ) // IAC, SE ); return bufs.toBuffer(); diff --git a/core/user.js b/core/user.js index 76c493ea..59f150bb 100644 --- a/core/user.js +++ b/core/user.js @@ -130,8 +130,8 @@ module.exports = class User { // // Use constant time comparison here for security feel-goods // - const passDkBuf = new Buffer(passDk, 'hex'); - const propsDkBuf = new Buffer(propsDk, 'hex'); + const passDkBuf = Buffer.from(passDk, 'hex'); + const propsDkBuf = Buffer.from(propsDk, 'hex'); if(passDkBuf.length !== propsDkBuf.length) { return callback(Errors.AccessDenied('Invalid password')); @@ -595,7 +595,7 @@ module.exports = class User { } static generatePasswordDerivedKey(password, salt, cb) { - password = new Buffer(password).toString('hex'); + password = Buffer.from(password).toString('hex'); crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', (err, dk) => { if(err) { diff --git a/core/uuid_util.js b/core/uuid_util.js index de64ec30..9af449dd 100644 --- a/core/uuid_util.js +++ b/core/uuid_util.js @@ -11,17 +11,17 @@ function createNamedUUID(namespaceUuid, key) { // https://github.com/download13/uuidv5/blob/master/uuid.js // if(!Buffer.isBuffer(namespaceUuid)) { - namespaceUuid = new Buffer(namespaceUuid); + namespaceUuid = Buffer.from(namespaceUuid); } if(!Buffer.isBuffer(key)) { - key = new Buffer(key); + key = Buffer.from(key); } let digest = createHash('sha1').update( Buffer.concat( [ namespaceUuid, key ] )).digest(); - let u = new Buffer(16); + let u = Buffer.alloc(16); // bbbb - bb - bb - bb - bbbbbb digest.copy(u, 0, 0, 4); // time_low From 0d7676a871d32b251b0e809c33b0abffc882cec6 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 28 Apr 2018 14:06:36 +0100 Subject: [PATCH 109/569] Buffer.alloc to init Buffer with a length --- core/archive_util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index 8ce2abc2..346e569e 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -82,7 +82,7 @@ module.exports = class ArchiveUtil { fileType.offset = fileType.offset || 0; // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well - const sigLen =fileType.offset + fileType.sig.length; + const sigLen = fileType.offset + fileType.sig.length; if(sigLen > this.longestSignature) { this.longestSignature = sigLen; } @@ -120,7 +120,7 @@ module.exports = class ArchiveUtil { return cb(err); } - const buf = Buffer.from(this.longestSignature); + const buf = Buffer.alloc(this.longestSignature); fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { if(err) { return cb(err); From b45a6a8743e7283223ce4b352ced9797f833faa7 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sat, 28 Apr 2018 21:39:04 +0100 Subject: [PATCH 110/569] * Buffer froms that should be allocs * Remove unnecessary Buffer fill after alloc * minor cleanup on fnv1a.js --- core/fnv1a.js | 2 +- core/ftn_mail_packet.js | 8 ++++---- core/ftn_util.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/fnv1a.js b/core/fnv1a.js index ce1fe105..1b8ece32 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -38,7 +38,7 @@ module.exports = class FNV1a { digest(encoding) { encoding = encoding || 'binary'; - let buf = Buffer.alloc(4); + const buf = Buffer.alloc(4); buf.writeInt32BE(this.hash & 0xffffffff, 0); return buf.toString(encoding); } diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 5db02227..c42d859b 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -273,7 +273,7 @@ function Packet(options) { }; this.getPacketHeaderBuffer = function(packetHeader) { - let buffer = Buffer.from(FTN_PACKET_HEADER_SIZE); + let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); @@ -311,7 +311,7 @@ function Packet(options) { }; this.writePacketHeader = function(packetHeader, ws) { - let buffer = Buffer.from(FTN_PACKET_HEADER_SIZE); + let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); @@ -747,7 +747,7 @@ function Packet(options) { async.waterfall( [ function prepareHeaderAndKludges(callback) { - const basicHeader = Buffer.from(34); + const basicHeader = Buffer.alloc(34); self.writeMessageHeader(message, basicHeader); // @@ -864,7 +864,7 @@ function Packet(options) { }; this.writeMessage = function(message, ws, options) { - let basicHeader = Buffer.from(34); + const basicHeader = Buffer.alloc(34); self.writeMessageHeader(message, basicHeader); ws.write(basicHeader); diff --git a/core/ftn_util.js b/core/ftn_util.js index e40d2687..d1f69e0f 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -46,7 +46,7 @@ exports.getQuotePrefix = getQuotePrefix; // See list here: https://github.com/Mithgol/node-fidonet-jam function stringToNullPaddedBuffer(s, bufLen) { - let buffer = Buffer.alloc(bufLen).fill(0x00); + let buffer = Buffer.alloc(bufLen); let enc = iconv.encode(s, 'CP437').slice(0, bufLen); for(let i = 0; i < enc.length; ++i) { buffer[i] = enc[i]; From f692c593e7f09bfc10be581779a7de2f95b75a65 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Apr 2018 12:01:34 +0100 Subject: [PATCH 111/569] Buffer.alloc should be .from --- core/archive_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/archive_util.js b/core/archive_util.js index 346e569e..e0d102d9 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -78,7 +78,7 @@ module.exports = class ArchiveUtil { Object.keys(Config.fileTypes).forEach(mimeType => { const fileType = Config.fileTypes[mimeType]; if(fileType.sig) { - fileType.sig = Buffer.alloc(fileType.sig, 'hex'); + fileType.sig = Buffer.from(fileType.sig, 'hex'); fileType.offset = fileType.offset || 0; // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well From a03305bc58f1e15be0e543a4020c7f2ad167fb5b Mon Sep 17 00:00:00 2001 From: David Stephens Date: Sun, 29 Apr 2018 12:01:51 +0100 Subject: [PATCH 112/569] Set node.js requirement to 8 LTS --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27c5332a..1155298c 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,6 @@ }, "devDependencies": {}, "engines": { - "node": ">=10" + "node": ">=8" } } From a13a0762a0cc6ee96b8684d993e184382ee4bc71 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Apr 2018 09:13:16 -0600 Subject: [PATCH 113/569] Fix some formatting --- docs/filebase/first-file-area.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index 5e83e8d7..7d7a133d 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -55,15 +55,15 @@ fileBase: { storageTags: { retro_pc: "retro_pc" - retro_pc_bbs: "retro_pc/bbs" + retro_pc_bbs: "retro_pc/bbs" } areas: { retro_pc: { - name: Retro PC - desc: Oldschool PC/DOS - storageTags: [ "retro_pc", "retro_pc_bbs" ] - } + name: Retro PC + desc: Oldschool PC/DOS + storageTags: [ "retro_pc", "retro_pc_bbs" ] + } } } ``` From 388e581b904a34ca56d203837f7e830f7140b36e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 12 May 2018 09:33:41 -0600 Subject: [PATCH 114/569] * Fix file transfer bug for WebSockets and SSH. Set/restore temp data handler belongs in base client. * Lint some files --- core/client.js | 10 +++++ core/servers/login/ssh.js | 18 ++++---- core/servers/login/telnet.js | 74 ++++++++++++++------------------- core/servers/login/websocket.js | 8 ++-- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/core/client.js b/core/client.js index a6c6b78a..d75b1b6d 100644 --- a/core/client.js +++ b/core/client.js @@ -100,6 +100,16 @@ function Client(/*input, output*/) { } }); + this.setTemporaryDirectDataHandler = function(handler) { + this.input.removeAllListeners('data'); + this.input.on('data', handler); + }; + + this.restoreDataHandler = function() { + this.input.removeAllListeners('data'); + this.input.on('data', this.dataHandler); + }; + // // Peek at incoming |data| and emit events for any special diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 80a99c7e..4ec57d2f 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -129,6 +129,10 @@ function SSHClient(clientConn) { } }); + this.dataHandler = function(data) { + self.emit('data', data); + }; + this.updateTermInfo = function(info) { // // From ssh2 docs: @@ -167,7 +171,7 @@ function SSHClient(clientConn) { self.log.info('SSH authentication success'); clientConn.on('session', accept => { - + const session = accept(); session.on('pty', function pty(accept, reject, info) { @@ -191,9 +195,7 @@ function SSHClient(clientConn) { self.setInputOutput(channel.stdin, channel.stdout); - channel.stdin.on('data', data => { - self.emit('data', data); - }); + channel.stdin.on('data', self.dataHandler); if(self.cachedPtyInfo) { self.updateTermInfo(self.cachedPtyInfo); @@ -207,7 +209,7 @@ function SSHClient(clientConn) { session.on('window-change', (accept, reject, info) => { self.log.debug(info, 'SSH window-change event'); - + self.updateTermInfo(info); }); @@ -237,13 +239,13 @@ exports.getModule = class SSHServerModule extends LoginServerModule { hostKeys : [ { key : fs.readFileSync(Config.loginServers.ssh.privateKeyPem), - passphrase : Config.loginServers.ssh.privateKeyPass, + passphrase : Config.loginServers.ssh.privateKeyPass, } ], ident : 'enigma-bbs-' + enigVersion + '-srv', - + // Note that sending 'banner' breaks at least EtherTerm! - debug : (sshDebugLine) => { + debug : (sshDebugLine) => { if(true === Config.loginServers.ssh.traceConnections) { Log.trace(`SSH: ${sshDebugLine}`); } diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 3c585c84..06f5199f 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -39,8 +39,8 @@ exports.TelnetClient = TelnetClient; * Document OPTIONS -- add any missing * Internally handle OPTIONS: * Some should be emitted generically - * Some shoudl be handled internally -- denied, handled, etc. - * + * Some shoudl be handled internally -- denied, handled, etc. + * * Allow term (ttype) to be set by environ sub negotiation @@ -67,7 +67,7 @@ const COMMANDS = { EL : 248, // Erase Line GA : 249, // Go Ahead SB : 250, // Start Sub-Negotiation Parameters - WILL : 251, // + WILL : 251, // WONT : 252, DO : 253, DONT : 254, @@ -101,7 +101,7 @@ const OPTIONS = { TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt //OUPUT_LINE_WIDTH : 8, - //OUTPUT_PAGE_SIZE : 9, // + //OUTPUT_PAGE_SIZE : 9, // //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 @@ -195,15 +195,15 @@ function unknownOption(bufs, i, event) { const OPTION_IMPLS = {}; // :TODO: fill in the rest... OPTION_IMPLS.NO_ARGS = -OPTION_IMPLS[OPTIONS.ECHO] = +OPTION_IMPLS[OPTIONS.ECHO] = OPTION_IMPLS[OPTIONS.STATUS] = -OPTION_IMPLS[OPTIONS.LINEMODE] = -OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = +OPTION_IMPLS[OPTIONS.LINEMODE] = +OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = OPTION_IMPLS[OPTIONS.AUTHENTICATION] = OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] = OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] = OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = -OPTION_IMPLS[OPTIONS.SEND_LOCATION] = +OPTION_IMPLS[OPTIONS.SEND_LOCATION] = OPTION_IMPLS[OPTIONS.ARE_YOU_THERE] = OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { event.buf = bufs.splice(0, i).toBuffer(); @@ -453,7 +453,7 @@ function parseCommand(bufs, i, event) { } event.buf = bufs.splice(0, 2).toBuffer(); - return event; + return event; } } @@ -486,16 +486,6 @@ function TelnetClient(input, output) { newEnvironRequested : false, }; - this.setTemporaryDirectDataHandler = function(handler) { - this.input.removeAllListeners('data'); - this.input.on('data', handler); - }; - - this.restoreDataHandler = function() { - this.input.removeAllListeners('data'); - this.input.on('data', this.dataHandler); - }; - this.dataHandler = function(b) { if(!Buffer.isBuffer(b)) { EnigAssert(false, `Cannot push non-buffer ${typeof b}`); @@ -516,15 +506,15 @@ function TelnetClient(input, output) { } EnigAssert(bufs.length > (i + 1)); - + if(i > 0) { self.emit('data', bufs.splice(0, i).toBuffer()); } i = parseBufs(bufs); - + if(MORE_DATA_REQUIRED === i) { - break; + break; } else if(i) { if(i.option) { self.emit(i.option, i); // "transmit binary", "echo", ... @@ -589,7 +579,7 @@ util.inherits(TelnetClient, baseClient.Client); // Telnet Command/Option handling /////////////////////////////////////////////////////////////////////////////// TelnetClient.prototype.handleTelnetEvent = function(evt) { - + if(!evt.command) { return this.connectionWarn( { evt : evt }, 'No command for event'); } @@ -611,7 +601,7 @@ TelnetClient.prototype.handleWillCommand = function(evt) { // // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html // - this.requestTerminalType(); + this.requestTerminalType(); } else if('new environment' === evt.option) { // // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html @@ -630,7 +620,7 @@ TelnetClient.prototype.handleWontCommand = function(evt) { this.sentDont[evt.option] = true; - if('new environment' === evt.option) { + if('new environment' === evt.option) { this.dont.new_environment(); } else { this.connectionTrace(evt, 'WONT'); @@ -693,16 +683,16 @@ TelnetClient.prototype.handleSbCommand = function(evt) { self.term.termHeight = parseInt(evt.envVars[name]); self.clearMciCache(); // term size changes = invalidate cache self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); - } else { + } else { if(name in self.term.env) { EnigAssert( - SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type, + 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' ); } else { @@ -719,7 +709,7 @@ TelnetClient.prototype.handleSbCommand = function(evt) { // self.term.termWidth = evt.width; self.term.termHeight = evt.height; - + if(evt.width > 0) { self.term.env.COLUMNS = evt.height; } @@ -752,11 +742,11 @@ TelnetClient.prototype.handleMiscCommand = function(evt) { if('ip' === evt.command) { // Interrupt Process (IP) this.log.debug('Interrupt Process (IP) - Ending'); - + this.input.end(); } else if('ayt' === evt.command) { this.output.write('\b'); - + this.log.debug('Are You There (AYT) - Replied "\\b"'); } else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) { this.log.debug({ evt : evt }, 'Ignoring command'); @@ -767,11 +757,11 @@ TelnetClient.prototype.handleMiscCommand = function(evt) { TelnetClient.prototype.requestTerminalType = function() { const buf = Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.TERMINAL_TYPE, - SB_COMMANDS.SEND, - COMMANDS.IAC, + COMMANDS.IAC, + COMMANDS.SB, + OPTIONS.TERMINAL_TYPE, + SB_COMMANDS.SEND, + COMMANDS.IAC, COMMANDS.SE ]); this.output.write(buf); }; @@ -790,15 +780,15 @@ TelnetClient.prototype.requestNewEnvironment = function() { return; } - const self = this; + const self = this; const bufs = buffers(); bufs.push(Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.NEW_ENVIRONMENT, + COMMANDS.IAC, + COMMANDS.SB, + OPTIONS.NEW_ENVIRONMENT, SB_COMMANDS.SEND ] - )); + )); for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { bufs.push(Buffer.from( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); @@ -828,7 +818,7 @@ TelnetClient.prototype.banner = function() { function Command(command, client) { this.command = COMMANDS[command.toUpperCase()]; - this.client = client; + this.client = client; } // Create Command objects with echo, transmit_binary, ... diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index 9e480ac9..ed12bf0b 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -30,6 +30,10 @@ function WebSocketClient(ws, req, serverType) { const self = this; + this.dataHandler = function(data) { + self.socketBridge.emit('data', data); + }; + // // This bridge makes accessible various calls that client sub classes // want to access on I/O socket @@ -65,9 +69,7 @@ function WebSocketClient(ws, req, serverType) { } }(ws); - ws.on('message', data => { - this.socketBridge.emit('data', data); - }); + ws.on('message', this.dataHandler); ws.on('close', () => { // we'll remove client connection which will in turn end() via our SocketBridge above From 9385cd2c936ded1556714a0b6428e9c0e0b96b63 Mon Sep 17 00:00:00 2001 From: SemperFu Date: Sat, 12 May 2018 15:59:28 -0400 Subject: [PATCH 115/569] Update telnet.js Spelling --- core/servers/login/telnet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 06f5199f..3004513d 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -39,7 +39,7 @@ exports.TelnetClient = TelnetClient; * Document OPTIONS -- add any missing * Internally handle OPTIONS: * Some should be emitted generically - * Some shoudl be handled internally -- denied, handled, etc. + * Some should be handled internally -- denied, handled, etc. * * Allow term (ttype) to be set by environ sub negotiation From 8a428e6f7485286859e9a25dff4c793be2b18a0f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 21 May 2018 20:36:34 -0600 Subject: [PATCH 116/569] oputil updates * oputil.js user ... now works more like other "action" based commands * add oputil.js user group .... for add/removal from groups --- core/oputil/oputil_help.js | 19 ++-- core/oputil/oputil_user.js | 208 ++++++++++++++++++++++++++----------- 2 files changed, 160 insertions(+), 67 deletions(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index c5e096b5..a560b17e 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -21,18 +21,19 @@ commands: fb file base management mb message base management `, - User : -`usage: optutil.js user --user USERNAME + User : +`usage: optutil.js user [] -valid args: - --user USERNAME specify username for further actions - --password PASS set new password - --delete delete user - --activate activate user - --deactivate deactivate user +actions: + pw USERNAME PASSWORD set password to PASSWORD for USERNAME + rm USERNAME permanantely removes USERNAME user from system + activate USERNAME sets USERNAME's status to active + deactivate USERNAME sets USERNAME's status to deactive + disable USERNAME sets USERNAME's status to disabled + group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP `, - Config : + Config : `usage: optutil.js config [] actions: diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index afe243bf..36762edb 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -7,65 +7,23 @@ const ExitCodes = require('./oputil_common.js').ExitCodes; const argv = require('./oputil_common.js').argv; const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; const getHelpFor = require('./oputil_help.js').getHelpFor; +const Errors = require('../enig_error.js').Errors; const async = require('async'); const _ = require('lodash'); exports.handleUserCommand = handleUserCommand; -function handleUserCommand() { - if(true === argv.help || !_.isString(argv.user) || 0 === argv.user.length) { - return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); - } - - if(_.isString(argv.password)) { - if(0 === argv.password.length) { - process.exitCode = ExitCodes.BAD_ARGS; - return console.error('Invalid password'); - } - - async.waterfall( - [ - function init(callback) { - initAndGetUser(argv.user, callback); - }, - function setNewPass(user, callback) { - user.setNewAuthCredentials(argv.password, function credsSet(err) { - if(err) { - process.exitCode = ExitCodes.ERROR; - callback(new Error('Failed setting password')); - } else { - callback(null); - } - }); - } - ], - function complete(err) { - if(err) { - console.error(err.message); - } else { - console.info('Password set'); - } - } - ); - } else if(argv.activate) { - setAccountStatus(argv.user, true); - } else if(argv.deactivate) { - setAccountStatus(argv.user, false); - } -} - function getUser(userName, cb) { const User = require('../../core/user.js'); - User.getUserIdAndName(argv.user, function userNameAndId(err, userId) { + User.getUserIdAndName(userName, (err, userId) => { if(err) { process.exitCode = ExitCodes.BAD_ARGS; - return cb(new Error('Failed to retrieve user')); - } else { - let u = new User(); - u.userId = userId; - return cb(null, u); + return cb(err); } + const u = new User(); + u.userId = userId; + return cb(null, u); }); } @@ -76,14 +34,14 @@ function initAndGetUser(userName, cb) { initConfigAndDatabases(callback); }, function getUserObject(callback) { - getUser(argv.user, (err, user) => { + getUser(userName, (err, user) => { if(err) { process.exitCode = ExitCodes.BAD_ARGS; return callback(err); } return callback(null, user); }); - } + } ], (err, user) => { return cb(err, user); @@ -91,23 +49,157 @@ function initAndGetUser(userName, cb) { ); } -function setAccountStatus(userName, active) { +function setAccountStatus(user, status) { + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + + const AccountStatus = require('../../core/user.js').AccountStatus; + const statusDesc = _.invert(AccountStatus)[status]; + user.persistProperty('account_status', status, err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } else { + console.info(`User status set to ${statusDesc}`); + } + }); +} + +function setUserPassword(user) { + if(argv._.length < 4) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + async.waterfall( [ - function init(callback) { - initAndGetUser(argv.user, callback); + function validate(callback) { + // :TODO: prompt if no password provided (more secure, no history, etc.) + const password = argv._[argv._.length - 1]; + if(0 === password.length) { + return callback(Errors.Invalid('Invalid password')); + } + return callback(null, password); }, - function activateUser(user, callback) { - const AccountStatus = require('../../core/user.js').AccountStatus; - user.persistProperty('account_status', active ? AccountStatus.active : AccountStatus.inactive, callback); + function set(password, callback) { + user.setNewAuthCredentials(password, err => { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + } + return callback(err); + }); } ], err => { if(err) { console.error(err.message); } else { - console.info('User ' + ((true === active) ? 'activated' : 'deactivated')); + console.info('New password set'); } } - ); + ); +} + +function removeUser(user) { + console.error('NOT YET IMPLEMENTED'); +} + +function modUserGroups(user) { + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + + let groupName = argv._[argv._.length - 1].replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" + let action = groupName[0]; // + or - + + if('-' === action || '+' === action) { + groupName = groupName.substr(1); + } + + action = action || '+'; + + if(0 === groupName.length) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + + // + // Groups are currently arbritary, so do a slight validation + // + if(!/[A-Za-z0-9]+/.test(groupName)) { + process.exitCode = ExitCodes.BAD_ARGS; + return console.error('Bad group name'); + } + + function done(err) { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + console.error(err.message); + } else { + console.info('User groups modified'); + } + } + + const UserGroup = require('../../core/user_group.js'); + if('-' === action) { + UserGroup.removeUserFromGroup(user.userId, groupName, done); + } else { + UserGroup.addUserToGroup(user.userId, groupName, done); + } +} + +function activateUser(user) { + const AccountStatus = require('../../core/user.js').AccountStatus; + return setAccountStatus(user, AccountStatus.active); +} + +function deactivateUser(user) { + const AccountStatus = require('../../core/user.js').AccountStatus; + return setAccountStatus(user, AccountStatus.inactive); +} + +function disableUser(user) { + const AccountStatus = require('../../core/user.js').AccountStatus; + return setAccountStatus(user, AccountStatus.disabled); +} + +function handleUserCommand() { + function errUsage() { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } + + if(true === argv.help) { + return errUsage(); + } + + const action = argv._[1]; + const usernameIdx = [ 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1; + const userName = argv._[usernameIdx]; + + if(!userName) { + return errUsage(); + } + + initAndGetUser(userName, (err, user) => { + if(err) { + process.exitCode = ExitCodes.ERROR; + return console.error(err.message); + } + + return ({ + pass : setUserPassword, + passwd : setUserPassword, + password : setUserPassword, + + rm : removeUser, + remove : removeUser, + del : removeUser, + delete : removeUser, + + activate : activateUser, + deactivate : deactivateUser, + disable : disableUser, + + group : modUserGroups, + }[action] || errUsage)(user); + }); } \ No newline at end of file From 39be44434a1c82ffacb031d9395645b9439e625c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 21 May 2018 20:39:52 -0600 Subject: [PATCH 117/569] Ensure all number groups work --- core/oputil/oputil_user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 36762edb..59813b3b 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -109,7 +109,7 @@ function modUserGroups(user) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } - let groupName = argv._[argv._.length - 1].replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" + let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" let action = groupName[0]; // + or - if('-' === action || '+' === action) { From 37e5948f6527d92e9f7049a68f097e02b163ec0c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 31 May 2018 20:58:24 -0600 Subject: [PATCH 118/569] Add lzx archive support via unlzx --- core/archive_util.js | 19 ++++++++++++++----- core/config.js | 24 ++++++++++++++++++++++++ core/mime_util.js | 1 + 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index e0d102d9..c3915389 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -207,7 +207,12 @@ module.exports = class ArchiveUtil { extractPath : extractPath, }; - const action = haveFileList ? 'extract' : 'decompress'; + let action = haveFileList ? 'extract' : 'decompress'; + if('extract' === action && !_.isObject(archiver[action])) { + // we're forced to do a full decompress + action = 'decompress'; + haveFileList = false; + } // we need to treat {fileList} special in that it should be broken up to 0:n args const args = archiver[action].args.map( arg => { @@ -222,7 +227,7 @@ module.exports = class ArchiveUtil { let proc; try { - proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts()); + proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath)); } catch(e) { return cb(e); } @@ -278,13 +283,17 @@ module.exports = class ArchiveUtil { }); } - getPtyOpts() { - return { - // :TODO: cwd + getPtyOpts(extractPath) { + const opts = { name : 'enigma-archiver', cols : 80, rows : 24, env : process.env, }; + if(extractPath) { + opts.cwd = extractPath; + } + // :TODO: set cwd to supplied temp path if not sepcific extract + return opts; } }; diff --git a/core/config.js b/core/config.js index 2564c256..b782531f 100644 --- a/core/config.js +++ b/core/config.js @@ -415,6 +415,12 @@ function getDefaultConfig() { offset : 2, archiveHandler : 'Lha', }, + 'application/x-lzx' : { + desc : 'LZX Archive', + sig : '4c5a5800', + offset : 0, + archiveHandler : 'Lzx', + }, 'application/x-7z-compressed' : { desc : '7-Zip Archive', sig : '377abcaf271c', @@ -473,6 +479,24 @@ function getDefaultConfig() { } }, + Lzx : { + // + // 'unlzx' command can be obtained from: + // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) + // * Source: http://xavprods.free.fr/lzx/ + // + decompress : { + cmd : 'unlzx', + // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first + args : [ '-x', '{archivePath}' ], + }, + list : { + cmd : 'unlzx', + args : [ '-v', '{archivePath}' ], + entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', + } + }, + Arj : { // // 'arj' command can be obtained from: diff --git a/core/mime_util.js b/core/mime_util.js index a110572c..b9a7c5af 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -16,6 +16,7 @@ function startup(cb) { const ADDITIONAL_EXT_MIMETYPES = { ans : 'text/x-ansi', gz : 'application/gzip', // not in mime-types 2.1.15 :( + lzx : 'application/x-lzx', // :TODO: submit to mime-types }; _.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => { From 881f9765a6c7a70945f70a7b874d6bf3bdab00ee Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 1 Jun 2018 19:10:42 -0600 Subject: [PATCH 119/569] Remove dead code, init MIME DB when doing file scan --- core/oputil/oputil_file_base.js | 78 ++------------------------------- 1 file changed, 3 insertions(+), 75 deletions(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index a2abbe99..68f190fb 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -263,81 +263,6 @@ function scanFileAreaForChanges(areaInfo, options, cb) { } ] ); - - /* - fileArea.scanFile( - fullPath, - { - areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag - }, - (err, fileEntry, dupeEntries) => { - if(err) { - console.info(`Error: ${err.message}`); - return nextFile(null); // try next anyway - } - - // - // We'll update the entry if the following conditions are met: - // * We have a single duplicate, and: - // * --update was passed or the existing entry's desc, - // longDesc, or est_release_year meta are blank/empty - // - if(argv.update && 1 === dupeEntries.length) { - const FileEntry = require('../../core/file_entry.js'); - const existingEntry = new FileEntry(); - - return existingEntry.load(dupeEntries[0].fileId, err => { - if(err) { - console.info('Dupe (cannot update)'); - return nextFile(null); - } - - // - // Update only if tags or desc changed - // - const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; - const tagsEq = _.isEqual(optTags, existingEntry.hashTags); - - if( tagsEq && - fileEntry.desc === existingEntry.desc && - fileEntry.descLong == existingEntry.descLong && - fileEntry.meta.est_release_year == existingEntry.meta.est_release_year) - { - console.info('Dupe'); - return nextFile(null); - } - - console.info('Dupe (updating)'); - - // don't allow overwrite of values if new version is blank - existingEntry.desc = fileEntry.desc || existingEntry.desc; - existingEntry.descLong = fileEntry.descLong || existingEntry.descLong; - - if(fileEntry.meta.est_release_year) { - existingEntry.meta.est_release_year = fileEntry.meta.est_release_year; - } - - updateTags(existingEntry); - - finalizeEntryAndPersist(true, existingEntry, descHandler, err => { - return nextFile(err); - }); - }); - } else if(dupeEntries.length > 0) { - console.info('Dupe'); - return nextFile(null); - } - - console.info('Done!'); - updateTags(fileEntry); - - finalizeEntryAndPersist(false, fileEntry, descHandler, err => { - return nextFile(err); - }); - } - ); - */ }); }, err => { return callback(err); @@ -521,6 +446,9 @@ function scanFileAreas() { function init(callback) { return initConfigAndDatabases(callback); }, + function initMime(callback) { + return require('../../core/mime_util.js').startup(callback); + }, function initGlobalDescHandler(callback) { // // If options.descFile is a String, it represents a FILE|PATH. We'll init From 83dd4402191ec336c07bf6411a312b694792282d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 1 Jun 2018 19:32:00 -0600 Subject: [PATCH 120/569] Lzx / unlzx info --- core/config.js | 1 + docs/configuration/archivers.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/core/config.js b/core/config.js index b782531f..070a6920 100644 --- a/core/config.js +++ b/core/config.js @@ -483,6 +483,7 @@ function getDefaultConfig() { // // 'unlzx' command can be obtained from: // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) + // * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html // * Source: http://xavprods.free.fr/lzx/ // decompress : { diff --git a/docs/configuration/archivers.md b/docs/configuration/archivers.md index 26755162..3d3f952a 100644 --- a/docs/configuration/archivers.md +++ b/docs/configuration/archivers.md @@ -20,6 +20,11 @@ The following archivers are pre-configured in ENiGMA½ as of this writing. Remem * Key: `Lha` * Homepage/package: `lhasa` on most *nix environments. See also https://fragglet.github.io/lhasa/ and http://www2m.biglobe.ne.jp/~dolphin/lha/lha-unix.htm +#### Lzx +* Formats: Amiga LZX +* Key: `Lzx` +* Homepage/package: `unlzx` under most *nix platforms ([Debian/Ubuntu](https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127), [RedHat](https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html), [Source](http://xavprods.free.fr/lzx/)) + #### Arj * Formats: .arj * Key: `Arj` From 70ce81c01ace8a2eb041000da33323f533254e21 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 1 Jun 2018 20:15:47 -0600 Subject: [PATCH 121/569] Fix bug with quote escaping in DB sanatizeString() --- core/database.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/database.js b/core/database.js index 2717a545..0998f738 100644 --- a/core/database.js +++ b/core/database.js @@ -72,6 +72,8 @@ function sanatizeString(s) { case '"' : case '\'' : + return `${c}${c}`; + case '\\' : case '%' : return `\\${c}`; From 95422f71bac3b27104726739ec1f7505799980d2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 1 Jun 2018 20:16:08 -0600 Subject: [PATCH 122/569] Fix possible SQL injection in file tags search --- core/file_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_entry.js b/core/file_entry.js index 169bbb74..f6b86c64 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -548,7 +548,7 @@ module.exports = class FileEntry { if(filter.tags && filter.tags.length > 0) { // build list of quoted tags; filter.tags comes in as a space and/or comma separated values - const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${tag}"` ).join(','); + const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanatizeString(tag)}"` ).join(','); appendWhereClause( `f.file_id IN ( From 3ecadebf9137b9e41e763d3c13d90628830671ae Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Jun 2018 16:06:04 -0600 Subject: [PATCH 123/569] Generic MIME types (file types) such as application/octet-stream can how have sub types for handlers (archive, info extract, ...) + Add Amiga DMS support via xdms --- core/archive_util.js | 82 +++++++++++++++++++++++++------------ core/config.js | 29 ++++++++++--- core/file_area_list.js | 14 ++++++- core/file_base_area.js | 17 ++++++-- docs/installation/manual.md | 6 +-- 5 files changed, 108 insertions(+), 40 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index c3915389..71bad641 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -11,6 +11,7 @@ const resolveMimeType = require('./mime_util.js').resolveMimeType; const fs = require('graceful-fs'); const _ = require('lodash'); const pty = require('node-pty'); +const paths = require('path'); let archiveUtil; @@ -75,32 +76,56 @@ module.exports = class ArchiveUtil { } if(_.isObject(Config.fileTypes)) { + const updateSig = (ft) => { + ft.sig = Buffer.from(ft.sig, 'hex'); + ft.offset = ft.offset || 0; + + // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well + const sigLen = ft.offset + ft.sig.length; + if(sigLen > this.longestSignature) { + this.longestSignature = sigLen; + } + }; + Object.keys(Config.fileTypes).forEach(mimeType => { const fileType = Config.fileTypes[mimeType]; - if(fileType.sig) { - fileType.sig = Buffer.from(fileType.sig, 'hex'); - fileType.offset = fileType.offset || 0; - - // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well - const sigLen = fileType.offset + fileType.sig.length; - if(sigLen > this.longestSignature) { - this.longestSignature = sigLen; - } + if(Array.isArray(fileType)) { + fileType.forEach(ft => { + if(ft.sig) { + updateSig(ft); + } + }); + } else if(fileType.sig) { + updateSig(fileType); } }); } } - getArchiver(mimeTypeOrExtension) { - mimeTypeOrExtension = resolveMimeType(mimeTypeOrExtension); + getArchiver(mimeTypeOrExtension, justExtention) { + const mimeType = resolveMimeType(mimeTypeOrExtension); - if(!mimeTypeOrExtension) { // lookup returns false on failure + if(!mimeType) { // lookup returns false on failure return; } - const archiveHandler = _.get( Config, [ 'fileTypes', mimeTypeOrExtension, 'archiveHandler'] ); - if(archiveHandler) { - return _.get( Config, [ 'archives', 'archivers', archiveHandler ] ); + let fileType = _.get(Config, [ 'fileTypes', mimeType ] ); + + if(Array.isArray(fileType)) { + if(!justExtention) { + // need extention for lookup; ambiguous as-is :( + return; + } + // further refine by extention + fileType = fileType.find(ft => justExtention === ft.ext); + } + + if(!_.isObject(fileType)) { + return; + } + + if(fileType.archiveHandler) { + return _.get( Config, [ 'archives', 'archivers', fileType.archiveHandler ] ); } } @@ -127,18 +152,21 @@ module.exports = class ArchiveUtil { } const archFormat = _.findKey(Config.fileTypes, fileTypeInfo => { - if(!fileTypeInfo.sig) { - return false; - } + const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ]; + return fileTypeInfos.find(fti => { + if(!fti.sig || !fti.archiveHandler) { + return false; + } - const lenNeeded = fileTypeInfo.offset + fileTypeInfo.sig.length; + const lenNeeded = fti.offset + fti.sig.length; - if(bytesRead < lenNeeded) { - return false; - } + if(bytesRead < lenNeeded) { + return false; + } - const comp = buf.slice(fileTypeInfo.offset, fileTypeInfo.offset + fileTypeInfo.sig.length); - return (fileTypeInfo.sig.equals(comp)); + const comp = buf.slice(fti.offset, fti.offset + fti.sig.length); + return (fti.sig.equals(comp)); + }); }); return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); @@ -162,7 +190,7 @@ module.exports = class ArchiveUtil { } compressTo(archType, archivePath, files, cb) { - const archiver = this.getArchiver(archType); + const archiver = this.getArchiver(archType, paths.extname(archivePath)); if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); @@ -196,7 +224,7 @@ module.exports = class ArchiveUtil { haveFileList = true; } - const archiver = this.getArchiver(archType); + const archiver = this.getArchiver(archType, paths.extname(archivePath)); if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); @@ -236,7 +264,7 @@ module.exports = class ArchiveUtil { } listEntries(archivePath, archType, cb) { - const archiver = this.getArchiver(archType); + const archiver = this.getArchiver(archType, paths.extname(archivePath)); if(!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); diff --git a/core/config.js b/core/config.js index 070a6920..70406e47 100644 --- a/core/config.js +++ b/core/config.js @@ -297,6 +297,16 @@ function getDefaultConfig() { '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', '--metadatadate', '--xmptoolkit' ] + }, + XDMS2Desc : { + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'd', '{filePath}' ] + }, + XDMS2LongDesc : { + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'f', '{filePath}' ] } }, @@ -426,13 +436,20 @@ function getDefaultConfig() { sig : '377abcaf271c', offset : 0, archiveHandler : '7Zip', - } + }, - // :TODO: update archives::formats to fall here - // * archive handler -> archiveHandler (consider archive if archiveHandler present) - // * sig, offset, ... - // * mime-db -> exts lookup - // * + // + // Generics that need further mapping + // + 'application/octet-stream' : [ + { + desc : 'Amiga DISKMASHER', + sig : '444d5321', // DMS! + ext : '.dms', + shortDescUtil : 'XDMS2Desc', + longDescUtil : 'XDMS2LongDesc', + } + ] }, archives : { diff --git a/core/file_area_list.js b/core/file_area_list.js index f42bf93e..b3d55e2c 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -24,6 +24,7 @@ const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi; const async = require('async'); const _ = require('lodash'); const moment = require('moment'); +const paths = require('path'); exports.moduleInfo = { name : 'File Area List', @@ -252,7 +253,18 @@ exports.getModule = class FileAreaList extends MenuModule { if(entryInfo.archiveType) { const mimeType = resolveMimeType(entryInfo.archiveType); - entryInfo.archiveTypeDesc = mimeType ? _.get(Config, [ 'fileTypes', mimeType, 'desc' ] ) || mimeType : entryInfo.archiveType; + let desc; + if(mimeType) { + let fileType = _.get(Config, [ 'fileTypes', mimeType ] ); + + if(Array.isArray(fileType)) { + // further refine by extention + fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext); + } + desc = fileType && fileType.desc; + } + entryInfo.archiveTypeDesc = desc || mimeType || entryInfo.archiveType; + //entryInfo.archiveTypeDesc = mimeType ? _.get(Config, [ 'fileTypes', mimeType, 'desc' ] ) || mimeType : entryInfo.archiveType; } else { entryInfo.archiveTypeDesc = 'N/A'; } diff --git a/core/file_base_area.js b/core/file_base_area.js index 5f3b974a..9cfb1cb9 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -487,8 +487,19 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c ); } -function getInfoExtractUtilForDesc(mimeType, descType) { - let util = _.get(Config, [ 'fileTypes', mimeType, `${descType}DescUtil` ]); +function getInfoExtractUtilForDesc(mimeType, filePath, descType) { + let fileType = _.get(Config, [ 'fileTypes', mimeType ] ); + + if(Array.isArray(fileType)) { + // further refine by extention + fileType = fileType.find(ft => paths.extname(filePath) === ft.ext); + } + + if(!_.isObject(fileType)) { + return; + } + + let util = _.get(fileType, `${descType}DescUtil`); if(!_.isString(util)) { return; } @@ -508,7 +519,7 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { } async.eachSeries( [ 'short', 'long' ], (descType, nextDesc) => { - const util = getInfoExtractUtilForDesc(mimeType, descType); + const util = getInfoExtractUtilForDesc(mimeType, filePath, descType); if(!util) { return nextDesc(null); } diff --git a/docs/installation/manual.md b/docs/installation/manual.md index 9958c2e3..f03df762 100644 --- a/docs/installation/manual.md +++ b/docs/installation/manual.md @@ -50,7 +50,7 @@ ENiGMA BBS makes use of a few packages for unarchiving and modem support. They'r running ENiGMA, but without them you'll miss certain functionality. Once installed, they should be made available on your system path. -| Package | Description | Ubuntu Package | CentOS Package Name | Windows Package | +| Package | Description | Debian/Ubuntu Package (APT/DEP) | Red Hat Package (YUM/RPM) | Windows Package | |------------|-----------------------------------|--------------------------------------------|---------------------------------------------------|------------------------------------------------------------------| | arj | Unpacking arj archives | `arj` | n/a, binaries [here](http://arj.sourceforge.net/) | [ARJ](http://arj.sourceforge.net/) | | 7zip | Unpacking zip, rar, archives | `p7zip-full` | `p7zip-full` | [7-zip](http://www.7-zip.org/) | @@ -58,8 +58,8 @@ available on your system path. | Rar | Unpacking rar archives | `unrar` | n/a, binaries [here](https://www.rarlab.com/download.htm) | Unknown | | lrzsz | sz/rz: X/Y/Z modem support | `lrzsz` | `lrzsz` | Unknown | | sexyz | SexyZ modem support | [sexyz](https://l33t.codes/outgoing/sexyz) | [sexyz](https://l33t.codes/outgoing/sexyz) | Available with [Synchronet](http://wiki.synchro.net/install:win) | - - - exiftool & other external tools +| exiftool | [ExifTool](https://www.sno.phy.queensu.ca/~phil/exiftool/) | libimage-exiftool-perl | perl-Image-ExifTool | Unknown +| xdms | Unpack/view Amiga DMS | [xdms](http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html) | xdms | Unknown ## Config Files From ccf29ea8d4ced5900d990aae3ab87657ee3d1051 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Jun 2018 17:09:43 -0600 Subject: [PATCH 124/569] Force overwrite when extracting lha archives - they can contain dupes! --- core/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/config.js b/core/config.js index 70406e47..63e52032 100644 --- a/core/config.js +++ b/core/config.js @@ -483,7 +483,7 @@ function getDefaultConfig() { // decompress : { cmd : 'lha', - args : [ '-ew={extractPath}', '{archivePath}' ], + args : [ '-efw={extractPath}', '{archivePath}' ], }, list : { cmd : 'lha', @@ -492,7 +492,7 @@ function getDefaultConfig() { }, extract : { cmd : 'lha', - args : [ '-ew={extractPath}', '{archivePath}', '{fileList}' ] + args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] } }, From 57ecac535072ae26ec5fb0f53997160a9dc5bc58 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Jun 2018 20:51:09 -0600 Subject: [PATCH 125/569] Add ESC support (actually works) --- core/telnet_bridge.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 150996dd..36d12f5d 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -96,6 +96,15 @@ class TelnetClientConnection extends EventEmitter { } } + destroy() { + if(this.bridgeConnection) { + this.bridgeConnection.destroy(); + this.bridgeConnection.removeAllListeners(); + this.restorePipe(); + this.emit('end'); + } + } + getTermTypeNegotiationBuffer() { // // Create a TERMINAL-TYPE sub negotiation buffer using the @@ -153,7 +162,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { self.client.term.write(resetScreen()); self.client.term.write( - ` Connecting to ${connectOpts.host}, please wait...\n` + ` Connecting to ${connectOpts.host}, please wait...\n (Press ESC to cancel)\n` ); const telnetConnection = new TelnetClientConnection(self.client); @@ -161,7 +170,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { const connectionKeyPressHandler = (ch, key) => { if('escape' === key.name) { self.client.removeListener('key press', connectionKeyPressHandler); - telnetConnection.disconnect(); + telnetConnection.destroy(); } }; From 2eb07aebb816a94076eb3f6447cf0450e1b3b5c9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Jun 2018 21:13:27 -0600 Subject: [PATCH 126/569] Dep. upgrades --- package-lock.json | 167 ++++++++++++++++++++++++---------------------- package.json | 26 ++++---- 2 files changed, 101 insertions(+), 92 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e52f2b9..c0ba359d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,9 +80,9 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", "requires": { "lodash": "4.17.10" } @@ -206,16 +206,13 @@ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, - "buffers": { - "version": "github:NuSkooler/node-buffers#cd0855598f7048b02f0a51c90e22573973e9e2c2" - }, "bunyan": { "version": "1.8.12", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", "requires": { "dtrace-provider": "0.8.6", - "moment": "2.22.1", + "moment": "2.22.2", "mv": "2.1.1", "safe-json-stringify": "1.1.0" } @@ -236,6 +233,14 @@ "unset-value": "1.0.0" } }, + "capture-exit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", + "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", + "requires": { + "rsvp": "3.6.2" + } + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -246,11 +251,6 @@ "supports-color": "5.4.0" } }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -464,16 +464,6 @@ } } }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "requires": { - "chardet": "0.4.2", - "iconv-lite": "0.4.21", - "tmp": "0.0.33" - } - }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -584,9 +574,9 @@ } }, "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", "requires": { "graceful-fs": "4.1.11", "jsonfile": "4.0.0", @@ -1141,9 +1131,9 @@ "integrity": "sha512-1oGkOq4sssz7HFZ8Is9HuTR47r8gSC46qAzQxVlAkj0lNKpS+W5Lv2eci+c5+fFqL+Idtj5EvprFreUwH29a8A==" }, "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", "requires": { "safer-buffer": "2.1.2" } @@ -1163,23 +1153,48 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.0.0.tgz", + "integrity": "sha512-tISQWRwtcAgrz+SHPhTH7d3e73k31gsOy6i1csonLc0u1dVK/wYvuOnFeiWqC5OXFIYbmrIFInef31wbT8MEJg==", "requires": { "ansi-escapes": "3.1.0", "chalk": "2.4.1", "cli-cursor": "2.1.0", "cli-width": "2.2.0", - "external-editor": "2.2.0", + "external-editor": "3.0.0", "figures": "2.0.0", "lodash": "4.17.10", "mute-stream": "0.0.7", "run-async": "2.3.0", - "rxjs": "5.5.10", + "rxjs": "6.2.0", "string-width": "2.1.1", "strip-ansi": "4.0.0", "through": "2.3.8" + }, + "dependencies": { + "chardet": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.5.0.tgz", + "integrity": "sha512-9ZTaoBaePSCFvNlNGrsyI8ZVACP2svUtq0DkM7t4K2ClAa96sqOIRjAzDTc8zXzFt1cZR46rRzLTiHFSJ+Qw0g==" + }, + "external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-mpkfj0FEdxrIhOC04zk85X7StNtr0yXnG7zCb+8ikO8OJi2jsHh5YGoknNTyXgsbHOf1WOOcVU3kPFWT2WgCkQ==", + "requires": { + "chardet": "0.5.0", + "iconv-lite": "0.4.23", + "tmp": "0.0.33" + } + }, + "rxjs": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.0.tgz", + "integrity": "sha512-qBzf5uu6eOKiCZuAE0SgZ0/Qp+l54oeVxFfC2t+mJ2SFI6IB8gmMdJHs5DUMu5kqifqcCtsKS2XHjhZu6RKvAw==", + "requires": { + "tslib": "1.9.2" + } + } } }, "is-accessor-descriptor": { @@ -1469,9 +1484,9 @@ } }, "moment": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", - "integrity": "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==" + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" }, "ms": { "version": "2.0.0", @@ -1538,9 +1553,9 @@ } }, "nodemailer": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz", - "integrity": "sha512-SD4uuX7NMzZ5f5m1XHDd13J4UC3SmdJk8DsmU1g6Nrs5h3x9LcXr6EBPZIqXRJ3LrF7RdklzGhZRF/TuylTcLg==" + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.5.tgz", + "integrity": "sha512-+bt+BgmnOXDz1uIaWXfXuTESth8UHkhtu7+X8+X2W+CHAn0AuuCyCk854qnathYQLWEC2jkpx7/pkVHcfmLKDw==" }, "normalize-path": { "version": "2.1.1", @@ -1728,6 +1743,11 @@ "resolved": "https://registry.npmjs.org/rlogin/-/rlogin-1.0.0.tgz", "integrity": "sha1-2wcyKzEhkSZiXZ0KqYctfr6KxAM=" }, + "rsvp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", + "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==" + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", @@ -1736,19 +1756,6 @@ "is-promise": "2.1.0" } }, - "rxjs": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.10.tgz", - "integrity": "sha512-SRjimIDUHJkon+2hFo7xnvNC4ZEHGzCRwh9P7nzX3zPkCGFEg/tuElrNR7L/rZMagnK2JeH2jQwPRpmyXyLB6A==", - "requires": { - "symbol-observable": "1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "safe-json-stringify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz", @@ -1769,11 +1776,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sane": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.0.tgz", - "integrity": "sha512-glfKd7YH4UCrh/7dD+UESsr8ylKWRE7UQPoXuz28FgmcF0ViJQhCTCCZHICRKxf8G8O1KdLEn20dcICK54c7ew==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", + "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", "requires": { "anymatch": "2.0.0", + "capture-exit": "1.2.0", "exec-sh": "0.2.1", "fb-watchman": "2.0.0", "fsevents": "1.2.3", @@ -2368,21 +2376,23 @@ } }, "ssh2": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.5.5.tgz", - "integrity": "sha1-x3gezS7OcwSiU89iD6taXCK7IjU=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.6.1.tgz", + "integrity": "sha512-fNvocq+xetsaAZtBG/9Vhh0GDjw1jQeW7Uq/DPh4fVrJd0XxSfXAqBjOGVk4o2jyWHvyC6HiaPFpfHlR12coDw==", "requires": { - "ssh2-streams": "0.1.20" - } - }, - "ssh2-streams": { - "version": "0.1.20", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.1.20.tgz", - "integrity": "sha1-URGNFUVV31Rp7h9n4M8efoosDjo=", - "requires": { - "asn1": "0.2.3", - "semver": "5.5.0", - "streamsearch": "0.1.2" + "ssh2-streams": "0.2.1" + }, + "dependencies": { + "ssh2-streams": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.2.1.tgz", + "integrity": "sha512-3zCOsmunh1JWgPshfhKmBCL3lUtHPoh+a/cyQ49Ft0Q0aF7xgN06b76L+oKtFi0fgO57FLjFztb1GlJcEZ4a3Q==", + "requires": { + "asn1": "0.2.3", + "semver": "5.5.0", + "streamsearch": "0.1.2" + } + } } }, "static-extend": { @@ -2434,11 +2444,6 @@ "has-flag": "3.0.0" } }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" - }, "temptmp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.0.0.tgz", @@ -2511,6 +2516,11 @@ "utf8-byte-length": "1.0.4" } }, + "tslib": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", + "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", @@ -2635,12 +2645,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", - "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.0.tgz", + "integrity": "sha512-c18dMeW+PEQdDFzkhDsnBAlS4Z8KGStBQQUcQ5mf7Nf689jyGk0594L+i9RaQuf4gog6SvWLJorz2NfSaqxZ7w==", "requires": { - "async-limiter": "1.0.0", - "safe-buffer": "5.1.2" + "async-limiter": "1.0.0" } }, "xxhash": { diff --git a/package.json b/package.json index 1155298c..8b6cf110 100644 --- a/package.json +++ b/package.json @@ -22,35 +22,35 @@ "retro" ], "dependencies": { - "async": "^2.5.0", + "async": "^2.6.1", "binary-parser": "^1.3.2", "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "fs-extra": "^5.0.0", + "fs-extra": "^6.0.1", "glob": "^7.1.2", "graceful-fs": "^4.1.11", "hashids": "^1.1.1", - "hjson": "^3.1.0", - "iconv-lite": "^0.4.18", - "inquirer": "^5.0.0", + "hjson": "^3.1.1", + "iconv-lite": "^0.4.23", + "inquirer": "^6.0.0", "later": "1.2.0", - "lodash": "^4.17.4", - "mime-types": "^2.1.17", + "lodash": "^4.17.10", + "mime-types": "^2.1.18", "minimist": "1.2.x", - "moment": "^2.20.1", + "moment": "^2.22.2", "node-pty": "^0.7.4", - "nodemailer": "^4.4.1", + "nodemailer": "^4.6.5", "rlogin": "^1.0.0", - "sane": "^2.2.0", + "sane": "^2.5.2", "sanitize-filename": "^1.6.1", "sqlite3": "^4.0.0", "sqlite3-trans": "^1.2.0", - "ssh2": "^0.5.5", + "ssh2": "^0.6.1", "temptmp": "^1.0.0", - "uuid": "^3.1.0", + "uuid": "^3.2.1", "uuid-parse": "^1.0.0", - "ws": "^4.0.0", + "ws": "^5.2.0", "xxhash": "^0.2.4", "yazl": "^2.4.2" }, From b273101b61e628765d9faf6ef9c1dd3b3902af8b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 3 Jun 2018 17:00:54 -0600 Subject: [PATCH 127/569] Work on Events system: + system_event.js + codes.l33t.enigma.system.user_upload and codes.l33t.enigma.system.user_download events --- core/system_events.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 core/system_events.js diff --git a/core/system_events.js b/core/system_events.js new file mode 100644 index 00000000..706ad8a5 --- /dev/null +++ b/core/system_events.js @@ -0,0 +1,14 @@ +/* jslint node: true */ +'use strict'; + +module.exports = { + ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } + ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } + TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } + + // User - includes { user, ...} + UserLogin : 'codes.l33t.enigma.system.user_login', + UserLogoff : 'codes.l33t.enigma.system.user_logoff', + UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } + UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } +}; \ No newline at end of file From c142a9c3d31a06fb32e29b7e14fc2b5afaa16b3a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 3 Jun 2018 17:02:28 -0600 Subject: [PATCH 128/569] Work on Events missed files (see prev) --- core/client_connections.js | 10 ++++++-- core/connect.js | 2 +- core/download_queue.js | 16 ++++++++---- core/events.js | 9 ++----- core/file_area_web.js | 41 ++++++++++++++++-------------- core/file_base_user_list_export.js | 2 +- core/file_transfer.js | 12 ++++++++- core/upload.js | 13 +++++++++- 8 files changed, 68 insertions(+), 37 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index d81d0922..176b027a 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -77,7 +77,10 @@ function addNewClient(client, clientSock) { client.log.info(connInfo, 'Client connected'); - Events.emit('codes.l33t.enigma.system.connected', { client : client, connectionCount : clientConnections.length } ); + Events.emit( + Events.getSystemEvents().ClientConnected, + { client : client, connectionCount : clientConnections.length } + ); return id; } @@ -97,7 +100,10 @@ function removeClient(client) { 'Client disconnected' ); - Events.emit('codes.l33t.enigma.system.disconnected', { client : client, connectionCount : clientConnections.length } ); + Events.emit( + Events.getSystemEvents().ClientDisconnected, + { client : client, connectionCount : clientConnections.length } + ); } } diff --git a/core/connect.js b/core/connect.js index 8593dff8..51ee4e37 100644 --- a/core/connect.js +++ b/core/connect.js @@ -177,7 +177,7 @@ function connectEntry(client, nextMenu) { displayBanner(term); // fire event - Events.emit('codes.l33t.enigma.system.term_detected', { client : client } ); + Events.emit(Events.getSystemEvents().TermDetected, { client : client } ); setTimeout( () => { return client.menuStack.goto(nextMenu); diff --git a/core/download_queue.js b/core/download_queue.js index 0f45b04d..0c31b13c 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -1,7 +1,10 @@ /* jslint node: true */ 'use strict'; -const FileEntry = require('./file_entry.js'); +const FileEntry = require('./file_entry.js'); + +// deps +const { partition } = require('lodash'); module.exports = class DownloadQueue { constructor(client) { @@ -24,21 +27,22 @@ module.exports = class DownloadQueue { this.client.user.downloadQueue = []; } - toggle(fileEntry) { + toggle(fileEntry, systemFile=false) { if(this.isQueued(fileEntry)) { this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); } else { - this.add(fileEntry); + this.add(fileEntry, systemFile); } } - add(fileEntry) { + add(fileEntry, systemFile=false) { this.client.user.downloadQueue.push({ fileId : fileEntry.fileId, areaTag : fileEntry.areaTag, fileName : fileEntry.fileName, path : fileEntry.filePath, byteSize : fileEntry.meta.byte_size || 0, + systemFile : systemFile, }); } @@ -47,7 +51,9 @@ module.exports = class DownloadQueue { fileIds = [ fileIds ]; } - this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => ( -1 === fileIds.indexOf(e.fileId) ) ); + const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); + this.client.user.downloadQueue = remain; + return removed; } isQueued(entryOrId) { diff --git a/core/events.js b/core/events.js index b1e15c21..e50c5723 100644 --- a/core/events.js +++ b/core/events.js @@ -4,25 +4,20 @@ const paths = require('path'); const events = require('events'); const Log = require('./logger.js').log; +const SystemEvents = require('./system_events.js'); // deps const _ = require('lodash'); const async = require('async'); const glob = require('glob'); -const SYSTEM_EVENTS = { - ClientConnected : 'codes.l33t.enigma.system.connected', - ClientDisconnected : 'codes.l33t.enigma.system.disconnected', - TermDetected : 'codes.l33t.enigma.term_detected', -}; - module.exports = new class Events extends events.EventEmitter { constructor() { super(); } getSystemEvents() { - return SYSTEM_EVENTS; + return SystemEvents; } addListener(event, listener) { diff --git a/core/file_area_web.js b/core/file_area_web.js index b8a630fc..928be942 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -14,6 +14,7 @@ const User = require('./user.js'); const Log = require('./logger.js').log; const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const Events = require('./events.js'); // deps const hashids = require('hashids'); @@ -337,7 +338,7 @@ class FileAreaWebAccess { resp.on('finish', () => { // transfer completed fully - this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size); + this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); }); const headers = { @@ -382,24 +383,21 @@ class FileAreaWebAccess { ); }, function loadFileEntries(fileIds, callback) { - const filePaths = []; - async.eachSeries(fileIds, (fileId, nextFileId) => { + async.map(fileIds, (fileId, nextFileId) => { const fileEntry = new FileEntry(); fileEntry.load(fileId, err => { - if(!err) { - filePaths.push(fileEntry.filePath); - } - return nextFileId(err); + return nextFileId(err, fileEntry); }); - }, err => { + }, (err, fileEntries) => { if(err) { - return callback(Errors.DoesNotExist('Coudl not load file IDs for batch')); + return callback(Errors.DoesNotExist('Could not load file IDs for batch')); } - return callback(null, filePaths); + return callback(null, fileEntries); }); }, - function createAndServeStream(filePaths, callback) { + function createAndServeStream(fileEntries, callback) { + const filePaths = fileEntries.map(fe => fe.filePath); Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request'); const zipFile = new yazl.ZipFile(); @@ -430,7 +428,7 @@ class FileAreaWebAccess { resp.on('finish', () => { // transfer completed fully - self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize); + self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); }); const batchFileName = `batch_${servedItem.hashId}.zip`; @@ -457,7 +455,7 @@ class FileAreaWebAccess { ); } - updateDownloadStatsForUserIdAndSystem(userId, dlBytes, cb) { + updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) { async.waterfall( [ function fetchActiveUser(callback) { @@ -477,14 +475,19 @@ class FileAreaWebAccess { StatLog.incrementSystemStat('dl_total_count', 1); StatLog.incrementSystemStat('dl_total_bytes', dlBytes); + return callback(null, user); + }, + function sendEvent(user, callback) { + Events.emit( + Events.getSystemEvents().UserDownload, + { + user : user, + files : fileEntries, + } + ); return callback(null); } - ], - err => { - if(cb) { - return cb(err); - } - } + ] ); } } diff --git a/core/file_base_user_list_export.js b/core/file_base_user_list_export.js index 2dba52ad..0b30d582 100644 --- a/core/file_base_user_list_export.js +++ b/core/file_base_user_list_export.js @@ -223,7 +223,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { if(!err) { // queue it! const dlQueue = new DownloadQueue(self.client); - dlQueue.add(newEntry); + dlQueue.add(newEntry, true); // true=systemFile // clean up after ourselves when the session ends const thisClientId = self.client.session.id; diff --git a/core/file_transfer.js b/core/file_transfer.js index 19388bab..a7aee20c 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -10,6 +10,7 @@ const DownloadQueue = require('./download_queue.js'); const StatLog = require('./stat_log.js'); const FileEntry = require('./file_entry.js'); const Log = require('./logger.js').log; +const Events = require('./events.js'); // deps const async = require('async'); @@ -545,7 +546,16 @@ exports.getModule = class TransferFileModule extends MenuModule { if(sentFileIds.length > 0) { // remove items we sent from the D/L queue const dlQueue = new DownloadQueue(self.client); - dlQueue.removeItems(sentFileIds); + const dlFileEntries = dlQueue.removeItems(sentFileIds); + + // fire event for downloaded entries + Events.emit( + Events.getSystemEvents().UserDownload, + { + user : self.client.user, + files : dlFileEntries + } + ); self.sentFileIds = sentFileIds; } diff --git a/core/upload.js b/core/upload.js index b4130433..4ff6b3fb 100644 --- a/core/upload.js +++ b/core/upload.js @@ -16,6 +16,7 @@ const Log = require('./logger.js').log; const Errors = require('./enig_error.js').Errors; const FileEntry = require('./file_entry.js'); const isAnsi = require('./string_util.js').isAnsi; +const Events = require('./events.js'); // deps const async = require('async'); @@ -567,8 +568,18 @@ exports.getModule = class UploadModule extends MenuModule { // here as I/O can take quite a bit of time. Log any failures. // self.moveAndPersistUploadsToDatabase(scanResults.newEntries); - return callback(null); + return callback(null, scanResults.newEntries); }, + function sendEvent(uploadedEntries, callback) { + Events.emit( + Events.getSystemEvents().UserUpload, + { + user : self.client.user, + files : uploadedEntries, + } + ); + return callback(null); + } ], err => { if(err) { From 0ae9d0d1434797bcb0922f16cab97efcfb52b3fc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 3 Jun 2018 17:59:16 -0600 Subject: [PATCH 129/569] + User login and logoff events --- core/client_connections.js | 4 ++++ core/user.js | 3 ++- core/user_login.js | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/client_connections.js b/core/client_connections.js index 176b027a..121ea0fa 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -100,6 +100,10 @@ function removeClient(client) { 'Client disconnected' ); + if(client.user && client.user.isValid()) { + Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } ); + } + Events.emit( Events.getSystemEvents().ClientDisconnected, { client : client, connectionCount : clientConnections.length } diff --git a/core/user.js b/core/user.js index 59f150bb..16f84e8b 100644 --- a/core/user.js +++ b/core/user.js @@ -67,7 +67,8 @@ module.exports = class User { return false; } - return this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2 && this.prop_name.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2; + return ((this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2) && + (this.properties.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2)); } isRoot() { diff --git a/core/user_login.js b/core/user_login.js index 37c3b306..8e0e7404 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -6,6 +6,7 @@ const setClientTheme = require('./theme.js').setClientTheme; const clientConnections = require('./client_connections.js').clientConnections; const StatLog = require('./stat_log.js'); const logger = require('./logger.js'); +const Events = require('./events.js'); // deps const async = require('async'); @@ -59,6 +60,8 @@ function userLogin(client, username, password, cb) { client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username }); client.log.info('Successful login'); + Events.emit(Events.getSystemEvents().UserLogin, { user } ); + async.parallel( [ function setTheme(callback) { From fbe87640c5eca185e328e137c5b70aa8b65c350a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 3 Jun 2018 19:58:05 -0600 Subject: [PATCH 130/569] + New user event --- core/system_events.js | 12 +++++++++--- core/user.js | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/system_events.js b/core/system_events.js index 706ad8a5..133561cb 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -2,13 +2,19 @@ 'use strict'; module.exports = { - ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } - ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } + ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } + ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } // User - includes { user, ...} + NewUser : 'codes.l33t.enigma.system.new_user', UserLogin : 'codes.l33t.enigma.system.user_login', UserLogoff : 'codes.l33t.enigma.system.user_logoff', UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } -}; \ No newline at end of file + + // NYI below here: + UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', + UserSendMail : 'codes.l33t.enigma.system.user_send_mail', + UserSendRunDoor : 'codes.l33t.enigma.system.user_run_door', +}; diff --git a/core/user.js b/core/user.js index 16f84e8b..7f50223d 100644 --- a/core/user.js +++ b/core/user.js @@ -5,6 +5,7 @@ const userDb = require('./database.js').dbs.user; const Config = require('./config.js').config; const userGroup = require('./user_group.js'); const Errors = require('./enig_error.js').Errors; +const Events = require('./events.js'); // deps const crypto = require('crypto'); @@ -240,6 +241,10 @@ module.exports = class User { self.persistWithTransaction(trans, err => { return callback(err, trans); }); + }, + function sendEvent(trans, callback) { + Events.emit(Events.getSystemEvents().NewUser, { user : self }); + return callback(null, trans); } ], (err, trans) => { From 1cb811576b4b7f6d36d87edd9421800dee6e1571 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 3 Jun 2018 19:58:31 -0600 Subject: [PATCH 131/569] + Add unique session ID to client sessions * Aliased to user for convienence * Added to logs for easy tracing * Can be used from events/etc. for grouping --- core/client_connections.js | 6 +++++- core/user_login.js | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index 121ea0fa..3e7378f9 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -8,6 +8,7 @@ const Events = require('./events.js'); // deps const _ = require('lodash'); const moment = require('moment'); +const hashids = require('hashids'); exports.getActiveConnections = getActiveConnections; exports.getActiveNodeList = getActiveNodeList; @@ -60,9 +61,12 @@ function addNewClient(client, clientSock) { const id = client.session.id = clientConnections.push(client) - 1; const remoteAddress = client.remoteAddress = clientSock.remoteAddress; + // create a uniqe identifier one-time ID for this session + client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); + // Create a client specific logger // Note that this will be updated @ login with additional information - client.log = logger.log.child( { clientId : id } ); + client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } ); const connInfo = { remoteAddress : remoteAddress, diff --git a/core/user_login.js b/core/user_login.js index 8e0e7404..80d832e0 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -55,11 +55,19 @@ function userLogin(client, username, password, cb) { return cb(existingConnError); } - // update client logger with addition of username - client.log = logger.log.child( { clientId : client.log.fields.clientId, username : user.username }); + client.log = logger.log.child( + { + clientId : client.log.fields.clientId, + sessionId : client.log.fields.sessionId, + username : user.username, + } + ); client.log.info('Successful login'); + // User's unique session identifier is the same as the connection itself + user.sessionId = client.session.uniqueId; // convienence + Events.emit(Events.getSystemEvents().UserLogin, { user } ); async.parallel( From 973e10fb8b20e5e6c37ee1123e6cbc241d3e3a17 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Jun 2018 22:45:01 -0600 Subject: [PATCH 132/569] HOME/END key support in lists --- core/menu_view.js | 20 ++++++++++++++++---- core/vertical_menu_view.js | 17 +++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/core/menu_view.js b/core/menu_view.js index dc2f5c81..598e04cd 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -60,6 +60,10 @@ function MenuView(options) { } return -1; }; + + this.emitIndexUpdate = function() { + self.emit('index update', self.focusedItemIndex); + } } util.inherits(MenuView, View); @@ -174,19 +178,27 @@ MenuView.prototype.getItem = function(index) { }; MenuView.prototype.focusNext = function() { - this.emit('index update', this.focusedItemIndex); + this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { - this.emit('index update', this.focusedItemIndex); + this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { - this.emit('index update', this.focusedItemIndex); + this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { - this.emit('index update', this.focusedItemIndex); + this.emitIndexUpdate(); +}; + +MenuView.prototype.focusFirst = function() { + this.emitIndexUpdate(); +}; + +MenuView.prototype.focusLast = function() { + this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 57570e16..fe5c1203 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -165,7 +165,6 @@ VerticalMenuView.prototype.setFocusItemIndex = function(index) { }; VerticalMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { if(this.isKeyMapped('up', key.name)) { this.focusPrevious(); @@ -173,8 +172,12 @@ VerticalMenuView.prototype.onKeyPress = function(ch, key) { this.focusNext(); } else if(this.isKeyMapped('page up', key.name)) { this.focusPreviousPageItem(); - } else if( this.isKeyMapped('page down', key.name)) { + } else if(this.isKeyMapped('page down', key.name)) { this.focusNextPageItem(); + } else if(this.isKeyMapped('home', key.name)) { + this.focusFirst(); + } else if(this.isKeyMapped('end', key.name)) { + this.focusLast(); } } @@ -308,6 +311,16 @@ VerticalMenuView.prototype.focusNextPageItem = function() { return VerticalMenuView.super_.prototype.focusNextPageItem.call(this); }; +VerticalMenuView.prototype.focusFirst = function() { + this.setFocusItemIndex(0); + return VerticalMenuView.super_.prototype.focusFirst.call(this); +}; + +VerticalMenuView.prototype.focusLast = function() { + this.setFocusItemIndex(this.items.length - 1); + return VerticalMenuView.super_.prototype.focusLast.call(this); +}; + VerticalMenuView.prototype.setFocusItems = function(items) { VerticalMenuView.super_.prototype.setFocusItems.call(this, items); From ec30c595c48037d6b59013cc3f5b7a1ff96bc8c2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Jun 2018 22:50:57 -0600 Subject: [PATCH 133/569] Fix drawing like page up/down --- core/vertical_menu_view.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index fe5c1203..7e8fc808 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -312,12 +312,31 @@ VerticalMenuView.prototype.focusNextPageItem = function() { }; VerticalMenuView.prototype.focusFirst = function() { + if(0 < this.viewWindow.top) { + this.oldDimens = Object.assign({}, this.dimens); + } this.setFocusItemIndex(0); return VerticalMenuView.super_.prototype.focusFirst.call(this); }; VerticalMenuView.prototype.focusLast = function() { - this.setFocusItemIndex(this.items.length - 1); + const index = 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.focusLast.call(this); }; From 82da4b8e1ea708ea2fe48e598138c5e46ff2abb9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 13 Jun 2018 20:54:17 -0600 Subject: [PATCH 134/569] Resolve non-conditionals as-is --- core/acs.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/acs.js b/core/acs.js index 9532ad78..90c7c2a6 100644 --- a/core/acs.js +++ b/core/acs.js @@ -52,7 +52,11 @@ class ACS { } getConditionalValue(condArray, memberName) { - assert(_.isArray(condArray)); + if(!Array.isArray(condArray)) { + // no cond array, just use the value + return condArray; + } + assert(_.isString(memberName)); const matchCond = condArray.find( cond => { From c08e4dbe046b0f4b0d6732cb51c4b60da41d7fae Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 13 Jun 2018 20:54:59 -0600 Subject: [PATCH 135/569] New system events --- core/system_events.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/system_events.js b/core/system_events.js index 133561cb..a5e94cc1 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -6,12 +6,17 @@ module.exports = { ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } + ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // { themeId } + ConfigChanged : 'codes.l33t.enigma.system.config_changed', + MenusChanged : 'codes.l33t.enigma.system.menus_changed', + PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', + // User - includes { user, ...} NewUser : 'codes.l33t.enigma.system.new_user', UserLogin : 'codes.l33t.enigma.system.user_login', UserLogoff : 'codes.l33t.enigma.system.user_logoff', - UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } - UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } + UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } + UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } // NYI below here: UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', From 7748765ce05e5d7f4b2c99bf8c3dbb43117cbdb0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 13 Jun 2018 20:58:02 -0600 Subject: [PATCH 136/569] Clean up code for updated getConditionalValue() --- core/menu_util.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/menu_util.js b/core/menu_util.js index a86690e5..4a4665b8 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -221,11 +221,7 @@ function handleAction(client, formData, conf, cb) { } function handleNext(client, nextSpec, conf, cb) { - assert(_.isString(nextSpec) || _.isArray(nextSpec)); - - if(_.isArray(nextSpec)) { - nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); - } + nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); // :TODO: getAssetWithShorthand() can return undefined - handle it! From ceab8a01808feca0301e5d1711df2beb7dcd86b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 13 Jun 2018 20:58:59 -0600 Subject: [PATCH 137/569] Code cleanup --- core/logger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/logger.js b/core/logger.js index 1f1efde2..063757ff 100644 --- a/core/logger.js +++ b/core/logger.js @@ -26,12 +26,12 @@ module.exports = class Log { } const serializers = { - err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. + err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. }; // try to remove sensitive info by default, e.g. 'password' fields [ 'formData', 'formValue' ].forEach(keyName => { - serializers[keyName] = (fd) => Log.hideSensitive(fd); + serializers[keyName] = (fd) => Log.hideSensitive(fd); }); this.log = bunyan.createLogger({ From 1870db7d38aa1c5f9a16dc6391f26442474c204f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 13 Jun 2018 20:59:43 -0600 Subject: [PATCH 138/569] Cleanup code for new getConditionalValue() support --- core/menu_stack.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/core/menu_stack.js b/core/menu_stack.js index 073dece5..360c2b38 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -52,18 +52,13 @@ module.exports = class MenuStack { const currentModuleInfo = this.top(); assert(currentModuleInfo, 'Empty menu stack!'); - const menuConfig = currentModuleInfo.instance.menuConfig; - let nextMenu; - - if(_.isArray(menuConfig.next)) { - nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); - if(!nextMenu) { - return cb(Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH')); - } - } else if(_.isString(menuConfig.next)) { - nextMenu = menuConfig.next; - } else { - return cb(Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT')); + const menuConfig = currentModuleInfo.instance.menuConfig; + const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); + if(!nextMenu) { + return cb(Array.isArray(menuConfig.next) ? + Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') : + Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT') + ); } if(nextMenu === currentModuleInfo.name) { From 4aab8224eda69431c48be37ee5714fd296d260a5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 13 Jun 2018 21:02:00 -0600 Subject: [PATCH 139/569] Initial version of hot-reload of config, menus, and prompts * Themes use ES6 Map vs object{} * Re-write and re-enable config cache using sane * Events sent for config, prompt, or menu changes * Event sent for theme changes * Theme (or parent menu/prompt) changes cause re-merge and updates to connected clients --- core/bbs.js | 7 +- core/client.js | 7 ++ core/config.js | 90 +++++++++---------- core/config_cache.js | 121 +++++++++++-------------- core/config_util.js | 61 +++++++++++-- core/fse.js | 13 +-- core/theme.js | 205 +++++++++++++++++++++---------------------- core/user_config.js | 13 +-- 8 files changed, 275 insertions(+), 242 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index a7246b8b..883e1a87 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -190,10 +190,13 @@ function initialize(cb) { function initStatLog(callback) { return require('./stat_log.js').init(callback); }, + function initConfigs(callback) { + return require('./config_util.js').init(callback); + }, function initThemes(callback) { // Have to pull in here so it's after Config init - require('./theme.js').initAvailableThemes(function onThemesInit(err, themeCount) { - logger.log.info({ themeCount : themeCount }, 'Themes initialized'); + require('./theme.js').initAvailableThemes( (err, themeCount) => { + logger.log.info({ themeCount }, 'Themes initialized'); return callback(err); }); }, diff --git a/core/client.js b/core/client.js index d75b1b6d..d3692cd5 100644 --- a/core/client.js +++ b/core/client.js @@ -38,6 +38,7 @@ const User = require('./user.js'); const Config = require('./config.js').config; const MenuStack = require('./menu_stack.js'); const ACS = require('./acs.js'); +const Events = require('./events.js'); // deps const stream = require('stream'); @@ -110,6 +111,12 @@ function Client(/*input, output*/) { this.input.on('data', this.dataHandler); }; + Events.on(Events.getSystemEvents().ThemeChanged, ( { themeId } ) => { + if(_.get(this.currentTheme, 'info.themeId') === themeId) { + this.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); + } + }); + // // Peek at incoming |data| and emit events for any special diff --git a/core/config.js b/core/config.js index 63e52032..b11c43e9 100644 --- a/core/config.js +++ b/core/config.js @@ -2,13 +2,12 @@ 'use strict'; // ENiGMA½ +const Errors = require('./enig_error.js').Errors; // deps -const fs = require('graceful-fs'); const paths = require('path'); const async = require('async'); const _ = require('lodash'); -const hjson = require('hjson'); const assert = require('assert'); exports.init = init; @@ -40,39 +39,13 @@ function hasMessageConferenceAndArea(config) { return result; } -function init(configPath, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } - +function mergeValidateAndFinalize(config, cb) { async.waterfall( [ - function loadUserConfig(callback) { - if(!_.isString(configPath)) { - return callback(null, { } ); - } - - fs.readFile(configPath, { encoding : 'utf8' }, (err, configData) => { - if(err) { - return callback(err); - } - - let configJson; - try { - configJson = hjson.parse(configData, options); - } catch(e) { - return callback(e); - } - - return callback(null, configJson); - }); - }, - function mergeWithDefaultConfig(configJson, callback) { - + function mergeWithDefaultConfig(callback) { const mergedConfig = _.mergeWith( getDefaultConfig(), - configJson, (conf1, conf2) => { + config, (conf1, conf2) => { // Arrays should always concat if(_.isArray(conf1)) { // :TODO: look for collisions & override dupes @@ -89,26 +62,53 @@ function init(configPath, options, cb) { // // :TODO: Logic is broken here: if(hasMessageConferenceAndArea(mergedConfig)) { - var msgAreasErr = new Error('Please create at least one message conference and area!'); - msgAreasErr.code = 'EBADCONFIG'; - return callback(msgAreasErr); - } else { - return callback(null, mergedConfig); + return callback(Errors.MissingConfig('Please create at least one message conference and area!')); } + return callback(null, mergedConfig); + }, + function setIt(mergedConfig, callback) { + exports.config = mergedConfig; + + exports.config.get = (path) => { + return _.get(exports.config, path); + }; + + return callback(null); } ], - function complete(err, mergedConfig) { - exports.config = mergedConfig; - - exports.config.get = function(path) { - return _.get(exports.config, path); - }; - - return cb(err); + err => { + if(cb) { + return cb(err); + } } ); } +function init(configPath, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } + + const changed = ( { fileName, fileRoot } ) => { + const reCachedPath = paths.join(fileRoot, fileName); + ConfigCache.getConfig(reCachedPath, (err, config) => { + if(!err) { + mergeValidateAndFinalize(config); + } + }); + }; + + const ConfigCache = require('./config_cache.js'); + ConfigCache.getConfigWithOptions( { filePath : configPath, callback : changed }, (err, config) => { + if(err) { + return cb(err); + } + + return mergeValidateAndFinalize(config, cb); + }); +} + function getDefaultPath() { // e.g. /enigma-bbs-install-path/config/ return './config/'; @@ -804,7 +804,7 @@ function getDefaultConfig() { }, misc : { - preAuthIdleLogoutSeconds : 60 * 3, // 2m + preAuthIdleLogoutSeconds : 60 * 3, // 3m idleLogoutSeconds : 60 * 6, // 6m }, diff --git a/core/config_cache.js b/core/config_cache.js index 875e1b2e..8ec7280c 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -1,85 +1,70 @@ /* jslint node: true */ 'use strict'; -var Config = require('./config.js').config; -var Log = require('./logger.js').log; +// deps +const paths = require('path'); +const fs = require('graceful-fs'); +const hjson = require('hjson'); +const sane = require('sane'); -var paths = require('path'); -var fs = require('graceful-fs'); -var events = require('events'); -var util = require('util'); -var assert = require('assert'); -var hjson = require('hjson'); -var _ = require('lodash'); +module.exports = new class ConfigCache +{ + constructor() { + this.cache = new Map(); // path->parsed config + } -function ConfigCache() { - events.EventEmitter.call(this); + getConfigWithOptions(options, cb) { + const cached = this.cache.has(options.filePath); - var self = this; - this.cache = {}; // filePath -> HJSON - //this.gaze = new Gaze(); + if(options.forceReCache || !cached) { + this.recacheConfigFromFile(options.filePath, (err, config) => { + if(!err && !cached) { + const watcher = sane( + paths.dirname(options.filePath), + { + glob : `**/${paths.basename(options.filePath)}` + } + ); - this.reCacheConfigFromFile = function(filePath, cb) { - fs.readFile(filePath, { encoding : 'utf-8' }, function fileRead(err, data) { - try { - self.cache[filePath] = hjson.parse(data); - cb(null, self.cache[filePath]); - } catch(e) { - Log.error( { filePath : filePath, error : e.toString() }, 'Failed recaching'); - cb(e); - } - }); - }; + watcher.on('change', (fileName, fileRoot) => { + require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching'); -/* - this.gaze.on('error', function gazeErr(err) { + this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { + if(!err) { + if(options.callback) { + options.callback( { fileName, fileRoot } ); + } + } + }); + }); + } + return cb(err, config, true); + }); + } else { + return cb(null, this.cache.get(options.filePath), false); + } + } - }); + getConfig(filePath, cb) { + return this.getConfigWithOptions( { filePath }, cb); + } - this.gaze.on('changed', function fileChanged(filePath) { - assert(filePath in self.cache); - - Log.info( { path : filePath }, 'Configuration file changed; re-caching'); - - self.reCacheConfigFromFile(filePath, function reCached(err) { + recacheConfigFromFile(path, cb) { + fs.readFile(path, { encoding : 'utf-8' }, (err, data) => { if(err) { - Log.error( { error : err.message, path : filePath } , 'Failed re-caching configuration'); - } else { - self.emit('recached', filePath); + return cb(err); } - }); - }); - */ -} - -util.inherits(ConfigCache, events.EventEmitter); - -ConfigCache.prototype.getConfigWithOptions = function(options, cb) { - assert(_.isString(options.filePath)); - - // var self = this; - var isCached = (options.filePath in this.cache); - - if(options.forceReCache || !isCached) { - this.reCacheConfigFromFile(options.filePath, function fileCached(err, config) { - if(!err && !isCached) { - //self.gaze.add(options.filePath); + let parsed; + try { + parsed = hjson.parse(data); + this.cache.set(path, parsed); + } catch(e) { + require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); + return cb(e); } - cb(err, config, true); + + return cb(null, parsed); }); - } else { - cb(null, this.cache[options.filePath], false); } }; - - -ConfigCache.prototype.getConfig = function(filePath, cb) { - this.getConfigWithOptions( { filePath : filePath }, cb); -}; - -ConfigCache.prototype.getModConfig = function(fileName, cb) { - this.getConfig(paths.join(Config.paths.mods, fileName), cb); -}; - -module.exports = exports = new ConfigCache(); diff --git a/core/config_util.js b/core/config_util.js index 40723d9a..94ce4344 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -1,18 +1,65 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').config; -const configCache = require('./config_cache.js'); -const paths = require('path'); +const Config = require('./config.js').config; +const ConfigCache = require('./config_cache.js'); +const Events = require('./events.js'); + +// deps +const paths = require('path'); +const async = require('async'); + +exports.init = init; exports.getFullConfig = getFullConfig; -function getFullConfig(filePath, cb) { +function getConfigPath(filePath) { // |filePath| is assumed to be in the config path if it's only a file name if('.' === paths.dirname(filePath)) { filePath = paths.join(Config.paths.config, filePath); } + return filePath; +} - configCache.getConfig(filePath, function loaded(err, configJson) { - cb(err, configJson); +function init(cb) { + // pre-cache menu.hjson and prompt.hjson + establish events + const changed = ( { fileName, fileRoot } ) => { + const reCachedPath = paths.join(fileRoot, fileName); + if(reCachedPath === getConfigPath(Config.general.menuFile)) { + Events.emit(Events.getSystemEvents().MenusChanged); + } else if(reCachedPath === getConfigPath(Config.general.promptFile)) { + Events.emit(Events.getSystemEvents().PromptsChanged); + } + }; + + async.series( + [ + function menu(callback) { + return ConfigCache.getConfigWithOptions( + { + filePath : getConfigPath(Config.general.menuFile), + callback : changed, + }, + callback + ); + }, + function prompt(callback) { + return ConfigCache.getConfigWithOptions( + { + filePath : getConfigPath(Config.general.promptFile), + callback : changed, + }, + callback + ); + } + ], + err => { + return cb(err); + } + ); +} + +function getFullConfig(filePath, cb) { + ConfigCache.getConfig(getConfigPath(filePath), (err, config) => { + return cb(err, config); }); -} \ No newline at end of file +} diff --git a/core/fse.js b/core/fse.js index 8a409589..70b1ecec 100644 --- a/core/fse.js +++ b/core/fse.js @@ -532,12 +532,13 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul theme.displayThemedAsset( art[n], self.client, - { font : self.menuConfig.font }, + { font : self.menuConfig.font, acsCondMember : 'art' }, function displayed(err) { next(err); } ); }, function complete(err) { + //self.body.height = self.client.term.termHeight - self.header.height - 1; callback(err); }); }, @@ -607,14 +608,11 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul self.beforeArt(callback); }, function displayHeaderAndBodyArt(callback) { - assert(_.isString(art.header)); - assert(_.isString(art.body)); - async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) { theme.displayThemedAsset( art[n], self.client, - { font : self.menuConfig.font }, + { font : self.menuConfig.font, acsCondMember : 'art' }, function displayed(err, artData) { if(artData) { mciData[n] = artData; @@ -879,13 +877,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul async.waterfall( [ function clearAndDisplayArt(callback) { - - // :TODO: use termHeight, not hard coded 24 here: - // :TODO: NetRunner does NOT support delete line, so this does not work: self.client.term.rawWrite( ansi.goto(self.header.height + 1, 1) + - ansi.deleteLine(24 - self.header.height)); + ansi.deleteLine((self.client.term.termHeight - self.header.height) - 1)); theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) { callback(err, artData); diff --git a/core/theme.js b/core/theme.js index 2c984ae0..77c737fc 100644 --- a/core/theme.js +++ b/core/theme.js @@ -5,12 +5,13 @@ const Config = require('./config.js').config; const art = require('./art.js'); const ansi = require('./ansi_term.js'); const Log = require('./logger.js').log; -const configCache = require('./config_cache.js'); +const ConfigCache = require('./config_cache.js'); const getFullConfig = require('./config_util.js').getFullConfig; const asset = require('./asset.js'); const ViewController = require('./view_controller.js').ViewController; const Errors = require('./enig_error.js').Errors; const ErrorReasons = require('./enig_error.js').ErrorReasons; +const Events = require('./events.js'); const fs = require('graceful-fs'); const paths = require('path'); @@ -63,11 +64,23 @@ function refreshThemeHelpers(theme) { }; } -function loadTheme(themeID, cb) { +function loadTheme(themeId, cb) { + const path = paths.join(Config.paths.themes, themeId, 'theme.hjson'); - const path = paths.join(Config.paths.themes, themeID, 'theme.hjson'); + const changed = ( { fileName, fileRoot } ) => { + const reCachedPath = paths.join(fileRoot, fileName); + if(reCachedPath === path) { + reloadTheme(themeId); + } + }; - configCache.getConfigWithOptions( { filePath : path, forceReCache : true }, (err, theme) => { + const getOpts = { + filePath : path, + forceReCache : true, + callback : changed, + }; + + ConfigCache.getConfigWithOptions(getOpts, (err, theme) => { if(err) { return cb(err); } @@ -89,7 +102,7 @@ function loadTheme(themeID, cb) { }); } -const availableThemes = {}; +const availableThemes = new Map(); const IMMUTABLE_MCI_PROPERTIES = [ 'maxLength', 'argName', 'submit', 'validate' @@ -248,6 +261,56 @@ function getMergedTheme(menuConfig, promptConfig, theme) { return mergedTheme; } +function reloadTheme(themeId) { + async.waterfall( + [ + function loadMenuConfig(callback) { + getFullConfig(Config.general.menuFile, (err, menuConfig) => { + return callback(err, menuConfig); + }); + }, + function loadPromptConfig(menuConfig, callback) { + getFullConfig(Config.general.promptFile, (err, promptConfig) => { + return callback(err, menuConfig, promptConfig); + }); + }, + function loadIt(menuConfig, promptConfig, callback) { + loadTheme(themeId, (err, theme) => { + if(err) { + if(ErrorReasons.NotEnabled !== err.reasonCode) { + Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); + return; + } + return callback(err); + } + + Object.assign(theme.info, { themeId } ); + availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); + + Events.emit( + Events.getSystemEvents().ThemeChanged, + { themeId } + ); + + return callback(null, theme); + }); + } + ], + (err, theme) => { + if(err) { + Log.warn( { themeId, error : err.message }, 'Failed to reload theme'); + } else { + Log.debug( { info : theme.info }, 'Theme recached' ); + } + } + ); +} + +function reloadAllThemes() +{ + async.each([ ...availableThemes.keys() ], themeId => reloadTheme(themeId)); +} + function initAvailableThemes(cb) { async.waterfall( @@ -281,7 +344,7 @@ function initAvailableThemes(cb) { }, function populateAvailable(menuConfig, promptConfig, themeDirectories, callback) { async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID - loadTheme(themeId, (err, theme, themePath) => { + loadTheme(themeId, (err, theme) => { if(err) { if(ErrorReasons.NotEnabled !== err.reasonCode) { Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); @@ -290,31 +353,27 @@ function initAvailableThemes(cb) { return nextThemeDir(null); // try next } - 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 - } - }); - } - }); - + Object.assign(theme.info, { themeId } ); + availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); return nextThemeDir(null); }); }, err => { return callback(err); }); + }, + function initEvents(callback) { + Events.on(Events.getSystemEvents().MenusChanged, () => { + return reloadAllThemes(); + }); + Events.on(Events.getSystemEvents().PromptsChanged, () => { + return reloadAllThemes(); + }); + + return callback(null); } ], err => { - return cb(err, availableThemes ? availableThemes.length : 0); + return cb(err, availableThemes.size); } ); } @@ -324,31 +383,30 @@ function getAvailableThemes() { } function getRandomTheme() { - if(Object.getOwnPropertyNames(availableThemes).length > 0) { - var themeIds = Object.keys(availableThemes); + if(availableThemes.size > 0) { + const themeIds = [ ...availableThemes.keys() ]; return themeIds[Math.floor(Math.random() * themeIds.length)]; } } function setClientTheme(client, themeId) { - let logMsg; - const availThemes = getAvailableThemes(); - client.currentTheme = availThemes[themeId]; - if(client.currentTheme) { - logMsg = 'Set client theme'; + let msg; + let setThemeId; + if(availThemes.has(themeId)) { + msg = 'Set client theme'; + setThemeId = themeId; + } else if(availThemes.has(Config.defaults.theme)) { + msg = 'Failed setting theme by supplied ID; Using default'; + setThemeId = Config.defaults.theme; } 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'; - } + msg = 'Failed setting theme by system default ID; Using the first one we can find'; + setThemeId = availThemes.keys().next().value; } - client.log.debug( { themeId : themeId, info : client.currentTheme.info }, logMsg); + client.currentTheme = availThemes.get(setThemeId); + client.log.debug( { setThemeId, requestedThemeId : themeId, info : client.currentTheme.info }, msg); } function getThemeArt(options, cb) { @@ -465,73 +523,6 @@ function displayThemeArt(options, cb) { }); } -/* -function displayThemedPrompt(name, client, options, cb) { - - async.waterfall( - [ - function loadConfig(callback) { - configCache.getModConfig('prompt.hjson', (err, promptJson) => { - if(err) { - return callback(err); - } - - if(_.has(promptJson, [ 'prompts', name ] )) { - return callback(Errors.DoesNotExist(`Prompt "${name}" does not exist`)); - } - - const promptConfig = promptJson.prompts[name]; - if(!_.isObject(promptConfig)) { - return callback(Errors.Invalid(`Prompt "${name} is invalid`)); - } - - return callback(null, promptConfig); - }); - }, - function display(promptConfig, callback) { - if(options.clearScreen) { - client.term.rawWrite(ansi.clearScreen()); - } - - // - // If we did not clear the screen, don't let the font change - // - const dispOptions = Object.assign( {}, promptConfig.options ); - if(!options.clearScreen) { - dispOptions.font = 'not_really_a_font!'; - } - - displayThemedAsset( - promptConfig.art, - client, - dispOptions, - (err, artData) => { - if(err) { - return callback(err); - } - - return callback(null, promptConfig, artData.mciMap); - } - ); - }, - function prepViews(promptConfig, mciMap, callback) { - vc = new ViewController( { client : client } ); - - const loadOpts = { - promptName : name, - mciMap : mciMap, - config : promptConfig, - }; - - vc.loadFromPromptConfig(loadOpts, err => { - callback(null); - }); - } - ] - ); -} -*/ - function displayThemedPrompt(name, client, options, cb) { const useTempViewController = _.isUndefined(options.viewController); @@ -663,6 +654,10 @@ function displayThemedAsset(assetSpec, client, options, cb) { options = {}; } + if(Array.isArray(assetSpec) && _.isString(options.acsCondMember)) { + assetSpec = client.acs.getConditionalValue(assetSpec, options.acsCondMember); + } + const artAsset = asset.getArtAsset(assetSpec); if(!artAsset) { return cb(new Error('Asset not found: ' + assetSpec)); diff --git a/core/user_config.js b/core/user_config.js index f60d2155..6a51a36b 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -164,13 +164,14 @@ exports.getModule = class UserConfigModule extends MenuModule { vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); }, function prepareAvailableThemes(callback) { - self.availThemeInfo = _.sortBy(_.map(theme.getAvailableThemes(), function makeThemeInfo(t, themeId) { + self.availThemeInfo = _.sortBy([...theme.getAvailableThemes()].map(entry => { + const theme = entry[1]; return { - themeId : themeId, - name : t.info.name, - author : t.info.author, - desc : _.isString(t.info.desc) ? t.info.desc : '', - group : _.isString(t.info.group) ? t.info.group : '', + themeId : theme.info.themeId, + name : theme.info.name, + author : theme.info.author, + desc : _.isString(theme.info.desc) ? theme.info.desc : '', + group : _.isString(theme.info.group) ? theme.info.group : '', }; }), 'name'); From 2f09f3e995a45c409f82e28bf1fb41f71fb0a37f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 14 Jun 2018 20:00:01 -0600 Subject: [PATCH 140/569] Fix a couple rare bugs around SSH sessions --- core/bbs.js | 5 ++++- core/servers/login/ssh.js | 14 +++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 883e1a87..a352c555 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -119,7 +119,10 @@ function shutdownSystem() { const activeConnections = ClientConns.getActiveConnections(); let i = activeConnections.length; while(i--) { - activeConnections[i].term.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); + const activeTerm = activeConnections[i].term; + if(activeTerm) { + activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); + } ClientConns.removeClient(activeConnections[i]); } callback(null); diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 4ec57d2f..c50a7d70 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -184,7 +184,7 @@ function SSHClient(clientConn) { if(self.input) { // do we have I/O? self.updateTermInfo(info); } else { - self.cachedPtyInfo = info; + self.cachedTermInfo = info; } }); @@ -197,9 +197,9 @@ function SSHClient(clientConn) { channel.stdin.on('data', self.dataHandler); - if(self.cachedPtyInfo) { - self.updateTermInfo(self.cachedPtyInfo); - delete self.cachedPtyInfo; + if(self.cachedTermInfo) { + self.updateTermInfo(self.cachedTermInfo); + delete self.cachedTermInfo; } // we're ready! @@ -210,7 +210,11 @@ function SSHClient(clientConn) { session.on('window-change', (accept, reject, info) => { self.log.debug(info, 'SSH window-change event'); - self.updateTermInfo(info); + if(self.input) { + self.updateTermInfo(info); + } else { + self.cachedTermInfo = info; + } }); }); From 057ba684ea5fdb265b7dc215407bbae92cb1b6d7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Jun 2018 08:41:41 -0600 Subject: [PATCH 141/569] Use pre-generated table vs parsing a string --- core/crc.js | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/core/crc.js b/core/crc.js index e4bd8551..d110807b 100644 --- a/core/crc.js +++ b/core/crc.js @@ -1,8 +1,45 @@ /* jslint node: true */ 'use strict'; -const CRC32_TABLE = new Int32Array( - '00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D'.split(' ').map(s => parseInt(s, 16))); +const CRC32_TABLE = new Int32Array([ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, + 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, + 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, + 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, + 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, + 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, + 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, + 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, + 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, + 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, + 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, + 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, + 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, + 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, + 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, + 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, + 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, + 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d +]); exports.CRC32 = class CRC32 { constructor() { From 5f0c9ed1abff8d6ae6f0f024aea0970cadb8ffd0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Jun 2018 08:42:16 -0600 Subject: [PATCH 142/569] Fix require paths --- core/exodus.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/exodus.js b/core/exodus.js index 8b3c7548..129671cb 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -2,8 +2,8 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('../core/menu_module.js').MenuModule; -const resetScreen = require('../core/ansi_term.js').resetScreen; +const MenuModule = require('./menu_module.js').MenuModule; +const resetScreen = require('./ansi_term.js').resetScreen; const Config = require('./config.js').config; const Errors = require('./enig_error.js').Errors; const Log = require('./logger.js').log; From c9674e68fbb38616f02b453ea267a28b31c641a2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Jun 2018 10:01:08 -0600 Subject: [PATCH 143/569] * Re-work menu stack goto() a bit - cleaner, support 'mergeFlags', and 'forwardArgs' menuFlags. * Add show_art.js module: Advanced ways to show art in menu stacks. For example, by extraArgs, fileBase area art, etc -- this will replace e.g. showing message conf art later as to be more generic --- core/file_base_area_select.js | 2 +- core/menu_stack.js | 27 +++++- core/show_art.js | 170 ++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 core/show_art.js diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index df859bea..6c45a5d1 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -33,7 +33,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { extraArgs : { filterCriteria : filterCriteria, }, - menuFlags : [ 'popParent' ], + menuFlags : [ 'popParent', 'mergeFlags' ], }; return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); diff --git a/core/menu_stack.js b/core/menu_stack.js index 360c2b38..3775c065 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -97,6 +97,7 @@ module.exports = class MenuStack { options = {}; } + options = options || {}; const self = this; if(currentModuleInfo && name === currentModuleInfo.name) { @@ -111,10 +112,12 @@ module.exports = class MenuStack { client : self.client, }; - if(_.isObject(options)) { - loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); - loadOpts.lastMenuResult = options.lastMenuResult; + if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { + loadOpts.extraArgs = currentModuleInfo.extraArgs; + } else { + loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); } + loadOpts.lastMenuResult = options.lastMenuResult; loadMenu(loadOpts, (err, modInst) => { if(err) { @@ -124,7 +127,21 @@ module.exports = class MenuStack { } else { self.client.log.debug( { menuName : name }, 'Goto menu module'); - const menuFlags = (options && Array.isArray(options.menuFlags)) ? options.menuFlags : modInst.menuConfig.options.menuFlags; + // + // If menuFlags were supplied in menu.hjson, they should win over + // anything supplied in code. + // + let menuFlags; + if(0 === modInst.menuConfig.options.menuFlags.length) { + menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : []; + } else { + menuFlags = modInst.menuConfig.options.menuFlags; + + // in code we can ask to merge in + if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { + menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); + } + } if(currentModuleInfo) { // save stack state @@ -149,7 +166,7 @@ module.exports = class MenuStack { }); // restore previous state if requested - if(options && options.savedState) { + if(options.savedState) { modInst.restoreSavedState(options.savedState); } diff --git a/core/show_art.js b/core/show_art.js new file mode 100644 index 00000000..fbbb3141 --- /dev/null +++ b/core/show_art.js @@ -0,0 +1,170 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const Errors = require('../core/enig_error.js').Errors; +const ANSI = require('./ansi_term.js'); +const Config = require('./config.js').config; + +// deps +const async = require('async'); +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'Show Art', + desc : 'Module for more advanced methods of displaying art', + author : 'NuSkooler', +}; + +exports.getModule = class ShowArtModule extends MenuModule { + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + + this.config.method = this.config.method || 'random'; + this.config.optional = _.get(this.config, 'optional', true); + } + + initSequence() { + const self = this; + + async.series( + [ + function before(callback) { + return self.beforeArt(callback); + }, + function showArt(callback) { + // + // How we show art depends on our configuration + // + let handler = { + extraArgs : self.showByExtraArgs, + sequence : self.showBySequence, + random : self.showByRandom, + fileBaseArea : self.showByFileBaseArea, + }[self.config.method] || self.showRandomArt; + + handler = handler.bind(self); + + return handler(callback); + } + ], + err => { + if(err && !self.config.optional) { + self.client.log.warn('Error during init sequence', { error : err.message } ); + return self.prevMenu( () => { /* dummy */ } ); + } + + self.finishedLoading(); + return self.autoNextMenu( () => { /* dummy */ } ); + } + ); + } + + showByExtraArgs(cb) { + this.getArtKeyValue( (err, artSpec) => { + if(err) { + return cb(err); + } + const options = { + pause : this.shouldPause(), + desc : 'extraArgs', + }; + return this.displaySingleArtWithOptions(artSpec, options, cb); + }); + } + + showBySequence(cb) { + return cb(null); + } + + showByRandom(cb) { + return cb(null); + } + + showByFileBaseArea(cb) { + this.getArtKeyValue( (err, key) => { + if(err) { + return cb(err); + } + + // further resolve key -> file base area art + const artSpec = _.get(Config, [ 'fileBase', 'areas', key, 'art' ]); + if(!artSpec) { + return cb(Errors.MissingConfig(`No art defined for file base area "${key}"`)); + } + const options = { + pause : this.shouldPause(), + desc : 'fileBaseArea', + }; + return this.displaySingleArtWithOptions(artSpec, options, cb); + }); + } + + getArtKeyValue(cb) { + const key = this.config.key; + if(!_.isString(key)) { + return cb(Errors.MissingConfig('Config option "key" is required for method "extraArgs"')); + } + + const path = key.split('.'); + const artKey = _.get(this.config, [ 'extraArgs' ].concat(path) ); + if(!_.isString(artKey)) { + return cb(Errors.MissingParam(`Invalid or missing "extraArgs.${key}" value`)); + } + + return cb(null, artKey); + } + + displaySingleArtWithOptions(artSpec, options, cb) { + const self = this; + async.waterfall( + [ + function art(callback) { + // :TODO: we really need a way to supply an explicit path to look in, e.g. general/area_art/ + self.displayAsset( + artSpec, + self.menuConfig.options, + (err, artData) => { + if(err) { + return callback(err); + } + const mciData = { menu : artData.mciMap }; + return callback(null, mciData); + } + ); + }, + function recordCursorPosition(mciData, callback) { + if(!options.pause) { + return callback(null, mciData, null); // cursor position not needed + } + + self.client.once('cursor position report', pos => { + const pausePosition = { row : pos[0], col : 1 }; + return callback(null, mciData, pausePosition); + }); + + self.client.term.rawWrite(ANSI.queryPos()); + }, + function afterArtDisplayed(mciData, pausePosition, callback) { + self.mciReady(mciData, err => { + return callback(err, pausePosition); + }); + }, + function displayPauseIfRequested(pausePosition, callback) { + if(!options.pause) { + return callback(null); + } + return self.pausePrompt(pausePosition, callback); + }, + ], + err => { + if(err) { + self.client.log.warn( { artSpec, error : err.message }, `Failed to display "${options.desc}" art`); + } + return cb(err); + } + ); + } +}; From f3cd36ad079d0cf10f2a3a89b26e3ea79dff1686 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 17 Jun 2018 15:14:31 -0600 Subject: [PATCH 144/569] Fix oputil hang --- core/config.js | 9 ++++++++- core/config_cache.js | 34 ++++++++++++++++++---------------- core/oputil/oputil_common.js | 6 ++---- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/core/config.js b/core/config.js index b11c43e9..d8f1c873 100644 --- a/core/config.js +++ b/core/config.js @@ -100,7 +100,14 @@ function init(configPath, options, cb) { }; const ConfigCache = require('./config_cache.js'); - ConfigCache.getConfigWithOptions( { filePath : configPath, callback : changed }, (err, config) => { + const getConfigOptions = { + filePath : configPath, + noWatch : options.noWatch, + }; + if(!options.noWatch) { + getConfigOptions.callback = changed; + } + ConfigCache.getConfigWithOptions(getConfigOptions, (err, config) => { if(err) { return cb(err); } diff --git a/core/config_cache.js b/core/config_cache.js index 8ec7280c..4a1d1c5a 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -19,24 +19,26 @@ module.exports = new class ConfigCache if(options.forceReCache || !cached) { this.recacheConfigFromFile(options.filePath, (err, config) => { if(!err && !cached) { - const watcher = sane( - paths.dirname(options.filePath), - { - glob : `**/${paths.basename(options.filePath)}` - } - ); - - watcher.on('change', (fileName, fileRoot) => { - require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching'); - - this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { - if(!err) { - if(options.callback) { - options.callback( { fileName, fileRoot } ); - } + if(!options.noWatch) { + const watcher = sane( + paths.dirname(options.filePath), + { + glob : `**/${paths.basename(options.filePath)}` } + ); + + watcher.on('change', (fileName, fileRoot) => { + require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching'); + + this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { + if(!err) { + if(options.callback) { + options.callback( { fileName, fileRoot } ); + } + } + }); }); - }); + } } return cb(err, config, true); }); diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index d03bb82f..18546c98 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -2,8 +2,6 @@ /* eslint-disable no-console */ 'use strict'; -const resolvePath = require('../misc_util.js').resolvePath; - const config = require('../../core/config.js'); const db = require('../../core/database.js'); @@ -46,7 +44,7 @@ function printUsageAndSetExitCode(errMsg, exitCode) { } function getDefaultConfigPath() { - return './config/'; + return './config/'; } function getConfigPath() { @@ -57,7 +55,7 @@ function getConfigPath() { function initConfig(cb) { const configPath = getConfigPath(); - config.init(configPath, { keepWsc : true }, cb); + config.init(configPath, { keepWsc : true, noWatch : true }, cb); } function initConfigAndDatabases(cb) { From 681e45cb6d1b5a573a06ff934d0f077c11574350 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 17 Jun 2018 20:39:43 -0600 Subject: [PATCH 145/569] Much faster hash calculation / processing & therefor faster scanFile() * Manaul read of buffers vs stream (fs.createReadStream()) * Small optimization by skipping work if no progress iterator * Don't use async loop for updating hashes - vanilla for loop --- core/file_base_area.js | 246 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 222 insertions(+), 24 deletions(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 9cfb1cb9..0c065357 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -630,23 +630,224 @@ function scanFile(filePath, options, iterator, cb) { fileName : paths.basename(filePath), }; - function callIter(next) { - if(iterator) { - return iterator(stepInfo, next); - } else { - return next(null); - } - } + const callIter = (next) => { + return iterator ? iterator(stepInfo, next) : next(null); + }; - function readErrorCallIter(origError, next) { + const readErrorCallIter = (origError, next) => { stepInfo.step = 'read_error'; stepInfo.error = origError.message; callIter( () => { return next(origError); }); + }; + + let lastCalcHashPercent; + + // don't re-calc hashes for any we already have in |options| + const hashesToCalc = HASH_NAMES.filter(hn => { + if('sha256' === hn && fileEntry.fileSha256) { + return false; + } + + if(`file_${hn}` in fileEntry.meta) { + return false; + } + + return true; + }); + + async.waterfall( + [ + function startScan(callback) { + fs.stat(filePath, (err, stats) => { + if(err) { + return readErrorCallIter(err, callback); + } + + stepInfo.step = 'start'; + stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; + + return callIter(callback); + }); + }, + function processPhysicalFileGeneric(callback) { + stepInfo.bytesProcessed = 0; + + const hashes = {}; + hashesToCalc.forEach(hashName => { + if('crc32' === hashName) { + hashes.crc32 = new CRC32; + } else { + hashes[hashName] = crypto.createHash(hashName); + } + }); + + const updateHashes = (data) => { + for(let i = 0; i < hashesToCalc.length; ++i) { + hashes[hashesToCalc[i]].update(data); + } + }; + + // + // Note that we are not using fs.createReadStream() here: + // While convenient, it is quite a bit slower -- which adds + // up to many seconds in time for larger files. + // + const chunkSize = 1024 * 64; + const buffer = new Buffer(chunkSize); + + fs.open(filePath, 'r', (err, fd) => { + if(err) { + return readErrorCallIter(err, callback); + } + + const nextChunk = () => { + fs.read(fd, buffer, 0, chunkSize, null, (err, bytesRead) => { + if(err) { + fs.close(fd); + return readErrorCallIter(err, callback); + } + + if(0 === bytesRead) { + // done - finalize + fileEntry.meta.byte_size = stepInfo.bytesProcessed; + + for(let i = 0; i < hashesToCalc.length; ++i) { + const hashName = hashesToCalc[i]; + if('sha256' === hashName) { + stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); + } else if('sha1' === hashName || 'md5' === hashName) { + stepInfo[hashName] = fileEntry.meta[`file_${hashName}`] = hashes[hashName].digest('hex'); + } else if('crc32' === hashName) { + stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16); + } + } + + stepInfo.step = 'hash_finish'; + fs.close(fd); + return callIter(callback); + } + + stepInfo.bytesProcessed += bytesRead; + stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); + + // + // Only send 'hash_update' step update if we have a noticable percentage change in progress + // + const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; + if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { + updateHashes(data); + return nextChunk(); + } else { + lastCalcHashPercent = stepInfo.calcHashPercent; + stepInfo.step = 'hash_update'; + + callIter(err => { + if(err) { + return callback(err); + } + + updateHashes(data); + return nextChunk(); + }); + } + }); + }; + + nextChunk(); + }); + }, + function processPhysicalFileByType(callback) { + const archiveUtil = ArchiveUtil.getInstance(); + + archiveUtil.detectType(filePath, (err, archiveType) => { + if(archiveType) { + // save this off + fileEntry.meta.archive_type = archiveType; + + populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { + if(err) { + populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } + return callback(null); // ignore err + }); + } else { + return callback(null); + } + }); + } else { + populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } + return callback(null); // ignore err + }); + } + }); + }, + function fetchExistingEntry(callback) { + getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => { + return callback(err, dupeEntries); + }); + }, + function finished(dupeEntries, callback) { + stepInfo.step = 'finished'; + callIter( () => { + return callback(null, dupeEntries); + }); + } + ], + (err, dupeEntries) => { + if(err) { + return cb(err); + } + + return cb(null, fileEntry, dupeEntries); + } + ); +} + +function scanFile2(filePath, options, iterator, cb) { + + if(3 === arguments.length && _.isFunction(iterator)) { + cb = iterator; + iterator = null; + } else if(2 === arguments.length && _.isFunction(options)) { + cb = options; + iterator = null; + options = {}; } + const fileEntry = new FileEntry({ + areaTag : options.areaTag, + meta : options.meta, + hashTags : options.hashTags, // Set() or Array + fileName : paths.basename(filePath), + storageTag : options.storageTag, + fileSha256 : options.sha256, // caller may know this already + }); + + const stepInfo = { + filePath : filePath, + fileName : paths.basename(filePath), + }; + + const callIter = (next) => { + return iterator ? iterator(stepInfo, next) : next(null); + }; + + const readErrorCallIter = (origError, next) => { + stepInfo.step = 'read_error'; + stepInfo.error = origError.message; + + callIter( () => { + return next(origError); + }); + }; let lastCalcHashPercent; @@ -691,17 +892,15 @@ function scanFile(filePath, options, iterator, cb) { const stream = fs.createReadStream(filePath); - function updateHashes(data) { - async.each(hashesToCalc, (hashName, nextHash) => { - hashes[hashName].update(data); - return nextHash(null); - }, () => { - return stream.resume(); - }); - } + const updateHashes = (data) => { + for(let i = 0; i < hashesToCalc.length; ++i) { + hashes[hashesToCalc[i]].update(data); + } + return stream.resume(); + }; stream.on('data', data => { - stream.pause(); // until iterator compeltes + stream.pause(); // until iterator completes stepInfo.bytesProcessed += data.length; stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); @@ -709,7 +908,7 @@ function scanFile(filePath, options, iterator, cb) { // // Only send 'hash_update' step update if we have a noticable percentage change in progress // - if(stepInfo.calcHashPercent === lastCalcHashPercent) { + if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { updateHashes(data); } else { lastCalcHashPercent = stepInfo.calcHashPercent; @@ -729,7 +928,8 @@ function scanFile(filePath, options, iterator, cb) { stream.on('end', () => { fileEntry.meta.byte_size = stepInfo.bytesProcessed; - async.each(hashesToCalc, (hashName, nextHash) => { + for(let i = 0; i < hashesToCalc.length; ++i) { + const hashName = hashesToCalc[i]; if('sha256' === hashName) { stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); } else if('sha1' === hashName || 'md5' === hashName) { @@ -737,12 +937,10 @@ function scanFile(filePath, options, iterator, cb) { } else if('crc32' === hashName) { stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16); } + } - return nextHash(null); - }, () => { - stepInfo.step = 'hash_finish'; - return callIter(callback); - }); + stepInfo.step = 'hash_finish'; + return callIter(callback); }); stream.on('error', err => { From 4074d6852644def770ea6d65e4981e7f6f808289 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 17 Jun 2018 20:42:42 -0600 Subject: [PATCH 146/569] #195: Finish scanFile() & hash updates: Clean up code --- core/file_base_area.js | 188 ----------------------------------------- 1 file changed, 188 deletions(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 0c065357..3e0f9ff2 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -811,194 +811,6 @@ function scanFile(filePath, options, iterator, cb) { ); } -function scanFile2(filePath, options, iterator, cb) { - - if(3 === arguments.length && _.isFunction(iterator)) { - cb = iterator; - iterator = null; - } else if(2 === arguments.length && _.isFunction(options)) { - cb = options; - iterator = null; - options = {}; - } - - const fileEntry = new FileEntry({ - areaTag : options.areaTag, - meta : options.meta, - hashTags : options.hashTags, // Set() or Array - fileName : paths.basename(filePath), - storageTag : options.storageTag, - fileSha256 : options.sha256, // caller may know this already - }); - - const stepInfo = { - filePath : filePath, - fileName : paths.basename(filePath), - }; - - const callIter = (next) => { - return iterator ? iterator(stepInfo, next) : next(null); - }; - - const readErrorCallIter = (origError, next) => { - stepInfo.step = 'read_error'; - stepInfo.error = origError.message; - - callIter( () => { - return next(origError); - }); - }; - - let lastCalcHashPercent; - - // don't re-calc hashes for any we already have in |options| - const hashesToCalc = HASH_NAMES.filter(hn => { - if('sha256' === hn && fileEntry.fileSha256) { - return false; - } - - if(`file_${hn}` in fileEntry.meta) { - return false; - } - - return true; - }); - - async.waterfall( - [ - function startScan(callback) { - fs.stat(filePath, (err, stats) => { - if(err) { - return readErrorCallIter(err, callback); - } - - stepInfo.step = 'start'; - stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; - - return callIter(callback); - }); - }, - function processPhysicalFileGeneric(callback) { - stepInfo.bytesProcessed = 0; - - const hashes = {}; - hashesToCalc.forEach(hashName => { - if('crc32' === hashName) { - hashes.crc32 = new CRC32; - } else { - hashes[hashName] = crypto.createHash(hashName); - } - }); - - const stream = fs.createReadStream(filePath); - - const updateHashes = (data) => { - for(let i = 0; i < hashesToCalc.length; ++i) { - hashes[hashesToCalc[i]].update(data); - } - return stream.resume(); - }; - - stream.on('data', data => { - stream.pause(); // until iterator completes - - stepInfo.bytesProcessed += data.length; - stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); - - // - // Only send 'hash_update' step update if we have a noticable percentage change in progress - // - if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { - updateHashes(data); - } else { - lastCalcHashPercent = stepInfo.calcHashPercent; - stepInfo.step = 'hash_update'; - - callIter(err => { - if(err) { - stream.destroy(); // cancel read - return callback(err); - } - - updateHashes(data); - }); - } - }); - - stream.on('end', () => { - fileEntry.meta.byte_size = stepInfo.bytesProcessed; - - for(let i = 0; i < hashesToCalc.length; ++i) { - const hashName = hashesToCalc[i]; - if('sha256' === hashName) { - stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); - } else if('sha1' === hashName || 'md5' === hashName) { - stepInfo[hashName] = fileEntry.meta[`file_${hashName}`] = hashes[hashName].digest('hex'); - } else if('crc32' === hashName) { - stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16); - } - } - - stepInfo.step = 'hash_finish'; - return callIter(callback); - }); - - stream.on('error', err => { - return readErrorCallIter(err, callback); - }); - }, - function processPhysicalFileByType(callback) { - const archiveUtil = ArchiveUtil.getInstance(); - - archiveUtil.detectType(filePath, (err, archiveType) => { - if(archiveType) { - // save this off - fileEntry.meta.archive_type = archiveType; - - populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { - if(err) { - populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - if(err) { - logDebug( { error : err.message }, 'Non-archive file entry population failed'); - } - return callback(null); // ignore err - }); - } else { - return callback(null); - } - }); - } else { - populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - if(err) { - logDebug( { error : err.message }, 'Non-archive file entry population failed'); - } - return callback(null); // ignore err - }); - } - }); - }, - function fetchExistingEntry(callback) { - getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => { - return callback(err, dupeEntries); - }); - }, - function finished(dupeEntries, callback) { - stepInfo.step = 'finished'; - callIter( () => { - return callback(null, dupeEntries); - }); - } - ], - (err, dupeEntries) => { - if(err) { - return cb(err); - } - - return cb(null, fileEntry, dupeEntries); - } - ); -} - function scanFileAreaForChanges(areaInfo, options, iterator, cb) { if(3 === arguments.length && _.isFunction(iterator)) { cb = iterator; From ca0149eaf02d268a93892b58881fd230ef9638f8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 18 Jun 2018 20:17:56 -0600 Subject: [PATCH 147/569] Fix rare race crash in CombatNet module --- core/combatnet.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/combatnet.js b/core/combatnet.js index 217e6d17..8fb92de1 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -45,7 +45,9 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.term.write('Connecting to CombatNet, please wait...\n'); const restorePipeToNormal = function() { - self.client.term.output.removeListener('data', sendToRloginBuffer); + if(self.client.term.output) { + self.client.term.output.removeListener('data', sendToRloginBuffer); + } }; const rlogin = new RLogin( From 1fe46894d36a7bb2c7d45d1cf39847277b6ca6bf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 20 Jun 2018 19:57:06 -0600 Subject: [PATCH 148/569] More Hot-Reload related changes * Config.get(): Returns the latest config * Update code all over the place to use Config.get() vs Config.conf (which will be deprecated) --- WHATSNEW.md | 3 +- core/archive_util.js | 22 ++++---- core/art.js | 4 +- core/asset.js | 4 +- core/client.js | 8 +-- core/config.js | 10 ++-- core/config_util.js | 13 ++--- core/dropfile.js | 8 +-- core/email.js | 9 ++-- core/enigma_assert.js | 4 +- core/event_scheduler.js | 7 +-- core/exodus.js | 4 +- core/file_area_list.js | 7 ++- core/file_area_web.js | 10 ++-- core/file_base_area.js | 43 ++++++++-------- core/file_base_list_export.js | 8 +-- core/file_base_web_download_manager.js | 7 +-- core/file_entry.js | 7 +-- core/file_transfer.js | 9 ++-- core/file_transfer_protocol_select.js | 4 +- core/fse.js | 4 +- core/ftn_util.js | 24 ++++----- core/logger.js | 2 +- core/menu_module.js | 4 +- core/menu_util.js | 8 +-- core/message_area.js | 32 +++++++----- core/module_util.js | 15 +++--- core/nua.js | 9 ++-- core/oputil/oputil_config.js | 14 ++--- core/oputil/oputil_file_base.js | 4 +- core/predefined_mci.js | 4 +- core/scanner_tossers/ftn_bso.js | 71 ++++++++++++++------------ core/servers/content/gopher.js | 30 ++++++----- core/servers/content/web.js | 44 ++++++++-------- core/servers/login/ssh.js | 27 +++++----- core/servers/login/telnet.js | 11 ++-- core/servers/login/websocket.js | 8 +-- core/show_art.js | 4 +- core/system_view_validate.js | 16 +++--- core/theme.js | 43 ++++++++-------- core/user.js | 11 ++-- core/web_password_reset.js | 17 +++--- 42 files changed, 320 insertions(+), 273 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 99c47786..27f9380d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -10,7 +10,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Setting the `data` member of an object will cause form submissions to use this value instead of the selected items index. * See the default `luciano_blocktronics` `matrix` menu for example usage. * You can now set the `sort` property on a menu to sort items. If `true` items are sorted by `text`. If the value is a string, it represents the key in menu objects to sort by. - +* Hot-reload of configuration files such as menu.hjson, config.hjson, your themes.hjson, etc.: When a file is saved, it will be hot-reloaded into the running system + * Note that any custom modules should make use of the new Config.get() method. ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. diff --git a/core/archive_util.js b/core/archive_util.js index 71bad641..e4604b62 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const stringFormat = require('./string_format.js'); const Errors = require('./enig_error.js').Errors; const resolveMimeType = require('./mime_util.js').resolveMimeType; @@ -61,10 +61,11 @@ module.exports = class ArchiveUtil { // // Load configuration // - if(_.has(Config, 'archives.archivers')) { - Object.keys(Config.archives.archivers).forEach(archKey => { + const config = Config(); + if(_.has(config, 'archives.archivers')) { + Object.keys(config.archives.archivers).forEach(archKey => { - const archConfig = Config.archives.archivers[archKey]; + const archConfig = config.archives.archivers[archKey]; const archiver = new Archiver(archConfig); if(!archiver.ok()) { @@ -75,7 +76,7 @@ module.exports = class ArchiveUtil { }); } - if(_.isObject(Config.fileTypes)) { + if(_.isObject(config.fileTypes)) { const updateSig = (ft) => { ft.sig = Buffer.from(ft.sig, 'hex'); ft.offset = ft.offset || 0; @@ -87,8 +88,8 @@ module.exports = class ArchiveUtil { } }; - Object.keys(Config.fileTypes).forEach(mimeType => { - const fileType = Config.fileTypes[mimeType]; + Object.keys(config.fileTypes).forEach(mimeType => { + const fileType = config.fileTypes[mimeType]; if(Array.isArray(fileType)) { fileType.forEach(ft => { if(ft.sig) { @@ -109,7 +110,8 @@ module.exports = class ArchiveUtil { return; } - let fileType = _.get(Config, [ 'fileTypes', mimeType ] ); + const config = Config(); + let fileType = _.get(config, [ 'fileTypes', mimeType ] ); if(Array.isArray(fileType)) { if(!justExtention) { @@ -125,7 +127,7 @@ module.exports = class ArchiveUtil { } if(fileType.archiveHandler) { - return _.get( Config, [ 'archives', 'archivers', fileType.archiveHandler ] ); + return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] ); } } @@ -151,7 +153,7 @@ module.exports = class ArchiveUtil { return cb(err); } - const archFormat = _.findKey(Config.fileTypes, fileTypeInfo => { + const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => { const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ]; return fileTypeInfos.find(fti => { if(!fti.sig || !fti.archiveHandler) { diff --git a/core/art.js b/core/art.js index 9234542d..dc873836 100644 --- a/core/art.js +++ b/core/art.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const miscUtil = require('./misc_util.js'); const ansi = require('./ansi_term.js'); const aep = require('./ansi_escape_parser.js'); @@ -126,7 +126,7 @@ function getArtFromPath(path, options, cb) { function getArt(name, options, cb) { const ext = paths.extname(name); - options.basePath = miscUtil.valueWithDefault(options.basePath, Config.paths.art); + options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); // :TODO: make use of asAnsi option and convert from supported -> ansi diff --git a/core/asset.js b/core/asset.js index 3f44a604..43c881c0 100644 --- a/core/asset.js +++ b/core/asset.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const StatLog = require('./stat_log.js'); // deps @@ -95,7 +95,7 @@ function resolveConfigAsset(spec) { assert('config' === asset.type); const path = asset.asset.split('.'); - let conf = Config; + let conf = Config(); for(let i = 0; i < path.length; ++i) { if(_.isUndefined(conf[path[i]])) { return spec; diff --git a/core/client.js b/core/client.js index d3692cd5..10409c70 100644 --- a/core/client.js +++ b/core/client.js @@ -35,7 +35,7 @@ const term = require('./client_term.js'); const ansi = require('./ansi_term.js'); const User = require('./user.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; const MenuStack = require('./menu_stack.js'); const ACS = require('./acs.js'); const Events = require('./events.js'); @@ -400,7 +400,7 @@ function Client(/*input, output*/) { } if(key || ch) { - if(Config.logging.traceUserKeyboardInput) { + if(Config().logging.traceUserKeyboardInput) { self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line } @@ -440,8 +440,8 @@ Client.prototype.startIdleMonitor = function() { const nowMs = Date.now(); const idleLogoutSeconds = this.user.isAuthenticated() ? - Config.misc.idleLogoutSeconds : - Config.misc.preAuthIdleLogoutSeconds; + Config().misc.idleLogoutSeconds : + Config().misc.preAuthIdleLogoutSeconds; if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { this.emit('idle timeout'); diff --git a/core/config.js b/core/config.js index d8f1c873..38962b1f 100644 --- a/core/config.js +++ b/core/config.js @@ -13,6 +13,8 @@ const assert = require('assert'); exports.init = init; exports.getDefaultPath = getDefaultPath; +let currentConfiguration = {}; + function hasMessageConferenceAndArea(config) { assert(_.isObject(config.messageConferences)); // we create one ourself! @@ -67,12 +69,10 @@ function mergeValidateAndFinalize(config, cb) { return callback(null, mergedConfig); }, function setIt(mergedConfig, callback) { - exports.config = mergedConfig; - - exports.config.get = (path) => { - return _.get(exports.config, path); - }; + // :TODO: .config property is to be deprecated once conversions are done + exports.config = currentConfiguration = mergedConfig; + exports.get = () => currentConfiguration; return callback(null); } ], diff --git a/core/config_util.js b/core/config_util.js index 94ce4344..4b7ce5ed 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const ConfigCache = require('./config_cache.js'); const Events = require('./events.js'); @@ -15,7 +15,7 @@ exports.getFullConfig = getFullConfig; function getConfigPath(filePath) { // |filePath| is assumed to be in the config path if it's only a file name if('.' === paths.dirname(filePath)) { - filePath = paths.join(Config.paths.config, filePath); + filePath = paths.join(Config().paths.config, filePath); } return filePath; } @@ -24,19 +24,20 @@ function init(cb) { // pre-cache menu.hjson and prompt.hjson + establish events const changed = ( { fileName, fileRoot } ) => { const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === getConfigPath(Config.general.menuFile)) { + if(reCachedPath === getConfigPath(Config().general.menuFile)) { Events.emit(Events.getSystemEvents().MenusChanged); - } else if(reCachedPath === getConfigPath(Config.general.promptFile)) { + } else if(reCachedPath === getConfigPath(Config().general.promptFile)) { Events.emit(Events.getSystemEvents().PromptsChanged); } }; + const config = Config(); async.series( [ function menu(callback) { return ConfigCache.getConfigWithOptions( { - filePath : getConfigPath(Config.general.menuFile), + filePath : getConfigPath(config.general.menuFile), callback : changed, }, callback @@ -45,7 +46,7 @@ function init(cb) { function prompt(callback) { return ConfigCache.getConfigWithOptions( { - filePath : getConfigPath(Config.general.promptFile), + filePath : getConfigPath(config.general.promptFile), callback : changed, }, callback diff --git a/core/dropfile.js b/core/dropfile.js index 20c027e3..bdb3f3d1 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -var Config = require('./config.js').config; +var Config = require('./config.js').get; const StatLog = require('./stat_log.js'); var fs = require('graceful-fs'); @@ -29,7 +29,7 @@ function DropFile(client, fileType) { Object.defineProperty(this, 'fullPath', { get : function() { - return paths.join(Config.paths.dropFiles, ('node' + self.client.node), self.fileName); + return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName); } }); @@ -157,7 +157,7 @@ function DropFile(client, fileType) { // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! '57600', - Config.general.boardName, + Config().general.boardName, self.client.user.userId.toString(), self.client.user.properties.real_name || self.client.user.username, self.client.user.username, @@ -183,7 +183,7 @@ function DropFile(client, fileType) { var secLevel = self.client.user.getLegacySecurityLevel().toString(); return iconv.encode( [ - Config.general.boardName, // "The name of the system." + Config().general.boardName, // "The name of the system." opUn, // "The sysop's name up to the first space." opUn, // "The sysop's name following the first space." 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." diff --git a/core/email.js b/core/email.js index 0daf06b2..5cc66836 100644 --- a/core/email.js +++ b/core/email.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Errors = require('./enig_error.js').Errors; const Log = require('./logger.js').log; @@ -13,13 +13,14 @@ const nodeMailer = require('nodemailer'); exports.sendMail = sendMail; function sendMail(message, cb) { - if(!_.has(Config, 'email.transport')) { + const config = Config(); + if(!_.has(config, 'email.transport')) { return cb(Errors.MissingConfig('Email "email::transport" configuration missing')); } - message.from = message.from || Config.email.defaultFrom; + message.from = message.from || config.email.defaultFrom; - const transportOptions = Object.assign( {}, Config.email.transport, { + const transportOptions = Object.assign( {}, config.email.transport, { logger : Log, }); diff --git a/core/enigma_assert.js b/core/enigma_assert.js index 9217ea49..2b72227a 100644 --- a/core/enigma_assert.js +++ b/core/enigma_assert.js @@ -2,14 +2,14 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Log = require('./logger.js').log; // deps const assert = require('assert'); module.exports = function(condition, message) { - if(Config.debug.assertsEnabled) { + if(Config().debug.assertsEnabled) { assert.apply(this, arguments); } else if(!(condition)) { const stack = new Error().stack; diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 10c59f72..e425d3bd 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -3,7 +3,7 @@ // ENiGMA½ const PluginModule = require('./plugin_module.js').PluginModule; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Log = require('./logger.js').log; const _ = require('lodash'); @@ -155,8 +155,9 @@ class ScheduledEvent { function EventSchedulerModule(options) { PluginModule.call(this, options); - if(_.has(Config, 'eventScheduler')) { - this.moduleConfig = Config.eventScheduler; + const config = Config(); + if(_.has(config, 'eventScheduler')) { + this.moduleConfig = config.eventScheduler; } const self = this; diff --git a/core/exodus.js b/core/exodus.js index 129671cb..409b5f2a 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -4,7 +4,7 @@ // ENiGMA½ const MenuModule = require('./menu_module.js').MenuModule; const resetScreen = require('./ansi_term.js').resetScreen; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Errors = require('./enig_error.js').Errors; const Log = require('./logger.js').log; const getEnigmaUserAgent = require('./misc_util.js').getEnigmaUserAgent; @@ -66,7 +66,7 @@ exports.getModule = class ExodusModule extends MenuModule { this.config.sshHost = this.config.sshHost || this.config.ticketHost; this.config.sshPort = this.config.sshPort || 22; this.config.sshUser = this.config.sshUser || 'exodus_server'; - this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config.paths.misc, 'exodus.id_rsa'); + this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); } initSequence() { diff --git a/core/file_area_list.js b/core/file_area_list.js index b3d55e2c..ae03ee11 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -12,7 +12,7 @@ const FileArea = require('./file_base_area.js'); const Errors = require('./enig_error.js').Errors; const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; const ArchiveUtil = require('./archive_util.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; const DownloadQueue = require('./download_queue.js'); const FileAreaWeb = require('./file_area_web.js'); const FileBaseFilters = require('./file_base_filter.js'); @@ -255,7 +255,7 @@ exports.getModule = class FileAreaList extends MenuModule { const mimeType = resolveMimeType(entryInfo.archiveType); let desc; if(mimeType) { - let fileType = _.get(Config, [ 'fileTypes', mimeType ] ); + let fileType = _.get(Config(), [ 'fileTypes', mimeType ] ); if(Array.isArray(fileType)) { // further refine by extention @@ -264,7 +264,6 @@ exports.getModule = class FileAreaList extends MenuModule { desc = fileType && fileType.desc; } entryInfo.archiveTypeDesc = desc || mimeType || entryInfo.archiveType; - //entryInfo.archiveTypeDesc = mimeType ? _.get(Config, [ 'fileTypes', mimeType, 'desc' ] ) || mimeType : entryInfo.archiveType; } else { entryInfo.archiveTypeDesc = 'N/A'; } @@ -510,7 +509,7 @@ exports.getModule = class FileAreaList extends MenuModule { return callback(null); } - const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); + const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); FileAreaWeb.createAndServeTempDownload( self.client, diff --git a/core/file_area_web.js b/core/file_area_web.js index 928be942..94a2a664 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const FileDb = require('./database.js').dbs.file; const getISOTimestampString = require('./database.js').getISOTimestampString; const FileEntry = require('./file_entry.js'); @@ -31,7 +31,7 @@ function notEnabledError() { class FileAreaWebAccess { constructor() { - this.hashids = new hashids(Config.general.boardName); + this.hashids = new hashids(Config().general.boardName); this.expireTimers = {}; // hashId->timer } @@ -52,7 +52,7 @@ class FileAreaWebAccess { if(self.isEnabled()) { const routeAdded = self.webServer.instance.addRoute({ method : 'GET', - path : Config.fileBase.web.routePath, + path : Config().fileBase.web.routePath, handler : self.routeWebRequest.bind(self), }); return callback(routeAdded ? null : Errors.General('Failed adding route')); @@ -184,11 +184,11 @@ class FileAreaWebAccess { buildSingleFileTempDownloadLink(client, fileEntry, hashId) { hashId = hashId || this.getSingleFileHashId(client, fileEntry); - return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`); + return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); } buildBatchArchiveTempDownloadLink(client, hashId) { - return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`); + return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); } getExistingTempDownloadServeItem(client, fileEntry, cb) { diff --git a/core/file_base_area.js b/core/file_base_area.js index 3e0f9ff2..6400ed6f 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Errors = require('./enig_error.js').Errors; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; const FileEntry = require('./file_entry.js'); @@ -66,7 +66,7 @@ function getAvailableFileAreas(client, options) { options = options || { }; // perform ACS check per conf & omit internal if desired - const allAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); + const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); return _.omitBy(allAreas, areaInfo => { if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { @@ -96,16 +96,17 @@ function getSortedAvailableFileAreas(client, options) { } function getDefaultFileAreaTag(client, disableAcsCheck) { - let defaultArea = _.findKey(Config.fileBase, o => o.default); + const config = Config(); + let defaultArea = _.findKey(config.fileBase, o => o.default); if(defaultArea) { - const area = Config.fileBase.areas[defaultArea]; + const area = config.fileBase.areas[defaultArea]; if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) { return defaultArea; } } // just use anything we can - defaultArea = _.findKey(Config.fileBase.areas, (area, areaTag) => { + defaultArea = _.findKey(config.fileBase.areas, (area, areaTag) => { return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area)); }); @@ -113,7 +114,7 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { } function getFileAreaByTag(areaTag) { - const areaInfo = Config.fileBase.areas[areaTag]; + const areaInfo = Config().fileBase.areas[areaTag]; if(areaInfo) { areaInfo.areaTag = areaTag; // convienence! areaInfo.storage = getAreaStorageLocations(areaInfo); @@ -157,13 +158,14 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) { } function isValidStorageTag(storageTag) { - return storageTag in Config.fileBase.storageTags; + return storageTag in Config().fileBase.storageTags; } function getAreaStorageDirectoryByTag(storageTag) { - const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]); + const config = Config(); + const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); - return paths.resolve(Config.fileBase.areaStoragePrefix, storageLocation || ''); + return paths.resolve(config.fileBase.areaStoragePrefix, storageLocation || ''); } function getAreaDefaultStorageDirectory(areaInfo) { @@ -176,7 +178,7 @@ function getAreaStorageLocations(areaInfo) { areaInfo.storageTags : [ areaInfo.storageTags || '' ]; - const avail = Config.fileBase.storageTags; + const avail = Config().fileBase.storageTags; return _.compact(storageTags.map(storageTag => { if(avail[storageTag]) { @@ -233,7 +235,7 @@ function sliceAtSauceMarker(data) { function attemptSetEstimatedReleaseDate(fileEntry) { // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time - const patterns = Config.fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); + const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); function getMatch(input) { if(input) { @@ -290,11 +292,11 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { function extractDescFiles(callback) { // :TODO: would be nice if these RegExp's were cached // :TODO: this is long winded... - + const config = Config(); const extractList = []; const shortDescFile = archiveEntries.find( e => { - return Config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + return config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); }); if(shortDescFile) { @@ -302,7 +304,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } const longDescFile = archiveEntries.find( e => { - return Config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + return config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) ); }); if(longDescFile) { @@ -334,6 +336,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { }); }, function readDescFiles(descFiles, callback) { + const config = Config(); async.each(Object.keys(descFiles), (descType, next) => { const path = descFiles[descType]; if(!path) { @@ -346,10 +349,9 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } // skip entries that are too large - const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; - - if(Config.fileBase[maxFileSizeKey] && stats.size > Config.fileBase[maxFileSizeKey]) { - logDebug( { byteSize : stats.size, maxByteSize : Config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); + const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; + if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) { + logDebug( { byteSize : stats.size, maxByteSize : config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); return next(null); } @@ -488,7 +490,8 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c } function getInfoExtractUtilForDesc(mimeType, filePath, descType) { - let fileType = _.get(Config, [ 'fileTypes', mimeType ] ); + const config = Config(); + let fileType = _.get(config, [ 'fileTypes', mimeType ] ); if(Array.isArray(fileType)) { // further refine by extention @@ -504,7 +507,7 @@ function getInfoExtractUtilForDesc(mimeType, filePath, descType) { return; } - util = _.get(Config, [ 'infoExtractUtils', util ]); + util = _.get(config, [ 'infoExtractUtils', util ]); if(!util || !_.isString(util.cmd)) { return; } diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index 442946e3..8b45da83 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -5,7 +5,7 @@ const stringFormat = require('./string_format.js'); const FileEntry = require('./file_entry.js'); const FileArea = require('./file_base_area.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; const { Errors } = require('./enig_error.js'); const { splitTextAtTerms, @@ -64,12 +64,14 @@ function exportFileList(filterCriteria, options, cb) { { name : options.headerTemplate, req : false }, { name : options.entryTemplate, req : true } ]; + + const config = Config(); async.map(templateFiles, (template, nextTemplate) => { if(!template.name && !template.req) { return nextTemplate(null, Buffer.from([])); } - template.name = paths.isAbsolute(template.name) ? template.name : paths.join(Config.paths.misc, template.name); + template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name); fs.readFile(template.name, (err, data) => { return nextTemplate(err, data); }); @@ -221,7 +223,7 @@ function exportFileList(filterCriteria, options, cb) { const headerFormatObj = { nowTs : moment().format(options.tsFormat), - boardName : Config.general.boardName, + boardName : Config().general.boardName, totalFileCount : totals.fileCount, totalFileSize : totals.bytes, filterAreaTag : filterCriteria.areaTag || '-ALL-', diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index 13b7de33..f046de86 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -11,7 +11,7 @@ const Errors = require('./enig_error.js').Errors; const stringFormat = require('./string_format.js'); const FileAreaWeb = require('./file_area_web.js'); const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; -const Config = require('./config.js').config; +const Config = require('./config.js').get; // deps const async = require('async'); @@ -139,7 +139,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { } generateAndDisplayBatchLink(cb) { - const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); + const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); FileAreaWeb.createAndServeTempBatchDownload( this.client, @@ -183,6 +183,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { function prepareQueueDownloadLinks(callback) { const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + const config = Config(); async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => { FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { if(err) { @@ -190,7 +191,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { return nextFileEntry(err); // we should have caught this prior } - const expireTime = moment().add(Config.fileBase.web.expireMinutes, 'minutes'); + const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); FileAreaWeb.createAndServeTempDownload( self.client, diff --git a/core/file_entry.js b/core/file_entry.js index f6b86c64..1310d40a 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -7,7 +7,7 @@ const { getISOTimestampString, sanatizeString } = require('./database.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; // deps const async = require('async'); @@ -202,7 +202,8 @@ module.exports = class FileEntry { } static getAreaStorageDirectoryByTag(storageTag) { - const storageLocation = (storageTag && Config.fileBase.storageTags[storageTag]); + const config = Config(); + const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); // absolute paths as-is if(storageLocation && '/' === storageLocation.charAt(0)) { @@ -210,7 +211,7 @@ module.exports = class FileEntry { } // relative to |areaStoragePrefix| - return paths.join(Config.fileBase.areaStoragePrefix, storageLocation || ''); + return paths.join(config.fileBase.areaStoragePrefix, storageLocation || ''); } get filePath() { diff --git a/core/file_transfer.js b/core/file_transfer.js index a7aee20c..456898c9 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -3,7 +3,7 @@ // enigma-bbs const MenuModule = require('./menu_module.js').MenuModule; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const stringFormat = require('./string_format.js'); const Errors = require('./enig_error.js').Errors; const DownloadQueue = require('./download_queue.js'); @@ -56,9 +56,10 @@ exports.getModule = class TransferFileModule extends MenuModule { // // Most options can be set via extraArgs or config block // + const config = Config(); if(options.extraArgs) { if(options.extraArgs.protocol) { - this.protocolConfig = Config.fileTransferProtocols[options.extraArgs.protocol]; + this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol]; } if(options.extraArgs.direction) { @@ -78,7 +79,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } } else { if(this.config.protocol) { - this.protocolConfig = Config.fileTransferProtocols[this.config.protocol]; + this.protocolConfig = config.fileTransferProtocols[this.config.protocol]; } if(this.config.direction) { @@ -98,7 +99,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } } - this.protocolConfig = this.protocolConfig || Config.fileTransferProtocols.zmodem8kSz; // try for *something* + this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* this.direction = this.direction || 'send'; this.sendQueue = this.sendQueue || []; diff --git a/core/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js index 299c8af2..3d3bd37b 100644 --- a/core/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -3,7 +3,7 @@ // enigma-bbs const MenuModule = require('./menu_module.js').MenuModule; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const stringFormat = require('./string_format.js'); const ViewController = require('./view_controller.js').ViewController; @@ -129,7 +129,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { } loadAvailProtocols() { - this.protocols = _.map(Config.fileTransferProtocols, (protInfo, protocol) => { + this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { return { protocol : protocol, name : protInfo.name, diff --git a/core/fse.js b/core/fse.js index 70b1ecec..736f735a 100644 --- a/core/fse.js +++ b/core/fse.js @@ -14,7 +14,7 @@ const StatLog = require('./stat_log.js'); const stringFormat = require('./string_format.js'); const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const { isAnsi, cleanControlCodes, insert } = require('./string_util.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; const { getAddressedToInfo } = require('./mail_util.js'); // deps @@ -335,7 +335,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // 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' } }; + msgOpts.meta = { System : { 'explicit_encoding' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; } } diff --git a/core/ftn_util.js b/core/ftn_util.js index d1f69e0f..3fc51cd3 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -1,18 +1,17 @@ /* jslint node: true */ 'use strict'; -let Config = require('./config.js').config; -let Address = require('./ftn_address.js'); -let FNV1a = require('./fnv1a.js'); +const Config = require('./config.js').get; +const Address = require('./ftn_address.js'); +const FNV1a = require('./fnv1a.js'); const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion; -let _ = require('lodash'); -let iconv = require('iconv-lite'); -let moment = require('moment'); -//let uuid = require('node-uuid'); -let os = require('os'); +const _ = require('lodash'); +const iconv = require('iconv-lite'); +const moment = require('moment'); +const os = require('os'); -let packageJson = require('../package.json'); +const packageJson = require('../package.json'); // :TODO: Remove "Ftn" from most of these -- it's implied in the module exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; @@ -199,9 +198,10 @@ function getQuotePrefix(name) { // http://ftsc.org/docs/fts-0004.001 // function getOrigin(address) { - const origin = _.has(Config, 'messageNetworks.originLine') ? - Config.messageNetworks.originLine : - Config.general.boardName; + const config = Config(); + const origin = _.has(config, 'messageNetworks.originLine') ? + config.messageNetworks.originLine : + config.general.boardName; const addrStr = new Address(address).toString('5D'); return ` * Origin: ${origin} (${addrStr})`; diff --git a/core/logger.js b/core/logger.js index 063757ff..c9a75faf 100644 --- a/core/logger.js +++ b/core/logger.js @@ -10,7 +10,7 @@ const _ = require('lodash'); module.exports = class Log { static init() { - const Config = require('./config.js').config; + const Config = require('./config.js').get(); const logPath = Config.paths.logs; const err = this.checkLogPath(logPath); diff --git a/core/menu_module.js b/core/menu_module.js index 97536bc7..20680354 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -6,7 +6,7 @@ const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); const ViewController = require('./view_controller.js').ViewController; const menuUtil = require('./menu_util.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; const stringFormat = require('../core/string_format.js'); const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; const Errors = require('../core/enig_error.js').Errors; @@ -29,7 +29,7 @@ exports.MenuModule = class MenuModule extends PluginModule { this.menuMethods = {}; // methods called from @method's this.menuConfig.config = this.menuConfig.config || {}; - this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config.menus.cls; + this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; this.viewControllers = {}; } diff --git a/core/menu_util.js b/core/menu_util.js index 4a4665b8..c6ad3a85 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -4,7 +4,7 @@ // ENiGMA½ var moduleUtil = require('./module_util.js'); var Log = require('./logger.js').log; -var Config = require('./config.js').config; +var Config = require('./config.js').get; var asset = require('./asset.js'); var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; @@ -75,7 +75,7 @@ function loadMenu(options, cb) { const modLoadOpts = { name : modSupplied ? modAsset.asset : 'standard_menu', - path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config.paths.mods, + path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', }; @@ -189,7 +189,7 @@ function handleAction(client, formData, conf, cb) { return callModuleMenuMethod( client, actionAsset, - paths.join(Config.paths.mods, actionAsset.location), + paths.join(Config().paths.mods, actionAsset.location), formData, conf.extraArgs, cb); @@ -234,7 +234,7 @@ function handleNext(client, nextSpec, conf, cb) { case 'method' : case 'systemMethod' : if(_.isString(nextAsset.location)) { - return callModuleMenuMethod(client, nextAsset, paths.join(Config.paths.mods, nextAsset.location), {}, extraArgs, cb); + return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb); } else if('systemMethod' === nextAsset.type) { // :TODO: see other notes about system_menu_method.js here return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb); diff --git a/core/message_area.js b/core/message_area.js index 985da046..44a94c9a 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -3,7 +3,7 @@ // ENiGMA½ const msgDb = require('./database.js').dbs.message; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Message = require('./message.js'); const Log = require('./logger.js').log; const msgNetRecord = require('./msg_network.js').recordMessage; @@ -40,7 +40,7 @@ function getAvailableMessageConferences(client, options) { assert(client || true === options.noClient); // perform ACS check per conf & omit system_internal if desired - return _.omitBy(Config.messageConferences, (conf, confTag) => { + return _.omitBy(Config().messageConferences, (conf, confTag) => { if(!options.includeSystemInternal && 'system_internal' === confTag) { return true; } @@ -68,8 +68,9 @@ function getAvailableMessageAreasByConfTag(confTag, options) { // :TODO: confTag === "" then find default - if(_.has(Config.messageConferences, [ confTag, 'areas' ])) { - const areas = Config.messageConferences[confTag].areas; + const config = Config(); + if(_.has(config.messageConferences, [ confTag, 'areas' ])) { + const areas = config.messageConferences[confTag].areas; if(!options.client || true === options.noAcsCheck) { // everything - no ACS checks @@ -109,16 +110,17 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) { // // Note that built in 'system_internal' is always ommited here // - let defaultConf = _.findKey(Config.messageConferences, o => o.default); + const config = Config(); + let defaultConf = _.findKey(config.messageConferences, o => o.default); if(defaultConf) { - const conf = Config.messageConferences[defaultConf]; + const conf = config.messageConferences[defaultConf]; if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { return defaultConf; } } // just use anything we can - defaultConf = _.findKey(Config.messageConferences, (conf, confTag) => { + defaultConf = _.findKey(config.messageConferences, (conf, confTag) => { return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)); }); @@ -135,8 +137,9 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { // confTag = confTag || getDefaultMessageConferenceTag(client); - if(confTag && _.has(Config.messageConferences, [ confTag, 'areas' ])) { - const areaPool = Config.messageConferences[confTag].areas; + const config = Config(); + if(confTag && _.has(config.messageConferences, [ confTag, 'areas' ])) { + const areaPool = config.messageConferences[confTag].areas; let defaultArea = _.findKey(areaPool, o => o.default); if(defaultArea) { const area = areaPool[defaultArea]; @@ -154,18 +157,18 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { } function getMessageConferenceByTag(confTag) { - return Config.messageConferences[confTag]; + return Config().messageConferences[confTag]; } function getMessageConfTagByAreaTag(areaTag) { - const confs = Config.messageConferences; + const confs = Config().messageConferences; return Object.keys(confs).find( (confTag) => { return _.has(confs, [ confTag, 'areas', areaTag]); }); } function getMessageAreaByTag(areaTag, optionalConfTag) { - const confs = Config.messageConferences; + const confs = Config().messageConferences; // :TODO: this could be cached if(_.isString(optionalConfTag)) { @@ -535,10 +538,11 @@ function trimMessageAreasScheduledEvent(args, cb) { let areaInfos = []; // determine maxMessages & maxAgeDays per area + const config = Config(); areaTags.forEach(areaTag => { - let maxMessages = Config.messageAreaDefaults.maxMessages; - let maxAgeDays = Config.messageAreaDefaults.maxAgeDays; + let maxMessages = config.messageAreaDefaults.maxMessages; + let maxAgeDays = config.messageAreaDefaults.maxAgeDays; const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here if(area) { diff --git a/core/module_util.js b/core/module_util.js index 67e87306..3bfd88d2 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; // deps const fs = require('graceful-fs'); @@ -64,7 +64,7 @@ function loadModuleEx(options, cb) { } function loadModule(name, category, cb) { - const path = Config.paths[category]; + const path = Config().paths[category]; if(!_.isString(path)) { return cb(new Error(`Not sure where to look for "${name}" of category "${category}"`)); @@ -77,7 +77,7 @@ function loadModule(name, category, cb) { function loadModulesForCategory(category, iterator, complete) { - fs.readdir(Config.paths[category], (err, files) => { + fs.readdir(Config().paths[category], (err, files) => { if(err) { return iterator(err); } @@ -100,10 +100,11 @@ function loadModulesForCategory(category, iterator, complete) { } function getModulePaths() { + const config = Config(); return [ - Config.paths.mods, - Config.paths.loginServers, - Config.paths.contentServers, - Config.paths.scannerTossers, + config.paths.mods, + config.paths.loginServers, + config.paths.contentServers, + config.paths.scannerTossers, ]; } diff --git a/core/nua.js b/core/nua.js index 61dbeb62..cb7e16a7 100644 --- a/core/nua.js +++ b/core/nua.js @@ -6,7 +6,7 @@ const MenuModule = require('./menu_module.js').MenuModule; const User = require('./user.js'); const theme = require('./theme.js'); const login = require('./system_menu_method.js').login; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const messageArea = require('./message_area.js'); exports.moduleInfo = { @@ -62,6 +62,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { // submitApplication : function(formData, extraArgs, cb) { const newUser = new User(); + const config = Config(); newUser.username = formData.value.username; @@ -95,10 +96,10 @@ exports.getModule = class NewUserAppModule extends MenuModule { // :TODO: should probably have a place to create defaults/etc. }; - if('*' === Config.defaults.theme) { + if('*' === config.defaults.theme) { newUser.properties.theme_id = theme.getRandomTheme(); } else { - newUser.properties.theme_id = Config.defaults.theme; + newUser.properties.theme_id = config.defaults.theme; } // :TODO: User.create() should validate email uniqueness! @@ -118,7 +119,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { // Cache SysOp information now // :TODO: Similar to bbs.js. DRY if(newUser.isSysOp()) { - Config.general.sysOp = { + config.general.sysOp = { username : formData.value.username, properties : newUser.properties, }; diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 4d1eb54b..3edfc1f3 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -333,7 +333,7 @@ function importAreas() { }, function validateAndCollectInput(callback) { const msgArea = require('../../core/message_area.js'); - const Config = require('../../core/config.js').config; + const sysConfig = require('../../core/config.js').get(); let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } ); if(!msgConfs) { @@ -355,8 +355,8 @@ function importAreas() { } let existingNetworkNames = []; - if(_.has(Config, 'messageNetworks.ftn.networks')) { - existingNetworkNames = Object.keys(Config.messageNetworks.ftn.networks); + if(_.has(sysConfig, 'messageNetworks.ftn.networks')) { + existingNetworkNames = Object.keys(sysConfig.messageNetworks.ftn.networks); } if(0 === existingNetworkNames.length) { @@ -366,7 +366,7 @@ function importAreas() { if(networkName && !existingNetworkNames.find(net => networkName === net)) { return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`)); } - + getAnswers([ { name : 'confTag', @@ -407,13 +407,13 @@ function importAreas() { }); }, function confirmWithUser(callback) { - const Config = require('../../core/config.js').config; + const sysConfig = require('../../core/config.js').get(); - console.info(`Importing the following for "${confTag}" - (${Config.messageConferences[confTag].name} - ${Config.messageConferences[confTag].desc})`); + console.info(`Importing the following for "${confTag}" - (${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`); importEntries.forEach(ie => { console.info(` ${ie.ftnTag} - ${ie.name}`); }); - + console.info(''); console.info('Importing will NOT create required FTN network configurations.'); console.info('If you have not yet done this, you will need to complete additional steps after importing.'); diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 68f190fb..b9ae8aeb 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -390,10 +390,10 @@ function displayFileAreaInfo() { return initConfigAndDatabases(callback); }, function dumpInfo(callback) { - const Config = require('../../core/config.js').config; + const sysConfig = require('../../core/config.js').get(); let suppliedAreas = argv._.slice(2); if(!suppliedAreas || 0 === suppliedAreas.length) { - suppliedAreas = _.map(Config.fileBase.areas, (areaInfo, areaTag) => areaTag); + suppliedAreas = _.map(sysConfig.fileBase.areas, (areaInfo, areaTag) => areaTag); } const areaAndStorageInfo = getAreaAndStorage(suppliedAreas); diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 47370a66..c1f7e9fb 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Log = require('./logger.js').log; const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; @@ -56,7 +56,7 @@ const PREDEFINED_MCI_GENERATORS = { // // Board // - BN : function boardName() { return Config.general.boardName; }, + BN : function boardName() { return Config().general.boardName; }, // ENiGMA VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index a98c6f4d..2d978852 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -3,7 +3,7 @@ // ENiGMA½ const MessageScanTossModule = require('../msg_scan_toss_module.js').MessageScanTossModule; -const Config = require('../config.js').config; +const Config = require('../config.js').get; const ftnMailPacket = require('../ftn_mail_packet.js'); const ftnUtil = require('../ftn_util.js'); const Address = require('../ftn_address.js'); @@ -60,8 +60,9 @@ function FTNMessageScanTossModule() { this.archUtil = ArchiveUtil.getInstance(); - if(_.has(Config, 'scannerTossers.ftn_bso')) { - this.moduleConfig = Config.scannerTossers.ftn_bso; + const config = Config(); + if(_.has(config, 'scannerTossers.ftn_bso')) { + this.moduleConfig = config.scannerTossers.ftn_bso; } this.getDefaultNetworkName = function() { @@ -69,19 +70,20 @@ function FTNMessageScanTossModule() { return this.moduleConfig.defaultNetwork.toLowerCase(); } - const networkNames = Object.keys(Config.messageNetworks.ftn.networks); + const networkNames = Object.keys(config.messageNetworks.ftn.networks); if(1 === networkNames.length) { return networkNames[0].toLowerCase(); } }; this.getDefaultZone = function(networkName) { - if(_.isNumber(Config.messageNetworks.ftn.networks[networkName].defaultZone)) { - return Config.messageNetworks.ftn.networks[networkName].defaultZone; + const config = Config(); + if(_.isNumber(config.messageNetworks.ftn.networks[networkName].defaultZone)) { + return config.messageNetworks.ftn.networks[networkName].defaultZone; } // non-explicit: default to local address zone - const networkLocalAddress = Config.messageNetworks.ftn.networks[networkName].localAddress; + const networkLocalAddress = config.messageNetworks.ftn.networks[networkName].localAddress; if(networkLocalAddress) { const addr = Address.fromString(networkLocalAddress); return addr.zone; @@ -96,14 +98,14 @@ function FTNMessageScanTossModule() { */ this.getNetworkNameByAddress = function(remoteAddress) { - return _.findKey(Config.messageNetworks.ftn.networks, network => { + return _.findKey(Config().messageNetworks.ftn.networks, network => { const localAddress = Address.fromString(network.localAddress); return !_.isUndefined(localAddress) && localAddress.isEqual(remoteAddress); }); }; this.getNetworkNameByAddressPattern = function(remoteAddressPattern) { - return _.findKey(Config.messageNetworks.ftn.networks, network => { + return _.findKey(Config().messageNetworks.ftn.networks, network => { const localAddress = Address.fromString(network.localAddress); return !_.isUndefined(localAddress) && localAddress.isPatternMatch(remoteAddressPattern); }); @@ -111,7 +113,7 @@ function FTNMessageScanTossModule() { this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) { ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper - return _.findKey(Config.messageNetworks.ftn.areas, areaConf => { + return _.findKey(Config().messageNetworks.ftn.areas, areaConf => { return areaConf.tag.toUpperCase() === ftnAreaTag; }); }; @@ -357,6 +359,7 @@ function FTNMessageScanTossModule() { let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system + const config = Config(); if(self.isNetMailMessage(message)) { // // Set route and message destination properties -- they may differ @@ -405,14 +408,14 @@ function FTNMessageScanTossModule() { // // EchoMail requires some additional properties & kludges // - message.meta.FtnProperty.ftn_area = Config.messageNetworks.ftn.areas[message.areaTag].tag; + message.meta.FtnProperty.ftn_area = config.messageNetworks.ftn.areas[message.areaTag].tag; // // When exporting messages, we should create/update SEEN-BY // with remote address(s) we are exporting to. // const seenByAdditions = - [ `${localAddress.net}/${localAddress.node}` ].concat(Config.messageNetworks.ftn.areas[message.areaTag].uplinks); + [ `${localAddress.net}/${localAddress.node}` ].concat(config.messageNetworks.ftn.areas[message.areaTag].uplinks); message.meta.FtnProperty.ftn_seen_by = ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions); @@ -453,7 +456,7 @@ function FTNMessageScanTossModule() { // Determine CHRS and actual internal encoding name. If the message has an // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set. // - let encoding = options.nodeConfig.encoding || Config.scannerTossers.ftn_bso.packetMsgEncoding || 'utf8'; + let encoding = options.nodeConfig.encoding || config.scannerTossers.ftn_bso.packetMsgEncoding || 'utf8'; const explicitEncoding = _.get(message.meta, 'System.explicit_encoding'); if(explicitEncoding) { encoding = explicitEncoding; @@ -513,7 +516,7 @@ function FTNMessageScanTossModule() { this.hasValidConfiguration = function() { - if(!_.has(this, 'moduleConfig.nodes') || !_.has(Config, 'messageNetworks.ftn.areas')) { + if(!_.has(this, 'moduleConfig.nodes') || !_.has(Config(), 'messageNetworks.ftn.areas')) { return false; } @@ -820,7 +823,7 @@ function FTNMessageScanTossModule() { // // Route full|wildcard -> full adddress/network lookup // - const routes = _.get(Config, 'scannerTossers.ftn_bso.netMail.routes'); + const routes = _.get(Config(), 'scannerTossers.ftn_bso.netMail.routes'); if(!routes) { return; } @@ -860,7 +863,7 @@ function FTNMessageScanTossModule() { const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { return routeAddress.isPatternMatch(nodeAddrWildcard); - }) || { packetType : '2+', encoding : Config.scannerTossers.ftn_bso.packetMsgEncoding }; + }) || { packetType : '2+', encoding : Config().scannerTossers.ftn_bso.packetMsgEncoding }; // we should never be failing here; we may just be using defaults. return cb( @@ -899,7 +902,7 @@ function FTNMessageScanTossModule() { exportOpts.destAddress = dstAddr; exportOpts.routeAddress = routeInfo.routeAddress; exportOpts.fileCase = routeInfo.config.fileCase || 'lower'; - exportOpts.network = Config.messageNetworks.ftn.networks[routeInfo.networkName]; + exportOpts.network = Config().messageNetworks.ftn.networks[routeInfo.networkName]; exportOpts.networkName = routeInfo.networkName; exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); exportOpts.exportType = self.getExportType(routeInfo.config); @@ -966,6 +969,7 @@ function FTNMessageScanTossModule() { }; this.exportEchoMailMessagesToUplinks = function(messageUuids, areaConfig, cb) { + const config = Config(); async.each(areaConfig.uplinks, (uplink, nextUplink) => { const nodeConfig = self.getNodeConfigByAddress(uplink); if(!nodeConfig) { @@ -974,7 +978,7 @@ function FTNMessageScanTossModule() { const exportOpts = { nodeConfig, - network : Config.messageNetworks.ftn.networks[areaConfig.network], + network : config.messageNetworks.ftn.networks[areaConfig.network], destAddress : Address.fromString(uplink), networkName : areaConfig.network, fileCase : nodeConfig.fileCase || 'lower', @@ -1119,7 +1123,7 @@ function FTNMessageScanTossModule() { this.getLocalUserNameFromAlias = function(lookup) { lookup = lookup.toLowerCase(); - const aliases = _.get(Config, 'messageNetworks.ftn.netMail.aliases'); + const aliases = _.get(Config(), 'messageNetworks.ftn.netMail.aliases'); if(!aliases) { return lookup; // keep orig } @@ -1195,7 +1199,7 @@ function FTNMessageScanTossModule() { // a random UUID. Otherwise, don't assign the UUID just yet. It will be // generated at persist() time and should be consistent across import/exports // - if(true === _.get(Config, [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) { + if(true === _.get(Config(), [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) { // just generate a UUID & therefor always allow for dupes message.uuid = uuidV4(); } @@ -1650,7 +1654,8 @@ function FTNMessageScanTossModule() { }; this.getLocalAreaTagsForTic = function() { - return _.union(Object.keys(Config.scannerTossers.ftn_bso.ticAreas || {} ), Object.keys(Config.fileBase.areas)); + const config = Config(); + return _.union(Object.keys(config.scannerTossers.ftn_bso.ticAreas || {} ), Object.keys(config.fileBase.areas)); }; this.processSingleTicFile = function(ticFileInfo, cb) { @@ -1659,9 +1664,10 @@ function FTNMessageScanTossModule() { async.waterfall( [ function generalValidation(callback) { + const sysConfig = Config(); const config = { - nodes : Config.scannerTossers.ftn_bso.nodes, - defaultPassword : Config.scannerTossers.ftn_bso.tic.password, + nodes : sysConfig.scannerTossers.ftn_bso.nodes, + defaultPassword : sysConfig.scannerTossers.ftn_bso.tic.password, localAreaTags : self.getLocalAreaTagsForTic(), }; @@ -1672,7 +1678,7 @@ function FTNMessageScanTossModule() { } // We may need to map |localAreaTag| back to real areaTag if it's a mapping/alias - const mappedLocalAreaTag = _.get(Config.scannerTossers.ftn_bso, [ 'ticAreas', localInfo.areaTag ]); + const mappedLocalAreaTag = _.get(Config().scannerTossers.ftn_bso, [ 'ticAreas', localInfo.areaTag ]); if(mappedLocalAreaTag) { if(_.isString(mappedLocalAreaTag.areaTag)) { @@ -1699,7 +1705,7 @@ function FTNMessageScanTossModule() { // Lastly, we will only replace if the item is in the same/specified area // and that come from the same origin as a previous entry. // - const allowReplace = _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config.scannerTossers.ftn_bso.tic.allowReplace); + const allowReplace = _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config().scannerTossers.ftn_bso.tic.allowReplace); const replaces = ticFileInfo.getAsString('Replaces'); if(!allowReplace || !replaces) { @@ -1755,7 +1761,7 @@ function FTNMessageScanTossModule() { short_file_name : ticFileInfo.getAsString('File').toUpperCase(), // upper to ensure no case issues later; this should be a DOS 8.3 name tic_origin : ticFileInfo.getAsString('Origin'), tic_desc : ticFileInfo.getAsString('Desc'), - upload_by_username : _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config.scannerTossers.ftn_bso.tic.uploadBy), + upload_by_username : _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config().scannerTossers.ftn_bso.tic.uploadBy), } }; @@ -1769,7 +1775,7 @@ function FTNMessageScanTossModule() { // const hashTags = localInfo.hashTags || - _.get(Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/ + _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/ if(hashTags) { scanOpts.hashTags = new Set(hashTags.split(/[\s,]+/)); @@ -1817,8 +1823,8 @@ function FTNMessageScanTossModule() { // We will still fallback as needed from -> -> // const descPriority = _.get( - Config.scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ], - Config.scannerTossers.ftn_bso.tic.descPriority + Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ], + Config().scannerTossers.ftn_bso.tic.descPriority ); if('tic' === descPriority) { @@ -1926,11 +1932,12 @@ function FTNMessageScanTossModule() { ; // we shouldn't, but be sure we don't try to pick up private mail here - const areaTags = Object.keys(Config.messageNetworks.ftn.areas) + const config = Config(); + const areaTags = Object.keys(config.messageNetworks.ftn.areas) .filter(areaTag => Message.WellKnownAreaTags.Private !== areaTag); async.each(areaTags, (areaTag, nextArea) => { - const areaConfig = Config.messageNetworks.ftn.areas[areaTag]; + const areaConfig = config.messageNetworks.ftn.areas[areaTag]; if(!this.isAreaConfigValid(areaConfig)) { return nextArea(); } @@ -2336,7 +2343,7 @@ FTNMessageScanTossModule.prototype.record = function(message) { } else if(message.areaTag) { Object.assign(info, { type : 'EchoMail' } ); - const areaConfig = Config.messageNetworks.ftn.areas[message.areaTag]; + const areaConfig = Config().messageNetworks.ftn.areas[message.areaTag]; if(!this.isAreaConfigValid(areaConfig)) { return; } diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 5e0ee42f..d613052a 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -4,7 +4,7 @@ // ENiGMA½ const Log = require('../../logger.js').log; const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').config; +const Config = require('../../config.js').get; const { splitTextAtTerms, isAnsi, @@ -73,8 +73,9 @@ exports.getModule = class GopherModule extends ServerModule { return; } - this.publicHostname = Config.contentServers.gopher.publicHostname; - this.publicPort = Config.contentServers.gopher.publicPort; + const config = Config(); + this.publicHostname = config.contentServers.gopher.publicHostname; + this.publicPort = config.contentServers.gopher.publicPort; this.addRoute(/^\/?\r\n$/, this.defaultGenerator); this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); @@ -99,9 +100,10 @@ exports.getModule = class GopherModule extends ServerModule { return true; // nothing to do, but not an error } - const port = parseInt(Config.contentServers.gopher.port); + const config = Config(); + const port = parseInt(config.contentServers.gopher.port); if(isNaN(port)) { - this.log.warn( { port : Config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); + this.log.warn( { port : config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); return false; } @@ -109,13 +111,14 @@ exports.getModule = class GopherModule extends ServerModule { } get enabled() { - return _.get(Config, 'contentServers.gopher.enabled', false) && this.isConfigured(); + return _.get(Config(), 'contentServers.gopher.enabled', false) && this.isConfigured(); } isConfigured() { // public hostname & port must be set; responses contain them! - return _.isString(_.get(Config, 'contentServers.gopher.publicHostname')) && - _.isNumber(_.get(Config, 'contentServers.gopher.publicPort')); + const config = Config(); + return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) && + _.isNumber(_.get(config, 'contentServers.gopher.publicPort')); } addRoute(selectorRegExp, generatorHandler) { @@ -155,7 +158,7 @@ exports.getModule = class GopherModule extends ServerModule { defaultGenerator(selectorMatch, cb) { this.log.trace( { selector : selectorMatch[0] }, 'Serving default content'); - let bannerFile = _.get(Config, 'contentServers.gopher.bannerFile', 'startup_banner.asc'); + let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc'); bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); fs.readFile(bannerFile, 'utf8', (err, banner) => { if(err) { @@ -174,7 +177,7 @@ exports.getModule = class GopherModule extends ServerModule { } isAreaAndConfExposed(confTag, areaTag) { - const conf = _.get(Config, [ 'contentServers', 'gopher', 'messageConferences', confTag ]); + const conf = _.get(Config(), [ 'contentServers', 'gopher', 'messageConferences', confTag ]); return Array.isArray(conf) && conf.includes(areaTag); } @@ -281,13 +284,14 @@ ${msgBody} }); } else if(selectorMatch[1]) { // list areas in conf + const sysConfig = Config(); const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); - const conf = _.get(Config, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); + const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); if(!conf) { return this.notFoundGenerator(selectorMatch, cb); } - const areas = _.get(Config, [ 'contentServers', 'gopher', 'messageConferences', confTag ], {}) + const areas = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ], {}) .map(areaTag => Object.assign( { areaTag }, getMessageAreaByTag(areaTag))) .filter(area => area && !Message.isPrivateAreaTag(area.areaTag)); @@ -307,7 +311,7 @@ ${msgBody} return cb(response); } else { // message area base (list confs) - const confs = Object.keys(_.get(Config, 'contentServers.gopher.messageConferences', {})) + const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {})) .map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag))) .filter(conf => conf); // remove any baddies diff --git a/core/servers/content/web.js b/core/servers/content/web.js index d1edb221..47e0661f 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -4,7 +4,7 @@ // ENiGMA½ const Log = require('../../logger.js').log; const ServerModule = require('../../server_module.js').ServerModule; -const Config = require('../../config.js').config; +const Config = require('../../config.js').get; // deps const http = require('http'); @@ -55,12 +55,13 @@ exports.getModule = class WebServerModule extends ServerModule { constructor() { super(); - this.enableHttp = Config.contentServers.web.http.enabled || false; - this.enableHttps = Config.contentServers.web.https.enabled || false; + const config = Config(); + this.enableHttp = config.contentServers.web.http.enabled || false; + this.enableHttps = config.contentServers.web.https.enabled || false; this.routes = {}; - if(this.isEnabled() && Config.contentServers.web.staticRoot) { + if(this.isEnabled() && config.contentServers.web.staticRoot) { this.addRoute({ method : 'GET', path : '/static/.*$', @@ -77,25 +78,26 @@ exports.getModule = class WebServerModule extends ServerModule { // Prefer HTTPS over HTTP. Be explicit about the port // only if non-standard. Allow users to override full prefix in config. // - if(_.isString(Config.contentServers.web.overrideUrlPrefix)) { - return `${Config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`; + const config = Config(); + if(_.isString(config.contentServers.web.overrideUrlPrefix)) { + return `${config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`; } let schema; let port; - if(Config.contentServers.web.https.enabled) { + if(config.contentServers.web.https.enabled) { schema = 'https://'; - port = (443 === Config.contentServers.web.https.port) ? + port = (443 === config.contentServers.web.https.port) ? '' : - `:${Config.contentServers.web.https.port}`; + `:${config.contentServers.web.https.port}`; } else { schema = 'http://'; - port = (80 === Config.contentServers.web.http.port) ? + port = (80 === config.contentServers.web.http.port) ? '' : - `:${Config.contentServers.web.http.port}`; + `:${config.contentServers.web.http.port}`; } - return `${schema}${Config.contentServers.web.domain}${port}${pathAndQuery}`; + return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`; } isEnabled() { @@ -107,14 +109,15 @@ exports.getModule = class WebServerModule extends ServerModule { this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) ); } + const config = Config(); if(this.enableHttps) { const options = { - cert : fs.readFileSync(Config.contentServers.web.https.certPem), - key : fs.readFileSync(Config.contentServers.web.https.keyPem), + cert : fs.readFileSync(config.contentServers.web.https.certPem), + key : fs.readFileSync(config.contentServers.web.https.keyPem), }; // additional options - Object.assign(options, Config.contentServers.web.https.options || {} ); + Object.assign(options, config.contentServers.web.https.options || {} ); this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) ); } @@ -123,13 +126,14 @@ exports.getModule = class WebServerModule extends ServerModule { listen() { let ok = true; + const config = Config(); [ 'http', 'https' ].forEach(service => { const name = `${service}Server`; if(this[name]) { - const port = parseInt(Config.contentServers.web[service].port); + const port = parseInt(config.contentServers.web[service].port); if(isNaN(port)) { ok = false; - return Log.warn( { port : Config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); + return Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); } return this[name].listen(port); } @@ -167,7 +171,7 @@ exports.getModule = class WebServerModule extends ServerModule { } respondWithError(resp, code, bodyText, title) { - const customErrorPage = paths.join(Config.contentServers.web.staticRoot, `${code}.html`); + const customErrorPage = paths.join(Config().contentServers.web.staticRoot, `${code}.html`); fs.readFile(customErrorPage, 'utf8', (err, data) => { resp.writeHead(code, { 'Content-Type' : 'text/html' } ); @@ -202,14 +206,14 @@ exports.getModule = class WebServerModule extends ServerModule { } routeIndex(req, resp) { - const filePath = paths.join(Config.contentServers.web.staticRoot, 'index.html'); + const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html'); return this.returnStaticPage(filePath, resp); } routeStaticFile(req, resp) { const fileName = req.url.substr(req.url.indexOf('/', 1)); - const filePath = paths.join(Config.contentServers.web.staticRoot, fileName); + const filePath = paths.join(Config().contentServers.web.staticRoot, fileName); return this.returnStaticPage(filePath, resp); } diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index c50a7d70..0b8e3082 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('../../config.js').config; +const Config = require('../../config.js').get; const baseClient = require('../../client.js'); const Log = require('../../logger.js').log; const LoginServerModule = require('../../login_server_module.js'); @@ -42,7 +42,8 @@ function SSHClient(clientConn) { const username = ctx.username || ''; const password = ctx.password || ''; - self.isNewUser = (Config.users.newUserNames || []).indexOf(username) > -1; + const config = Config(); + self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); @@ -60,7 +61,7 @@ function SSHClient(clientConn) { // If the system is open and |isNewUser| is true, the login // sequence is hijacked in order to start the applicaiton process. // - if(false === Config.general.closedSystem && self.isNewUser) { + if(false === config.general.closedSystem && self.isNewUser) { return ctx.accept(); } @@ -99,7 +100,7 @@ function SSHClient(clientConn) { return alreadyLoggedIn(username); } - if(loginAttempts >= Config.general.loginAttempts) { + if(loginAttempts >= config.general.loginAttempts) { return terminateConnection(); } @@ -113,8 +114,8 @@ function SSHClient(clientConn) { if(err) { interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; } else { - const newUserNameList = _.has(Config, 'users.newUserNames') && Config.users.newUserNames.length > 0 ? - Config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : + const newUserNameList = _.has(config, 'users.newUserNames') && config.users.newUserNames.length > 0 ? + config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : '(No new user names enabled!)'; interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; @@ -203,7 +204,7 @@ function SSHClient(clientConn) { } // we're ready! - const firstMenu = self.isNewUser ? Config.loginServers.ssh.firstMenuNewUser : Config.loginServers.ssh.firstMenu; + const firstMenu = self.isNewUser ? Config().loginServers.ssh.firstMenuNewUser : Config().loginServers.ssh.firstMenu; self.emit('ready', { firstMenu : firstMenu } ); }); @@ -239,18 +240,19 @@ exports.getModule = class SSHServerModule extends LoginServerModule { } createServer() { + const config = Config(); const serverConf = { hostKeys : [ { - key : fs.readFileSync(Config.loginServers.ssh.privateKeyPem), - passphrase : Config.loginServers.ssh.privateKeyPass, + key : fs.readFileSync(config.loginServers.ssh.privateKeyPem), + passphrase : config.loginServers.ssh.privateKeyPass, } ], ident : 'enigma-bbs-' + enigVersion + '-srv', // Note that sending 'banner' breaks at least EtherTerm! debug : (sshDebugLine) => { - if(true === Config.loginServers.ssh.traceConnections) { + if(true === config.loginServers.ssh.traceConnections) { Log.trace(`SSH: ${sshDebugLine}`); } }, @@ -265,9 +267,10 @@ exports.getModule = class SSHServerModule extends LoginServerModule { } listen() { - const port = parseInt(Config.loginServers.ssh.port); + const config = Config(); + const port = parseInt(config.loginServers.ssh.port); if(isNaN(port)) { - Log.error( { server : ModuleInfo.name, port : Config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); + Log.error( { server : ModuleInfo.name, port : config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); return false; } diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 3004513d..3a58ae4e 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -5,7 +5,7 @@ const baseClient = require('../../client.js'); const Log = require('../../logger.js').log; const LoginServerModule = require('../../login_server_module.js'); -const Config = require('../../config.js').config; +const Config = require('../../config.js').get; const EnigAssert = require('../../enigma_assert.js'); const { stringFromNullTermBuffer } = require('../../string_util.js'); @@ -549,7 +549,7 @@ function TelnetClient(input, output) { }); this.connectionTrace = (info, msg) => { - if(Config.loginServers.telnet.traceConnections) { + if(Config().loginServers.telnet.traceConnections) { const logger = self.log || Log; return logger.trace(info, `Telnet: ${msg}`); } @@ -568,7 +568,7 @@ function TelnetClient(input, output) { this.readyNow = () => { if(!this.didReady) { this.didReady = true; - this.emit('ready', { firstMenu : Config.loginServers.telnet.firstMenu } ); + this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); } }; } @@ -879,9 +879,10 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { } listen() { - const port = parseInt(Config.loginServers.telnet.port); + const config = Config(); + const port = parseInt(config.loginServers.telnet.port); if(isNaN(port)) { - Log.error( { server : ModuleInfo.name, port : Config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); + Log.error( { server : ModuleInfo.name, port : config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); return false; } diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index ed12bf0b..e30d0303 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('../../config.js').config; +const Config = require('../../config.js').get; const TelnetClient = require('./telnet.js').TelnetClient; const Log = require('../../logger.js').log; const LoginServerModule = require('../../login_server_module.js'); @@ -92,7 +92,7 @@ function WebSocketClient(ws, req, serverType) { // If the config allows it, look for 'x-forwarded-proto' as "https" // to override |isSecure| // - if(true === _.get(Config, 'loginServers.webSocket.proxied') && + if(true === _.get(Config(), 'loginServers.webSocket.proxied') && 'https' === req.headers['x-forwarded-proto']) { Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); @@ -120,7 +120,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { // * insecure websocket (ws://) // * secure (tls) websocket (wss://) // - const config = _.get(Config, 'loginServers.webSocket'); + const config = _.get(Config(), 'loginServers.webSocket'); if(!_.isObject(config)) { return; } @@ -162,7 +162,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { } const serverName = `${ModuleInfo.name} (${serverType})`; - const port = parseInt(_.get(Config, [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); + const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); if(isNaN(port)) { Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); diff --git a/core/show_art.js b/core/show_art.js index fbbb3141..6fb8d01b 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -5,7 +5,7 @@ const MenuModule = require('./menu_module.js').MenuModule; const Errors = require('../core/enig_error.js').Errors; const ANSI = require('./ansi_term.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; // deps const async = require('async'); @@ -90,7 +90,7 @@ exports.getModule = class ShowArtModule extends MenuModule { } // further resolve key -> file base area art - const artSpec = _.get(Config, [ 'fileBase', 'areas', key, 'art' ]); + const artSpec = _.get(Config(), [ 'fileBase', 'areas', key, 'art' ]); if(!artSpec) { return cb(Errors.MissingConfig(`No art defined for file base area "${key}"`)); } diff --git a/core/system_view_validate.js b/core/system_view_validate.js index beb6bcce..e2d01a89 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -3,7 +3,7 @@ // ENiGMA½ const User = require('./user.js'); -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Log = require('./logger.js').log; const { getAddressedToInfo } = require('./mail_util.js'); const Message = require('./message.js'); @@ -30,14 +30,15 @@ function validateMessageSubject(data, cb) { } function validateUserNameAvail(data, cb) { - if(!data || data.length < Config.users.usernameMin) { + const config = Config(); + if(!data || data.length < config.users.usernameMin) { cb(new Error('Username too short')); - } else if(data.length > Config.users.usernameMax) { + } else if(data.length > config.users.usernameMax) { // generally should be unreached due to view restraints return cb(new Error('Username too long')); } else { - const usernameRegExp = new RegExp(Config.users.usernamePattern); - const invalidNames = Config.users.newUserNames + Config.users.badUserNames; + const usernameRegExp = new RegExp(config.users.usernamePattern); + const invalidNames = config.users.newUserNames + config.users.badUserNames; if(!usernameRegExp.test(data)) { return cb(new Error('Username contains invalid characters')); @@ -133,12 +134,13 @@ function validateBirthdate(data, cb) { } function validatePasswordSpec(data, cb) { - if(!data || data.length < Config.users.passwordMin) { + const config = Config(); + if(!data || data.length < config.users.passwordMin) { return cb(new Error('Password too short')); } // check badpass, if avail - fs.readFile(Config.users.badPassFile, 'utf8', (err, passwords) => { + fs.readFile(config.users.badPassFile, 'utf8', (err, passwords) => { if(err) { Log.warn( { error : err.message }, 'Cannot read bad pass file'); return cb(null); diff --git a/core/theme.js b/core/theme.js index 77c737fc..5545fad3 100644 --- a/core/theme.js +++ b/core/theme.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const art = require('./art.js'); const ansi = require('./ansi_term.js'); const Log = require('./logger.js').log; @@ -38,7 +38,7 @@ function refreshThemeHelpers(theme) { let pwChar = _.get( theme, 'customization.defaults.general.passwordChar', - Config.defaults.passwordChar + Config().defaults.passwordChar ); if(_.isString(pwChar)) { @@ -50,22 +50,22 @@ function refreshThemeHelpers(theme) { return pwChar; }, getDateFormat : function(style = 'short') { - const format = Config.defaults.dateFormat[style] || 'MM/DD/YYYY'; + const format = Config().defaults.dateFormat[style] || 'MM/DD/YYYY'; return _.get(theme, `customization.defaults.dateFormat.${style}`, format); }, getTimeFormat : function(style = 'short') { - const format = Config.defaults.timeFormat[style] || 'h:mm a'; + const format = Config().defaults.timeFormat[style] || 'h:mm a'; return _.get(theme, `customization.defaults.timeFormat.${style}`, format); }, getDateTimeFormat : function(style = 'short') { - const format = Config.defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + const format = Config().defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); } }; } function loadTheme(themeId, cb) { - const path = paths.join(Config.paths.themes, themeId, 'theme.hjson'); + const path = paths.join(Config().paths.themes, themeId, 'theme.hjson'); const changed = ( { fileName, fileRoot } ) => { const reCachedPath = paths.join(fileRoot, fileName); @@ -262,15 +262,16 @@ function getMergedTheme(menuConfig, promptConfig, theme) { } function reloadTheme(themeId) { + const config = Config(); async.waterfall( [ function loadMenuConfig(callback) { - getFullConfig(Config.general.menuFile, (err, menuConfig) => { + getFullConfig(config.general.menuFile, (err, menuConfig) => { return callback(err, menuConfig); }); }, function loadPromptConfig(menuConfig, callback) { - getFullConfig(Config.general.promptFile, (err, promptConfig) => { + getFullConfig(config.general.promptFile, (err, promptConfig) => { return callback(err, menuConfig, promptConfig); }); }, @@ -312,21 +313,21 @@ function reloadAllThemes() } function initAvailableThemes(cb) { - + const config = Config(); async.waterfall( [ function loadMenuConfig(callback) { - getFullConfig(Config.general.menuFile, (err, menuConfig) => { + getFullConfig(config.general.menuFile, (err, menuConfig) => { return callback(err, menuConfig); }); }, function loadPromptConfig(menuConfig, callback) { - getFullConfig(Config.general.promptFile, (err, promptConfig) => { + getFullConfig(config.general.promptFile, (err, promptConfig) => { return callback(err, menuConfig, promptConfig); }); }, function getThemeDirectories(menuConfig, promptConfig, callback) { - fs.readdir(Config.paths.themes, (err, files) => { + fs.readdir(config.paths.themes, (err, files) => { if(err) { return callback(err); } @@ -337,7 +338,7 @@ function initAvailableThemes(cb) { 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(); + return fs.statSync(paths.join(config.paths.themes, f)).isDirectory(); }) ); }); @@ -394,12 +395,13 @@ function setClientTheme(client, themeId) { let msg; let setThemeId; + const config = Config(); if(availThemes.has(themeId)) { msg = 'Set client theme'; setThemeId = themeId; - } else if(availThemes.has(Config.defaults.theme)) { + } else if(availThemes.has(config.defaults.theme)) { msg = 'Failed setting theme by supplied ID; Using default'; - setThemeId = Config.defaults.theme; + setThemeId = config.defaults.theme; } else { msg = 'Failed setting theme by system default ID; Using the first one we can find'; setThemeId = availThemes.keys().next().value; @@ -421,10 +423,11 @@ function getThemeArt(options, cb) { // readSauce // random // + const config = Config(); if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { options.themeId = options.client.user.properties.theme_id; } else { - options.themeId = Config.defaults.theme; + options.themeId = config.defaults.theme; } // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... @@ -465,17 +468,17 @@ function getThemeArt(options, cb) { return callback(null, artInfo); } - options.basePath = paths.join(Config.paths.themes, options.themeId); + options.basePath = paths.join(config.paths.themes, options.themeId); art.getArt(options.name, options, (err, artInfo) => { return callback(null, artInfo); }); }, function fromDefaultTheme(artInfo, callback) { - if(artInfo || Config.defaults.theme === options.themeId) { + if(artInfo || config.defaults.theme === options.themeId) { return callback(null, artInfo); } - options.basePath = paths.join(Config.paths.themes, Config.defaults.theme); + options.basePath = paths.join(config.paths.themes, config.defaults.theme); art.getArt(options.name, options, (err, artInfo) => { return callback(null, artInfo); }); @@ -485,7 +488,7 @@ function getThemeArt(options, cb) { return callback(null, artInfo); } - options.basePath = Config.paths.art; + options.basePath = config.paths.art; art.getArt(options.name, options, (err, artInfo) => { return callback(err, artInfo); }); diff --git a/core/user.js b/core/user.js index 7f50223d..457604b3 100644 --- a/core/user.js +++ b/core/user.js @@ -2,7 +2,7 @@ 'use strict'; const userDb = require('./database.js').dbs.user; -const Config = require('./config.js').config; +const Config = require('./config.js').get; const userGroup = require('./user_group.js'); const Errors = require('./enig_error.js').Errors; const Events = require('./events.js'); @@ -56,7 +56,7 @@ module.exports = class User { } isValid() { - if(this.userId <= 0 || this.username.length < Config.users.usernameMin) { + if(this.userId <= 0 || this.username.length < Config().users.usernameMin) { return false; } @@ -181,15 +181,16 @@ module.exports = class User { create(password, cb) { assert(0 === this.userId); + const config = Config(); - if(this.username.length < Config.users.usernameMin || this.username.length > Config.users.usernameMax) { + if(this.username.length < config.users.usernameMin || this.username.length > config.users.usernameMax) { return cb(Errors.Invalid('Invalid username length')); } const self = this; // :TODO: set various defaults, e.g. default activation status, etc. - self.properties.account_status = Config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; + self.properties.account_status = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; async.waterfall( [ @@ -229,7 +230,7 @@ module.exports = class User { }); }, function setInitialGroupMembership(trans, callback) { - self.groups = Config.users.defaultGroups; + self.groups = config.users.defaultGroups; if(User.RootUserID === self.userId) { // root/SysOp? self.groups.push('sysops'); diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 6ea1e6f8..2f98823c 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -2,7 +2,7 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; +const Config = require('./config.js').get; const Errors = require('./enig_error.js').Errors; const getServer = require('./listening_server.js').getServer; const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; @@ -89,12 +89,13 @@ class WebPasswordReset { }, function getEmailTemplates(user, callback) { - fs.readFile(Config.contentServers.web.resetPassword.resetPassEmailText, 'utf8', (err, textTemplate) => { + const config = Config(); + fs.readFile(config.contentServers.web.resetPassword.resetPassEmailText, 'utf8', (err, textTemplate) => { if(err) { textTemplate = PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT; } - fs.readFile(Config.contentServers.web.resetPassword.resetPassEmailHtml, 'utf8', (err, htmlTemplate) => { + fs.readFile(config.contentServers.web.resetPassword.resetPassEmailHtml, 'utf8', (err, htmlTemplate) => { return callback(null, user, textTemplate, htmlTemplate); }); }); @@ -106,7 +107,7 @@ class WebPasswordReset { function replaceTokens(s) { return s - .replace(/%BOARDNAME%/g, Config.general.boardName) + .replace(/%BOARDNAME%/g, Config().general.boardName) .replace(/%USERNAME%/g, user.username) .replace(/%TOKEN%/g, user.properties.email_password_reset_token) .replace(/%RESET_URL%/g, resetUrl) @@ -229,12 +230,13 @@ class WebPasswordReset { const postResetUrl = webServer.instance.buildUrl('/reset_password'); + const config = Config(); return webServer.instance.routeTemplateFilePage( - Config.contentServers.web.resetPassword.resetPageTemplate, + config.contentServers.web.resetPassword.resetPageTemplate, (templateData, preprocessFinished) => { const finalPage = templateData - .replace(/%BOARDNAME%/g, Config.general.boardName) + .replace(/%BOARDNAME%/g, config.general.boardName) .replace(/%USERNAME%/g, user.username) .replace(/%TOKEN%/g, token) .replace(/%RESET_URL%/g, postResetUrl) @@ -262,9 +264,10 @@ class WebPasswordReset { req.on('end', () => { const formData = querystring.parse(bodyData); + const config = Config(); if(!formData.token || !formData.password || !formData.confirm_password || formData.password !== formData.confirm_password || - formData.password.length < Config.users.passwordMin || formData.password.length > Config.users.passwordMax) + formData.password.length < config.users.passwordMin || formData.password.length > config.users.passwordMax) { return badRequest(); } From 6325f92fa5e6880a48eb6168f0590c28cf585b5f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 Jun 2018 18:37:48 -0600 Subject: [PATCH 149/569] Increase max listeners in Events - experimental: may change this in the near future --- core/events.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/events.js b/core/events.js index e50c5723..7bf307ad 100644 --- a/core/events.js +++ b/core/events.js @@ -14,6 +14,7 @@ const glob = require('glob'); module.exports = new class Events extends events.EventEmitter { constructor() { super(); + this.setMaxListeners(32); // :TODO: play with this... } getSystemEvents() { From 5ddf04c8826608c3d64f2ecf17da9c9495698d3f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 Jun 2018 22:58:56 -0600 Subject: [PATCH 150/569] Change tab rule to spaces --- .eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 5e9b45b6..612da123 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,7 @@ "rules": { "indent": [ "error", - "tab", + 4, { "SwitchCase" : 1 } From e9787cee3e2a6ef8bc0b1446c4db349dc9ea2610 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 Jun 2018 23:15:04 -0600 Subject: [PATCH 151/569] ENiGMA 1/2 WILL USE SPACES FROM THIS POINT ON VS TABS * Really just to make GitHub formatting happy. Arg. --- core/abracadabra.js | 216 +- core/acs.js | 124 +- core/ansi_escape_parser.js | 696 ++-- core/ansi_prep.js | 330 +- core/ansi_term.js | 554 ++-- core/archive_util.js | 494 +-- core/art.js | 550 ++-- core/asset.js | 138 +- core/bbs.js | 452 +-- core/bbs_link.js | 282 +- core/bbs_list.js | 708 ++-- core/button_view.js | 26 +- core/client.js | 718 ++--- core/client_connections.js | 138 +- core/client_term.js | 274 +- core/color_codes.js | 392 +-- core/combatnet.js | 168 +- core/conf_area_util.js | 26 +- core/config.js | 1422 ++++---- core/config_cache.js | 104 +- core/config_util.js | 86 +- core/connect.js | 268 +- core/crc.js | 154 +- core/database.js | 330 +- core/descript_ion_file.js | 102 +- core/door.js | 206 +- core/door_party.js | 196 +- core/download_queue.js | 112 +- core/dropfile.js | 328 +- core/edit_text_view.js | 110 +- core/email.js | 24 +- core/enig_error.js | 60 +- core/enigma_assert.js | 12 +- core/erc_client.js | 234 +- core/event_scheduler.js | 386 +-- core/events.js | 106 +- core/exodus.js | 298 +- core/file_area_filter_edit.js | 550 ++-- core/file_area_list.js | 1346 ++++---- core/file_area_web.js | 754 ++--- core/file_base_area.js | 1482 ++++----- core/file_base_area_select.js | 116 +- core/file_base_download_manager.js | 352 +- core/file_base_filter.js | 236 +- core/file_base_list_export.js | 474 +-- core/file_base_search.js | 172 +- core/file_base_user_list_export.js | 406 +-- core/file_base_web_download_manager.js | 428 +-- core/file_entry.js | 1000 +++--- core/file_transfer.js | 852 ++--- core/file_transfer_protocol_select.js | 222 +- core/file_util.js | 104 +- core/fnv1a.js | 62 +- core/fse.js | 1804 +++++------ core/ftn_address.js | 276 +- core/ftn_mail_packet.js | 1736 +++++----- core/ftn_util.js | 354 +- core/horizontal_menu_view.js | 190 +- core/key_entry_view.js | 102 +- core/last_callers.js | 204 +- core/listening_server.js | 68 +- core/logger.js | 108 +- core/login_server_module.js | 114 +- core/mail_packet.js | 32 +- core/mail_util.js | 88 +- core/mask_edit_text_view.js | 252 +- core/mci_view_factory.js | 278 +- core/menu_module.js | 666 ++-- core/menu_stack.js | 282 +- core/menu_util.js | 390 +-- core/menu_view.js | 330 +- core/message.js | 1226 +++---- core/message_area.js | 890 ++--- core/message_base_search.js | 216 +- core/mime_util.js | 44 +- core/misc_util.js | 36 +- core/mod_mixins.js | 42 +- core/module_util.js | 138 +- core/msg_area_list.js | 234 +- core/msg_area_post_fse.js | 92 +- core/msg_area_reply_fse.js | 8 +- core/msg_area_view_fse.js | 204 +- core/msg_conf_list.js | 218 +- core/msg_list.js | 376 +-- core/msg_network.js | 84 +- core/msg_scan_toss_module.js | 6 +- core/multi_line_edit_text_view.js | 1936 +++++------ core/new_scan.js | 396 +-- core/nua.js | 208 +- core/onelinerz.js | 498 +-- core/oputil/oputil_common.js | 102 +- core/oputil/oputil_config.js | 928 +++--- core/oputil/oputil_file_base.js | 1108 +++---- core/oputil/oputil_help.js | 14 +- core/oputil/oputil_main.js | 30 +- core/oputil/oputil_message_base.js | 214 +- core/oputil/oputil_user.js | 288 +- core/predefined_mci.js | 366 +-- core/rumorz.js | 390 +-- core/sauce.js | 230 +- core/scanner_tossers/ftn_bso.js | 4110 ++++++++++++------------ core/server_module.js | 2 +- core/servers/content/gopher.js | 504 +-- core/servers/content/web.js | 366 +-- core/servers/login/ssh.js | 398 +-- core/servers/login/telnet.js | 1200 +++---- core/servers/login/websocket.js | 292 +- core/set_newscan_date.js | 396 +-- core/show_art.js | 270 +- core/spinner_menu_view.js | 118 +- core/standard_menu.js | 30 +- core/stat_log.js | 416 +-- core/string_format.js | 482 +-- core/string_util.js | 544 ++-- core/system_events.js | 34 +- core/system_menu_method.js | 196 +- core/system_view_validate.js | 184 +- core/telnet_bridge.js | 292 +- core/text_view.js | 238 +- core/theme.js | 1084 +++---- core/tic_file_info.js | 426 +-- core/toggle_menu_view.js | 116 +- core/upload.js | 1392 ++++---- core/user.js | 984 +++--- core/user_config.js | 352 +- core/user_group.js | 68 +- core/user_list.js | 138 +- core/user_login.js | 140 +- core/uuid_util.js | 44 +- core/vertical_menu_view.js | 452 +-- core/view.js | 350 +- core/view_controller.js | 1290 ++++---- core/web_password_reset.js | 452 +-- core/whos_online.js | 112 +- core/word_wrap.js | 146 +- 135 files changed, 27397 insertions(+), 27397 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 0ac17887..77a5e4c3 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -18,9 +18,9 @@ const mkdirs = require('fs-extra').mkdirs; const activeDoorNodeInstances = {}; exports.moduleInfo = { - name : 'Abracadabra', - desc : 'External BBS Door Module', - author : 'NuSkooler', + name : 'Abracadabra', + desc : 'External BBS Door Module', + author : 'NuSkooler', }; /* @@ -60,138 +60,138 @@ exports.moduleInfo = { */ exports.getModule = class AbracadabraModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.config = options.menuConfig.config; - // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } - assert(_.isString(this.config.name, 'Config \'name\' is required')); - assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); - assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); + this.config = options.menuConfig.config; + // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } + assert(_.isString(this.config.name, 'Config \'name\' is required')); + assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); + assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); - this.config.nodeMax = this.config.nodeMax || 0; - this.config.args = this.config.args || []; - } + this.config.nodeMax = this.config.nodeMax || 0; + this.config.args = this.config.args || []; + } - /* + /* :TODO: * disconnecting wile door is open leaves dosemu * http://bbslink.net/sysop.php support * Font support ala all other menus... or does this just work? */ - initSequence() { - const self = this; + initSequence() { + const self = this; - async.series( - [ - function validateNodeCount(callback) { - if(self.config.nodeMax > 0 && + async.series( + [ + function validateNodeCount(callback) { + if(self.config.nodeMax > 0 && _.isNumber(activeDoorNodeInstances[self.config.name]) && activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) - { - self.client.log.info( - { - name : self.config.name, - activeCount : activeDoorNodeInstances[self.config.name] - }, - 'Too many active instances'); + { + self.client.log.info( + { + name : self.config.name, + activeCount : activeDoorNodeInstances[self.config.name] + }, + 'Too many active instances'); - if(_.isString(self.config.tooManyArt)) { - theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { - self.pausePrompt( () => { - callback(new Error('Too many active instances')); - }); - }); - } else { - self.client.term.write('\nToo many active instances. Try again later.\n'); + if(_.isString(self.config.tooManyArt)) { + theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { + self.pausePrompt( () => { + callback(new Error('Too many active instances')); + }); + }); + } else { + self.client.term.write('\nToo many active instances. Try again later.\n'); - // :TODO: Use MenuModule.pausePrompt() - self.pausePrompt( () => { - callback(new Error('Too many active instances')); - }); - } - } else { - // :TODO: JS elegant way to do this? - if(activeDoorNodeInstances[self.config.name]) { - activeDoorNodeInstances[self.config.name] += 1; - } else { - activeDoorNodeInstances[self.config.name] = 1; - } + // :TODO: Use MenuModule.pausePrompt() + self.pausePrompt( () => { + callback(new Error('Too many active instances')); + }); + } + } else { + // :TODO: JS elegant way to do this? + if(activeDoorNodeInstances[self.config.name]) { + activeDoorNodeInstances[self.config.name] += 1; + } else { + activeDoorNodeInstances[self.config.name] = 1; + } - callback(null); - } - }, - function generateDropfile(callback) { - self.dropFile = new DropFile(self.client, self.config.dropFileType); - var fullPath = self.dropFile.fullPath; + callback(null); + } + }, + function generateDropfile(callback) { + self.dropFile = new DropFile(self.client, self.config.dropFileType); + var fullPath = self.dropFile.fullPath; - mkdirs(paths.dirname(fullPath), function dirCreated(err) { - if(err) { - callback(err); - } else { - self.dropFile.createFile(function created(err) { - callback(err); - }); - } - }); - } - ], - function complete(err) { - if(err) { - self.client.log.warn( { error : err.toString() }, 'Could not start door'); - self.lastError = err; - self.prevMenu(); - } else { - self.finishedLoading(); - } - } - ); - } + mkdirs(paths.dirname(fullPath), function dirCreated(err) { + if(err) { + callback(err); + } else { + self.dropFile.createFile(function created(err) { + callback(err); + }); + } + }); + } + ], + function complete(err) { + if(err) { + self.client.log.warn( { error : err.toString() }, 'Could not start door'); + self.lastError = err; + self.prevMenu(); + } else { + self.finishedLoading(); + } + } + ); + } - runDoor() { + runDoor() { - const exeInfo = { - cmd : this.config.cmd, - args : this.config.args, - io : this.config.io || 'stdio', - encoding : this.config.encoding || this.client.term.outputEncoding, - dropFile : this.dropFile.fileName, - node : this.client.node, - //inhSocket : this.client.output._handle.fd, - }; + const exeInfo = { + cmd : this.config.cmd, + args : this.config.args, + io : this.config.io || 'stdio', + encoding : this.config.encoding || this.client.term.outputEncoding, + dropFile : this.dropFile.fileName, + node : this.client.node, + //inhSocket : this.client.output._handle.fd, + }; - const doorInstance = new door.Door(this.client, exeInfo); + const doorInstance = new door.Door(this.client, exeInfo); - doorInstance.once('finished', () => { - // - // Try to clean up various settings such as scroll regions that may - // have been set within the door - // - this.client.term.rawWrite( - ansi.normal() + + doorInstance.once('finished', () => { + // + // Try to clean up various settings such as scroll regions that may + // have been set within the door + // + this.client.term.rawWrite( + ansi.normal() + ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + ansi.setScrollRegion() + ansi.goto(this.client.term.termHeight, 0) + '\r\n\r\n' - ); + ); - this.prevMenu(); - }); + this.prevMenu(); + }); - this.client.term.write(ansi.resetScreen()); + this.client.term.write(ansi.resetScreen()); - doorInstance.run(); - } + doorInstance.run(); + } - leave() { - super.leave(); - if(!this.lastError) { - activeDoorNodeInstances[this.config.name] -= 1; - } - } + leave() { + super.leave(); + if(!this.lastError) { + activeDoorNodeInstances[this.config.name] -= 1; + } + } - finishedLoading() { - this.runDoor(); - } + finishedLoading() { + this.runDoor(); + } }; diff --git a/core/acs.js b/core/acs.js index 90c7c2a6..b1461400 100644 --- a/core/acs.js +++ b/core/acs.js @@ -10,81 +10,81 @@ const assert = require('assert'); const _ = require('lodash'); class ACS { - constructor(client) { - this.client = client; - } + constructor(client) { + this.client = client; + } - check(acs, scope, defaultAcs) { - acs = acs ? acs[scope] : defaultAcs; - acs = acs || defaultAcs; - try { - return checkAcs(acs, { client : this.client } ); - } catch(e) { - Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); - return false; - } - } + check(acs, scope, defaultAcs) { + acs = acs ? acs[scope] : defaultAcs; + acs = acs || defaultAcs; + try { + return checkAcs(acs, { client : this.client } ); + } catch(e) { + Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); + return false; + } + } - // - // Message Conferences & Areas - // - hasMessageConfRead(conf) { - return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead); - } + // + // Message Conferences & Areas + // + hasMessageConfRead(conf) { + return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead); + } - hasMessageAreaRead(area) { - return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead); - } + hasMessageAreaRead(area) { + return this.check(area.acs, 'read', ACS.Defaults.MessageAreaRead); + } - // - // File Base / Areas - // - hasFileAreaRead(area) { - return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); - } + // + // File Base / Areas + // + hasFileAreaRead(area) { + return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); + } - hasFileAreaWrite(area) { - return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite); - } + hasFileAreaWrite(area) { + return this.check(area.acs, 'write', ACS.Defaults.FileAreaWrite); + } - hasFileAreaDownload(area) { - return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload); - } + hasFileAreaDownload(area) { + return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload); + } - getConditionalValue(condArray, memberName) { - if(!Array.isArray(condArray)) { - // no cond array, just use the value - return condArray; - } + getConditionalValue(condArray, memberName) { + if(!Array.isArray(condArray)) { + // no cond array, just use the value + return condArray; + } - assert(_.isString(memberName)); + assert(_.isString(memberName)); - const matchCond = condArray.find( cond => { - if(_.has(cond, 'acs')) { - try { - return checkAcs(cond.acs, { client : this.client } ); - } catch(e) { - Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS'); - return false; - } - } else { - return true; // no acs check req. - } - }); + const matchCond = condArray.find( cond => { + if(_.has(cond, 'acs')) { + try { + return checkAcs(cond.acs, { client : this.client } ); + } catch(e) { + Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS'); + return false; + } + } else { + return true; // no acs check req. + } + }); - if(matchCond) { - return matchCond[memberName]; - } - } + if(matchCond) { + return matchCond[memberName]; + } + } } ACS.Defaults = { - MessageAreaRead : 'GM[users]', - MessageConfRead : 'GM[users]', + MessageAreaRead : 'GM[users]', + MessageConfRead : 'GM[users]', - FileAreaRead : 'GM[users]', - FileAreaWrite : 'GM[sysops]', - FileAreaDownload : 'GM[users]', + FileAreaRead : 'GM[users]', + FileAreaWrite : 'GM[sysops]', + FileAreaDownload : 'GM[users]', }; -module.exports = ACS; \ No newline at end of file +module.exports = ACS; diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index feb7b164..49001363 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -16,278 +16,278 @@ const CR = 0x0d; const LF = 0x0a; function ANSIEscapeParser(options) { - var self = this; + var self = this; - events.EventEmitter.call(this); + events.EventEmitter.call(this); - this.column = 1; - this.row = 1; - this.scrollBack = 0; - this.graphicRendition = {}; + this.column = 1; + this.row = 1; + this.scrollBack = 0; + this.graphicRendition = {}; - this.parseState = { - re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex - }; + this.parseState = { + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex + }; - options = miscUtil.valueWithDefault(options, { - mciReplaceChar : '', - termHeight : 25, - termWidth : 80, - trailingLF : 'default', // default|omit|no|yes, ... - }); + options = miscUtil.valueWithDefault(options, { + mciReplaceChar : '', + termHeight : 25, + termWidth : 80, + trailingLF : 'default', // default|omit|no|yes, ... + }); - this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); - this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); - this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); - this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); + this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); + this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); + this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); + this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); - self.moveCursor = function(cols, rows) { - self.column += cols; - self.row += rows; + self.moveCursor = function(cols, rows) { + self.column += cols; + self.row += rows; - self.column = Math.max(self.column, 1); - self.column = Math.min(self.column, self.termWidth); // can't move past term width - self.row = Math.max(self.row, 1); + self.column = Math.max(self.column, 1); + self.column = Math.min(self.column, self.termWidth); // can't move past term width + self.row = Math.max(self.row, 1); - self.positionUpdated(); - }; + self.positionUpdated(); + }; - self.saveCursorPosition = function() { - self.savedPosition = { - row : self.row, - column : self.column - }; - }; + self.saveCursorPosition = function() { + self.savedPosition = { + row : self.row, + column : self.column + }; + }; - self.restoreCursorPosition = function() { - self.row = self.savedPosition.row; - self.column = self.savedPosition.column; - delete self.savedPosition; + self.restoreCursorPosition = function() { + self.row = self.savedPosition.row; + self.column = self.savedPosition.column; + delete self.savedPosition; - self.positionUpdated(); - // self.rowUpdated(); - }; + self.positionUpdated(); + // self.rowUpdated(); + }; - self.clearScreen = function() { - // :TODO: should be doing something with row/column? - self.emit('clear screen'); - }; + self.clearScreen = function() { + // :TODO: should be doing something with row/column? + self.emit('clear screen'); + }; - /* + /* self.rowUpdated = function() { self.emit('row update', self.row + self.scrollBack); };*/ - self.positionUpdated = function() { - self.emit('position update', self.row, self.column); - }; + self.positionUpdated = function() { + self.emit('position update', self.row, self.column); + }; - function literal(text) { - const len = text.length; - let pos = 0; - let start = 0; - let charCode; + function literal(text) { + const len = text.length; + let pos = 0; + let start = 0; + let charCode; - while(pos < len) { - charCode = text.charCodeAt(pos) & 0xff; // 8bit clean + while(pos < len) { + charCode = text.charCodeAt(pos) & 0xff; // 8bit clean - switch(charCode) { - case CR : - self.emit('literal', text.slice(start, pos)); - start = pos; + switch(charCode) { + case CR : + self.emit('literal', text.slice(start, pos)); + start = pos; - self.column = 1; + self.column = 1; - self.positionUpdated(); - break; + self.positionUpdated(); + break; - case LF : - self.emit('literal', text.slice(start, pos)); - start = pos; + case LF : + self.emit('literal', text.slice(start, pos)); + start = pos; - self.row += 1; + self.row += 1; - self.positionUpdated(); - break; + self.positionUpdated(); + break; - default : - if(self.column === self.termWidth) { - self.emit('literal', text.slice(start, pos + 1)); - start = pos + 1; + default : + if(self.column === self.termWidth) { + self.emit('literal', text.slice(start, pos + 1)); + start = pos + 1; - self.column = 1; - self.row += 1; + self.column = 1; + self.row += 1; - self.positionUpdated(); - } else { - self.column += 1; - } - break; - } + self.positionUpdated(); + } else { + self.column += 1; + } + break; + } - ++pos; - } + ++pos; + } - // - // Finalize this chunk - // - if(self.column > self.termWidth) { - self.column = 1; - self.row += 1; + // + // Finalize this chunk + // + if(self.column > self.termWidth) { + self.column = 1; + self.row += 1; - self.positionUpdated(); - } + self.positionUpdated(); + } - const rem = text.slice(start); - if(rem) { - self.emit('literal', rem); - } - } + const rem = text.slice(start); + if(rem) { + self.emit('literal', rem); + } + } - function parseMCI(buffer) { - // :TODO: move this to "constants" seciton @ top - var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; - var pos = 0; - var match; - var mciCode; - var args; - var id; + function parseMCI(buffer) { + // :TODO: move this to "constants" seciton @ top + var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; + var pos = 0; + var match; + var mciCode; + var args; + var id; - do { - pos = mciRe.lastIndex; - match = mciRe.exec(buffer); + do { + pos = mciRe.lastIndex; + match = mciRe.exec(buffer); - if(null !== match) { - if(match.index > pos) { - literal(buffer.slice(pos, match.index)); - } + if(null !== match) { + if(match.index > pos) { + literal(buffer.slice(pos, match.index)); + } - mciCode = match[1]; - id = match[2] || null; + mciCode = match[1]; + id = match[2] || null; - if(match[3]) { - args = match[3].split(','); - } else { - args = []; - } + if(match[3]) { + args = match[3].split(','); + } else { + args = []; + } - // if MCI codes are changing, save off the current color - var fullMciCode = mciCode + (id || ''); - if(self.lastMciCode !== fullMciCode) { + // if MCI codes are changing, save off the current color + var fullMciCode = mciCode + (id || ''); + if(self.lastMciCode !== fullMciCode) { - self.lastMciCode = fullMciCode; + self.lastMciCode = fullMciCode; - self.graphicRenditionForErase = _.clone(self.graphicRendition); - } + self.graphicRenditionForErase = _.clone(self.graphicRendition); + } - self.emit('mci', { - mci : mciCode, - id : id ? parseInt(id, 10) : null, - args : args, - SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) - }); + self.emit('mci', { + mci : mciCode, + id : id ? parseInt(id, 10) : null, + args : args, + SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + }); - if(self.mciReplaceChar.length > 0) { - const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase); + if(self.mciReplaceChar.length > 0) { + const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase); - self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)); + self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)); - literal(new Array(match[0].length + 1).join(self.mciReplaceChar)); - } else { - literal(match[0]); - } - } + literal(new Array(match[0].length + 1).join(self.mciReplaceChar)); + } else { + literal(match[0]); + } + } - } while(0 !== mciRe.lastIndex); + } while(0 !== mciRe.lastIndex); - if(pos < buffer.length) { - literal(buffer.slice(pos)); - } - } + if(pos < buffer.length) { + literal(buffer.slice(pos)); + } + } - self.reset = function(input) { - self.parseState = { - // ignore anything past EOF marker, if any - buffer : input.split(String.fromCharCode(0x1a), 1)[0], - re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex - stop : false, - }; - }; + self.reset = function(input) { + self.parseState = { + // ignore anything past EOF marker, if any + buffer : input.split(String.fromCharCode(0x1a), 1)[0], + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex + stop : false, + }; + }; - self.stop = function() { - self.parseState.stop = true; - }; + self.stop = function() { + self.parseState.stop = true; + }; - self.parse = function(input) { - if(input) { - self.reset(input); - } + self.parse = function(input) { + if(input) { + self.reset(input); + } - // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. - var pos; - var match; - var opCode; - var args; - var re = self.parseState.re; - var buffer = self.parseState.buffer; + // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. + var pos; + var match; + var opCode; + var args; + var re = self.parseState.re; + var buffer = self.parseState.buffer; - self.parseState.stop = false; + self.parseState.stop = false; - do { - if(self.parseState.stop) { - return; - } + do { + if(self.parseState.stop) { + return; + } - pos = re.lastIndex; - match = re.exec(buffer); + pos = re.lastIndex; + match = re.exec(buffer); - if(null !== match) { - if(match.index > pos) { - parseMCI(buffer.slice(pos, match.index)); - } + if(null !== match) { + if(match.index > pos) { + parseMCI(buffer.slice(pos, match.index)); + } - opCode = match[2]; - args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints + opCode = match[2]; + args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints - escape(opCode, args); + escape(opCode, args); - //self.emit('chunk', match[0]); - self.emit('control', match[0], opCode, args); - } - } while(0 !== re.lastIndex); + //self.emit('chunk', match[0]); + self.emit('control', match[0], opCode, args); + } + } while(0 !== re.lastIndex); - if(pos < buffer.length) { - var lastBit = buffer.slice(pos); + if(pos < buffer.length) { + var lastBit = buffer.slice(pos); - // :TODO: check for various ending LF's, not just DOS \r\n - if('\r\n' === lastBit.slice(-2).toString()) { - switch(self.trailingLF) { - case 'default' : - // - // Default is to *not* omit the trailing LF - // if we're going to end on termHeight - // - if(this.termHeight === self.row) { - lastBit = lastBit.slice(0, -2); - } - break; + // :TODO: check for various ending LF's, not just DOS \r\n + if('\r\n' === lastBit.slice(-2).toString()) { + switch(self.trailingLF) { + case 'default' : + // + // Default is to *not* omit the trailing LF + // if we're going to end on termHeight + // + if(this.termHeight === self.row) { + lastBit = lastBit.slice(0, -2); + } + break; - case 'omit' : - case 'no' : - case false : - lastBit = lastBit.slice(0, -2); - break; - } - } + case 'omit' : + case 'no' : + case false : + lastBit = lastBit.slice(0, -2); + break; + } + } - parseMCI(lastBit); - } + parseMCI(lastBit); + } - self.emit('complete'); - }; + self.emit('complete'); + }; - /* + /* self.parse = function(buffer, savedRe) { // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. // :TODO: move this to "constants" section @ top @@ -329,164 +329,164 @@ function ANSIEscapeParser(options) { }; */ - function escape(opCode, args) { - let arg; + function escape(opCode, args) { + let arg; - switch(opCode) { - // cursor up - case 'A' : - //arg = args[0] || 1; - arg = isNaN(args[0]) ? 1 : args[0]; - self.moveCursor(0, -arg); - break; + switch(opCode) { + // cursor up + case 'A' : + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; + self.moveCursor(0, -arg); + break; - // cursor down - case 'B' : - //arg = args[0] || 1; - arg = isNaN(args[0]) ? 1 : args[0]; - self.moveCursor(0, arg); - break; + // cursor down + case 'B' : + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; + self.moveCursor(0, arg); + break; - // cursor forward/right - case 'C' : - //arg = args[0] || 1; - arg = isNaN(args[0]) ? 1 : args[0]; - self.moveCursor(arg, 0); - break; + // cursor forward/right + case 'C' : + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; + self.moveCursor(arg, 0); + break; - // cursor back/left - case 'D' : - //arg = args[0] || 1; - arg = isNaN(args[0]) ? 1 : args[0]; - self.moveCursor(-arg, 0); - break; + // cursor back/left + case 'D' : + //arg = args[0] || 1; + arg = isNaN(args[0]) ? 1 : args[0]; + self.moveCursor(-arg, 0); + break; - case 'f' : // horiz & vertical - case 'H' : // cursor position - //self.row = args[0] || 1; - //self.column = args[1] || 1; - self.row = isNaN(args[0]) ? 1 : args[0]; - self.column = isNaN(args[1]) ? 1 : args[1]; - //self.rowUpdated(); - self.positionUpdated(); - break; + case 'f' : // horiz & vertical + case 'H' : // cursor position + //self.row = args[0] || 1; + //self.column = args[1] || 1; + self.row = isNaN(args[0]) ? 1 : args[0]; + self.column = isNaN(args[1]) ? 1 : args[1]; + //self.rowUpdated(); + self.positionUpdated(); + break; - // save position - case 's' : - self.saveCursorPosition(); - break; + // save position + case 's' : + self.saveCursorPosition(); + break; - // restore position - case 'u' : - self.restoreCursorPosition(); - break; + // restore position + case 'u' : + self.restoreCursorPosition(); + break; - // set graphic rendition - case 'm' : - self.graphicRendition.reset = false; + // set graphic rendition + case 'm' : + self.graphicRendition.reset = false; - for(let i = 0, len = args.length; i < len; ++i) { - arg = args[i]; + for(let i = 0, len = args.length; i < len; ++i) { + arg = args[i]; - if(ANSIEscapeParser.foregroundColors[arg]) { - self.graphicRendition.fg = arg; - } else if(ANSIEscapeParser.backgroundColors[arg]) { - self.graphicRendition.bg = arg; - } else if(ANSIEscapeParser.styles[arg]) { - switch(arg) { - case 0 : - // clear out everything - delete self.graphicRendition.intensity; - delete self.graphicRendition.underline; - delete self.graphicRendition.blink; - delete self.graphicRendition.negative; - delete self.graphicRendition.invisible; + if(ANSIEscapeParser.foregroundColors[arg]) { + self.graphicRendition.fg = arg; + } else if(ANSIEscapeParser.backgroundColors[arg]) { + self.graphicRendition.bg = arg; + } else if(ANSIEscapeParser.styles[arg]) { + switch(arg) { + case 0 : + // clear out everything + delete self.graphicRendition.intensity; + delete self.graphicRendition.underline; + delete self.graphicRendition.blink; + delete self.graphicRendition.negative; + delete self.graphicRendition.invisible; - delete self.graphicRendition.fg; - delete self.graphicRendition.bg; + delete self.graphicRendition.fg; + delete self.graphicRendition.bg; - self.graphicRendition.reset = true; - //self.graphicRendition.fg = 39; - //self.graphicRendition.bg = 49; - break; + self.graphicRendition.reset = true; + //self.graphicRendition.fg = 39; + //self.graphicRendition.bg = 49; + break; - case 1 : - case 2 : - case 22 : - self.graphicRendition.intensity = arg; - break; + case 1 : + case 2 : + case 22 : + self.graphicRendition.intensity = arg; + break; - case 4 : - case 24 : - self.graphicRendition.underline = arg; - break; + case 4 : + case 24 : + self.graphicRendition.underline = arg; + break; - case 5 : - case 6 : - case 25 : - self.graphicRendition.blink = arg; - break; + case 5 : + case 6 : + case 25 : + self.graphicRendition.blink = arg; + break; - case 7 : - case 27 : - self.graphicRendition.negative = arg; - break; + case 7 : + case 27 : + self.graphicRendition.negative = arg; + break; - case 8 : - case 28 : - self.graphicRendition.invisible = arg; - break; + case 8 : + case 28 : + self.graphicRendition.invisible = arg; + break; - default : - Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI'); - break; - } - } - } + default : + Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI'); + break; + } + } + } - self.emit('sgr update', self.graphicRendition); - break; // m + self.emit('sgr update', self.graphicRendition); + break; // m - // :TODO: s, u, K + // :TODO: s, u, K - // erase display/screen - case 'J' : - // :TODO: Handle other 'J' types! - if(2 === args[0]) { - self.clearScreen(); - } - break; - } - } + // erase display/screen + case 'J' : + // :TODO: Handle other 'J' types! + if(2 === args[0]) { + self.clearScreen(); + } + break; + } + } } util.inherits(ANSIEscapeParser, events.EventEmitter); ANSIEscapeParser.foregroundColors = { - 30 : 'black', - 31 : 'red', - 32 : 'green', - 33 : 'yellow', - 34 : 'blue', - 35 : 'magenta', - 36 : 'cyan', - 37 : 'white', - 39 : 'default', // same as white for most implementations + 30 : 'black', + 31 : 'red', + 32 : 'green', + 33 : 'yellow', + 34 : 'blue', + 35 : 'magenta', + 36 : 'cyan', + 37 : 'white', + 39 : 'default', // same as white for most implementations - 90 : 'grey' + 90 : 'grey' }; Object.freeze(ANSIEscapeParser.foregroundColors); ANSIEscapeParser.backgroundColors = { - 40 : 'black', - 41 : 'red', - 42 : 'green', - 43 : 'yellow', - 44 : 'blue', - 45 : 'magenta', - 46 : 'cyan', - 47 : 'white', - 49 : 'default', // same as black for most implementations + 40 : 'black', + 41 : 'red', + 42 : 'green', + 43 : 'yellow', + 44 : 'blue', + 45 : 'magenta', + 46 : 'cyan', + 47 : 'white', + 49 : 'default', // same as black for most implementations }; Object.freeze(ANSIEscapeParser.backgroundColors); @@ -501,24 +501,24 @@ Object.freeze(ANSIEscapeParser.backgroundColors); // can be grouped by concept here in code. // ANSIEscapeParser.styles = { - 0 : 'default', // Everything disabled + 0 : 'default', // Everything disabled - 1 : 'intensityBright', // aka bold - 2 : 'intensityDim', - 22 : 'intensityNormal', + 1 : 'intensityBright', // aka bold + 2 : 'intensityDim', + 22 : 'intensityNormal', - 4 : 'underlineOn', // Not supported by most BBS-like terminals - 24 : 'underlineOff', // Not supported by most BBS-like terminals + 4 : 'underlineOn', // Not supported by most BBS-like terminals + 24 : 'underlineOff', // Not supported by most BBS-like terminals - 5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same - 6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same - 25 : 'blinkOff', + 5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same + 6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same + 25 : 'blinkOff', - 7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG" - 27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG" + 7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG" + 27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG" - 8 : 'invisibleOn', // FG set to BG - 28 : 'invisibleOff', // Not supported by most BBS-like terminals + 8 : 'invisibleOn', // FG set to BG + 28 : 'invisibleOff', // Not supported by most BBS-like terminals }; Object.freeze(ANSIEscapeParser.styles); diff --git a/core/ansi_prep.js b/core/ansi_prep.js index a4c894d8..3eb05b08 100644 --- a/core/ansi_prep.js +++ b/core/ansi_prep.js @@ -5,216 +5,216 @@ const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSI = require('./ansi_term.js'); const { - splitTextAtTerms, - renderStringLength + splitTextAtTerms, + renderStringLength } = require('./string_util.js'); // deps const _ = require('lodash'); module.exports = function ansiPrep(input, options, cb) { - if(!input) { - return cb(null, ''); - } + if(!input) { + return cb(null, ''); + } - options.termWidth = options.termWidth || 80; - options.termHeight = options.termHeight || 25; - options.cols = options.cols || options.termWidth || 80; - options.rows = options.rows || options.termHeight || 'auto'; - options.startCol = options.startCol || 1; - options.exportMode = options.exportMode || false; - options.fillLines = _.get(options, 'fillLines', true); - options.indent = options.indent || 0; + options.termWidth = options.termWidth || 80; + options.termHeight = options.termHeight || 25; + options.cols = options.cols || options.termWidth || 80; + options.rows = options.rows || options.termHeight || 'auto'; + options.startCol = options.startCol || 1; + options.exportMode = options.exportMode || false; + options.fillLines = _.get(options, 'fillLines', true); + options.indent = options.indent || 0; - // in auto we start out at 25 rows, but can always expand for more - const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); - const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } ); + // in auto we start out at 25 rows, but can always expand for more + const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); + const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } ); - const state = { - row : 0, - col : 0, - }; + const state = { + row : 0, + col : 0, + }; - let lastRow = 0; + let lastRow = 0; - function ensureRow(row) { - if(canvas[row]) { - return; - } + function ensureRow(row) { + if(canvas[row]) { + return; + } - canvas[row] = Array.from( { length : options.cols}, () => new Object() ); - } + canvas[row] = Array.from( { length : options.cols}, () => new Object() ); + } - parser.on('position update', (row, col) => { - state.row = row - 1; - state.col = col - 1; + parser.on('position update', (row, col) => { + state.row = row - 1; + state.col = col - 1; - if(0 === state.col) { - state.initialSgr = state.lastSgr; - } + if(0 === state.col) { + state.initialSgr = state.lastSgr; + } - lastRow = Math.max(state.row, lastRow); - }); + lastRow = Math.max(state.row, lastRow); + }); - parser.on('literal', literal => { - // - // CR/LF are handled for 'position update'; we don't need the chars themselves - // - literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); + parser.on('literal', literal => { + // + // CR/LF are handled for 'position update'; we don't need the chars themselves + // + literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); - for(let c of literal) { - if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) { - ensureRow(state.row); + for(let c of literal) { + if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) { + ensureRow(state.row); - if(0 === state.col) { - canvas[state.row][state.col].initialSgr = state.initialSgr; - } + if(0 === state.col) { + canvas[state.row][state.col].initialSgr = state.initialSgr; + } - canvas[state.row][state.col].char = c; + canvas[state.row][state.col].char = c; - if(state.sgr) { - canvas[state.row][state.col].sgr = _.clone(state.sgr); - state.lastSgr = canvas[state.row][state.col].sgr; - state.sgr = null; - } - } + if(state.sgr) { + canvas[state.row][state.col].sgr = _.clone(state.sgr); + state.lastSgr = canvas[state.row][state.col].sgr; + state.sgr = null; + } + } - state.col += 1; - } - }); + state.col += 1; + } + }); - parser.on('sgr update', sgr => { - ensureRow(state.row); + parser.on('sgr update', sgr => { + ensureRow(state.row); - if(state.col < options.cols) { - canvas[state.row][state.col].sgr = _.clone(sgr); - state.lastSgr = canvas[state.row][state.col].sgr; - } else { - state.sgr = sgr; - } - }); + if(state.col < options.cols) { + canvas[state.row][state.col].sgr = _.clone(sgr); + state.lastSgr = canvas[state.row][state.col].sgr; + } else { + state.sgr = sgr; + } + }); - function getLastPopulatedColumn(row) { - let col = row.length; - while(--col > 0) { - if(row[col].char || row[col].sgr) { - break; - } - } - return col; - } + function getLastPopulatedColumn(row) { + let col = row.length; + while(--col > 0) { + if(row[col].char || row[col].sgr) { + break; + } + } + return col; + } - parser.on('complete', () => { - let output = ''; - let line; - let sgr; + parser.on('complete', () => { + let output = ''; + let line; + let sgr; - canvas.slice(0, lastRow + 1).forEach(row => { - const lastCol = getLastPopulatedColumn(row) + 1; + canvas.slice(0, lastRow + 1).forEach(row => { + const lastCol = getLastPopulatedColumn(row) + 1; - let i; - line = options.indent ? - output.length > 0 ? ' '.repeat(options.indent) : '' : - ''; + let i; + line = options.indent ? + output.length > 0 ? ' '.repeat(options.indent) : '' : + ''; - for(i = 0; i < lastCol; ++i) { - const col = row[i]; + for(i = 0; i < lastCol; ++i) { + const col = row[i]; - sgr = !options.asciiMode && 0 === i ? - col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : - ''; + sgr = !options.asciiMode && 0 === i ? + col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : + ''; - if(!options.asciiMode && col.sgr) { - sgr += ANSI.getSGRFromGraphicRendition(col.sgr); - } + if(!options.asciiMode && col.sgr) { + sgr += ANSI.getSGRFromGraphicRendition(col.sgr); + } - line += `${sgr}${col.char || ' '}`; - } + line += `${sgr}${col.char || ' '}`; + } - output += line; + output += line; - if(i < row.length) { - output += `${options.asciiMode ? '' : ANSI.blackBG()}`; - if(options.fillLines) { - output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; - } - } + if(i < row.length) { + output += `${options.asciiMode ? '' : ANSI.blackBG()}`; + if(options.fillLines) { + output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; + } + } - if(options.startCol + i < options.termWidth || options.forceLineTerm) { - output += '\r\n'; - } - }); + if(options.startCol + i < options.termWidth || options.forceLineTerm) { + output += '\r\n'; + } + }); - if(options.exportMode) { - // - // If we're in export mode, we do some additional hackery: - // - // * Hard wrap ALL lines at <= 79 *characters* (not visible columns) - // if a line must wrap early, we'll place a ESC[A ESC[C where - // represents chars to get back to the position we were previously at - // - // * Replace contig spaces with ESC[C as well to save... space. - // - // :TODO: this would be better to do as part of the processing above, but this will do for now - const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with - let exportOutput = ''; + if(options.exportMode) { + // + // If we're in export mode, we do some additional hackery: + // + // * Hard wrap ALL lines at <= 79 *characters* (not visible columns) + // if a line must wrap early, we'll place a ESC[A ESC[C where + // represents chars to get back to the position we were previously at + // + // * Replace contig spaces with ESC[C as well to save... space. + // + // :TODO: this would be better to do as part of the processing above, but this will do for now + const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with + let exportOutput = ''; - let m; - let afterSeq; - let wantMore; - let renderStart; + let m; + let afterSeq; + let wantMore; + let renderStart; - splitTextAtTerms(output).forEach(fullLine => { - renderStart = 0; + splitTextAtTerms(output).forEach(fullLine => { + renderStart = 0; - while(fullLine.length > 0) { - let splitAt; - const ANSI_REGEXP = ANSI.getFullMatchRegExp(); - wantMore = true; + while(fullLine.length > 0) { + let splitAt; + const ANSI_REGEXP = ANSI.getFullMatchRegExp(); + wantMore = true; - while((m = ANSI_REGEXP.exec(fullLine))) { - afterSeq = m.index + m[0].length; + while((m = ANSI_REGEXP.exec(fullLine))) { + afterSeq = m.index + m[0].length; - if(afterSeq < MAX_CHARS) { - // after current seq - splitAt = afterSeq; - } else { - if(m.index < MAX_CHARS) { - // before last found seq - splitAt = m.index; - wantMore = false; // can't eat up any more - } + if(afterSeq < MAX_CHARS) { + // after current seq + splitAt = afterSeq; + } else { + if(m.index < MAX_CHARS) { + // before last found seq + splitAt = m.index; + wantMore = false; // can't eat up any more + } - break; // seq's beyond this point are >= MAX_CHARS - } - } + break; // seq's beyond this point are >= MAX_CHARS + } + } - if(splitAt) { - if(wantMore) { - splitAt = Math.min(fullLine.length, MAX_CHARS - 1); - } - } else { - splitAt = Math.min(fullLine.length, MAX_CHARS - 1); - } + if(splitAt) { + if(wantMore) { + splitAt = Math.min(fullLine.length, MAX_CHARS - 1); + } + } else { + splitAt = Math.min(fullLine.length, MAX_CHARS - 1); + } - const part = fullLine.slice(0, splitAt); - fullLine = fullLine.slice(splitAt); - renderStart += renderStringLength(part); - exportOutput += `${part}\r\n`; + const part = fullLine.slice(0, splitAt); + fullLine = fullLine.slice(splitAt); + renderStart += renderStringLength(part); + exportOutput += `${part}\r\n`; - if(fullLine.length > 0) { // more to go for this line? - exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; - } else { - exportOutput += ANSI.up(); - } - } - }); + if(fullLine.length > 0) { // more to go for this line? + exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; + } else { + exportOutput += ANSI.up(); + } + } + }); - return cb(null, exportOutput); - } + return cb(null, exportOutput); + } - return cb(null, output); - }); + return cb(null, output); + }); - parser.parse(input); + parser.parse(input); }; diff --git a/core/ansi_term.js b/core/ansi_term.js index 0a1eaa41..dc3399a6 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -65,86 +65,86 @@ exports.vtxHyperlink = vtxHyperlink; const ESC_CSI = '\u001b['; const CONTROL = { - up : 'A', - down : 'B', + up : 'A', + down : 'B', - forward : 'C', - right : 'C', + forward : 'C', + right : 'C', - back : 'D', - left : 'D', + back : 'D', + left : 'D', - nextLine : 'E', - prevLine : 'F', - horizAbsolute : 'G', + nextLine : 'E', + prevLine : 'F', + horizAbsolute : 'G', - // - // CSI [ p1 ] J - // Erase in Page / Erase Data - // Defaults: p1 = 0 - // Erases from the current screen according to the value of p1 - // 0 - Erase from the current position to the end of the screen. - // 1 - Erase from the current position to the start of the screen. - // 2 - Erase entire screen. As a violation of ECMA-048, also moves - // the cursor to position 1/1 as a number of BBS programs assume - // this behaviour. - // Erased characters are set to the current attribute. - // - // Support: - // * SyncTERM: Works as expected - // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 - // and screen remainder - // - eraseData : 'J', + // + // CSI [ p1 ] J + // Erase in Page / Erase Data + // Defaults: p1 = 0 + // Erases from the current screen according to the value of p1 + // 0 - Erase from the current position to the end of the screen. + // 1 - Erase from the current position to the start of the screen. + // 2 - Erase entire screen. As a violation of ECMA-048, also moves + // the cursor to position 1/1 as a number of BBS programs assume + // this behaviour. + // Erased characters are set to the current attribute. + // + // Support: + // * SyncTERM: Works as expected + // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 + // and screen remainder + // + eraseData : 'J', - eraseLine : 'K', - insertLine : 'L', + eraseLine : 'K', + insertLine : 'L', - // - // CSI [ p1 ] M - // Delete Line(s) / "ANSI" Music - // Defaults: p1 = 1 - // Deletes the current line and the p1 - 1 lines after it scrolling the - // first non-deleted line up to the current line and filling the newly - // empty lines at the end of the screen with the current attribute. - // If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music - // instead. - // See "ANSI" MUSIC section for more details. - // - // Support: - // * SyncTERM: Works as expected - // * NetRunner: - // - // General Notes: - // See also notes in bansi.txt and cterm.txt about the various - // incompatibilities & oddities around this sequence. ANSI-BBS - // states that it *should* work with any value of p1. - // - deleteLine : 'M', - ansiMusic : 'M', + // + // CSI [ p1 ] M + // Delete Line(s) / "ANSI" Music + // Defaults: p1 = 1 + // Deletes the current line and the p1 - 1 lines after it scrolling the + // first non-deleted line up to the current line and filling the newly + // empty lines at the end of the screen with the current attribute. + // If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music + // instead. + // See "ANSI" MUSIC section for more details. + // + // Support: + // * SyncTERM: Works as expected + // * NetRunner: + // + // General Notes: + // See also notes in bansi.txt and cterm.txt about the various + // incompatibilities & oddities around this sequence. ANSI-BBS + // states that it *should* work with any value of p1. + // + deleteLine : 'M', + ansiMusic : 'M', - scrollUp : 'S', - scrollDown : 'T', - setScrollRegion : 'r', - savePos : 's', - restorePos : 'u', - queryPos : '6n', - queryScreenSize : '255n', // See bansi.txt - goto : 'H', // row Pr, column Pc -- same as f - gotoAlt : 'f', // same as H + scrollUp : 'S', + scrollDown : 'T', + setScrollRegion : 'r', + savePos : 's', + restorePos : 'u', + queryPos : '6n', + queryScreenSize : '255n', // See bansi.txt + goto : 'H', // row Pr, column Pc -- same as f + gotoAlt : 'f', // same as H - blinkToBrightIntensity : '?33h', - blinkNormal : '?33l', + blinkToBrightIntensity : '?33h', + blinkNormal : '?33l', - emulationSpeed : '*r', // Set output emulation speed. See cterm.txt + emulationSpeed : '*r', // Set output emulation speed. See cterm.txt - hideCursor : '?25l', // Nonstandard - cterm.txt - showCursor : '?25h', // Nonstandard - cterm.txt + hideCursor : '?25l', // Nonstandard - cterm.txt + showCursor : '?25h', // Nonstandard - cterm.txt - queryDeviceAttributes : 'c', // Nonstandard - cterm.txt + queryDeviceAttributes : 'c', // Nonstandard - cterm.txt - // :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes - // apparently some terms can report screen size and text area via 18t and 19t + // :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes + // apparently some terms can report screen size and text area via 18t and 19t }; // @@ -152,49 +152,49 @@ const CONTROL = { // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // const SGRValues = { - reset : 0, - bold : 1, - dim : 2, - blink : 5, - fastBlink : 6, - negative : 7, - hidden : 8, + reset : 0, + bold : 1, + dim : 2, + blink : 5, + fastBlink : 6, + negative : 7, + hidden : 8, - normal : 22, // - steady : 25, - positive : 27, + normal : 22, // + steady : 25, + positive : 27, - black : 30, - red : 31, - green : 32, - yellow : 33, - blue : 34, - magenta : 35, - cyan : 36, - white : 37, + black : 30, + red : 31, + green : 32, + yellow : 33, + blue : 34, + magenta : 35, + cyan : 36, + white : 37, - blackBG : 40, - redBG : 41, - greenBG : 42, - yellowBG : 43, - blueBG : 44, - magentaBG : 45, - cyanBG : 46, - whiteBG : 47, + blackBG : 40, + redBG : 41, + greenBG : 42, + yellowBG : 43, + blueBG : 44, + magentaBG : 45, + cyanBG : 46, + whiteBG : 47, }; function getFullMatchRegExp(flags = 'g') { - // :TODO: expand this a bit - see strip-ansi/etc. - // :TODO: \u009b ? - return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex + // :TODO: expand this a bit - see strip-ansi/etc. + // :TODO: \u009b ? + return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex } function getFGColorValue(name) { - return SGRValues[name]; + return SGRValues[name]; } function getBGColorValue(name) { - return SGRValues[name + 'BG']; + return SGRValues[name + 'BG']; } @@ -214,49 +214,49 @@ function getBGColorValue(name) { // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // const SYNCTERM_FONT_AND_ENCODING_TABLE = [ - 'cp437', - 'cp1251', - 'koi8_r', - 'iso8859_2', - 'iso8859_4', - 'cp866', - 'iso8859_9', - 'haik8', - 'iso8859_8', - 'koi8_u', - 'iso8859_15', - 'iso8859_4', - 'koi8_r_b', - 'iso8859_4', - 'iso8859_5', - 'ARMSCII_8', - 'iso8859_15', - 'cp850', - 'cp850', - 'cp885', - 'cp1251', - 'iso8859_7', - 'koi8-r_c', - 'iso8859_4', - 'iso8859_1', - 'cp866', - 'cp437', - 'cp866', - 'cp885', - 'cp866_u', - 'iso8859_1', - 'cp1131', - 'c64_upper', - 'c64_lower', - 'c128_upper', - 'c128_lower', - 'atari', - 'pot_noodle', - 'mo_soul', - 'microknight_plus', - 'topaz_plus', - 'microknight', - 'topaz', + 'cp437', + 'cp1251', + 'koi8_r', + 'iso8859_2', + 'iso8859_4', + 'cp866', + 'iso8859_9', + 'haik8', + 'iso8859_8', + 'koi8_u', + 'iso8859_15', + 'iso8859_4', + 'koi8_r_b', + 'iso8859_4', + 'iso8859_5', + 'ARMSCII_8', + 'iso8859_15', + 'cp850', + 'cp850', + 'cp885', + 'cp1251', + 'iso8859_7', + 'koi8-r_c', + 'iso8859_4', + 'iso8859_1', + 'cp866', + 'cp437', + 'cp866', + 'cp885', + 'cp866_u', + 'iso8859_1', + 'cp1131', + 'c64_upper', + 'c64_lower', + 'c128_upper', + 'c128_lower', + 'atari', + 'pot_noodle', + 'mo_soul', + 'microknight_plus', + 'topaz_plus', + 'microknight', + 'topaz', ]; // @@ -267,137 +267,137 @@ const SYNCTERM_FONT_AND_ENCODING_TABLE = [ // replaced with '_' for lookup purposes. // const FONT_ALIAS_TO_SYNCTERM_MAP = { - 'cp437' : 'cp437', - 'ibm_vga' : 'cp437', - 'ibmpc' : 'cp437', - 'ibm_pc' : 'cp437', - 'pc' : 'cp437', - 'cp437_art' : 'cp437', - 'ibmpcart' : 'cp437', - 'ibmpc_art' : 'cp437', - 'ibm_pc_art' : 'cp437', - 'msdos_art' : 'cp437', - 'msdosart' : 'cp437', - 'pc_art' : 'cp437', - 'pcart' : 'cp437', + 'cp437' : 'cp437', + 'ibm_vga' : 'cp437', + 'ibmpc' : 'cp437', + 'ibm_pc' : 'cp437', + 'pc' : 'cp437', + 'cp437_art' : 'cp437', + 'ibmpcart' : 'cp437', + 'ibmpc_art' : 'cp437', + 'ibm_pc_art' : 'cp437', + 'msdos_art' : 'cp437', + 'msdosart' : 'cp437', + 'pc_art' : 'cp437', + 'pcart' : 'cp437', - 'ibm_vga50' : 'cp437', - 'ibm_vga25g' : 'cp437', - 'ibm_ega' : 'cp437', - 'ibm_ega43' : 'cp437', + 'ibm_vga50' : 'cp437', + 'ibm_vga25g' : 'cp437', + 'ibm_ega' : 'cp437', + 'ibm_ega43' : 'cp437', - 'topaz' : 'topaz', - 'amiga_topaz_1' : 'topaz', - 'amiga_topaz_1+' : 'topaz_plus', - 'topazplus' : 'topaz_plus', - 'topaz_plus' : 'topaz_plus', - 'amiga_topaz_2' : 'topaz', - 'amiga_topaz_2+' : 'topaz_plus', - 'topaz2plus' : 'topaz_plus', + 'topaz' : 'topaz', + 'amiga_topaz_1' : 'topaz', + 'amiga_topaz_1+' : 'topaz_plus', + 'topazplus' : 'topaz_plus', + 'topaz_plus' : 'topaz_plus', + 'amiga_topaz_2' : 'topaz', + 'amiga_topaz_2+' : 'topaz_plus', + 'topaz2plus' : 'topaz_plus', - 'pot_noodle' : 'pot_noodle', - 'p0tnoodle' : 'pot_noodle', - 'amiga_p0t-noodle' : 'pot_noodle', + 'pot_noodle' : 'pot_noodle', + 'p0tnoodle' : 'pot_noodle', + 'amiga_p0t-noodle' : 'pot_noodle', - 'mo_soul' : 'mo_soul', - 'mosoul' : 'mo_soul', - 'mO\'sOul' : 'mo_soul', + 'mo_soul' : 'mo_soul', + 'mosoul' : 'mo_soul', + 'mO\'sOul' : 'mo_soul', - 'amiga_microknight' : 'microknight', - 'amiga_microknight+' : 'microknight_plus', + 'amiga_microknight' : 'microknight', + 'amiga_microknight+' : 'microknight_plus', - 'atari' : 'atari', - 'atarist' : 'atari', + 'atari' : 'atari', + 'atarist' : 'atari', }; function setSyncTERMFont(name, fontPage) { - const p1 = miscUtil.valueWithDefault(fontPage, 0); + const p1 = miscUtil.valueWithDefault(fontPage, 0); - assert(p1 >= 0 && p1 <= 3); + assert(p1 >= 0 && p1 <= 3); - const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name); - if(p2 > -1) { - return `${ESC_CSI}${p1};${p2} D`; - } + const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name); + if(p2 > -1) { + return `${ESC_CSI}${p1};${p2} D`; + } - return ''; + return ''; } function getSyncTERMFontFromAlias(alias) { - return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')]; + return FONT_ALIAS_TO_SYNCTERM_MAP[alias.toLowerCase().replace(/ /g, '_')]; } function setSyncTermFontWithAlias(nameOrAlias) { - nameOrAlias = getSyncTERMFontFromAlias(nameOrAlias) || nameOrAlias; - return setSyncTERMFont(nameOrAlias); + nameOrAlias = getSyncTERMFontFromAlias(nameOrAlias) || nameOrAlias; + return setSyncTERMFont(nameOrAlias); } const DEC_CURSOR_STYLE = { - 'blinking block' : 0, - 'default' : 1, - 'steady block' : 2, - 'blinking underline' : 3, - 'steady underline' : 4, - 'blinking bar' : 5, - 'steady bar' : 6, + 'blinking block' : 0, + 'default' : 1, + 'steady block' : 2, + 'blinking underline' : 3, + 'steady underline' : 4, + 'blinking bar' : 5, + 'steady bar' : 6, }; function setCursorStyle(cursorStyle) { - const ps = DEC_CURSOR_STYLE[cursorStyle]; - if(ps) { - return `${ESC_CSI}${ps} q`; - } - return ''; + const ps = DEC_CURSOR_STYLE[cursorStyle]; + if(ps) { + return `${ESC_CSI}${ps} q`; + } + return ''; } // Create methods such as up(), nextLine(),... Object.keys(CONTROL).forEach(function onControlName(name) { - const code = CONTROL[name]; + const code = CONTROL[name]; - exports[name] = function() { - let c = code; - if(arguments.length > 0) { - // arguments are array like -- we want an array - c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; - } - return `${ESC_CSI}${c}`; - }; + exports[name] = function() { + let c = code; + if(arguments.length > 0) { + // arguments are array like -- we want an array + c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; + } + return `${ESC_CSI}${c}`; + }; }); // Create various color methods such as white(), yellowBG(), reset(), ... Object.keys(SGRValues).forEach( name => { - const code = SGRValues[name]; + const code = SGRValues[name]; - exports[name] = function() { - return `${ESC_CSI}${code}m`; - }; + exports[name] = function() { + return `${ESC_CSI}${code}m`; + }; }); function sgr() { - // - // - Allow an single array or variable number of arguments - // - Each element can be either a integer or string found in SGRValues - // which in turn maps to a integer - // - if(arguments.length <= 0) { - return ''; - } + // + // - Allow an single array or variable number of arguments + // - Each element can be either a integer or string found in SGRValues + // which in turn maps to a integer + // + if(arguments.length <= 0) { + return ''; + } - let result = []; - const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; + let result = []; + const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; - for(let i = 0; i < args.length; ++i) { - const arg = args[i]; - if(_.isString(arg) && arg in SGRValues) { - result.push(SGRValues[arg]); - } else if(_.isNumber(arg)) { - result.push(arg); - } - } + for(let i = 0; i < args.length; ++i) { + const arg = args[i]; + if(_.isString(arg) && arg in SGRValues) { + result.push(SGRValues[arg]); + } else if(_.isNumber(arg)) { + result.push(arg); + } + } - return `${ESC_CSI}${result.join(';')}m`; + return `${ESC_CSI}${result.join(';')}m`; } // @@ -405,29 +405,29 @@ function sgr() { // to a ANSI SGR sequence. // function getSGRFromGraphicRendition(graphicRendition, initialReset) { - let sgrSeq = []; - let styleCount = 0; + let sgrSeq = []; + let styleCount = 0; - [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => { - if(graphicRendition[s]) { - sgrSeq.push(graphicRendition[s]); - ++styleCount; - } - }); + [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => { + if(graphicRendition[s]) { + sgrSeq.push(graphicRendition[s]); + ++styleCount; + } + }); - if(graphicRendition.fg) { - sgrSeq.push(graphicRendition.fg); - } + if(graphicRendition.fg) { + sgrSeq.push(graphicRendition.fg); + } - if(graphicRendition.bg) { - sgrSeq.push(graphicRendition.bg); - } + if(graphicRendition.bg) { + sgrSeq.push(graphicRendition.bg); + } - if(0 === styleCount || initialReset) { - sgrSeq.unshift(0); - } + if(0 === styleCount || initialReset) { + sgrSeq.unshift(0); + } - return sgr(sgrSeq); + return sgr(sgrSeq); } /////////////////////////////////////////////////////////////////////////////// @@ -435,19 +435,19 @@ function getSGRFromGraphicRendition(graphicRendition, initialReset) { /////////////////////////////////////////////////////////////////////////////// function clearScreen() { - return exports.eraseData(2); + return exports.eraseData(2); } function resetScreen() { - return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`; + return `${exports.reset()}${exports.eraseData(2)}${exports.goHome()}`; } function normal() { - return sgr( [ 'normal', 'reset' ] ); + return sgr( [ 'normal', 'reset' ] ); } function goHome() { - return exports.goto(); // no params = home = 1,1 + return exports.goto(); // no params = home = 1,1 } // @@ -463,36 +463,36 @@ function goHome() { // and use term width -- generally 80 columns -- will display garbled! // function disableVT100LineWrapping() { - return `${ESC_CSI}?7l`; + return `${ESC_CSI}?7l`; } function setEmulatedBaudRate(rate) { - const speed = { - unlimited : 0, - off : 0, - 0 : 0, - 300 : 1, - 600 : 2, - 1200 : 3, - 2400 : 4, - 4800 : 5, - 9600 : 6, - 19200 : 7, - 38400 : 8, - 57600 : 9, - 76800 : 10, - 115200 : 11, - }[rate] || 0; - return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); + const speed = { + unlimited : 0, + off : 0, + 0 : 0, + 300 : 1, + 600 : 2, + 1200 : 3, + 2400 : 4, + 4800 : 5, + 9600 : 6, + 19200 : 7, + 38400 : 8, + 57600 : 9, + 76800 : 10, + 115200 : 11, + }[rate] || 0; + return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); } function vtxHyperlink(client, url, len) { - if(!client.terminalSupports('vtx_hyperlink')) { - return ''; - } + if(!client.terminalSupports('vtx_hyperlink')) { + return ''; + } - len = len || url.length; + len = len || url.length; - url = url.split('').map(c => c.charCodeAt(0)).join(';'); - return `${ESC_CSI}1;${len};1;1;${url}\\`; + url = url.split('').map(c => c.charCodeAt(0)).join(';'); + return `${ESC_CSI}1;${len};1;1;${url}\\`; } \ No newline at end of file diff --git a/core/archive_util.js b/core/archive_util.js index e4604b62..d59a2609 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -16,314 +16,314 @@ const paths = require('path'); let archiveUtil; class Archiver { - constructor(config) { - this.compress = config.compress; - this.decompress = config.decompress; - this.list = config.list; - this.extract = config.extract; - } + constructor(config) { + this.compress = config.compress; + this.decompress = config.decompress; + this.list = config.list; + this.extract = config.extract; + } - ok() { - return this.canCompress() && this.canDecompress(); - } + ok() { + return this.canCompress() && this.canDecompress(); + } - can(what) { - if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) { - return false; - } + can(what) { + if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) { + return false; + } - return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0; - } + return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0; + } - canCompress() { return this.can('compress'); } - canDecompress() { return this.can('decompress'); } - canList() { return this.can('list'); } // :TODO: validate entryMatch - canExtract() { return this.can('extract'); } + canCompress() { return this.can('compress'); } + canDecompress() { return this.can('decompress'); } + canList() { return this.can('list'); } // :TODO: validate entryMatch + canExtract() { return this.can('extract'); } } module.exports = class ArchiveUtil { - constructor() { - this.archivers = {}; - this.longestSignature = 0; - } + constructor() { + this.archivers = {}; + this.longestSignature = 0; + } - // singleton access - static getInstance() { - if(!archiveUtil) { - archiveUtil = new ArchiveUtil(); - archiveUtil.init(); - } - return archiveUtil; - } + // singleton access + static getInstance() { + if(!archiveUtil) { + archiveUtil = new ArchiveUtil(); + archiveUtil.init(); + } + return archiveUtil; + } - init() { - // - // Load configuration - // - const config = Config(); - if(_.has(config, 'archives.archivers')) { - Object.keys(config.archives.archivers).forEach(archKey => { + init() { + // + // Load configuration + // + const config = Config(); + if(_.has(config, 'archives.archivers')) { + Object.keys(config.archives.archivers).forEach(archKey => { - const archConfig = config.archives.archivers[archKey]; - const archiver = new Archiver(archConfig); + const archConfig = config.archives.archivers[archKey]; + const archiver = new Archiver(archConfig); - if(!archiver.ok()) { - // :TODO: Log warning - bad archiver/config - } + if(!archiver.ok()) { + // :TODO: Log warning - bad archiver/config + } - this.archivers[archKey] = archiver; - }); - } + this.archivers[archKey] = archiver; + }); + } - if(_.isObject(config.fileTypes)) { - const updateSig = (ft) => { - ft.sig = Buffer.from(ft.sig, 'hex'); - ft.offset = ft.offset || 0; + if(_.isObject(config.fileTypes)) { + const updateSig = (ft) => { + ft.sig = Buffer.from(ft.sig, 'hex'); + ft.offset = ft.offset || 0; - // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well - const sigLen = ft.offset + ft.sig.length; - if(sigLen > this.longestSignature) { - this.longestSignature = sigLen; - } - }; + // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well + const sigLen = ft.offset + ft.sig.length; + if(sigLen > this.longestSignature) { + this.longestSignature = sigLen; + } + }; - Object.keys(config.fileTypes).forEach(mimeType => { - const fileType = config.fileTypes[mimeType]; - if(Array.isArray(fileType)) { - fileType.forEach(ft => { - if(ft.sig) { - updateSig(ft); - } - }); - } else if(fileType.sig) { - updateSig(fileType); - } - }); - } - } + Object.keys(config.fileTypes).forEach(mimeType => { + const fileType = config.fileTypes[mimeType]; + if(Array.isArray(fileType)) { + fileType.forEach(ft => { + if(ft.sig) { + updateSig(ft); + } + }); + } else if(fileType.sig) { + updateSig(fileType); + } + }); + } + } - getArchiver(mimeTypeOrExtension, justExtention) { - const mimeType = resolveMimeType(mimeTypeOrExtension); + getArchiver(mimeTypeOrExtension, justExtention) { + const mimeType = resolveMimeType(mimeTypeOrExtension); - if(!mimeType) { // lookup returns false on failure - return; - } + if(!mimeType) { // lookup returns false on failure + return; + } - const config = Config(); - let fileType = _.get(config, [ 'fileTypes', mimeType ] ); + const config = Config(); + let fileType = _.get(config, [ 'fileTypes', mimeType ] ); - if(Array.isArray(fileType)) { - if(!justExtention) { - // need extention for lookup; ambiguous as-is :( - return; - } - // further refine by extention - fileType = fileType.find(ft => justExtention === ft.ext); - } + if(Array.isArray(fileType)) { + if(!justExtention) { + // need extention for lookup; ambiguous as-is :( + return; + } + // further refine by extention + fileType = fileType.find(ft => justExtention === ft.ext); + } - if(!_.isObject(fileType)) { - return; - } + if(!_.isObject(fileType)) { + return; + } - if(fileType.archiveHandler) { - return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] ); - } - } + if(fileType.archiveHandler) { + return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] ); + } + } - haveArchiver(archType) { - return this.getArchiver(archType) ? true : false; - } + haveArchiver(archType) { + return this.getArchiver(archType) ? true : false; + } - // :TODO: implement me: - /* - detectTypeWithBuf(buf, cb) { + // :TODO: implement me: + /* + detectTypeWithBuf(buf, cb) { } */ - detectType(path, cb) { - fs.open(path, 'r', (err, fd) => { - if(err) { - return cb(err); - } + detectType(path, cb) { + fs.open(path, 'r', (err, fd) => { + if(err) { + return cb(err); + } - const buf = Buffer.alloc(this.longestSignature); - fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { - if(err) { - return cb(err); - } + const buf = Buffer.alloc(this.longestSignature); + fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { + if(err) { + return cb(err); + } - const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => { - const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ]; - return fileTypeInfos.find(fti => { - if(!fti.sig || !fti.archiveHandler) { - return false; - } + const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => { + const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ]; + return fileTypeInfos.find(fti => { + if(!fti.sig || !fti.archiveHandler) { + return false; + } - const lenNeeded = fti.offset + fti.sig.length; + const lenNeeded = fti.offset + fti.sig.length; - if(bytesRead < lenNeeded) { - return false; - } + if(bytesRead < lenNeeded) { + return false; + } - const comp = buf.slice(fti.offset, fti.offset + fti.sig.length); - return (fti.sig.equals(comp)); - }); - }); + const comp = buf.slice(fti.offset, fti.offset + fti.sig.length); + return (fti.sig.equals(comp)); + }); + }); - return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); - }); - }); - } + return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); + }); + }); + } - spawnHandler(proc, action, cb) { - // pty.js doesn't currently give us a error when things fail, - // so we have this horrible, horrible hack: - let err; - proc.once('data', d => { - if(_.isString(d) && d.startsWith('execvp(3) failed.')) { - err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); - } - }); + spawnHandler(proc, action, cb) { + // pty.js doesn't currently give us a error when things fail, + // so we have this horrible, horrible hack: + let err; + proc.once('data', d => { + if(_.isString(d) && d.startsWith('execvp(3) failed.')) { + err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); + } + }); - proc.once('exit', exitCode => { - return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err); - }); - } + proc.once('exit', exitCode => { + return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err); + }); + } - compressTo(archType, archivePath, files, cb) { - const archiver = this.getArchiver(archType, paths.extname(archivePath)); + compressTo(archType, archivePath, files, cb) { + const archiver = this.getArchiver(archType, paths.extname(archivePath)); - if(!archiver) { - return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); - } + if(!archiver) { + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); + } - const fmtObj = { - archivePath : archivePath, - fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! - }; + const fmtObj = { + archivePath : archivePath, + fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! + }; - const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); + const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); - let proc; - try { - proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); - } catch(e) { - return cb(e); - } + let proc; + try { + proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); + } catch(e) { + return cb(e); + } - return this.spawnHandler(proc, 'Compression', cb); - } + return this.spawnHandler(proc, 'Compression', cb); + } - extractTo(archivePath, extractPath, archType, fileList, cb) { - let haveFileList; + extractTo(archivePath, extractPath, archType, fileList, cb) { + let haveFileList; - if(!cb && _.isFunction(fileList)) { - cb = fileList; - fileList = []; - haveFileList = false; - } else { - haveFileList = true; - } + if(!cb && _.isFunction(fileList)) { + cb = fileList; + fileList = []; + haveFileList = false; + } else { + haveFileList = true; + } - const archiver = this.getArchiver(archType, paths.extname(archivePath)); + const archiver = this.getArchiver(archType, paths.extname(archivePath)); - if(!archiver) { - return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); - } + if(!archiver) { + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); + } - const fmtObj = { - archivePath : archivePath, - extractPath : extractPath, - }; + const fmtObj = { + archivePath : archivePath, + extractPath : extractPath, + }; - let action = haveFileList ? 'extract' : 'decompress'; - if('extract' === action && !_.isObject(archiver[action])) { - // we're forced to do a full decompress - action = 'decompress'; - haveFileList = false; - } + let action = haveFileList ? 'extract' : 'decompress'; + if('extract' === action && !_.isObject(archiver[action])) { + // we're forced to do a full decompress + action = 'decompress'; + haveFileList = false; + } - // we need to treat {fileList} special in that it should be broken up to 0:n args - const args = archiver[action].args.map( arg => { - return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); - }); + // we need to treat {fileList} special in that it should be broken up to 0:n args + const args = archiver[action].args.map( arg => { + return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); + }); - const fileListPos = args.indexOf('{fileList}'); - if(fileListPos > -1) { - // replace {fileList} with 0:n sep file list arguments - args.splice.apply(args, [fileListPos, 1].concat(fileList)); - } + const fileListPos = args.indexOf('{fileList}'); + if(fileListPos > -1) { + // replace {fileList} with 0:n sep file list arguments + args.splice.apply(args, [fileListPos, 1].concat(fileList)); + } - let proc; - try { - proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath)); - } catch(e) { - return cb(e); - } + let proc; + try { + proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath)); + } catch(e) { + return cb(e); + } - return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb); - } + return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb); + } - listEntries(archivePath, archType, cb) { - const archiver = this.getArchiver(archType, paths.extname(archivePath)); + listEntries(archivePath, archType, cb) { + const archiver = this.getArchiver(archType, paths.extname(archivePath)); - if(!archiver) { - return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); - } + if(!archiver) { + return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); + } - const fmtObj = { - archivePath : archivePath, - }; + const fmtObj = { + archivePath : archivePath, + }; - const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); + const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); - let proc; - try { - proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); - } catch(e) { - return cb(e); - } + let proc; + try { + proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); + } catch(e) { + return cb(e); + } - let output = ''; - proc.on('data', data => { - // :TODO: hack for: execvp(3) failed.: No such file or directory + let output = ''; + proc.on('data', data => { + // :TODO: hack for: execvp(3) failed.: No such file or directory - output += data; - }); + output += data; + }); - proc.once('exit', exitCode => { - if(exitCode) { - return cb(Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)); - } + proc.once('exit', exitCode => { + if(exitCode) { + return cb(Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)); + } - const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 }; + const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 }; - const entries = []; - const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm'); - let m; - while((m = entryMatchRe.exec(output))) { - entries.push({ - byteSize : parseInt(m[entryGroupOrder.byteSize]), - fileName : m[entryGroupOrder.fileName].trim(), - }); - } + const entries = []; + const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm'); + let m; + while((m = entryMatchRe.exec(output))) { + entries.push({ + byteSize : parseInt(m[entryGroupOrder.byteSize]), + fileName : m[entryGroupOrder.fileName].trim(), + }); + } - return cb(null, entries); - }); - } + return cb(null, entries); + }); + } - getPtyOpts(extractPath) { - const opts = { - name : 'enigma-archiver', - cols : 80, - rows : 24, - env : process.env, - }; - if(extractPath) { - opts.cwd = extractPath; - } - // :TODO: set cwd to supplied temp path if not sepcific extract - return opts; - } + getPtyOpts(extractPath) { + const opts = { + name : 'enigma-archiver', + cols : 80, + rows : 24, + env : process.env, + }; + if(extractPath) { + opts.cwd = extractPath; + } + // :TODO: set cwd to supplied temp path if not sepcific extract + return opts; + } }; diff --git a/core/art.js b/core/art.js index dc873836..3a1949b1 100644 --- a/core/art.js +++ b/core/art.js @@ -26,87 +26,87 @@ exports.defaultEncodingFromExtension = defaultEncodingFromExtension; // :TODO: return font + font mapped information from SAUCE const SUPPORTED_ART_TYPES = { - // :TODO: the defualt encoding are really useless if they are all the same ... - // perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf - '.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a }, - '.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a }, - '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, - '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, + // :TODO: the defualt encoding are really useless if they are all the same ... + // perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf + '.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a }, + '.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a }, + '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, + '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, - '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, - '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, - // :TODO: extentions for wwiv, renegade, celerity, syncronet, ... - // :TODO: extension for atari - // :TODO: extension for topaz ansi/ascii. + '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, + '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, + // :TODO: extentions for wwiv, renegade, celerity, syncronet, ... + // :TODO: extension for atari + // :TODO: extension for topaz ansi/ascii. }; function getFontNameFromSAUCE(sauce) { - if(sauce.Character) { - return sauce.Character.fontName; - } + if(sauce.Character) { + return sauce.Character.fontName; + } } function sliceAtEOF(data, eofMarker) { - let eof = data.length; - const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) + let eof = data.length; + const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) - for(let i = eof - 1; i > stopPos; i--) { - if(eofMarker === data[i]) { - eof = i; - break; - } - } - return data.slice(0, eof); + for(let i = eof - 1; i > stopPos; i--) { + if(eofMarker === data[i]) { + eof = i; + break; + } + } + return data.slice(0, eof); } function getArtFromPath(path, options, cb) { - fs.readFile(path, (err, data) => { - if(err) { - return cb(err); - } + fs.readFile(path, (err, data) => { + if(err) { + return cb(err); + } - // - // Convert from encodedAs -> j - // - const ext = paths.extname(path).toLowerCase(); - const encoding = options.encodedAs || defaultEncodingFromExtension(ext); + // + // Convert from encodedAs -> j + // + const ext = paths.extname(path).toLowerCase(); + const encoding = options.encodedAs || defaultEncodingFromExtension(ext); - // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? + // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? - function sliceOfData() { - if(options.fullFile === true) { - return iconv.decode(data, encoding); - } else { - const eofMarker = defaultEofFromExtension(ext); - return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding); - } - } + function sliceOfData() { + if(options.fullFile === true) { + return iconv.decode(data, encoding); + } else { + const eofMarker = defaultEofFromExtension(ext); + return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding); + } + } - function getResult(sauce) { - const result = { - data : sliceOfData(), - fromPath : path, - }; + function getResult(sauce) { + const result = { + data : sliceOfData(), + fromPath : path, + }; - if(sauce) { - result.sauce = sauce; - } + if(sauce) { + result.sauce = sauce; + } - return result; - } + return result; + } - if(options.readSauce === true) { - sauce.readSAUCE(data, (err, sauce) => { - if(err) { - return cb(null, getResult()); - } + if(options.readSauce === true) { + sauce.readSAUCE(data, (err, sauce) => { + if(err) { + return cb(null, getResult()); + } - // - // If a encoding was not provided & we have a mapping from - // the information provided by SAUCE, use that. - // - if(!options.encodedAs) { - /* + // + // If a encoding was not provided & we have a mapping from + // the information provided by SAUCE, use that. + // + if(!options.encodedAs) { + /* if(sauce.Character && sauce.Character.fontName) { var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; if(enc) { @@ -114,115 +114,115 @@ function getArtFromPath(path, options, cb) { } } */ - } - return cb(null, getResult(sauce)); - }); - } else { - return cb(null, getResult()); - } - }); + } + return cb(null, getResult(sauce)); + }); + } else { + return cb(null, getResult()); + } + }); } function getArt(name, options, cb) { - const ext = paths.extname(name); + const ext = paths.extname(name); - options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); - options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); + options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); + options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); - // :TODO: make use of asAnsi option and convert from supported -> ansi + // :TODO: make use of asAnsi option and convert from supported -> ansi - if('' !== ext) { - options.types = [ ext.toLowerCase() ]; - } else { - if(_.isUndefined(options.types)) { - options.types = Object.keys(SUPPORTED_ART_TYPES); - } else if(_.isString(options.types)) { - options.types = [ options.types.toLowerCase() ]; - } - } + if('' !== ext) { + options.types = [ ext.toLowerCase() ]; + } else { + if(_.isUndefined(options.types)) { + options.types = Object.keys(SUPPORTED_ART_TYPES); + } else if(_.isString(options.types)) { + options.types = [ options.types.toLowerCase() ]; + } + } - // If an extension is provided, just read the file now - if('' !== ext) { - const directPath = paths.join(options.basePath, name); - return getArtFromPath(directPath, options, cb); - } + // If an extension is provided, just read the file now + if('' !== ext) { + const directPath = paths.join(options.basePath, name); + return getArtFromPath(directPath, options, cb); + } - fs.readdir(options.basePath, (err, files) => { - if(err) { - return cb(err); - } + fs.readdir(options.basePath, (err, files) => { + if(err) { + return cb(err); + } - const filtered = files.filter( file => { - // - // Ignore anything not allowed in |options.types| - // - const fext = paths.extname(file); - if(!options.types.includes(fext.toLowerCase())) { - return false; - } + const filtered = files.filter( file => { + // + // Ignore anything not allowed in |options.types| + // + const fext = paths.extname(file); + if(!options.types.includes(fext.toLowerCase())) { + return false; + } - const bn = paths.basename(file, fext).toLowerCase(); - if(options.random) { - const suppliedBn = paths.basename(name, fext).toLowerCase(); + const bn = paths.basename(file, fext).toLowerCase(); + if(options.random) { + const suppliedBn = paths.basename(name, fext).toLowerCase(); - // - // Random selection enabled. We'll allow for - // basename1.ext, basename2.ext, ... - // - if(!bn.startsWith(suppliedBn)) { - return false; - } + // + // Random selection enabled. We'll allow for + // basename1.ext, basename2.ext, ... + // + if(!bn.startsWith(suppliedBn)) { + return false; + } - const num = bn.substr(suppliedBn.length); - if(num.length > 0) { - if(isNaN(parseInt(num, 10))) { - return false; - } - } - } else { - // - // We've already validated the extension (above). Must be an exact - // match to basename here - // - if(bn != paths.basename(name, fext).toLowerCase()) { - return false; - } - } + const num = bn.substr(suppliedBn.length); + if(num.length > 0) { + if(isNaN(parseInt(num, 10))) { + return false; + } + } + } else { + // + // We've already validated the extension (above). Must be an exact + // match to basename here + // + if(bn != paths.basename(name, fext).toLowerCase()) { + return false; + } + } - return true; - }); + return true; + }); - if(filtered.length > 0) { - // - // We should now have: - // - Exactly (1) item in |filtered| if non-random - // - 1:n items in |filtered| to choose from if random - // - let readPath; - if(options.random) { - readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]); - } else { - assert(1 === filtered.length); - readPath = paths.join(options.basePath, filtered[0]); - } + if(filtered.length > 0) { + // + // We should now have: + // - Exactly (1) item in |filtered| if non-random + // - 1:n items in |filtered| to choose from if random + // + let readPath; + if(options.random) { + readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]); + } else { + assert(1 === filtered.length); + readPath = paths.join(options.basePath, filtered[0]); + } - return getArtFromPath(readPath, options, cb); - } + return getArtFromPath(readPath, options, cb); + } - return cb(new Error(`No matching art for supplied criteria: ${name}`)); - }); + return cb(new Error(`No matching art for supplied criteria: ${name}`)); + }); } function defaultEncodingFromExtension(ext) { - const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; - return artType ? artType.defaultEncoding : 'utf8'; + const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; + return artType ? artType.defaultEncoding : 'utf8'; } function defaultEofFromExtension(ext) { - const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; - if(artType) { - return artType.eof; - } + const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; + if(artType) { + return artType.eof; + } } // :TODO: Implement the following @@ -230,161 +230,161 @@ function defaultEofFromExtension(ext) { // * Cancel (disabled | ) // * Resume from pause -> continous (disabled | ) function display(client, art, options, cb) { - if(_.isFunction(options) && !cb) { - cb = options; - options = {}; - } + if(_.isFunction(options) && !cb) { + cb = options; + options = {}; + } - if(!art || !art.length) { - return cb(new Error('Empty art')); - } + if(!art || !art.length) { + return cb(new Error('Empty art')); + } - options.mciReplaceChar = options.mciReplaceChar || ' '; - options.disableMciCache = options.disableMciCache || false; + options.mciReplaceChar = options.mciReplaceChar || ' '; + options.disableMciCache = options.disableMciCache || false; - // :TODO: this is going to be broken into two approaches controlled via options: - // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. - // 2) CPR driven + // :TODO: this is going to be broken into two approaches controlled via options: + // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. + // 2) CPR driven - if(!_.isBoolean(options.iceColors)) { - // try to detect from SAUCE - if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { - options.iceColors = true; - } - } + if(!_.isBoolean(options.iceColors)) { + // try to detect from SAUCE + if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { + options.iceColors = true; + } + } - const ansiParser = new aep.ANSIEscapeParser({ - mciReplaceChar : options.mciReplaceChar, - termHeight : client.term.termHeight, - termWidth : client.term.termWidth, - trailingLF : options.trailingLF, - }); + const ansiParser = new aep.ANSIEscapeParser({ + mciReplaceChar : options.mciReplaceChar, + termHeight : client.term.termHeight, + termWidth : client.term.termWidth, + trailingLF : options.trailingLF, + }); - let parseComplete = false; - let cprListener; - let mciMap; - const mciCprQueue = []; - let artHash; - let mciMapFromCache; + let parseComplete = false; + let cprListener; + let mciMap; + const mciCprQueue = []; + let artHash; + let mciMapFromCache; - function completed() { - if(cprListener) { - client.removeListener('cursor position report', cprListener); - } + function completed() { + if(cprListener) { + client.removeListener('cursor position report', cprListener); + } - if(!options.disableMciCache && !mciMapFromCache) { - // cache our MCI findings... - client.mciCache[artHash] = mciMap; - client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache'); - } + if(!options.disableMciCache && !mciMapFromCache) { + // cache our MCI findings... + client.mciCache[artHash] = mciMap; + client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache'); + } - ansiParser.removeAllListeners(); // :TODO: Necessary??? + ansiParser.removeAllListeners(); // :TODO: Necessary??? - const extraInfo = { - height : ansiParser.row - 1, - }; + const extraInfo = { + height : ansiParser.row - 1, + }; - return cb(null, mciMap, extraInfo); - } + return cb(null, mciMap, extraInfo); + } - if(!options.disableMciCache) { - artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE); + if(!options.disableMciCache) { + artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE); - // see if we have a mciMap cached for this art - if(client.mciCache) { - mciMap = client.mciCache[artHash]; - } - } + // see if we have a mciMap cached for this art + if(client.mciCache) { + mciMap = client.mciCache[artHash]; + } + } - if(mciMap) { - mciMapFromCache = true; - client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache'); - } else { - // no cached MCI info - mciMap = {}; + if(mciMap) { + mciMapFromCache = true; + client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache'); + } else { + // no cached MCI info + mciMap = {}; - cprListener = function(pos) { - if(mciCprQueue.length > 0) { - mciMap[mciCprQueue.shift()].position = pos; + cprListener = function(pos) { + if(mciCprQueue.length > 0) { + mciMap[mciCprQueue.shift()].position = pos; - if(parseComplete && 0 === mciCprQueue.length) { - return completed(); - } - } - }; + if(parseComplete && 0 === mciCprQueue.length) { + return completed(); + } + } + }; - client.on('cursor position report', cprListener); + client.on('cursor position report', cprListener); - let generatedId = 100; + let generatedId = 100; - ansiParser.on('mci', mciInfo => { - // :TODO: ensure generatedId's do not conflict with any existing |id| - const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId; - const mapKey = `${mciInfo.mci}${id}`; - const mapEntry = mciMap[mapKey]; + ansiParser.on('mci', mciInfo => { + // :TODO: ensure generatedId's do not conflict with any existing |id| + const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId; + const mapKey = `${mciInfo.mci}${id}`; + const mapEntry = mciMap[mapKey]; - if(mapEntry) { - mapEntry.focusSGR = mciInfo.SGR; - mapEntry.focusArgs = mciInfo.args; - } else { - mciMap[mapKey] = { - args : mciInfo.args, - SGR : mciInfo.SGR, - code : mciInfo.mci, - id : id, - }; + if(mapEntry) { + mapEntry.focusSGR = mciInfo.SGR; + mapEntry.focusArgs = mciInfo.args; + } else { + mciMap[mapKey] = { + args : mciInfo.args, + SGR : mciInfo.SGR, + code : mciInfo.mci, + id : id, + }; - if(!mciInfo.id) { - ++generatedId; - } + if(!mciInfo.id) { + ++generatedId; + } - mciCprQueue.push(mapKey); - client.term.rawWrite(ansi.queryPos()); - } + mciCprQueue.push(mapKey); + client.term.rawWrite(ansi.queryPos()); + } - }); - } + }); + } - ansiParser.on('literal', literal => client.term.write(literal, false) ); - ansiParser.on('control', control => client.term.rawWrite(control) ); + ansiParser.on('literal', literal => client.term.write(literal, false) ); + ansiParser.on('control', control => client.term.rawWrite(control) ); - ansiParser.on('complete', () => { - parseComplete = true; + ansiParser.on('complete', () => { + parseComplete = true; - if(0 === mciCprQueue.length) { - return completed(); - } - }); + if(0 === mciCprQueue.length) { + return completed(); + } + }); - let initSeq = ''; - if(options.font) { - initSeq = ansi.setSyncTermFontWithAlias(options.font); - } else if(options.sauce) { - let fontName = getFontNameFromSAUCE(options.sauce); - if(fontName) { - fontName = ansi.getSyncTERMFontFromAlias(fontName); - } + let initSeq = ''; + if(options.font) { + initSeq = ansi.setSyncTermFontWithAlias(options.font); + } else if(options.sauce) { + let fontName = getFontNameFromSAUCE(options.sauce); + if(fontName) { + fontName = ansi.getSyncTERMFontFromAlias(fontName); + } - // - // Set SyncTERM font if we're switching only. Most terminals - // that support this ESC sequence can only show *one* font - // at a time. This applies to detection only (e.g. SAUCE). - // If explicit, we'll set it no matter what (above) - // - if(fontName && client.term.currentSyncFont != fontName) { - client.term.currentSyncFont = fontName; - initSeq = ansi.setSyncTERMFont(fontName); - } - } + // + // Set SyncTERM font if we're switching only. Most terminals + // that support this ESC sequence can only show *one* font + // at a time. This applies to detection only (e.g. SAUCE). + // If explicit, we'll set it no matter what (above) + // + if(fontName && client.term.currentSyncFont != fontName) { + client.term.currentSyncFont = fontName; + initSeq = ansi.setSyncTERMFont(fontName); + } + } - if(options.iceColors) { - initSeq += ansi.blinkToBrightIntensity(); - } + if(options.iceColors) { + initSeq += ansi.blinkToBrightIntensity(); + } - if(initSeq) { - client.term.rawWrite(initSeq); - } + if(initSeq) { + client.term.rawWrite(initSeq); + } - ansiParser.reset(art); - return ansiParser.parse(); + ansiParser.reset(art); + return ansiParser.parse(); } diff --git a/core/asset.js b/core/asset.js index 43c881c0..ece05cfc 100644 --- a/core/asset.js +++ b/core/asset.js @@ -18,111 +18,111 @@ exports.resolveSystemStatAsset = resolveSystemStatAsset; exports.getViewPropertyAsset = getViewPropertyAsset; const ALL_ASSETS = [ - 'art', - 'menu', - 'method', - 'userModule', - 'systemMethod', - 'systemModule', - 'prompt', - 'config', - 'sysStat', + 'art', + 'menu', + 'method', + 'userModule', + 'systemMethod', + 'systemModule', + 'prompt', + 'config', + 'sysStat', ]; const ASSET_RE = new RegExp('\\@(' + ALL_ASSETS.join('|') + ')\\:([\\w\\d\\.]*)(?:\\/([\\w\\d\\_]+))*'); function parseAsset(s) { - const m = ASSET_RE.exec(s); + const m = ASSET_RE.exec(s); - if(m) { - let result = { type : m[1] }; + if(m) { + let result = { type : m[1] }; - if(m[3]) { - result.location = m[2]; - result.asset = m[3]; - } else { - result.asset = m[2]; - } + if(m[3]) { + result.location = m[2]; + result.asset = m[3]; + } else { + result.asset = m[2]; + } - return result; - } + return result; + } } function getAssetWithShorthand(spec, defaultType) { - if(!_.isString(spec)) { - return null; - } + if(!_.isString(spec)) { + return null; + } - if('@' === spec[0]) { - const asset = parseAsset(spec); - assert(_.isString(asset.type)); + if('@' === spec[0]) { + const asset = parseAsset(spec); + assert(_.isString(asset.type)); - return asset; - } + return asset; + } - return { - type : defaultType, - asset : spec, - }; + return { + type : defaultType, + asset : spec, + }; } function getArtAsset(spec) { - const asset = getAssetWithShorthand(spec, 'art'); + const asset = getAssetWithShorthand(spec, 'art'); - if(!asset) { - return null; - } + if(!asset) { + return null; + } - assert( ['art', 'method' ].indexOf(asset.type) > -1); - return asset; + assert( ['art', 'method' ].indexOf(asset.type) > -1); + return asset; } function getModuleAsset(spec) { - const asset = getAssetWithShorthand(spec, 'systemModule'); + const asset = getAssetWithShorthand(spec, 'systemModule'); - if(!asset) { - return null; - } + if(!asset) { + return null; + } - assert( ['userModule', 'systemModule' ].includes(asset.type) ); + assert( ['userModule', 'systemModule' ].includes(asset.type) ); - return asset; + return asset; } function resolveConfigAsset(spec) { - const asset = parseAsset(spec); - if(asset) { - assert('config' === asset.type); + const asset = parseAsset(spec); + if(asset) { + assert('config' === asset.type); - const path = asset.asset.split('.'); - let conf = Config(); - for(let i = 0; i < path.length; ++i) { - if(_.isUndefined(conf[path[i]])) { - return spec; - } - conf = conf[path[i]]; - } - return conf; - } else { - return spec; - } + const path = asset.asset.split('.'); + let conf = Config(); + for(let i = 0; i < path.length; ++i) { + if(_.isUndefined(conf[path[i]])) { + return spec; + } + conf = conf[path[i]]; + } + return conf; + } else { + return spec; + } } function resolveSystemStatAsset(spec) { - const asset = parseAsset(spec); - if(!asset) { - return spec; - } + const asset = parseAsset(spec); + if(!asset) { + return spec; + } - assert('sysStat' === asset.type); + assert('sysStat' === asset.type); - return StatLog.getSystemStat(asset.asset) || spec; + return StatLog.getSystemStat(asset.asset) || spec; } function getViewPropertyAsset(src) { - if(!_.isString(src) || '@' !== src.charAt(0)) { - return null; - } + if(!_.isString(src) || '@' !== src.charAt(0)) { + return null; + } - return parseAsset(src); + return parseAsset(src); } diff --git a/core/bbs.js b/core/bbs.js index a352c555..597a0b97 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -41,253 +41,253 @@ valid args: `; function printHelpAndExit() { - console.info(HELP); - process.exit(); + console.info(HELP); + process.exit(); } function main() { - async.waterfall( - [ - function processArgs(callback) { - const argv = require('minimist')(process.argv.slice(2)); + async.waterfall( + [ + function processArgs(callback) { + const argv = require('minimist')(process.argv.slice(2)); - if(argv.help) { - printHelpAndExit(); - } + if(argv.help) { + printHelpAndExit(); + } - const configOverridePath = argv.config; + const configOverridePath = argv.config; - return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath)); - }, - function initConfig(configPath, configPathSupplied, callback) { - const configFile = configPath + 'config.hjson'; - conf.init(resolvePath(configFile), function configInit(err) { + return callback(null, configOverridePath || conf.getDefaultPath(), _.isString(configOverridePath)); + }, + function initConfig(configPath, configPathSupplied, callback) { + const configFile = configPath + 'config.hjson'; + conf.init(resolvePath(configFile), function configInit(err) { - // - // If the user supplied a path and we can't read/parse it - // then it's a fatal error - // - if(err) { - if('ENOENT' === err.code) { - if(configPathSupplied) { - console.error('Configuration file does not exist: ' + configFile); - } else { - configPathSupplied = null; // make non-fatal; we'll go with defaults - } - } else { - console.error(err.toString()); - } - } - callback(err); - }); - }, - function initSystem(callback) { - initialize(function init(err) { - if(err) { - console.error('Error initializing: ' + util.inspect(err)); - } - return callback(err); - }); - } - ], - function complete(err) { - // note this is escaped: - fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { - console.info(FULL_COPYRIGHT); - if(!err) { - console.info(banner); - } - console.info('System started!'); - }); + // + // If the user supplied a path and we can't read/parse it + // then it's a fatal error + // + if(err) { + if('ENOENT' === err.code) { + if(configPathSupplied) { + console.error('Configuration file does not exist: ' + configFile); + } else { + configPathSupplied = null; // make non-fatal; we'll go with defaults + } + } else { + console.error(err.toString()); + } + } + callback(err); + }); + }, + function initSystem(callback) { + initialize(function init(err) { + if(err) { + console.error('Error initializing: ' + util.inspect(err)); + } + return callback(err); + }); + } + ], + function complete(err) { + // note this is escaped: + fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { + console.info(FULL_COPYRIGHT); + if(!err) { + console.info(banner); + } + console.info('System started!'); + }); - if(err) { - console.error('Error initializing: ' + util.inspect(err)); - } - } - ); + if(err) { + console.error('Error initializing: ' + util.inspect(err)); + } + } + ); } function shutdownSystem() { - const msg = 'Process interrupted. Shutting down...'; - console.info(msg); - logger.log.info(msg); + const msg = 'Process interrupted. Shutting down...'; + console.info(msg); + logger.log.info(msg); - async.series( - [ - function closeConnections(callback) { - const ClientConns = require('./client_connections.js'); - const activeConnections = ClientConns.getActiveConnections(); - let i = activeConnections.length; - while(i--) { - const activeTerm = activeConnections[i].term; - if(activeTerm) { - activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); - } - ClientConns.removeClient(activeConnections[i]); - } - callback(null); - }, - function stopListeningServers(callback) { - return require('./listening_server.js').shutdown( () => { - return callback(null); // ignore err - }); - }, - function stopEventScheduler(callback) { - if(initServices.eventScheduler) { - return initServices.eventScheduler.shutdown( () => { - return callback(null); // ignore err - }); - } else { - return callback(null); - } - }, - function stopFileAreaWeb(callback) { - require('./file_area_web.js').startup( () => { - return callback(null); // ignore err - }); - }, - function stopMsgNetwork(callback) { - require('./msg_network.js').shutdown(callback); - } - ], - () => { - console.info('Goodbye!'); - return process.exit(); - } - ); + async.series( + [ + function closeConnections(callback) { + const ClientConns = require('./client_connections.js'); + const activeConnections = ClientConns.getActiveConnections(); + let i = activeConnections.length; + while(i--) { + const activeTerm = activeConnections[i].term; + if(activeTerm) { + activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); + } + ClientConns.removeClient(activeConnections[i]); + } + callback(null); + }, + function stopListeningServers(callback) { + return require('./listening_server.js').shutdown( () => { + return callback(null); // ignore err + }); + }, + function stopEventScheduler(callback) { + if(initServices.eventScheduler) { + return initServices.eventScheduler.shutdown( () => { + return callback(null); // ignore err + }); + } else { + return callback(null); + } + }, + function stopFileAreaWeb(callback) { + require('./file_area_web.js').startup( () => { + return callback(null); // ignore err + }); + }, + function stopMsgNetwork(callback) { + require('./msg_network.js').shutdown(callback); + } + ], + () => { + console.info('Goodbye!'); + return process.exit(); + } + ); } function initialize(cb) { - async.series( - [ - function createMissingDirectories(callback) { - async.each(Object.keys(conf.config.paths), function entry(pathKey, next) { - mkdirs(conf.config.paths[pathKey], function dirCreated(err) { - if(err) { - console.error('Could not create path: ' + conf.config.paths[pathKey] + ': ' + err.toString()); - } - return next(err); - }); - }, function dirCreationComplete(err) { - return callback(err); - }); - }, - function basicInit(callback) { - logger.init(); - logger.log.info( - { version : require('../package.json').version }, - '**** ENiGMA½ Bulletin Board System Starting Up! ****'); + async.series( + [ + function createMissingDirectories(callback) { + async.each(Object.keys(conf.config.paths), function entry(pathKey, next) { + mkdirs(conf.config.paths[pathKey], function dirCreated(err) { + if(err) { + console.error('Could not create path: ' + conf.config.paths[pathKey] + ': ' + err.toString()); + } + return next(err); + }); + }, function dirCreationComplete(err) { + return callback(err); + }); + }, + function basicInit(callback) { + logger.init(); + logger.log.info( + { version : require('../package.json').version }, + '**** ENiGMA½ Bulletin Board System Starting Up! ****'); - process.on('SIGINT', shutdownSystem); + process.on('SIGINT', shutdownSystem); - require('later').date.localTime(); // use local times for later.js/scheduling + require('later').date.localTime(); // use local times for later.js/scheduling - return callback(null); - }, - function initDatabases(callback) { - return database.initializeDatabases(callback); - }, - function initMimeTypes(callback) { - return require('./mime_util.js').startup(callback); - }, - function initStatLog(callback) { - return require('./stat_log.js').init(callback); - }, - function initConfigs(callback) { - return require('./config_util.js').init(callback); - }, - function initThemes(callback) { - // Have to pull in here so it's after Config init - require('./theme.js').initAvailableThemes( (err, themeCount) => { - logger.log.info({ themeCount }, 'Themes initialized'); - return callback(err); - }); - }, - function loadSysOpInformation(callback) { - // - // Copy over some +op information from the user DB -> system propertys. - // * Makes this accessible for MCI codes, easy non-blocking access, etc. - // * We do this every time as the op is free to change this information just - // like any other user - // - const User = require('./user.js'); + return callback(null); + }, + function initDatabases(callback) { + return database.initializeDatabases(callback); + }, + function initMimeTypes(callback) { + return require('./mime_util.js').startup(callback); + }, + function initStatLog(callback) { + return require('./stat_log.js').init(callback); + }, + function initConfigs(callback) { + return require('./config_util.js').init(callback); + }, + function initThemes(callback) { + // Have to pull in here so it's after Config init + require('./theme.js').initAvailableThemes( (err, themeCount) => { + logger.log.info({ themeCount }, 'Themes initialized'); + return callback(err); + }); + }, + function loadSysOpInformation(callback) { + // + // Copy over some +op information from the user DB -> system propertys. + // * Makes this accessible for MCI codes, easy non-blocking access, etc. + // * We do this every time as the op is free to change this information just + // like any other user + // + const User = require('./user.js'); - async.waterfall( - [ - function getOpUserName(next) { - return User.getUserName(1, next); - }, - function getOpProps(opUserName, next) { - const propLoadOpts = { - names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], - }; - User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { - return next(err, opUserName, opProps); - }); - } - ], - (err, opUserName, opProps) => { - const StatLog = require('./stat_log.js'); + async.waterfall( + [ + function getOpUserName(next) { + return User.getUserName(1, next); + }, + function getOpProps(opUserName, next) { + const propLoadOpts = { + names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], + }; + User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { + return next(err, opUserName, opProps); + }); + } + ], + (err, opUserName, opProps) => { + const StatLog = require('./stat_log.js'); - if(err) { - [ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => { - StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); - }); - } else { - opProps.username = opUserName; + if(err) { + [ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => { + StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); + }); + } else { + opProps.username = opUserName; - _.each(opProps, (v, k) => { - StatLog.setNonPeristentSystemStat(`sysop_${k}`, v); - }); - } + _.each(opProps, (v, k) => { + StatLog.setNonPeristentSystemStat(`sysop_${k}`, v); + }); + } - return callback(null); - } - ); - }, - function initFileAreaStats(callback) { - const getAreaStats = require('./file_base_area.js').getAreaStats; - getAreaStats( (err, stats) => { - if(!err) { - const StatLog = require('./stat_log.js'); - StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); - } + return callback(null); + } + ); + }, + function initFileAreaStats(callback) { + const getAreaStats = require('./file_base_area.js').getAreaStats; + getAreaStats( (err, stats) => { + if(!err) { + const StatLog = require('./stat_log.js'); + StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + } - return callback(null); - }); - }, - function initMCI(callback) { - return require('./predefined_mci.js').init(callback); - }, - function readyMessageNetworkSupport(callback) { - return require('./msg_network.js').startup(callback); - }, - function readyEvents(callback) { - return require('./events.js').startup(callback); - }, - function listenConnections(callback) { - return require('./listening_server.js').startup(callback); - }, - function readyFileBaseArea(callback) { - return require('./file_base_area.js').startup(callback); - }, - function readyFileAreaWeb(callback) { - return require('./file_area_web.js').startup(callback); - }, - function readyPasswordReset(callback) { - const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; - return WebPasswordReset.startup(callback); - }, - function readyEventScheduler(callback) { - const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; - EventSchedulerModule.loadAndStart( (err, modInst) => { - initServices.eventScheduler = modInst; - return callback(err); - }); - } - ], - function onComplete(err) { - return cb(err); - } - ); + return callback(null); + }); + }, + function initMCI(callback) { + return require('./predefined_mci.js').init(callback); + }, + function readyMessageNetworkSupport(callback) { + return require('./msg_network.js').startup(callback); + }, + function readyEvents(callback) { + return require('./events.js').startup(callback); + }, + function listenConnections(callback) { + return require('./listening_server.js').startup(callback); + }, + function readyFileBaseArea(callback) { + return require('./file_base_area.js').startup(callback); + }, + function readyFileAreaWeb(callback) { + return require('./file_area_web.js').startup(callback); + }, + function readyPasswordReset(callback) { + const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; + return WebPasswordReset.startup(callback); + }, + function readyEventScheduler(callback) { + const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; + EventSchedulerModule.loadAndStart( (err, modInst) => { + initServices.eventScheduler = modInst; + return callback(err); + }); + } + ], + function onComplete(err) { + return cb(err); + } + ); } diff --git a/core/bbs_link.js b/core/bbs_link.js index 15416c2e..4034b383 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -37,171 +37,171 @@ const packageJson = require('../package.json'); // :TODO: ENH: Support nodeMax and tooManyArt exports.moduleInfo = { - name : 'BBSLink', - desc : 'BBSLink Access Module', - author : 'NuSkooler', + name : 'BBSLink', + desc : 'BBSLink Access Module', + author : 'NuSkooler', }; exports.getModule = class BBSLinkModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'games.bbslink.net'; - this.config.port = this.config.port || 23; - } + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'games.bbslink.net'; + this.config.port = this.config.port || 23; + } - initSequence() { - let token; - let randomKey; - let clientTerminated; - const self = this; + initSequence() { + let token; + let randomKey; + let clientTerminated; + const self = this; - async.series( - [ - function validateConfig(callback) { - if(_.isString(self.config.sysCode) && + async.series( + [ + function validateConfig(callback) { + if(_.isString(self.config.sysCode) && _.isString(self.config.authCode) && _.isString(self.config.schemeCode) && _.isString(self.config.door)) - { - callback(null); - } else { - callback(new Error('Configuration is missing option(s)')); - } - }, - function acquireToken(callback) { - // - // Acquire an authentication token - // - crypto.randomBytes(16, function rand(ex, buf) { - if(ex) { - callback(ex); - } else { - randomKey = buf.toString('base64').substr(0, 6); - self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) { - if(err) { - callback(err); - } else { - token = body.trim(); - self.client.log.trace( { token : token }, 'BBSLink token'); - callback(null); - } - }); - } - }); - }, - function authenticateToken(callback) { - // - // Authenticate the token we acquired previously - // - var headers = { - 'X-User' : self.client.user.userId.toString(), - 'X-System' : self.config.sysCode, - 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), - 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), - 'X-Rows' : self.client.term.termHeight.toString(), - 'X-Key' : randomKey, - 'X-Door' : self.config.door, - 'X-Token' : token, - 'X-Type' : 'enigma-bbs', - 'X-Version' : packageJson.version, - }; + { + callback(null); + } else { + callback(new Error('Configuration is missing option(s)')); + } + }, + function acquireToken(callback) { + // + // Acquire an authentication token + // + crypto.randomBytes(16, function rand(ex, buf) { + if(ex) { + callback(ex); + } else { + randomKey = buf.toString('base64').substr(0, 6); + self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) { + if(err) { + callback(err); + } else { + token = body.trim(); + self.client.log.trace( { token : token }, 'BBSLink token'); + callback(null); + } + }); + } + }); + }, + function authenticateToken(callback) { + // + // Authenticate the token we acquired previously + // + var headers = { + 'X-User' : self.client.user.userId.toString(), + 'X-System' : self.config.sysCode, + 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), + 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), + 'X-Rows' : self.client.term.termHeight.toString(), + 'X-Key' : randomKey, + 'X-Door' : self.config.door, + 'X-Token' : token, + 'X-Type' : 'enigma-bbs', + 'X-Version' : packageJson.version, + }; - self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { - var status = body.trim(); + self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { + var status = body.trim(); - if('complete' === status) { - callback(null); - } else { - callback(new Error('Bad authentication status: ' + status)); - } - }); - }, - function createTelnetBridge(callback) { - // - // Authentication with BBSLink successful. Now, we need to create a telnet - // bridge from us to them - // - var connectOpts = { - port : self.config.port, - host : self.config.host, - }; + if('complete' === status) { + callback(null); + } else { + callback(new Error('Bad authentication status: ' + status)); + } + }); + }, + function createTelnetBridge(callback) { + // + // Authentication with BBSLink successful. Now, we need to create a telnet + // bridge from us to them + // + var connectOpts = { + port : self.config.port, + host : self.config.host, + }; - var clientTerminated; + var clientTerminated; - self.client.term.write(resetScreen()); - self.client.term.write(' Connecting to BBSLink.net, please wait...\n'); + self.client.term.write(resetScreen()); + self.client.term.write(' Connecting to BBSLink.net, please wait...\n'); - var bridgeConnection = net.createConnection(connectOpts, function connected() { - self.client.log.info(connectOpts, 'BBSLink bridge connection established'); + var bridgeConnection = net.createConnection(connectOpts, function connected() { + self.client.log.info(connectOpts, 'BBSLink bridge connection established'); - self.client.term.output.pipe(bridgeConnection); + self.client.term.output.pipe(bridgeConnection); - self.client.once('end', function clientEnd() { - self.client.log.info('Connection ended. Terminating BBSLink connection'); - clientTerminated = true; - bridgeConnection.end(); - }); - }); + self.client.once('end', function clientEnd() { + self.client.log.info('Connection ended. Terminating BBSLink connection'); + clientTerminated = true; + bridgeConnection.end(); + }); + }); - var restorePipe = function() { - self.client.term.output.unpipe(bridgeConnection); - self.client.term.output.resume(); - }; + var restorePipe = function() { + self.client.term.output.unpipe(bridgeConnection); + self.client.term.output.resume(); + }; - bridgeConnection.on('data', function incomingData(data) { - // pass along - // :TODO: just pipe this as well - self.client.term.rawWrite(data); - }); + bridgeConnection.on('data', function incomingData(data) { + // pass along + // :TODO: just pipe this as well + self.client.term.rawWrite(data); + }); - bridgeConnection.on('end', function connectionEnd() { - restorePipe(); - callback(clientTerminated ? new Error('Client connection terminated') : null); - }); + bridgeConnection.on('end', function connectionEnd() { + restorePipe(); + callback(clientTerminated ? new Error('Client connection terminated') : null); + }); - bridgeConnection.on('error', function error(err) { - self.client.log.info('BBSLink bridge connection error: ' + err.message); - restorePipe(); - callback(err); - }); - } - ], - function complete(err) { - if(err) { - self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); - } + bridgeConnection.on('error', function error(err) { + self.client.log.info('BBSLink bridge connection error: ' + err.message); + restorePipe(); + callback(err); + }); + } + ], + function complete(err) { + if(err) { + self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); + } - if(!clientTerminated) { - self.prevMenu(); - } - } - ); - } + if(!clientTerminated) { + self.prevMenu(); + } + } + ); + } - simpleHttpRequest(path, headers, cb) { - const getOpts = { - host : this.config.host, - path : path, - headers : headers, - }; + simpleHttpRequest(path, headers, cb) { + const getOpts = { + host : this.config.host, + path : path, + headers : headers, + }; - const req = http.get(getOpts, function response(resp) { - let data = ''; + const req = http.get(getOpts, function response(resp) { + let data = ''; - resp.on('data', function chunk(c) { - data += c; - }); + resp.on('data', function chunk(c) { + data += c; + }); - resp.on('end', function respEnd() { - cb(null, data); - req.end(); - }); - }); + resp.on('end', function respEnd() { + cb(null, data); + req.end(); + }); + }); - req.on('error', function reqErr(err) { - cb(err); - }); - } + req.on('error', function reqErr(err) { + cb(err); + }); + } }; diff --git a/core/bbs_list.js b/core/bbs_list.js index abff376f..b0da3dbb 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -5,8 +5,8 @@ const MenuModule = require('./menu_module.js').MenuModule; const { - getModDatabasePath, - getTransactionDatabase + getModDatabasePath, + getTransactionDatabase } = require('./database.js'); const ViewController = require('./view_controller.js').ViewController; @@ -23,397 +23,397 @@ const _ = require('lodash'); // :TODO: add notes field const moduleInfo = exports.moduleInfo = { - name : 'BBS List', - desc : 'List of other BBSes', - author : 'Andrew Pamment', - packageName : 'com.magickabbs.enigma.bbslist' + name : 'BBS List', + desc : 'List of other BBSes', + author : 'Andrew Pamment', + packageName : 'com.magickabbs.enigma.bbslist' }; const MciViewIds = { - view : { - BBSList : 1, - SelectedBBSName : 2, - SelectedBBSSysOp : 3, - SelectedBBSTelnet : 4, - SelectedBBSWww : 5, - SelectedBBSLoc : 6, - SelectedBBSSoftware : 7, - SelectedBBSNotes : 8, - SelectedBBSSubmitter : 9, - }, - add : { - BBSName : 1, - Sysop : 2, - Telnet : 3, - Www : 4, - Location : 5, - Software : 6, - Notes : 7, - Error : 8, - } + view : { + BBSList : 1, + SelectedBBSName : 2, + SelectedBBSSysOp : 3, + SelectedBBSTelnet : 4, + SelectedBBSWww : 5, + SelectedBBSLoc : 6, + SelectedBBSSoftware : 7, + SelectedBBSNotes : 8, + SelectedBBSSubmitter : 9, + }, + add : { + BBSName : 1, + Sysop : 2, + Telnet : 3, + Www : 4, + Location : 5, + Software : 6, + Notes : 7, + Error : 8, + } }; const FormIds = { - View : 0, - Add : 1, + View : 0, + Add : 1, }; const SELECTED_MCI_NAME_TO_ENTRY = { - SelectedBBSName : 'bbsName', - SelectedBBSSysOp : 'sysOp', - SelectedBBSTelnet : 'telnet', - SelectedBBSWww : 'www', - SelectedBBSLoc : 'location', - SelectedBBSSoftware : 'software', - SelectedBBSSubmitter : 'submitter', - SelectedBBSSubmitterId : 'submitterUserId', - SelectedBBSNotes : 'notes', + SelectedBBSName : 'bbsName', + SelectedBBSSysOp : 'sysOp', + SelectedBBSTelnet : 'telnet', + SelectedBBSWww : 'www', + SelectedBBSLoc : 'location', + SelectedBBSSoftware : 'software', + SelectedBBSSubmitter : 'submitter', + SelectedBBSSubmitterId : 'submitterUserId', + SelectedBBSNotes : 'notes', }; exports.getModule = class BBSListModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - const self = this; - this.menuMethods = { - // - // Validators - // - viewValidationListener : function(err, cb) { - const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); - if(errMsgView) { - if(err) { - errMsgView.setText(err.message); - } else { - errMsgView.clearText(); - } - } + const self = this; + this.menuMethods = { + // + // Validators + // + viewValidationListener : function(err, cb) { + const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); + if(errMsgView) { + if(err) { + errMsgView.setText(err.message); + } else { + errMsgView.clearText(); + } + } - return cb(null); - }, + return cb(null); + }, - // - // Key & submit handlers - // - addBBS : function(formData, extraArgs, cb) { - self.displayAddScreen(cb); - }, - deleteBBS : function(formData, extraArgs, cb) { - if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) { - return cb(null); - } + // + // Key & submit handlers + // + addBBS : function(formData, extraArgs, cb) { + self.displayAddScreen(cb); + }, + deleteBBS : function(formData, extraArgs, cb) { + if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) { + return cb(null); + } - const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); + const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); - if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { - // must be owner or +op - return cb(null); - } + if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { + // must be owner or +op + return cb(null); + } - const entry = self.entries[self.selectedBBS]; - if(!entry) { - return cb(null); - } + const entry = self.entries[self.selectedBBS]; + if(!entry) { + return cb(null); + } - self.database.run( - `DELETE FROM bbs_list + self.database.run( + `DELETE FROM bbs_list WHERE id=?;`, - [ entry.id ], - err => { - if (err) { - self.client.log.error( { err : err }, 'Error deleting from BBS list'); - } else { - self.entries.splice(self.selectedBBS, 1); + [ entry.id ], + err => { + if (err) { + self.client.log.error( { err : err }, 'Error deleting from BBS list'); + } else { + self.entries.splice(self.selectedBBS, 1); - self.setEntries(entriesView); + self.setEntries(entriesView); - if(self.entries.length > 0) { - entriesView.focusPrevious(); - } + if(self.entries.length > 0) { + entriesView.focusPrevious(); + } - self.viewControllers.view.redrawAll(); - } + self.viewControllers.view.redrawAll(); + } - return cb(null); - } - ); - }, - submitBBS : function(formData, extraArgs, cb) { + return cb(null); + } + ); + }, + submitBBS : function(formData, extraArgs, cb) { - let ok = true; - [ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => { - if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) { - ok = false; - } - }); - if(!ok) { - // validators should prevent this! - return cb(null); - } + let ok = true; + [ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => { + if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) { + ok = false; + } + }); + if(!ok) { + // validators should prevent this! + return cb(null); + } - self.database.run( - `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) + self.database.run( + `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, - [ - formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, - formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes - ], - err => { - if(err) { - self.client.log.error( { err : err }, 'Error adding to BBS list'); - } + [ + formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, + formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes + ], + err => { + if(err) { + self.client.log.error( { err : err }, 'Error adding to BBS list'); + } - self.clearAddForm(); - self.displayBBSList(true, cb); - } - ); - }, - cancelSubmit : function(formData, extraArgs, cb) { - self.clearAddForm(); - self.displayBBSList(true, cb); - } - }; - } + self.clearAddForm(); + self.displayBBSList(true, cb); + } + ); + }, + cancelSubmit : function(formData, extraArgs, cb) { + self.clearAddForm(); + self.displayBBSList(true, cb); + } + }; + } - initSequence() { - const self = this; - async.series( - [ - function beforeDisplayArt(callback) { - self.beforeArt(callback); - }, - function display(callback) { - self.displayBBSList(false, callback); - } - ], - err => { - if(err) { - // :TODO: Handle me -- initSequence() should really take a completion callback - } - self.finishedLoading(); - } - ); - } + initSequence() { + const self = this; + async.series( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function display(callback) { + self.displayBBSList(false, callback); + } + ], + err => { + if(err) { + // :TODO: Handle me -- initSequence() should really take a completion callback + } + self.finishedLoading(); + } + ); + } - drawSelectedEntry(entry) { - if(!entry) { - Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { - this.setViewText('view', MciViewIds.view[mciName], ''); - }); - } else { - const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)'; + drawSelectedEntry(entry) { + if(!entry) { + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { + this.setViewText('view', MciViewIds.view[mciName], ''); + }); + } else { + const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)'; - Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { - const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]]; - if(MciViewIds.view[mciName]) { + Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { + const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]]; + if(MciViewIds.view[mciName]) { - if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) { - this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry)); - } else { - this.setViewText('view',MciViewIds.view[mciName], t); - } - } - }); - } - } + if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) { + this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry)); + } else { + this.setViewText('view',MciViewIds.view[mciName], t); + } + } + }); + } + } - setEntries(entriesView) { - const config = this.menuConfig.config; - const listFormat = config.listFormat || '{bbsName}'; - const focusListFormat = config.focusListFormat || '{bbsName}'; + setEntries(entriesView) { + const config = this.menuConfig.config; + const listFormat = config.listFormat || '{bbsName}'; + const focusListFormat = config.focusListFormat || '{bbsName}'; - entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) ); - entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) ); - } + entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) ); + entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) ); + } - displayBBSList(clearScreen, cb) { - const self = this; + displayBBSList(clearScreen, cb) { + const self = this; - async.waterfall( - [ - function clearAndDisplayArt(callback) { - if(self.viewControllers.add) { - self.viewControllers.add.setFocus(false); - } - if (clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } - theme.displayThemedAsset( - self.menuConfig.config.art.entries, - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'view', - new ViewController( { client : self.client, formId : FormIds.View } ) - ); + async.waterfall( + [ + function clearAndDisplayArt(callback) { + if(self.viewControllers.add) { + self.viewControllers.add.setFocus(false); + } + if (clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } + theme.displayThemedAsset( + self.menuConfig.config.art.entries, + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'view', + new ViewController( { client : self.client, formId : FormIds.View } ) + ); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw(); - return callback(null); - } - }, - function fetchEntries(callback) { - const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); - self.entries = []; + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.view.setFocus(true); + self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw(); + return callback(null); + } + }, + function fetchEntries(callback) { + const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); + self.entries = []; - self.database.each( - `SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes + self.database.each( + `SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes FROM bbs_list;`, - (err, row) => { - if (!err) { - self.entries.push({ - id : row.id, - bbsName : row.bbs_name, - sysOp : row.sysop, - telnet : row.telnet, - www : row.www, - location : row.location, - software : row.software, - submitterUserId : row.submitter_user_id, - notes : row.notes, - }); - } - }, - err => { - return callback(err, entriesView); - } - ); - }, - function getUserNames(entriesView, callback) { - async.each(self.entries, (entry, next) => { - User.getUserName(entry.submitterUserId, (err, username) => { - if(username) { - entry.submitter = username; - } else { - entry.submitter = 'N/A'; - } - return next(); - }); - }, () => { - return callback(null, entriesView); - }); - }, - function populateEntries(entriesView, callback) { - self.setEntries(entriesView); + (err, row) => { + if (!err) { + self.entries.push({ + id : row.id, + bbsName : row.bbs_name, + sysOp : row.sysop, + telnet : row.telnet, + www : row.www, + location : row.location, + software : row.software, + submitterUserId : row.submitter_user_id, + notes : row.notes, + }); + } + }, + err => { + return callback(err, entriesView); + } + ); + }, + function getUserNames(entriesView, callback) { + async.each(self.entries, (entry, next) => { + User.getUserName(entry.submitterUserId, (err, username) => { + if(username) { + entry.submitter = username; + } else { + entry.submitter = 'N/A'; + } + return next(); + }); + }, () => { + return callback(null, entriesView); + }); + }, + function populateEntries(entriesView, callback) { + self.setEntries(entriesView); - entriesView.on('index update', idx => { - const entry = self.entries[idx]; + entriesView.on('index update', idx => { + const entry = self.entries[idx]; - self.drawSelectedEntry(entry); + self.drawSelectedEntry(entry); - if(!entry) { - self.selectedBBS = -1; - } else { - self.selectedBBS = idx; - } - }); + if(!entry) { + self.selectedBBS = -1; + } else { + self.selectedBBS = idx; + } + }); - if (self.selectedBBS >= 0) { - entriesView.setFocusItemIndex(self.selectedBBS); - self.drawSelectedEntry(self.entries[self.selectedBBS]); - } else if (self.entries.length > 0) { - self.selectedBBS = 0; - entriesView.setFocusItemIndex(0); - self.drawSelectedEntry(self.entries[0]); - } + if (self.selectedBBS >= 0) { + entriesView.setFocusItemIndex(self.selectedBBS); + self.drawSelectedEntry(self.entries[self.selectedBBS]); + } else if (self.entries.length > 0) { + self.selectedBBS = 0; + entriesView.setFocusItemIndex(0); + self.drawSelectedEntry(self.entries[0]); + } - entriesView.redraw(); + entriesView.redraw(); - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - displayAddScreen(cb) { - const self = this; + displayAddScreen(cb) { + const self = this; - async.waterfall( - [ - function clearAndDisplayArt(callback) { - self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(ansi.resetScreen()); + async.waterfall( + [ + function clearAndDisplayArt(callback) { + self.viewControllers.view.setFocus(false); + self.client.term.rawWrite(ansi.resetScreen()); - theme.displayThemedAsset( - self.menuConfig.config.art.add, - self.client, - { font : self.menuConfig.font }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'add', - new ViewController( { client : self.client, formId : FormIds.Add } ) - ); + theme.displayThemedAsset( + self.menuConfig.config.art.add, + self.client, + { font : self.menuConfig.font }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'add', + new ViewController( { client : self.client, formId : FormIds.Add } ) + ); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.add.setFocus(true); - self.viewControllers.add.redrawAll(); - self.viewControllers.add.switchFocus(MciViewIds.add.BBSName); - return callback(null); - } - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.add.setFocus(true); + self.viewControllers.add.redrawAll(); + self.viewControllers.add.switchFocus(MciViewIds.add.BBSName); + return callback(null); + } + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - clearAddForm() { - [ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => { - this.setViewText('add', MciViewIds.add[mciName], ''); - }); - } + clearAddForm() { + [ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => { + this.setViewText('add', MciViewIds.add[mciName], ''); + }); + } - initDatabase(cb) { - const self = this; + initDatabase(cb) { + const self = this; - async.series( - [ - function openDatabase(callback) { - self.database = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(moduleInfo), - callback - )); - }, - function createTables(callback) { - self.database.serialize( () => { - self.database.run( - `CREATE TABLE IF NOT EXISTS bbs_list ( + async.series( + [ + function openDatabase(callback) { + self.database = getTransactionDatabase(new sqlite3.Database( + getModDatabasePath(moduleInfo), + callback + )); + }, + function createTables(callback) { + self.database.serialize( () => { + self.database.run( + `CREATE TABLE IF NOT EXISTS bbs_list ( id INTEGER PRIMARY KEY, bbs_name VARCHAR NOT NULL, sysop VARCHAR NOT NULL, @@ -424,20 +424,20 @@ exports.getModule = class BBSListModule extends MenuModule { submitter_user_id INTEGER NOT NULL, notes VARCHAR );` - ); - }); - callback(null); - } - ], - err => { - return cb(err); - } - ); - } + ); + }); + callback(null); + } + ], + err => { + return cb(err); + } + ); + } - beforeArt(cb) { - super.beforeArt(err => { - return err ? cb(err) : this.initDatabase(cb); - }); - } + beforeArt(cb) { + super.beforeArt(err => { + return err ? cb(err) : this.initDatabase(cb); + }); + } }; diff --git a/core/button_view.js b/core/button_view.js index d5b858c7..6c86b5c3 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -8,24 +8,24 @@ const util = require('util'); exports.ButtonView = ButtonView; function ButtonView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.justify = miscUtil.valueWithDefault(options.justify, 'center'); - options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.justify = miscUtil.valueWithDefault(options.justify, 'center'); + options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); - TextView.call(this, options); + TextView.call(this, options); } util.inherits(ButtonView, TextView); ButtonView.prototype.onKeyPress = function(ch, key) { - if(this.isKeyMapped('accept', key.name) || ' ' === ch) { - this.submitData = 'accept'; - this.emit('action', 'accept'); - delete this.submitData; - } else { - ButtonView.super_.prototype.onKeyPress.call(this, ch, key); - } + if(this.isKeyMapped('accept', key.name) || ' ' === ch) { + this.submitData = 'accept'; + this.emit('action', 'accept'); + delete this.submitData; + } else { + ButtonView.super_.prototype.onKeyPress.call(this, ch, key); + } }; /* ButtonView.prototype.onKeyPress = function(ch, key) { @@ -39,5 +39,5 @@ ButtonView.prototype.onKeyPress = function(ch, key) { */ ButtonView.prototype.getData = function() { - return this.submitData || null; + return this.submitData || null; }; diff --git a/core/client.js b/core/client.js index 10409c70..4347c0b2 100644 --- a/core/client.js +++ b/core/client.js @@ -58,442 +58,442 @@ const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ - '(\\d+)(?:;(\\d+))?([~^$])', - '(?:M([@ #!a`])(.)(.))', // mouse stuff - '(?:1;)?(\\d+)?([a-zA-Z@])' + '(\\d+)(?:;(\\d+))?([~^$])', + '(?:M([@ #!a`])(.)(.))', // mouse stuff + '(?:1;)?(\\d+)?([a-zA-Z@])' ].join('|') + ')'); const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); const RE_ESC_CODE_ANYWHERE = new RegExp( [ - RE_FUNCTION_KEYCODE_ANYWHERE.source, - RE_META_KEYCODE_ANYWHERE.source, - RE_DSR_RESPONSE_ANYWHERE.source, - RE_DEV_ATTR_RESPONSE_ANYWHERE.source, - /\u001b./.source + RE_FUNCTION_KEYCODE_ANYWHERE.source, + RE_META_KEYCODE_ANYWHERE.source, + RE_DSR_RESPONSE_ANYWHERE.source, + RE_DEV_ATTR_RESPONSE_ANYWHERE.source, + /\u001b./.source ].join('|')); function Client(/*input, output*/) { - stream.call(this); + stream.call(this); - const self = this; + const self = this; - this.user = new User(); - this.currentTheme = { info : { name : 'N/A', description : 'None' } }; - this.lastKeyPressMs = Date.now(); - this.menuStack = new MenuStack(this); - this.acs = new ACS(this); - this.mciCache = {}; + this.user = new User(); + this.currentTheme = { info : { name : 'N/A', description : 'None' } }; + this.lastKeyPressMs = Date.now(); + this.menuStack = new MenuStack(this); + this.acs = new ACS(this); + this.mciCache = {}; - this.clearMciCache = function() { - this.mciCache = {}; - }; + this.clearMciCache = function() { + this.mciCache = {}; + }; - Object.defineProperty(this, 'node', { - get : function() { - return self.session.id + 1; - } - }); + Object.defineProperty(this, 'node', { + get : function() { + return self.session.id + 1; + } + }); - Object.defineProperty(this, 'currentMenuModule', { - get : function() { - return self.menuStack.currentModule; - } - }); + Object.defineProperty(this, 'currentMenuModule', { + get : function() { + return self.menuStack.currentModule; + } + }); - this.setTemporaryDirectDataHandler = function(handler) { - this.input.removeAllListeners('data'); - this.input.on('data', handler); - }; + this.setTemporaryDirectDataHandler = function(handler) { + this.input.removeAllListeners('data'); + this.input.on('data', handler); + }; - this.restoreDataHandler = function() { - this.input.removeAllListeners('data'); - this.input.on('data', this.dataHandler); - }; + this.restoreDataHandler = function() { + this.input.removeAllListeners('data'); + this.input.on('data', this.dataHandler); + }; - Events.on(Events.getSystemEvents().ThemeChanged, ( { themeId } ) => { - if(_.get(this.currentTheme, 'info.themeId') === themeId) { - this.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); - } - }); + Events.on(Events.getSystemEvents().ThemeChanged, ( { themeId } ) => { + if(_.get(this.currentTheme, 'info.themeId') === themeId) { + this.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); + } + }); - // - // Peek at incoming |data| and emit events for any special - // handling that may include: - // * Keyboard input - // * ANSI CSR's and the like - // - // References: - // * http://www.ansi-bbs.org/ansi-bbs-core-server.html - // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ - // - this.getTermClient = function(deviceAttr) { - let termClient = { - // - // See http://www.fbl.cz/arctel/download/techman.pdf - // - // Known clients: - // * Irssi ConnectBot (Android) - // - '63;1;2' : 'arctel', - '50;86;84;88' : 'vtx', - }[deviceAttr]; + // + // Peek at incoming |data| and emit events for any special + // handling that may include: + // * Keyboard input + // * ANSI CSR's and the like + // + // References: + // * http://www.ansi-bbs.org/ansi-bbs-core-server.html + // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ + // + this.getTermClient = function(deviceAttr) { + let termClient = { + // + // See http://www.fbl.cz/arctel/download/techman.pdf + // + // Known clients: + // * Irssi ConnectBot (Android) + // + '63;1;2' : 'arctel', + '50;86;84;88' : 'vtx', + }[deviceAttr]; - if(!termClient) { - if(_.startsWith(deviceAttr, '67;84;101;114;109')) { - // - // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt - // - // Known clients: - // * SyncTERM - // - termClient = 'cterm'; - } - } + if(!termClient) { + if(_.startsWith(deviceAttr, '67;84;101;114;109')) { + // + // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt + // + // Known clients: + // * SyncTERM + // + termClient = 'cterm'; + } + } - return termClient; - }; + return termClient; + }; - this.isMouseInput = function(data) { - return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex + this.isMouseInput = function(data) { + return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex /\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || /\u001b\[(O|I)/.test(data); - }; + }; - this.getKeyComponentsFromCode = function(code) { - return { - // xterm/gnome - 'OP' : { name : 'f1' }, - 'OQ' : { name : 'f2' }, - 'OR' : { name : 'f3' }, - 'OS' : { name : 'f4' }, + this.getKeyComponentsFromCode = function(code) { + return { + // xterm/gnome + 'OP' : { name : 'f1' }, + 'OQ' : { name : 'f2' }, + 'OR' : { name : 'f3' }, + 'OS' : { name : 'f4' }, - 'OA' : { name : 'up arrow' }, - 'OB' : { name : 'down arrow' }, - 'OC' : { name : 'right arrow' }, - 'OD' : { name : 'left arrow' }, - 'OE' : { name : 'clear' }, - 'OF' : { name : 'end' }, - 'OH' : { name : 'home' }, + 'OA' : { name : 'up arrow' }, + 'OB' : { name : 'down arrow' }, + 'OC' : { name : 'right arrow' }, + 'OD' : { name : 'left arrow' }, + 'OE' : { name : 'clear' }, + 'OF' : { name : 'end' }, + 'OH' : { name : 'home' }, - // xterm/rxvt - '[11~' : { name : 'f1' }, - '[12~' : { name : 'f2' }, - '[13~' : { name : 'f3' }, - '[14~' : { name : 'f4' }, + // xterm/rxvt + '[11~' : { name : 'f1' }, + '[12~' : { name : 'f2' }, + '[13~' : { name : 'f3' }, + '[14~' : { name : 'f4' }, - '[1~' : { name : 'home' }, - '[2~' : { name : 'insert' }, - '[3~' : { name : 'delete' }, - '[4~' : { name : 'end' }, - '[5~' : { name : 'page up' }, - '[6~' : { name : 'page down' }, + '[1~' : { name : 'home' }, + '[2~' : { name : 'insert' }, + '[3~' : { name : 'delete' }, + '[4~' : { name : 'end' }, + '[5~' : { name : 'page up' }, + '[6~' : { name : 'page down' }, - // Cygwin & libuv - '[[A' : { name : 'f1' }, - '[[B' : { name : 'f2' }, - '[[C' : { name : 'f3' }, - '[[D' : { name : 'f4' }, - '[[E' : { name : 'f5' }, + // Cygwin & libuv + '[[A' : { name : 'f1' }, + '[[B' : { name : 'f2' }, + '[[C' : { name : 'f3' }, + '[[D' : { name : 'f4' }, + '[[E' : { name : 'f5' }, - // Common impls - '[15~' : { name : 'f5' }, - '[17~' : { name : 'f6' }, - '[18~' : { name : 'f7' }, - '[19~' : { name : 'f8' }, - '[20~' : { name : 'f9' }, - '[21~' : { name : 'f10' }, - '[23~' : { name : 'f11' }, - '[24~' : { name : 'f12' }, + // Common impls + '[15~' : { name : 'f5' }, + '[17~' : { name : 'f6' }, + '[18~' : { name : 'f7' }, + '[19~' : { name : 'f8' }, + '[20~' : { name : 'f9' }, + '[21~' : { name : 'f10' }, + '[23~' : { name : 'f11' }, + '[24~' : { name : 'f12' }, - // xterm - '[A' : { name : 'up arrow' }, - '[B' : { name : 'down arrow' }, - '[C' : { name : 'right arrow' }, - '[D' : { name : 'left arrow' }, - '[E' : { name : 'clear' }, - '[F' : { name : 'end' }, - '[H' : { name : 'home' }, + // xterm + '[A' : { name : 'up arrow' }, + '[B' : { name : 'down arrow' }, + '[C' : { name : 'right arrow' }, + '[D' : { name : 'left arrow' }, + '[E' : { name : 'clear' }, + '[F' : { name : 'end' }, + '[H' : { name : 'home' }, - // PuTTY - '[[5~' : { name : 'page up' }, - '[[6~' : { name : 'page down' }, + // PuTTY + '[[5~' : { name : 'page up' }, + '[[6~' : { name : 'page down' }, - // rvxt - '[7~' : { name : 'home' }, - '[8~' : { name : 'end' }, + // rvxt + '[7~' : { name : 'home' }, + '[8~' : { name : 'end' }, - // rxvt with modifiers - '[a' : { name : 'up arrow', shift : true }, - '[b' : { name : 'down arrow', shift : true }, - '[c' : { name : 'right arrow', shift : true }, - '[d' : { name : 'left arrow', shift : true }, - '[e' : { name : 'clear', shift : true }, + // rxvt with modifiers + '[a' : { name : 'up arrow', shift : true }, + '[b' : { name : 'down arrow', shift : true }, + '[c' : { name : 'right arrow', shift : true }, + '[d' : { name : 'left arrow', shift : true }, + '[e' : { name : 'clear', shift : true }, - '[2$' : { name : 'insert', shift : true }, - '[3$' : { name : 'delete', shift : true }, - '[5$' : { name : 'page up', shift : true }, - '[6$' : { name : 'page down', shift : true }, - '[7$' : { name : 'home', shift : true }, - '[8$' : { name : 'end', shift : true }, + '[2$' : { name : 'insert', shift : true }, + '[3$' : { name : 'delete', shift : true }, + '[5$' : { name : 'page up', shift : true }, + '[6$' : { name : 'page down', shift : true }, + '[7$' : { name : 'home', shift : true }, + '[8$' : { name : 'end', shift : true }, - 'Oa' : { name : 'up arrow', ctrl : true }, - 'Ob' : { name : 'down arrow', ctrl : true }, - 'Oc' : { name : 'right arrow', ctrl : true }, - 'Od' : { name : 'left arrow', ctrl : true }, - 'Oe' : { name : 'clear', ctrl : true }, + 'Oa' : { name : 'up arrow', ctrl : true }, + 'Ob' : { name : 'down arrow', ctrl : true }, + 'Oc' : { name : 'right arrow', ctrl : true }, + 'Od' : { name : 'left arrow', ctrl : true }, + 'Oe' : { name : 'clear', ctrl : true }, - '[2^' : { name : 'insert', ctrl : true }, - '[3^' : { name : 'delete', ctrl : true }, - '[5^' : { name : 'page up', ctrl : true }, - '[6^' : { name : 'page down', ctrl : true }, - '[7^' : { name : 'home', ctrl : true }, - '[8^' : { name : 'end', ctrl : true }, + '[2^' : { name : 'insert', ctrl : true }, + '[3^' : { name : 'delete', ctrl : true }, + '[5^' : { name : 'page up', ctrl : true }, + '[6^' : { name : 'page down', ctrl : true }, + '[7^' : { name : 'home', ctrl : true }, + '[8^' : { name : 'end', ctrl : true }, - // SyncTERM / EtherTerm - '[K' : { name : 'end' }, - '[@' : { name : 'insert' }, - '[V' : { name : 'page up' }, - '[U' : { name : 'page down' }, + // SyncTERM / EtherTerm + '[K' : { name : 'end' }, + '[@' : { name : 'insert' }, + '[V' : { name : 'page up' }, + '[U' : { name : 'page down' }, - // other - '[Z' : { name : 'tab', shift : true }, - }[code]; - }; + // other + '[Z' : { name : 'tab', shift : true }, + }[code]; + }; - this.on('data', function clientData(data) { - // create a uniform format that can be parsed below - if(data[0] > 127 && undefined === data[1]) { - data[0] -= 128; - data = '\u001b' + data.toString('utf-8'); - } else { - data = data.toString('utf-8'); - } + this.on('data', function clientData(data) { + // create a uniform format that can be parsed below + if(data[0] > 127 && undefined === data[1]) { + data[0] -= 128; + data = '\u001b' + data.toString('utf-8'); + } else { + data = data.toString('utf-8'); + } - if(self.isMouseInput(data)) { - return; - } + if(self.isMouseInput(data)) { + return; + } - var buf = []; - var m; - while((m = RE_ESC_CODE_ANYWHERE.exec(data))) { - buf = buf.concat(data.slice(0, m.index).split('')); - buf.push(m[0]); - data = data.slice(m.index + m[0].length); - } + var buf = []; + var m; + while((m = RE_ESC_CODE_ANYWHERE.exec(data))) { + buf = buf.concat(data.slice(0, m.index).split('')); + buf.push(m[0]); + data = data.slice(m.index + m[0].length); + } - buf = buf.concat(data.split('')); // remainder + buf = buf.concat(data.split('')); // remainder - buf.forEach(function bufPart(s) { - var key = { - seq : s, - name : undefined, - ctrl : false, - meta : false, - shift : false, - }; + buf.forEach(function bufPart(s) { + var key = { + seq : s, + name : undefined, + ctrl : false, + meta : false, + shift : false, + }; - var parts; + var parts; - if((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) { - if('R' === parts[2]) { - const cprArgs = parts[1].split(';').map(v => (parseInt(v, 10) || 0) ); - if(2 === cprArgs.length) { - if(self.cprOffset) { - cprArgs[0] = cprArgs[0] + self.cprOffset; - cprArgs[1] = cprArgs[1] + self.cprOffset; - } - self.emit('cursor position report', cprArgs); - } - } - } else if((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) { - assert('c' === parts[2]); - var termClient = self.getTermClient(parts[1]); - if(termClient) { - self.term.termClient = termClient; - } - } else if('\r' === s) { - key.name = 'return'; - } else if('\n' === s) { - key.name = 'line feed'; - } else if('\t' === s) { - key.name = 'tab'; - } else if('\x7f' === s) { - // - // Backspace vs delete is a crazy thing, especially in *nix. - // - ANSI-BBS uses 0x7f for DEL - // - xterm et. al clients send 0x7f for backspace... ugg. - // - // See http://www.hypexr.org/linux_ruboff.php - // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html - // - if(self.term.isNixTerm()) { - key.name = 'backspace'; - } else { - key.name = 'delete'; - } - } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { - // backspace, CTRL-H - key.name = 'backspace'; - key.meta = ('\x1b' === s.charAt(0)); - } else if('\x1b' === s || '\x1b\x1b' === s) { - key.name = 'escape'; - key.meta = (2 === s.length); - } else if (' ' === s || '\x1b ' === s) { - // rather annoying that space can come in other than just " " - key.name = 'space'; - key.meta = (2 === s.length); - } else if(1 === s.length && s <= '\x1a') { - // CTRL- - key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); - key.ctrl = true; - } else if(1 === s.length && s >= 'a' && s <= 'z') { - // normal, lowercased letter - key.name = s; - } else if(1 === s.length && s >= 'A' && s <= 'Z') { - key.name = s.toLowerCase(); - key.shift = true; - } else if ((parts = RE_META_KEYCODE.exec(s))) { - // meta with character key - key.name = parts[1].toLowerCase(); - key.meta = true; - key.shift = /^[A-Z]$/.test(parts[1]); - } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { - var code = + if((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) { + if('R' === parts[2]) { + const cprArgs = parts[1].split(';').map(v => (parseInt(v, 10) || 0) ); + if(2 === cprArgs.length) { + if(self.cprOffset) { + cprArgs[0] = cprArgs[0] + self.cprOffset; + cprArgs[1] = cprArgs[1] + self.cprOffset; + } + self.emit('cursor position report', cprArgs); + } + } + } else if((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) { + assert('c' === parts[2]); + var termClient = self.getTermClient(parts[1]); + if(termClient) { + self.term.termClient = termClient; + } + } else if('\r' === s) { + key.name = 'return'; + } else if('\n' === s) { + key.name = 'line feed'; + } else if('\t' === s) { + key.name = 'tab'; + } else if('\x7f' === s) { + // + // Backspace vs delete is a crazy thing, especially in *nix. + // - ANSI-BBS uses 0x7f for DEL + // - xterm et. al clients send 0x7f for backspace... ugg. + // + // See http://www.hypexr.org/linux_ruboff.php + // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html + // + if(self.term.isNixTerm()) { + key.name = 'backspace'; + } else { + key.name = 'delete'; + } + } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { + // backspace, CTRL-H + key.name = 'backspace'; + key.meta = ('\x1b' === s.charAt(0)); + } else if('\x1b' === s || '\x1b\x1b' === s) { + key.name = 'escape'; + key.meta = (2 === s.length); + } else if (' ' === s || '\x1b ' === s) { + // rather annoying that space can come in other than just " " + key.name = 'space'; + key.meta = (2 === s.length); + } else if(1 === s.length && s <= '\x1a') { + // CTRL- + key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); + key.ctrl = true; + } else if(1 === s.length && s >= 'a' && s <= 'z') { + // normal, lowercased letter + key.name = s; + } else if(1 === s.length && s >= 'A' && s <= 'Z') { + key.name = s.toLowerCase(); + key.shift = true; + } else if ((parts = RE_META_KEYCODE.exec(s))) { + // meta with character key + key.name = parts[1].toLowerCase(); + key.meta = true; + key.shift = /^[A-Z]$/.test(parts[1]); + } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { + var code = (parts[1] || '') + (parts[2] || '') + (parts[4] || '') + (parts[9] || ''); - var modifier = (parts[3] || parts[8] || 1) - 1; + var modifier = (parts[3] || parts[8] || 1) - 1; - key.ctrl = !!(modifier & 4); - key.meta = !!(modifier & 10); - key.shift = !!(modifier & 1); - key.code = code; + key.ctrl = !!(modifier & 4); + key.meta = !!(modifier & 10); + key.shift = !!(modifier & 1); + key.code = code; - _.assign(key, self.getKeyComponentsFromCode(code)); - } + _.assign(key, self.getKeyComponentsFromCode(code)); + } - var ch; - if(1 === s.length) { - ch = s; - } else if('space' === key.name) { - // stupid hack to always get space as a regular char - ch = ' '; - } + var ch; + if(1 === s.length) { + ch = s; + } else if('space' === key.name) { + // stupid hack to always get space as a regular char + ch = ' '; + } - if(_.isUndefined(key.name)) { - key = undefined; - } else { - // - // Adjust name for CTRL/Shift/Meta modifiers - // - key.name = + if(_.isUndefined(key.name)) { + key = undefined; + } else { + // + // Adjust name for CTRL/Shift/Meta modifiers + // + key.name = (key.ctrl ? 'ctrl + ' : '') + (key.meta ? 'meta + ' : '') + (key.shift ? 'shift + ' : '') + key.name; - } + } - if(key || ch) { - if(Config().logging.traceUserKeyboardInput) { - self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line - } + if(key || ch) { + if(Config().logging.traceUserKeyboardInput) { + self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line + } - self.lastKeyPressMs = Date.now(); + self.lastKeyPressMs = Date.now(); - if(!self.ignoreInput) { - self.emit('key press', ch, key); - } - } - }); - }); + if(!self.ignoreInput) { + self.emit('key press', ch, key); + } + } + }); + }); } require('util').inherits(Client, stream); Client.prototype.setInputOutput = function(input, output) { - this.input = input; - this.output = output; + this.input = input; + this.output = output; - this.term = new term.ClientTerminal(this.output); + this.term = new term.ClientTerminal(this.output); }; Client.prototype.setTermType = function(termType) { - this.term.env.TERM = termType; - this.term.termType = termType; + this.term.env.TERM = termType; + this.term.termType = termType; - this.log.debug( { termType : termType }, 'Set terminal type'); + this.log.debug( { termType : termType }, 'Set terminal type'); }; Client.prototype.startIdleMonitor = function() { - this.lastKeyPressMs = Date.now(); + this.lastKeyPressMs = Date.now(); - // - // Every 1m, check for idle. - // - this.idleCheck = setInterval( () => { - const nowMs = Date.now(); + // + // Every 1m, check for idle. + // + this.idleCheck = setInterval( () => { + const nowMs = Date.now(); - const idleLogoutSeconds = this.user.isAuthenticated() ? - Config().misc.idleLogoutSeconds : - Config().misc.preAuthIdleLogoutSeconds; + const idleLogoutSeconds = this.user.isAuthenticated() ? + Config().misc.idleLogoutSeconds : + Config().misc.preAuthIdleLogoutSeconds; - if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { - this.emit('idle timeout'); - } - }, 1000 * 60); + if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { + this.emit('idle timeout'); + } + }, 1000 * 60); }; Client.prototype.stopIdleMonitor = function() { - clearInterval(this.idleCheck); + clearInterval(this.idleCheck); }; Client.prototype.end = function () { - if(this.term) { - this.term.disconnect(); - } + if(this.term) { + this.term.disconnect(); + } - var currentModule = this.menuStack.getCurrentModule; + var currentModule = this.menuStack.getCurrentModule; - if(currentModule) { - currentModule.leave(); - } + if(currentModule) { + currentModule.leave(); + } - this.stopIdleMonitor(); + this.stopIdleMonitor(); - try { - // - // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH - // - // :TODO: is this OK? - return this.output.end.apply(this.output, arguments); - } catch(e) { - // TypeError - } + try { + // + // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH + // + // :TODO: is this OK? + return this.output.end.apply(this.output, arguments); + } catch(e) { + // TypeError + } }; Client.prototype.destroy = function () { - return this.output.destroy.apply(this.output, arguments); + return this.output.destroy.apply(this.output, arguments); }; Client.prototype.destroySoon = function () { - return this.output.destroySoon.apply(this.output, arguments); + return this.output.destroySoon.apply(this.output, arguments); }; Client.prototype.waitForKeyPress = function(cb) { - this.once('key press', function kp(ch, key) { - cb(ch, key); - }); + this.once('key press', function kp(ch, key) { + cb(ch, key); + }); }; Client.prototype.isLocal = function() { - // :TODO: Handle ipv6 better - return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); + // :TODO: Handle ipv6 better + return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); }; /////////////////////////////////////////////////////////////////////////////// @@ -502,41 +502,41 @@ Client.prototype.isLocal = function() { // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something Client.prototype.defaultHandlerMissingMod = function() { - var self = this; + var self = this; - function handler(err) { - self.log.error(err); + function handler(err) { + self.log.error(err); - self.term.write(ansi.resetScreen()); - self.term.write('An unrecoverable error has been encountered!\n'); - self.term.write('This has been logged for your SysOp to review.\n'); - self.term.write('\nGoodbye!\n'); + self.term.write(ansi.resetScreen()); + self.term.write('An unrecoverable error has been encountered!\n'); + self.term.write('This has been logged for your SysOp to review.\n'); + self.term.write('\nGoodbye!\n'); - //self.term.write(err); + //self.term.write(err); - //if(miscUtil.isDevelopment() && err.stack) { - // self.term.write('\n' + err.stack + '\n'); - //} + //if(miscUtil.isDevelopment() && err.stack) { + // self.term.write('\n' + err.stack + '\n'); + //} - self.end(); - } + self.end(); + } - return handler; + return handler; }; Client.prototype.terminalSupports = function(query) { - const termClient = this.term.termClient; + const termClient = this.term.termClient; - switch(query) { - case 'vtx_audio' : - // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt - return 'vtx' === termClient; + switch(query) { + case 'vtx_audio' : + // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt + return 'vtx' === termClient; - case 'vtx_hyperlink' : - return 'vtx' === termClient; + case 'vtx_hyperlink' : + return 'vtx' === termClient; - default : - return false; - } + default : + return false; + } }; diff --git a/core/client_connections.js b/core/client_connections.js index 3e7378f9..bdeb4539 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -23,98 +23,98 @@ function getActiveConnections() { return clientConnections; } function getActiveNodeList(authUsersOnly) { - if(!_.isBoolean(authUsersOnly)) { - authUsersOnly = true; - } + if(!_.isBoolean(authUsersOnly)) { + authUsersOnly = true; + } - const now = moment(); + const now = moment(); - const activeConnections = getActiveConnections().filter(ac => { - return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly); - }); + const activeConnections = getActiveConnections().filter(ac => { + return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly); + }); - return _.map(activeConnections, ac => { - const entry = { - node : ac.node, - authenticated : ac.user.isAuthenticated(), - userId : ac.user.userId, - action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown', - }; + return _.map(activeConnections, ac => { + const entry = { + node : ac.node, + authenticated : ac.user.isAuthenticated(), + userId : ac.user.userId, + action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown', + }; - // - // There may be a connection, but not a logged in user as of yet - // - if(ac.user.isAuthenticated()) { - entry.userName = ac.user.username; - entry.realName = ac.user.properties.real_name; - entry.location = ac.user.properties.location; - entry.affils = ac.user.properties.affiliation; + // + // There may be a connection, but not a logged in user as of yet + // + if(ac.user.isAuthenticated()) { + entry.userName = ac.user.username; + entry.realName = ac.user.properties.real_name; + entry.location = ac.user.properties.location; + entry.affils = ac.user.properties.affiliation; - const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); - entry.timeOn = moment.duration(diff, 'minutes'); - } - return entry; - }); + const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); + entry.timeOn = moment.duration(diff, 'minutes'); + } + return entry; + }); } function addNewClient(client, clientSock) { - const id = client.session.id = clientConnections.push(client) - 1; - const remoteAddress = client.remoteAddress = clientSock.remoteAddress; + const id = client.session.id = clientConnections.push(client) - 1; + const remoteAddress = client.remoteAddress = clientSock.remoteAddress; - // create a uniqe identifier one-time ID for this session - client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); + // create a uniqe identifier one-time ID for this session + client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); - // Create a client specific logger - // Note that this will be updated @ login with additional information - client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } ); + // Create a client specific logger + // Note that this will be updated @ login with additional information + client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } ); - const connInfo = { - remoteAddress : remoteAddress, - serverName : client.session.serverName, - isSecure : client.session.isSecure, - }; + const connInfo = { + remoteAddress : remoteAddress, + serverName : client.session.serverName, + isSecure : client.session.isSecure, + }; - if(client.log.debug()) { - connInfo.port = clientSock.localPort; - connInfo.family = clientSock.localFamily; - } + if(client.log.debug()) { + connInfo.port = clientSock.localPort; + connInfo.family = clientSock.localFamily; + } - client.log.info(connInfo, 'Client connected'); + client.log.info(connInfo, 'Client connected'); - Events.emit( - Events.getSystemEvents().ClientConnected, - { client : client, connectionCount : clientConnections.length } - ); + Events.emit( + Events.getSystemEvents().ClientConnected, + { client : client, connectionCount : clientConnections.length } + ); - return id; + return id; } function removeClient(client) { - client.end(); + client.end(); - const i = clientConnections.indexOf(client); - if(i > -1) { - clientConnections.splice(i, 1); + const i = clientConnections.indexOf(client); + if(i > -1) { + clientConnections.splice(i, 1); - logger.log.info( - { - connectionCount : clientConnections.length, - clientId : client.session.id - }, - 'Client disconnected' - ); + logger.log.info( + { + connectionCount : clientConnections.length, + clientId : client.session.id + }, + 'Client disconnected' + ); - if(client.user && client.user.isValid()) { - Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } ); - } + if(client.user && client.user.isValid()) { + Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } ); + } - Events.emit( - Events.getSystemEvents().ClientDisconnected, - { client : client, connectionCount : clientConnections.length } - ); - } + Events.emit( + Events.getSystemEvents().ClientDisconnected, + { client : client, connectionCount : clientConnections.length } + ); + } } function getConnectionByUserId(userId) { - return getActiveConnections().find( ac => userId === ac.user.userId ); + return getActiveConnections().find( ac => userId === ac.user.userId ); } diff --git a/core/client_term.js b/core/client_term.js index b944988d..77196586 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -13,185 +13,185 @@ var _ = require('lodash'); exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { - this.output = output; + this.output = output; - var outputEncoding = 'cp437'; - assert(iconv.encodingExists(outputEncoding)); + var outputEncoding = 'cp437'; + assert(iconv.encodingExists(outputEncoding)); - // convert line feeds such as \n -> \r\n - this.convertLF = true; + // convert line feeds such as \n -> \r\n + this.convertLF = true; - // - // Some terminal we handle specially - // They can also be found in this.env{} - // - var termType = 'unknown'; - var termHeight = 0; - var termWidth = 0; - var termClient = 'unknown'; + // + // Some terminal we handle specially + // They can also be found in this.env{} + // + var termType = 'unknown'; + var termHeight = 0; + var termWidth = 0; + var termClient = 'unknown'; - this.currentSyncFont = 'not_set'; + this.currentSyncFont = 'not_set'; - // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. - this.env = {}; + // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. + this.env = {}; - Object.defineProperty(this, 'outputEncoding', { - get : function() { - return outputEncoding; - }, - set : function(enc) { - if(iconv.encodingExists(enc)) { - outputEncoding = enc; - } else { - Log.warn({ encoding : enc }, 'Unknown encoding'); - } - } - }); + Object.defineProperty(this, 'outputEncoding', { + get : function() { + return outputEncoding; + }, + set : function(enc) { + if(iconv.encodingExists(enc)) { + outputEncoding = enc; + } else { + Log.warn({ encoding : enc }, 'Unknown encoding'); + } + } + }); - Object.defineProperty(this, 'termType', { - get : function() { - return termType; - }, - set : function(ttype) { - termType = ttype.toLowerCase(); + Object.defineProperty(this, 'termType', { + get : function() { + return termType; + }, + set : function(ttype) { + termType = ttype.toLowerCase(); - if(this.isANSI()) { - this.outputEncoding = 'cp437'; - } else { - // :TODO: See how x84 does this -- only set if local/remote are binary - this.outputEncoding = 'utf8'; - } + if(this.isANSI()) { + this.outputEncoding = 'cp437'; + } else { + // :TODO: See how x84 does this -- only set if local/remote are binary + this.outputEncoding = 'utf8'; + } - // :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification - // Windows telnet will send "VTNT". If so, set termClient='windows' - // there are some others on the page as well + // :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification + // Windows telnet will send "VTNT". If so, set termClient='windows' + // there are some others on the page as well - Log.debug( { encoding : this.outputEncoding }, 'Set output encoding due to terminal type change'); - } - }); + Log.debug( { encoding : this.outputEncoding }, 'Set output encoding due to terminal type change'); + } + }); - Object.defineProperty(this, 'termWidth', { - get : function() { - return termWidth; - }, - set : function(width) { - if(width > 0) { - termWidth = width; - } - } - }); + Object.defineProperty(this, 'termWidth', { + get : function() { + return termWidth; + }, + set : function(width) { + if(width > 0) { + termWidth = width; + } + } + }); - Object.defineProperty(this, 'termHeight', { - get : function() { - return termHeight; - }, - set : function(height) { - if(height > 0) { - termHeight = height; - } - } - }); + Object.defineProperty(this, 'termHeight', { + get : function() { + return termHeight; + }, + set : function(height) { + if(height > 0) { + termHeight = height; + } + } + }); - Object.defineProperty(this, 'termClient', { - get : function() { - return termClient; - }, - set : function(tc) { - termClient = tc; + Object.defineProperty(this, 'termClient', { + get : function() { + return termClient; + }, + set : function(tc) { + termClient = tc; - Log.debug( { termClient : this.termClient }, 'Set known terminal client'); - } - }); + Log.debug( { termClient : this.termClient }, 'Set known terminal client'); + } + }); } ClientTerminal.prototype.disconnect = function() { - this.output = null; + this.output = null; }; ClientTerminal.prototype.isNixTerm = function() { - // - // Standard *nix type terminals - // - if(this.termType.startsWith('xterm')) { - return true; - } + // + // Standard *nix type terminals + // + if(this.termType.startsWith('xterm')) { + return true; + } - return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType); + return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType); }; ClientTerminal.prototype.isANSI = function() { - // - // ANSI terminals should be encoded to CP437 - // - // Some terminal types provided by Mercyful Fate / Enthral: - // ANSI-BBS - // PC-ANSI - // QANSI - // SCOANSI - // VT100 - // QNX - // - // Reports from various terminals - // - // syncterm: - // * SyncTERM - // - // xterm: - // * PuTTY - // - // ansi-bbs: - // * fTelnet - // - // pcansi: - // * ZOC - // - // screen: - // * ConnectBot (Android) - // - // linux: - // * JuiceSSH (note: TERM=linux also) - // - return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType); + // + // ANSI terminals should be encoded to CP437 + // + // Some terminal types provided by Mercyful Fate / Enthral: + // ANSI-BBS + // PC-ANSI + // QANSI + // SCOANSI + // VT100 + // QNX + // + // Reports from various terminals + // + // syncterm: + // * SyncTERM + // + // xterm: + // * PuTTY + // + // ansi-bbs: + // * fTelnet + // + // pcansi: + // * ZOC + // + // screen: + // * ConnectBot (Android) + // + // linux: + // * JuiceSSH (note: TERM=linux also) + // + return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType); }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) { - this.rawWrite(this.encode(s, convertLineFeeds), cb); + this.rawWrite(this.encode(s, convertLineFeeds), cb); }; ClientTerminal.prototype.rawWrite = function(s, cb) { - if(this.output) { - this.output.write(s, err => { - if(cb) { - return cb(err); - } + if(this.output) { + this.output.write(s, err => { + if(cb) { + return cb(err); + } - if(err) { - Log.warn( { error : err.message }, 'Failed writing to socket'); - } - }); - } + if(err) { + Log.warn( { error : err.message }, 'Failed writing to socket'); + } + }); + } }; ClientTerminal.prototype.pipeWrite = function(s, spec, cb) { - spec = spec || 'renegade'; + spec = spec || 'renegade'; - var conv = { - enigma : enigmaToAnsi, - renegade : renegadeToAnsi, - }[spec] || renegadeToAnsi; + var conv = { + enigma : enigmaToAnsi, + renegade : renegadeToAnsi, + }[spec] || renegadeToAnsi; - this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| + this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| }; ClientTerminal.prototype.encode = function(s, convertLineFeeds) { - convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF; + convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF; - if(convertLineFeeds && _.isString(s)) { - s = s.replace(/\n/g, '\r\n'); - } - return iconv.encode(s, this.outputEncoding); + if(convertLineFeeds && _.isString(s)) { + s = s.replace(/\n/g, '\r\n'); + } + return iconv.encode(s, this.outputEncoding); }; diff --git a/core/color_codes.js b/core/color_codes.js index 10e7a2c3..b04d2c0b 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -28,139 +28,139 @@ exports.controlCodesToAnsi = controlCodesToAnsi; // :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc... function enigmaToAnsi(s, client) { - if(-1 == s.indexOf('|')) { - return s; // no pipe codes present - } + if(-1 == s.indexOf('|')) { + return s; // no pipe codes present + } - var result = ''; - var re = /\|([A-Z\d]{2}|\|)/g; - var m; - var lastIndex = 0; - while((m = re.exec(s))) { - var val = m[1]; + var result = ''; + var re = /\|([A-Z\d]{2}|\|)/g; + var m; + var lastIndex = 0; + while((m = re.exec(s))) { + var val = m[1]; - if('|' == val) { - result += '|'; - continue; - } + if('|' == val) { + result += '|'; + continue; + } - // convert to number - val = parseInt(val, 10); - if(isNaN(val)) { - // - // ENiGMA MCI code? Only available if |client| - // is supplied. - // - val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal - } + // convert to number + val = parseInt(val, 10); + if(isNaN(val)) { + // + // ENiGMA MCI code? Only available if |client| + // is supplied. + // + val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal + } - if(_.isString(val)) { - result += s.substr(lastIndex, m.index - lastIndex) + val; - } else { - assert(val >= 0 && val <= 47); + if(_.isString(val)) { + result += s.substr(lastIndex, m.index - lastIndex) + val; + } else { + assert(val >= 0 && val <= 47); - var attr = ''; - if(7 == val) { - attr = ansi.sgr('normal'); - } else if (val < 7 || val >= 16) { - attr = ansi.sgr(['normal', val]); - } else if (val <= 15) { - attr = ansi.sgr(['normal', val - 8, 'bold']); - } + var attr = ''; + if(7 == val) { + attr = ansi.sgr('normal'); + } else if (val < 7 || val >= 16) { + attr = ansi.sgr(['normal', val]); + } else if (val <= 15) { + attr = ansi.sgr(['normal', val - 8, 'bold']); + } - result += s.substr(lastIndex, m.index - lastIndex) + attr; - } + result += s.substr(lastIndex, m.index - lastIndex) + attr; + } - lastIndex = re.lastIndex; - } + lastIndex = re.lastIndex; + } - result = (0 === result.length ? s : result + s.substr(lastIndex)); + result = (0 === result.length ? s : result + s.substr(lastIndex)); - return result; + return result; } function stripEnigmaCodes(s) { - return s.replace(/\|[A-Z\d]{2}/g, ''); + return s.replace(/\|[A-Z\d]{2}/g, ''); } function enigmaStrLen(s) { - return stripEnigmaCodes(s).length; + return stripEnigmaCodes(s).length; } function ansiSgrFromRenegadeColorCode(cc) { - return ansi.sgr({ - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], + return ansi.sgr({ + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], - 8 : [ 'bold', 'black' ], - 9 : [ 'bold', 'blue' ], - 10 : [ 'bold', 'green' ], - 11 : [ 'bold', 'cyan' ], - 12 : [ 'bold', 'red' ], - 13 : [ 'bold', 'magenta' ], - 14 : [ 'bold', 'yellow' ], - 15 : [ 'bold', 'white' ], + 8 : [ 'bold', 'black' ], + 9 : [ 'bold', 'blue' ], + 10 : [ 'bold', 'green' ], + 11 : [ 'bold', 'cyan' ], + 12 : [ 'bold', 'red' ], + 13 : [ 'bold', 'magenta' ], + 14 : [ 'bold', 'yellow' ], + 15 : [ 'bold', 'white' ], - 16 : [ 'blackBG' ], - 17 : [ 'blueBG' ], - 18 : [ 'greenBG' ], - 19 : [ 'cyanBG' ], - 20 : [ 'redBG' ], - 21 : [ 'magentaBG' ], - 22 : [ 'yellowBG' ], - 23 : [ 'whiteBG' ], + 16 : [ 'blackBG' ], + 17 : [ 'blueBG' ], + 18 : [ 'greenBG' ], + 19 : [ 'cyanBG' ], + 20 : [ 'redBG' ], + 21 : [ 'magentaBG' ], + 22 : [ 'yellowBG' ], + 23 : [ 'whiteBG' ], - 24 : [ 'blink', 'blackBG' ], - 25 : [ 'blink', 'blueBG' ], - 26 : [ 'blink', 'greenBG' ], - 27 : [ 'blink', 'cyanBG' ], - 28 : [ 'blink', 'redBG' ], - 29 : [ 'blink', 'magentaBG' ], - 30 : [ 'blink', 'yellowBG' ], - 31 : [ 'blink', 'whiteBG' ], - }[cc] || 'normal'); + 24 : [ 'blink', 'blackBG' ], + 25 : [ 'blink', 'blueBG' ], + 26 : [ 'blink', 'greenBG' ], + 27 : [ 'blink', 'cyanBG' ], + 28 : [ 'blink', 'redBG' ], + 29 : [ 'blink', 'magentaBG' ], + 30 : [ 'blink', 'yellowBG' ], + 31 : [ 'blink', 'whiteBG' ], + }[cc] || 'normal'); } function renegadeToAnsi(s, client) { - if(-1 == s.indexOf('|')) { - return s; // no pipe codes present - } + if(-1 == s.indexOf('|')) { + return s; // no pipe codes present + } - var result = ''; - var re = /\|([A-Z\d]{2}|\|)/g; - var m; - var lastIndex = 0; - while((m = re.exec(s))) { - var val = m[1]; + var result = ''; + var re = /\|([A-Z\d]{2}|\|)/g; + var m; + var lastIndex = 0; + while((m = re.exec(s))) { + var val = m[1]; - if('|' == val) { - result += '|'; - continue; - } + if('|' == val) { + result += '|'; + continue; + } - // convert to number - val = parseInt(val, 10); - if(isNaN(val)) { - val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal - } + // convert to number + val = parseInt(val, 10); + if(isNaN(val)) { + val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal + } - if(_.isString(val)) { - result += s.substr(lastIndex, m.index - lastIndex) + val; - } else { - const attr = ansiSgrFromRenegadeColorCode(val); - result += s.substr(lastIndex, m.index - lastIndex) + attr; - } + if(_.isString(val)) { + result += s.substr(lastIndex, m.index - lastIndex) + val; + } else { + const attr = ansiSgrFromRenegadeColorCode(val); + result += s.substr(lastIndex, m.index - lastIndex) + attr; + } - lastIndex = re.lastIndex; - } + lastIndex = re.lastIndex; + } - return (0 === result.length ? s : result + s.substr(lastIndex)); + return (0 === result.length ? s : result + s.substr(lastIndex)); } // @@ -180,113 +180,113 @@ function renegadeToAnsi(s, client) { // * http://wiki.synchro.net/custom:colors // function controlCodesToAnsi(s, client) { - const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex + const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex - let m; - let result = ''; - let lastIndex = 0; - let v; - let fg; - let bg; + let m; + let result = ''; + let lastIndex = 0; + let v; + let fg; + let bg; - while((m = RE.exec(s))) { - switch(m[0].charAt(0)) { - case '|' : - // Renegade or ENiGMA MCI - v = parseInt(m[2], 10); + while((m = RE.exec(s))) { + switch(m[0].charAt(0)) { + case '|' : + // Renegade or ENiGMA MCI + v = parseInt(m[2], 10); - if(isNaN(v)) { - v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal - } + if(isNaN(v)) { + v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal + } - if(_.isString(v)) { - result += s.substr(lastIndex, m.index - lastIndex) + v; - } else { - v = ansiSgrFromRenegadeColorCode(v); - result += s.substr(lastIndex, m.index - lastIndex) + v; - } - break; + if(_.isString(v)) { + result += s.substr(lastIndex, m.index - lastIndex) + v; + } else { + v = ansiSgrFromRenegadeColorCode(v); + result += s.substr(lastIndex, m.index - lastIndex) + v; + } + break; - case '@' : - // PCBoard @X## or Wildcat! @##@ - if('@' === m[0].substr(-1)) { - // Wildcat! - v = m[6]; - } else { - v = m[4]; - } + case '@' : + // PCBoard @X## or Wildcat! @##@ + if('@' === m[0].substr(-1)) { + // Wildcat! + v = m[6]; + } else { + v = m[4]; + } - fg = { - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], + fg = { + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], - 8 : [ 'blink', 'black' ], - 9 : [ 'blink', 'blue' ], - A : [ 'blink', 'green' ], - B : [ 'blink', 'cyan' ], - C : [ 'blink', 'red' ], - D : [ 'blink', 'magenta' ], - E : [ 'blink', 'yellow' ], - F : [ 'blink', 'white' ], - }[v.charAt(0)] || ['normal']; + 8 : [ 'blink', 'black' ], + 9 : [ 'blink', 'blue' ], + A : [ 'blink', 'green' ], + B : [ 'blink', 'cyan' ], + C : [ 'blink', 'red' ], + D : [ 'blink', 'magenta' ], + E : [ 'blink', 'yellow' ], + F : [ 'blink', 'white' ], + }[v.charAt(0)] || ['normal']; - bg = { - 0 : [ 'blackBG' ], - 1 : [ 'blueBG' ], - 2 : [ 'greenBG' ], - 3 : [ 'cyanBG' ], - 4 : [ 'redBG' ], - 5 : [ 'magentaBG' ], - 6 : [ 'yellowBG' ], - 7 : [ 'whiteBG' ], + bg = { + 0 : [ 'blackBG' ], + 1 : [ 'blueBG' ], + 2 : [ 'greenBG' ], + 3 : [ 'cyanBG' ], + 4 : [ 'redBG' ], + 5 : [ 'magentaBG' ], + 6 : [ 'yellowBG' ], + 7 : [ 'whiteBG' ], - 8 : [ 'bold', 'blackBG' ], - 9 : [ 'bold', 'blueBG' ], - A : [ 'bold', 'greenBG' ], - B : [ 'bold', 'cyanBG' ], - C : [ 'bold', 'redBG' ], - D : [ 'bold', 'magentaBG' ], - E : [ 'bold', 'yellowBG' ], - F : [ 'bold', 'whiteBG' ], - }[v.charAt(1)] || [ 'normal' ]; + 8 : [ 'bold', 'blackBG' ], + 9 : [ 'bold', 'blueBG' ], + A : [ 'bold', 'greenBG' ], + B : [ 'bold', 'cyanBG' ], + C : [ 'bold', 'redBG' ], + D : [ 'bold', 'magentaBG' ], + E : [ 'bold', 'yellowBG' ], + F : [ 'bold', 'whiteBG' ], + }[v.charAt(1)] || [ 'normal' ]; - v = ansi.sgr(fg.concat(bg)); - result += s.substr(lastIndex, m.index - lastIndex) + v; - break; + v = ansi.sgr(fg.concat(bg)); + result += s.substr(lastIndex, m.index - lastIndex) + v; + break; - case '\x03' : - v = parseInt(m[8], 10); + case '\x03' : + v = parseInt(m[8], 10); - if(isNaN(v)) { - v += m[0]; - } else { - v = ansi.sgr({ - 0 : [ 'reset', 'black' ], - 1 : [ 'bold', 'cyan' ], - 2 : [ 'bold', 'yellow' ], - 3 : [ 'reset', 'magenta' ], - 4 : [ 'bold', 'white', 'blueBG' ], - 5 : [ 'reset', 'green' ], - 6 : [ 'bold', 'blink', 'red' ], - 7 : [ 'bold', 'blue' ], - 8 : [ 'reset', 'blue' ], - 9 : [ 'reset', 'cyan' ], - }[v] || 'normal'); - } + if(isNaN(v)) { + v += m[0]; + } else { + v = ansi.sgr({ + 0 : [ 'reset', 'black' ], + 1 : [ 'bold', 'cyan' ], + 2 : [ 'bold', 'yellow' ], + 3 : [ 'reset', 'magenta' ], + 4 : [ 'bold', 'white', 'blueBG' ], + 5 : [ 'reset', 'green' ], + 6 : [ 'bold', 'blink', 'red' ], + 7 : [ 'bold', 'blue' ], + 8 : [ 'reset', 'blue' ], + 9 : [ 'reset', 'cyan' ], + }[v] || 'normal'); + } - result += s.substr(lastIndex, m.index - lastIndex) + v; + result += s.substr(lastIndex, m.index - lastIndex) + v; - break; - } + break; + } - lastIndex = RE.lastIndex; - } + lastIndex = RE.lastIndex; + } - return (0 === result.length ? s : result + s.substr(lastIndex)); + return (0 === result.length ? s : result + s.substr(lastIndex)); } \ No newline at end of file diff --git a/core/combatnet.js b/core/combatnet.js index 8fb92de1..d6616449 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -11,107 +11,107 @@ const _ = require('lodash'); const RLogin = require('rlogin'); exports.moduleInfo = { - name : 'CombatNet', - desc : 'CombatNet Access Module', - author : 'Dave Stephens', + name : 'CombatNet', + desc : 'CombatNet Access Module', + author : 'Dave Stephens', }; exports.getModule = class CombatNetModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - // establish defaults - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'bbs.combatnet.us'; - this.config.rloginPort = this.config.rloginPort || 4513; - } + // establish defaults + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'bbs.combatnet.us'; + this.config.rloginPort = this.config.rloginPort || 4513; + } - initSequence() { - const self = this; + initSequence() { + const self = this; - async.series( - [ - function validateConfig(callback) { - if(!_.isString(self.config.password)) { - return callback(new Error('Config requires "password"!')); - } - if(!_.isString(self.config.bbsTag)) { - return callback(new Error('Config requires "bbsTag"!')); - } - return callback(null); - }, - function establishRloginConnection(callback) { - self.client.term.write(resetScreen()); - self.client.term.write('Connecting to CombatNet, please wait...\n'); + async.series( + [ + function validateConfig(callback) { + if(!_.isString(self.config.password)) { + return callback(new Error('Config requires "password"!')); + } + if(!_.isString(self.config.bbsTag)) { + return callback(new Error('Config requires "bbsTag"!')); + } + return callback(null); + }, + function establishRloginConnection(callback) { + self.client.term.write(resetScreen()); + self.client.term.write('Connecting to CombatNet, please wait...\n'); - const restorePipeToNormal = function() { - if(self.client.term.output) { - self.client.term.output.removeListener('data', sendToRloginBuffer); - } - }; + const restorePipeToNormal = function() { + if(self.client.term.output) { + self.client.term.output.removeListener('data', sendToRloginBuffer); + } + }; - const rlogin = new RLogin( - { 'clientUsername' : self.config.password, - 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, - 'host' : self.config.host, - 'port' : self.config.rloginPort, - 'terminalType' : self.client.term.termClient, - 'terminalSpeed' : 57600 - } - ); + const rlogin = new RLogin( + { 'clientUsername' : self.config.password, + 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, + 'host' : self.config.host, + 'port' : self.config.rloginPort, + 'terminalType' : self.client.term.termClient, + 'terminalSpeed' : 57600 + } + ); - // If there was an error ... - rlogin.on('error', err => { - self.client.log.info(`CombatNet rlogin client error: ${err.message}`); - restorePipeToNormal(); - return callback(err); - }); + // If there was an error ... + rlogin.on('error', err => { + self.client.log.info(`CombatNet rlogin client error: ${err.message}`); + restorePipeToNormal(); + return callback(err); + }); - // If we've been disconnected ... - rlogin.on('disconnect', () => { - self.client.log.info('Disconnected from CombatNet'); - restorePipeToNormal(); - return callback(null); - }); + // If we've been disconnected ... + rlogin.on('disconnect', () => { + self.client.log.info('Disconnected from CombatNet'); + restorePipeToNormal(); + return callback(null); + }); - function sendToRloginBuffer(buffer) { - rlogin.send(buffer); - } + function sendToRloginBuffer(buffer) { + rlogin.send(buffer); + } - rlogin.on('connect', - /* The 'connect' event handler will be supplied with one argument, + rlogin.on('connect', + /* The 'connect' event handler will be supplied with one argument, a boolean indicating whether or not the connection was established. */ - function(state) { - if(state) { - self.client.log.info('Connected to CombatNet'); - self.client.term.output.on('data', sendToRloginBuffer); + function(state) { + if(state) { + self.client.log.info('Connected to CombatNet'); + self.client.term.output.on('data', sendToRloginBuffer); - } else { - return callback(new Error('Failed to establish establish CombatNet connection')); - } - } - ); + } else { + return callback(new Error('Failed to establish establish CombatNet connection')); + } + } + ); - // If data (a Buffer) has been received from the server ... - rlogin.on('data', (data) => { - self.client.term.rawWrite(data); - }); + // If data (a Buffer) has been received from the server ... + rlogin.on('data', (data) => { + self.client.term.rawWrite(data); + }); - // connect... - rlogin.connect(); + // connect... + rlogin.connect(); - // note: no explicit callback() until we're finished! - } - ], - err => { - if(err) { - self.client.log.warn( { error : err.message }, 'CombatNet error'); - } + // note: no explicit callback() until we're finished! + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'CombatNet error'); + } - // if the client is still here, go to previous - self.prevMenu(); - } - ); - } + // if the client is still here, go to previous + self.prevMenu(); + } + ); + } }; diff --git a/core/conf_area_util.js b/core/conf_area_util.js index 6b71061b..7c4bf5bb 100644 --- a/core/conf_area_util.js +++ b/core/conf_area_util.js @@ -12,19 +12,19 @@ exports.sortAreasOrConfs = sortAreasOrConfs; // Otherwise, use a locale comparison on the sort key or name as a fallback // function sortAreasOrConfs(areasOrConfs, type) { - let entryA; - let entryB; + let entryA; + let entryB; - areasOrConfs.sort((a, b) => { - entryA = type ? a[type] : a; - entryB = type ? b[type] : b; + areasOrConfs.sort((a, b) => { + entryA = type ? a[type] : a; + entryB = type ? b[type] : b; - if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { - return entryA.sort - entryB.sort; - } else { - const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; - const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; - return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare - } - }); + if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { + return entryA.sort - entryB.sort; + } else { + const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; + const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; + return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare + } + }); } \ No newline at end of file diff --git a/core/config.js b/core/config.js index 38962b1f..3a1359d7 100644 --- a/core/config.js +++ b/core/config.js @@ -16,157 +16,157 @@ exports.getDefaultPath = getDefaultPath; let currentConfiguration = {}; function hasMessageConferenceAndArea(config) { - assert(_.isObject(config.messageConferences)); // we create one ourself! + assert(_.isObject(config.messageConferences)); // we create one ourself! - const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => { - return 'system_internal' !== confTag; - }); + const nonInternalConfs = Object.keys(config.messageConferences).filter(confTag => { + return 'system_internal' !== confTag; + }); - if(0 === nonInternalConfs.length) { - return false; - } + if(0 === nonInternalConfs.length) { + return false; + } - // :TODO: there is likely a better/cleaner way of doing this + // :TODO: there is likely a better/cleaner way of doing this - let result = false; - _.forEach(nonInternalConfs, confTag => { - if(_.has(config.messageConferences[confTag], 'areas') && + let result = false; + _.forEach(nonInternalConfs, confTag => { + if(_.has(config.messageConferences[confTag], 'areas') && Object.keys(config.messageConferences[confTag].areas) > 0) - { - result = true; - return false; // stop iteration - } - }); + { + result = true; + return false; // stop iteration + } + }); - return result; + return result; } function mergeValidateAndFinalize(config, cb) { - async.waterfall( - [ - function mergeWithDefaultConfig(callback) { - const mergedConfig = _.mergeWith( - getDefaultConfig(), - config, (conf1, conf2) => { - // Arrays should always concat - if(_.isArray(conf1)) { - // :TODO: look for collisions & override dupes - return conf1.concat(conf2); - } - } - ); + async.waterfall( + [ + function mergeWithDefaultConfig(callback) { + const mergedConfig = _.mergeWith( + getDefaultConfig(), + config, (conf1, conf2) => { + // Arrays should always concat + if(_.isArray(conf1)) { + // :TODO: look for collisions & override dupes + return conf1.concat(conf2); + } + } + ); - return callback(null, mergedConfig); - }, - function validate(mergedConfig, callback) { - // - // Various sections must now exist in config - // - // :TODO: Logic is broken here: - if(hasMessageConferenceAndArea(mergedConfig)) { - return callback(Errors.MissingConfig('Please create at least one message conference and area!')); - } - return callback(null, mergedConfig); - }, - function setIt(mergedConfig, callback) { - // :TODO: .config property is to be deprecated once conversions are done - exports.config = currentConfiguration = mergedConfig; + return callback(null, mergedConfig); + }, + function validate(mergedConfig, callback) { + // + // Various sections must now exist in config + // + // :TODO: Logic is broken here: + if(hasMessageConferenceAndArea(mergedConfig)) { + return callback(Errors.MissingConfig('Please create at least one message conference and area!')); + } + return callback(null, mergedConfig); + }, + function setIt(mergedConfig, callback) { + // :TODO: .config property is to be deprecated once conversions are done + exports.config = currentConfiguration = mergedConfig; - exports.get = () => currentConfiguration; - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); + exports.get = () => currentConfiguration; + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); } function init(configPath, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - ConfigCache.getConfig(reCachedPath, (err, config) => { - if(!err) { - mergeValidateAndFinalize(config); - } - }); - }; + const changed = ( { fileName, fileRoot } ) => { + const reCachedPath = paths.join(fileRoot, fileName); + ConfigCache.getConfig(reCachedPath, (err, config) => { + if(!err) { + mergeValidateAndFinalize(config); + } + }); + }; - const ConfigCache = require('./config_cache.js'); - const getConfigOptions = { - filePath : configPath, - noWatch : options.noWatch, - }; - if(!options.noWatch) { - getConfigOptions.callback = changed; - } - ConfigCache.getConfigWithOptions(getConfigOptions, (err, config) => { - if(err) { - return cb(err); - } + const ConfigCache = require('./config_cache.js'); + const getConfigOptions = { + filePath : configPath, + noWatch : options.noWatch, + }; + if(!options.noWatch) { + getConfigOptions.callback = changed; + } + ConfigCache.getConfigWithOptions(getConfigOptions, (err, config) => { + if(err) { + return cb(err); + } - return mergeValidateAndFinalize(config, cb); - }); + return mergeValidateAndFinalize(config, cb); + }); } function getDefaultPath() { - // e.g. /enigma-bbs-install-path/config/ - return './config/'; + // e.g. /enigma-bbs-install-path/config/ + return './config/'; } function getDefaultConfig() { - return { - general : { - boardName : 'Another Fine ENiGMA½ BBS', + return { + general : { + boardName : 'Another Fine ENiGMA½ BBS', - closedSystem : false, // is the system closed to new users? + closedSystem : false, // is the system closed to new users? - loginAttempts : 3, + loginAttempts : 3, - menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config) - promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config) - }, + menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config) + promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config) + }, - // :TODO: see notes below about 'theme' section - move this! - preLoginTheme : 'luciano_blocktronics', + // :TODO: see notes below about 'theme' section - move this! + preLoginTheme : 'luciano_blocktronics', - users : { - usernameMin : 2, - usernameMax : 16, // Note that FidoNet wants 36 max - usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ ]+$', + users : { + usernameMin : 2, + usernameMax : 16, // Note that FidoNet wants 36 max + usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ ]+$', - passwordMin : 6, - passwordMax : 128, - badPassFile : paths.join(__dirname, '../misc/10_million_password_list_top_10000.txt'), // https://github.com/danielmiessler/SecLists + passwordMin : 6, + passwordMax : 128, + badPassFile : paths.join(__dirname, '../misc/10_million_password_list_top_10000.txt'), // https://github.com/danielmiessler/SecLists - realNameMax : 32, - locationMax : 32, - affilsMax : 32, - emailMax : 255, - webMax : 255, + realNameMax : 32, + locationMax : 32, + affilsMax : 32, + emailMax : 255, + webMax : 255, - requireActivation : false, // require SysOp activation? false = auto-activate + requireActivation : false, // require SysOp activation? false = auto-activate - groups : [ 'users', 'sysops' ], // built in groups - defaultGroups : [ 'users' ], // default groups new users belong to + groups : [ 'users', 'sysops' ], // built in groups + defaultGroups : [ 'users' ], // default groups new users belong to - newUserNames : [ 'new', 'apply' ], // Names reserved for applying + newUserNames : [ 'new', 'apply' ], // Names reserved for applying - badUserNames : [ - 'sysop', 'admin', 'administrator', 'root', 'all', - 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' - ], - }, + badUserNames : [ + 'sysop', 'admin', 'administrator', 'root', 'all', + 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' + ], + }, - // :TODO: better name for "defaults"... which is redundant here! - /* + // :TODO: better name for "defaults"... which is redundant here! + /* Concept "theme" : { "default" : "defaultThemeName", // or "*" @@ -175,662 +175,662 @@ function getDefaultConfig() { ... } */ - defaults : { - theme : 'luciano_blocktronics', - passwordChar : '*', // TODO: move to user ? - dateFormat : { - short : 'MM/DD/YYYY', - long : 'ddd, MMMM Do, YYYY', - }, - timeFormat : { - short : 'h:mm a', - }, - dateTimeFormat : { - short : 'MM/DD/YYYY h:mm a', - long : 'ddd, MMMM Do, YYYY, h:mm a', - } - }, + defaults : { + theme : 'luciano_blocktronics', + passwordChar : '*', // TODO: move to user ? + dateFormat : { + short : 'MM/DD/YYYY', + long : 'ddd, MMMM Do, YYYY', + }, + timeFormat : { + short : 'h:mm a', + }, + dateTimeFormat : { + short : 'MM/DD/YYYY h:mm a', + long : 'ddd, MMMM Do, YYYY, h:mm a', + } + }, - menus : { - cls : true, // Clear screen before each menu by default? - }, + menus : { + cls : true, // Clear screen before each menu by default? + }, - paths : { - config : paths.join(__dirname, './../config/'), - mods : paths.join(__dirname, './../mods/'), - loginServers : paths.join(__dirname, './servers/login/'), - contentServers : paths.join(__dirname, './servers/content/'), + paths : { + config : paths.join(__dirname, './../config/'), + mods : paths.join(__dirname, './../mods/'), + loginServers : paths.join(__dirname, './servers/login/'), + contentServers : paths.join(__dirname, './servers/content/'), - scannerTossers : paths.join(__dirname, './scanner_tossers/'), - mailers : paths.join(__dirname, './mailers/') , + scannerTossers : paths.join(__dirname, './scanner_tossers/'), + mailers : paths.join(__dirname, './mailers/') , - art : paths.join(__dirname, './../art/general/'), - themes : paths.join(__dirname, './../art/themes/'), - logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such - db : paths.join(__dirname, './../db/'), - modsDb : paths.join(__dirname, './../db/mods/'), - dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/ - misc : paths.join(__dirname, './../misc/'), - }, + art : paths.join(__dirname, './../art/general/'), + themes : paths.join(__dirname, './../art/themes/'), + logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such + db : paths.join(__dirname, './../db/'), + modsDb : paths.join(__dirname, './../db/mods/'), + dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/ + misc : paths.join(__dirname, './../misc/'), + }, - loginServers : { - telnet : { - port : 8888, - enabled : true, - firstMenu : 'telnetConnected', - }, - ssh : { - port : 8889, - enabled : false, // default to false as PK/pass in config.hjson are required + loginServers : { + telnet : { + port : 8888, + enabled : true, + firstMenu : 'telnetConnected', + }, + ssh : { + port : 8889, + enabled : false, // default to false as PK/pass in config.hjson are required - // - // Private key in PEM format - // - // Generating your PK: - // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 - // - // Then, set servers.ssh.privateKeyPass to the password you use above - // in your config.hjson - // - privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), - firstMenu : 'sshConnected', - firstMenuNewUser : 'sshConnectedNewUser', - }, - webSocket : { - ws : { - // non-secure ws:// - enabled : false, - port : 8810, - }, - wss : { - // secure ws:// - // must provide valid certPem and keyPem - enabled : false, - port : 8811, - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), - }, - }, - }, + // + // Private key in PEM format + // + // Generating your PK: + // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 + // + // Then, set servers.ssh.privateKeyPass to the password you use above + // in your config.hjson + // + privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), + firstMenu : 'sshConnected', + firstMenuNewUser : 'sshConnectedNewUser', + }, + webSocket : { + ws : { + // non-secure ws:// + enabled : false, + port : 8810, + }, + wss : { + // secure ws:// + // must provide valid certPem and keyPem + enabled : false, + port : 8811, + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + }, + }, + }, - contentServers : { - web : { - domain : 'another-fine-enigma-bbs.org', + contentServers : { + web : { + domain : 'another-fine-enigma-bbs.org', - staticRoot : paths.join(__dirname, './../www'), + staticRoot : paths.join(__dirname, './../www'), - resetPassword : { - // - // The following templates have these variables available to them: - // - // * %BOARDNAME% : Name of BBS - // * %USERNAME% : Username of whom to reset password - // * %TOKEN% : Reset token - // * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page, - // URL to POST submit reset form. + resetPassword : { + // + // The following templates have these variables available to them: + // + // * %BOARDNAME% : Name of BBS + // * %USERNAME% : Username of whom to reset password + // * %TOKEN% : Reset token + // * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page, + // URL to POST submit reset form. - // templates for pw reset *email* - resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version - resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version + // templates for pw reset *email* + resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version + resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version - // tempalte for pw reset *landing page* - // - resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'), - }, + // tempalte for pw reset *landing page* + // + resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'), + }, - http : { - enabled : false, - port : 8080, - }, - https : { - enabled : false, - port : 8443, - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), - } - } - }, + http : { + enabled : false, + port : 8080, + }, + https : { + enabled : false, + port : 8443, + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + } + } + }, - infoExtractUtils : { - Exiftool2Desc : { - cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x - }, - Exiftool : { - cmd : 'exiftool', - args : [ - '-charset', 'utf8', '{filePath}', - // exclude the following: - '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', - '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', - '--metadatadate', '--xmptoolkit' - ] - }, - XDMS2Desc : { - // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'd', '{filePath}' ] - }, - XDMS2LongDesc : { - // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'f', '{filePath}' ] - } - }, + infoExtractUtils : { + Exiftool2Desc : { + cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x + }, + Exiftool : { + cmd : 'exiftool', + args : [ + '-charset', 'utf8', '{filePath}', + // exclude the following: + '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', + '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', + '--metadatadate', '--xmptoolkit' + ] + }, + XDMS2Desc : { + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'd', '{filePath}' ] + }, + XDMS2LongDesc : { + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'f', '{filePath}' ] + } + }, - fileTypes : { - // - // File types explicitly known to the system. Here we can configure - // information extraction, archive treatment, etc. - // - // MIME types can be found in mime-db: https://github.com/jshttp/mime-db - // - // Resources for signature/magic bytes: - // * http://www.garykessler.net/library/file_sigs.html - // - // - // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads - // :TODO: textual : bool -- if text, we can view. - // :TODO: asText : { cmd, args[] } -> viewable text + fileTypes : { + // + // File types explicitly known to the system. Here we can configure + // information extraction, archive treatment, etc. + // + // MIME types can be found in mime-db: https://github.com/jshttp/mime-db + // + // Resources for signature/magic bytes: + // * http://www.garykessler.net/library/file_sigs.html + // + // + // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads + // :TODO: textual : bool -- if text, we can view. + // :TODO: asText : { cmd, args[] } -> viewable text - // - // Audio - // - 'audio/mpeg' : { - desc : 'MP3 Audio', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'application/pdf' : { - desc : 'Adobe PDF', - shortDescUtil : 'Exiftool2Desc', - 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 - // - 'image/jpeg' : { - desc : 'JPEG Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'image/png' : { - desc : 'Portable Network Graphic Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'image/gif' : { - desc : 'Graphics Interchange Format Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - 'image/webp' : { - desc : 'WebP Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', - }, - // - // Archives - // - 'application/zip' : { - desc : 'ZIP Archive', - sig : '504b0304', - offset : 0, - archiveHandler : '7Zip', - }, - /* + // + // Audio + // + 'audio/mpeg' : { + desc : 'MP3 Audio', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'application/pdf' : { + desc : 'Adobe PDF', + shortDescUtil : 'Exiftool2Desc', + 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 + // + 'image/jpeg' : { + desc : 'JPEG Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/png' : { + desc : 'Portable Network Graphic Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/gif' : { + desc : 'Graphics Interchange Format Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + 'image/webp' : { + desc : 'WebP Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', + }, + // + // Archives + // + 'application/zip' : { + desc : 'ZIP Archive', + sig : '504b0304', + offset : 0, + archiveHandler : '7Zip', + }, + /* 'application/x-cbr' : { desc : 'Comic Book Archive', sig : '504b0304', }, */ - 'application/x-arj' : { - desc : 'ARJ Archive', - sig : '60ea', - offset : 0, - archiveHandler : 'Arj', - }, - 'application/x-rar-compressed' : { - desc : 'RAR Archive', - sig : '526172211a0700', - offset : 0, - archiveHandler : 'Rar', - }, - 'application/gzip' : { - desc : 'Gzip Archive', - sig : '1f8b', - offset : 0, - archiveHandler : 'TarGz', - }, - // :TODO: application/x-bzip - 'application/x-bzip2' : { - desc : 'BZip2 Archive', - sig : '425a68', - offset : 0, - archiveHandler : '7Zip', - }, - 'application/x-lzh-compressed' : { - desc : 'LHArc Archive', - sig : '2d6c68', - offset : 2, - archiveHandler : 'Lha', - }, - 'application/x-lzx' : { - desc : 'LZX Archive', - sig : '4c5a5800', - offset : 0, - archiveHandler : 'Lzx', - }, - 'application/x-7z-compressed' : { - desc : '7-Zip Archive', - sig : '377abcaf271c', - offset : 0, - archiveHandler : '7Zip', - }, + 'application/x-arj' : { + desc : 'ARJ Archive', + sig : '60ea', + offset : 0, + archiveHandler : 'Arj', + }, + 'application/x-rar-compressed' : { + desc : 'RAR Archive', + sig : '526172211a0700', + offset : 0, + archiveHandler : 'Rar', + }, + 'application/gzip' : { + desc : 'Gzip Archive', + sig : '1f8b', + offset : 0, + archiveHandler : 'TarGz', + }, + // :TODO: application/x-bzip + 'application/x-bzip2' : { + desc : 'BZip2 Archive', + sig : '425a68', + offset : 0, + archiveHandler : '7Zip', + }, + 'application/x-lzh-compressed' : { + desc : 'LHArc Archive', + sig : '2d6c68', + offset : 2, + archiveHandler : 'Lha', + }, + 'application/x-lzx' : { + desc : 'LZX Archive', + sig : '4c5a5800', + offset : 0, + archiveHandler : 'Lzx', + }, + 'application/x-7z-compressed' : { + desc : '7-Zip Archive', + sig : '377abcaf271c', + offset : 0, + archiveHandler : '7Zip', + }, - // - // Generics that need further mapping - // - 'application/octet-stream' : [ - { - desc : 'Amiga DISKMASHER', - sig : '444d5321', // DMS! - ext : '.dms', - shortDescUtil : 'XDMS2Desc', - longDescUtil : 'XDMS2LongDesc', - } - ] - }, + // + // Generics that need further mapping + // + 'application/octet-stream' : [ + { + desc : 'Amiga DISKMASHER', + sig : '444d5321', // DMS! + ext : '.dms', + shortDescUtil : 'XDMS2Desc', + longDescUtil : 'XDMS2LongDesc', + } + ] + }, - archives : { - archivers : { - '7Zip' : { - compress : { - cmd : '7za', - args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], - }, - decompress : { - cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? - }, - list : { - cmd : '7za', - args : [ 'l', '{archivePath}' ], - entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', - }, - extract : { - cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], - }, - }, + archives : { + archivers : { + '7Zip' : { + compress : { + cmd : '7za', + args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], + }, + decompress : { + cmd : '7za', + args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? + }, + list : { + cmd : '7za', + args : [ 'l', '{archivePath}' ], + entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : '7za', + args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], + }, + }, - Lha : { - // - // 'lha' command can be obtained from: - // * apt-get: lhasa - // - // (compress not currently supported) - // - decompress : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}' ], - }, - list : { - cmd : 'lha', - args : [ '-l', '{archivePath}' ], - entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', - }, - extract : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] - } - }, + Lha : { + // + // 'lha' command can be obtained from: + // * apt-get: lhasa + // + // (compress not currently supported) + // + decompress : { + cmd : 'lha', + args : [ '-efw={extractPath}', '{archivePath}' ], + }, + list : { + cmd : 'lha', + args : [ '-l', '{archivePath}' ], + entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', + }, + extract : { + cmd : 'lha', + args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] + } + }, - Lzx : { - // - // 'unlzx' command can be obtained from: - // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) - // * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html - // * Source: http://xavprods.free.fr/lzx/ - // - decompress : { - cmd : 'unlzx', - // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first - args : [ '-x', '{archivePath}' ], - }, - list : { - cmd : 'unlzx', - args : [ '-v', '{archivePath}' ], - entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', - } - }, + Lzx : { + // + // 'unlzx' command can be obtained from: + // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) + // * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html + // * Source: http://xavprods.free.fr/lzx/ + // + decompress : { + cmd : 'unlzx', + // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first + args : [ '-x', '{archivePath}' ], + }, + list : { + cmd : 'unlzx', + args : [ '-v', '{archivePath}' ], + entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', + } + }, - Arj : { - // - // 'arj' command can be obtained from: - // * apt-get: arj - // - decompress : { - cmd : 'arj', - args : [ 'x', '{archivePath}', '{extractPath}' ], - }, - list : { - cmd : 'arj', - args : [ 'l', '{archivePath}' ], - entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', - entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } - fileName : 1, - byteSize : 2, - } - }, - extract : { - cmd : 'arj', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], - } - }, + Arj : { + // + // 'arj' command can be obtained from: + // * apt-get: arj + // + decompress : { + cmd : 'arj', + args : [ 'x', '{archivePath}', '{extractPath}' ], + }, + list : { + cmd : 'arj', + args : [ 'l', '{archivePath}' ], + entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', + entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } + fileName : 1, + byteSize : 2, + } + }, + extract : { + cmd : 'arj', + args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], + } + }, - Rar : { - decompress : { - cmd : 'unrar', - args : [ 'x', '{archivePath}', '{extractPath}' ], - }, - list : { - cmd : 'unrar', - args : [ 'l', '{archivePath}' ], - entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', - }, - extract : { - cmd : 'unrar', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], - } - }, + Rar : { + decompress : { + cmd : 'unrar', + args : [ 'x', '{archivePath}', '{extractPath}' ], + }, + list : { + cmd : 'unrar', + args : [ 'l', '{archivePath}' ], + entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', + }, + extract : { + cmd : 'unrar', + 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}' ], - } - } - }, - }, + 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}' ], + } + } + }, + }, - fileTransferProtocols : { - // - // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ - // - zmodem8kSexyz : { - name : 'ZModem 8k (SEXYZ)', - type : 'external', - sort : 1, - external : { - // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems - sendCmd : 'sexyz', - sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], - recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], - } - }, + fileTransferProtocols : { + // + // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ + // + zmodem8kSexyz : { + name : 'ZModem 8k (SEXYZ)', + type : 'external', + sort : 1, + external : { + // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems + sendCmd : 'sexyz', + sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], + recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], + } + }, - xmodemSexyz : { - name : 'XModem (SEXYZ)', - type : 'external', - sort : 3, - external : { - sendCmd : 'sexyz', - sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ] - } - }, + xmodemSexyz : { + name : 'XModem (SEXYZ)', + type : 'external', + sort : 3, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ] + } + }, - ymodemSexyz : { - name : 'YModem (SEXYZ)', - type : 'external', - sort : 4, - external : { - sendCmd : 'sexyz', - sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', 'ry', '{uploadDir}' ], - } - }, + ymodemSexyz : { + name : 'YModem (SEXYZ)', + type : 'external', + sort : 4, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', 'ry', '{uploadDir}' ], + } + }, - zmodem8kSz : { - name : 'ZModem 8k', - type : 'external', - sort : 2, - external : { - sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" - sendArgs : [ - // :TODO: try -q - '--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}' - ], - recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" - recvArgs : [ - '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} - ], - // :TODO: can we not just use --escape ? - escapeTelnet : true, // set to true to escape Telnet codes such as IAC - } - } - }, + zmodem8kSz : { + name : 'ZModem 8k', + type : 'external', + sort : 2, + external : { + sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + sendArgs : [ + // :TODO: try -q + '--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}' + ], + recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + recvArgs : [ + '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} + ], + // :TODO: can we not just use --escape ? + escapeTelnet : true, // set to true to escape Telnet codes such as IAC + } + } + }, - messageAreaDefaults : { - // - // The following can be override per-area as well - // - maxMessages : 1024, // 0 = unlimited - maxAgeDays : 0, // 0 = unlimited - }, + messageAreaDefaults : { + // + // The following can be override per-area as well + // + maxMessages : 1024, // 0 = unlimited + maxAgeDays : 0, // 0 = unlimited + }, - messageConferences : { - system_internal : { - name : 'System Internal', - desc : 'Built in conference for private messages, bulletins, etc.', + messageConferences : { + system_internal : { + name : 'System Internal', + desc : 'Built in conference for private messages, bulletins, etc.', - areas : { - private_mail : { - name : 'Private Mail', - desc : 'Private user to user mail/email', - maxExternalSentAgeDays : 30, // max external "outbox" item age - }, + areas : { + private_mail : { + name : 'Private Mail', + desc : 'Private user to user mail/email', + maxExternalSentAgeDays : 30, // max external "outbox" item age + }, - local_bulletin : { - name : 'System Bulletins', - desc : 'Bulletin messages for all users', - } - } - } - }, + local_bulletin : { + name : 'System Bulletins', + desc : 'Bulletin messages for all users', + } + } + } + }, - scannerTossers : { - ftn_bso : { - paths : { - outbound : paths.join(__dirname, './../mail/ftn_out/'), - inbound : paths.join(__dirname, './../mail/ftn_in/'), - secInbound : paths.join(__dirname, './../mail/ftn_secin/'), - reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. - //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), - // set 'retain' to a valid path to keep good pkt files - }, + scannerTossers : { + ftn_bso : { + paths : { + outbound : paths.join(__dirname, './../mail/ftn_out/'), + inbound : paths.join(__dirname, './../mail/ftn_in/'), + secInbound : paths.join(__dirname, './../mail/ftn_secin/'), + reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. + //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), + // set 'retain' to a valid path to keep good pkt files + }, - // - // Packet and (ArcMail) bundle target sizes are just that: targets. - // Actual sizes may be slightly larger when we must place a full - // PKT contents *somewhere* - // - packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt - 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 + // + // Packet and (ArcMail) bundle target sizes are just that: targets. + // Actual sizes may be slightly larger when we must place a full + // PKT contents *somewhere* + // + packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt + 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 : { - secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) - uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) - allowReplace : false, // use "Replaces" TIC field - descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc - } - } - }, + tic : { + secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) + uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) + allowReplace : false, // use "Replaces" TIC field + descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc + } + } + }, - fileBase: { - // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: - areaStoragePrefix : paths.join(__dirname, './../file_base/'), + fileBase: { + // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: + areaStoragePrefix : paths.join(__dirname, './../file_base/'), - maxDescFileByteSize : 471859, // ~1/4 MB - maxDescLongFileByteSize : 524288, // 1/2 MB + maxDescFileByteSize : 471859, // ~1/4 MB + maxDescLongFileByteSize : 524288, // 1/2 MB - fileNamePatterns: { - // These are NOT case sensitive - // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ - // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. - desc : [ - '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' // eslint-disable-line no-useless-escape - ], + fileNamePatterns: { + // These are NOT case sensitive + // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ + // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. + desc : [ + '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' // eslint-disable-line no-useless-escape + ], - // common README filename - https://en.wikipedia.org/wiki/README - descLong : [ - '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' // eslint-disable-line no-useless-escape - ], - }, + // common README filename - https://en.wikipedia.org/wiki/README + descLong : [ + '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' // eslint-disable-line no-useless-escape + ], + }, - yearEstPatterns: [ - // - // Patterns should produce the year in the first submatch. - // The extracted year may be YY or YYYY - // - '\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ... - '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ... - '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ... - '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... - //'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc. - //"\\b('[1789][0-9])\\b", // eslint-disable-line quotes - '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', - '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 - '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority - '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries - '\\b\'([17-9][0-9])\\b', // '95, '17, ... - // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. - ], + yearEstPatterns: [ + // + // Patterns should produce the year in the first submatch. + // The extracted year may be YY or YYYY + // + '\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ... + '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... + //'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc. + //"\\b('[1789][0-9])\\b", // eslint-disable-line quotes + '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', + '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 + '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority + '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries + '\\b\'([17-9][0-9])\\b', // '95, '17, ... + // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. + ], - web : { - path : '/f/', - routePath : '/f/[a-zA-Z0-9]+$', - expireMinutes : 1440, // 1 day - }, + web : { + path : '/f/', + routePath : '/f/[a-zA-Z0-9]+$', + expireMinutes : 1440, // 1 day + }, - // - // File area storage location tag/value pairs. - // Non-absolute paths are relative to |areaStoragePrefix|. - // - storageTags : { - sys_msg_attach : 'sys_msg_attach', - sys_temp_download : 'sys_temp_download', - }, + // + // File area storage location tag/value pairs. + // Non-absolute paths are relative to |areaStoragePrefix|. + // + storageTags : { + sys_msg_attach : 'sys_msg_attach', + sys_temp_download : 'sys_temp_download', + }, - areas: { - system_message_attachment : { - name : 'System Message Attachments', - desc : 'File attachments to messages', - storageTags : [ 'sys_msg_attach' ], - }, + areas: { + system_message_attachment : { + name : 'System Message Attachments', + desc : 'File attachments to messages', + storageTags : [ 'sys_msg_attach' ], + }, - system_temporary_download : { - name : 'System Temporary Downloads', - desc : 'Temporary downloadables', - storageTags : [ 'sys_temp_download' ], - } - } - }, + system_temporary_download : { + name : 'System Temporary Downloads', + desc : 'Temporary downloadables', + storageTags : [ 'sys_temp_download' ], + } + } + }, - eventScheduler : { + eventScheduler : { - events : { - trimMessageAreas : { - // may optionally use [or ]@watch:/path/to/file - schedule : 'every 24 hours', + events : { + trimMessageAreas : { + // may optionally use [or ]@watch:/path/to/file + schedule : 'every 24 hours', - // action: - // - @method:path/to/module.js:theMethodName - // (path is relative to engima base dir) - // - // - @execute:/path/to/something/executable.sh - // - action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', - }, + // action: + // - @method:path/to/module.js:theMethodName + // (path is relative to engima base dir) + // + // - @execute:/path/to/something/executable.sh + // + action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + }, - updateFileAreaStats : { - schedule : 'every 1 hours', - action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', - }, + updateFileAreaStats : { + schedule : 'every 1 hours', + action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + }, - forgotPasswordMaintenance : { - schedule : 'every 24 hours', - action : '@method:core/web_password_reset.js:performMaintenanceTask', - args : [ '24 hours' ] // items older than this will be removed - }, + forgotPasswordMaintenance : { + schedule : 'every 24 hours', + action : '@method:core/web_password_reset.js:performMaintenanceTask', + args : [ '24 hours' ] // items older than this will be removed + }, - // - // Enable the following entry in your config.hjson to periodically create/update - // DESCRIPT.ION files for your file base - // - /* + // + // Enable the following entry in your config.hjson to periodically create/update + // DESCRIPT.ION files for your file base + // + /* updateDescriptIonFiles : { schedule : 'on the last day of the week', action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', } */ - } - }, + } + }, - misc : { - preAuthIdleLogoutSeconds : 60 * 3, // 3m - idleLogoutSeconds : 60 * 6, // 6m - }, + misc : { + preAuthIdleLogoutSeconds : 60 * 3, // 3m + idleLogoutSeconds : 60 * 6, // 6m + }, - logging : { - level : 'debug', + logging : { + level : 'debug', - rotatingFile : { // set to 'disabled' or false to disable - type : 'rotating-file', - fileName : 'enigma-bbs.log', - period : '1d', - count : 3, - level : 'debug', - } + rotatingFile : { // set to 'disabled' or false to disable + type : 'rotating-file', + fileName : 'enigma-bbs.log', + period : '1d', + count : 3, + level : 'debug', + } - // :TODO: syslog - https://github.com/mcavage/node-bunyan-syslog - }, + // :TODO: syslog - https://github.com/mcavage/node-bunyan-syslog + }, - debug : { - assertsEnabled : false, - } - }; + debug : { + assertsEnabled : false, + } + }; } diff --git a/core/config_cache.js b/core/config_cache.js index 4a1d1c5a..15143efc 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -9,64 +9,64 @@ const sane = require('sane'); module.exports = new class ConfigCache { - constructor() { - this.cache = new Map(); // path->parsed config - } + constructor() { + this.cache = new Map(); // path->parsed config + } - getConfigWithOptions(options, cb) { - const cached = this.cache.has(options.filePath); + getConfigWithOptions(options, cb) { + const cached = this.cache.has(options.filePath); - if(options.forceReCache || !cached) { - this.recacheConfigFromFile(options.filePath, (err, config) => { - if(!err && !cached) { - if(!options.noWatch) { - const watcher = sane( - paths.dirname(options.filePath), - { - glob : `**/${paths.basename(options.filePath)}` - } - ); + if(options.forceReCache || !cached) { + this.recacheConfigFromFile(options.filePath, (err, config) => { + if(!err && !cached) { + if(!options.noWatch) { + const watcher = sane( + paths.dirname(options.filePath), + { + glob : `**/${paths.basename(options.filePath)}` + } + ); - watcher.on('change', (fileName, fileRoot) => { - require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching'); + watcher.on('change', (fileName, fileRoot) => { + require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching'); - this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { - if(!err) { - if(options.callback) { - options.callback( { fileName, fileRoot } ); - } - } - }); - }); - } - } - return cb(err, config, true); - }); - } else { - return cb(null, this.cache.get(options.filePath), false); - } - } + this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { + if(!err) { + if(options.callback) { + options.callback( { fileName, fileRoot } ); + } + } + }); + }); + } + } + return cb(err, config, true); + }); + } else { + return cb(null, this.cache.get(options.filePath), false); + } + } - getConfig(filePath, cb) { - return this.getConfigWithOptions( { filePath }, cb); - } + getConfig(filePath, cb) { + return this.getConfigWithOptions( { filePath }, cb); + } - recacheConfigFromFile(path, cb) { - fs.readFile(path, { encoding : 'utf-8' }, (err, data) => { - if(err) { - return cb(err); - } + recacheConfigFromFile(path, cb) { + fs.readFile(path, { encoding : 'utf-8' }, (err, data) => { + if(err) { + return cb(err); + } - let parsed; - try { - parsed = hjson.parse(data); - this.cache.set(path, parsed); - } catch(e) { - require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); - return cb(e); - } + let parsed; + try { + parsed = hjson.parse(data); + this.cache.set(path, parsed); + } catch(e) { + require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); + return cb(e); + } - return cb(null, parsed); - }); - } + return cb(null, parsed); + }); + } }; diff --git a/core/config_util.js b/core/config_util.js index 4b7ce5ed..1c61162c 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -13,54 +13,54 @@ exports.init = init; exports.getFullConfig = getFullConfig; function getConfigPath(filePath) { - // |filePath| is assumed to be in the config path if it's only a file name - if('.' === paths.dirname(filePath)) { - filePath = paths.join(Config().paths.config, filePath); - } - return filePath; + // |filePath| is assumed to be in the config path if it's only a file name + if('.' === paths.dirname(filePath)) { + filePath = paths.join(Config().paths.config, filePath); + } + return filePath; } function init(cb) { - // pre-cache menu.hjson and prompt.hjson + establish events - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === getConfigPath(Config().general.menuFile)) { - Events.emit(Events.getSystemEvents().MenusChanged); - } else if(reCachedPath === getConfigPath(Config().general.promptFile)) { - Events.emit(Events.getSystemEvents().PromptsChanged); - } - }; + // pre-cache menu.hjson and prompt.hjson + establish events + const changed = ( { fileName, fileRoot } ) => { + const reCachedPath = paths.join(fileRoot, fileName); + if(reCachedPath === getConfigPath(Config().general.menuFile)) { + Events.emit(Events.getSystemEvents().MenusChanged); + } else if(reCachedPath === getConfigPath(Config().general.promptFile)) { + Events.emit(Events.getSystemEvents().PromptsChanged); + } + }; - const config = Config(); - async.series( - [ - function menu(callback) { - return ConfigCache.getConfigWithOptions( - { - filePath : getConfigPath(config.general.menuFile), - callback : changed, - }, - callback - ); - }, - function prompt(callback) { - return ConfigCache.getConfigWithOptions( - { - filePath : getConfigPath(config.general.promptFile), - callback : changed, - }, - callback - ); - } - ], - err => { - return cb(err); - } - ); + const config = Config(); + async.series( + [ + function menu(callback) { + return ConfigCache.getConfigWithOptions( + { + filePath : getConfigPath(config.general.menuFile), + callback : changed, + }, + callback + ); + }, + function prompt(callback) { + return ConfigCache.getConfigWithOptions( + { + filePath : getConfigPath(config.general.promptFile), + callback : changed, + }, + callback + ); + } + ], + err => { + return cb(err); + } + ); } function getFullConfig(filePath, cb) { - ConfigCache.getConfig(getConfigPath(filePath), (err, config) => { - return cb(err, config); - }); + ConfigCache.getConfig(getConfigPath(filePath), (err, config) => { + return cb(err, config); + }); } diff --git a/core/connect.js b/core/connect.js index 51ee4e37..86cf3833 100644 --- a/core/connect.js +++ b/core/connect.js @@ -11,177 +11,177 @@ const async = require('async'); exports.connectEntry = connectEntry; function ansiDiscoverHomePosition(client, cb) { - // - // We want to find the home position. ANSI-BBS and most terminals - // utilize 1,1 as home. However, some terminals such as ConnectBot - // think of home as 0,0. If this is the case, we need to offset - // our positioning to accomodate for such. - // - const done = function(err) { - client.removeListener('cursor position report', cprListener); - clearTimeout(giveUpTimer); - return cb(err); - }; + // + // We want to find the home position. ANSI-BBS and most terminals + // utilize 1,1 as home. However, some terminals such as ConnectBot + // think of home as 0,0. If this is the case, we need to offset + // our positioning to accomodate for such. + // + const done = function(err) { + client.removeListener('cursor position report', cprListener); + clearTimeout(giveUpTimer); + return cb(err); + }; - const cprListener = function(pos) { - const h = pos[0]; - const w = pos[1]; + const cprListener = function(pos) { + const h = pos[0]; + const w = pos[1]; - // - // We expect either 0,0, or 1,1. Anything else will be filed as bad data - // - if(h > 1 || w > 1) { - client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); - return done(new Error('Home position CPR expected to be 0,0, or 1,1')); - } + // + // We expect either 0,0, or 1,1. Anything else will be filed as bad data + // + if(h > 1 || w > 1) { + client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); + return done(new Error('Home position CPR expected to be 0,0, or 1,1')); + } - if(0 === h & 0 === w) { - // - // Store a CPR offset in the client. All CPR's from this point on will offset by this amount - // - client.log.info('Setting CPR offset to 1'); - client.cprOffset = 1; - } + if(0 === h & 0 === w) { + // + // Store a CPR offset in the client. All CPR's from this point on will offset by this amount + // + client.log.info('Setting CPR offset to 1'); + client.cprOffset = 1; + } - return done(null); - }; + return done(null); + }; - client.once('cursor position report', cprListener); + client.once('cursor position report', cprListener); - const giveUpTimer = setTimeout( () => { - return done(new Error('Giving up on home position CPR')); - }, 3000); // 3s + const giveUpTimer = setTimeout( () => { + return done(new Error('Giving up on home position CPR')); + }, 3000); // 3s - client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos + client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos } function ansiQueryTermSizeIfNeeded(client, cb) { - if(client.term.termHeight > 0 || client.term.termWidth > 0) { - return cb(null); - } + if(client.term.termHeight > 0 || client.term.termWidth > 0) { + return cb(null); + } - const done = function(err) { - client.removeListener('cursor position report', cprListener); - clearTimeout(giveUpTimer); - return cb(err); - }; + const done = function(err) { + client.removeListener('cursor position report', cprListener); + clearTimeout(giveUpTimer); + return cb(err); + }; - const cprListener = function(pos) { - // - // If we've already found out, disregard - // - if(client.term.termHeight > 0 || client.term.termWidth > 0) { - return done(null); - } + const cprListener = function(pos) { + // + // If we've already found out, disregard + // + if(client.term.termHeight > 0 || client.term.termWidth > 0) { + return done(null); + } - const h = pos[0]; - const w = pos[1]; + const h = pos[0]; + const w = pos[1]; - // - // Netrunner for example gives us 1x1 here. Not really useful. Ignore - // values that seem obviously bad. - // - if(h < 10 || w < 10) { - client.log.warn( - { height : h, width : w }, - 'Ignoring ANSI CPR screen size query response due to very small values'); - return done(new Error('Term size <= 10 considered invalid')); - } + // + // Netrunner for example gives us 1x1 here. Not really useful. Ignore + // values that seem obviously bad. + // + if(h < 10 || w < 10) { + client.log.warn( + { height : h, width : w }, + 'Ignoring ANSI CPR screen size query response due to very small values'); + return done(new Error('Term size <= 10 considered invalid')); + } - client.term.termHeight = h; - client.term.termWidth = w; + client.term.termHeight = h; + client.term.termWidth = w; - client.log.debug( - { - termWidth : client.term.termWidth, - termHeight : client.term.termHeight, - source : 'ANSI CPR' - }, - 'Window size updated' - ); + client.log.debug( + { + termWidth : client.term.termWidth, + termHeight : client.term.termHeight, + source : 'ANSI CPR' + }, + 'Window size updated' + ); - return done(null); - }; + return done(null); + }; - client.once('cursor position report', cprListener); + client.once('cursor position report', cprListener); - // give up after 2s - const giveUpTimer = setTimeout( () => { - return done(new Error('No term size established by CPR within timeout')); - }, 2000); + // give up after 2s + const giveUpTimer = setTimeout( () => { + return done(new Error('No term size established by CPR within timeout')); + }, 2000); - // Start the process: Query for CPR - client.term.rawWrite(ansi.queryScreenSize()); + // Start the process: Query for CPR + client.term.rawWrite(ansi.queryScreenSize()); } function prepareTerminal(term) { - term.rawWrite(ansi.normal()); - //term.rawWrite(ansi.disableVT100LineWrapping()); - // :TODO: set xterm stuff -- see x84/others + term.rawWrite(ansi.normal()); + //term.rawWrite(ansi.disableVT100LineWrapping()); + // :TODO: set xterm stuff -- see x84/others } function displayBanner(term) { - // note: intentional formatting: - term.pipeWrite(` + // note: intentional formatting: + term.pipeWrite(` |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN |06Copyright (c) 2014-2018 Bryan Ashby |14- |12http://l33t.codes/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |00` - ); + ); } function connectEntry(client, nextMenu) { - const term = client.term; + const term = client.term; - async.series( - [ - function basicPrepWork(callback) { - term.rawWrite(ansi.queryDeviceAttributes(0)); - return callback(null); - }, - function discoverHomePosition(callback) { - ansiDiscoverHomePosition(client, () => { - // :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required - return callback(null); // we try to continue anyway - }); - }, - function queryTermSizeByNonStandardAnsi(callback) { - ansiQueryTermSizeIfNeeded(client, err => { - if(err) { - // - // Check again; We may have got via NAWS/similar before CPR completed. - // - if(0 === term.termHeight || 0 === term.termWidth) { - // - // We still don't have something good for term height/width. - // Default to DOS size 80x25. - // - // :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? - client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!'); + async.series( + [ + function basicPrepWork(callback) { + term.rawWrite(ansi.queryDeviceAttributes(0)); + return callback(null); + }, + function discoverHomePosition(callback) { + ansiDiscoverHomePosition(client, () => { + // :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required + return callback(null); // we try to continue anyway + }); + }, + function queryTermSizeByNonStandardAnsi(callback) { + ansiQueryTermSizeIfNeeded(client, err => { + if(err) { + // + // Check again; We may have got via NAWS/similar before CPR completed. + // + if(0 === term.termHeight || 0 === term.termWidth) { + // + // We still don't have something good for term height/width. + // Default to DOS size 80x25. + // + // :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? + client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!'); - term.termHeight = 25; - term.termWidth = 80; - } - } + term.termHeight = 25; + term.termWidth = 80; + } + } - return callback(null); - }); - }, - ], - () => { - prepareTerminal(term); + return callback(null); + }); + }, + ], + () => { + prepareTerminal(term); - // - // Always show an ENiGMA½ banner - // - displayBanner(term); + // + // Always show an ENiGMA½ banner + // + displayBanner(term); - // fire event - Events.emit(Events.getSystemEvents().TermDetected, { client : client } ); + // fire event + Events.emit(Events.getSystemEvents().TermDetected, { client : client } ); - setTimeout( () => { - return client.menuStack.goto(nextMenu); - }, 500); - } - ); + setTimeout( () => { + return client.menuStack.goto(nextMenu); + }, 500); + } + ); } diff --git a/core/crc.js b/core/crc.js index d110807b..d7974c66 100644 --- a/core/crc.js +++ b/core/crc.js @@ -2,90 +2,90 @@ 'use strict'; const CRC32_TABLE = new Int32Array([ - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, - 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, - 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, - 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, - 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, - 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, - 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, - 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, - 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, - 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, - 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, - 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, - 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, - 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, - 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, - 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, - 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, - 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, - 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, - 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, - 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, - 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, - 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, - 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, - 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, - 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, - 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, - 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, - 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, - 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, - 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, - 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, - 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, - 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, + 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, + 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, + 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, + 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, + 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, + 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, + 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, + 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, + 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, + 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, + 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, + 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, + 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, + 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, + 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, + 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, + 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, + 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, + 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, + 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, + 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, + 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, + 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, + 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d ]); exports.CRC32 = class CRC32 { - constructor() { - this.crc = -1; - } + constructor() { + this.crc = -1; + } - update(input) { - input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); - return input.length > 10240 ? this.update_8(input) : this.update_4(input); - } + update(input) { + input = Buffer.isBuffer(input) ? input : Buffer.from(input, 'binary'); + return input.length > 10240 ? this.update_8(input) : this.update_4(input); + } - update_4(input) { - const len = input.length - 3; - let i = 0; + update_4(input) { + const len = input.length - 3; + let i = 0; - for(i = 0; i < len;) { - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - } - while(i < len + 3) { - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; - } - } + for(i = 0; i < len;) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + } + while(i < len + 3) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; + } + } - update_8(input) { - const len = input.length - 7; - let i = 0; + update_8(input) { + const len = input.length - 7; + let i = 0; - for(i = 0; i < len;) { - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - } - while(i < len + 7) { - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; - } - } + for(i = 0; i < len;) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; + } + while(i < len + 7) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; + } + } - finalize() { - return (this.crc ^ (-1)) >>> 0; - } + finalize() { + return (this.crc ^ (-1)) >>> 0; + } }; diff --git a/core/database.js b/core/database.js index 0998f738..3ce2e031 100644 --- a/core/database.js +++ b/core/database.js @@ -25,98 +25,98 @@ exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; function getTransactionDatabase(db) { - return sqlite3Trans.wrap(db); + return sqlite3Trans.wrap(db); } function getDatabasePath(name) { - return paths.join(conf.config.paths.db, `${name}.sqlite3`); + return paths.join(conf.config.paths.db, `${name}.sqlite3`); } function getModDatabasePath(moduleInfo, suffix) { - // - // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) - // We expect that moduleInfo defines packageName which will be the base of the modules - // filename. An optional suffix may be supplied as well. - // - const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; + // + // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) + // We expect that moduleInfo defines packageName which will be the base of the modules + // filename. An optional suffix may be supplied as well. + // + const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; - assert(_.isObject(moduleInfo)); - assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); + assert(_.isObject(moduleInfo)); + assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); - let full = moduleInfo.packageName; - if(suffix) { - full += `.${suffix}`; - } + let full = moduleInfo.packageName; + if(suffix) { + full += `.${suffix}`; + } - assert( - (full.split('.').length > 1 && HOST_RE.test(full)), - 'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'); + assert( + (full.split('.').length > 1 && HOST_RE.test(full)), + 'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'); - return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`); + return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`); } function getISOTimestampString(ts) { - ts = ts || moment(); - return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + ts = ts || moment(); + return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } function sanatizeString(s) { - return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex - switch (c) { - case '\0' : return '\\0'; - case '\x08' : return '\\b'; - case '\x09' : return '\\t'; - case '\x1a' : return '\\z'; - case '\n' : return '\\n'; - case '\r' : return '\\r'; + return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex + switch (c) { + case '\0' : return '\\0'; + case '\x08' : return '\\b'; + case '\x09' : return '\\t'; + case '\x1a' : return '\\z'; + case '\n' : return '\\n'; + case '\r' : return '\\r'; - case '"' : - case '\'' : - return `${c}${c}`; + case '"' : + case '\'' : + return `${c}${c}`; - case '\\' : - case '%' : - return `\\${c}`; - } - }); + case '\\' : + case '%' : + return `\\${c}`; + } + }); } function initializeDatabases(cb) { - async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { - dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { - if(err) { - return cb(err); - } + async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { + dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { + if(err) { + return cb(err); + } - dbs[dbName].serialize( () => { - DB_INIT_TABLE[dbName]( () => { - return next(null); - }); - }); - })); - }, err => { - return cb(err); - }); + dbs[dbName].serialize( () => { + DB_INIT_TABLE[dbName]( () => { + return next(null); + }); + }); + })); + }, err => { + return cb(err); + }); } function enableForeignKeys(db) { - db.run('PRAGMA foreign_keys = ON;'); + db.run('PRAGMA foreign_keys = ON;'); } const DB_INIT_TABLE = { - system : (cb) => { - enableForeignKeys(dbs.system); + system : (cb) => { + enableForeignKeys(dbs.system); - // Various stat/event logging - see stat_log.js - dbs.system.run( - `CREATE TABLE IF NOT EXISTS system_stat ( + // Various stat/event logging - see stat_log.js + dbs.system.run( + `CREATE TABLE IF NOT EXISTS system_stat ( stat_name VARCHAR PRIMARY KEY NOT NULL, stat_value VARCHAR NOT NULL );` - ); + ); - dbs.system.run( - `CREATE TABLE IF NOT EXISTS system_event_log ( + dbs.system.run( + `CREATE TABLE IF NOT EXISTS system_event_log ( id INTEGER PRIMARY KEY, timestamp DATETIME NOT NULL, log_name VARCHAR NOT NULL, @@ -124,10 +124,10 @@ const DB_INIT_TABLE = { UNIQUE(timestamp, log_name) );` - ); + ); - dbs.system.run( - `CREATE TABLE IF NOT EXISTS user_event_log ( + dbs.system.run( + `CREATE TABLE IF NOT EXISTS user_event_log ( id INTEGER PRIMARY KEY, timestamp DATETIME NOT NULL, user_id INTEGER NOT NULL, @@ -136,58 +136,58 @@ const DB_INIT_TABLE = { UNIQUE(timestamp, user_id, log_name) );` - ); + ); - return cb(null); - }, + return cb(null); + }, - user : (cb) => { - enableForeignKeys(dbs.user); + user : (cb) => { + enableForeignKeys(dbs.user); - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user ( + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY, user_name VARCHAR NOT NULL, UNIQUE(user_name) );` - ); + ); - // :TODO: create FK on delete/etc. + // :TODO: create FK on delete/etc. - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_property ( + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_property ( user_id INTEGER NOT NULL, prop_name VARCHAR NOT NULL, prop_value VARCHAR, UNIQUE(user_id, prop_name), FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE );` - ); + ); - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_group_member ( + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_group_member ( group_name VARCHAR NOT NULL, user_id INTEGER NOT NULL, UNIQUE(group_name, user_id) );` - ); + ); - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_login_history ( + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_login_history ( user_id INTEGER NOT NULL, user_name VARCHAR NOT NULL, timestamp DATETIME NOT NULL );` - ); + ); - return cb(null); - }, + return cb(null); + }, - message : (cb) => { - enableForeignKeys(dbs.message); + message : (cb) => { + enableForeignKeys(dbs.message); - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message ( + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message ( message_id INTEGER PRIMARY KEY, area_tag VARCHAR NOT NULL, message_uuid VARCHAR(36) NOT NULL, @@ -200,47 +200,47 @@ const DB_INIT_TABLE = { view_count INTEGER NOT NULL DEFAULT 0, UNIQUE(message_uuid) );` - ); + ); - dbs.message.run( - `CREATE INDEX IF NOT EXISTS message_by_area_tag_index + dbs.message.run( + `CREATE INDEX IF NOT EXISTS message_by_area_tag_index ON message (area_tag);` - ); + ); - dbs.message.run( - `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 ( + dbs.message.run( + `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 ( content="message", subject, message );` - ); + ); - dbs.message.run( - `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN DELETE FROM message_fts WHERE docid=old.rowid; END;` - ); + ); - dbs.message.run( - `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN DELETE FROM message_fts WHERE docid=old.rowid; END;` - ); + ); - dbs.message.run( - `CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); END;` - ); + ); - dbs.message.run( - `CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN + dbs.message.run( + `CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); END;` - ); + ); - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message_meta ( + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message_meta ( message_id INTEGER NOT NULL, meta_category INTEGER NOT NULL, meta_name VARCHAR NOT NULL, @@ -248,11 +248,11 @@ const DB_INIT_TABLE = { UNIQUE(message_id, meta_category, meta_name, meta_value), FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE );` - ); + ); - // :TODO: need SQL to ensure cleaned up if delete from message? - /* + // :TODO: need SQL to ensure cleaned up if delete from message? + /* dbs.message.run( `CREATE TABLE IF NOT EXISTS hash_tag ( hash_tag_id INTEGER PRIMARY KEY, @@ -270,33 +270,33 @@ const DB_INIT_TABLE = { ); */ - dbs.message.run( - `CREATE TABLE IF NOT EXISTS user_message_area_last_read ( + dbs.message.run( + `CREATE TABLE IF NOT EXISTS user_message_area_last_read ( user_id INTEGER NOT NULL, area_tag VARCHAR NOT NULL, message_id INTEGER NOT NULL, UNIQUE(user_id, area_tag) );` - ); + ); - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message_area_last_scan ( + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message_area_last_scan ( scan_toss VARCHAR NOT NULL, area_tag VARCHAR NOT NULL, message_id INTEGER NOT NULL, UNIQUE(scan_toss, area_tag) );` - ); + ); - return cb(null); - }, + return cb(null); + }, - file : (cb) => { - enableForeignKeys(dbs.file); + file : (cb) => { + enableForeignKeys(dbs.file); - dbs.file.run( - // :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system - `CREATE TABLE IF NOT EXISTS file ( + dbs.file.run( + // :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system + `CREATE TABLE IF NOT EXISTS file ( file_id INTEGER PRIMARY KEY, area_tag VARCHAR NOT NULL, file_sha256 VARCHAR NOT NULL, @@ -306,105 +306,105 @@ const DB_INIT_TABLE = { desc_long, /* FTS @ file_fts */ upload_timestamp DATETIME NOT NULL );` - ); + ); - dbs.file.run( - `CREATE INDEX IF NOT EXISTS file_by_area_tag_index + dbs.file.run( + `CREATE INDEX IF NOT EXISTS file_by_area_tag_index ON file (area_tag);` - ); + ); - dbs.file.run( - `CREATE INDEX IF NOT EXISTS file_by_sha256_index + dbs.file.run( + `CREATE INDEX IF NOT EXISTS file_by_sha256_index ON file (file_sha256);` - ); + ); - dbs.file.run( - `CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 ( + dbs.file.run( + `CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 ( content="file", file_name, desc, desc_long );` - ); + ); - dbs.file.run( - `CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN DELETE FROM file_fts WHERE docid=old.rowid; END;` - ); + ); - dbs.file.run( - `CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN DELETE FROM file_fts WHERE docid=old.rowid; END;` - ); + ); - dbs.file.run( - `CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); END;` - ); + ); - dbs.file.run( - `CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN + dbs.file.run( + `CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); END;` - ); + ); - dbs.file.run( - `CREATE TABLE IF NOT EXISTS file_meta ( + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_meta ( file_id INTEGER NOT NULL, meta_name VARCHAR NOT NULL, meta_value VARCHAR NOT NULL, UNIQUE(file_id, meta_name, meta_value), FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE );` - ); + ); - dbs.file.run( - `CREATE TABLE IF NOT EXISTS hash_tag ( + dbs.file.run( + `CREATE TABLE IF NOT EXISTS hash_tag ( hash_tag_id INTEGER PRIMARY KEY, hash_tag VARCHAR NOT NULL, UNIQUE(hash_tag) );` - ); + ); - dbs.file.run( - `CREATE TABLE IF NOT EXISTS file_hash_tag ( + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_hash_tag ( hash_tag_id INTEGER NOT NULL, file_id INTEGER NOT NULL, UNIQUE(hash_tag_id, file_id) );` - ); + ); - dbs.file.run( - `CREATE TABLE IF NOT EXISTS file_user_rating ( + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_user_rating ( file_id INTEGER NOT NULL, user_id INTEGER NOT NULL, rating INTEGER NOT NULL, UNIQUE(file_id, user_id) );` - ); + ); - dbs.file.run( - `CREATE TABLE IF NOT EXISTS file_web_serve ( + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_web_serve ( hash_id VARCHAR NOT NULL PRIMARY KEY, expire_timestamp DATETIME NOT NULL );` - ); + ); - dbs.file.run( - `CREATE TABLE IF NOT EXISTS file_web_serve_batch ( + dbs.file.run( + `CREATE TABLE IF NOT EXISTS file_web_serve_batch ( hash_id VARCHAR NOT NULL, file_id INTEGER NOT NULL, UNIQUE(hash_id, file_id) );` - ); + ); - return cb(null); - } + return cb(null); + } }; \ No newline at end of file diff --git a/core/descript_ion_file.js b/core/descript_ion_file.js index 8f2bc1b3..1ead544f 100644 --- a/core/descript_ion_file.js +++ b/core/descript_ion_file.js @@ -7,66 +7,66 @@ const iconv = require('iconv-lite'); const async = require('async'); module.exports = class DescriptIonFile { - constructor() { - this.entries = new Map(); - } + constructor() { + this.entries = new Map(); + } - get(fileName) { - return this.entries.get(fileName); - } + get(fileName) { + return this.entries.get(fileName); + } - getDescription(fileName) { - const entry = this.get(fileName); - if(entry) { - return entry.desc; - } - } + getDescription(fileName) { + const entry = this.get(fileName); + if(entry) { + return entry.desc; + } + } - static createFromFile(path, cb) { - fs.readFile(path, (err, descData) => { - if(err) { - return cb(err); - } + static createFromFile(path, cb) { + fs.readFile(path, (err, descData) => { + if(err) { + return cb(err); + } - const descIonFile = new DescriptIonFile(); + const descIonFile = new DescriptIonFile(); - // DESCRIPT.ION entries are terminated with a CR and/or LF - const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); + // DESCRIPT.ION entries are terminated with a CR and/or LF + const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); - async.each(lines, (entryData, nextLine) => { - // - // We allow quoted (long) filenames or non-quoted filenames. - // FILENAMEDESC<0x04> - // - const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex - if(!parts) { - return nextLine(null); - } + async.each(lines, (entryData, nextLine) => { + // + // We allow quoted (long) filenames or non-quoted filenames. + // FILENAMEDESC<0x04> + // + const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex + if(!parts) { + return nextLine(null); + } - const fileName = parts[1] || parts[2]; + const fileName = parts[1] || parts[2]; - // - // Un-escape CR/LF's - // - escapped \r and/or \n - // - BBBS style @n - See https://www.bbbs.net/sysop.html - // - const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n'); + // + // Un-escape CR/LF's + // - escapped \r and/or \n + // - BBBS style @n - See https://www.bbbs.net/sysop.html + // + const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n'); - descIonFile.entries.set( - fileName, - { - desc : desc, - programId : parts[4], - programData : parts[5], - } - ); + descIonFile.entries.set( + fileName, + { + desc : desc, + programId : parts[4], + programData : parts[5], + } + ); - return nextLine(null); - }, - () => { - return cb(null, descIonFile); - }); - }); - } + return nextLine(null); + }, + () => { + return cb(null, descIonFile); + }); + }); + } }; diff --git a/core/door.js b/core/door.js index 58c8effa..06a10f60 100644 --- a/core/door.js +++ b/core/door.js @@ -13,137 +13,137 @@ const createServer = require('net').createServer; exports.Door = Door; function Door(client, exeInfo) { - events.EventEmitter.call(this); + events.EventEmitter.call(this); - const self = this; - this.client = client; - this.exeInfo = exeInfo; - this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); - let restored = false; + const self = this; + this.client = client; + this.exeInfo = exeInfo; + this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); + let restored = false; - // - // Members of exeInfo: - // cmd - // args[] - // env{} - // cwd - // io - // encoding - // dropFile - // node - // inhSocket - // + // + // Members of exeInfo: + // cmd + // args[] + // env{} + // cwd + // io + // encoding + // dropFile + // node + // inhSocket + // - this.doorDataHandler = function(data) { - self.client.term.write(decode(data, self.exeInfo.encoding)); - }; + this.doorDataHandler = function(data) { + self.client.term.write(decode(data, self.exeInfo.encoding)); + }; - this.restoreIo = function(piped) { - if(!restored && self.client.term.output) { - self.client.term.output.unpipe(piped); - self.client.term.output.resume(); - restored = true; - } - }; + this.restoreIo = function(piped) { + if(!restored && self.client.term.output) { + self.client.term.output.unpipe(piped); + self.client.term.output.resume(); + restored = true; + } + }; - this.prepareSocketIoServer = function(cb) { - if('socket' === self.exeInfo.io) { - const sockServer = createServer(conn => { + this.prepareSocketIoServer = function(cb) { + if('socket' === self.exeInfo.io) { + const sockServer = createServer(conn => { - sockServer.getConnections( (err, count) => { + sockServer.getConnections( (err, count) => { - // We expect only one connection from our DOOR/emulator/etc. - if(!err && count <= 1) { - self.client.term.output.pipe(conn); + // We expect only one connection from our DOOR/emulator/etc. + if(!err && count <= 1) { + self.client.term.output.pipe(conn); - conn.on('data', self.doorDataHandler); + conn.on('data', self.doorDataHandler); - conn.once('end', () => { - return self.restoreIo(conn); - }); + conn.once('end', () => { + return self.restoreIo(conn); + }); - conn.once('error', err => { - self.client.log.info( { error : err.toString() }, 'Door socket server connection'); - return self.restoreIo(conn); - }); - } - }); - }); + conn.once('error', err => { + self.client.log.info( { error : err.toString() }, 'Door socket server connection'); + return self.restoreIo(conn); + }); + } + }); + }); - sockServer.listen(0, () => { - return cb(null, sockServer); - }); - } else { - return cb(null); - } - }; + sockServer.listen(0, () => { + return cb(null, sockServer); + }); + } else { + return cb(null); + } + }; - this.doorExited = function() { - self.emit('finished'); - }; + this.doorExited = function() { + self.emit('finished'); + }; } require('util').inherits(Door, events.EventEmitter); Door.prototype.run = function() { - const self = this; + const self = this; - this.prepareSocketIoServer( (err, sockServer) => { - if(err) { - this.client.log.warn( { error : err.toString() }, 'Failed executing door'); - return self.doorExited(); - } + this.prepareSocketIoServer( (err, sockServer) => { + if(err) { + this.client.log.warn( { error : err.toString() }, 'Failed executing door'); + return self.doorExited(); + } - // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS - // :TODO: Use .map() here - let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified + // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS + // :TODO: Use .map() here + let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified - for(let i = 0; i < args.length; ++i) { - args[i] = stringFormat(self.exeInfo.args[i], { - dropFile : self.exeInfo.dropFile, - node : self.exeInfo.node.toString(), - srvPort : sockServer ? sockServer.address().port.toString() : '-1', - userId : self.client.user.userId.toString(), - }); - } + for(let i = 0; i < args.length; ++i) { + args[i] = stringFormat(self.exeInfo.args[i], { + dropFile : self.exeInfo.dropFile, + node : self.exeInfo.node.toString(), + srvPort : sockServer ? sockServer.address().port.toString() : '-1', + userId : self.client.user.userId.toString(), + }); + } - const door = pty.spawn(self.exeInfo.cmd, args, { - cols : self.client.term.termWidth, - rows : self.client.term.termHeight, - // :TODO: cwd - env : self.exeInfo.env, - encoding : null, // we want to handle all encoding ourself - }); + const door = pty.spawn(self.exeInfo.cmd, args, { + cols : self.client.term.termWidth, + rows : self.client.term.termHeight, + // :TODO: cwd + env : self.exeInfo.env, + encoding : null, // we want to handle all encoding ourself + }); - if('stdio' === self.exeInfo.io) { - self.client.log.debug('Using stdio for door I/O'); + if('stdio' === self.exeInfo.io) { + self.client.log.debug('Using stdio for door I/O'); - self.client.term.output.pipe(door); + self.client.term.output.pipe(door); - door.on('data', self.doorDataHandler); + door.on('data', self.doorDataHandler); - door.once('close', () => { - return self.restoreIo(door); - }); - } else if('socket' === self.exeInfo.io) { - self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O'); - } + door.once('close', () => { + return self.restoreIo(door); + }); + } else if('socket' === self.exeInfo.io) { + self.client.log.debug( { port : sockServer.address().port }, 'Using temporary socket server for door I/O'); + } - door.once('exit', exitCode => { - self.client.log.info( { exitCode : exitCode }, 'Door exited'); + door.once('exit', exitCode => { + self.client.log.info( { exitCode : exitCode }, 'Door exited'); - if(sockServer) { - sockServer.close(); - } + if(sockServer) { + sockServer.close(); + } - // we may not get a close - if('stdio' === self.exeInfo.io) { - self.restoreIo(door); - } + // we may not get a close + if('stdio' === self.exeInfo.io) { + self.restoreIo(door); + } - door.removeAllListeners(); + door.removeAllListeners(); - return self.doorExited(); - }); - }); + return self.doorExited(); + }); + }); }; diff --git a/core/door_party.js b/core/door_party.js index a64f92c8..3c5b29a7 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -11,121 +11,121 @@ const _ = require('lodash'); const SSHClient = require('ssh2').Client; exports.moduleInfo = { - name : 'DoorParty', - desc : 'DoorParty Access Module', - author : 'NuSkooler', + name : 'DoorParty', + desc : 'DoorParty Access Module', + author : 'NuSkooler', }; exports.getModule = class DoorPartyModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - // establish defaults - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'dp.throwbackbbs.com'; - this.config.sshPort = this.config.sshPort || 2022; - this.config.rloginPort = this.config.rloginPort || 513; - } + // establish defaults + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'dp.throwbackbbs.com'; + this.config.sshPort = this.config.sshPort || 2022; + this.config.rloginPort = this.config.rloginPort || 513; + } - initSequence() { - let clientTerminated; - const self = this; + initSequence() { + let clientTerminated; + const self = this; - async.series( - [ - function validateConfig(callback) { - if(!_.isString(self.config.username)) { - return callback(new Error('Config requires "username"!')); - } - if(!_.isString(self.config.password)) { - return callback(new Error('Config requires "password"!')); - } - if(!_.isString(self.config.bbsTag)) { - return callback(new Error('Config requires "bbsTag"!')); - } - return callback(null); - }, - function establishSecureConnection(callback) { - self.client.term.write(resetScreen()); - self.client.term.write('Connecting to DoorParty, please wait...\n'); + async.series( + [ + function validateConfig(callback) { + if(!_.isString(self.config.username)) { + return callback(new Error('Config requires "username"!')); + } + if(!_.isString(self.config.password)) { + return callback(new Error('Config requires "password"!')); + } + if(!_.isString(self.config.bbsTag)) { + return callback(new Error('Config requires "bbsTag"!')); + } + return callback(null); + }, + function establishSecureConnection(callback) { + self.client.term.write(resetScreen()); + self.client.term.write('Connecting to DoorParty, please wait...\n'); - const sshClient = new SSHClient(); + const sshClient = new SSHClient(); - let pipeRestored = false; - let pipedStream; - const restorePipe = function() { - if(pipedStream && !pipeRestored && !clientTerminated) { - self.client.term.output.unpipe(pipedStream); - self.client.term.output.resume(); - } - }; + let pipeRestored = false; + let pipedStream; + const restorePipe = function() { + if(pipedStream && !pipeRestored && !clientTerminated) { + self.client.term.output.unpipe(pipedStream); + self.client.term.output.resume(); + } + }; - sshClient.on('ready', () => { - // track client termination so we can clean up early - self.client.once('end', () => { - self.client.log.info('Connection ended. Terminating DoorParty connection'); - clientTerminated = true; - sshClient.end(); - }); + sshClient.on('ready', () => { + // track client termination so we can clean up early + self.client.once('end', () => { + self.client.log.info('Connection ended. Terminating DoorParty connection'); + clientTerminated = true; + sshClient.end(); + }); - // establish tunnel for rlogin - sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { - if(err) { - return callback(new Error('Failed to establish tunnel')); - } + // establish tunnel for rlogin + sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { + if(err) { + return callback(new Error('Failed to establish tunnel')); + } - // - // Send rlogin - // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. - // [XA]nuskooler - // - const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; - stream.write(rlogin); + // + // Send rlogin + // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. + // [XA]nuskooler + // + const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; + stream.write(rlogin); - pipedStream = stream; // :TODO: this is hacky... - self.client.term.output.pipe(stream); + pipedStream = stream; // :TODO: this is hacky... + self.client.term.output.pipe(stream); - stream.on('data', d => { - // :TODO: we should just pipe this... - self.client.term.rawWrite(d); - }); + stream.on('data', d => { + // :TODO: we should just pipe this... + self.client.term.rawWrite(d); + }); - stream.on('close', () => { - restorePipe(); - sshClient.end(); - }); - }); - }); + stream.on('close', () => { + restorePipe(); + sshClient.end(); + }); + }); + }); - sshClient.on('error', err => { - self.client.log.info(`DoorParty SSH client error: ${err.message}`); - }); + sshClient.on('error', err => { + self.client.log.info(`DoorParty SSH client error: ${err.message}`); + }); - sshClient.on('close', () => { - restorePipe(); - callback(null); - }); + sshClient.on('close', () => { + restorePipe(); + callback(null); + }); - sshClient.connect( { - host : self.config.host, - port : self.config.sshPort, - username : self.config.username, - password : self.config.password, - }); + sshClient.connect( { + host : self.config.host, + port : self.config.sshPort, + username : self.config.username, + password : self.config.password, + }); - // note: no explicit callback() until we're finished! - } - ], - err => { - if(err) { - self.client.log.warn( { error : err.message }, 'DoorParty error'); - } + // note: no explicit callback() until we're finished! + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'DoorParty error'); + } - // if the client is stil here, go to previous - if(!clientTerminated) { - self.prevMenu(); - } - } - ); - } + // if the client is stil here, go to previous + if(!clientTerminated) { + self.prevMenu(); + } + } + ); + } }; diff --git a/core/download_queue.js b/core/download_queue.js index 0c31b13c..d8617f75 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -7,72 +7,72 @@ const FileEntry = require('./file_entry.js'); const { partition } = require('lodash'); module.exports = class DownloadQueue { - constructor(client) { - this.client = client; + constructor(client) { + this.client = client; - if(!Array.isArray(this.client.user.downloadQueue)) { - if(this.client.user.properties.dl_queue) { - this.loadFromProperty(this.client.user.properties.dl_queue); - } else { - this.client.user.downloadQueue = []; - } - } - } + if(!Array.isArray(this.client.user.downloadQueue)) { + if(this.client.user.properties.dl_queue) { + this.loadFromProperty(this.client.user.properties.dl_queue); + } else { + this.client.user.downloadQueue = []; + } + } + } - get items() { - return this.client.user.downloadQueue; - } + get items() { + return this.client.user.downloadQueue; + } - clear() { - this.client.user.downloadQueue = []; - } + clear() { + this.client.user.downloadQueue = []; + } - toggle(fileEntry, systemFile=false) { - if(this.isQueued(fileEntry)) { - this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); - } else { - this.add(fileEntry, systemFile); - } - } + toggle(fileEntry, systemFile=false) { + if(this.isQueued(fileEntry)) { + this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); + } else { + this.add(fileEntry, systemFile); + } + } - add(fileEntry, systemFile=false) { - this.client.user.downloadQueue.push({ - fileId : fileEntry.fileId, - areaTag : fileEntry.areaTag, - fileName : fileEntry.fileName, - path : fileEntry.filePath, - byteSize : fileEntry.meta.byte_size || 0, - systemFile : systemFile, - }); - } + add(fileEntry, systemFile=false) { + this.client.user.downloadQueue.push({ + fileId : fileEntry.fileId, + areaTag : fileEntry.areaTag, + fileName : fileEntry.fileName, + path : fileEntry.filePath, + byteSize : fileEntry.meta.byte_size || 0, + systemFile : systemFile, + }); + } - removeItems(fileIds) { - if(!Array.isArray(fileIds)) { - fileIds = [ fileIds ]; - } + removeItems(fileIds) { + if(!Array.isArray(fileIds)) { + fileIds = [ fileIds ]; + } - const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); - this.client.user.downloadQueue = remain; - return removed; - } + const [ remain, removed ] = partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); + this.client.user.downloadQueue = remain; + return removed; + } - isQueued(entryOrId) { - if(entryOrId instanceof FileEntry) { - entryOrId = entryOrId.fileId; - } + isQueued(entryOrId) { + if(entryOrId instanceof FileEntry) { + entryOrId = entryOrId.fileId; + } - return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false; - } + return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false; + } - toProperty() { return JSON.stringify(this.client.user.downloadQueue); } + toProperty() { return JSON.stringify(this.client.user.downloadQueue); } - loadFromProperty(prop) { - try { - this.client.user.downloadQueue = JSON.parse(prop); - } catch(e) { - this.client.user.downloadQueue = []; + loadFromProperty(prop) { + try { + this.client.user.downloadQueue = JSON.parse(prop); + } catch(e) { + this.client.user.downloadQueue = []; - this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); - } - } + this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); + } + } }; diff --git a/core/dropfile.js b/core/dropfile.js index bdb3f3d1..7a49cca4 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -23,189 +23,189 @@ exports.DropFile = DropFile; function DropFile(client, fileType) { - var self = this; - this.client = client; - this.fileType = (fileType || 'DORINFO').toUpperCase(); + var self = this; + this.client = client; + this.fileType = (fileType || 'DORINFO').toUpperCase(); - Object.defineProperty(this, 'fullPath', { - get : function() { - return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName); - } - }); + Object.defineProperty(this, 'fullPath', { + get : function() { + return paths.join(Config().paths.dropFiles, ('node' + self.client.node), self.fileName); + } + }); - Object.defineProperty(this, 'fileName', { - get : function() { - return { - DOOR : 'DOOR.SYS', // GAP BBS, many others - DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... - CALLINFO : 'CALLINFO.BBS', // Citadel? - DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... - CHAIN : 'CHAIN.TXT', // WWIV - CURRUSER : 'CURRUSER.BBS', // RyBBS - SFDOORS : 'SFDOORS.DAT', // Spitfire - PCBOARD : 'PCBOARD.SYS', // PCBoard - TRIBBS : 'TRIBBS.SYS', // TriBBS - USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ - JUMPER : 'JUMPER.DAT', // 2AM BBS - SXDOOR : // System/X, dESiRE + Object.defineProperty(this, 'fileName', { + get : function() { + return { + DOOR : 'DOOR.SYS', // GAP BBS, many others + DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... + CALLINFO : 'CALLINFO.BBS', // Citadel? + DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... + CHAIN : 'CHAIN.TXT', // WWIV + CURRUSER : 'CURRUSER.BBS', // RyBBS + SFDOORS : 'SFDOORS.DAT', // Spitfire + PCBOARD : 'PCBOARD.SYS', // PCBoard + TRIBBS : 'TRIBBS.SYS', // TriBBS + USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ + JUMPER : 'JUMPER.DAT', // 2AM BBS + SXDOOR : // System/X, dESiRE 'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'), - INFO : 'INFO.BBS', // Phoenix BBS - }[self.fileType]; - } - }); + INFO : 'INFO.BBS', // Phoenix BBS + }[self.fileType]; + } + }); - Object.defineProperty(this, 'dropFileContents', { - get : function() { - return { - DOOR : self.getDoorSysBuffer(), - DOOR32 : self.getDoor32Buffer(), - DORINFO : self.getDoorInfoDefBuffer(), - }[self.fileType]; - } - }); + Object.defineProperty(this, 'dropFileContents', { + get : function() { + return { + DOOR : self.getDoorSysBuffer(), + DOOR32 : self.getDoor32Buffer(), + DORINFO : self.getDoorInfoDefBuffer(), + }[self.fileType]; + } + }); - this.getDoorInfoFileName = function() { - var x; - var node = self.client.node; - if(10 === node) { - x = 0; - } else if(node < 10) { - x = node; - } else { - x = String.fromCharCode('a'.charCodeAt(0) + (node - 11)); - } - return 'DORINFO' + x + '.DEF'; - }; + this.getDoorInfoFileName = function() { + var x; + var node = self.client.node; + if(10 === node) { + x = 0; + } else if(node < 10) { + x = node; + } else { + x = String.fromCharCode('a'.charCodeAt(0) + (node - 11)); + } + return 'DORINFO' + x + '.DEF'; + }; - this.getDoorSysBuffer = function() { - var up = self.client.user.properties; - var now = moment(); - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + this.getDoorSysBuffer = function() { + var up = self.client.user.properties; + var now = moment(); + var secLevel = self.client.user.getLegacySecurityLevel().toString(); - // :TODO: fix time remaining - // :TODO: fix default protocol -- user prop: transfer_protocol + // :TODO: fix time remaining + // :TODO: fix default protocol -- user prop: transfer_protocol - return iconv.encode( [ - 'COM1:', // "Comm Port - COM0: = LOCAL MODE" - '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) - '8', // "Parity - 7 or 8" - self.client.node.toString(), // "Node Number - 1 to 99" - '57600', // "DTE Rate. Actual BPS rate to use. (kg)" - 'Y', // "Screen Display - Y=On N=Off (Default to Y)" - 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" - 'Y', // "Page Bell - Y=On N=Off (Default to Y)" - 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" - up.real_name || self.client.user.username, // "User Full Name" - up.location || 'Anywhere', // "Calling From" - '123-456-7890', // "Home Phone" - '123-456-7890', // "Work/Data Phone" - 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) - secLevel, // "Security Level" - up.login_count.toString(), // "Total Times On" - now.format('MM/DD/YY'), // "Last Date Called" - '15360', // "Seconds Remaining THIS call (for those that particular)" - '256', // "Minutes Remaining THIS call" - 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" - self.client.term.termHeight.toString(), // "Page Length" - 'N', // "User Mode - Y = Expert, N = Novice" - '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" - '1', // "Conference Exited To DOOR From (G)" - '01/01/99', // "User Expiration Date (mm/dd/yy)" - self.client.user.userId.toString(), // "User File's Record Number" - 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." - // :TODO: fix up, down, etc. form user properties - '0', // "Total Uploads" - '0', // "Total Downloads" - '0', // "Daily Download "K" Total" - '999999', // "Daily Download Max. "K" Limit" - moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" - 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" - 'X:\\GEN\\', // "Path to the GEN directory" - StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" - self.client.user.username, // "Alias name" - '00:05', // "Event time (hh:mm)" (note: wat?) - 'Y', // "If its an error correcting connection (Y/N)" - 'Y', // "ANSI supported & caller using NG mode (Y/N)" - 'Y', // "Use Record Locking (Y/N)" - '7', // "BBS Default Color (Standard IBM color code, ie, 1-15)" - // :TODO: fix minutes here also: - '256', // "Time Credits In Minutes (positive/negative)" - '07/07/90', // "Last New Files Scan Date (mm/dd/yy)" - // :TODO: fix last vs now times: - now.format('hh:mm'), // "Time of This Call" - now.format('hh:mm'), // "Time of Last Call (hh:mm)" - '9999', // "Maximum daily files available" - // :TODO: fix these stats: - '0', // "Files d/led so far today" - '0', // "Total "K" Bytes Uploaded" - '0', // "Total "K" Bytes Downloaded" - up.user_comment || 'None', // "User Comment" - '0', // "Total Doors Opened" - '0', // "Total Messages Left" + return iconv.encode( [ + 'COM1:', // "Comm Port - COM0: = LOCAL MODE" + '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) + '8', // "Parity - 7 or 8" + self.client.node.toString(), // "Node Number - 1 to 99" + '57600', // "DTE Rate. Actual BPS rate to use. (kg)" + 'Y', // "Screen Display - Y=On N=Off (Default to Y)" + 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" + 'Y', // "Page Bell - Y=On N=Off (Default to Y)" + 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" + up.real_name || self.client.user.username, // "User Full Name" + up.location || 'Anywhere', // "Calling From" + '123-456-7890', // "Home Phone" + '123-456-7890', // "Work/Data Phone" + 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) + secLevel, // "Security Level" + up.login_count.toString(), // "Total Times On" + now.format('MM/DD/YY'), // "Last Date Called" + '15360', // "Seconds Remaining THIS call (for those that particular)" + '256', // "Minutes Remaining THIS call" + 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" + self.client.term.termHeight.toString(), // "Page Length" + 'N', // "User Mode - Y = Expert, N = Novice" + '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" + '1', // "Conference Exited To DOOR From (G)" + '01/01/99', // "User Expiration Date (mm/dd/yy)" + self.client.user.userId.toString(), // "User File's Record Number" + 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." + // :TODO: fix up, down, etc. form user properties + '0', // "Total Uploads" + '0', // "Total Downloads" + '0', // "Daily Download "K" Total" + '999999', // "Daily Download Max. "K" Limit" + moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" + 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" + 'X:\\GEN\\', // "Path to the GEN directory" + StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" + self.client.user.username, // "Alias name" + '00:05', // "Event time (hh:mm)" (note: wat?) + 'Y', // "If its an error correcting connection (Y/N)" + 'Y', // "ANSI supported & caller using NG mode (Y/N)" + 'Y', // "Use Record Locking (Y/N)" + '7', // "BBS Default Color (Standard IBM color code, ie, 1-15)" + // :TODO: fix minutes here also: + '256', // "Time Credits In Minutes (positive/negative)" + '07/07/90', // "Last New Files Scan Date (mm/dd/yy)" + // :TODO: fix last vs now times: + now.format('hh:mm'), // "Time of This Call" + now.format('hh:mm'), // "Time of Last Call (hh:mm)" + '9999', // "Maximum daily files available" + // :TODO: fix these stats: + '0', // "Files d/led so far today" + '0', // "Total "K" Bytes Uploaded" + '0', // "Total "K" Bytes Downloaded" + up.user_comment || 'None', // "User Comment" + '0', // "Total Doors Opened" + '0', // "Total Messages Left" - ].join('\r\n') + '\r\n', 'cp437'); - }; + ].join('\r\n') + '\r\n', 'cp437'); + }; - this.getDoor32Buffer = function() { - // - // Resources: - // * http://wiki.bbses.info/index.php/DOOR32.SYS - // - // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! - return iconv.encode([ - '2', // :TODO: This needs to be configurable! - // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely - '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! - '57600', - Config().general.boardName, - self.client.user.userId.toString(), - self.client.user.properties.real_name || self.client.user.username, - self.client.user.username, - self.client.user.getLegacySecurityLevel().toString(), - '546', // :TODO: Minutes left! - '1', // ANSI - self.client.node.toString(), - ].join('\r\n') + '\r\n', 'cp437'); + this.getDoor32Buffer = function() { + // + // Resources: + // * http://wiki.bbses.info/index.php/DOOR32.SYS + // + // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! + return iconv.encode([ + '2', // :TODO: This needs to be configurable! + // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely + '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! + '57600', + Config().general.boardName, + self.client.user.userId.toString(), + self.client.user.properties.real_name || self.client.user.username, + self.client.user.username, + self.client.user.getLegacySecurityLevel().toString(), + '546', // :TODO: Minutes left! + '1', // ANSI + self.client.node.toString(), + ].join('\r\n') + '\r\n', 'cp437'); - }; + }; - this.getDoorInfoDefBuffer = function() { - // :TODO: fix time remaining + this.getDoorInfoDefBuffer = function() { + // :TODO: fix time remaining - // - // Resources: - // * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm - // - // Note that usernames are just used for first/last names here - // - var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; - var un = /[^\s]*/.exec(self.client.user.username)[0]; - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + // + // Resources: + // * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm + // + // Note that usernames are just used for first/last names here + // + var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; + var un = /[^\s]*/.exec(self.client.user.username)[0]; + var secLevel = self.client.user.getLegacySecurityLevel().toString(); - return iconv.encode( [ - Config().general.boardName, // "The name of the system." - opUn, // "The sysop's name up to the first space." - opUn, // "The sysop's name following the first space." - 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." - '57600', // "The current port (DTE) rate." - '0', // "The number "0"" - un, // "The current user's name, up to the first space." - un, // "The current user's name, following the first space." - self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." - '1', // "The number "0" if TTY, or "1" if ANSI." - secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." - '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." - '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." - ].join('\r\n') + '\r\n', 'cp437'); - }; + return iconv.encode( [ + Config().general.boardName, // "The name of the system." + opUn, // "The sysop's name up to the first space." + opUn, // "The sysop's name following the first space." + 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." + '57600', // "The current port (DTE) rate." + '0', // "The number "0"" + un, // "The current user's name, up to the first space." + un, // "The current user's name, following the first space." + self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." + '1', // "The number "0" if TTY, or "1" if ANSI." + secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." + '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." + '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." + ].join('\r\n') + '\r\n', 'cp437'); + }; } DropFile.fileTypes = [ 'DORINFO' ]; DropFile.prototype.createFile = function(cb) { - fs.writeFile(this.fullPath, this.dropFileContents, function written(err) { - cb(err); - }); + fs.writeFile(this.fullPath, this.dropFileContents, function written(err) { + cb(err); + }); }; diff --git a/core/edit_text_view.js b/core/edit_text_view.js index 0db02638..b1b89726 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -12,79 +12,79 @@ const _ = require('lodash'); exports.EditTextView = EditTextView; function EditTextView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); - options.resizable = false; + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); + options.resizable = false; - TextView.call(this, options); + TextView.call(this, options); - this.cursorPos = { row : 0, col : 0 }; + this.cursorPos = { row : 0, col : 0 }; - this.clientBackspace = function() { - const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); - this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); - }; + this.clientBackspace = function() { + const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); + this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); + }; } require('util').inherits(EditTextView, TextView); EditTextView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('backspace', key.name)) { - if(this.text.length > 0) { - this.text = this.text.substr(0, this.text.length - 1); + if(key) { + if(this.isKeyMapped('backspace', key.name)) { + if(this.text.length > 0) { + this.text = this.text.substr(0, this.text.length - 1); - if(this.text.length >= this.dimens.width) { - this.redraw(); - } else { - this.cursorPos.col -= 1; - if(this.cursorPos.col >= 0) { - this.clientBackspace(); - } - } - } + if(this.text.length >= this.dimens.width) { + this.redraw(); + } else { + this.cursorPos.col -= 1; + if(this.cursorPos.col >= 0) { + this.clientBackspace(); + } + } + } - return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); - } else if(this.isKeyMapped('clearLine', key.name)) { - this.text = ''; - this.cursorPos.col = 0; - this.setFocus(true); // resetting focus will redraw & adjust cursor + return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); + } else if(this.isKeyMapped('clearLine', key.name)) { + this.text = ''; + this.cursorPos.col = 0; + this.setFocus(true); // resetting focus will redraw & adjust cursor - return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); - } - } + return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); + } + } - if(ch && strUtil.isPrintable(ch)) { - if(this.text.length < this.maxLength) { - ch = strUtil.stylizeString(ch, this.textStyle); + if(ch && strUtil.isPrintable(ch)) { + if(this.text.length < this.maxLength) { + ch = strUtil.stylizeString(ch, this.textStyle); - this.text += ch; + this.text += ch; - if(this.text.length > this.dimens.width) { - // no shortcuts - redraw the view - this.redraw(); - } else { - this.cursorPos.col += 1; + if(this.text.length > this.dimens.width) { + // no shortcuts - redraw the view + this.redraw(); + } else { + this.cursorPos.col += 1; - if(_.isString(this.textMaskChar)) { - if(this.textMaskChar.length > 0) { - this.client.term.write(this.textMaskChar); - } - } else { - this.client.term.write(ch); - } - } - } - } + if(_.isString(this.textMaskChar)) { + if(this.textMaskChar.length > 0) { + this.client.term.write(this.textMaskChar); + } + } else { + this.client.term.write(ch); + } + } + } + } - EditTextView.super_.prototype.onKeyPress.call(this, ch, key); + EditTextView.super_.prototype.onKeyPress.call(this, ch, key); }; EditTextView.prototype.setText = function(text) { - // draw & set |text| - EditTextView.super_.prototype.setText.call(this, text); + // draw & set |text| + EditTextView.super_.prototype.setText.call(this, text); - // adjust local cursor tracking - this.cursorPos = { row : 0, col : text.length }; + // adjust local cursor tracking + this.cursorPos = { row : 0, col : text.length }; }; diff --git a/core/email.js b/core/email.js index 5cc66836..af195da5 100644 --- a/core/email.js +++ b/core/email.js @@ -13,20 +13,20 @@ const nodeMailer = require('nodemailer'); exports.sendMail = sendMail; function sendMail(message, cb) { - const config = Config(); - if(!_.has(config, 'email.transport')) { - return cb(Errors.MissingConfig('Email "email::transport" configuration missing')); - } + const config = Config(); + if(!_.has(config, 'email.transport')) { + return cb(Errors.MissingConfig('Email "email::transport" configuration missing')); + } - message.from = message.from || config.email.defaultFrom; + message.from = message.from || config.email.defaultFrom; - const transportOptions = Object.assign( {}, config.email.transport, { - logger : Log, - }); + const transportOptions = Object.assign( {}, config.email.transport, { + logger : Log, + }); - const transport = nodeMailer.createTransport(transportOptions); + const transport = nodeMailer.createTransport(transportOptions); - transport.sendMail(message, (err, info) => { - return cb(err, info); - }); + transport.sendMail(message, (err, info) => { + return cb(err, info); + }); } diff --git a/core/enig_error.js b/core/enig_error.js index c6eb8097..879a18ab 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -2,45 +2,45 @@ 'use strict'; class EnigError extends Error { - constructor(message, code, reason, reasonCode) { - super(message); + constructor(message, code, reason, reasonCode) { + super(message); - this.name = this.constructor.name; - this.message = message; - this.code = code; - this.reason = reason; - this.reasonCode = reasonCode; + this.name = this.constructor.name; + this.message = message; + this.code = code; + this.reason = reason; + this.reasonCode = reasonCode; - if(this.reason) { - this.message += `: ${this.reason}`; - } + if(this.reason) { + this.message += `: ${this.reason}`; + } - if(typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, this.constructor); - } else { - this.stack = (new Error(message)).stack; - } - } + if(typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = (new Error(message)).stack; + } + } } exports.EnigError = EnigError; exports.Errors = { - General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), - MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), - DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), - AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), - Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), - ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), - MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), - UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), - MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), + General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), + MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), + DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), + AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), + Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), + ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), + MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), + UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), + MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), }; exports.ErrorReasons = { - AlreadyThere : 'ALREADYTHERE', - InvalidNextMenu : 'BADNEXT', - NoPreviousMenu : 'NOPREV', - NoConditionMatch : 'NOCONDMATCH', - NotEnabled : 'NOTENABLED', + AlreadyThere : 'ALREADYTHERE', + InvalidNextMenu : 'BADNEXT', + NoPreviousMenu : 'NOPREV', + NoConditionMatch : 'NOCONDMATCH', + NotEnabled : 'NOTENABLED', }; \ No newline at end of file diff --git a/core/enigma_assert.js b/core/enigma_assert.js index 2b72227a..0d1d5176 100644 --- a/core/enigma_assert.js +++ b/core/enigma_assert.js @@ -9,10 +9,10 @@ const Log = require('./logger.js').log; const assert = require('assert'); module.exports = function(condition, message) { - if(Config().debug.assertsEnabled) { - assert.apply(this, arguments); - } else if(!(condition)) { - const stack = new Error().stack; - Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' ); - } + if(Config().debug.assertsEnabled) { + assert.apply(this, arguments); + } else if(!(condition)) { + const stack = new Error().stack; + Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' ); + } }; diff --git a/core/erc_client.js b/core/erc_client.js index ccc70199..79a47240 100644 --- a/core/erc_client.js +++ b/core/erc_client.js @@ -23,157 +23,157 @@ const net = require('net'); exports.getModule = ErcClientModule; exports.moduleInfo = { - name : 'ENiGMA Relay Chat Client', - desc : 'Chat with other ENiGMA BBSes', - author : 'Andrew Pamment', + name : 'ENiGMA Relay Chat Client', + desc : 'Chat with other ENiGMA BBSes', + author : 'Andrew Pamment', }; var MciViewIds = { - ChatDisplay : 1, - InputArea : 3, + ChatDisplay : 1, + InputArea : 3, }; // :TODO: needs converted to ES6 MenuModule subclass function ErcClientModule(options) { - MenuModule.prototype.ctorShim.call(this, options); + MenuModule.prototype.ctorShim.call(this, options); - const self = this; - this.config = options.menuConfig.config; + const self = this; + this.config = options.menuConfig.config; - this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; - this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; + this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; + this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; - this.finishedLoading = function() { - async.waterfall( - [ - function validateConfig(callback) { - if(_.isString(self.config.host) && + this.finishedLoading = function() { + async.waterfall( + [ + function validateConfig(callback) { + if(_.isString(self.config.host) && _.isNumber(self.config.port) && _.isString(self.config.bbsTag)) - { - return callback(null); - } else { - return callback(new Error('Configuration is missing required option(s)')); - } - }, - function connectToServer(callback) { - const connectOpts = { - port : self.config.port, - host : self.config.host, - }; + { + return callback(null); + } else { + return callback(new Error('Configuration is missing required option(s)')); + } + }, + function connectToServer(callback) { + const connectOpts = { + port : self.config.port, + host : self.config.host, + }; - const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); + const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); - chatMessageView.setText('Connecting to server...'); - chatMessageView.redraw(); + chatMessageView.setText('Connecting to server...'); + chatMessageView.redraw(); - self.viewControllers.menu.switchFocus(MciViewIds.InputArea); + self.viewControllers.menu.switchFocus(MciViewIds.InputArea); - // :TODO: Track actual client->enig connection for optional prevMenu @ final CB - self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); + // :TODO: Track actual client->enig connection for optional prevMenu @ final CB + self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); - self.chatConnection.on('data', data => { - data = data.toString(); + self.chatConnection.on('data', data => { + data = data.toString(); - if(data.startsWith('ERCHANDSHAKE')) { - self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`); - } else if(data.startsWith('{')) { - try { - data = JSON.parse(data); - } catch(e) { - return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server'); - } + if(data.startsWith('ERCHANDSHAKE')) { + self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`); + } else if(data.startsWith('{')) { + try { + data = JSON.parse(data); + } catch(e) { + return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server'); + } - let text; - try { - if(data.userName) { - // user message - text = stringFormat(self.chatEntryFormat, data); - } else { - // system message - text = stringFormat(self.systemEntryFormat, data); - } - } catch(e) { - return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error'); - } + let text; + try { + if(data.userName) { + // user message + text = stringFormat(self.chatEntryFormat, data); + } else { + // system message + text = stringFormat(self.systemEntryFormat, data); + } + } catch(e) { + return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error'); + } - chatMessageView.addText(text); + chatMessageView.addText(text); - if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? - chatMessageView.deleteLine(0); - chatMessageView.scrollDown(); - } + if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? + chatMessageView.deleteLine(0); + chatMessageView.scrollDown(); + } - chatMessageView.redraw(); - self.viewControllers.menu.switchFocus(MciViewIds.InputArea); - } - }); + chatMessageView.redraw(); + self.viewControllers.menu.switchFocus(MciViewIds.InputArea); + } + }); - self.chatConnection.once('end', () => { - return callback(null); - }); + self.chatConnection.once('end', () => { + return callback(null); + }); - self.chatConnection.once('error', err => { - self.client.log.info(`ERC connection error: ${err.message}`); - return callback(new Error('Failed connecting to ERC server!')); - }); - } - ], - err => { - if(err) { - self.client.log.warn( { error : err.message }, 'ERC error'); - } + self.chatConnection.once('error', err => { + self.client.log.info(`ERC connection error: ${err.message}`); + return callback(new Error('Failed connecting to ERC server!')); + }); + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'ERC error'); + } - self.prevMenu(); - } - ); - }; + self.prevMenu(); + } + ); + }; - this.scrollHandler = function(keyName) { - const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); - const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); + this.scrollHandler = function(keyName) { + const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); + const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); - if('up arrow' === keyName) { - chatDisplayView.scrollUp(); - } else { - chatDisplayView.scrollDown(); - } + if('up arrow' === keyName) { + chatDisplayView.scrollUp(); + } else { + chatDisplayView.scrollDown(); + } - chatDisplayView.redraw(); - inputAreaView.setFocus(true); - }; + chatDisplayView.redraw(); + inputAreaView.setFocus(true); + }; - this.menuMethods = { - inputAreaSubmit : function(formData, extraArgs, cb) { - const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); - const inputData = inputAreaView.getData(); + this.menuMethods = { + inputAreaSubmit : function(formData, extraArgs, cb) { + const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); + const inputData = inputAreaView.getData(); - if('/quit' === inputData.toLowerCase()) { - self.chatConnection.end(); - } else { - try { - self.chatConnection.write(`${inputData}\r\n`); - } catch(e) { - self.client.log.warn( { error : e.message }, 'ERC error'); - } - inputAreaView.clearText(); - } - return cb(null); - }, - scrollUp : function(formData, extraArgs, cb) { - self.scrollHandler(formData.key.name); - return cb(null); - }, - scrollDown : function(formData, extraArgs, cb) { - self.scrollHandler(formData.key.name); - return cb(null); - } - }; + if('/quit' === inputData.toLowerCase()) { + self.chatConnection.end(); + } else { + try { + self.chatConnection.write(`${inputData}\r\n`); + } catch(e) { + self.client.log.warn( { error : e.message }, 'ERC error'); + } + inputAreaView.clearText(); + } + return cb(null); + }, + scrollUp : function(formData, extraArgs, cb) { + self.scrollHandler(formData.key.name); + return cb(null); + }, + scrollDown : function(formData, extraArgs, cb) { + self.scrollHandler(formData.key.name); + return cb(null); + } + }; } require('util').inherits(ErcClientModule, MenuModule); ErcClientModule.prototype.mciReady = function(mciData, cb) { - this.standardMCIReadyHandler(mciData, cb); + this.standardMCIReadyHandler(mciData, cb); }; diff --git a/core/event_scheduler.js b/core/event_scheduler.js index e425d3bd..1465d42b 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -19,251 +19,251 @@ exports.getModule = EventSchedulerModule; exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart exports.moduleInfo = { - name : 'Event Scheduler', - desc : 'Support for scheduling arbritary events', - author : 'NuSkooler', + name : 'Event Scheduler', + desc : 'Support for scheduling arbritary events', + author : 'NuSkooler', }; const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; class ScheduledEvent { - constructor(events, name) { - this.name = name; - this.schedule = this.parseScheduleString(events[name].schedule); - this.action = this.parseActionSpec(events[name].action); - if(this.action) { - this.action.args = events[name].args || []; - } - } + constructor(events, name) { + this.name = name; + this.schedule = this.parseScheduleString(events[name].schedule); + this.action = this.parseActionSpec(events[name].action); + if(this.action) { + this.action.args = events[name].args || []; + } + } - get isValid() { - if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { - return false; - } + get isValid() { + if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { + return false; + } - if('method' === this.action.type && !this.action.location) { - return false; - } + if('method' === this.action.type && !this.action.location) { + return false; + } - return true; - } + return true; + } - parseScheduleString(schedStr) { - if(!schedStr) { - return false; - } + parseScheduleString(schedStr) { + if(!schedStr) { + return false; + } - let schedule = {}; + let schedule = {}; - const m = SCHEDULE_REGEXP.exec(schedStr); - if(m) { - schedStr = schedStr.substr(0, m.index).trim(); + const m = SCHEDULE_REGEXP.exec(schedStr); + if(m) { + schedStr = schedStr.substr(0, m.index).trim(); - if('@watch:' === m[1]) { - schedule.watchFile = m[2]; - } - } + if('@watch:' === m[1]) { + schedule.watchFile = m[2]; + } + } - if(schedStr.length > 0) { - const sched = later.parse.text(schedStr); - if(-1 === sched.error) { - schedule.sched = sched; - } - } + if(schedStr.length > 0) { + const sched = later.parse.text(schedStr); + if(-1 === sched.error) { + schedule.sched = sched; + } + } - // return undefined if we couldn't parse out anything useful - if(!_.isEmpty(schedule)) { - return schedule; - } - } + // return undefined if we couldn't parse out anything useful + if(!_.isEmpty(schedule)) { + return schedule; + } + } - parseActionSpec(actionSpec) { - if(actionSpec) { - if('@' === actionSpec[0]) { - const m = ACTION_REGEXP.exec(actionSpec); - if(m) { - if(m[2].indexOf(':') > -1) { - const parts = m[2].split(':'); - return { - type : m[1], - location : parts[0], - what : parts[1], - }; - } else { - return { - type : m[1], - what : m[2], - }; - } - } - } else { - return { - type : 'execute', - what : actionSpec, - }; - } - } - } + parseActionSpec(actionSpec) { + if(actionSpec) { + if('@' === actionSpec[0]) { + const m = ACTION_REGEXP.exec(actionSpec); + if(m) { + if(m[2].indexOf(':') > -1) { + const parts = m[2].split(':'); + return { + type : m[1], + location : parts[0], + what : parts[1], + }; + } else { + return { + type : m[1], + what : m[2], + }; + } + } + } else { + return { + type : 'execute', + what : actionSpec, + }; + } + } + } - executeAction(reason, cb) { - Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...'); + executeAction(reason, cb) { + Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...'); - if('method' === this.action.type) { - const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') - try { - const methodModule = require(modulePath); - methodModule[this.action.what](this.action.args, err => { - if(err) { - Log.debug( - { error : err.toString(), eventName : this.name, action : this.action }, - 'Error performing scheduled event action'); - } + if('method' === this.action.type) { + const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') + try { + const methodModule = require(modulePath); + methodModule[this.action.what](this.action.args, err => { + if(err) { + Log.debug( + { error : err.toString(), eventName : this.name, action : this.action }, + 'Error performing scheduled event action'); + } - return cb(err); - }); - } catch(e) { - Log.warn( - { error : e.toString(), eventName : this.name, action : this.action }, - 'Failed to perform scheduled event action'); + return cb(err); + }); + } catch(e) { + Log.warn( + { error : e.toString(), eventName : this.name, action : this.action }, + 'Failed to perform scheduled event action'); - return cb(e); - } - } else if('execute' === this.action.type) { - const opts = { - // :TODO: cwd - name : this.name, - cols : 80, - rows : 24, - env : process.env, - }; + return cb(e); + } + } else if('execute' === this.action.type) { + const opts = { + // :TODO: cwd + name : this.name, + cols : 80, + rows : 24, + env : process.env, + }; - const proc = pty.spawn(this.action.what, this.action.args, opts); + const proc = pty.spawn(this.action.what, this.action.args, opts); - proc.once('exit', exitCode => { - if(exitCode) { - Log.warn( - { eventName : this.name, action : this.action, exitCode : exitCode }, - 'Bad exit code while performing scheduled event action'); - } - return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); - }); - } - } + proc.once('exit', exitCode => { + if(exitCode) { + Log.warn( + { eventName : this.name, action : this.action, exitCode : exitCode }, + 'Bad exit code while performing scheduled event action'); + } + return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); + }); + } + } } function EventSchedulerModule(options) { - PluginModule.call(this, options); + PluginModule.call(this, options); - const config = Config(); - if(_.has(config, 'eventScheduler')) { - this.moduleConfig = config.eventScheduler; - } + const config = Config(); + if(_.has(config, 'eventScheduler')) { + this.moduleConfig = config.eventScheduler; + } - const self = this; - this.runningActions = new Set(); + const self = this; + this.runningActions = new Set(); - this.performAction = function(schedEvent, reason) { - if(self.runningActions.has(schedEvent.name)) { - return; // already running - } + this.performAction = function(schedEvent, reason) { + if(self.runningActions.has(schedEvent.name)) { + return; // already running + } - self.runningActions.add(schedEvent.name); + self.runningActions.add(schedEvent.name); - schedEvent.executeAction(reason, () => { - self.runningActions.delete(schedEvent.name); - }); - }; + schedEvent.executeAction(reason, () => { + self.runningActions.delete(schedEvent.name); + }); + }; } // convienence static method for direct load + start EventSchedulerModule.loadAndStart = function(cb) { - const loadModuleEx = require('./module_util.js').loadModuleEx; + const loadModuleEx = require('./module_util.js').loadModuleEx; - const loadOpts = { - name : path.basename(__filename, '.js'), - path : __dirname, - }; + const loadOpts = { + name : path.basename(__filename, '.js'), + path : __dirname, + }; - loadModuleEx(loadOpts, (err, mod) => { - if(err) { - return cb(err); - } + loadModuleEx(loadOpts, (err, mod) => { + if(err) { + return cb(err); + } - const modInst = new mod.getModule(); - modInst.startup( err => { - return cb(err, modInst); - }); - }); + const modInst = new mod.getModule(); + modInst.startup( err => { + return cb(err, modInst); + }); + }); }; EventSchedulerModule.prototype.startup = function(cb) { - this.eventTimers = []; - const self = this; + this.eventTimers = []; + const self = this; - if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { - const events = Object.keys(this.moduleConfig.events).map( name => { - return new ScheduledEvent(this.moduleConfig.events, name); - }); + if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { + const events = Object.keys(this.moduleConfig.events).map( name => { + return new ScheduledEvent(this.moduleConfig.events, name); + }); - events.forEach( schedEvent => { - if(!schedEvent.isValid) { - Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); - return; - } + events.forEach( schedEvent => { + if(!schedEvent.isValid) { + Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); + return; + } - Log.debug( - { - eventName : schedEvent.name, - schedule : this.moduleConfig.events[schedEvent.name].schedule, - action : schedEvent.action, - next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', - }, - 'Scheduled event loaded' - ); + Log.debug( + { + eventName : schedEvent.name, + schedule : this.moduleConfig.events[schedEvent.name].schedule, + action : schedEvent.action, + next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', + }, + 'Scheduled event loaded' + ); - if(schedEvent.schedule.sched) { - this.eventTimers.push(later.setInterval( () => { - self.performAction(schedEvent, 'Schedule'); - }, schedEvent.schedule.sched)); - } + if(schedEvent.schedule.sched) { + this.eventTimers.push(later.setInterval( () => { + self.performAction(schedEvent, 'Schedule'); + }, schedEvent.schedule.sched)); + } - if(schedEvent.schedule.watchFile) { - const watcher = sane( - paths.dirname(schedEvent.schedule.watchFile), - { - glob : `**/${paths.basename(schedEvent.schedule.watchFile)}` - } - ); + if(schedEvent.schedule.watchFile) { + const watcher = sane( + paths.dirname(schedEvent.schedule.watchFile), + { + glob : `**/${paths.basename(schedEvent.schedule.watchFile)}` + } + ); - // :TODO: should track watched files & stop watching @ shutdown? + // :TODO: should track watched files & stop watching @ shutdown? - [ 'change', 'add', 'delete' ].forEach(event => { - watcher.on(event, (fileName, fileRoot) => { - const eventPath = paths.join(fileRoot, fileName); - if(schedEvent.schedule.watchFile === eventPath) { - self.performAction(schedEvent, `Watch file: ${eventPath}`); - } - }); - }); + [ 'change', 'add', 'delete' ].forEach(event => { + watcher.on(event, (fileName, fileRoot) => { + const eventPath = paths.join(fileRoot, fileName); + if(schedEvent.schedule.watchFile === eventPath) { + self.performAction(schedEvent, `Watch file: ${eventPath}`); + } + }); + }); - fse.exists(schedEvent.schedule.watchFile, exists => { - if(exists) { - self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`); - } - }); - } - }); - } + fse.exists(schedEvent.schedule.watchFile, exists => { + if(exists) { + self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`); + } + }); + } + }); + } - cb(null); + cb(null); }; EventSchedulerModule.prototype.shutdown = function(cb) { - if(this.eventTimers) { - this.eventTimers.forEach( et => et.clear() ); - } + if(this.eventTimers) { + this.eventTimers.forEach( et => et.clear() ); + } - cb(null); + cb(null); }; diff --git a/core/events.js b/core/events.js index 7bf307ad..aa75345f 100644 --- a/core/events.js +++ b/core/events.js @@ -12,68 +12,68 @@ const async = require('async'); const glob = require('glob'); module.exports = new class Events extends events.EventEmitter { - constructor() { - super(); - this.setMaxListeners(32); // :TODO: play with this... - } + constructor() { + super(); + this.setMaxListeners(32); // :TODO: play with this... + } - getSystemEvents() { - return SystemEvents; - } + getSystemEvents() { + return SystemEvents; + } - addListener(event, listener) { - Log.trace( { event : event }, 'Registering event listener'); - return super.addListener(event, listener); - } + addListener(event, listener) { + Log.trace( { event : event }, 'Registering event listener'); + return super.addListener(event, listener); + } - emit(event, ...args) { - Log.trace( { event : event }, 'Emitting event'); - return super.emit(event, ...args); - } + emit(event, ...args) { + Log.trace( { event : event }, 'Emitting event'); + return super.emit(event, ...args); + } - on(event, listener) { - Log.trace( { event : event }, 'Registering event listener'); - return super.on(event, listener); - } + on(event, listener) { + Log.trace( { event : event }, 'Registering event listener'); + return super.on(event, listener); + } - once(event, listener) { - Log.trace( { event : event }, 'Registering single use event listener'); - return super.once(event, listener); - } + once(event, listener) { + Log.trace( { event : event }, 'Registering single use event listener'); + return super.once(event, listener); + } - removeListener(event, listener) { - Log.trace( { event : event }, 'Removing listener'); - return super.removeListener(event, listener); - } + removeListener(event, listener) { + Log.trace( { event : event }, 'Removing listener'); + return super.removeListener(event, listener); + } - startup(cb) { - async.each(require('./module_util.js').getModulePaths(), (modulePath, nextPath) => { - glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { - if(err) { - return nextPath(err); - } + startup(cb) { + async.each(require('./module_util.js').getModulePaths(), (modulePath, nextPath) => { + glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { + if(err) { + return nextPath(err); + } - async.each(files, (moduleName, nextModule) => { - const fullModulePath = paths.join(modulePath, moduleName); + async.each(files, (moduleName, nextModule) => { + const fullModulePath = paths.join(modulePath, moduleName); - try { - const mod = require(fullModulePath); + try { + const mod = require(fullModulePath); - if(_.isFunction(mod.registerEvents)) { - // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? - mod.registerEvents(this); - } - } catch(e) { - Log.warn( { error : e }, 'Exception during module "registerEvents"'); - } + if(_.isFunction(mod.registerEvents)) { + // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? + mod.registerEvents(this); + } + } catch(e) { + Log.warn( { error : e }, 'Exception during module "registerEvents"'); + } - return nextModule(null); - }, err => { - return nextPath(err); - }); - }); - }, err => { - return cb(err); - }); - } + return nextModule(null); + }, err => { + return nextPath(err); + }); + }); + }, err => { + return cb(err); + }); + } }; diff --git a/core/exodus.js b/core/exodus.js index 409b5f2a..b20f18a1 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -49,183 +49,183 @@ const SSHClient = require('ssh2').Client; */ exports.moduleInfo = { - name : 'Exodus', - desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/', - author : 'NuSkooler', + name : 'Exodus', + desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/', + author : 'NuSkooler', }; exports.getModule = class ExodusModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.config = options.menuConfig.config || {}; - this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; - this.config.ticketPort = this.config.ticketPort || 1984, - this.config.ticketPath = this.config.ticketPath || '/exodus'; - this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); - this.config.sshHost = this.config.sshHost || this.config.ticketHost; - this.config.sshPort = this.config.sshPort || 22; - this.config.sshUser = this.config.sshUser || 'exodus_server'; - this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); - } + this.config = options.menuConfig.config || {}; + this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; + this.config.ticketPort = this.config.ticketPort || 1984, + this.config.ticketPath = this.config.ticketPath || '/exodus'; + this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); + this.config.sshHost = this.config.sshHost || this.config.ticketHost; + this.config.sshPort = this.config.sshPort || 22; + this.config.sshUser = this.config.sshUser || 'exodus_server'; + this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); + } - initSequence() { + initSequence() { - const self = this; - let clientTerminated = false; + const self = this; + let clientTerminated = false; - async.waterfall( - [ - function validateConfig(callback) { - // very basic validation on optionals - async.each( [ 'board', 'key', 'door' ], (key, next) => { - return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`)); - }, callback); - }, - function loadCertAuthorities(callback) { - if(!_.isString(self.config.caPem)) { - return callback(null, null); - } + async.waterfall( + [ + function validateConfig(callback) { + // very basic validation on optionals + async.each( [ 'board', 'key', 'door' ], (key, next) => { + return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`)); + }, callback); + }, + function loadCertAuthorities(callback) { + if(!_.isString(self.config.caPem)) { + return callback(null, null); + } - fs.readFile(self.config.caPem, (err, certAuthorities) => { - return callback(err, certAuthorities); - }); - }, - function getTicket(certAuthorities, callback) { - const now = moment.utc().unix(); - const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); - const token = `${sha256}|${now}`; + fs.readFile(self.config.caPem, (err, certAuthorities) => { + return callback(err, certAuthorities); + }); + }, + function getTicket(certAuthorities, callback) { + const now = moment.utc().unix(); + const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); + const token = `${sha256}|${now}`; - const postData = querystring.stringify({ - token : token, - board : self.config.board, - user : self.client.user.username, - door : self.config.door, - }); + const postData = querystring.stringify({ + token : token, + board : self.config.board, + user : self.client.user.username, + door : self.config.door, + }); - const reqOptions = { - hostname : self.config.ticketHost, - port : self.config.ticketPort, - path : self.config.ticketPath, - rejectUnauthorized : self.config.rejectUnauthorized, - method : 'POST', - headers : { - 'Content-Type' : 'application/x-www-form-urlencoded', - 'Content-Length' : postData.length, - 'User-Agent' : getEnigmaUserAgent(), - } - }; + const reqOptions = { + hostname : self.config.ticketHost, + port : self.config.ticketPort, + path : self.config.ticketPath, + rejectUnauthorized : self.config.rejectUnauthorized, + method : 'POST', + headers : { + 'Content-Type' : 'application/x-www-form-urlencoded', + 'Content-Length' : postData.length, + 'User-Agent' : getEnigmaUserAgent(), + } + }; - if(certAuthorities) { - reqOptions.ca = certAuthorities; - } + if(certAuthorities) { + reqOptions.ca = certAuthorities; + } - let ticket = ''; - const req = https.request(reqOptions, res => { - res.on('data', data => { - ticket += data; - }); + let ticket = ''; + const req = https.request(reqOptions, res => { + res.on('data', data => { + ticket += data; + }); - res.on('end', () => { - if(ticket.length !== 36) { - return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`)); - } + res.on('end', () => { + if(ticket.length !== 36) { + return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`)); + } - return callback(null, ticket); - }); - }); + return callback(null, ticket); + }); + }); - req.on('error', err => { - return callback(Errors.General(`Exodus error: ${err.message}`)); - }); + req.on('error', err => { + return callback(Errors.General(`Exodus error: ${err.message}`)); + }); - req.write(postData); - req.end(); - }, - function loadPrivateKey(ticket, callback) { - fs.readFile(self.config.sshKeyPem, (err, privateKey) => { - return callback(err, ticket, privateKey); - }); - }, - function establishSecureConnection(ticket, privateKey, callback) { + req.write(postData); + req.end(); + }, + function loadPrivateKey(ticket, callback) { + fs.readFile(self.config.sshKeyPem, (err, privateKey) => { + return callback(err, ticket, privateKey); + }); + }, + function establishSecureConnection(ticket, privateKey, callback) { - let pipeRestored = false; - let pipedStream; + let pipeRestored = false; + let pipedStream; - function restorePipe() { - if(pipedStream && !pipeRestored && !clientTerminated) { - self.client.term.output.unpipe(pipedStream); - self.client.term.output.resume(); - } - } + function restorePipe() { + if(pipedStream && !pipeRestored && !clientTerminated) { + self.client.term.output.unpipe(pipedStream); + self.client.term.output.resume(); + } + } - self.client.term.write(resetScreen()); - self.client.term.write('Connecting to Exodus server, please wait...\n'); + self.client.term.write(resetScreen()); + self.client.term.write('Connecting to Exodus server, please wait...\n'); - const sshClient = new SSHClient(); + const sshClient = new SSHClient(); - const window = { - rows : self.client.term.termHeight, - cols : self.client.term.termWidth, - width : 0, - height : 0, - term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( - }; + const window = { + rows : self.client.term.termHeight, + cols : self.client.term.termWidth, + width : 0, + height : 0, + term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( + }; - const options = { - env : { - exodus : ticket, - }, - }; + const options = { + env : { + exodus : ticket, + }, + }; - sshClient.on('ready', () => { - self.client.once('end', () => { - self.client.log.info('Connection ended. Terminating Exodus connection'); - clientTerminated = true; - return sshClient.end(); - }); + sshClient.on('ready', () => { + self.client.once('end', () => { + self.client.log.info('Connection ended. Terminating Exodus connection'); + clientTerminated = true; + return sshClient.end(); + }); - sshClient.shell(window, options, (err, stream) => { - pipedStream = stream; // :TODO: ewwwwwwwww hack - self.client.term.output.pipe(stream); + sshClient.shell(window, options, (err, stream) => { + pipedStream = stream; // :TODO: ewwwwwwwww hack + self.client.term.output.pipe(stream); - stream.on('data', d => { - return self.client.term.rawWrite(d); - }); + stream.on('data', d => { + return self.client.term.rawWrite(d); + }); - stream.on('close', () => { - restorePipe(); - return sshClient.end(); - }); + stream.on('close', () => { + restorePipe(); + return sshClient.end(); + }); - stream.on('error', err => { - Log.warn( { error : err.message }, 'Exodus SSH client stream error'); - }); - }); - }); + stream.on('error', err => { + Log.warn( { error : err.message }, 'Exodus SSH client stream error'); + }); + }); + }); - sshClient.on('close', () => { - restorePipe(); - return callback(null); - }); + sshClient.on('close', () => { + restorePipe(); + return callback(null); + }); - sshClient.connect({ - host : self.config.sshHost, - port : self.config.sshPort, - username : self.config.sshUser, - privateKey : privateKey, - }); - } - ], - err => { - if(err) { - self.client.log.warn( { error : err.message }, 'Exodus error'); - } + sshClient.connect({ + host : self.config.sshHost, + port : self.config.sshPort, + username : self.config.sshUser, + privateKey : privateKey, + }); + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'Exodus error'); + } - if(!clientTerminated) { - self.prevMenu(); - } - } - ); - } + if(!clientTerminated) { + self.prevMenu(); + } + } + ); + } }; diff --git a/core/file_area_filter_edit.js b/core/file_area_filter_edit.js index cc4c22c7..3cdeb68b 100644 --- a/core/file_area_filter_edit.js +++ b/core/file_area_filter_edit.js @@ -12,328 +12,328 @@ const stringFormat = require('./string_format.js'); const async = require('async'); exports.moduleInfo = { - name : 'File Area Filter Editor', - desc : 'Module for adding, deleting, and modifying file base filters', - author : 'NuSkooler', + name : 'File Area Filter Editor', + desc : 'Module for adding, deleting, and modifying file base filters', + author : 'NuSkooler', }; const MciViewIds = { - editor : { - searchTerms : 1, - tags : 2, - area : 3, - sort : 4, - order : 5, - filterName : 6, - navMenu : 7, + editor : { + searchTerms : 1, + tags : 2, + area : 3, + sort : 4, + order : 5, + filterName : 6, + navMenu : 7, - // :TODO: use the customs new standard thing - filter obj can have active/selected, etc. - selectedFilterInfo : 10, // { ...filter object ... } - activeFilterInfo : 11, // { ...filter object ... } - error : 12, // validation errors - } + // :TODO: use the customs new standard thing - filter obj can have active/selected, etc. + selectedFilterInfo : 10, // { ...filter object ... } + activeFilterInfo : 11, // { ...filter object ... } + error : 12, // validation errors + } }; exports.getModule = class FileAreaFilterEdit extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them - this.currentFilterIndex = 0; // into |filtersArray| + this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them + this.currentFilterIndex = 0; // into |filtersArray| - // - // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| - // - const activeFilter = FileBaseFilters.getActiveFilter(this.client); - this.filtersArray.sort( (filterA, filterB) => { - if(activeFilter) { - if(filterA.uuid === activeFilter.uuid) { - return -1; - } - if(filterB.uuid === activeFilter.uuid) { - return 1; - } - } + // + // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| + // + const activeFilter = FileBaseFilters.getActiveFilter(this.client); + this.filtersArray.sort( (filterA, filterB) => { + if(activeFilter) { + if(filterA.uuid === activeFilter.uuid) { + return -1; + } + if(filterB.uuid === activeFilter.uuid) { + return 1; + } + } - return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } ); - }); + return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } ); + }); - this.menuMethods = { - saveFilter : (formData, extraArgs, cb) => { - return this.saveCurrentFilter(formData, cb); - }, - prevFilter : (formData, extraArgs, cb) => { - this.currentFilterIndex -= 1; - if(this.currentFilterIndex < 0) { - this.currentFilterIndex = this.filtersArray.length - 1; - } - this.loadDataForFilter(this.currentFilterIndex); - return cb(null); - }, - nextFilter : (formData, extraArgs, cb) => { - this.currentFilterIndex += 1; - if(this.currentFilterIndex >= this.filtersArray.length) { - this.currentFilterIndex = 0; - } - this.loadDataForFilter(this.currentFilterIndex); - return cb(null); - }, - makeFilterActive : (formData, extraArgs, cb) => { - const filters = new FileBaseFilters(this.client); - filters.setActive(this.filtersArray[this.currentFilterIndex].uuid); + this.menuMethods = { + saveFilter : (formData, extraArgs, cb) => { + return this.saveCurrentFilter(formData, cb); + }, + prevFilter : (formData, extraArgs, cb) => { + this.currentFilterIndex -= 1; + if(this.currentFilterIndex < 0) { + this.currentFilterIndex = this.filtersArray.length - 1; + } + this.loadDataForFilter(this.currentFilterIndex); + return cb(null); + }, + nextFilter : (formData, extraArgs, cb) => { + this.currentFilterIndex += 1; + if(this.currentFilterIndex >= this.filtersArray.length) { + this.currentFilterIndex = 0; + } + this.loadDataForFilter(this.currentFilterIndex); + return cb(null); + }, + makeFilterActive : (formData, extraArgs, cb) => { + const filters = new FileBaseFilters(this.client); + filters.setActive(this.filtersArray[this.currentFilterIndex].uuid); - this.updateActiveLabel(); + this.updateActiveLabel(); - return cb(null); - }, - newFilter : (formData, extraArgs, cb) => { - this.currentFilterIndex = this.filtersArray.length; // next avail slot - this.clearForm(MciViewIds.editor.searchTerms); - return cb(null); - }, - deleteFilter : (formData, extraArgs, cb) => { - const selectedFilter = this.filtersArray[this.currentFilterIndex]; - const filterUuid = selectedFilter.uuid; + return cb(null); + }, + newFilter : (formData, extraArgs, cb) => { + this.currentFilterIndex = this.filtersArray.length; // next avail slot + this.clearForm(MciViewIds.editor.searchTerms); + return cb(null); + }, + deleteFilter : (formData, extraArgs, cb) => { + const selectedFilter = this.filtersArray[this.currentFilterIndex]; + const filterUuid = selectedFilter.uuid; - // cannot delete built-in/system filters - if(true === selectedFilter.system) { - this.showError('Cannot delete built in filters!'); - return cb(null); - } + // cannot delete built-in/system filters + if(true === selectedFilter.system) { + this.showError('Cannot delete built in filters!'); + return cb(null); + } - this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry + this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry - // remove from stored properties - const filters = new FileBaseFilters(this.client); - filters.remove(filterUuid); - filters.persist( () => { + // remove from stored properties + const filters = new FileBaseFilters(this.client); + filters.remove(filterUuid); + filters.persist( () => { - // - // If the item was also the active filter, we need to make a new one active - // - if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) { - const newActive = this.filtersArray[this.currentFilterIndex]; - if(newActive) { - filters.setActive(newActive.uuid); - } else { - // nothing to set active to - this.client.user.removeProperty('file_base_filter_active_uuid'); - } - } + // + // If the item was also the active filter, we need to make a new one active + // + if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) { + const newActive = this.filtersArray[this.currentFilterIndex]; + if(newActive) { + filters.setActive(newActive.uuid); + } else { + // nothing to set active to + this.client.user.removeProperty('file_base_filter_active_uuid'); + } + } - // update UI - this.updateActiveLabel(); + // update UI + this.updateActiveLabel(); - if(this.filtersArray.length > 0) { - this.loadDataForFilter(this.currentFilterIndex); - } else { - this.clearForm(); - } - return cb(null); - }); - }, + if(this.filtersArray.length > 0) { + this.loadDataForFilter(this.currentFilterIndex); + } else { + this.clearForm(); + } + return cb(null); + }); + }, - viewValidationListener : (err, cb) => { - const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); - let newFocusId; + viewValidationListener : (err, cb) => { + const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); + let newFocusId; - if(errorView) { - if(err) { - errorView.setText(err.message); - err.view.clearText(); // clear out the invalid data - } else { - errorView.clearText(); - } - } + if(errorView) { + if(err) { + errorView.setText(err.message); + err.view.clearText(); // clear out the invalid data + } else { + errorView.clearText(); + } + } - return cb(newFocusId); - }, - }; - } + return cb(newFocusId); + }, + }; + } - showError(errMsg) { - const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); - if(errorView) { - if(errMsg) { - errorView.setText(errMsg); - } else { - errorView.clearText(); - } - } - } + showError(errMsg) { + const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); + if(errorView) { + if(errMsg) { + errorView.setText(errMsg); + } else { + errorView.clearText(); + } + } + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) ); + const self = this; + const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) ); - async.series( - [ - function loadFromConfig(callback) { - return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); - }, - function populateAreas(callback) { - self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); + async.series( + [ + function loadFromConfig(callback) { + return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); + }, + function populateAreas(callback) { + self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); - const areasView = vc.getView(MciViewIds.editor.area); - if(areasView) { - areasView.setItems( self.availAreas.map( a => a.name ) ); - } + const areasView = vc.getView(MciViewIds.editor.area); + if(areasView) { + areasView.setItems( self.availAreas.map( a => a.name ) ); + } - self.updateActiveLabel(); - self.loadDataForFilter(self.currentFilterIndex); - self.viewControllers.editor.resetInitialFocus(); - return callback(null); - } - ], - err => { - return cb(err); - } - ); - }); - } + self.updateActiveLabel(); + self.loadDataForFilter(self.currentFilterIndex); + self.viewControllers.editor.resetInitialFocus(); + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } - getCurrentFilter() { - return this.filtersArray[this.currentFilterIndex]; - } + getCurrentFilter() { + return this.filtersArray[this.currentFilterIndex]; + } - setText(mciId, text) { - const view = this.viewControllers.editor.getView(mciId); - if(view) { - view.setText(text); - } - } + setText(mciId, text) { + const view = this.viewControllers.editor.getView(mciId); + if(view) { + view.setText(text); + } + } - updateActiveLabel() { - const activeFilter = FileBaseFilters.getActiveFilter(this.client); - if(activeFilter) { - const activeFormat = this.menuConfig.config.activeFormat || '{name}'; - this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); - } - } + updateActiveLabel() { + const activeFilter = FileBaseFilters.getActiveFilter(this.client); + if(activeFilter) { + const activeFormat = this.menuConfig.config.activeFormat || '{name}'; + this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); + } + } - setFocusItemIndex(mciId, index) { - const view = this.viewControllers.editor.getView(mciId); - if(view) { - view.setFocusItemIndex(index); - } - } + setFocusItemIndex(mciId, index) { + const view = this.viewControllers.editor.getView(mciId); + if(view) { + view.setFocusItemIndex(index); + } + } - clearForm(newFocusId) { - [ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => { - this.setText(mciId, ''); - }); + clearForm(newFocusId) { + [ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => { + this.setText(mciId, ''); + }); - [ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => { - this.setFocusItemIndex(mciId, 0); - }); + [ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => { + this.setFocusItemIndex(mciId, 0); + }); - if(newFocusId) { - this.viewControllers.editor.switchFocus(newFocusId); - } else { - this.viewControllers.editor.resetInitialFocus(); - } - } + if(newFocusId) { + this.viewControllers.editor.switchFocus(newFocusId); + } else { + this.viewControllers.editor.resetInitialFocus(); + } + } - getSelectedAreaTag(index) { - if(0 === index) { - return ''; // -ALL- - } - const area = this.availAreas[index]; - if(!area) { - return ''; - } - return area.areaTag; - } + getSelectedAreaTag(index) { + if(0 === index) { + return ''; // -ALL- + } + const area = this.availAreas[index]; + if(!area) { + return ''; + } + return area.areaTag; + } - getOrderBy(index) { - return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0]; - } + getOrderBy(index) { + return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0]; + } - setAreaIndexFromCurrentFilter() { - let index; - const filter = this.getCurrentFilter(); - if(filter) { - // special treatment: areaTag saved as blank ("") if -ALL- - index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; - } else { - index = 0; - } - this.setFocusItemIndex(MciViewIds.editor.area, index); - } + setAreaIndexFromCurrentFilter() { + let index; + const filter = this.getCurrentFilter(); + if(filter) { + // special treatment: areaTag saved as blank ("") if -ALL- + index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; + } else { + index = 0; + } + this.setFocusItemIndex(MciViewIds.editor.area, index); + } - setOrderByFromCurrentFilter() { - let index; - const filter = this.getCurrentFilter(); - if(filter) { - index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0; - } else { - index = 0; - } - this.setFocusItemIndex(MciViewIds.editor.order, index); - } + setOrderByFromCurrentFilter() { + let index; + const filter = this.getCurrentFilter(); + if(filter) { + index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0; + } else { + index = 0; + } + this.setFocusItemIndex(MciViewIds.editor.order, index); + } - setSortByFromCurrentFilter() { - let index; - const filter = this.getCurrentFilter(); - if(filter) { - index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0; - } else { - index = 0; - } - this.setFocusItemIndex(MciViewIds.editor.sort, index); - } + setSortByFromCurrentFilter() { + let index; + const filter = this.getCurrentFilter(); + if(filter) { + index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0; + } else { + index = 0; + } + this.setFocusItemIndex(MciViewIds.editor.sort, index); + } - getSortBy(index) { - return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0]; - } + getSortBy(index) { + return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0]; + } - setFilterValuesFromFormData(filter, formData) { - filter.name = formData.value.name; - filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); - filter.terms = formData.value.searchTerms; - filter.tags = formData.value.tags; - filter.order = this.getOrderBy(formData.value.orderByIndex); - filter.sort = this.getSortBy(formData.value.sortByIndex); - } + setFilterValuesFromFormData(filter, formData) { + filter.name = formData.value.name; + filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); + filter.terms = formData.value.searchTerms; + filter.tags = formData.value.tags; + filter.order = this.getOrderBy(formData.value.orderByIndex); + filter.sort = this.getSortBy(formData.value.sortByIndex); + } - saveCurrentFilter(formData, cb) { - const filters = new FileBaseFilters(this.client); - const selectedFilter = this.filtersArray[this.currentFilterIndex]; + saveCurrentFilter(formData, cb) { + const filters = new FileBaseFilters(this.client); + const selectedFilter = this.filtersArray[this.currentFilterIndex]; - if(selectedFilter) { - // *update* currently selected filter - this.setFilterValuesFromFormData(selectedFilter, formData); - filters.replace(selectedFilter.uuid, selectedFilter); - } else { - // add a new entry; note that UUID will be generated - const newFilter = {}; - this.setFilterValuesFromFormData(newFilter, formData); + if(selectedFilter) { + // *update* currently selected filter + this.setFilterValuesFromFormData(selectedFilter, formData); + filters.replace(selectedFilter.uuid, selectedFilter); + } else { + // add a new entry; note that UUID will be generated + const newFilter = {}; + this.setFilterValuesFromFormData(newFilter, formData); - // set current to what we just saved - newFilter.uuid = filters.add(newFilter); + // set current to what we just saved + newFilter.uuid = filters.add(newFilter); - // add to our array (at current index position) - this.filtersArray[this.currentFilterIndex] = newFilter; - } + // add to our array (at current index position) + this.filtersArray[this.currentFilterIndex] = newFilter; + } - return filters.persist(cb); - } + return filters.persist(cb); + } - loadDataForFilter(filterIndex) { - const filter = this.filtersArray[filterIndex]; - if(filter) { - this.setText(MciViewIds.editor.searchTerms, filter.terms); - this.setText(MciViewIds.editor.tags, filter.tags); - this.setText(MciViewIds.editor.filterName, filter.name); + loadDataForFilter(filterIndex) { + const filter = this.filtersArray[filterIndex]; + if(filter) { + this.setText(MciViewIds.editor.searchTerms, filter.terms); + this.setText(MciViewIds.editor.tags, filter.tags); + this.setText(MciViewIds.editor.filterName, filter.name); - this.setAreaIndexFromCurrentFilter(); - this.setSortByFromCurrentFilter(); - this.setOrderByFromCurrentFilter(); - } - } + this.setAreaIndexFromCurrentFilter(); + this.setSortByFromCurrentFilter(); + this.setOrderByFromCurrentFilter(); + } + } }; diff --git a/core/file_area_list.js b/core/file_area_list.js index ae03ee11..8b992f94 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -27,691 +27,691 @@ const moment = require('moment'); const paths = require('path'); exports.moduleInfo = { - name : 'File Area List', - desc : 'Lists contents of file an file area', - author : 'NuSkooler', + name : 'File Area List', + desc : 'Lists contents of file an file area', + author : 'NuSkooler', }; const FormIds = { - browse : 0, - details : 1, - detailsGeneral : 2, - detailsNfo : 3, - detailsFileList : 4, + browse : 0, + details : 1, + detailsGeneral : 2, + detailsNfo : 3, + detailsFileList : 4, }; const MciViewIds = { - browse : { - desc : 1, - navMenu : 2, + browse : { + desc : 1, + navMenu : 2, - customRangeStart : 10, // 10+ = customs - }, - details : { - navMenu : 1, - infoXyTop : 2, // %XY starting position for info area - infoXyBottom : 3, + customRangeStart : 10, // 10+ = customs + }, + details : { + navMenu : 1, + infoXyTop : 2, // %XY starting position for info area + infoXyBottom : 3, - customRangeStart : 10, // 10+ = customs - }, - detailsGeneral : { - customRangeStart : 10, // 10+ = customs - }, - detailsNfo : { - nfo : 1, + customRangeStart : 10, // 10+ = customs + }, + detailsGeneral : { + customRangeStart : 10, // 10+ = customs + }, + detailsNfo : { + nfo : 1, - customRangeStart : 10, // 10+ = customs - }, - detailsFileList : { - fileList : 1, + customRangeStart : 10, // 10+ = customs + }, + detailsFileList : { + fileList : 1, - customRangeStart : 10, // 10+ = customs - }, + customRangeStart : 10, // 10+ = customs + }, }; exports.getModule = class FileAreaList extends MenuModule { - constructor(options) { - super(options); - - this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); - this.fileList = _.get(options, 'extraArgs.fileList'); - this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); - - if(this.fileList) { - // we'll need to adjust position as well! - this.fileListPosition = 0; - } - - this.dlQueue = new DownloadQueue(this.client); - - if(!this.filterCriteria) { - this.filterCriteria = FileBaseFilters.getActiveFilter(this.client); - } - - if(_.isString(this.filterCriteria)) { - this.filterCriteria = JSON.parse(this.filterCriteria); - } - - if(_.has(options, 'lastMenuResult.value')) { - this.lastMenuResultValue = options.lastMenuResult.value; - } - - this.menuMethods = { - nextFile : (formData, extraArgs, cb) => { - if(this.fileListPosition + 1 < this.fileList.length) { - this.fileListPosition += 1; - - return this.displayBrowsePage(true, cb); // true=clerarScreen - } - - if(this.lastFileNextExit) { - return this.prevMenu(cb); - } - - return cb(null); - }, - prevFile : (formData, extraArgs, cb) => { - if(this.fileListPosition > 0) { - --this.fileListPosition; - - return this.displayBrowsePage(true, cb); // true=clearScreen - } - - return cb(null); - }, - viewDetails : (formData, extraArgs, cb) => { - this.viewControllers.browse.setFocus(false); - return this.displayDetailsPage(cb); - }, - detailsQuit : (formData, extraArgs, cb) => { - [ 'detailsNfo', 'detailsFileList', 'details' ].forEach(n => { - const vc = this.viewControllers[n]; - if(vc) { - vc.detachClientEvents(); - } - }); - - return this.displayBrowsePage(true, cb); // true=clearScreen - }, - toggleQueue : (formData, extraArgs, cb) => { - this.dlQueue.toggle(this.currentFileEntry); - this.updateQueueIndicator(); - return cb(null); - }, - showWebDownloadLink : (formData, extraArgs, cb) => { - return this.fetchAndDisplayWebDownloadLink(cb); - }, - displayHelp : (formData, extraArgs, cb) => { - return this.displayHelpPage(cb); - } - }; - } - - enter() { - super.enter(); - } - - leave() { - super.leave(); - } - - getSaveState() { - return { - fileList : this.fileList, - fileListPosition : this.fileListPosition, - }; - } - - restoreSavedState(savedState) { - if(savedState) { - this.fileList = savedState.fileList; - this.fileListPosition = savedState.fileListPosition; - } - } - - updateFileEntryWithMenuResult(cb) { - if(!this.lastMenuResultValue) { - return cb(null); - } - - if(_.isNumber(this.lastMenuResultValue.rating)) { - const fileId = this.fileList[this.fileListPosition]; - FileEntry.persistUserRating(fileId, this.client.user.userId, this.lastMenuResultValue.rating, err => { - if(err) { - this.client.log.warn( { error : err.message, fileId : fileId }, 'Failed to persist file rating' ); - } - return cb(null); - }); - } else { - return cb(null); - } - } - - initSequence() { - const self = this; - - async.series( - [ - function preInit(callback) { - return self.updateFileEntryWithMenuResult(callback); - }, - function beforeArt(callback) { - return self.beforeArt(callback); - }, - function display(callback) { - return self.displayBrowsePage(false, err => { - if(err && 'NORESULTS' === err.reasonCode) { - self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults'); - } - return callback(err); - }); - } - ], - () => { - self.finishedLoading(); - } - ); - } - - populateCurrentEntryInfo(cb) { - const config = this.menuConfig.config; - const currEntry = this.currentFileEntry; - - const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; - const area = FileArea.getFileAreaByTag(currEntry.areaTag); - const hashTagsSep = config.hashTagsSep || ', '; - const isQueuedIndicator = config.isQueuedIndicator || 'Y'; - const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; - - const entryInfo = currEntry.entryInfo = { - fileId : currEntry.fileId, - areaTag : currEntry.areaTag, - areaName : _.get(area, 'name') || 'N/A', - areaDesc : _.get(area, 'desc') || 'N/A', - fileSha256 : currEntry.fileSha256, - fileName : currEntry.fileName, - desc : currEntry.desc || '', - descLong : currEntry.descLong || '', - userRating : currEntry.userRating, - uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), - hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), - isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, - webDlLink : '', // :TODO: fetch web any existing web d/l link - webDlExpire : '', // :TODO: fetch web d/l link expire time - }; - - // - // We need the entry object to contain meta keys even if they are empty as - // consumers may very likely attempt to use them - // - const metaValues = FileEntry.WellKnownMetaValues; - metaValues.forEach(name => { - const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A'; - entryInfo[_.camelCase(name)] = value; - }); - - if(entryInfo.archiveType) { - const mimeType = resolveMimeType(entryInfo.archiveType); - let desc; - if(mimeType) { - let fileType = _.get(Config(), [ 'fileTypes', mimeType ] ); - - if(Array.isArray(fileType)) { - // further refine by extention - fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext); - } - desc = fileType && fileType.desc; - } - entryInfo.archiveTypeDesc = desc || mimeType || entryInfo.archiveType; - } else { - entryInfo.archiveTypeDesc = 'N/A'; - } - - entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported - entryInfo.hashTags = entryInfo.hashTags || '(none)'; - - // create a rating string, e.g. "**---" - const userRatingTicked = config.userRatingTicked || '*'; - const userRatingUnticked = config.userRatingUnticked || ''; - entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! - entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); - if(entryInfo.userRating < 5) { - entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) ); - } - - FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => { - if(err) { - entryInfo.webDlExpire = ''; - if(ErrNotEnabled === err.reasonCode) { - entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; - } else { - entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; - } - } else { - const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - - entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; - entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); - } - - return cb(null); - }); - } - - populateCustomLabels(category, startId) { - return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo); - } - - displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; - - async.waterfall( - [ - function readyAndDisplayArt(callback) { - if(options.clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } - - theme.displayThemedAsset( - config.art[name], - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { - const vcOpts = { - client : self.client, - formId : FormIds[name], - }; - - if(!_.isUndefined(options.noInput)) { - vcOpts.noInput = options.noInput; - } - - const vc = self.addViewController(name, new ViewController(vcOpts)); - - if('details' === name) { - try { - self.detailsInfoArea = { - top : artData.mciMap.XY2.position, - bottom : artData.mciMap.XY3.position, - }; - } catch(e) { - return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!')); - } - } - - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], - }; - - return vc.loadFromMenuConfig(loadOpts, callback); - } - - self.viewControllers[name].setFocus(true); - return callback(null); - - }, - ], - err => { - return cb(err); - } - ); - } - - displayBrowsePage(clearScreen, cb) { - const self = this; - - async.series( - [ - function fetchEntryData(callback) { - if(self.fileList) { - return callback(null); - } - return self.loadFileIds(false, callback); // false=do not force - }, - function checkEmptyResults(callback) { - if(0 === self.fileList.length) { - return callback(Errors.General('No results for criteria', 'NORESULTS')); - } - return callback(null); - }, - function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); - }, - function loadCurrentFileInfo(callback) { - self.currentFileEntry = new FileEntry(); - - self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { - if(err) { - return callback(err); - } - - return self.populateCurrentEntryInfo(callback); - }); - }, - function populateDesc(callback) { - if(_.isString(self.currentFileEntry.desc)) { - const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); - if(descView) { - // - // For descriptions we want to support as many color code systems - // as we can for coverage of what is found in the while (e.g. Renegade - // pipes, PCB @X##, etc.) - // - // MLTEV doesn't support all of this, so convert. If we produced ANSI - // esc sequences, we'll proceed with specialization, else just treat - // it as text. - // - const desc = controlCodesToAnsi(self.currentFileEntry.desc); - if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { - descView.setAnsi( - desc, - { - prepped : false, - forceLineTerm : true - }, - () => { - return callback(null); - } - ); - } else { - descView.setText(self.currentFileEntry.desc); - return callback(null); - } - } - } else { - return callback(null); - } - }, - function populateAdditionalViews(callback) { - self.updateQueueIndicator(); - self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart); - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } - - displayDetailsPage(cb) { - const self = this; - - async.series( - [ - function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback); - }, - function populateViews(callback) { - self.populateCustomLabels('details', MciViewIds.details.customRangeStart); - return callback(null); - }, - function prepSection(callback) { - return self.displayDetailsSection('general', false, callback); - }, - function listenNavChanges(callback) { - const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu); - navMenu.setFocusItemIndex(0); - - navMenu.on('index update', index => { - const sectionName = { - 0 : 'general', - 1 : 'nfo', - 2 : 'fileList', - }[index]; - - if(sectionName) { - self.displayDetailsSection(sectionName, true); - } - }); - - return callback(null); - } - ], - err => { - return cb(err); - } - ); - } - - displayHelpPage(cb) { - this.displayAsset( - this.menuConfig.config.art.help, - { clearScreen : true }, - () => { - this.client.waitForKeyPress( () => { - return this.displayBrowsePage(true, cb); - }); - } - ); - } - - fetchAndDisplayWebDownloadLink(cb) { - const self = this; - - async.series( - [ - function generateLinkIfNeeded(callback) { - - if(self.currentFileEntry.webDlExpireTime < moment()) { - return callback(null); - } - - const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); - - FileAreaWeb.createAndServeTempDownload( - self.client, - self.currentFileEntry, - { expireTime : expireTime }, - (err, url) => { - if(err) { - return callback(err); - } - - self.currentFileEntry.webDlExpireTime = expireTime; - - const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - - self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; - self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); - - return callback(null); - } - ); - }, - function updateActiveViews(callback) { - self.updateCustomViewTextsWithFilter( - 'browse', - MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, - { filter : [ '{webDlLink}', '{webDlExpire}' ] } - ); - return callback(null); - } - ], - err => { - return cb(err); - } - ); - } - - updateQueueIndicator() { - const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; - const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; - - this.currentFileEntry.entryInfo.isQueued = stringFormat( - this.dlQueue.isQueued(this.currentFileEntry) ? - isQueuedIndicator : - isNotQueuedIndicator - ); - - this.updateCustomViewTextsWithFilter( - 'browse', - MciViewIds.browse.customRangeStart, - this.currentFileEntry.entryInfo, - { filter : [ '{isQueued}' ] } - ); - } - - cacheArchiveEntries(cb) { - // check cache - if(this.currentFileEntry.archiveEntries) { - return cb(null, 'cache'); - } - - const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag); - if(!areaInfo) { - return cb(Errors.Invalid('Invalid area tag')); - } - - const filePath = this.currentFileEntry.filePath; - const archiveUtil = ArchiveUtil.getInstance(); - - archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => { - if(err) { - return cb(err); - } - - this.currentFileEntry.archiveEntries = entries; - return cb(null, 're-cached'); - }); - } - - populateFileListing() { - const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); - - if(this.currentFileEntry.entryInfo.archiveType) { - this.cacheArchiveEntries( (err, cacheStatus) => { - if(err) { - // :TODO: Handle me!!! - fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck - return; - } - - if('re-cached' === cacheStatus) { - const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? - const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; - - fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) ); - fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) ); - - fileListView.redraw(); - } - }); - } else { - fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] ); - } - } - - displayDetailsSection(sectionName, clearArea, cb) { - const self = this; - const name = `details${_.upperFirst(sectionName)}`; - - async.series( - [ - function detachPrevious(callback) { - if(self.lastDetailsViewController) { - self.lastDetailsViewController.detachClientEvents(); - } - return callback(null); - }, - function prepArtAndViewController(callback) { - - function gotoTopPos() { - self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1)); - } - - gotoTopPos(); - - if(clearArea) { - self.client.term.rawWrite(ansi.reset()); - - let pos = self.detailsInfoArea.top[0]; - const bottom = self.detailsInfoArea.bottom[0]; - - while(pos++ <= bottom) { - self.client.term.rawWrite(ansi.eraseLine() + ansi.down()); - } - - gotoTopPos(); - } - - return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback); - }, - function populateViews(callback) { - self.lastDetailsViewController = self.viewControllers[name]; - - switch(sectionName) { - case 'nfo' : - { - const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo); - 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); - return callback(null); - } - } - break; - - case 'fileList' : - self.populateFileListing(); - return callback(null); - - default : - return callback(null); - } - }, - function setLabels(callback) { - self.populateCustomLabels(name, MciViewIds[name].customRangeStart); - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } - - loadFileIds(force, cb) { - if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) { - this.fileListPosition = 0; - - const filterCriteria = Object.assign({}, this.filterCriteria); - if(!filterCriteria.areaTag) { - filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client); - } - - FileEntry.findFiles(filterCriteria, (err, fileIds) => { - this.fileList = fileIds; - return cb(err); - }); - } - } + constructor(options) { + super(options); + + this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); + this.fileList = _.get(options, 'extraArgs.fileList'); + this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); + + if(this.fileList) { + // we'll need to adjust position as well! + this.fileListPosition = 0; + } + + this.dlQueue = new DownloadQueue(this.client); + + if(!this.filterCriteria) { + this.filterCriteria = FileBaseFilters.getActiveFilter(this.client); + } + + if(_.isString(this.filterCriteria)) { + this.filterCriteria = JSON.parse(this.filterCriteria); + } + + if(_.has(options, 'lastMenuResult.value')) { + this.lastMenuResultValue = options.lastMenuResult.value; + } + + this.menuMethods = { + nextFile : (formData, extraArgs, cb) => { + if(this.fileListPosition + 1 < this.fileList.length) { + this.fileListPosition += 1; + + return this.displayBrowsePage(true, cb); // true=clerarScreen + } + + if(this.lastFileNextExit) { + return this.prevMenu(cb); + } + + return cb(null); + }, + prevFile : (formData, extraArgs, cb) => { + if(this.fileListPosition > 0) { + --this.fileListPosition; + + return this.displayBrowsePage(true, cb); // true=clearScreen + } + + return cb(null); + }, + viewDetails : (formData, extraArgs, cb) => { + this.viewControllers.browse.setFocus(false); + return this.displayDetailsPage(cb); + }, + detailsQuit : (formData, extraArgs, cb) => { + [ 'detailsNfo', 'detailsFileList', 'details' ].forEach(n => { + const vc = this.viewControllers[n]; + if(vc) { + vc.detachClientEvents(); + } + }); + + return this.displayBrowsePage(true, cb); // true=clearScreen + }, + toggleQueue : (formData, extraArgs, cb) => { + this.dlQueue.toggle(this.currentFileEntry); + this.updateQueueIndicator(); + return cb(null); + }, + showWebDownloadLink : (formData, extraArgs, cb) => { + return this.fetchAndDisplayWebDownloadLink(cb); + }, + displayHelp : (formData, extraArgs, cb) => { + return this.displayHelpPage(cb); + } + }; + } + + enter() { + super.enter(); + } + + leave() { + super.leave(); + } + + getSaveState() { + return { + fileList : this.fileList, + fileListPosition : this.fileListPosition, + }; + } + + restoreSavedState(savedState) { + if(savedState) { + this.fileList = savedState.fileList; + this.fileListPosition = savedState.fileListPosition; + } + } + + updateFileEntryWithMenuResult(cb) { + if(!this.lastMenuResultValue) { + return cb(null); + } + + if(_.isNumber(this.lastMenuResultValue.rating)) { + const fileId = this.fileList[this.fileListPosition]; + FileEntry.persistUserRating(fileId, this.client.user.userId, this.lastMenuResultValue.rating, err => { + if(err) { + this.client.log.warn( { error : err.message, fileId : fileId }, 'Failed to persist file rating' ); + } + return cb(null); + }); + } else { + return cb(null); + } + } + + initSequence() { + const self = this; + + async.series( + [ + function preInit(callback) { + return self.updateFileEntryWithMenuResult(callback); + }, + function beforeArt(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + return self.displayBrowsePage(false, err => { + if(err && 'NORESULTS' === err.reasonCode) { + self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults'); + } + return callback(err); + }); + } + ], + () => { + self.finishedLoading(); + } + ); + } + + populateCurrentEntryInfo(cb) { + const config = this.menuConfig.config; + const currEntry = this.currentFileEntry; + + const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; + const area = FileArea.getFileAreaByTag(currEntry.areaTag); + const hashTagsSep = config.hashTagsSep || ', '; + const isQueuedIndicator = config.isQueuedIndicator || 'Y'; + const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; + + const entryInfo = currEntry.entryInfo = { + fileId : currEntry.fileId, + areaTag : currEntry.areaTag, + areaName : _.get(area, 'name') || 'N/A', + areaDesc : _.get(area, 'desc') || 'N/A', + fileSha256 : currEntry.fileSha256, + fileName : currEntry.fileName, + desc : currEntry.desc || '', + descLong : currEntry.descLong || '', + userRating : currEntry.userRating, + uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), + hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), + isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, + webDlLink : '', // :TODO: fetch web any existing web d/l link + webDlExpire : '', // :TODO: fetch web d/l link expire time + }; + + // + // We need the entry object to contain meta keys even if they are empty as + // consumers may very likely attempt to use them + // + const metaValues = FileEntry.WellKnownMetaValues; + metaValues.forEach(name => { + const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A'; + entryInfo[_.camelCase(name)] = value; + }); + + if(entryInfo.archiveType) { + const mimeType = resolveMimeType(entryInfo.archiveType); + let desc; + if(mimeType) { + let fileType = _.get(Config(), [ 'fileTypes', mimeType ] ); + + if(Array.isArray(fileType)) { + // further refine by extention + fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext); + } + desc = fileType && fileType.desc; + } + entryInfo.archiveTypeDesc = desc || mimeType || entryInfo.archiveType; + } else { + entryInfo.archiveTypeDesc = 'N/A'; + } + + entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported + entryInfo.hashTags = entryInfo.hashTags || '(none)'; + + // create a rating string, e.g. "**---" + const userRatingTicked = config.userRatingTicked || '*'; + const userRatingUnticked = config.userRatingUnticked || ''; + entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! + entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); + if(entryInfo.userRating < 5) { + entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) ); + } + + FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => { + if(err) { + entryInfo.webDlExpire = ''; + if(ErrNotEnabled === err.reasonCode) { + entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; + } else { + entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; + } + } else { + const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + + entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; + entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + } + + return cb(null); + }); + } + + populateCustomLabels(category, startId) { + return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo); + } + + displayArtAndPrepViewController(name, options, cb) { + const self = this; + const config = this.menuConfig.config; + + async.waterfall( + [ + function readyAndDisplayArt(callback) { + if(options.clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } + + theme.displayThemedAsset( + config.art[name], + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function prepeareViewController(artData, callback) { + if(_.isUndefined(self.viewControllers[name])) { + const vcOpts = { + client : self.client, + formId : FormIds[name], + }; + + if(!_.isUndefined(options.noInput)) { + vcOpts.noInput = options.noInput; + } + + const vc = self.addViewController(name, new ViewController(vcOpts)); + + if('details' === name) { + try { + self.detailsInfoArea = { + top : artData.mciMap.XY2.position, + bottom : artData.mciMap.XY3.position, + }; + } catch(e) { + return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!')); + } + } + + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds[name], + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } + + self.viewControllers[name].setFocus(true); + return callback(null); + + }, + ], + err => { + return cb(err); + } + ); + } + + displayBrowsePage(clearScreen, cb) { + const self = this; + + async.series( + [ + function fetchEntryData(callback) { + if(self.fileList) { + return callback(null); + } + return self.loadFileIds(false, callback); // false=do not force + }, + function checkEmptyResults(callback) { + if(0 === self.fileList.length) { + return callback(Errors.General('No results for criteria', 'NORESULTS')); + } + return callback(null); + }, + function prepArtAndViewController(callback) { + return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); + }, + function loadCurrentFileInfo(callback) { + self.currentFileEntry = new FileEntry(); + + self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { + if(err) { + return callback(err); + } + + return self.populateCurrentEntryInfo(callback); + }); + }, + function populateDesc(callback) { + if(_.isString(self.currentFileEntry.desc)) { + const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); + if(descView) { + // + // For descriptions we want to support as many color code systems + // as we can for coverage of what is found in the while (e.g. Renegade + // pipes, PCB @X##, etc.) + // + // MLTEV doesn't support all of this, so convert. If we produced ANSI + // esc sequences, we'll proceed with specialization, else just treat + // it as text. + // + const desc = controlCodesToAnsi(self.currentFileEntry.desc); + if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { + descView.setAnsi( + desc, + { + prepped : false, + forceLineTerm : true + }, + () => { + return callback(null); + } + ); + } else { + descView.setText(self.currentFileEntry.desc); + return callback(null); + } + } + } else { + return callback(null); + } + }, + function populateAdditionalViews(callback) { + self.updateQueueIndicator(); + self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart); + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + + displayDetailsPage(cb) { + const self = this; + + async.series( + [ + function prepArtAndViewController(callback) { + return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback); + }, + function populateViews(callback) { + self.populateCustomLabels('details', MciViewIds.details.customRangeStart); + return callback(null); + }, + function prepSection(callback) { + return self.displayDetailsSection('general', false, callback); + }, + function listenNavChanges(callback) { + const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu); + navMenu.setFocusItemIndex(0); + + navMenu.on('index update', index => { + const sectionName = { + 0 : 'general', + 1 : 'nfo', + 2 : 'fileList', + }[index]; + + if(sectionName) { + self.displayDetailsSection(sectionName, true); + } + }); + + return callback(null); + } + ], + err => { + return cb(err); + } + ); + } + + displayHelpPage(cb) { + this.displayAsset( + this.menuConfig.config.art.help, + { clearScreen : true }, + () => { + this.client.waitForKeyPress( () => { + return this.displayBrowsePage(true, cb); + }); + } + ); + } + + fetchAndDisplayWebDownloadLink(cb) { + const self = this; + + async.series( + [ + function generateLinkIfNeeded(callback) { + + if(self.currentFileEntry.webDlExpireTime < moment()) { + return callback(null); + } + + const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); + + FileAreaWeb.createAndServeTempDownload( + self.client, + self.currentFileEntry, + { expireTime : expireTime }, + (err, url) => { + if(err) { + return callback(err); + } + + self.currentFileEntry.webDlExpireTime = expireTime; + + const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + + self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; + self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); + + return callback(null); + } + ); + }, + function updateActiveViews(callback) { + self.updateCustomViewTextsWithFilter( + 'browse', + MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, + { filter : [ '{webDlLink}', '{webDlExpire}' ] } + ); + return callback(null); + } + ], + err => { + return cb(err); + } + ); + } + + updateQueueIndicator() { + const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; + const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; + + this.currentFileEntry.entryInfo.isQueued = stringFormat( + this.dlQueue.isQueued(this.currentFileEntry) ? + isQueuedIndicator : + isNotQueuedIndicator + ); + + this.updateCustomViewTextsWithFilter( + 'browse', + MciViewIds.browse.customRangeStart, + this.currentFileEntry.entryInfo, + { filter : [ '{isQueued}' ] } + ); + } + + cacheArchiveEntries(cb) { + // check cache + if(this.currentFileEntry.archiveEntries) { + return cb(null, 'cache'); + } + + const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag); + if(!areaInfo) { + return cb(Errors.Invalid('Invalid area tag')); + } + + const filePath = this.currentFileEntry.filePath; + const archiveUtil = ArchiveUtil.getInstance(); + + archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => { + if(err) { + return cb(err); + } + + this.currentFileEntry.archiveEntries = entries; + return cb(null, 're-cached'); + }); + } + + populateFileListing() { + const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); + + if(this.currentFileEntry.entryInfo.archiveType) { + this.cacheArchiveEntries( (err, cacheStatus) => { + if(err) { + // :TODO: Handle me!!! + fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck + return; + } + + if('re-cached' === cacheStatus) { + const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? + const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; + + fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) ); + fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) ); + + fileListView.redraw(); + } + }); + } else { + fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] ); + } + } + + displayDetailsSection(sectionName, clearArea, cb) { + const self = this; + const name = `details${_.upperFirst(sectionName)}`; + + async.series( + [ + function detachPrevious(callback) { + if(self.lastDetailsViewController) { + self.lastDetailsViewController.detachClientEvents(); + } + return callback(null); + }, + function prepArtAndViewController(callback) { + + function gotoTopPos() { + self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1)); + } + + gotoTopPos(); + + if(clearArea) { + self.client.term.rawWrite(ansi.reset()); + + let pos = self.detailsInfoArea.top[0]; + const bottom = self.detailsInfoArea.bottom[0]; + + while(pos++ <= bottom) { + self.client.term.rawWrite(ansi.eraseLine() + ansi.down()); + } + + gotoTopPos(); + } + + return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback); + }, + function populateViews(callback) { + self.lastDetailsViewController = self.viewControllers[name]; + + switch(sectionName) { + case 'nfo' : + { + const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo); + 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); + return callback(null); + } + } + break; + + case 'fileList' : + self.populateFileListing(); + return callback(null); + + default : + return callback(null); + } + }, + function setLabels(callback) { + self.populateCustomLabels(name, MciViewIds[name].customRangeStart); + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + + loadFileIds(force, cb) { + if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) { + this.fileListPosition = 0; + + const filterCriteria = Object.assign({}, this.filterCriteria); + if(!filterCriteria.areaTag) { + filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client); + } + + FileEntry.findFiles(filterCriteria, (err, fileIds) => { + this.fileList = fileIds; + return cb(err); + }); + } + } }; diff --git a/core/file_area_web.js b/core/file_area_web.js index 94a2a664..88c108f6 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -26,470 +26,470 @@ const mimeTypes = require('mime-types'); const yazl = require('yazl'); function notEnabledError() { - return Errors.General('Web server is not enabled', ErrNotEnabled); + return Errors.General('Web server is not enabled', ErrNotEnabled); } class FileAreaWebAccess { - constructor() { - this.hashids = new hashids(Config().general.boardName); - this.expireTimers = {}; // hashId->timer - } + constructor() { + this.hashids = new hashids(Config().general.boardName); + this.expireTimers = {}; // hashId->timer + } - startup(cb) { - const self = this; + startup(cb) { + const self = this; - async.series( - [ - function initFromDb(callback) { - return self.load(callback); - }, - function addWebRoute(callback) { - self.webServer = getServer(webServerPackageName); - if(!self.webServer) { - return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`)); - } + async.series( + [ + function initFromDb(callback) { + return self.load(callback); + }, + function addWebRoute(callback) { + self.webServer = getServer(webServerPackageName); + if(!self.webServer) { + return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`)); + } - if(self.isEnabled()) { - const routeAdded = self.webServer.instance.addRoute({ - method : 'GET', - path : Config().fileBase.web.routePath, - handler : self.routeWebRequest.bind(self), - }); - return callback(routeAdded ? null : Errors.General('Failed adding route')); - } else { - return callback(null); // not enabled, but no error - } - } - ], - err => { - return cb(err); - } - ); - } + if(self.isEnabled()) { + const routeAdded = self.webServer.instance.addRoute({ + method : 'GET', + path : Config().fileBase.web.routePath, + handler : self.routeWebRequest.bind(self), + }); + return callback(routeAdded ? null : Errors.General('Failed adding route')); + } else { + return callback(null); // not enabled, but no error + } + } + ], + err => { + return cb(err); + } + ); + } - shutdown(cb) { - return cb(null); - } + shutdown(cb) { + return cb(null); + } - isEnabled() { - return this.webServer.instance.isEnabled(); - } + isEnabled() { + return this.webServer.instance.isEnabled(); + } - static getHashIdTypes() { - return { - SingleFile : 0, - BatchArchive : 1, - }; - } + static getHashIdTypes() { + return { + SingleFile : 0, + BatchArchive : 1, + }; + } - load(cb) { - // - // Load entries, register expiration timers - // - FileDb.each( - `SELECT hash_id, expire_timestamp + load(cb) { + // + // Load entries, register expiration timers + // + FileDb.each( + `SELECT hash_id, expire_timestamp FROM file_web_serve;`, - (err, row) => { - if(row) { - this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); - } - }, - err => { - return cb(err); - } - ); - } + (err, row) => { + if(row) { + this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); + } + }, + err => { + return cb(err); + } + ); + } - removeEntry(hashId) { - // - // Delete record from DB, and our timer - // - FileDb.run( - `DELETE FROM file_web_serve + removeEntry(hashId) { + // + // Delete record from DB, and our timer + // + FileDb.run( + `DELETE FROM file_web_serve WHERE hash_id = ?;`, - [ hashId ] - ); + [ hashId ] + ); - delete this.expireTimers[hashId]; - } + delete this.expireTimers[hashId]; + } - scheduleExpire(hashId, expireTime) { + scheduleExpire(hashId, expireTime) { - // remove any previous entry for this hashId - const previous = this.expireTimers[hashId]; - if(previous) { - clearTimeout(previous); - delete this.expireTimers[hashId]; - } + // remove any previous entry for this hashId + const previous = this.expireTimers[hashId]; + if(previous) { + clearTimeout(previous); + delete this.expireTimers[hashId]; + } - const timeoutMs = expireTime.diff(moment()); + const timeoutMs = expireTime.diff(moment()); - if(timeoutMs <= 0) { - setImmediate( () => { - this.removeEntry(hashId); - }); - } else { - this.expireTimers[hashId] = setTimeout( () => { - this.removeEntry(hashId); - }, timeoutMs); - } - } + if(timeoutMs <= 0) { + setImmediate( () => { + this.removeEntry(hashId); + }); + } else { + this.expireTimers[hashId] = setTimeout( () => { + this.removeEntry(hashId); + }, timeoutMs); + } + } - loadServedHashId(hashId, cb) { - FileDb.get( - `SELECT expire_timestamp FROM + loadServedHashId(hashId, cb) { + FileDb.get( + `SELECT expire_timestamp FROM file_web_serve WHERE hash_id = ?`, - [ hashId ], - (err, result) => { - if(err || !result) { - return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID')); - } + [ hashId ], + (err, result) => { + if(err || !result) { + return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID')); + } - const decoded = this.hashids.decode(hashId); + const decoded = this.hashids.decode(hashId); - // decode() should provide an array of [ userId, hashIdType, id, ... ] - if(!Array.isArray(decoded) || decoded.length < 3) { - return cb(Errors.Invalid('Invalid or unknown hash ID')); - } + // decode() should provide an array of [ userId, hashIdType, id, ... ] + if(!Array.isArray(decoded) || decoded.length < 3) { + return cb(Errors.Invalid('Invalid or unknown hash ID')); + } - const servedItem = { - hashId : hashId, - userId : decoded[0], - hashIdType : decoded[1], - expireTimestamp : moment(result.expire_timestamp), - }; + const servedItem = { + hashId : hashId, + userId : decoded[0], + hashIdType : decoded[1], + expireTimestamp : moment(result.expire_timestamp), + }; - if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { - servedItem.fileIds = decoded.slice(2); - } + if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { + servedItem.fileIds = decoded.slice(2); + } - return cb(null, servedItem); - } - ); - } + return cb(null, servedItem); + } + ); + } - getSingleFileHashId(client, fileEntry) { - return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] ); - } + getSingleFileHashId(client, fileEntry) { + return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] ); + } - getBatchArchiveHashId(client, batchId) { - return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId); - } + getBatchArchiveHashId(client, batchId) { + return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId); + } - getHashId(client, hashIdType, identifier) { - return this.hashids.encode(client.user.userId, hashIdType, identifier); - } + getHashId(client, hashIdType, identifier) { + return this.hashids.encode(client.user.userId, hashIdType, identifier); + } - buildSingleFileTempDownloadLink(client, fileEntry, hashId) { - hashId = hashId || this.getSingleFileHashId(client, fileEntry); + buildSingleFileTempDownloadLink(client, fileEntry, hashId) { + hashId = hashId || this.getSingleFileHashId(client, fileEntry); - return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); - } + return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); + } - buildBatchArchiveTempDownloadLink(client, hashId) { - return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); - } + buildBatchArchiveTempDownloadLink(client, hashId) { + return this.webServer.instance.buildUrl(`${Config().fileBase.web.path}${hashId}`); + } - getExistingTempDownloadServeItem(client, fileEntry, cb) { - if(!this.isEnabled()) { - return cb(notEnabledError()); - } + getExistingTempDownloadServeItem(client, fileEntry, cb) { + if(!this.isEnabled()) { + return cb(notEnabledError()); + } - const hashId = this.getSingleFileHashId(client, fileEntry); - this.loadServedHashId(hashId, (err, servedItem) => { - if(err) { - return cb(err); - } + const hashId = this.getSingleFileHashId(client, fileEntry); + this.loadServedHashId(hashId, (err, servedItem) => { + if(err) { + return cb(err); + } - servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); + servedItem.url = this.buildSingleFileTempDownloadLink(client, fileEntry); - return cb(null, servedItem); - }); - } + return cb(null, servedItem); + }); + } - _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { - // add/update rec with hash id and (latest) timestamp - dbOrTrans.run( - `REPLACE INTO file_web_serve (hash_id, expire_timestamp) + _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { + // add/update rec with hash id and (latest) timestamp + dbOrTrans.run( + `REPLACE INTO file_web_serve (hash_id, expire_timestamp) VALUES (?, ?);`, - [ hashId, getISOTimestampString(expireTime) ], - err => { - if(err) { - return cb(err); - } + [ hashId, getISOTimestampString(expireTime) ], + err => { + if(err) { + return cb(err); + } - this.scheduleExpire(hashId, expireTime); + this.scheduleExpire(hashId, expireTime); - return cb(null); - } - ); - } + return cb(null); + } + ); + } - createAndServeTempDownload(client, fileEntry, options, cb) { - if(!this.isEnabled()) { - return cb(notEnabledError()); - } + createAndServeTempDownload(client, fileEntry, options, cb) { + if(!this.isEnabled()) { + return cb(notEnabledError()); + } - const hashId = this.getSingleFileHashId(client, fileEntry); - const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId); - options.expireTime = options.expireTime || moment().add(2, 'days'); + const hashId = this.getSingleFileHashId(client, fileEntry); + const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId); + options.expireTime = options.expireTime || moment().add(2, 'days'); - this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => { - return cb(err, url); - }); - } + this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => { + return cb(err, url); + }); + } - createAndServeTempBatchDownload(client, fileEntries, options, cb) { - if(!this.isEnabled()) { - return cb(notEnabledError()); - } + createAndServeTempBatchDownload(client, fileEntries, options, cb) { + if(!this.isEnabled()) { + return cb(notEnabledError()); + } - const batchId = moment().utc().unix(); - const hashId = this.getBatchArchiveHashId(client, batchId); - const url = this.buildBatchArchiveTempDownloadLink(client, hashId); - options.expireTime = options.expireTime || moment().add(2, 'days'); + const batchId = moment().utc().unix(); + const hashId = this.getBatchArchiveHashId(client, batchId); + const url = this.buildBatchArchiveTempDownloadLink(client, hashId); + options.expireTime = options.expireTime || moment().add(2, 'days'); - FileDb.beginTransaction( (err, trans) => { - if(err) { - return cb(err); - } + FileDb.beginTransaction( (err, trans) => { + if(err) { + return cb(err); + } - this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => { - if(err) { - return trans.rollback( () => { - return cb(err); - }); - } + this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => { + if(err) { + return trans.rollback( () => { + return cb(err); + }); + } - async.eachSeries(fileEntries, (entry, nextEntry) => { - trans.run( - `INSERT INTO file_web_serve_batch (hash_id, file_id) + async.eachSeries(fileEntries, (entry, nextEntry) => { + trans.run( + `INSERT INTO file_web_serve_batch (hash_id, file_id) VALUES (?, ?);`, - [ hashId, entry.fileId ], - err => { - return nextEntry(err); - } - ); - }, err => { - trans[err ? 'rollback' : 'commit']( () => { - return cb(err, url); - }); - }); - }); - }); - } + [ hashId, entry.fileId ], + err => { + return nextEntry(err); + } + ); + }, err => { + trans[err ? 'rollback' : 'commit']( () => { + return cb(err, url); + }); + }); + }); + }); + } - fileNotFound(resp) { - return this.webServer.instance.fileNotFound(resp); - } + fileNotFound(resp) { + return this.webServer.instance.fileNotFound(resp); + } - routeWebRequest(req, resp) { - const hashId = paths.basename(req.url); + routeWebRequest(req, resp) { + const hashId = paths.basename(req.url); - Log.debug( { hashId : hashId, url : req.url }, 'File area web request'); + Log.debug( { hashId : hashId, url : req.url }, 'File area web request'); - this.loadServedHashId(hashId, (err, servedItem) => { + this.loadServedHashId(hashId, (err, servedItem) => { - if(err) { - return this.fileNotFound(resp); - } + if(err) { + return this.fileNotFound(resp); + } - const hashIdTypes = FileAreaWebAccess.getHashIdTypes(); - switch(servedItem.hashIdType) { - case hashIdTypes.SingleFile : - return this.routeWebRequestForSingleFile(servedItem, req, resp); + const hashIdTypes = FileAreaWebAccess.getHashIdTypes(); + switch(servedItem.hashIdType) { + case hashIdTypes.SingleFile : + return this.routeWebRequestForSingleFile(servedItem, req, resp); - case hashIdTypes.BatchArchive : - return this.routeWebRequestForBatchArchive(servedItem, req, resp); + case hashIdTypes.BatchArchive : + return this.routeWebRequestForBatchArchive(servedItem, req, resp); - default : - return this.fileNotFound(resp); - } - }); - } + default : + return this.fileNotFound(resp); + } + }); + } - routeWebRequestForSingleFile(servedItem, req, resp) { - Log.debug( { servedItem : servedItem }, 'Single file web request'); + routeWebRequestForSingleFile(servedItem, req, resp) { + Log.debug( { servedItem : servedItem }, 'Single file web request'); - const fileEntry = new FileEntry(); + const fileEntry = new FileEntry(); - servedItem.fileId = servedItem.fileIds[0]; + servedItem.fileId = servedItem.fileIds[0]; - fileEntry.load(servedItem.fileId, err => { - if(err) { - return this.fileNotFound(resp); - } + fileEntry.load(servedItem.fileId, err => { + if(err) { + return this.fileNotFound(resp); + } - const filePath = fileEntry.filePath; - if(!filePath) { - return this.fileNotFound(resp); - } + const filePath = fileEntry.filePath; + if(!filePath) { + return this.fileNotFound(resp); + } - fs.stat(filePath, (err, stats) => { - if(err) { - return this.fileNotFound(resp); - } + fs.stat(filePath, (err, stats) => { + if(err) { + return this.fileNotFound(resp); + } - resp.on('close', () => { - // connection closed *before* the response was fully sent - // :TODO: Log and such - }); + resp.on('close', () => { + // connection closed *before* the response was fully sent + // :TODO: Log and such + }); - resp.on('finish', () => { - // transfer completed fully - this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); - }); + resp.on('finish', () => { + // transfer completed fully + this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); + }); - const headers = { - 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), - 'Content-Length' : stats.size, - 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, - }; + const headers = { + 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), + 'Content-Length' : stats.size, + 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, + }; - const readStream = fs.createReadStream(filePath); - resp.writeHead(200, headers); - return readStream.pipe(resp); - }); - }); - } + const readStream = fs.createReadStream(filePath); + resp.writeHead(200, headers); + return readStream.pipe(resp); + }); + }); + } - routeWebRequestForBatchArchive(servedItem, req, resp) { - Log.debug( { servedItem : servedItem }, 'Batch file web request'); + routeWebRequestForBatchArchive(servedItem, req, resp) { + Log.debug( { servedItem : servedItem }, 'Batch file web request'); - // - // We are going to build an on-the-fly zip file stream of 1:n - // files in the batch. - // - // First, collect all file IDs - // - const self = this; + // + // We are going to build an on-the-fly zip file stream of 1:n + // files in the batch. + // + // First, collect all file IDs + // + const self = this; - async.waterfall( - [ - function fetchFileIds(callback) { - FileDb.all( - `SELECT file_id + async.waterfall( + [ + function fetchFileIds(callback) { + FileDb.all( + `SELECT file_id FROM file_web_serve_batch WHERE hash_id = ?;`, - [ servedItem.hashId ], - (err, fileIdRows) => { - if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { - return callback(Errors.DoesNotExist('Could not get file IDs for batch')); - } + [ servedItem.hashId ], + (err, fileIdRows) => { + if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { + return callback(Errors.DoesNotExist('Could not get file IDs for batch')); + } - return callback(null, fileIdRows.map(r => r.file_id)); - } - ); - }, - function loadFileEntries(fileIds, callback) { - async.map(fileIds, (fileId, nextFileId) => { - const fileEntry = new FileEntry(); - fileEntry.load(fileId, err => { - return nextFileId(err, fileEntry); - }); - }, (err, fileEntries) => { - if(err) { - return callback(Errors.DoesNotExist('Could not load file IDs for batch')); - } + return callback(null, fileIdRows.map(r => r.file_id)); + } + ); + }, + function loadFileEntries(fileIds, callback) { + async.map(fileIds, (fileId, nextFileId) => { + const fileEntry = new FileEntry(); + fileEntry.load(fileId, err => { + return nextFileId(err, fileEntry); + }); + }, (err, fileEntries) => { + if(err) { + return callback(Errors.DoesNotExist('Could not load file IDs for batch')); + } - return callback(null, fileEntries); - }); - }, - function createAndServeStream(fileEntries, callback) { - const filePaths = fileEntries.map(fe => fe.filePath); - Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request'); + return callback(null, fileEntries); + }); + }, + function createAndServeStream(fileEntries, callback) { + const filePaths = fileEntries.map(fe => fe.filePath); + Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request'); - const zipFile = new yazl.ZipFile(); + const zipFile = new yazl.ZipFile(); - zipFile.on('error', err => { - Log.warn( { error : err.message }, 'Error adding file to batch web request archive'); - }); + zipFile.on('error', err => { + Log.warn( { error : err.message }, 'Error adding file to batch web request archive'); + }); - filePaths.forEach(fp => { - zipFile.addFile( - fp, // path to physical file - paths.basename(fp), // filename/path *stored in archive* - { - compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. - } - ); - }); + filePaths.forEach(fp => { + zipFile.addFile( + fp, // path to physical file + paths.basename(fp), // filename/path *stored in archive* + { + compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. + } + ); + }); - zipFile.end( finalZipSize => { - if(-1 === finalZipSize) { - return callback(Errors.UnexpectedState('Unable to acquire final zip size')); - } + zipFile.end( finalZipSize => { + if(-1 === finalZipSize) { + return callback(Errors.UnexpectedState('Unable to acquire final zip size')); + } - resp.on('close', () => { - // connection closed *before* the response was fully sent - // :TODO: Log and such - }); + resp.on('close', () => { + // connection closed *before* the response was fully sent + // :TODO: Log and such + }); - resp.on('finish', () => { - // transfer completed fully - self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); - }); + resp.on('finish', () => { + // transfer completed fully + self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); + }); - const batchFileName = `batch_${servedItem.hashId}.zip`; + const batchFileName = `batch_${servedItem.hashId}.zip`; - const headers = { - 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), - 'Content-Length' : finalZipSize, - 'Content-Disposition' : `attachment; filename="${batchFileName}"`, - }; + const headers = { + 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), + 'Content-Length' : finalZipSize, + 'Content-Disposition' : `attachment; filename="${batchFileName}"`, + }; - resp.writeHead(200, headers); - return zipFile.outputStream.pipe(resp); - }); - } - ], - err => { - if(err) { - // :TODO: Log me! - return this.fileNotFound(resp); - } + resp.writeHead(200, headers); + return zipFile.outputStream.pipe(resp); + }); + } + ], + err => { + if(err) { + // :TODO: Log me! + return this.fileNotFound(resp); + } - // ...otherwise, we would have called resp() already. - } - ); - } + // ...otherwise, we would have called resp() already. + } + ); + } - updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) { - async.waterfall( - [ - function fetchActiveUser(callback) { - const clientForUserId = getConnectionByUserId(userId); - if(clientForUserId) { - return callback(null, clientForUserId.user); - } + updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) { + async.waterfall( + [ + function fetchActiveUser(callback) { + const clientForUserId = getConnectionByUserId(userId); + if(clientForUserId) { + return callback(null, clientForUserId.user); + } - // not online now - look 'em up - User.getUser(userId, (err, assocUser) => { - return callback(err, assocUser); - }); - }, - function updateStats(user, callback) { - StatLog.incrementUserStat(user, 'dl_total_count', 1); - StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes); - StatLog.incrementSystemStat('dl_total_count', 1); - StatLog.incrementSystemStat('dl_total_bytes', dlBytes); + // not online now - look 'em up + User.getUser(userId, (err, assocUser) => { + return callback(err, assocUser); + }); + }, + function updateStats(user, callback) { + StatLog.incrementUserStat(user, 'dl_total_count', 1); + StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes); + StatLog.incrementSystemStat('dl_total_count', 1); + StatLog.incrementSystemStat('dl_total_bytes', dlBytes); - return callback(null, user); - }, - function sendEvent(user, callback) { - Events.emit( - Events.getSystemEvents().UserDownload, - { - user : user, - files : fileEntries, - } - ); - return callback(null); - } - ] - ); - } + return callback(null, user); + }, + function sendEvent(user, callback) { + Events.emit( + Events.getSystemEvents().UserDownload, + { + user : user, + files : fileEntries, + } + ); + return callback(null); + } + ] + ); + } } module.exports = new FileAreaWebAccess(); \ No newline at end of file diff --git a/core/file_base_area.js b/core/file_base_area.js index 6400ed6f..511e11af 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -49,862 +49,862 @@ exports.cleanUpTempSessionItems = cleanUpTempSessionItems; exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; const WellKnownAreaTags = exports.WellKnownAreaTags = { - Invalid : '', - MessageAreaAttach : 'system_message_attachment', - TempDownloads : 'system_temporary_download', + Invalid : '', + MessageAreaAttach : 'system_message_attachment', + TempDownloads : 'system_temporary_download', }; function startup(cb) { - return cleanUpTempSessionItems(cb); + return cleanUpTempSessionItems(cb); } function isInternalArea(areaTag) { - return [ WellKnownAreaTags.MessageAreaAttach, WellKnownAreaTags.TempDownloads ].includes(areaTag); + return [ WellKnownAreaTags.MessageAreaAttach, WellKnownAreaTags.TempDownloads ].includes(areaTag); } function getAvailableFileAreas(client, options) { - options = options || { }; + options = options || { }; - // perform ACS check per conf & omit internal if desired - const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); + // perform ACS check per conf & omit internal if desired + const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); - return _.omitBy(allAreas, areaInfo => { - if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { - return true; - } + return _.omitBy(allAreas, areaInfo => { + if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { + return true; + } - if(options.skipAcsCheck) { - return false; // no ACS checks (below) - } + if(options.skipAcsCheck) { + return false; // no ACS checks (below) + } - if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) { - return true; // omit - } + if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) { + return true; // omit + } - return !client.acs.hasFileAreaRead(areaInfo); - }); + return !client.acs.hasFileAreaRead(areaInfo); + }); } function getAvailableFileAreaTags(client, options) { - return _.map(getAvailableFileAreas(client, options), area => area.areaTag); + return _.map(getAvailableFileAreas(client, options), area => area.areaTag); } function getSortedAvailableFileAreas(client, options) { - const areas = _.map(getAvailableFileAreas(client, options), v => v); - sortAreasOrConfs(areas); - return areas; + const areas = _.map(getAvailableFileAreas(client, options), v => v); + sortAreasOrConfs(areas); + return areas; } function getDefaultFileAreaTag(client, disableAcsCheck) { - const config = Config(); - let defaultArea = _.findKey(config.fileBase, o => o.default); - if(defaultArea) { - const area = config.fileBase.areas[defaultArea]; - if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) { - return defaultArea; - } - } + const config = Config(); + let defaultArea = _.findKey(config.fileBase, o => o.default); + if(defaultArea) { + const area = config.fileBase.areas[defaultArea]; + if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) { + return defaultArea; + } + } - // just use anything we can - defaultArea = _.findKey(config.fileBase.areas, (area, areaTag) => { - return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area)); - }); + // just use anything we can + defaultArea = _.findKey(config.fileBase.areas, (area, areaTag) => { + return WellKnownAreaTags.MessageAreaAttach !== areaTag && (true === disableAcsCheck || client.acs.hasFileAreaRead(area)); + }); - return defaultArea; + return defaultArea; } function getFileAreaByTag(areaTag) { - const areaInfo = Config().fileBase.areas[areaTag]; - if(areaInfo) { - areaInfo.areaTag = areaTag; // convienence! - areaInfo.storage = getAreaStorageLocations(areaInfo); - return areaInfo; - } + const areaInfo = Config().fileBase.areas[areaTag]; + if(areaInfo) { + areaInfo.areaTag = areaTag; // convienence! + areaInfo.storage = getAreaStorageLocations(areaInfo); + return areaInfo; + } } function changeFileAreaWithOptions(client, areaTag, options, cb) { - async.waterfall( - [ - function getArea(callback) { - const area = getFileAreaByTag(areaTag); - return callback(area ? null : Errors.Invalid('Invalid file areaTag'), area); - }, - function validateAccess(area, callback) { - if(!client.acs.hasFileAreaRead(area)) { - return callback(Errors.AccessDenied('No access to this area')); - } - }, - function changeArea(area, callback) { - if(true === options.persist) { - client.user.persistProperty('file_area_tag', areaTag, err => { - return callback(err, area); - }); - } else { - client.user.properties['file_area_tag'] = areaTag; - return callback(null, area); - } - } - ], - (err, area) => { - if(!err) { - client.log.info( { areaTag : areaTag, area : area }, 'Current file area changed'); - } else { - client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change file area'); - } + async.waterfall( + [ + function getArea(callback) { + const area = getFileAreaByTag(areaTag); + return callback(area ? null : Errors.Invalid('Invalid file areaTag'), area); + }, + function validateAccess(area, callback) { + if(!client.acs.hasFileAreaRead(area)) { + return callback(Errors.AccessDenied('No access to this area')); + } + }, + function changeArea(area, callback) { + if(true === options.persist) { + client.user.persistProperty('file_area_tag', areaTag, err => { + return callback(err, area); + }); + } else { + client.user.properties['file_area_tag'] = areaTag; + return callback(null, area); + } + } + ], + (err, area) => { + if(!err) { + client.log.info( { areaTag : areaTag, area : area }, 'Current file area changed'); + } else { + client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change file area'); + } - return cb(err); - } - ); + return cb(err); + } + ); } function isValidStorageTag(storageTag) { - return storageTag in Config().fileBase.storageTags; + return storageTag in Config().fileBase.storageTags; } function getAreaStorageDirectoryByTag(storageTag) { - const config = Config(); - const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); + const config = Config(); + const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); - return paths.resolve(config.fileBase.areaStoragePrefix, storageLocation || ''); + return paths.resolve(config.fileBase.areaStoragePrefix, storageLocation || ''); } function getAreaDefaultStorageDirectory(areaInfo) { - return getAreaStorageDirectoryByTag(areaInfo.storageTags[0]); + return getAreaStorageDirectoryByTag(areaInfo.storageTags[0]); } function getAreaStorageLocations(areaInfo) { - const storageTags = Array.isArray(areaInfo.storageTags) ? - areaInfo.storageTags : - [ areaInfo.storageTags || '' ]; + const storageTags = Array.isArray(areaInfo.storageTags) ? + areaInfo.storageTags : + [ areaInfo.storageTags || '' ]; - const avail = Config().fileBase.storageTags; + const avail = Config().fileBase.storageTags; - return _.compact(storageTags.map(storageTag => { - if(avail[storageTag]) { - return { - storageTag : storageTag, - dir : getAreaStorageDirectoryByTag(storageTag), - }; - } - })); + return _.compact(storageTags.map(storageTag => { + if(avail[storageTag]) { + return { + storageTag : storageTag, + dir : getAreaStorageDirectoryByTag(storageTag), + }; + } + })); } function getFileEntryPath(fileEntry) { - const areaInfo = getFileAreaByTag(fileEntry.areaTag); - if(areaInfo) { - return paths.join(areaInfo.storageDirectory, fileEntry.fileName); - } + const areaInfo = getFileAreaByTag(fileEntry.areaTag); + if(areaInfo) { + return paths.join(areaInfo.storageDirectory, fileEntry.fileName); + } } function getExistingFileEntriesBySha256(sha256, cb) { - const entries = []; + const entries = []; - FileDb.each( - `SELECT file_id, area_tag + FileDb.each( + `SELECT file_id, area_tag FROM file WHERE file_sha256=?;`, - [ sha256 ], - (err, fileRow) => { - if(fileRow) { - entries.push({ - fileId : fileRow.file_id, - areaTag : fileRow.area_tag, - }); - } - }, - err => { - return cb(err, entries); - } - ); + [ sha256 ], + (err, fileRow) => { + if(fileRow) { + entries.push({ + fileId : fileRow.file_id, + areaTag : fileRow.area_tag, + }); + } + }, + err => { + return cb(err, entries); + } + ); } // :TODO: This is bascially sliceAtEOF() from art.js .... DRY! function sliceAtSauceMarker(data) { - let eof = data.length; - const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) + let eof = data.length; + const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) - for(let i = eof - 1; i > stopPos; i--) { - if(0x1a === data[i]) { - eof = i; - break; - } - } - return data.slice(0, eof); + for(let i = eof - 1; i > stopPos; i--) { + if(0x1a === data[i]) { + eof = i; + break; + } + } + return data.slice(0, eof); } function attemptSetEstimatedReleaseDate(fileEntry) { - // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time - const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); + // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time + const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); - function getMatch(input) { - if(input) { - let m; - for(let i = 0; i < patterns.length; ++i) { - m = patterns[i].exec(input); - if(m) { - return m; - } - } - } - } + function getMatch(input) { + if(input) { + let m; + for(let i = 0; i < patterns.length; ++i) { + m = patterns[i].exec(input); + if(m) { + return m; + } + } + } + } - // - // We attempt detection in short -> long order - // - // Throw out anything that is current_year + 2 (we give some leway) - // with the assumption that must be wrong. - // - const maxYear = moment().add(2, 'year').year(); - const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); + // + // We attempt detection in short -> long order + // + // Throw out anything that is current_year + 2 (we give some leway) + // with the assumption that must be wrong. + // + const maxYear = moment().add(2, 'year').year(); + const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); - if(match && match[1]) { - let year; - if(2 === match[1].length) { - year = parseInt(match[1]); - if(year) { - if(year > 70) { - year += 1900; - } else { - year += 2000; - } - } - } else { - year = parseInt(match[1]); - } + if(match && match[1]) { + let year; + if(2 === match[1].length) { + year = parseInt(match[1]); + if(year) { + if(year > 70) { + year += 1900; + } else { + year += 2000; + } + } + } else { + year = parseInt(match[1]); + } - if(year && year <= maxYear) { - fileEntry.meta.est_release_year = year; - } - } + if(year && year <= maxYear) { + fileEntry.meta.est_release_year = year; + } + } } // a simple log proxy for when we call from oputil.js function logDebug(obj, msg) { - if(Log) { - Log.debug(obj, msg); - } + if(Log) { + Log.debug(obj, msg); + } } function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { - async.waterfall( - [ - function extractDescFiles(callback) { - // :TODO: would be nice if these RegExp's were cached - // :TODO: this is long winded... - const config = Config(); - const extractList = []; + async.waterfall( + [ + function extractDescFiles(callback) { + // :TODO: would be nice if these RegExp's were cached + // :TODO: this is long winded... + const config = Config(); + const extractList = []; - const shortDescFile = archiveEntries.find( e => { - return config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); - }); + const shortDescFile = archiveEntries.find( e => { + return config.fileBase.fileNamePatterns.desc.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + }); - if(shortDescFile) { - extractList.push(shortDescFile.fileName); - } + if(shortDescFile) { + extractList.push(shortDescFile.fileName); + } - const longDescFile = archiveEntries.find( e => { - return config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) ); - }); + const longDescFile = archiveEntries.find( e => { + return config.fileBase.fileNamePatterns.descLong.find( pat => new RegExp(pat, 'i').test(e.fileName) ); + }); - if(longDescFile) { - extractList.push(longDescFile.fileName); - } + if(longDescFile) { + extractList.push(longDescFile.fileName); + } - if(0 === extractList.length) { - return callback(null, [] ); - } + if(0 === extractList.length) { + return callback(null, [] ); + } - temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => { - if(err) { - return callback(err); - } + temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => { + if(err) { + return callback(err); + } - const archiveUtil = ArchiveUtil.getInstance(); - archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { - if(err) { - return callback(err); - } + const archiveUtil = ArchiveUtil.getInstance(); + archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { + if(err) { + return callback(err); + } - const descFiles = { - desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, - descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null, - }; + const descFiles = { + desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, + descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null, + }; - return callback(null, descFiles); - }); - }); - }, - function readDescFiles(descFiles, callback) { - const config = Config(); - async.each(Object.keys(descFiles), (descType, next) => { - const path = descFiles[descType]; - if(!path) { - return next(null); - } + return callback(null, descFiles); + }); + }); + }, + function readDescFiles(descFiles, callback) { + const config = Config(); + async.each(Object.keys(descFiles), (descType, next) => { + const path = descFiles[descType]; + if(!path) { + return next(null); + } - fs.stat(path, (err, stats) => { - if(err) { - return next(null); - } + fs.stat(path, (err, stats) => { + if(err) { + return next(null); + } - // skip entries that are too large - const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; - if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) { - logDebug( { byteSize : stats.size, maxByteSize : config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); - return next(null); - } + // skip entries that are too large + const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; + if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) { + logDebug( { byteSize : stats.size, maxByteSize : config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); + return next(null); + } - fs.readFile(path, (err, data) => { - if(err || !data) { - return next(null); - } + fs.readFile(path, (err, data) => { + if(err || !data) { + return next(null); + } - // - // Assume FILE_ID.DIZ, NFO files, etc. are CP437. - // - // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... - fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); - fileEntry[`${descType}Src`] = 'descFile'; - return next(null); - }); - }); - }, () => { - // cleanup but don't wait - temptmp.cleanup( paths => { - // note: don't use client logger here - may not be avail - logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); - }); - return callback(null); - }); - }, - ], - err => { - return cb(err); - } - ); + // + // Assume FILE_ID.DIZ, NFO files, etc. are CP437. + // + // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... + fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); + fileEntry[`${descType}Src`] = 'descFile'; + return next(null); + }); + }); + }, () => { + // cleanup but don't wait + temptmp.cleanup( paths => { + // note: don't use client logger here - may not be avail + logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); + }); + return callback(null); + }); + }, + ], + err => { + return cb(err); + } + ); } function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries, cb) { - async.waterfall( - [ - function extractToTemp(callback) { - // :TODO: we may want to skip this if the compressed file is too large... - temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => { - if(err) { - return callback(err); - } + async.waterfall( + [ + function extractToTemp(callback) { + // :TODO: we may want to skip this if the compressed file is too large... + temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => { + if(err) { + return callback(err); + } - const archiveUtil = ArchiveUtil.getInstance(); + const archiveUtil = ArchiveUtil.getInstance(); - // ensure we only extract one - there should only be one anyway -- we also just need the fileName - const extractList = archiveEntries.slice(0, 1).map(entry => entry.fileName); + // ensure we only extract one - there should only be one anyway -- we also just need the fileName + const extractList = archiveEntries.slice(0, 1).map(entry => entry.fileName); - archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { - if(err) { - return callback(err); - } + archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { + if(err) { + return callback(err); + } - return callback(null, paths.join(tempDir, extractList[0])); - }); - }); - }, - function processSingleExtractedFile(extractedFile, callback) { - populateFileEntryInfoFromFile(fileEntry, extractedFile, err => { - if(!fileEntry.desc) { - fileEntry.desc = getDescFromFileName(filePath); - fileEntry.descSrc = 'fileName'; - } - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); + return callback(null, paths.join(tempDir, extractList[0])); + }); + }); + }, + function processSingleExtractedFile(extractedFile, callback) { + populateFileEntryInfoFromFile(fileEntry, extractedFile, err => { + if(!fileEntry.desc) { + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; + } + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); } function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) { - const archiveUtil = ArchiveUtil.getInstance(); - const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() + const archiveUtil = ArchiveUtil.getInstance(); + const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() - async.waterfall( - [ - function getArchiveFileList(callback) { - stepInfo.step = 'archive_list_start'; + async.waterfall( + [ + function getArchiveFileList(callback) { + stepInfo.step = 'archive_list_start'; - iterator(err => { - if(err) { - return callback(err); - } + iterator(err => { + if(err) { + return callback(err); + } - archiveUtil.listEntries(filePath, archiveType, (err, entries) => { - if(err) { - stepInfo.step = 'archive_list_failed'; - } else { - stepInfo.step = 'archive_list_finish'; - stepInfo.archiveEntries = entries || []; - } + archiveUtil.listEntries(filePath, archiveType, (err, entries) => { + if(err) { + stepInfo.step = 'archive_list_failed'; + } else { + stepInfo.step = 'archive_list_finish'; + stepInfo.archiveEntries = entries || []; + } - iterator(iterErr => { - return callback( iterErr, entries || [] ); // ignore original |err| here - }); - }); - }); - }, - function processDescFilesStart(entries, callback) { - stepInfo.step = 'desc_files_start'; - iterator(err => { - return callback(err, entries); - }); - }, - function extractDescFromArchive(entries, callback) { - // - // If we have a -single- entry in the archive, extract that file - // and try retrieving info in the non-archive manor. This should - // work for things like zipped up .pdf files. - // - // Otherwise, try to find particular desc files such as FILE_ID.DIZ - // and README.1ST - // - const archDescHandler = (1 === entries.length) ? extractAndProcessSingleArchiveEntry : extractAndProcessDescFiles; - archDescHandler(fileEntry, filePath, entries, err => { - return callback(err); - }); - }, - function attemptReleaseYearEstimation(callback) { - attemptSetEstimatedReleaseDate(fileEntry); - return callback(null); - }, - function processDescFilesFinish(callback) { - stepInfo.step = 'desc_files_finish'; - return iterator(callback); - }, - ], - err => { - return cb(err); - } - ); + iterator(iterErr => { + return callback( iterErr, entries || [] ); // ignore original |err| here + }); + }); + }); + }, + function processDescFilesStart(entries, callback) { + stepInfo.step = 'desc_files_start'; + iterator(err => { + return callback(err, entries); + }); + }, + function extractDescFromArchive(entries, callback) { + // + // If we have a -single- entry in the archive, extract that file + // and try retrieving info in the non-archive manor. This should + // work for things like zipped up .pdf files. + // + // Otherwise, try to find particular desc files such as FILE_ID.DIZ + // and README.1ST + // + const archDescHandler = (1 === entries.length) ? extractAndProcessSingleArchiveEntry : extractAndProcessDescFiles; + archDescHandler(fileEntry, filePath, entries, err => { + return callback(err); + }); + }, + function attemptReleaseYearEstimation(callback) { + attemptSetEstimatedReleaseDate(fileEntry); + return callback(null); + }, + function processDescFilesFinish(callback) { + stepInfo.step = 'desc_files_finish'; + return iterator(callback); + }, + ], + err => { + return cb(err); + } + ); } function getInfoExtractUtilForDesc(mimeType, filePath, descType) { - const config = Config(); - let fileType = _.get(config, [ 'fileTypes', mimeType ] ); + const config = Config(); + let fileType = _.get(config, [ 'fileTypes', mimeType ] ); - if(Array.isArray(fileType)) { - // further refine by extention - fileType = fileType.find(ft => paths.extname(filePath) === ft.ext); - } + if(Array.isArray(fileType)) { + // further refine by extention + fileType = fileType.find(ft => paths.extname(filePath) === ft.ext); + } - if(!_.isObject(fileType)) { - return; - } + if(!_.isObject(fileType)) { + return; + } - let util = _.get(fileType, `${descType}DescUtil`); - if(!_.isString(util)) { - return; - } + let util = _.get(fileType, `${descType}DescUtil`); + if(!_.isString(util)) { + return; + } - util = _.get(config, [ 'infoExtractUtils', util ]); - if(!util || !_.isString(util.cmd)) { - return; - } + util = _.get(config, [ 'infoExtractUtils', util ]); + if(!util || !_.isString(util.cmd)) { + return; + } - return util; + return util; } function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { - const mimeType = resolveMimeType(filePath); - if(!mimeType) { - return cb(null); - } + const mimeType = resolveMimeType(filePath); + if(!mimeType) { + return cb(null); + } - async.eachSeries( [ 'short', 'long' ], (descType, nextDesc) => { - const util = getInfoExtractUtilForDesc(mimeType, filePath, descType); - if(!util) { - return nextDesc(null); - } + async.eachSeries( [ 'short', 'long' ], (descType, nextDesc) => { + const util = getInfoExtractUtilForDesc(mimeType, filePath, descType); + if(!util) { + return nextDesc(null); + } - const args = (util.args || [ '{filePath}'] ).map( arg => stringFormat(arg, { filePath : filePath } ) ); + const args = (util.args || [ '{filePath}'] ).map( arg => stringFormat(arg, { filePath : filePath } ) ); - execFile(util.cmd, args, { timeout : 1000 * 30 }, (err, stdout) => { - if(err || !stdout) { - const reason = err ? err.message : 'No description produced'; - logDebug( - { reason : reason, cmd : util.cmd, args : args }, - `${_.upperFirst(descType)} description command failed` - ); - } else { - stdout = (stdout || '').trim(); - if(stdout.length > 0) { - const key = 'short' === descType ? 'desc' : 'descLong'; - if('desc' === key) { - // - // Word wrap short descriptions to FILE_ID.DIZ spec - // - // "...no more than 45 characters long" - // - // See http://www.textfiles.com/computers/fileid.txt - // - stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n'); - } + execFile(util.cmd, args, { timeout : 1000 * 30 }, (err, stdout) => { + if(err || !stdout) { + const reason = err ? err.message : 'No description produced'; + logDebug( + { reason : reason, cmd : util.cmd, args : args }, + `${_.upperFirst(descType)} description command failed` + ); + } else { + stdout = (stdout || '').trim(); + if(stdout.length > 0) { + const key = 'short' === descType ? 'desc' : 'descLong'; + if('desc' === key) { + // + // Word wrap short descriptions to FILE_ID.DIZ spec + // + // "...no more than 45 characters long" + // + // See http://www.textfiles.com/computers/fileid.txt + // + stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n'); + } - fileEntry[key] = stdout; - fileEntry[`${key}Src`] = 'infoTool'; - } - } + fileEntry[key] = stdout; + fileEntry[`${key}Src`] = 'infoTool'; + } + } - return nextDesc(null); - }); - }, () => { - return cb(null); - }); + return nextDesc(null); + }); + }, () => { + return cb(null); + }); } function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb) { - async.series( - [ - function processDescFilesStart(callback) { - stepInfo.step = 'desc_files_start'; - return iterator(callback); - }, - function getDescriptions(callback) { - populateFileEntryInfoFromFile(fileEntry, filePath, err => { - if(!fileEntry.desc) { - fileEntry.desc = getDescFromFileName(filePath); - fileEntry.descSrc = 'fileName'; - } - return callback(err); - }); - }, - function processDescFilesFinish(callback) { - stepInfo.step = 'desc_files_finish'; - return iterator(callback); - }, - ], - err => { - return cb(err); - } - ); + async.series( + [ + function processDescFilesStart(callback) { + stepInfo.step = 'desc_files_start'; + return iterator(callback); + }, + function getDescriptions(callback) { + populateFileEntryInfoFromFile(fileEntry, filePath, err => { + if(!fileEntry.desc) { + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; + } + return callback(err); + }); + }, + function processDescFilesFinish(callback) { + stepInfo.step = 'desc_files_finish'; + return iterator(callback); + }, + ], + err => { + return cb(err); + } + ); } function addNewFileEntry(fileEntry, filePath, cb) { - // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data + // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data - async.series( - [ - function addNewDbRecord(callback) { - return fileEntry.persist(callback); - } - ], - err => { - return cb(err); - } - ); + async.series( + [ + function addNewDbRecord(callback) { + return fileEntry.persist(callback); + } + ], + err => { + return cb(err); + } + ); } const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ]; function scanFile(filePath, options, iterator, cb) { - if(3 === arguments.length && _.isFunction(iterator)) { - cb = iterator; - iterator = null; - } else if(2 === arguments.length && _.isFunction(options)) { - cb = options; - iterator = null; - options = {}; - } + if(3 === arguments.length && _.isFunction(iterator)) { + cb = iterator; + iterator = null; + } else if(2 === arguments.length && _.isFunction(options)) { + cb = options; + iterator = null; + options = {}; + } - const fileEntry = new FileEntry({ - areaTag : options.areaTag, - meta : options.meta, - hashTags : options.hashTags, // Set() or Array - fileName : paths.basename(filePath), - storageTag : options.storageTag, - fileSha256 : options.sha256, // caller may know this already - }); + const fileEntry = new FileEntry({ + areaTag : options.areaTag, + meta : options.meta, + hashTags : options.hashTags, // Set() or Array + fileName : paths.basename(filePath), + storageTag : options.storageTag, + fileSha256 : options.sha256, // caller may know this already + }); - const stepInfo = { - filePath : filePath, - fileName : paths.basename(filePath), - }; + const stepInfo = { + filePath : filePath, + fileName : paths.basename(filePath), + }; - const callIter = (next) => { - return iterator ? iterator(stepInfo, next) : next(null); - }; + const callIter = (next) => { + return iterator ? iterator(stepInfo, next) : next(null); + }; - const readErrorCallIter = (origError, next) => { - stepInfo.step = 'read_error'; - stepInfo.error = origError.message; + const readErrorCallIter = (origError, next) => { + stepInfo.step = 'read_error'; + stepInfo.error = origError.message; - callIter( () => { - return next(origError); - }); - }; + callIter( () => { + return next(origError); + }); + }; - let lastCalcHashPercent; + let lastCalcHashPercent; - // don't re-calc hashes for any we already have in |options| - const hashesToCalc = HASH_NAMES.filter(hn => { - if('sha256' === hn && fileEntry.fileSha256) { - return false; - } + // don't re-calc hashes for any we already have in |options| + const hashesToCalc = HASH_NAMES.filter(hn => { + if('sha256' === hn && fileEntry.fileSha256) { + return false; + } - if(`file_${hn}` in fileEntry.meta) { - return false; - } + if(`file_${hn}` in fileEntry.meta) { + return false; + } - return true; - }); + return true; + }); - async.waterfall( - [ - function startScan(callback) { - fs.stat(filePath, (err, stats) => { - if(err) { - return readErrorCallIter(err, callback); - } + async.waterfall( + [ + function startScan(callback) { + fs.stat(filePath, (err, stats) => { + if(err) { + return readErrorCallIter(err, callback); + } - stepInfo.step = 'start'; - stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; + stepInfo.step = 'start'; + stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; - return callIter(callback); - }); - }, - function processPhysicalFileGeneric(callback) { - stepInfo.bytesProcessed = 0; + return callIter(callback); + }); + }, + function processPhysicalFileGeneric(callback) { + stepInfo.bytesProcessed = 0; - const hashes = {}; - hashesToCalc.forEach(hashName => { - if('crc32' === hashName) { - hashes.crc32 = new CRC32; - } else { - hashes[hashName] = crypto.createHash(hashName); - } - }); + const hashes = {}; + hashesToCalc.forEach(hashName => { + if('crc32' === hashName) { + hashes.crc32 = new CRC32; + } else { + hashes[hashName] = crypto.createHash(hashName); + } + }); - const updateHashes = (data) => { - for(let i = 0; i < hashesToCalc.length; ++i) { - hashes[hashesToCalc[i]].update(data); - } - }; + const updateHashes = (data) => { + for(let i = 0; i < hashesToCalc.length; ++i) { + hashes[hashesToCalc[i]].update(data); + } + }; - // - // Note that we are not using fs.createReadStream() here: - // While convenient, it is quite a bit slower -- which adds - // up to many seconds in time for larger files. - // - const chunkSize = 1024 * 64; - const buffer = new Buffer(chunkSize); + // + // Note that we are not using fs.createReadStream() here: + // While convenient, it is quite a bit slower -- which adds + // up to many seconds in time for larger files. + // + const chunkSize = 1024 * 64; + const buffer = new Buffer(chunkSize); - fs.open(filePath, 'r', (err, fd) => { - if(err) { - return readErrorCallIter(err, callback); - } + fs.open(filePath, 'r', (err, fd) => { + if(err) { + return readErrorCallIter(err, callback); + } - const nextChunk = () => { - fs.read(fd, buffer, 0, chunkSize, null, (err, bytesRead) => { - if(err) { - fs.close(fd); - return readErrorCallIter(err, callback); - } + const nextChunk = () => { + fs.read(fd, buffer, 0, chunkSize, null, (err, bytesRead) => { + if(err) { + fs.close(fd); + return readErrorCallIter(err, callback); + } - if(0 === bytesRead) { - // done - finalize - fileEntry.meta.byte_size = stepInfo.bytesProcessed; + if(0 === bytesRead) { + // done - finalize + fileEntry.meta.byte_size = stepInfo.bytesProcessed; - for(let i = 0; i < hashesToCalc.length; ++i) { - const hashName = hashesToCalc[i]; - if('sha256' === hashName) { - stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); - } else if('sha1' === hashName || 'md5' === hashName) { - stepInfo[hashName] = fileEntry.meta[`file_${hashName}`] = hashes[hashName].digest('hex'); - } else if('crc32' === hashName) { - stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16); - } - } + for(let i = 0; i < hashesToCalc.length; ++i) { + const hashName = hashesToCalc[i]; + if('sha256' === hashName) { + stepInfo.sha256 = fileEntry.fileSha256 = hashes.sha256.digest('hex'); + } else if('sha1' === hashName || 'md5' === hashName) { + stepInfo[hashName] = fileEntry.meta[`file_${hashName}`] = hashes[hashName].digest('hex'); + } else if('crc32' === hashName) { + stepInfo.crc32 = fileEntry.meta.file_crc32 = hashes.crc32.finalize().toString(16); + } + } - stepInfo.step = 'hash_finish'; - fs.close(fd); - return callIter(callback); - } + stepInfo.step = 'hash_finish'; + fs.close(fd); + return callIter(callback); + } - stepInfo.bytesProcessed += bytesRead; - stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); + stepInfo.bytesProcessed += bytesRead; + stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); - // - // Only send 'hash_update' step update if we have a noticable percentage change in progress - // - const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; - if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { - updateHashes(data); - return nextChunk(); - } else { - lastCalcHashPercent = stepInfo.calcHashPercent; - stepInfo.step = 'hash_update'; + // + // Only send 'hash_update' step update if we have a noticable percentage change in progress + // + const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; + if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { + updateHashes(data); + return nextChunk(); + } else { + lastCalcHashPercent = stepInfo.calcHashPercent; + stepInfo.step = 'hash_update'; - callIter(err => { - if(err) { - return callback(err); - } + callIter(err => { + if(err) { + return callback(err); + } - updateHashes(data); - return nextChunk(); - }); - } - }); - }; + updateHashes(data); + return nextChunk(); + }); + } + }); + }; - nextChunk(); - }); - }, - function processPhysicalFileByType(callback) { - const archiveUtil = ArchiveUtil.getInstance(); + nextChunk(); + }); + }, + function processPhysicalFileByType(callback) { + const archiveUtil = ArchiveUtil.getInstance(); - archiveUtil.detectType(filePath, (err, archiveType) => { - if(archiveType) { - // save this off - fileEntry.meta.archive_type = archiveType; + archiveUtil.detectType(filePath, (err, archiveType) => { + if(archiveType) { + // save this off + fileEntry.meta.archive_type = archiveType; - populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { - if(err) { - populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - if(err) { - logDebug( { error : err.message }, 'Non-archive file entry population failed'); - } - return callback(null); // ignore err - }); - } else { - return callback(null); - } - }); - } else { - populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { - if(err) { - logDebug( { error : err.message }, 'Non-archive file entry population failed'); - } - return callback(null); // ignore err - }); - } - }); - }, - function fetchExistingEntry(callback) { - getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => { - return callback(err, dupeEntries); - }); - }, - function finished(dupeEntries, callback) { - stepInfo.step = 'finished'; - callIter( () => { - return callback(null, dupeEntries); - }); - } - ], - (err, dupeEntries) => { - if(err) { - return cb(err); - } + populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { + if(err) { + populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } + return callback(null); // ignore err + }); + } else { + return callback(null); + } + }); + } else { + populateFileEntryNonArchive(fileEntry, filePath, stepInfo, callIter, err => { + if(err) { + logDebug( { error : err.message }, 'Non-archive file entry population failed'); + } + return callback(null); // ignore err + }); + } + }); + }, + function fetchExistingEntry(callback) { + getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => { + return callback(err, dupeEntries); + }); + }, + function finished(dupeEntries, callback) { + stepInfo.step = 'finished'; + callIter( () => { + return callback(null, dupeEntries); + }); + } + ], + (err, dupeEntries) => { + if(err) { + return cb(err); + } - return cb(null, fileEntry, dupeEntries); - } - ); + return cb(null, fileEntry, dupeEntries); + } + ); } function scanFileAreaForChanges(areaInfo, options, iterator, cb) { - if(3 === arguments.length && _.isFunction(iterator)) { - cb = iterator; - iterator = null; - } else if(2 === arguments.length && _.isFunction(options)) { - cb = options; - iterator = null; - options = {}; - } + if(3 === arguments.length && _.isFunction(iterator)) { + cb = iterator; + iterator = null; + } else if(2 === arguments.length && _.isFunction(options)) { + cb = options; + iterator = null; + options = {}; + } - const storageLocations = getAreaStorageLocations(areaInfo); + const storageLocations = getAreaStorageLocations(areaInfo); - async.eachSeries(storageLocations, (storageLoc, nextLocation) => { - async.series( - [ - function scanPhysFiles(callback) { - const physDir = storageLoc.dir; + async.eachSeries(storageLocations, (storageLoc, nextLocation) => { + async.series( + [ + function scanPhysFiles(callback) { + const physDir = storageLoc.dir; - fs.readdir(physDir, (err, files) => { - if(err) { - return callback(err); - } + fs.readdir(physDir, (err, files) => { + if(err) { + return callback(err); + } - async.eachSeries(files, (fileName, nextFile) => { - const fullPath = paths.join(physDir, fileName); + async.eachSeries(files, (fileName, nextFile) => { + const fullPath = paths.join(physDir, fileName); - fs.stat(fullPath, (err, stats) => { - if(err) { - // :TODO: Log me! - return nextFile(null); // always try next file - } + fs.stat(fullPath, (err, stats) => { + if(err) { + // :TODO: Log me! + return nextFile(null); // always try next file + } - if(!stats.isFile()) { - return nextFile(null); - } + if(!stats.isFile()) { + return nextFile(null); + } - scanFile( - fullPath, - { - areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag - }, - iterator, - (err, fileEntry, dupeEntries) => { - if(err) { - // :TODO: Log me!!! - return nextFile(null); // try next anyway - } + scanFile( + fullPath, + { + areaTag : areaInfo.areaTag, + storageTag : storageLoc.storageTag + }, + iterator, + (err, fileEntry, dupeEntries) => { + if(err) { + // :TODO: Log me!!! + return nextFile(null); // try next anyway + } - if(dupeEntries.length > 0) { - // :TODO: Handle duplidates -- what to do here??? - } else { - if(Array.isArray(options.tags)) { - options.tags.forEach(tag => { - fileEntry.hashTags.add(tag); - }); - } - addNewFileEntry(fileEntry, fullPath, err => { - // pass along error; we failed to insert a record in our DB or something else bad - return nextFile(err); - }); - } - } - ); - }); - }, err => { - return callback(err); - }); - }); - }, - function scanDbEntries(callback) { - // :TODO: Look @ db entries for area that were *not* processed above - return callback(null); - } - ], - err => { - return nextLocation(err); - } - ); - }, - err => { - return cb(err); - }); + if(dupeEntries.length > 0) { + // :TODO: Handle duplidates -- what to do here??? + } else { + if(Array.isArray(options.tags)) { + options.tags.forEach(tag => { + fileEntry.hashTags.add(tag); + }); + } + addNewFileEntry(fileEntry, fullPath, err => { + // pass along error; we failed to insert a record in our DB or something else bad + return nextFile(err); + }); + } + } + ); + }); + }, err => { + return callback(err); + }); + }); + }, + function scanDbEntries(callback) { + // :TODO: Look @ db entries for area that were *not* processed above + return callback(null); + } + ], + err => { + return nextLocation(err); + } + ); + }, + err => { + return cb(err); + }); } function getDescFromFileName(fileName) { - // :TODO: this method could use some more logic to really be nice. - const ext = paths.extname(fileName); - const name = paths.basename(fileName, ext); + // :TODO: this method could use some more logic to really be nice. + const ext = paths.extname(fileName); + const name = paths.basename(fileName, ext); - return _.upperFirst(name.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' ')); + return _.upperFirst(name.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' ')); } // @@ -923,84 +923,84 @@ function getDescFromFileName(fileName) { // } // function getAreaStats(cb) { - FileDb.all( - `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size + FileDb.all( + `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size FROM file f, file_meta m WHERE f.file_id = m.file_id AND m.meta_name='byte_size' GROUP BY f.area_tag;`, - (err, statRows) => { - if(err) { - return cb(err); - } + (err, statRows) => { + if(err) { + return cb(err); + } - if(!statRows || 0 === statRows.length) { - return cb(Errors.DoesNotExist('No file areas to acquire stats from')); - } + if(!statRows || 0 === statRows.length) { + return cb(Errors.DoesNotExist('No file areas to acquire stats from')); + } - return cb( - null, - statRows.reduce( (stats, v) => { - stats.totalFiles = (stats.totalFiles || 0) + v.total_files; - stats.totalBytes = (stats.totalBytes || 0) + v.total_byte_size; + return cb( + null, + statRows.reduce( (stats, v) => { + stats.totalFiles = (stats.totalFiles || 0) + v.total_files; + stats.totalBytes = (stats.totalBytes || 0) + v.total_byte_size; - stats.areas = stats.areas || {}; + stats.areas = stats.areas || {}; - stats.areas[v.area_tag] = { - files : v.total_files, - bytes : v.total_byte_size, - }; - return stats; - }, {}) - ); - } - ); + stats.areas[v.area_tag] = { + files : v.total_files, + bytes : v.total_byte_size, + }; + return stats; + }, {}) + ); + } + ); } // method exposed for event scheduler function updateAreaStatsScheduledEvent(args, cb) { - getAreaStats( (err, stats) => { - if(!err) { - StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); - } + getAreaStats( (err, stats) => { + if(!err) { + StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + } - return cb(err); - }); + return cb(err); + }); } function cleanUpTempSessionItems(cb) { - // find (old) temporary session items and nuke 'em - const filter = { - areaTag : WellKnownAreaTags.TempDownloads, - metaPairs : [ - { - name : 'session_temp_dl', - value : 1 - } - ] - }; + // find (old) temporary session items and nuke 'em + const filter = { + areaTag : WellKnownAreaTags.TempDownloads, + metaPairs : [ + { + name : 'session_temp_dl', + value : 1 + } + ] + }; - FileEntry.findFiles(filter, (err, fileIds) => { - if(err) { - return cb(err); - } + FileEntry.findFiles(filter, (err, fileIds) => { + if(err) { + return cb(err); + } - async.each(fileIds, (fileId, nextFileId) => { - const fileEntry = new FileEntry(); - fileEntry.load(fileId, err => { - if(err) { - Log.warn( { fileId }, 'Failed loading temporary session download item for cleanup'); - return nextFileId(null); - } + async.each(fileIds, (fileId, nextFileId) => { + const fileEntry = new FileEntry(); + fileEntry.load(fileId, err => { + if(err) { + Log.warn( { fileId }, 'Failed loading temporary session download item for cleanup'); + return nextFileId(null); + } - FileEntry.removeEntry(fileEntry, { removePhysFile : true }, err => { - if(err) { - Log.warn( { fileId : fileEntry.fileId, filePath : fileEntry.filePath }, 'Failed to clean up temporary session download item'); - } - return nextFileId(null); - }); - }); - }, () => { - return cb(null); - }); - }); + FileEntry.removeEntry(fileEntry, { removePhysFile : true }, err => { + if(err) { + Log.warn( { fileId : fileEntry.fileId, filePath : fileEntry.filePath }, 'Failed to clean up temporary session download item'); + } + return nextFileId(null); + }); + }); + }, () => { + return cb(null); + }); + }); } \ No newline at end of file diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index 6c45a5d1..bdca4e72 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -10,78 +10,78 @@ const StatLog = require('./stat_log.js'); const async = require('async'); exports.moduleInfo = { - name : 'File Area Selector', - desc : 'Select from available file areas', - author : 'NuSkooler', + name : 'File Area Selector', + desc : 'Select from available file areas', + author : 'NuSkooler', }; const MciViewIds = { - areaList : 1, + areaList : 1, }; exports.getModule = class FileAreaSelectModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.menuMethods = { - selectArea : (formData, extraArgs, cb) => { - const filterCriteria = { - areaTag : formData.value.areaTag, - }; + this.menuMethods = { + selectArea : (formData, extraArgs, cb) => { + const filterCriteria = { + areaTag : formData.value.areaTag, + }; - const menuOpts = { - extraArgs : { - filterCriteria : filterCriteria, - }, - menuFlags : [ 'popParent', 'mergeFlags' ], - }; + const menuOpts = { + extraArgs : { + filterCriteria : filterCriteria, + }, + menuFlags : [ 'popParent', 'mergeFlags' ], + }; - return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); - } - }; - } + return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); + } + }; + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; + const self = this; - async.waterfall( - [ - function mergeAreaStats(callback) { - const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; + async.waterfall( + [ + function mergeAreaStats(callback) { + const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; - // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override - const availAreas = getSortedAvailableFileAreas(self.client); - availAreas.forEach(area => { - const stats = areaStats.areas[area.areaTag]; - area.totalFiles = stats ? stats.files : 0; - area.totalBytes = stats ? stats.bytes : 0; - }); + // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override + const availAreas = getSortedAvailableFileAreas(self.client); + availAreas.forEach(area => { + const stats = areaStats.areas[area.areaTag]; + area.totalFiles = stats ? stats.files : 0; + area.totalBytes = stats ? stats.bytes : 0; + }); - return callback(null, availAreas); - }, - function prepView(availAreas, callback) { - self.prepViewController('allViews', 0, mciData.menu, (err, vc) => { - if(err) { - return callback(err); - } + return callback(null, availAreas); + }, + function prepView(availAreas, callback) { + self.prepViewController('allViews', 0, mciData.menu, (err, vc) => { + if(err) { + return callback(err); + } - const areaListView = vc.getView(MciViewIds.areaList); - areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } ))); - areaListView.redraw(); + const areaListView = vc.getView(MciViewIds.areaList); + areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } ))); + areaListView.redraw(); - return callback(null); - }); - } - ], - err => { - return cb(err); - } - ); - }); - } + return callback(null); + }); + } + ], + err => { + return cb(err); + } + ); + }); + } }; diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index 88ed2ddd..fc7672d0 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -17,226 +17,226 @@ const _ = require('lodash'); const moment = require('moment'); exports.moduleInfo = { - name : 'File Base Download Queue Manager', - desc : 'Module for interacting with download queue/batch', - author : 'NuSkooler', + name : 'File Base Download Queue Manager', + desc : 'Module for interacting with download queue/batch', + author : 'NuSkooler', }; const FormIds = { - queueManager : 0, + queueManager : 0, }; const MciViewIds = { - queueManager : { - queue : 1, - navMenu : 2, + queueManager : { + queue : 1, + navMenu : 2, - customRangeStart : 10, - }, + customRangeStart : 10, + }, }; exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.dlQueue = new DownloadQueue(this.client); + this.dlQueue = new DownloadQueue(this.client); - if(_.has(options, 'lastMenuResult.sentFileIds')) { - this.sentFileIds = options.lastMenuResult.sentFileIds; - } + if(_.has(options, 'lastMenuResult.sentFileIds')) { + this.sentFileIds = options.lastMenuResult.sentFileIds; + } - this.fallbackOnly = options.lastMenuResult ? true : false; + this.fallbackOnly = options.lastMenuResult ? true : false; - this.menuMethods = { - downloadAll : (formData, extraArgs, cb) => { - const modOpts = { - extraArgs : { - sendQueue : this.dlQueue.items, - direction : 'send', - } - }; + this.menuMethods = { + downloadAll : (formData, extraArgs, cb) => { + const modOpts = { + extraArgs : { + sendQueue : this.dlQueue.items, + direction : 'send', + } + }; - return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); - }, - removeItem : (formData, extraArgs, cb) => { - const selectedItem = this.dlQueue.items[formData.value.queueItem]; - if(!selectedItem) { - return cb(null); - } + return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); + }, + removeItem : (formData, extraArgs, cb) => { + const selectedItem = this.dlQueue.items[formData.value.queueItem]; + if(!selectedItem) { + return cb(null); + } - this.dlQueue.removeItems(selectedItem.fileId); + this.dlQueue.removeItems(selectedItem.fileId); - // :TODO: broken: does not redraw menu properly - needs fixed! - return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); - }, - clearQueue : (formData, extraArgs, cb) => { - this.dlQueue.clear(); + // :TODO: broken: does not redraw menu properly - needs fixed! + return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); + }, + clearQueue : (formData, extraArgs, cb) => { + this.dlQueue.clear(); - // :TODO: broken: does not redraw menu properly - needs fixed! - return this.removeItemsFromDownloadQueueView('all', cb); - } - }; - } + // :TODO: broken: does not redraw menu properly - needs fixed! + return this.removeItemsFromDownloadQueueView('all', cb); + } + }; + } - initSequence() { - if(0 === this.dlQueue.items.length) { - if(this.sendFileIds) { - // we've finished everything up - just fall back - return this.prevMenu(); - } + initSequence() { + if(0 === this.dlQueue.items.length) { + if(this.sendFileIds) { + // we've finished everything up - just fall back + return this.prevMenu(); + } - // Simply an empty D/L queue: Present a specialized "empty queue" page - return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); - } + // Simply an empty D/L queue: Present a specialized "empty queue" page + return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); + } - const self = this; + const self = this; - async.series( - [ - function beforeArt(callback) { - return self.beforeArt(callback); - }, - function display(callback) { - return self.displayQueueManagerPage(false, callback); - } - ], - () => { - return self.finishedLoading(); - } - ); - } + async.series( + [ + function beforeArt(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + return self.displayQueueManagerPage(false, callback); + } + ], + () => { + return self.finishedLoading(); + } + ); + } - removeItemsFromDownloadQueueView(itemIndex, cb) { - const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); - if(!queueView) { - return cb(Errors.DoesNotExist('Queue view does not exist')); - } + removeItemsFromDownloadQueueView(itemIndex, cb) { + const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); + if(!queueView) { + return cb(Errors.DoesNotExist('Queue view does not exist')); + } - if('all' === itemIndex) { - queueView.setItems([]); - queueView.setFocusItems([]); - } else { - queueView.removeItem(itemIndex); - } + if('all' === itemIndex) { + queueView.setItems([]); + queueView.setFocusItems([]); + } else { + queueView.removeItem(itemIndex); + } - queueView.redraw(); - return cb(null); - } + queueView.redraw(); + return cb(null); + } - displayWebDownloadLinkForFileEntry(fileEntry) { - FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => { - if(serveItem && serveItem.url) { - const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + displayWebDownloadLinkForFileEntry(fileEntry) { + FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => { + if(serveItem && serveItem.url) { + const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; - fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); - } else { - fileEntry.webDlLink = ''; - fileEntry.webDlExpire = ''; - } + fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; + fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + } else { + fileEntry.webDlLink = ''; + fileEntry.webDlExpire = ''; + } - this.updateCustomViewTextsWithFilter( - 'queueManager', - MciViewIds.queueManager.customRangeStart, fileEntry, - { filter : [ '{webDlLink}', '{webDlExpire}' ] } - ); - }); - } + this.updateCustomViewTextsWithFilter( + 'queueManager', + MciViewIds.queueManager.customRangeStart, fileEntry, + { filter : [ '{webDlLink}', '{webDlExpire}' ] } + ); + }); + } - updateDownloadQueueView(cb) { - const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); - if(!queueView) { - return cb(Errors.DoesNotExist('Queue view does not exist')); - } + updateDownloadQueueView(cb) { + const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); + if(!queueView) { + return cb(Errors.DoesNotExist('Queue view does not exist')); + } - const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; - const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; + const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; + const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; - queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); - queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); + queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); + queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); - queueView.on('index update', idx => { - const fileEntry = this.dlQueue.items[idx]; - this.displayWebDownloadLinkForFileEntry(fileEntry); - }); + queueView.on('index update', idx => { + const fileEntry = this.dlQueue.items[idx]; + this.displayWebDownloadLinkForFileEntry(fileEntry); + }); - queueView.redraw(); - this.displayWebDownloadLinkForFileEntry(this.dlQueue.items[0]); + queueView.redraw(); + this.displayWebDownloadLinkForFileEntry(this.dlQueue.items[0]); - return cb(null); - } + return cb(null); + } - displayQueueManagerPage(clearScreen, cb) { - const self = this; + displayQueueManagerPage(clearScreen, cb) { + const self = this; - async.series( - [ - function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); - }, - function populateViews(callback) { - return self.updateDownloadQueueView(callback); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + async.series( + [ + function prepArtAndViewController(callback) { + return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); + }, + function populateViews(callback) { + return self.updateDownloadQueueView(callback); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + displayArtAndPrepViewController(name, options, cb) { + const self = this; + const config = this.menuConfig.config; - async.waterfall( - [ - function readyAndDisplayArt(callback) { - if(options.clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } + async.waterfall( + [ + function readyAndDisplayArt(callback) { + if(options.clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } - theme.displayThemedAsset( - config.art[name], - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { - const vcOpts = { - client : self.client, - formId : FormIds[name], - }; + theme.displayThemedAsset( + config.art[name], + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function prepeareViewController(artData, callback) { + if(_.isUndefined(self.viewControllers[name])) { + const vcOpts = { + client : self.client, + formId : FormIds[name], + }; - if(!_.isUndefined(options.noInput)) { - vcOpts.noInput = options.noInput; - } + if(!_.isUndefined(options.noInput)) { + vcOpts.noInput = options.noInput; + } - const vc = self.addViewController(name, new ViewController(vcOpts)); + const vc = self.addViewController(name, new ViewController(vcOpts)); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds[name], + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } + return vc.loadFromMenuConfig(loadOpts, callback); + } - self.viewControllers[name].setFocus(true); - return callback(null); + self.viewControllers[name].setFocus(true); + return callback(null); - }, - ], - err => { - return cb(err); - } - ); - } + }, + ], + err => { + return cb(err); + } + ); + } }; diff --git a/core/file_base_filter.js b/core/file_base_filter.js index d8b566b7..9a22051f 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -6,150 +6,150 @@ const _ = require('lodash'); const uuidV4 = require('uuid/v4'); module.exports = class FileBaseFilters { - constructor(client) { - this.client = client; + constructor(client) { + this.client = client; - this.load(); - } + this.load(); + } - static get OrderByValues() { - return [ 'descending', 'ascending' ]; - } + static get OrderByValues() { + return [ 'descending', 'ascending' ]; + } - static get SortByValues() { - return [ - 'upload_timestamp', - 'upload_by_username', - 'dl_count', - 'user_rating', - 'est_release_year', - 'byte_size', - 'file_name', - ]; - } + static get SortByValues() { + return [ + 'upload_timestamp', + 'upload_by_username', + 'dl_count', + 'user_rating', + 'est_release_year', + 'byte_size', + 'file_name', + ]; + } - toArray() { - return _.map(this.filters, (filter, uuid) => { - return Object.assign( { uuid : uuid }, filter ); - }); - } + toArray() { + return _.map(this.filters, (filter, uuid) => { + return Object.assign( { uuid : uuid }, filter ); + }); + } - get(filterUuid) { - return this.filters[filterUuid]; - } + get(filterUuid) { + return this.filters[filterUuid]; + } - add(filterInfo) { - const filterUuid = uuidV4(); + add(filterInfo) { + const filterUuid = uuidV4(); - filterInfo.tags = this.cleanTags(filterInfo.tags); + filterInfo.tags = this.cleanTags(filterInfo.tags); - this.filters[filterUuid] = filterInfo; + this.filters[filterUuid] = filterInfo; - return filterUuid; - } + return filterUuid; + } - replace(filterUuid, filterInfo) { - const filter = this.get(filterUuid); - if(!filter) { - return false; - } + replace(filterUuid, filterInfo) { + const filter = this.get(filterUuid); + if(!filter) { + return false; + } - filterInfo.tags = this.cleanTags(filterInfo.tags); - this.filters[filterUuid] = filterInfo; - return true; - } + filterInfo.tags = this.cleanTags(filterInfo.tags); + this.filters[filterUuid] = filterInfo; + return true; + } - remove(filterUuid) { - delete this.filters[filterUuid]; - } + remove(filterUuid) { + delete this.filters[filterUuid]; + } - load() { - let filtersProperty = this.client.user.properties.file_base_filters; - let defaulted; - if(!filtersProperty) { - filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters()); - defaulted = true; - } + load() { + let filtersProperty = this.client.user.properties.file_base_filters; + let defaulted; + if(!filtersProperty) { + filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters()); + defaulted = true; + } - try { - this.filters = JSON.parse(filtersProperty); - } catch(e) { - this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :( - defaulted = true; - this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); - } + try { + this.filters = JSON.parse(filtersProperty); + } catch(e) { + this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :( + defaulted = true; + this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); + } - if(defaulted) { - this.persist( err => { - if(!err) { - const defaultActiveUuid = this.toArray()[0].uuid; - this.setActive(defaultActiveUuid); - } - }); - } - } + if(defaulted) { + this.persist( err => { + if(!err) { + const defaultActiveUuid = this.toArray()[0].uuid; + this.setActive(defaultActiveUuid); + } + }); + } + } - persist(cb) { - return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb); - } + persist(cb) { + return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb); + } - cleanTags(tags) { - return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim(); - } + cleanTags(tags) { + return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim(); + } - setActive(filterUuid) { - const activeFilter = this.get(filterUuid); + setActive(filterUuid) { + const activeFilter = this.get(filterUuid); - if(activeFilter) { - this.activeFilter = activeFilter; - this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid); - return true; - } + if(activeFilter) { + this.activeFilter = activeFilter; + this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid); + return true; + } - return false; - } + return false; + } - static getBuiltInSystemFilters() { - const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; + static getBuiltInSystemFilters() { + const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; - const filters = { - [ U_LATEST ] : { - name : 'By Date Added', - areaTag : '', // all - terms : '', // * - tags : '', // * - order : 'descending', - sort : 'upload_timestamp', - uuid : U_LATEST, - system : true, - } - }; + const filters = { + [ U_LATEST ] : { + name : 'By Date Added', + areaTag : '', // all + terms : '', // * + tags : '', // * + order : 'descending', + sort : 'upload_timestamp', + uuid : U_LATEST, + system : true, + } + }; - return filters; - } + return filters; + } - static getActiveFilter(client) { - return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); - } + static getActiveFilter(client) { + return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); + } - static getFileBaseLastViewedFileIdByUser(user) { - return parseInt((user.properties.user_file_base_last_viewed || 0)); - } + static getFileBaseLastViewedFileIdByUser(user) { + return parseInt((user.properties.user_file_base_last_viewed || 0)); + } - static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { - if(!cb && _.isFunction(allowOlder)) { - cb = allowOlder; - allowOlder = false; - } + static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { + if(!cb && _.isFunction(allowOlder)) { + cb = allowOlder; + allowOlder = false; + } - const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user); - if(!allowOlder && fileId < current) { - if(cb) { - cb(null); - } - return; - } + const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user); + if(!allowOlder && fileId < current) { + if(cb) { + cb(null); + } + return; + } - return user.persistProperty('user_file_base_last_viewed', fileId, cb); - } + return user.persistProperty('user_file_base_last_viewed', fileId, cb); + } }; diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index 8b45da83..b66e04db 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -8,8 +8,8 @@ const FileArea = require('./file_base_area.js'); const Config = require('./config.js').get; const { Errors } = require('./enig_error.js'); const { - splitTextAtTerms, - isAnsi, + splitTextAtTerms, + isAnsi, } = require('./string_util.js'); const AnsiPrep = require('./ansi_prep.js'); const Log = require('./logger.js').log; @@ -26,276 +26,276 @@ exports.exportFileList = exportFileList; exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; function exportFileList(filterCriteria, options, cb) { - options.templateEncoding = options.templateEncoding || 'utf8'; - options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; - options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; - options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec - options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? + options.templateEncoding = options.templateEncoding || 'utf8'; + options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; + options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; + options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec + options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? - if(true === options.escapeDesc) { - options.escapeDesc = '\\n'; - } + if(true === options.escapeDesc) { + options.escapeDesc = '\\n'; + } - const state = { - total : 0, - current : 0, - step : 'preparing', - status : 'Preparing', - }; + const state = { + total : 0, + current : 0, + step : 'preparing', + status : 'Preparing', + }; - const updateProgress = _.isFunction(options.progress) ? - progCb => { - return options.progress(state, progCb); - } : - progCb => { - return progCb(null); - } + const updateProgress = _.isFunction(options.progress) ? + progCb => { + return options.progress(state, progCb); + } : + progCb => { + return progCb(null); + } ; - async.waterfall( - [ - function readTemplateFiles(callback) { - updateProgress(err => { - if(err) { - return callback(err); - } + async.waterfall( + [ + function readTemplateFiles(callback) { + updateProgress(err => { + if(err) { + return callback(err); + } - const templateFiles = [ - { name : options.headerTemplate, req : false }, - { name : options.entryTemplate, req : true } - ]; + const templateFiles = [ + { name : options.headerTemplate, req : false }, + { name : options.entryTemplate, req : true } + ]; - const config = Config(); - async.map(templateFiles, (template, nextTemplate) => { - if(!template.name && !template.req) { - return nextTemplate(null, Buffer.from([])); - } + const config = Config(); + async.map(templateFiles, (template, nextTemplate) => { + if(!template.name && !template.req) { + return nextTemplate(null, Buffer.from([])); + } - template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name); - fs.readFile(template.name, (err, data) => { - return nextTemplate(err, data); - }); - }, (err, templates) => { - if(err) { - return callback(Errors.General(err.message)); - } + template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name); + fs.readFile(template.name, (err, data) => { + return nextTemplate(err, data); + }); + }, (err, templates) => { + if(err) { + return callback(Errors.General(err.message)); + } - // decode + ensure DOS style CRLF - templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') ); + // decode + ensure DOS style CRLF + templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') ); - // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements - let descIndent = 0; - if(!options.escapeDesc) { - splitTextAtTerms(templates[1]).some(line => { - const pos = line.indexOf('{fileDesc}'); - if(pos > -1) { - descIndent = pos; - return true; // found it! - } - return false; // keep looking - }); - } + // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements + let descIndent = 0; + if(!options.escapeDesc) { + splitTextAtTerms(templates[1]).some(line => { + const pos = line.indexOf('{fileDesc}'); + if(pos > -1) { + descIndent = pos; + return true; // found it! + } + return false; // keep looking + }); + } - return callback(null, templates[0], templates[1], descIndent); - }); - }); - }, - function findFiles(headerTemplate, entryTemplate, descIndent, callback) { - state.step = 'gathering'; - state.status = 'Gathering files for supplied criteria'; - updateProgress(err => { - if(err) { - return callback(err); - } + return callback(null, templates[0], templates[1], descIndent); + }); + }); + }, + function findFiles(headerTemplate, entryTemplate, descIndent, callback) { + state.step = 'gathering'; + state.status = 'Gathering files for supplied criteria'; + updateProgress(err => { + if(err) { + return callback(err); + } - FileEntry.findFiles(filterCriteria, (err, fileIds) => { - if(0 === fileIds.length) { - return callback(Errors.General('No results for criteria', 'NORESULTS')); - } + FileEntry.findFiles(filterCriteria, (err, fileIds) => { + if(0 === fileIds.length) { + return callback(Errors.General('No results for criteria', 'NORESULTS')); + } - return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); - }); - }); - }, - function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { - const formatObj = { - totalFileCount : fileIds.length, - }; + return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); + }); + }); + }, + function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { + const formatObj = { + totalFileCount : fileIds.length, + }; - let current = 0; - let listBody = ''; - const totals = { fileCount : fileIds.length, bytes : 0 }; - state.total = fileIds.length; + let current = 0; + let listBody = ''; + const totals = { fileCount : fileIds.length, bytes : 0 }; + state.total = fileIds.length; - state.step = 'file'; + state.step = 'file'; - async.eachSeries(fileIds, (fileId, nextFileId) => { - const fileInfo = new FileEntry(); - current += 1; + async.eachSeries(fileIds, (fileId, nextFileId) => { + const fileInfo = new FileEntry(); + current += 1; - fileInfo.load(fileId, err => { - if(err) { - return nextFileId(null); // failed, but try the next - } + fileInfo.load(fileId, err => { + if(err) { + return nextFileId(null); // failed, but try the next + } - totals.bytes += fileInfo.meta.byte_size; + totals.bytes += fileInfo.meta.byte_size; - const appendFileInfo = () => { - if(options.escapeDesc) { - formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc); - } + const appendFileInfo = () => { + if(options.escapeDesc) { + formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc); + } - if(options.maxDescLen) { - formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen); - } + if(options.maxDescLen) { + formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen); + } - listBody += stringFormat(entryTemplate, formatObj); + listBody += stringFormat(entryTemplate, formatObj); - state.current = current; - state.status = `Processing ${fileInfo.fileName}`; - state.fileInfo = formatObj; + state.current = current; + state.status = `Processing ${fileInfo.fileName}`; + state.fileInfo = formatObj; - updateProgress(err => { - return nextFileId(err); - }); - }; + updateProgress(err => { + return nextFileId(err); + }); + }; - const area = FileArea.getFileAreaByTag(fileInfo.areaTag); + const area = FileArea.getFileAreaByTag(fileInfo.areaTag); - formatObj.fileId = fileId; - formatObj.areaName = _.get(area, 'name') || 'N/A'; - formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; - formatObj.userRating = fileInfo.userRating || 0; - formatObj.fileName = fileInfo.fileName; - formatObj.fileSize = fileInfo.meta.byte_size; - formatObj.fileDesc = fileInfo.desc || ''; - formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); - formatObj.fileSha256 = fileInfo.fileSha256; - formatObj.fileCrc32 = fileInfo.meta.file_crc32; - formatObj.fileMd5 = fileInfo.meta.file_md5; - formatObj.fileSha1 = fileInfo.meta.file_sha1; - formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; - formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); - formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; - formatObj.currentFile = current; - formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); + formatObj.fileId = fileId; + formatObj.areaName = _.get(area, 'name') || 'N/A'; + formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; + formatObj.userRating = fileInfo.userRating || 0; + formatObj.fileName = fileInfo.fileName; + formatObj.fileSize = fileInfo.meta.byte_size; + formatObj.fileDesc = fileInfo.desc || ''; + formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); + formatObj.fileSha256 = fileInfo.fileSha256; + formatObj.fileCrc32 = fileInfo.meta.file_crc32; + formatObj.fileMd5 = fileInfo.meta.file_md5; + formatObj.fileSha1 = fileInfo.meta.file_sha1; + formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; + formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); + formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; + formatObj.currentFile = current; + formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); - if(isAnsi(fileInfo.desc)) { - AnsiPrep( - fileInfo.desc, - { - cols : Math.min(options.descWidth, 79 - descIndent), - forceLineTerm : true, // ensure each line is term'd - asciiMode : true, // export to ASCII - fillLines : false, // don't fill up to |cols| - indent : descIndent, - }, - (err, desc) => { - if(desc) { - formatObj.fileDesc = desc; - } - return appendFileInfo(); - } - ); - } else { - const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; - formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; - return appendFileInfo(); - } - }); - }, err => { - return callback(err, listBody, headerTemplate, totals); - }); - }, - function buildHeader(listBody, headerTemplate, totals, callback) { - // header is built last such that we can have totals/etc. + if(isAnsi(fileInfo.desc)) { + AnsiPrep( + fileInfo.desc, + { + cols : Math.min(options.descWidth, 79 - descIndent), + forceLineTerm : true, // ensure each line is term'd + asciiMode : true, // export to ASCII + fillLines : false, // don't fill up to |cols| + indent : descIndent, + }, + (err, desc) => { + if(desc) { + formatObj.fileDesc = desc; + } + return appendFileInfo(); + } + ); + } else { + const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; + formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; + return appendFileInfo(); + } + }); + }, err => { + return callback(err, listBody, headerTemplate, totals); + }); + }, + function buildHeader(listBody, headerTemplate, totals, callback) { + // header is built last such that we can have totals/etc. - let filterAreaName; - let filterAreaDesc; - if(filterCriteria.areaTag) { - const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); - filterAreaName = _.get(area, 'name') || 'N/A'; - filterAreaDesc = _.get(area, 'desc') || 'N/A'; - } else { - filterAreaName = '-ALL-'; - filterAreaDesc = 'All areas'; - } + let filterAreaName; + let filterAreaDesc; + if(filterCriteria.areaTag) { + const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); + filterAreaName = _.get(area, 'name') || 'N/A'; + filterAreaDesc = _.get(area, 'desc') || 'N/A'; + } else { + filterAreaName = '-ALL-'; + filterAreaDesc = 'All areas'; + } - const headerFormatObj = { - nowTs : moment().format(options.tsFormat), - boardName : Config().general.boardName, - totalFileCount : totals.fileCount, - totalFileSize : totals.bytes, - filterAreaTag : filterCriteria.areaTag || '-ALL-', - filterAreaName : filterAreaName, - filterAreaDesc : filterAreaDesc, - filterTerms : filterCriteria.terms || '(none)', - filterHashTags : filterCriteria.tags || '(none)', - }; + const headerFormatObj = { + nowTs : moment().format(options.tsFormat), + boardName : Config().general.boardName, + totalFileCount : totals.fileCount, + totalFileSize : totals.bytes, + filterAreaTag : filterCriteria.areaTag || '-ALL-', + filterAreaName : filterAreaName, + filterAreaDesc : filterAreaDesc, + filterTerms : filterCriteria.terms || '(none)', + filterHashTags : filterCriteria.tags || '(none)', + }; - listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; - return callback(null, listBody); - }, - function done(listBody, callback) { - delete state.fileInfo; - state.step = 'finished'; - state.status = 'Finished processing'; - updateProgress( () => { - return callback(null, listBody); - }); - } - ], (err, listBody) => { - return cb(err, listBody); - } - ); + listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; + return callback(null, listBody); + }, + function done(listBody, callback) { + delete state.fileInfo; + state.step = 'finished'; + state.status = 'Finished processing'; + updateProgress( () => { + return callback(null, listBody); + }); + } + ], (err, listBody) => { + return cb(err, listBody); + } + ); } function updateFileBaseDescFilesScheduledEvent(args, cb) { - // - // For each area, loop over storage locations and build - // DESCRIPT.ION file to store in the same directory. - // - // Standard-ish 4DOS spec is as such: - // * Entry: [0x04]\r\n - // * Multi line descriptions are stored with *escaped* \r\n pairs - // * Default template uses 0x2c for as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec - // - const entryTemplate = args[0]; - const headerTemplate = args[1]; + // + // For each area, loop over storage locations and build + // DESCRIPT.ION file to store in the same directory. + // + // Standard-ish 4DOS spec is as such: + // * Entry: [0x04]\r\n + // * Multi line descriptions are stored with *escaped* \r\n pairs + // * Default template uses 0x2c for as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec + // + const entryTemplate = args[0]; + const headerTemplate = args[1]; - const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true }); - async.each(areas, (area, nextArea) => { - const storageLocations = FileArea.getAreaStorageLocations(area); + const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true }); + async.each(areas, (area, nextArea) => { + const storageLocations = FileArea.getAreaStorageLocations(area); - async.each(storageLocations, (storageLoc, nextStorageLoc) => { - const filterCriteria = { - areaTag : area.areaTag, - storageTag : storageLoc.storageTag, - }; + async.each(storageLocations, (storageLoc, nextStorageLoc) => { + const filterCriteria = { + areaTag : area.areaTag, + storageTag : storageLoc.storageTag, + }; - const exportOpts = { - headerTemplate : headerTemplate, - entryTemplate : entryTemplate, - escapeDesc : true, // escape CRLF's - maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes" - }; + const exportOpts = { + headerTemplate : headerTemplate, + entryTemplate : entryTemplate, + escapeDesc : true, // escape CRLF's + maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes" + }; - exportFileList(filterCriteria, exportOpts, (err, listBody) => { + exportFileList(filterCriteria, exportOpts, (err, listBody) => { - const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION'); - fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => { - if(err) { - Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION'); - } else { - Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION'); - } - return nextStorageLoc(null); - }); - }); - }, () => { - return nextArea(null); - }); - }, () => { - return cb(null); - }); + const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION'); + fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => { + if(err) { + Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION'); + } else { + Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION'); + } + return nextStorageLoc(null); + }); + }); + }, () => { + return nextArea(null); + }); + }, () => { + return cb(null); + }); } diff --git a/core/file_base_search.js b/core/file_base_search.js index 06dac204..3e754b91 100644 --- a/core/file_base_search.js +++ b/core/file_base_search.js @@ -11,110 +11,110 @@ const FileBaseFilters = require('./file_base_filter.js'); const async = require('async'); exports.moduleInfo = { - name : 'File Base Search', - desc : 'Module for quickly searching the file base', - author : 'NuSkooler', + name : 'File Base Search', + desc : 'Module for quickly searching the file base', + author : 'NuSkooler', }; const MciViewIds = { - search : { - searchTerms : 1, - search : 2, - tags : 3, - area : 4, - orderBy : 5, - sort : 6, - advSearch : 7, - } + search : { + searchTerms : 1, + search : 2, + tags : 3, + area : 4, + orderBy : 5, + sort : 6, + advSearch : 7, + } }; exports.getModule = class FileBaseSearch extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.menuMethods = { - search : (formData, extraArgs, cb) => { - const isAdvanced = formData.submitId === MciViewIds.search.advSearch; - return this.searchNow(formData, isAdvanced, cb); - }, - }; - } + this.menuMethods = { + search : (formData, extraArgs, cb) => { + const isAdvanced = formData.submitId === MciViewIds.search.advSearch; + return this.searchNow(formData, isAdvanced, cb); + }, + }; + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) ); + const self = this; + const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) ); - async.series( - [ - function loadFromConfig(callback) { - return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); - }, - function populateAreas(callback) { - self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); + async.series( + [ + function loadFromConfig(callback) { + return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); + }, + function populateAreas(callback) { + self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); - const areasView = vc.getView(MciViewIds.search.area); - areasView.setItems( self.availAreas.map( a => a.name ) ); - areasView.redraw(); - vc.switchFocus(MciViewIds.search.searchTerms); + const areasView = vc.getView(MciViewIds.search.area); + areasView.setItems( self.availAreas.map( a => a.name ) ); + areasView.redraw(); + vc.switchFocus(MciViewIds.search.searchTerms); - return callback(null); - } - ], - err => { - return cb(err); - } - ); - }); - } + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } - getSelectedAreaTag(index) { - if(0 === index) { - return ''; // -ALL- - } - const area = this.availAreas[index]; - if(!area) { - return ''; - } - return area.areaTag; - } + getSelectedAreaTag(index) { + if(0 === index) { + return ''; // -ALL- + } + const area = this.availAreas[index]; + if(!area) { + return ''; + } + return area.areaTag; + } - getOrderBy(index) { - return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0]; - } + getOrderBy(index) { + return FileBaseFilters.OrderByValues[index] || FileBaseFilters.OrderByValues[0]; + } - getSortBy(index) { - return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0]; - } + getSortBy(index) { + return FileBaseFilters.SortByValues[index] || FileBaseFilters.SortByValues[0]; + } - getFilterValuesFromFormData(formData, isAdvanced) { - const areaIndex = isAdvanced ? formData.value.areaIndex : 0; - const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; - const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; + getFilterValuesFromFormData(formData, isAdvanced) { + const areaIndex = isAdvanced ? formData.value.areaIndex : 0; + const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; + const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; - return { - areaTag : this.getSelectedAreaTag(areaIndex), - terms : formData.value.searchTerms, - tags : isAdvanced ? formData.value.tags : '', - order : this.getOrderBy(orderByIndex), - sort : this.getSortBy(sortByIndex), - }; - } + return { + areaTag : this.getSelectedAreaTag(areaIndex), + terms : formData.value.searchTerms, + tags : isAdvanced ? formData.value.tags : '', + order : this.getOrderBy(orderByIndex), + sort : this.getSortBy(sortByIndex), + }; + } - searchNow(formData, isAdvanced, cb) { - const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced); + searchNow(formData, isAdvanced, cb) { + const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced); - const menuOpts = { - extraArgs : { - filterCriteria : filterCriteria, - }, - menuFlags : [ 'popParent' ], - }; + const menuOpts = { + extraArgs : { + filterCriteria : filterCriteria, + }, + menuFlags : [ 'popParent' ], + }; - return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); - } + return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); + } }; diff --git a/core/file_base_user_list_export.js b/core/file_base_user_list_export.js index 0b30d582..8f691273 100644 --- a/core/file_base_user_list_export.js +++ b/core/file_base_user_list_export.js @@ -46,249 +46,249 @@ const yazl = require('yazl'); */ exports.moduleInfo = { - name : 'File Base List Export', - desc : 'Exports file base listings for download', - author : 'NuSkooler', + name : 'File Base List Export', + desc : 'Exports file base listings for download', + author : 'NuSkooler', }; const FormIds = { - main : 0, + main : 0, }; const MciViewIds = { - main : { - status : 1, - progressBar : 2, + main : { + status : 1, + progressBar : 2, - customRangeStart : 10, - } + customRangeStart : 10, + } }; exports.getModule = class FileBaseListExport extends MenuModule { - constructor(options) { - super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.config.templateEncoding = this.config.templateEncoding || 'utf8'; - this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); - this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ - this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); - this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) - } + this.config.templateEncoding = this.config.templateEncoding || 'utf8'; + this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); + this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ + this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); + this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - async.series( - [ - (callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), - (callback) => this.prepareList(callback), - ], - err => { - if(err) { - if('NORESULTS' === err.reasonCode) { - return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults'); - } + async.series( + [ + (callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), + (callback) => this.prepareList(callback), + ], + err => { + if(err) { + if('NORESULTS' === err.reasonCode) { + return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults'); + } - return this.prevMenu(); - } - return cb(err); - } - ); - }); - } + return this.prevMenu(); + } + return cb(err); + } + ); + }); + } - finishedLoading() { - this.prevMenu(); - } + finishedLoading() { + this.prevMenu(); + } - prepareList(cb) { - const self = this; + prepareList(cb) { + const self = this; - const statusView = self.viewControllers.main.getView(MciViewIds.main.status); - const updateStatus = (status) => { - if(statusView) { - statusView.setText(status); - } - }; + const statusView = self.viewControllers.main.getView(MciViewIds.main.status); + const updateStatus = (status) => { + if(statusView) { + statusView.setText(status); + } + }; - const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); - const updateProgressBar = (curr, total) => { - if(progBarView) { - const prog = Math.floor( (curr / total) * progBarView.dimens.width ); - progBarView.setText(self.config.progBarChar.repeat(prog)); - } - }; + const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); + const updateProgressBar = (curr, total) => { + if(progBarView) { + const prog = Math.floor( (curr / total) * progBarView.dimens.width ); + progBarView.setText(self.config.progBarChar.repeat(prog)); + } + }; - let cancel = false; + let cancel = false; - const exportListProgress = (state, progNext) => { - switch(state.step) { - case 'preparing' : - case 'gathering' : - updateStatus(state.status); - break; - case 'file' : - updateStatus(state.status); - updateProgressBar(state.current, state.total); - self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo); - break; - default : - break; - } + const exportListProgress = (state, progNext) => { + switch(state.step) { + case 'preparing' : + case 'gathering' : + updateStatus(state.status); + break; + case 'file' : + updateStatus(state.status); + updateProgressBar(state.current, state.total); + self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo); + break; + default : + break; + } - return progNext(cancel ? Errors.General('User canceled') : null); - }; + return progNext(cancel ? Errors.General('User canceled') : null); + }; - const keyPressHandler = (ch, key) => { - if('escape' === key.name) { - cancel = true; - self.client.removeListener('key press', keyPressHandler); - } - }; + const keyPressHandler = (ch, key) => { + if('escape' === key.name) { + cancel = true; + self.client.removeListener('key press', keyPressHandler); + } + }; - async.waterfall( - [ - function buildList(callback) { - // this may take quite a while; temp disable of idle monitor - self.client.stopIdleMonitor(); + async.waterfall( + [ + function buildList(callback) { + // this may take quite a while; temp disable of idle monitor + self.client.stopIdleMonitor(); - self.client.on('key press', keyPressHandler); + self.client.on('key press', keyPressHandler); - const filterCriteria = Object.assign({}, self.config.filterCriteria); - if(!filterCriteria.areaTag) { - filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); - } + const filterCriteria = Object.assign({}, self.config.filterCriteria); + if(!filterCriteria.areaTag) { + filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); + } - const opts = { - templateEncoding : self.config.templateEncoding, - headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), - entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), - tsFormat : self.config.tsFormat, - descWidth : self.config.descWidth, - progress : exportListProgress, - }; + const opts = { + templateEncoding : self.config.templateEncoding, + headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), + entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), + tsFormat : self.config.tsFormat, + descWidth : self.config.descWidth, + progress : exportListProgress, + }; - exportFileList(filterCriteria, opts, (err, listBody) => { - return callback(err, listBody); - }); - }, - function persistList(listBody, callback) { - updateStatus('Persisting list'); + exportFileList(filterCriteria, opts, (err, listBody) => { + return callback(err, listBody); + }); + }, + function persistList(listBody, callback) { + updateStatus('Persisting list'); - const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); - const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); + const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); + const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); - fse.mkdirs(sysTempDownloadDir, err => { - if(err) { - return callback(err); - } + fse.mkdirs(sysTempDownloadDir, err => { + if(err) { + return callback(err); + } - const outputFileName = paths.join( - sysTempDownloadDir, - `file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` - ); + const outputFileName = paths.join( + sysTempDownloadDir, + `file_list_${uuidv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` + ); - fs.writeFile(outputFileName, listBody, 'utf8', err => { - if(err) { - return callback(err); - } + fs.writeFile(outputFileName, listBody, 'utf8', err => { + if(err) { + return callback(err); + } - self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { - return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); - }); - }); - }); - }, - function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { - const newEntry = new FileEntry({ - areaTag : sysTempDownloadArea.areaTag, - fileName : paths.basename(outputFileName), - storageTag : sysTempDownloadArea.storageTags[0], - meta : { - upload_by_username : self.client.user.username, - upload_by_user_id : self.client.user.userId, - byte_size : fileSize, - session_temp_dl : 1, // download is valid until session is over - } - }); + self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { + return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); + }); + }); + }); + }, + function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { + const newEntry = new FileEntry({ + areaTag : sysTempDownloadArea.areaTag, + fileName : paths.basename(outputFileName), + storageTag : sysTempDownloadArea.storageTags[0], + meta : { + upload_by_username : self.client.user.username, + upload_by_user_id : self.client.user.userId, + byte_size : fileSize, + session_temp_dl : 1, // download is valid until session is over + } + }); - newEntry.desc = 'File List Export'; + newEntry.desc = 'File List Export'; - newEntry.persist(err => { - if(!err) { - // queue it! - const dlQueue = new DownloadQueue(self.client); - dlQueue.add(newEntry, true); // true=systemFile + newEntry.persist(err => { + if(!err) { + // queue it! + const dlQueue = new DownloadQueue(self.client); + dlQueue.add(newEntry, true); // true=systemFile - // clean up after ourselves when the session ends - const thisClientId = self.client.session.id; - Events.once(Events.getSystemEvents().ClientDisconnected, evt => { - if(thisClientId === _.get(evt, 'client.session.id')) { - FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { - if(err) { - Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); - } else { - Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); - } - }); - } - }); - } - return callback(err); - }); - }, - function done(callback) { - // re-enable idle monitor - self.client.startIdleMonitor(); + // clean up after ourselves when the session ends + const thisClientId = self.client.session.id; + Events.once(Events.getSystemEvents().ClientDisconnected, evt => { + if(thisClientId === _.get(evt, 'client.session.id')) { + FileEntry.removeEntry(newEntry, { removePhysFile : true }, err => { + if(err) { + Log.warn( { fileId : newEntry.fileId, path : outputFileName }, 'Failed removing temporary session download' ); + } else { + Log.debug( { fileId : newEntry.fileId, path : outputFileName }, 'Removed temporary session download item' ); + } + }); + } + }); + } + return callback(err); + }); + }, + function done(callback) { + // re-enable idle monitor + self.client.startIdleMonitor(); - updateStatus('Exported list has been added to your download queue'); - return callback(null); - } - ], - err => { - self.client.removeListener('key press', keyPressHandler); - return cb(err); - } - ); - } + updateStatus('Exported list has been added to your download queue'); + return callback(null); + } + ], + err => { + self.client.removeListener('key press', keyPressHandler); + return cb(err); + } + ); + } - getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { - fse.stat(filePath, (err, stats) => { - if(err) { - return cb(err); - } + getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { + fse.stat(filePath, (err, stats) => { + if(err) { + return cb(err); + } - if(stats.size < this.config.compressThreshold) { - // small enough, keep orig - return cb(null, filePath, stats.size); - } + if(stats.size < this.config.compressThreshold) { + // small enough, keep orig + return cb(null, filePath, stats.size); + } - const zipFilePath = `${filePath}.zip`; + const zipFilePath = `${filePath}.zip`; - const zipFile = new yazl.ZipFile(); - zipFile.addFile(filePath, paths.basename(filePath)); - zipFile.end( () => { - const outZipFile = fs.createWriteStream(zipFilePath); - zipFile.outputStream.pipe(outZipFile); - zipFile.outputStream.on('finish', () => { - // delete the original - fse.unlink(filePath, err => { - if(err) { - return cb(err); - } + const zipFile = new yazl.ZipFile(); + zipFile.addFile(filePath, paths.basename(filePath)); + zipFile.end( () => { + const outZipFile = fs.createWriteStream(zipFilePath); + zipFile.outputStream.pipe(outZipFile); + zipFile.outputStream.on('finish', () => { + // delete the original + fse.unlink(filePath, err => { + if(err) { + return cb(err); + } - // finally stat the new output - fse.stat(zipFilePath, (err, stats) => { - return cb(err, zipFilePath, stats ? stats.size : 0); - }); - }); - }); - }); - }); - } + // finally stat the new output + fse.stat(zipFilePath, (err, stats) => { + return cb(err, zipFilePath, stats ? stats.size : 0); + }); + }); + }); + }); + }); + } }; \ No newline at end of file diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index f046de86..69c87ec8 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -19,269 +19,269 @@ const _ = require('lodash'); const moment = require('moment'); exports.moduleInfo = { - name : 'File Base Download Web Queue Manager', - desc : 'Module for interacting with web backed download queue/batch', - author : 'NuSkooler', + name : 'File Base Download Web Queue Manager', + desc : 'Module for interacting with web backed download queue/batch', + author : 'NuSkooler', }; const FormIds = { - queueManager : 0 + queueManager : 0 }; const MciViewIds = { - queueManager : { - queue : 1, - navMenu : 2, + queueManager : { + queue : 1, + navMenu : 2, - customRangeStart : 10, - } + customRangeStart : 10, + } }; exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.dlQueue = new DownloadQueue(this.client); + this.dlQueue = new DownloadQueue(this.client); - this.menuMethods = { - removeItem : (formData, extraArgs, cb) => { - const selectedItem = this.dlQueue.items[formData.value.queueItem]; - if(!selectedItem) { - return cb(null); - } + this.menuMethods = { + removeItem : (formData, extraArgs, cb) => { + const selectedItem = this.dlQueue.items[formData.value.queueItem]; + if(!selectedItem) { + return cb(null); + } - this.dlQueue.removeItems(selectedItem.fileId); + this.dlQueue.removeItems(selectedItem.fileId); - // :TODO: broken: does not redraw menu properly - needs fixed! - return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); - }, - clearQueue : (formData, extraArgs, cb) => { - this.dlQueue.clear(); + // :TODO: broken: does not redraw menu properly - needs fixed! + return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); + }, + clearQueue : (formData, extraArgs, cb) => { + this.dlQueue.clear(); - // :TODO: broken: does not redraw menu properly - needs fixed! - return this.removeItemsFromDownloadQueueView('all', cb); - }, - getBatchLink : (formData, extraArgs, cb) => { - return this.generateAndDisplayBatchLink(cb); - } - }; - } + // :TODO: broken: does not redraw menu properly - needs fixed! + return this.removeItemsFromDownloadQueueView('all', cb); + }, + getBatchLink : (formData, extraArgs, cb) => { + return this.generateAndDisplayBatchLink(cb); + } + }; + } - initSequence() { - if(0 === this.dlQueue.items.length) { - return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); - } + initSequence() { + if(0 === this.dlQueue.items.length) { + return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); + } - const self = this; + const self = this; - async.series( - [ - function beforeArt(callback) { - return self.beforeArt(callback); - }, - function display(callback) { - return self.displayQueueManagerPage(false, callback); - } - ], - () => { - return self.finishedLoading(); - } - ); - } + async.series( + [ + function beforeArt(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + return self.displayQueueManagerPage(false, callback); + } + ], + () => { + return self.finishedLoading(); + } + ); + } - removeItemsFromDownloadQueueView(itemIndex, cb) { - const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); - if(!queueView) { - return cb(Errors.DoesNotExist('Queue view does not exist')); - } + removeItemsFromDownloadQueueView(itemIndex, cb) { + const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); + if(!queueView) { + return cb(Errors.DoesNotExist('Queue view does not exist')); + } - if('all' === itemIndex) { - queueView.setItems([]); - queueView.setFocusItems([]); - } else { - queueView.removeItem(itemIndex); - } + if('all' === itemIndex) { + queueView.setItems([]); + queueView.setFocusItems([]); + } else { + queueView.removeItem(itemIndex); + } - queueView.redraw(); - return cb(null); - } + queueView.redraw(); + return cb(null); + } - displayFileInfoForFileEntry(fileEntry) { - this.updateCustomViewTextsWithFilter( - 'queueManager', - MciViewIds.queueManager.customRangeStart, fileEntry, - { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... - ); - } + displayFileInfoForFileEntry(fileEntry) { + this.updateCustomViewTextsWithFilter( + 'queueManager', + MciViewIds.queueManager.customRangeStart, fileEntry, + { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... + ); + } - updateDownloadQueueView(cb) { - const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); - if(!queueView) { - return cb(Errors.DoesNotExist('Queue view does not exist')); - } + updateDownloadQueueView(cb) { + const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); + if(!queueView) { + return cb(Errors.DoesNotExist('Queue view does not exist')); + } - const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; - const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; + const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; + const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; - queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); - queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); + queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); + queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); - queueView.on('index update', idx => { - const fileEntry = this.dlQueue.items[idx]; - this.displayFileInfoForFileEntry(fileEntry); - }); + queueView.on('index update', idx => { + const fileEntry = this.dlQueue.items[idx]; + this.displayFileInfoForFileEntry(fileEntry); + }); - queueView.redraw(); - this.displayFileInfoForFileEntry(this.dlQueue.items[0]); + queueView.redraw(); + this.displayFileInfoForFileEntry(this.dlQueue.items[0]); - return cb(null); - } + return cb(null); + } - generateAndDisplayBatchLink(cb) { - const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); + generateAndDisplayBatchLink(cb) { + const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); - FileAreaWeb.createAndServeTempBatchDownload( - this.client, - this.dlQueue.items, - { - expireTime : expireTime - }, - (err, webBatchDlLink) => { - // :TODO: handle not enabled -> display such - if(err) { - return cb(err); - } + FileAreaWeb.createAndServeTempBatchDownload( + this.client, + this.dlQueue.items, + { + expireTime : expireTime + }, + (err, webBatchDlLink) => { + // :TODO: handle not enabled -> display such + if(err) { + return cb(err); + } - const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - const formatObj = { - webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, - webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), - }; + const formatObj = { + webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, + webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), + }; - this.updateCustomViewTextsWithFilter( - 'queueManager', - MciViewIds.queueManager.customRangeStart, - formatObj, - { filter : Object.keys(formatObj).map(k => '{' + k + '}' ) } - ); + this.updateCustomViewTextsWithFilter( + 'queueManager', + MciViewIds.queueManager.customRangeStart, + formatObj, + { filter : Object.keys(formatObj).map(k => '{' + k + '}' ) } + ); - return cb(null); - } - ); - } + return cb(null); + } + ); + } - displayQueueManagerPage(clearScreen, cb) { - const self = this; + displayQueueManagerPage(clearScreen, cb) { + const self = this; - async.series( - [ - function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); - }, - function prepareQueueDownloadLinks(callback) { - const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + async.series( + [ + function prepArtAndViewController(callback) { + return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); + }, + function prepareQueueDownloadLinks(callback) { + const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - const config = Config(); - async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => { - FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { - if(err) { - if(ErrNotEnabled === err.reasonCode) { - return nextFileEntry(err); // we should have caught this prior - } + const config = Config(); + async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => { + FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { + if(err) { + if(ErrNotEnabled === err.reasonCode) { + return nextFileEntry(err); // we should have caught this prior + } - const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); + const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); - FileAreaWeb.createAndServeTempDownload( - self.client, - fileEntry, - { expireTime : expireTime }, - (err, url) => { - if(err) { - return nextFileEntry(err); - } + FileAreaWeb.createAndServeTempDownload( + self.client, + fileEntry, + { expireTime : expireTime }, + (err, url) => { + if(err) { + return nextFileEntry(err); + } - fileEntry.webDlLinkRaw = url; - fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; - fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); + fileEntry.webDlLinkRaw = url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; + fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); - return nextFileEntry(null); - } - ); - } else { - fileEntry.webDlLinkRaw = serveItem.url; - fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; - fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); - return nextFileEntry(null); - } - }); - }, err => { - return callback(err); - }); - }, - function populateViews(callback) { - return self.updateDownloadQueueView(callback); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + return nextFileEntry(null); + } + ); + } else { + fileEntry.webDlLinkRaw = serveItem.url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; + fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + return nextFileEntry(null); + } + }); + }, err => { + return callback(err); + }); + }, + function populateViews(callback) { + return self.updateDownloadQueueView(callback); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + displayArtAndPrepViewController(name, options, cb) { + const self = this; + const config = this.menuConfig.config; - async.waterfall( - [ - function readyAndDisplayArt(callback) { - if(options.clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } + async.waterfall( + [ + function readyAndDisplayArt(callback) { + if(options.clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } - theme.displayThemedAsset( - config.art[name], - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { - const vcOpts = { - client : self.client, - formId : FormIds[name], - }; + theme.displayThemedAsset( + config.art[name], + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function prepeareViewController(artData, callback) { + if(_.isUndefined(self.viewControllers[name])) { + const vcOpts = { + client : self.client, + formId : FormIds[name], + }; - if(!_.isUndefined(options.noInput)) { - vcOpts.noInput = options.noInput; - } + if(!_.isUndefined(options.noInput)) { + vcOpts.noInput = options.noInput; + } - const vc = self.addViewController(name, new ViewController(vcOpts)); + const vc = self.addViewController(name, new ViewController(vcOpts)); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds[name], + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } + return vc.loadFromMenuConfig(loadOpts, callback); + } - self.viewControllers[name].setFocus(true); - return callback(null); + self.viewControllers[name].setFocus(true); + return callback(null); - }, - ], - err => { - return cb(err); - } - ); - } + }, + ], + err => { + return cb(err); + } + ); + } }; diff --git a/core/file_entry.js b/core/file_entry.js index 1310d40a..75fafb29 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -4,8 +4,8 @@ const fileDb = require('./database.js').dbs.file; const Errors = require('./enig_error.js').Errors; const { - getISOTimestampString, - sanatizeString + getISOTimestampString, + sanatizeString } = require('./database.js'); const Config = require('./config.js').get; @@ -19,461 +19,461 @@ const crypto = require('crypto'); const moment = require('moment'); const FILE_TABLE_MEMBERS = [ - 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', - 'desc', 'desc_long', 'upload_timestamp' + 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', + 'desc', 'desc_long', 'upload_timestamp' ]; const FILE_WELL_KNOWN_META = { - // name -> *read* converter, if any - upload_by_username : null, - upload_by_user_id : (u) => parseInt(u) || 0, - file_md5 : null, - file_sha1 : null, - file_crc32 : null, - est_release_year : (y) => parseInt(y) || new Date().getFullYear(), - dl_count : (d) => parseInt(d) || 0, - byte_size : (b) => parseInt(b) || 0, - archive_type : null, - short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import - tic_origin : null, // TIC "Origin" - tic_desc : null, // TIC "Desc" - tic_ldesc : null, // TIC "Ldesc" joined by '\n' - session_temp_dl : (v) => parseInt(v) ? true : false, + // name -> *read* converter, if any + upload_by_username : null, + upload_by_user_id : (u) => parseInt(u) || 0, + file_md5 : null, + file_sha1 : null, + file_crc32 : null, + est_release_year : (y) => parseInt(y) || new Date().getFullYear(), + dl_count : (d) => parseInt(d) || 0, + byte_size : (b) => parseInt(b) || 0, + archive_type : null, + short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import + tic_origin : null, // TIC "Origin" + tic_desc : null, // TIC "Desc" + tic_ldesc : null, // TIC "Ldesc" joined by '\n' + session_temp_dl : (v) => parseInt(v) ? true : false, }; module.exports = class FileEntry { - constructor(options) { - options = options || {}; + constructor(options) { + options = options || {}; - this.fileId = options.fileId || 0; - this.areaTag = options.areaTag || ''; - this.meta = Object.assign( { dl_count : 0 }, options.meta); - this.hashTags = options.hashTags || new Set(); - this.fileName = options.fileName; - this.storageTag = options.storageTag; - this.fileSha256 = options.fileSha256; - } + this.fileId = options.fileId || 0; + this.areaTag = options.areaTag || ''; + this.meta = Object.assign( { dl_count : 0 }, options.meta); + this.hashTags = options.hashTags || new Set(); + this.fileName = options.fileName; + this.storageTag = options.storageTag; + this.fileSha256 = options.fileSha256; + } - static loadBasicEntry(fileId, dest, cb) { - dest = dest || {}; + static loadBasicEntry(fileId, dest, cb) { + dest = dest || {}; - fileDb.get( - `SELECT ${FILE_TABLE_MEMBERS.join(', ')} + fileDb.get( + `SELECT ${FILE_TABLE_MEMBERS.join(', ')} FROM file WHERE file_id=? LIMIT 1;`, - [ fileId ], - (err, file) => { - if(err) { - return cb(err); - } + [ fileId ], + (err, file) => { + if(err) { + return cb(err); + } - if(!file) { - return cb(Errors.DoesNotExist('No file is available by that ID')); - } + if(!file) { + return cb(Errors.DoesNotExist('No file is available by that ID')); + } - // assign props from |file| - FILE_TABLE_MEMBERS.forEach(prop => { - dest[_.camelCase(prop)] = file[prop]; - }); + // assign props from |file| + FILE_TABLE_MEMBERS.forEach(prop => { + dest[_.camelCase(prop)] = file[prop]; + }); - return cb(null, dest); - } - ); - } + return cb(null, dest); + } + ); + } - load(fileId, cb) { - const self = this; + load(fileId, cb) { + const self = this; - async.series( - [ - function loadBasicEntry(callback) { - FileEntry.loadBasicEntry(fileId, self, callback); - }, - function loadMeta(callback) { - return self.loadMeta(callback); - }, - function loadHashTags(callback) { - return self.loadHashTags(callback); - }, - function loadUserRating(callback) { - return self.loadRating(callback); - } - ], - err => { - return cb(err); - } - ); - } + async.series( + [ + function loadBasicEntry(callback) { + FileEntry.loadBasicEntry(fileId, self, callback); + }, + function loadMeta(callback) { + return self.loadMeta(callback); + }, + function loadHashTags(callback) { + return self.loadHashTags(callback); + }, + function loadUserRating(callback) { + return self.loadRating(callback); + } + ], + err => { + return cb(err); + } + ); + } - persist(isUpdate, cb) { - if(!cb && _.isFunction(isUpdate)) { - cb = isUpdate; - isUpdate = false; - } + persist(isUpdate, cb) { + if(!cb && _.isFunction(isUpdate)) { + cb = isUpdate; + isUpdate = false; + } - const self = this; + const self = this; - async.waterfall( - [ - function check(callback) { - if(isUpdate && !self.fileId) { - return callback(Errors.Invalid('Cannot update file entry without an existing "fileId" member')); - } - return callback(null); - }, - function calcSha256IfNeeded(callback) { - if(self.fileSha256) { - return callback(null); - } + async.waterfall( + [ + function check(callback) { + if(isUpdate && !self.fileId) { + return callback(Errors.Invalid('Cannot update file entry without an existing "fileId" member')); + } + return callback(null); + }, + function calcSha256IfNeeded(callback) { + if(self.fileSha256) { + return callback(null); + } - if(isUpdate) { - return callback(Errors.MissingParam('fileSha256 property must be set for updates!')); - } + if(isUpdate) { + return callback(Errors.MissingParam('fileSha256 property must be set for updates!')); + } - readFile(self.filePath, (err, data) => { - if(err) { - return callback(err); - } + readFile(self.filePath, (err, data) => { + if(err) { + return callback(err); + } - const sha256 = crypto.createHash('sha256'); - sha256.update(data); - self.fileSha256 = sha256.digest('hex'); - return callback(null); - }); - }, - function startTrans(callback) { - return fileDb.beginTransaction(callback); - }, - function storeEntry(trans, callback) { - if(isUpdate) { - trans.run( - `REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) + const sha256 = crypto.createHash('sha256'); + sha256.update(data); + self.fileSha256 = sha256.digest('hex'); + return callback(null); + }); + }, + function startTrans(callback) { + return fileDb.beginTransaction(callback); + }, + function storeEntry(trans, callback) { + if(isUpdate) { + trans.run( + `REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, - [ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], - err => { - return callback(err, trans); - } - ); - } else { - trans.run( - `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) + [ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], + err => { + return callback(err, trans); + } + ); + } else { + trans.run( + `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) VALUES(?, ?, ?, ?, ?, ?, ?);`, - [ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], - function inserted(err) { // use non-arrow func for 'this' scope / lastID - if(!err) { - self.fileId = this.lastID; - } - return callback(err, trans); - } - ); - } - }, - function storeMeta(trans, callback) { - async.each(Object.keys(self.meta), (n, next) => { - const v = self.meta[n]; - return FileEntry.persistMetaValue(self.fileId, n, v, trans, next); - }, - err => { - return callback(err, trans); - }); - }, - function storeHashTags(trans, callback) { - const hashTagsArray = Array.from(self.hashTags); - async.each(hashTagsArray, (hashTag, next) => { - return FileEntry.persistHashTag(self.fileId, hashTag, trans, next); - }, - err => { - return callback(err, trans); - }); - } - ], - (err, trans) => { - // :TODO: Log orig err - if(trans) { - trans[err ? 'rollback' : 'commit'](transErr => { - return cb(transErr ? transErr : err); - }); - } else { - return cb(err); - } - } - ); - } + [ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], + function inserted(err) { // use non-arrow func for 'this' scope / lastID + if(!err) { + self.fileId = this.lastID; + } + return callback(err, trans); + } + ); + } + }, + function storeMeta(trans, callback) { + async.each(Object.keys(self.meta), (n, next) => { + const v = self.meta[n]; + return FileEntry.persistMetaValue(self.fileId, n, v, trans, next); + }, + err => { + return callback(err, trans); + }); + }, + function storeHashTags(trans, callback) { + const hashTagsArray = Array.from(self.hashTags); + async.each(hashTagsArray, (hashTag, next) => { + return FileEntry.persistHashTag(self.fileId, hashTag, trans, next); + }, + err => { + return callback(err, trans); + }); + } + ], + (err, trans) => { + // :TODO: Log orig err + if(trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(transErr ? transErr : err); + }); + } else { + return cb(err); + } + } + ); + } - static getAreaStorageDirectoryByTag(storageTag) { - const config = Config(); - const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); + static getAreaStorageDirectoryByTag(storageTag) { + const config = Config(); + const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); - // absolute paths as-is - if(storageLocation && '/' === storageLocation.charAt(0)) { - return storageLocation; - } + // absolute paths as-is + if(storageLocation && '/' === storageLocation.charAt(0)) { + return storageLocation; + } - // relative to |areaStoragePrefix| - return paths.join(config.fileBase.areaStoragePrefix, storageLocation || ''); - } + // relative to |areaStoragePrefix| + return paths.join(config.fileBase.areaStoragePrefix, storageLocation || ''); + } - get filePath() { - const storageDir = FileEntry.getAreaStorageDirectoryByTag(this.storageTag); - return paths.join(storageDir, this.fileName); - } + get filePath() { + const storageDir = FileEntry.getAreaStorageDirectoryByTag(this.storageTag); + return paths.join(storageDir, this.fileName); + } - static quickCheckExistsByPath(fullPath, cb) { - fileDb.get( - `SELECT COUNT() AS count + static quickCheckExistsByPath(fullPath, cb) { + fileDb.get( + `SELECT COUNT() AS count FROM file WHERE file_name = ? LIMIT 1;`, - [ paths.basename(fullPath) ], - (err, rows) => { - return err ? cb(err) : cb(null, rows.count > 0 ? true : false); - } - ); - } + [ paths.basename(fullPath) ], + (err, rows) => { + return err ? cb(err) : cb(null, rows.count > 0 ? true : false); + } + ); + } - static persistUserRating(fileId, userId, rating, cb) { - return fileDb.run( - `REPLACE INTO file_user_rating (file_id, user_id, rating) + static persistUserRating(fileId, userId, rating, cb) { + return fileDb.run( + `REPLACE INTO file_user_rating (file_id, user_id, rating) VALUES (?, ?, ?);`, - [ fileId, userId, rating ], - cb - ); - } + [ fileId, userId, rating ], + cb + ); + } - static persistMetaValue(fileId, name, value, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = fileDb; - } + static persistMetaValue(fileId, name, value, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = fileDb; + } - return transOrDb.run( - `REPLACE INTO file_meta (file_id, meta_name, meta_value) + return transOrDb.run( + `REPLACE INTO file_meta (file_id, meta_name, meta_value) VALUES (?, ?, ?);`, - [ fileId, name, value ], - cb - ); - } + [ fileId, name, value ], + cb + ); + } - static incrementAndPersistMetaValue(fileId, name, incrementBy, cb) { - incrementBy = incrementBy || 1; - fileDb.run( - `UPDATE file_meta + static incrementAndPersistMetaValue(fileId, name, incrementBy, cb) { + incrementBy = incrementBy || 1; + fileDb.run( + `UPDATE file_meta SET meta_value = meta_value + ? WHERE file_id = ? AND meta_name = ?;`, - [ incrementBy, fileId, name ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + [ incrementBy, fileId, name ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - loadMeta(cb) { - fileDb.each( - `SELECT meta_name, meta_value + loadMeta(cb) { + fileDb.each( + `SELECT meta_name, meta_value FROM file_meta WHERE file_id=?;`, - [ this.fileId ], - (err, meta) => { - if(meta) { - const conv = FILE_WELL_KNOWN_META[meta.meta_name]; - this.meta[meta.meta_name] = conv ? conv(meta.meta_value) : meta.meta_value; - } - }, - err => { - return cb(err); - } - ); - } + [ this.fileId ], + (err, meta) => { + if(meta) { + const conv = FILE_WELL_KNOWN_META[meta.meta_name]; + this.meta[meta.meta_name] = conv ? conv(meta.meta_value) : meta.meta_value; + } + }, + err => { + return cb(err); + } + ); + } - static persistHashTag(fileId, hashTag, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = fileDb; - } + static persistHashTag(fileId, hashTag, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = fileDb; + } - transOrDb.serialize( () => { - transOrDb.run( - `INSERT OR IGNORE INTO hash_tag (hash_tag) + transOrDb.serialize( () => { + transOrDb.run( + `INSERT OR IGNORE INTO hash_tag (hash_tag) VALUES (?);`, - [ hashTag ] - ); + [ hashTag ] + ); - transOrDb.run( - `REPLACE INTO file_hash_tag (hash_tag_id, file_id) + transOrDb.run( + `REPLACE INTO file_hash_tag (hash_tag_id, file_id) VALUES ( (SELECT hash_tag_id FROM hash_tag WHERE hash_tag = ?), ? );`, - [ hashTag, fileId ], - err => { - return cb(err); - } - ); - }); - } + [ hashTag, fileId ], + err => { + return cb(err); + } + ); + }); + } - loadHashTags(cb) { - fileDb.each( - `SELECT ht.hash_tag_id, ht.hash_tag + loadHashTags(cb) { + fileDb.each( + `SELECT ht.hash_tag_id, ht.hash_tag FROM hash_tag ht WHERE ht.hash_tag_id IN ( SELECT hash_tag_id FROM file_hash_tag WHERE file_id=? );`, - [ this.fileId ], - (err, hashTag) => { - if(hashTag) { - this.hashTags.add(hashTag.hash_tag); - } - }, - err => { - return cb(err); - } - ); - } + [ this.fileId ], + (err, hashTag) => { + if(hashTag) { + this.hashTags.add(hashTag.hash_tag); + } + }, + err => { + return cb(err); + } + ); + } - loadRating(cb) { - fileDb.get( - `SELECT AVG(fur.rating) AS avg_rating + loadRating(cb) { + fileDb.get( + `SELECT AVG(fur.rating) AS avg_rating FROM file_user_rating fur INNER JOIN file f ON f.file_id = fur.file_id AND f.file_id = ?`, - [ this.fileId ], - (err, result) => { - if(result) { - this.userRating = result.avg_rating; - } - return cb(err); - } - ); - } + [ this.fileId ], + (err, result) => { + if(result) { + this.userRating = result.avg_rating; + } + return cb(err); + } + ); + } - setHashTags(hashTags) { - if(_.isString(hashTags)) { - this.hashTags = new Set(hashTags.split(/[\s,]+/)); - } else if(Array.isArray(hashTags)) { - this.hashTags = new Set(hashTags); - } else if(hashTags instanceof Set) { - this.hashTags = hashTags; - } - } + setHashTags(hashTags) { + if(_.isString(hashTags)) { + this.hashTags = new Set(hashTags.split(/[\s,]+/)); + } else if(Array.isArray(hashTags)) { + this.hashTags = new Set(hashTags); + } else if(hashTags instanceof Set) { + this.hashTags = hashTags; + } + } - static get WellKnownMetaValues() { - return Object.keys(FILE_WELL_KNOWN_META); - } + static get WellKnownMetaValues() { + return Object.keys(FILE_WELL_KNOWN_META); + } - static findFileBySha(sha, cb) { - // full or partial SHA-256 - fileDb.all( - `SELECT file_id + static findFileBySha(sha, cb) { + // full or partial SHA-256 + fileDb.all( + `SELECT file_id FROM file WHERE file_sha256 LIKE "${sha}%" LIMIT 2;`, // limit 2 such that we can find if there are dupes - (err, fileIdRows) => { - if(err) { - return cb(err); - } + (err, fileIdRows) => { + if(err) { + return cb(err); + } - if(!fileIdRows || 0 === fileIdRows.length) { - return cb(Errors.DoesNotExist('No matches')); - } + if(!fileIdRows || 0 === fileIdRows.length) { + return cb(Errors.DoesNotExist('No matches')); + } - if(fileIdRows.length > 1) { - return cb(Errors.Invalid('SHA is ambiguous')); - } + if(fileIdRows.length > 1) { + return cb(Errors.Invalid('SHA is ambiguous')); + } - const fileEntry = new FileEntry(); - return fileEntry.load(fileIdRows[0].file_id, err => { - return cb(err, fileEntry); - }); - } - ); - } + const fileEntry = new FileEntry(); + return fileEntry.load(fileIdRows[0].file_id, err => { + return cb(err, fileEntry); + }); + } + ); + } - static findByFileNameWildcard(wc, cb) { - // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html - wc = wc.replace(/\*/g, '%').replace(/\?/g, '_'); + static findByFileNameWildcard(wc, cb) { + // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html + wc = wc.replace(/\*/g, '%').replace(/\?/g, '_'); - fileDb.all( - `SELECT file_id + fileDb.all( + `SELECT file_id FROM file WHERE file_name LIKE "${wc}" `, - (err, fileIdRows) => { - if(err) { - return cb(err); - } + (err, fileIdRows) => { + if(err) { + return cb(err); + } - if(!fileIdRows || 0 === fileIdRows.length) { - return cb(Errors.DoesNotExist('No matches')); - } + if(!fileIdRows || 0 === fileIdRows.length) { + return cb(Errors.DoesNotExist('No matches')); + } - const entries = []; - async.each(fileIdRows, (row, nextRow) => { - const fileEntry = new FileEntry(); - fileEntry.load(row.file_id, err => { - if(!err) { - entries.push(fileEntry); - } - return nextRow(err); - }); - }, - err => { - return cb(err, entries); - }); - } - ); - } + const entries = []; + async.each(fileIdRows, (row, nextRow) => { + const fileEntry = new FileEntry(); + fileEntry.load(row.file_id, err => { + if(!err) { + entries.push(fileEntry); + } + return nextRow(err); + }); + }, + err => { + return cb(err, entries); + }); + } + ); + } - static findFiles(filter, cb) { - filter = filter || {}; + static findFiles(filter, cb) { + filter = filter || {}; - let sql; - let sqlWhere = ''; - let sqlOrderBy; - const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; + let sql; + let sqlWhere = ''; + let sqlOrderBy; + const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; - if(moment.isMoment(filter.newerThanTimestamp)) { - filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); - } + if(moment.isMoment(filter.newerThanTimestamp)) { + filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); + } - function getOrderByWithCast(ob) { - if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) { - return `ORDER BY CAST(${ob} AS INTEGER)`; - } + function getOrderByWithCast(ob) { + if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) { + return `ORDER BY CAST(${ob} AS INTEGER)`; + } - return `ORDER BY ${ob}`; - } + return `ORDER BY ${ob}`; + } - function appendWhereClause(clause) { - if(sqlWhere) { - sqlWhere += ' AND '; - } else { - sqlWhere += ' WHERE '; - } - sqlWhere += clause; - } + function appendWhereClause(clause) { + if(sqlWhere) { + sqlWhere += ' AND '; + } else { + sqlWhere += ' WHERE '; + } + sqlWhere += clause; + } - if(filter.sort && filter.sort.length > 0) { - if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? - sql = + if(filter.sort && filter.sort.length > 0) { + if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? + sql = `SELECT DISTINCT f.file_id FROM file f, file_meta m`; - appendWhereClause(`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`); + appendWhereClause(`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`); - sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`; - } else { - // additional special treatment for user ratings: we need to average them - if('user_rating' === filter.sort) { - sql = + sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`; + } else { + // additional special treatment for user ratings: we need to average them + if('user_rating' === filter.sort) { + sql = `SELECT DISTINCT f.file_id, (SELECT IFNULL(AVG(rating), 0) rating FROM file_user_rating @@ -481,78 +481,78 @@ module.exports = class FileEntry { AS avg_rating FROM file f`; - sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; - } else { - sql = + sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; + } else { + sql = `SELECT DISTINCT f.file_id FROM file f`; - sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; - } - } - } else { - sql = + sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; + } + } + } else { + sql = `SELECT DISTINCT f.file_id FROM file f`; - sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; - } + sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; + } - if(filter.areaTag && filter.areaTag.length > 0) { - if(Array.isArray(filter.areaTag)) { - const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); - appendWhereClause(`f.area_tag IN(${areaList})`); - } else { - appendWhereClause(`f.area_tag = "${filter.areaTag}"`); - } - } + if(filter.areaTag && filter.areaTag.length > 0) { + if(Array.isArray(filter.areaTag)) { + const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); + appendWhereClause(`f.area_tag IN(${areaList})`); + } else { + appendWhereClause(`f.area_tag = "${filter.areaTag}"`); + } + } - if(filter.metaPairs && filter.metaPairs.length > 0) { + if(filter.metaPairs && filter.metaPairs.length > 0) { - filter.metaPairs.forEach(mp => { - if(mp.wildcards) { - // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html - mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); - appendWhereClause( - `f.file_id IN ( + filter.metaPairs.forEach(mp => { + if(mp.wildcards) { + // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html + mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); + appendWhereClause( + `f.file_id IN ( SELECT file_id FROM file_meta WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}" )` - ); - } else { - appendWhereClause( - `f.file_id IN ( + ); + } else { + appendWhereClause( + `f.file_id IN ( SELECT file_id FROM file_meta WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}" )` - ); - } - }); - } + ); + } + }); + } - if(filter.storageTag && filter.storageTag.length > 0) { - appendWhereClause(`f.storage_tag="${filter.storageTag}"`); - } + if(filter.storageTag && filter.storageTag.length > 0) { + appendWhereClause(`f.storage_tag="${filter.storageTag}"`); + } - if(filter.terms && filter.terms.length > 0) { - // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex - appendWhereClause( - `f.file_id IN ( + if(filter.terms && filter.terms.length > 0) { + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + appendWhereClause( + `f.file_id IN ( SELECT rowid FROM file_fts WHERE file_fts MATCH ":${sanatizeString(filter.terms)}" )` - ); - } + ); + } - if(filter.tags && filter.tags.length > 0) { - // build list of quoted tags; filter.tags comes in as a space and/or comma separated values - const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanatizeString(tag)}"` ).join(','); + if(filter.tags && filter.tags.length > 0) { + // build list of quoted tags; filter.tags comes in as a space and/or comma separated values + const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanatizeString(tag)}"` ).join(','); - appendWhereClause( - `f.file_id IN ( + appendWhereClause( + `f.file_id IN ( SELECT file_id FROM file_hash_tag WHERE hash_tag_id IN ( @@ -561,111 +561,111 @@ module.exports = class FileEntry { WHERE hash_tag IN (${tags}) ) )` - ); - } + ); + } - if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { - appendWhereClause(`DATETIME(f.upload_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); - } + 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}`); - } + if(_.isNumber(filter.newerThanFileId)) { + appendWhereClause(`f.file_id > ${filter.newerThanFileId}`); + } - sql += `${sqlWhere} ${sqlOrderBy}`; + sql += `${sqlWhere} ${sqlOrderBy}`; - if(_.isNumber(filter.limit)) { - sql += ` LIMIT ${filter.limit}`; - } + if(_.isNumber(filter.limit)) { + sql += ` LIMIT ${filter.limit}`; + } - sql += ';'; + sql += ';'; - fileDb.all(sql, (err, rows) => { - if(err) { - return cb(err); - } - if(!rows || 0 === rows.length) { - return cb(null, []); // no matches - } - return cb(null, rows.map(r => r.file_id)); - }); - } + fileDb.all(sql, (err, rows) => { + if(err) { + return cb(err); + } + if(!rows || 0 === rows.length) { + return cb(null, []); // no matches + } + return cb(null, rows.map(r => r.file_id)); + }); + } - static removeEntry(srcFileEntry, options, cb) { - if(!_.isFunction(cb) && _.isFunction(options)) { - cb = options; - options = {}; - } + static removeEntry(srcFileEntry, options, cb) { + if(!_.isFunction(cb) && _.isFunction(options)) { + cb = options; + options = {}; + } - async.series( - [ - function removeFromDatabase(callback) { - fileDb.run( - `DELETE FROM file + async.series( + [ + function removeFromDatabase(callback) { + fileDb.run( + `DELETE FROM file WHERE file_id = ?;`, - [ srcFileEntry.fileId ], - err => { - return callback(err); - } - ); - }, - function optionallyRemovePhysicalFile(callback) { - if(true !== options.removePhysFile) { - return callback(null); - } + [ srcFileEntry.fileId ], + err => { + return callback(err); + } + ); + }, + function optionallyRemovePhysicalFile(callback) { + if(true !== options.removePhysFile) { + return callback(null); + } - unlink(srcFileEntry.filePath, err => { - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); - } + unlink(srcFileEntry.filePath, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } - static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) { - if(!cb && _.isFunction(destFileName)) { - cb = destFileName; - destFileName = srcFileEntry.fileName; - } + static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) { + if(!cb && _.isFunction(destFileName)) { + cb = destFileName; + destFileName = srcFileEntry.fileName; + } - const srcPath = srcFileEntry.filePath; - const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); + const srcPath = srcFileEntry.filePath; + const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); - if(!dstDir) { - return cb(Errors.Invalid('Invalid storage tag')); - } + if(!dstDir) { + return cb(Errors.Invalid('Invalid storage tag')); + } - const dstPath = paths.join(dstDir, destFileName); + const dstPath = paths.join(dstDir, destFileName); - async.series( - [ - function movePhysFile(callback) { - if(srcPath === dstPath) { - return callback(null); // don't need to move file, but may change areas - } + async.series( + [ + function movePhysFile(callback) { + if(srcPath === dstPath) { + return callback(null); // don't need to move file, but may change areas + } - fse.move(srcPath, dstPath, err => { - return callback(err); - }); - }, - function updateDatabase(callback) { - fileDb.run( - `UPDATE file + fse.move(srcPath, dstPath, err => { + return callback(err); + }); + }, + function updateDatabase(callback) { + fileDb.run( + `UPDATE file SET area_tag = ?, file_name = ?, storage_tag = ? WHERE file_id = ?;`, - [ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ], - err => { - return callback(err); - } - ); - } - ], - err => { - return cb(err); - } - ); - } + [ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ], + err => { + return callback(err); + } + ); + } + ], + err => { + return cb(err); + } + ); + } }; diff --git a/core/file_transfer.js b/core/file_transfer.js index 456898c9..e66a98f7 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -42,113 +42,113 @@ const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. */ exports.moduleInfo = { - name : 'Transfer file', - desc : 'Sends or receives a file(s)', - author : 'NuSkooler', + name : 'Transfer file', + desc : 'Sends or receives a file(s)', + author : 'NuSkooler', }; exports.getModule = class TransferFileModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.config = this.menuConfig.config || {}; + this.config = this.menuConfig.config || {}; - // - // Most options can be set via extraArgs or config block - // - const config = Config(); - if(options.extraArgs) { - if(options.extraArgs.protocol) { - this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol]; - } + // + // Most options can be set via extraArgs or config block + // + const config = Config(); + if(options.extraArgs) { + if(options.extraArgs.protocol) { + this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol]; + } - if(options.extraArgs.direction) { - this.direction = options.extraArgs.direction; - } + if(options.extraArgs.direction) { + this.direction = options.extraArgs.direction; + } - if(options.extraArgs.sendQueue) { - this.sendQueue = options.extraArgs.sendQueue; - } + if(options.extraArgs.sendQueue) { + this.sendQueue = options.extraArgs.sendQueue; + } - if(options.extraArgs.recvFileName) { - this.recvFileName = options.extraArgs.recvFileName; - } + if(options.extraArgs.recvFileName) { + this.recvFileName = options.extraArgs.recvFileName; + } - if(options.extraArgs.recvDirectory) { - this.recvDirectory = options.extraArgs.recvDirectory; - } - } else { - if(this.config.protocol) { - this.protocolConfig = config.fileTransferProtocols[this.config.protocol]; - } + if(options.extraArgs.recvDirectory) { + this.recvDirectory = options.extraArgs.recvDirectory; + } + } else { + if(this.config.protocol) { + this.protocolConfig = config.fileTransferProtocols[this.config.protocol]; + } - if(this.config.direction) { - this.direction = this.config.direction; - } + if(this.config.direction) { + this.direction = this.config.direction; + } - if(this.config.sendQueue) { - this.sendQueue = this.config.sendQueue; - } + if(this.config.sendQueue) { + this.sendQueue = this.config.sendQueue; + } - if(this.config.recvFileName) { - this.recvFileName = this.config.recvFileName; - } + if(this.config.recvFileName) { + this.recvFileName = this.config.recvFileName; + } - if(this.config.recvDirectory) { - this.recvDirectory = this.config.recvDirectory; - } - } + if(this.config.recvDirectory) { + this.recvDirectory = this.config.recvDirectory; + } + } - this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* - this.direction = this.direction || 'send'; - this.sendQueue = this.sendQueue || []; + this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* + this.direction = this.direction || 'send'; + this.sendQueue = this.sendQueue || []; - // Ensure sendQueue is an array of objects that contain at least a 'path' member - this.sendQueue = this.sendQueue.map(item => { - if(_.isString(item)) { - return { path : item }; - } else { - return item; - } - }); + // Ensure sendQueue is an array of objects that contain at least a 'path' member + this.sendQueue = this.sendQueue.map(item => { + if(_.isString(item)) { + return { path : item }; + } else { + return item; + } + }); - this.sentFileIds = []; - } + this.sentFileIds = []; + } - isSending() { - return ('send' === this.direction); - } + isSending() { + return ('send' === this.direction); + } - restorePipeAfterExternalProc() { - if(!this.pipeRestored) { - this.pipeRestored = true; + restorePipeAfterExternalProc() { + if(!this.pipeRestored) { + this.pipeRestored = true; - this.client.restoreDataHandler(); - } - } + this.client.restoreDataHandler(); + } + } - sendFiles(cb) { - // assume *sending* can always batch - // :TODO: Look into this further - const allFiles = this.sendQueue.map(f => f.path); - this.executeExternalProtocolHandlerForSend(allFiles, err => { - if(err) { - this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); - } else { - const sentFiles = []; - this.sendQueue.forEach(f => { - f.sent = true; - sentFiles.push(f.path); + sendFiles(cb) { + // assume *sending* can always batch + // :TODO: Look into this further + const allFiles = this.sendQueue.map(f => f.path); + this.executeExternalProtocolHandlerForSend(allFiles, err => { + if(err) { + this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); + } else { + const sentFiles = []; + this.sendQueue.forEach(f => { + f.sent = true; + sentFiles.push(f.path); - }); + }); - this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); - } - return cb(err); - }); - } + this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); + } + return cb(err); + }); + } - /* + /* sendFiles(cb) { // :TODO: built in/native protocol support @@ -189,408 +189,408 @@ exports.getModule = class TransferFileModule extends MenuModule { } */ - moveFileWithCollisionHandling(src, dst, cb) { - // - // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. - // in the case of collisions. - // - const dstPath = paths.dirname(dst); - const dstFileExt = paths.extname(dst); - const dstFileSuffix = paths.basename(dst, dstFileExt); + moveFileWithCollisionHandling(src, dst, cb) { + // + // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. + // in the case of collisions. + // + const dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); + const dstFileSuffix = paths.basename(dst, dstFileExt); - let renameIndex = 0; - let movedOk = false; - let tryDstPath; + let renameIndex = 0; + let movedOk = false; + let tryDstPath; - async.until( - () => movedOk, // until moved OK - (cb) => { - if(0 === renameIndex) { - // try originally supplied path first - tryDstPath = dst; - } else { - tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); - } + async.until( + () => movedOk, // until moved OK + (cb) => { + if(0 === renameIndex) { + // try originally supplied path first + tryDstPath = dst; + } else { + tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); + } - fse.move(src, tryDstPath, err => { - if(err) { - if('EEXIST' === err.code) { - renameIndex += 1; - return cb(null); // keep trying - } + fse.move(src, tryDstPath, err => { + if(err) { + if('EEXIST' === err.code) { + renameIndex += 1; + return cb(null); // keep trying + } - return cb(err); - } + return cb(err); + } - movedOk = true; - return cb(null, tryDstPath); - }); - }, - (err, finalPath) => { - return cb(err, finalPath); - } - ); - } + movedOk = true; + return cb(null, tryDstPath); + }); + }, + (err, finalPath) => { + return cb(err, finalPath); + } + ); + } - recvFiles(cb) { - this.executeExternalProtocolHandlerForRecv(err => { - if(err) { - return cb(err); - } + recvFiles(cb) { + this.executeExternalProtocolHandlerForRecv(err => { + if(err) { + return cb(err); + } - this.recvFilePaths = []; + this.recvFilePaths = []; - if(this.recvFileName) { - // - // file name specified - we expect a single file in |this.recvDirectory| - // by the name of |this.recvFileName| - // - const recvFullPath = paths.join(this.recvDirectory, this.recvFileName); - fs.stat(recvFullPath, (err, stats) => { - if(err) { - return cb(err); - } + if(this.recvFileName) { + // + // file name specified - we expect a single file in |this.recvDirectory| + // by the name of |this.recvFileName| + // + const recvFullPath = paths.join(this.recvDirectory, this.recvFileName); + fs.stat(recvFullPath, (err, stats) => { + if(err) { + return cb(err); + } - if(!stats.isFile()) { - return cb(Errors.Invalid('Expected file entry in recv directory')); - } + if(!stats.isFile()) { + return cb(Errors.Invalid('Expected file entry in recv directory')); + } - this.recvFilePaths.push(recvFullPath); - return cb(null); - }); - } else { - // - // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already - // - fs.readdir(this.recvDirectory, (err, files) => { - if(err) { - return cb(err); - } + this.recvFilePaths.push(recvFullPath); + return cb(null); + }); + } else { + // + // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already + // + fs.readdir(this.recvDirectory, (err, files) => { + if(err) { + return cb(err); + } - // stat each to grab files only - async.each(files, (fileName, nextFile) => { - const recvFullPath = paths.join(this.recvDirectory, fileName); + // stat each to grab files only + async.each(files, (fileName, nextFile) => { + const recvFullPath = paths.join(this.recvDirectory, fileName); - fs.stat(recvFullPath, (err, stats) => { - if(err) { - this.client.log.warn('Failed to stat file', { path : recvFullPath } ); - return nextFile(null); // just try the next one - } + fs.stat(recvFullPath, (err, stats) => { + if(err) { + this.client.log.warn('Failed to stat file', { path : recvFullPath } ); + return nextFile(null); // just try the next one + } - if(stats.isFile()) { - this.recvFilePaths.push(recvFullPath); - } + if(stats.isFile()) { + this.recvFilePaths.push(recvFullPath); + } - return nextFile(null); - }); - }, () => { - return cb(null); - }); - }); - } - }); - } + return nextFile(null); + }); + }, () => { + return cb(null); + }); + }); + } + }); + } - pathWithTerminatingSeparator(path) { - if(path && paths.sep !== path.charAt(path.length - 1)) { - path = path + paths.sep; - } - return path; - } + pathWithTerminatingSeparator(path) { + if(path && paths.sep !== path.charAt(path.length - 1)) { + path = path + paths.sep; + } + return path; + } - prepAndBuildSendArgs(filePaths, cb) { - const externalArgs = this.protocolConfig.external['sendArgs']; + prepAndBuildSendArgs(filePaths, cb) { + const externalArgs = this.protocolConfig.external['sendArgs']; - async.waterfall( - [ - function getTempFileListPath(callback) { - const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) ); - if(!hasFileList) { - return callback(null, null); - } + async.waterfall( + [ + function getTempFileListPath(callback) { + const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) ); + if(!hasFileList) { + return callback(null, null); + } - temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { - if(err) { - return callback(err); // failed to create it - } + temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { + if(err) { + return callback(err); // failed to create it + } - fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); - fs.close(tempFileInfo.fd, err => { - return callback(err, tempFileInfo.path); - }); - }); - }, - function createArgs(tempFileListPath, callback) { - // initial args: ignore {filePaths} as we must break that into it's own sep array items - const args = externalArgs.map(arg => { - return '{filePaths}' === arg ? arg : stringFormat(arg, { - fileListPath : tempFileListPath || '', - }); - }); + fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); + fs.close(tempFileInfo.fd, err => { + return callback(err, tempFileInfo.path); + }); + }); + }, + function createArgs(tempFileListPath, callback) { + // initial args: ignore {filePaths} as we must break that into it's own sep array items + const args = externalArgs.map(arg => { + return '{filePaths}' === arg ? arg : stringFormat(arg, { + fileListPath : tempFileListPath || '', + }); + }); - const filePathsPos = args.indexOf('{filePaths}'); - if(filePathsPos > -1) { - // replace {filePaths} with 0:n individual entries in |args| - args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); - } + const filePathsPos = args.indexOf('{filePaths}'); + if(filePathsPos > -1) { + // replace {filePaths} with 0:n individual entries in |args| + args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); + } - return callback(null, args); - } - ], - (err, args) => { - return cb(err, args); - } - ); - } + return callback(null, args); + } + ], + (err, args) => { + return cb(err, args); + } + ); + } - prepAndBuildRecvArgs(cb) { - const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; - const externalArgs = this.protocolConfig.external[argsKey]; - const args = externalArgs.map(arg => stringFormat(arg, { - uploadDir : this.recvDirectory, - fileName : this.recvFileName || '', - })); + prepAndBuildRecvArgs(cb) { + const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; + const externalArgs = this.protocolConfig.external[argsKey]; + const args = externalArgs.map(arg => stringFormat(arg, { + uploadDir : this.recvDirectory, + fileName : this.recvFileName || '', + })); - return cb(null, args); - } + return cb(null, args); + } - executeExternalProtocolHandler(args, cb) { - const external = this.protocolConfig.external; - const cmd = external[`${this.direction}Cmd`]; + executeExternalProtocolHandler(args, cb) { + const external = this.protocolConfig.external; + const cmd = external[`${this.direction}Cmd`]; - this.client.log.debug( - { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, - 'Executing external protocol' - ); + this.client.log.debug( + { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, + 'Executing external protocol' + ); - const spawnOpts = { - cols : this.client.term.termWidth, - rows : this.client.term.termHeight, - cwd : this.recvDirectory, - encoding : null, // don't bork our data! - }; + const spawnOpts = { + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, + cwd : this.recvDirectory, + encoding : null, // don't bork our data! + }; - const externalProc = pty.spawn(cmd, args, spawnOpts); + const externalProc = pty.spawn(cmd, args, spawnOpts); - this.client.setTemporaryDirectDataHandler(data => { - // needed for things like sz/rz - if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape - externalProc.write(Buffer.from(tmp, 'binary')); - } else { - externalProc.write(data); - } - }); + this.client.setTemporaryDirectDataHandler(data => { + // needed for things like sz/rz + if(external.escapeTelnet) { + const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape + externalProc.write(Buffer.from(tmp, 'binary')); + } else { + externalProc.write(data); + } + }); - externalProc.on('data', data => { - // needed for things like sz/rz - if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape - this.client.term.rawWrite(Buffer.from(tmp, 'binary')); - } else { - this.client.term.rawWrite(data); - } - }); + externalProc.on('data', data => { + // needed for things like sz/rz + if(external.escapeTelnet) { + const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape + this.client.term.rawWrite(Buffer.from(tmp, 'binary')); + } else { + this.client.term.rawWrite(data); + } + }); - externalProc.once('close', () => { - return this.restorePipeAfterExternalProc(); - }); + externalProc.once('close', () => { + return this.restorePipeAfterExternalProc(); + }); - externalProc.once('exit', (exitCode) => { - this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); + externalProc.once('exit', (exitCode) => { + this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); - this.restorePipeAfterExternalProc(); - externalProc.removeAllListeners(); + this.restorePipeAfterExternalProc(); + externalProc.removeAllListeners(); - return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null); - }); - } + return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null); + }); + } - executeExternalProtocolHandlerForSend(filePaths, cb) { - if(!Array.isArray(filePaths)) { - filePaths = [ filePaths ]; - } + executeExternalProtocolHandlerForSend(filePaths, cb) { + if(!Array.isArray(filePaths)) { + filePaths = [ filePaths ]; + } - this.prepAndBuildSendArgs(filePaths, (err, args) => { - if(err) { - return cb(err); - } + this.prepAndBuildSendArgs(filePaths, (err, args) => { + if(err) { + return cb(err); + } - this.executeExternalProtocolHandler(args, err => { - return cb(err); - }); - }); - } + this.executeExternalProtocolHandler(args, err => { + return cb(err); + }); + }); + } - executeExternalProtocolHandlerForRecv(cb) { - this.prepAndBuildRecvArgs( (err, args) => { - if(err) { - return cb(err); - } + executeExternalProtocolHandlerForRecv(cb) { + this.prepAndBuildRecvArgs( (err, args) => { + if(err) { + return cb(err); + } - this.executeExternalProtocolHandler(args, err => { - return cb(err); - }); - }); - } + this.executeExternalProtocolHandler(args, err => { + return cb(err); + }); + }); + } - getMenuResult() { - if(this.isSending()) { - return { sentFileIds : this.sentFileIds }; - } else { - return { recvFilePaths : this.recvFilePaths }; - } - } + getMenuResult() { + if(this.isSending()) { + return { sentFileIds : this.sentFileIds }; + } else { + return { recvFilePaths : this.recvFilePaths }; + } + } - updateSendStats(cb) { - let downloadBytes = 0; - let downloadCount = 0; - let fileIds = []; + updateSendStats(cb) { + let downloadBytes = 0; + let downloadCount = 0; + let fileIds = []; - async.each(this.sendQueue, (queueItem, next) => { - if(!queueItem.sent) { - return next(null); - } + async.each(this.sendQueue, (queueItem, next) => { + if(!queueItem.sent) { + return next(null); + } - if(queueItem.fileId) { - fileIds.push(queueItem.fileId); - } + if(queueItem.fileId) { + fileIds.push(queueItem.fileId); + } - if(_.isNumber(queueItem.byteSize)) { - downloadCount += 1; - downloadBytes += queueItem.byteSize; - return next(null); - } + if(_.isNumber(queueItem.byteSize)) { + downloadCount += 1; + downloadBytes += queueItem.byteSize; + return next(null); + } - // we just have a path - figure it out - fs.stat(queueItem.path, (err, stats) => { - if(err) { - this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); - } else { - downloadCount += 1; - downloadBytes += stats.size; - } + // we just have a path - figure it out + fs.stat(queueItem.path, (err, stats) => { + if(err) { + this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); + } else { + downloadCount += 1; + downloadBytes += stats.size; + } - return next(null); - }); - }, () => { - // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks - StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount); - StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes); - StatLog.incrementSystemStat('dl_total_count', downloadCount); - StatLog.incrementSystemStat('dl_total_bytes', downloadBytes); + return next(null); + }); + }, () => { + // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks + StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount); + StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes); + StatLog.incrementSystemStat('dl_total_count', downloadCount); + StatLog.incrementSystemStat('dl_total_bytes', downloadBytes); - fileIds.forEach(fileId => { - FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); - }); + fileIds.forEach(fileId => { + FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); + }); - return cb(null); - }); - } + return cb(null); + }); + } - updateRecvStats(cb) { - let uploadBytes = 0; - let uploadCount = 0; + updateRecvStats(cb) { + let uploadBytes = 0; + let uploadCount = 0; - async.each(this.recvFilePaths, (filePath, next) => { - // we just have a path - figure it out - fs.stat(filePath, (err, stats) => { - if(err) { - this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); - } else { - uploadCount += 1; - uploadBytes += stats.size; - } + async.each(this.recvFilePaths, (filePath, next) => { + // we just have a path - figure it out + fs.stat(filePath, (err, stats) => { + if(err) { + this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); + } else { + uploadCount += 1; + uploadBytes += stats.size; + } - return next(null); - }); - }, () => { - StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount); - StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes); - StatLog.incrementSystemStat('ul_total_count', uploadCount); - StatLog.incrementSystemStat('ul_total_bytes', uploadBytes); + return next(null); + }); + }, () => { + StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount); + StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes); + StatLog.incrementSystemStat('ul_total_count', uploadCount); + StatLog.incrementSystemStat('ul_total_bytes', uploadBytes); - return cb(null); - }); - } + return cb(null); + }); + } - initSequence() { - const self = this; + initSequence() { + const self = this; - // :TODO: break this up to send|recv + // :TODO: break this up to send|recv - async.series( - [ - function validateConfig(callback) { - if(self.isSending()) { - if(!Array.isArray(self.sendQueue)) { - self.sendQueue = [ self.sendQueue ]; - } - } + async.series( + [ + function validateConfig(callback) { + if(self.isSending()) { + if(!Array.isArray(self.sendQueue)) { + self.sendQueue = [ self.sendQueue ]; + } + } - return callback(null); - }, - function transferFiles(callback) { - if(self.isSending()) { - self.sendFiles( err => { - if(err) { - return callback(err); - } + return callback(null); + }, + function transferFiles(callback) { + if(self.isSending()) { + self.sendFiles( err => { + if(err) { + return callback(err); + } - const sentFileIds = []; - self.sendQueue.forEach(queueItem => { - if(queueItem.sent && queueItem.fileId) { - sentFileIds.push(queueItem.fileId); - } - }); + const sentFileIds = []; + self.sendQueue.forEach(queueItem => { + if(queueItem.sent && queueItem.fileId) { + sentFileIds.push(queueItem.fileId); + } + }); - if(sentFileIds.length > 0) { - // remove items we sent from the D/L queue - const dlQueue = new DownloadQueue(self.client); - const dlFileEntries = dlQueue.removeItems(sentFileIds); + if(sentFileIds.length > 0) { + // remove items we sent from the D/L queue + const dlQueue = new DownloadQueue(self.client); + const dlFileEntries = dlQueue.removeItems(sentFileIds); - // fire event for downloaded entries - Events.emit( - Events.getSystemEvents().UserDownload, - { - user : self.client.user, - files : dlFileEntries - } - ); + // fire event for downloaded entries + Events.emit( + Events.getSystemEvents().UserDownload, + { + user : self.client.user, + files : dlFileEntries + } + ); - self.sentFileIds = sentFileIds; - } + self.sentFileIds = sentFileIds; + } - return callback(null); - }); - } else { - self.recvFiles( err => { - return callback(err); - }); - } - }, - function cleanupTempFiles(callback) { - temptmp.cleanup( paths => { - Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); - }); + return callback(null); + }); + } else { + self.recvFiles( err => { + return callback(err); + }); + } + }, + function cleanupTempFiles(callback) { + temptmp.cleanup( paths => { + Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); + }); - return callback(null); - }, - function updateUserAndSystemStats(callback) { - if(self.isSending()) { - return self.updateSendStats(callback); - } else { - return self.updateRecvStats(callback); - } - } - ], - err => { - if(err) { - self.client.log.warn( { error : err.message }, 'File transfer error'); - } + return callback(null); + }, + function updateUserAndSystemStats(callback) { + if(self.isSending()) { + return self.updateSendStats(callback); + } else { + return self.updateRecvStats(callback); + } + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'File transfer error'); + } - return self.prevMenu(); - } - ); - } + return self.prevMenu(); + } + ); + } }; diff --git a/core/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js index 3d3bd37b..1fe1944b 100644 --- a/core/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -12,147 +12,147 @@ const async = require('async'); const _ = require('lodash'); exports.moduleInfo = { - name : 'File transfer protocol selection', - desc : 'Select protocol / method for file transfer', - author : 'NuSkooler', + name : 'File transfer protocol selection', + desc : 'Select protocol / method for file transfer', + author : 'NuSkooler', }; const MciViewIds = { - protList : 1, + protList : 1, }; exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.config = this.menuConfig.config || {}; + this.config = this.menuConfig.config || {}; - if(options.extraArgs) { - if(options.extraArgs.direction) { - this.config.direction = options.extraArgs.direction; - } - } + if(options.extraArgs) { + if(options.extraArgs.direction) { + this.config.direction = options.extraArgs.direction; + } + } - this.config.direction = this.config.direction || 'send'; + this.config.direction = this.config.direction || 'send'; - this.extraArgs = options.extraArgs; + this.extraArgs = options.extraArgs; - if(_.has(options, 'lastMenuResult.sentFileIds')) { - this.sentFileIds = options.lastMenuResult.sentFileIds; - } + if(_.has(options, 'lastMenuResult.sentFileIds')) { + this.sentFileIds = options.lastMenuResult.sentFileIds; + } - if(_.has(options, 'lastMenuResult.recvFilePaths')) { - this.recvFilePaths = options.lastMenuResult.recvFilePaths; - } + if(_.has(options, 'lastMenuResult.recvFilePaths')) { + this.recvFilePaths = options.lastMenuResult.recvFilePaths; + } - this.fallbackOnly = options.lastMenuResult ? true : false; + this.fallbackOnly = options.lastMenuResult ? true : false; - this.loadAvailProtocols(); + this.loadAvailProtocols(); - this.menuMethods = { - selectProtocol : (formData, extraArgs, cb) => { - const protocol = this.protocols[formData.value.protocol]; - const finalExtraArgs = this.extraArgs || {}; - Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs ); + this.menuMethods = { + selectProtocol : (formData, extraArgs, cb) => { + const protocol = this.protocols[formData.value.protocol]; + const finalExtraArgs = this.extraArgs || {}; + Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs ); - const modOpts = { - extraArgs : finalExtraArgs, - }; + const modOpts = { + extraArgs : finalExtraArgs, + }; - if('send' === this.config.direction) { - return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb); - } else { - return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb); - } - }, - }; - } + if('send' === this.config.direction) { + return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb); + } else { + return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb); + } + }, + }; + } - getMenuResult() { - if(this.sentFileIds) { - return { sentFileIds : this.sentFileIds }; - } + getMenuResult() { + if(this.sentFileIds) { + return { sentFileIds : this.sentFileIds }; + } - if(this.recvFilePaths) { - return { recvFilePaths : this.recvFilePaths }; - } - } + if(this.recvFilePaths) { + return { recvFilePaths : this.recvFilePaths }; + } + } - initSequence() { - if(this.sentFileIds || this.recvFilePaths) { - // nothing to do here; move along (we're just falling through) - this.prevMenu(); - } else { - super.initSequence(); - } - } + initSequence() { + if(this.sentFileIds || this.recvFilePaths) { + // nothing to do here; move along (we're just falling through) + this.prevMenu(); + } else { + super.initSequence(); + } + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - async.series( - [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu - }; + async.series( + [ + function loadFromConfig(callback) { + const loadOpts = { + callingMenu : self, + mciMap : mciData.menu + }; - return vc.loadFromMenuConfig(loadOpts, callback); - }, - function populateList(callback) { - const protListView = vc.getView(MciViewIds.protList); + return vc.loadFromMenuConfig(loadOpts, callback); + }, + function populateList(callback) { + const protListView = vc.getView(MciViewIds.protList); - const protListFormat = self.config.protListFormat || '{name}'; - const protListFocusFormat = self.config.protListFocusFormat || protListFormat; + const protListFormat = self.config.protListFormat || '{name}'; + const protListFocusFormat = self.config.protListFocusFormat || protListFormat; - protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) ); - protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) ); + protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) ); + protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) ); - protListView.redraw(); + protListView.redraw(); - return callback(null); - } - ], - err => { - return cb(err); - } - ); - }); - } + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } - loadAvailProtocols() { - this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { - return { - protocol : protocol, - name : protInfo.name, - hasBatch : _.has(protInfo, 'external.recvArgs'), - hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), - sort : protInfo.sort, - }; - }); + loadAvailProtocols() { + this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { + return { + protocol : protocol, + name : protInfo.name, + hasBatch : _.has(protInfo, 'external.recvArgs'), + hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), + sort : protInfo.sort, + }; + }); - // Filter out batch vs non-batch only protocols - if(this.extraArgs.recvFileName) { // non-batch aka non-blind - this.protocols = this.protocols.filter( prot => prot.hasNonBatch ); - } else { - this.protocols = this.protocols.filter( prot => prot.hasBatch ); - } + // Filter out batch vs non-batch only protocols + if(this.extraArgs.recvFileName) { // non-batch aka non-blind + this.protocols = this.protocols.filter( prot => prot.hasNonBatch ); + } else { + this.protocols = this.protocols.filter( prot => prot.hasBatch ); + } - // natural sort taking explicit orders into consideration - this.protocols.sort( (a, b) => { - if(_.isNumber(a.sort) && _.isNumber(b.sort)) { - return a.sort - b.sort; - } else { - return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } ); - } - }); - } + // natural sort taking explicit orders into consideration + this.protocols.sort( (a, b) => { + if(_.isNumber(a.sort) && _.isNumber(b.sort)) { + return a.sort - b.sort; + } else { + return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } ); + } + }); + } }; diff --git a/core/file_util.js b/core/file_util.js index 0f91e71a..428622da 100644 --- a/core/file_util.js +++ b/core/file_util.js @@ -14,59 +14,59 @@ exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling; exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { - operation = operation || 'copy'; - const dstPath = paths.dirname(dst); - const dstFileExt = paths.extname(dst); - const dstFileSuffix = paths.basename(dst, dstFileExt); + operation = operation || 'copy'; + const dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); + const dstFileSuffix = paths.basename(dst, dstFileExt); - EnigAssert('move' === operation || 'copy' === operation); + EnigAssert('move' === operation || 'copy' === operation); - let renameIndex = 0; - let opOk = false; - let tryDstPath; + let renameIndex = 0; + let opOk = false; + let tryDstPath; - function tryOperation(src, dst, callback) { - if('move' === operation) { - fse.move(src, tryDstPath, err => { - return callback(err); - }); - } else if('copy' === operation) { - fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => { - return callback(err); - }); - } - } + function tryOperation(src, dst, callback) { + if('move' === operation) { + fse.move(src, tryDstPath, err => { + return callback(err); + }); + } else if('copy' === operation) { + fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => { + return callback(err); + }); + } + } - async.until( - () => opOk, // until moved OK - (cb) => { - if(0 === renameIndex) { - // try originally supplied path first - tryDstPath = dst; - } else { - tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); - } + async.until( + () => opOk, // until moved OK + (cb) => { + if(0 === renameIndex) { + // try originally supplied path first + tryDstPath = dst; + } else { + tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); + } - tryOperation(src, tryDstPath, err => { - if(err) { - // for some reason fs-extra copy doesn't pass err.code - // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST - if('EEXIST' === err.code || 'copy' === operation) { - renameIndex += 1; - return cb(null); // keep trying - } + tryOperation(src, tryDstPath, err => { + if(err) { + // for some reason fs-extra copy doesn't pass err.code + // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST + if('EEXIST' === err.code || 'copy' === operation) { + renameIndex += 1; + return cb(null); // keep trying + } - return cb(err); - } + return cb(err); + } - opOk = true; - return cb(null, tryDstPath); - }); - }, - (err, finalPath) => { - return cb(err, finalPath); - } - ); + opOk = true; + return cb(null, tryDstPath); + }); + }, + (err, finalPath) => { + return cb(err, finalPath); + } + ); } // @@ -74,16 +74,16 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { // in the case of collisions. // function moveFileWithCollisionHandling(src, dst, cb) { - return moveOrCopyFileWithCollisionHandling(src, dst, 'move', cb); + return moveOrCopyFileWithCollisionHandling(src, dst, 'move', cb); } function copyFileWithCollisionHandling(src, dst, cb) { - return moveOrCopyFileWithCollisionHandling(src, dst, 'copy', cb); + return moveOrCopyFileWithCollisionHandling(src, dst, 'copy', cb); } function pathWithTerminatingSeparator(path) { - if(path && paths.sep !== path.charAt(path.length - 1)) { - path = path + paths.sep; - } - return path; + if(path && paths.sep !== path.charAt(path.length - 1)) { + path = path + paths.sep; + } + return path; } diff --git a/core/fnv1a.js b/core/fnv1a.js index 1b8ece32..53400a66 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -5,46 +5,46 @@ let _ = require('lodash'); // FNV-1a based on work here: https://github.com/wiedi/node-fnv module.exports = class FNV1a { - constructor(data) { - this.hash = 0x811c9dc5; + constructor(data) { + this.hash = 0x811c9dc5; - if(!_.isUndefined(data)) { - this.update(data); - } - } + if(!_.isUndefined(data)) { + this.update(data); + } + } - update(data) { - if(_.isNumber(data)) { - data = data.toString(); - } + update(data) { + if(_.isNumber(data)) { + data = data.toString(); + } - if(_.isString(data)) { - data = Buffer.from(data); - } + if(_.isString(data)) { + data = Buffer.from(data); + } - if(!Buffer.isBuffer(data)) { - throw new Error('data must be String or Buffer!'); - } + if(!Buffer.isBuffer(data)) { + throw new Error('data must be String or Buffer!'); + } - for(let b of data) { - this.hash = this.hash ^ b; - this.hash += + for(let b of data) { + this.hash = this.hash ^ b; + this.hash += (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + (this.hash << 4) + (this.hash << 1); - } + } - return this; - } + return this; + } - digest(encoding) { - encoding = encoding || 'binary'; - const buf = Buffer.alloc(4); - buf.writeInt32BE(this.hash & 0xffffffff, 0); - return buf.toString(encoding); - } + digest(encoding) { + encoding = encoding || 'binary'; + const buf = Buffer.alloc(4); + buf.writeInt32BE(this.hash & 0xffffffff, 0); + return buf.toString(encoding); + } - get value() { - return this.hash & 0xffffffff; - } + get value() { + return this.hash & 0xffffffff; + } }; diff --git a/core/fse.js b/core/fse.js index 736f735a..6dea6a1b 100644 --- a/core/fse.js +++ b/core/fse.js @@ -24,42 +24,42 @@ const _ = require('lodash'); const moment = require('moment'); exports.moduleInfo = { - name : 'Full Screen Editor (FSE)', - desc : 'A full screen editor/viewer', - author : 'NuSkooler', + name : 'Full Screen Editor (FSE)', + desc : 'A full screen editor/viewer', + author : 'NuSkooler', }; const MciViewIds = { - header : { - from : 1, - to : 2, - subject : 3, - errorMsg : 4, - modTimestamp : 5, - msgNum : 6, - msgTotal : 7, + header : { + from : 1, + to : 2, + subject : 3, + errorMsg : 4, + modTimestamp : 5, + msgNum : 6, + msgTotal : 7, - customRangeStart : 10, // 10+ = customs - }, + customRangeStart : 10, // 10+ = customs + }, - body : { - message : 1, - }, + body : { + message : 1, + }, - // :TODO: quote builder MCIs - remove all magic #'s + // :TODO: quote builder MCIs - remove all magic #'s - // :TODO: consolidate all footer MCI's - remove all magic #'s - ViewModeFooter : { - MsgNum : 6, - MsgTotal : 7, - // :TODO: Just use custom ranges - }, + // :TODO: consolidate all footer MCI's - remove all magic #'s + ViewModeFooter : { + MsgNum : 6, + MsgTotal : 7, + // :TODO: Just use custom ranges + }, - quoteBuilder : { - quotedMsg : 1, - // 2 NYI - quoteLines : 3, - } + quoteBuilder : { + quotedMsg : 1, + // 2 NYI + quoteLines : 3, + } }; /* @@ -84,693 +84,693 @@ const MciViewIds = { exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModule extends MessageAreaConfTempSwitcher(MenuModule) { - constructor(options) { - super(options); + constructor(options) { + super(options); - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; - // - // menuConfig.config: - // editorType : email | area - // editorMode : view | edit | quote - // - // menuConfig.config or extraArgs - // messageAreaTag - // messageIndex / messageTotal - // toUserId - // - this.editorType = config.editorType; - this.editorMode = config.editorMode; + // + // menuConfig.config: + // editorType : email | area + // editorMode : view | edit | quote + // + // menuConfig.config or extraArgs + // messageAreaTag + // messageIndex / messageTotal + // toUserId + // + this.editorType = config.editorType; + this.editorMode = config.editorMode; - if(config.messageAreaTag) { - // :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs - this.messageAreaTag = config.messageAreaTag; - } + if(config.messageAreaTag) { + // :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs + this.messageAreaTag = config.messageAreaTag; + } - this.messageIndex = config.messageIndex || 0; - this.messageTotal = config.messageTotal || 0; - this.toUserId = config.toUserId || 0; + this.messageIndex = config.messageIndex || 0; + this.messageTotal = config.messageTotal || 0; + this.toUserId = config.toUserId || 0; - // extraArgs can override some config - if(_.isObject(options.extraArgs)) { - if(options.extraArgs.messageAreaTag) { - this.messageAreaTag = options.extraArgs.messageAreaTag; - } - if(options.extraArgs.messageIndex) { - this.messageIndex = options.extraArgs.messageIndex; - } - if(options.extraArgs.messageTotal) { - this.messageTotal = options.extraArgs.messageTotal; - } - if(options.extraArgs.toUserId) { - this.toUserId = options.extraArgs.toUserId; - } - } + // extraArgs can override some config + if(_.isObject(options.extraArgs)) { + if(options.extraArgs.messageAreaTag) { + this.messageAreaTag = options.extraArgs.messageAreaTag; + } + if(options.extraArgs.messageIndex) { + this.messageIndex = options.extraArgs.messageIndex; + } + if(options.extraArgs.messageTotal) { + this.messageTotal = options.extraArgs.messageTotal; + } + if(options.extraArgs.toUserId) { + this.toUserId = options.extraArgs.toUserId; + } + } - this.noUpdateLastReadId = _.get(options, 'extraArgs.noUpdateLastReadId', config.noUpdateLastReadId) || false; + this.noUpdateLastReadId = _.get(options, 'extraArgs.noUpdateLastReadId', config.noUpdateLastReadId) || false; - this.isReady = false; + this.isReady = false; - if(_.has(options, 'extraArgs.message')) { - this.setMessage(options.extraArgs.message); - } else if(_.has(options, 'extraArgs.replyToMessage')) { - this.replyToMessage = options.extraArgs.replyToMessage; - } + if(_.has(options, 'extraArgs.message')) { + this.setMessage(options.extraArgs.message); + } else if(_.has(options, 'extraArgs.replyToMessage')) { + this.replyToMessage = options.extraArgs.replyToMessage; + } - this.menuMethods = { - // - // Validation stuff - // - viewValidationListener : function(err, cb) { - var errMsgView = self.viewControllers.header.getView(MciViewIds.header.errorMsg); - var newFocusViewId; - if(errMsgView) { - if(err) { - errMsgView.setText(err.message); + this.menuMethods = { + // + // Validation stuff + // + viewValidationListener : function(err, cb) { + var errMsgView = self.viewControllers.header.getView(MciViewIds.header.errorMsg); + var newFocusViewId; + if(errMsgView) { + if(err) { + errMsgView.setText(err.message); - if(MciViewIds.header.subject === err.view.getId()) { - // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) - } - } else { - errMsgView.clearText(); - } - } - cb(newFocusViewId); - }, - headerSubmit : function(formData, extraArgs, cb) { - self.switchToBody(); - return cb(null); - }, - editModeEscPressed : function(formData, extraArgs, cb) { - self.footerMode = 'editor' === self.footerMode ? 'editorMenu' : 'editor'; + if(MciViewIds.header.subject === err.view.getId()) { + // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) + } + } else { + errMsgView.clearText(); + } + } + cb(newFocusViewId); + }, + headerSubmit : function(formData, extraArgs, cb) { + self.switchToBody(); + return cb(null); + }, + editModeEscPressed : function(formData, extraArgs, cb) { + self.footerMode = 'editor' === self.footerMode ? 'editorMenu' : 'editor'; - self.switchFooter(function next(err) { - if(err) { - return cb(err); - } + self.switchFooter(function next(err) { + if(err) { + return cb(err); + } - switch(self.footerMode) { - case 'editor' : - if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { - self.viewControllers.footerEditorMenu.detachClientEvents(); - } - self.viewControllers.body.switchFocus(1); - self.observeEditorEvents(); - break; + switch(self.footerMode) { + case 'editor' : + if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { + self.viewControllers.footerEditorMenu.detachClientEvents(); + } + self.viewControllers.body.switchFocus(1); + self.observeEditorEvents(); + break; - case 'editorMenu' : - self.viewControllers.body.setFocus(false); - self.viewControllers.footerEditorMenu.switchFocus(1); - break; + case 'editorMenu' : + self.viewControllers.body.setFocus(false); + self.viewControllers.footerEditorMenu.switchFocus(1); + break; - default : throw new Error('Unexpected mode'); - } + default : throw new Error('Unexpected mode'); + } - return cb(null); - }); - }, - editModeMenuQuote : function(formData, extraArgs, cb) { - self.viewControllers.footerEditorMenu.setFocus(false); - self.displayQuoteBuilder(); - return cb(null); - }, - appendQuoteEntry: function(formData, extraArgs, cb) { - const quoteMsgView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); + return cb(null); + }); + }, + editModeMenuQuote : function(formData, extraArgs, cb) { + self.viewControllers.footerEditorMenu.setFocus(false); + self.displayQuoteBuilder(); + return cb(null); + }, + appendQuoteEntry: function(formData, extraArgs, cb) { + const quoteMsgView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); - if(self.newQuoteBlock) { - self.newQuoteBlock = false; + if(self.newQuoteBlock) { + self.newQuoteBlock = false; - // :TODO: If replying to ANSI, add a blank sepration line here + // :TODO: If replying to ANSI, add a blank sepration line here - quoteMsgView.addText(self.getQuoteByHeader()); - } + quoteMsgView.addText(self.getQuoteByHeader()); + } - const quoteListView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); - const quoteText = quoteListView.getItem(formData.value.quote); + const quoteListView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); + const quoteText = quoteListView.getItem(formData.value.quote); - quoteMsgView.addText(quoteText); + quoteMsgView.addText(quoteText); - // - // If this is *not* the last item, advance. Otherwise, do nothing as we - // don't want to jump back to the top and repeat already quoted lines - // - - if(quoteListView.getData() !== quoteListView.getCount() - 1) { - quoteListView.focusNext(); - } else { - self.quoteBuilderFinalize(); - } + // + // 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 + // - return cb(null); - }, - quoteBuilderEscPressed : function(formData, extraArgs, cb) { - self.quoteBuilderFinalize(); - return cb(null); - }, - /* + if(quoteListView.getData() !== quoteListView.getCount() - 1) { + quoteListView.focusNext(); + } else { + self.quoteBuilderFinalize(); + } + + return cb(null); + }, + quoteBuilderEscPressed : function(formData, extraArgs, cb) { + self.quoteBuilderFinalize(); + return cb(null); + }, + /* replyDiscard : function(formData, extraArgs) { // :TODO: need to prompt yes/no // :TODO: @method for fallback would be better self.prevMenu(); }, */ - editModeMenuHelp : function(formData, extraArgs, cb) { - self.viewControllers.footerEditorMenu.setFocus(false); - return self.displayHelp(cb); - }, - /////////////////////////////////////////////////////////////////////// - // View Mode - /////////////////////////////////////////////////////////////////////// - viewModeMenuHelp : function(formData, extraArgs, cb) { - self.viewControllers.footerView.setFocus(false); - return self.displayHelp(cb); - } - }; - } - - isEditMode() { - return 'edit' === this.editorMode; - } - - isViewMode() { - return 'view' === this.editorMode; - } - - isPrivateMail() { - return Message.WellKnownAreaTags.Private === this.messageAreaTag; - } - - isReply() { - return !_.isUndefined(this.replyToMessage); - } - - getFooterName() { - return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... - } - - getFormId(name) { - return { - header : 0, - body : 1, - footerEditor : 2, - footerEditorMenu : 3, - footerView : 4, - quoteBuilder : 5, - - help : 50, - }[name]; - } - - getHeaderFormatObj() { - const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A'; - const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A'; - const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat(); - - return { - // :TODO: ensure we show real names for form/to if they are enforced in the area - fromUserName : this.message.fromUserName, - toUserName : this.message.toUserName, - // :TODO: - //fromRealName - //toRealName - fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail), - toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail), - fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail), - toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail), - subject : this.message.subject, - modTimestamp : this.message.modTimestamp.format(modTimestampFormat), - msgNum : this.messageIndex + 1, - msgTotal : this.messageTotal, - messageId : this.message.messageId, - }; - } - - setInitialFooterMode() { - switch(this.editorMode) { - case 'edit' : this.footerMode = 'editor'; break; - case 'view' : this.footerMode = 'view'; break; - } - } - - buildMessage(cb) { - const headerValues = this.viewControllers.header.getFormData().value; - - const msgOpts = { - areaTag : this.messageAreaTag, - toUserName : headerValues.to, - fromUserName : this.client.user.username, - subject : headerValues.subject, - // :TODO: don't hard code 1 here: - message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), - }; - - if(this.isReply()) { - 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' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; - msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; - } - } - - this.message = new Message(msgOpts); - - return cb(null); - } - - updateLastReadId(cb) { - if(this.noUpdateLastReadId) { - return cb(null); - } - - return updateMessageAreaLastReadId( - this.client.user.userId, this.messageAreaTag, this.message.messageId, cb - ); - } - - setMessage(message) { - this.message = message; - - this.updateLastReadId( () => { - if(this.isReady) { - this.initHeaderViewMode(); - this.initFooterViewMode(); - - const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); - let msg = this.message.message; - - if(bodyMessageView && _.has(this, 'message.message')) { - // - // We handle ANSI messages differently than standard messages -- this is required as - // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted - // how the author wanted it - // - 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)); - } - } - } - }); - } - - getMessage(cb) { - const self = this; - - async.series( - [ - function buildIfNecessary(callback) { - if(self.isEditMode()) { - return self.buildMessage(callback); // creates initial self.message - } - - return callback(null); - }, - function populateLocalUserInfo(callback) { - self.message.setLocalFromUserId(self.client.user.userId); - - if(!self.isPrivateMail()) { - return callback(null); - } - - if(self.toUserId > 0) { - self.message.setLocalToUserId(self.toUserId); - return callback(null); - } - - // - // If the message we're replying to is from a remote user - // don't try to look up the local user ID. Instead, mark the mail - // for export with the remote to address. - // - if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) { - self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]); - self.message.setExternalFlavor(self.replyToMessage.meta.System[Message.SystemMetaNames.ExternalFlavor]); - return callback(null); - } - - // - // Detect if the user is attempting to send to a remote mail type that we support - // - // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such - const addressedToInfo = getAddressedToInfo(self.message.toUserName); - if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) { - self.message.setRemoteToUser(addressedToInfo.remote); - self.message.setExternalFlavor(addressedToInfo.flavor); - self.message.toUserName = addressedToInfo.name; - return callback(null); - } - - // we need to look it up - User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => { - if(err) { - return callback(err); - } - - self.message.setLocalToUserId(toUserId); - return callback(null); - }); - } - ], - err => { - return cb(err, self.message); - } - ); - } - - updateUserStats(cb) { - if(Message.isPrivateAreaTag(this.message.areaTag)) { - if(cb) { - cb(null); - } - return; // don't inc stats for private messages - } - - return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); - } - - redrawFooter(options, cb) { - const self = this; - - async.waterfall( - [ - function moveToFooterPosition(callback) { - // - // Calculate footer starting position - // - // row = (header height + body height) - // - var footerRow = self.header.height + self.body.height; - self.client.term.rawWrite(ansi.goto(footerRow, 1)); - callback(null); - }, - function clearFooterArea(callback) { - if(options.clear) { - // footer up to 3 rows in height - - // :TODO: We'd like to delete up to N rows, but this does not work - // in NetRunner: - self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); - - self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)); - } - callback(null); - }, - function displayFooterArt(callback) { - const footerArt = self.menuConfig.config.art[options.footerName]; - - theme.displayThemedAsset( - footerArt, - self.client, - { font : self.menuConfig.font }, - function displayed(err, artData) { - callback(err, artData); - } - ); - } - ], - function complete(err, artData) { - cb(err, artData); - } - ); - } - - redrawScreen(cb) { - var comps = [ 'header', 'body' ]; - const self = this; - var art = self.menuConfig.config.art; - - self.client.term.rawWrite(ansi.resetScreen()); - - async.series( - [ - function displayHeaderAndBody(callback) { - async.eachSeries( comps, function dispArt(n, next) { - theme.displayThemedAsset( - art[n], - self.client, - { font : self.menuConfig.font, acsCondMember : 'art' }, - function displayed(err) { - next(err); - } - ); - }, function complete(err) { - //self.body.height = self.client.term.termHeight - self.header.height - 1; - callback(err); - }); - }, - function displayFooter(callback) { - // we have to treat the footer special - self.redrawFooter( { clear : false, footerName : self.getFooterName() }, function footerDisplayed(err) { - callback(err); - }); - }, - function refreshViews(callback) { - comps.push(self.getFooterName()); - - comps.forEach(function artComp(n) { - self.viewControllers[n].redrawAll(); - }); - - callback(null); - } - ], - function complete(err) { - cb(err); - } - ); - } - - switchFooter(cb) { - var footerName = this.getFooterName(); - - this.redrawFooter( { footerName : footerName, clear : true }, (err, artData) => { - if(err) { - cb(err); - return; - } - - var formId = this.getFormId(footerName); - - if(_.isUndefined(this.viewControllers[footerName])) { - var menuLoadOpts = { - callingMenu : this, - formId : formId, - mciMap : artData.mciMap - }; - - this.addViewController( - footerName, - new ViewController( { client : this.client, formId : formId } ) - ).loadFromMenuConfig(menuLoadOpts, err => { - cb(err); - }); - } else { - this.viewControllers[footerName].redrawAll(); - cb(null); - } - }); - } - - initSequence() { - var mciData = { }; - const self = this; - var art = self.menuConfig.config.art; - - assert(_.isObject(art)); - - async.series( - [ - function beforeDisplayArt(callback) { - self.beforeArt(callback); - }, - function displayHeaderAndBodyArt(callback) { - async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) { - theme.displayThemedAsset( - art[n], - self.client, - { font : self.menuConfig.font, acsCondMember : 'art' }, - function displayed(err, artData) { - if(artData) { - mciData[n] = artData; - self[n] = { height : artData.height }; - } - - next(err); - } - ); - }, function complete(err) { - callback(err); - }); - }, - function displayFooter(callback) { - self.setInitialFooterMode(); - - var footerName = self.getFooterName(); - - self.redrawFooter( { footerName : footerName }, function artDisplayed(err, artData) { - mciData[footerName] = artData; - callback(err); - }); - }, - function afterArtDisplayed(callback) { - self.mciReady(mciData, callback); - } - ], - function complete(err) { - if(err) { - self.client.log.warn( { error : err.message }, 'FSE init error'); - } else { - self.isReady = true; - self.finishedLoading(); - } - } - ); - } - - createInitialViews(mciData, cb) { - const self = this; - var menuLoadOpts = { callingMenu : self }; - - async.series( - [ - function header(callback) { - menuLoadOpts.formId = self.getFormId('header'); - menuLoadOpts.mciMap = mciData.header.mciMap; - - self.addViewController( - 'header', - new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) - ).loadFromMenuConfig(menuLoadOpts, function headerReady(err) { - callback(err); - }); - }, - function body(callback) { - menuLoadOpts.formId = self.getFormId('body'); - menuLoadOpts.mciMap = mciData.body.mciMap; - - self.addViewController( - 'body', - new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) - ).loadFromMenuConfig(menuLoadOpts, function bodyReady(err) { - callback(err); - }); - }, - function footer(callback) { - var footerName = self.getFooterName(); - - menuLoadOpts.formId = self.getFormId(footerName); - menuLoadOpts.mciMap = mciData[footerName].mciMap; - - self.addViewController( - footerName, - new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) - ).loadFromMenuConfig(menuLoadOpts, function footerReady(err) { - callback(err); - }); - }, - function prepareViewStates(callback) { - var header = self.viewControllers.header; - var from = header.getView(MciViewIds.header.from); - from.acceptsFocus = false; - //from.setText(self.client.user.username); - - // :TODO: make this a method - var body = self.viewControllers.body.getView(MciViewIds.body.message); - self.updateTextEditMode(body.getTextEditMode()); - self.updateEditModePosition(body.getEditPosition()); - - // :TODO: If view mode, set body to read only... which needs an impl... - - callback(null); - }, - function setInitialData(callback) { - - switch(self.editorMode) { - case 'view' : - if(self.message) { - self.initHeaderViewMode(); - self.initFooterViewMode(); - - var bodyMessageView = self.viewControllers.body.getView(MciViewIds.body.message); - if(bodyMessageView && _.has(self, 'message.message')) { - //self.setBodyMessageViewText(); - bodyMessageView.setText(cleanControlCodes(self.message.message)); - } - } - break; - - case 'edit' : - { - const fromView = self.viewControllers.header.getView(MciViewIds.header.from); - const area = getMessageAreaByTag(self.messageAreaTag); - if(area && area.realNames) { - fromView.setText(self.client.user.properties.real_name || self.client.user.username); - } else { - fromView.setText(self.client.user.username); - } - - if(self.replyToMessage) { - self.initHeaderReplyEditMode(); - } - } - break; - } - - callback(null); - }, - function setInitialFocus(callback) { - - switch(self.editorMode) { - case 'edit' : - self.switchToHeader(); - break; - - case 'view' : - self.switchToFooter(); - //self.observeViewPosition(); - break; - } - - callback(null); - } - ], - function complete(err) { - return cb(err); - } - ); - } - - mciReadyHandler(mciData, cb) { - - this.createInitialViews(mciData, err => { - // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in - // place - if this is for existing usernames else validate spec - - /* + editModeMenuHelp : function(formData, extraArgs, cb) { + self.viewControllers.footerEditorMenu.setFocus(false); + return self.displayHelp(cb); + }, + /////////////////////////////////////////////////////////////////////// + // View Mode + /////////////////////////////////////////////////////////////////////// + viewModeMenuHelp : function(formData, extraArgs, cb) { + self.viewControllers.footerView.setFocus(false); + return self.displayHelp(cb); + } + }; + } + + isEditMode() { + return 'edit' === this.editorMode; + } + + isViewMode() { + return 'view' === this.editorMode; + } + + isPrivateMail() { + return Message.WellKnownAreaTags.Private === this.messageAreaTag; + } + + isReply() { + return !_.isUndefined(this.replyToMessage); + } + + getFooterName() { + return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... + } + + getFormId(name) { + return { + header : 0, + body : 1, + footerEditor : 2, + footerEditorMenu : 3, + footerView : 4, + quoteBuilder : 5, + + help : 50, + }[name]; + } + + getHeaderFormatObj() { + const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A'; + const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A'; + const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat(); + + return { + // :TODO: ensure we show real names for form/to if they are enforced in the area + fromUserName : this.message.fromUserName, + toUserName : this.message.toUserName, + // :TODO: + //fromRealName + //toRealName + fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail), + toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail), + fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail), + toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail), + subject : this.message.subject, + modTimestamp : this.message.modTimestamp.format(modTimestampFormat), + msgNum : this.messageIndex + 1, + msgTotal : this.messageTotal, + messageId : this.message.messageId, + }; + } + + setInitialFooterMode() { + switch(this.editorMode) { + case 'edit' : this.footerMode = 'editor'; break; + case 'view' : this.footerMode = 'view'; break; + } + } + + buildMessage(cb) { + const headerValues = this.viewControllers.header.getFormData().value; + + const msgOpts = { + areaTag : this.messageAreaTag, + toUserName : headerValues.to, + fromUserName : this.client.user.username, + subject : headerValues.subject, + // :TODO: don't hard code 1 here: + message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), + }; + + if(this.isReply()) { + 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' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; + msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; + } + } + + this.message = new Message(msgOpts); + + return cb(null); + } + + updateLastReadId(cb) { + if(this.noUpdateLastReadId) { + return cb(null); + } + + return updateMessageAreaLastReadId( + this.client.user.userId, this.messageAreaTag, this.message.messageId, cb + ); + } + + setMessage(message) { + this.message = message; + + this.updateLastReadId( () => { + if(this.isReady) { + this.initHeaderViewMode(); + this.initFooterViewMode(); + + const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); + let msg = this.message.message; + + if(bodyMessageView && _.has(this, 'message.message')) { + // + // We handle ANSI messages differently than standard messages -- this is required as + // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted + // how the author wanted it + // + 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)); + } + } + } + }); + } + + getMessage(cb) { + const self = this; + + async.series( + [ + function buildIfNecessary(callback) { + if(self.isEditMode()) { + return self.buildMessage(callback); // creates initial self.message + } + + return callback(null); + }, + function populateLocalUserInfo(callback) { + self.message.setLocalFromUserId(self.client.user.userId); + + if(!self.isPrivateMail()) { + return callback(null); + } + + if(self.toUserId > 0) { + self.message.setLocalToUserId(self.toUserId); + return callback(null); + } + + // + // If the message we're replying to is from a remote user + // don't try to look up the local user ID. Instead, mark the mail + // for export with the remote to address. + // + if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) { + self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]); + self.message.setExternalFlavor(self.replyToMessage.meta.System[Message.SystemMetaNames.ExternalFlavor]); + return callback(null); + } + + // + // Detect if the user is attempting to send to a remote mail type that we support + // + // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such + const addressedToInfo = getAddressedToInfo(self.message.toUserName); + if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) { + self.message.setRemoteToUser(addressedToInfo.remote); + self.message.setExternalFlavor(addressedToInfo.flavor); + self.message.toUserName = addressedToInfo.name; + return callback(null); + } + + // we need to look it up + User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => { + if(err) { + return callback(err); + } + + self.message.setLocalToUserId(toUserId); + return callback(null); + }); + } + ], + err => { + return cb(err, self.message); + } + ); + } + + updateUserStats(cb) { + if(Message.isPrivateAreaTag(this.message.areaTag)) { + if(cb) { + cb(null); + } + return; // don't inc stats for private messages + } + + return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); + } + + redrawFooter(options, cb) { + const self = this; + + async.waterfall( + [ + function moveToFooterPosition(callback) { + // + // Calculate footer starting position + // + // row = (header height + body height) + // + var footerRow = self.header.height + self.body.height; + self.client.term.rawWrite(ansi.goto(footerRow, 1)); + callback(null); + }, + function clearFooterArea(callback) { + if(options.clear) { + // footer up to 3 rows in height + + // :TODO: We'd like to delete up to N rows, but this does not work + // in NetRunner: + self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); + + self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)); + } + callback(null); + }, + function displayFooterArt(callback) { + const footerArt = self.menuConfig.config.art[options.footerName]; + + theme.displayThemedAsset( + footerArt, + self.client, + { font : self.menuConfig.font }, + function displayed(err, artData) { + callback(err, artData); + } + ); + } + ], + function complete(err, artData) { + cb(err, artData); + } + ); + } + + redrawScreen(cb) { + var comps = [ 'header', 'body' ]; + const self = this; + var art = self.menuConfig.config.art; + + self.client.term.rawWrite(ansi.resetScreen()); + + async.series( + [ + function displayHeaderAndBody(callback) { + async.eachSeries( comps, function dispArt(n, next) { + theme.displayThemedAsset( + art[n], + self.client, + { font : self.menuConfig.font, acsCondMember : 'art' }, + function displayed(err) { + next(err); + } + ); + }, function complete(err) { + //self.body.height = self.client.term.termHeight - self.header.height - 1; + callback(err); + }); + }, + function displayFooter(callback) { + // we have to treat the footer special + self.redrawFooter( { clear : false, footerName : self.getFooterName() }, function footerDisplayed(err) { + callback(err); + }); + }, + function refreshViews(callback) { + comps.push(self.getFooterName()); + + comps.forEach(function artComp(n) { + self.viewControllers[n].redrawAll(); + }); + + callback(null); + } + ], + function complete(err) { + cb(err); + } + ); + } + + switchFooter(cb) { + var footerName = this.getFooterName(); + + this.redrawFooter( { footerName : footerName, clear : true }, (err, artData) => { + if(err) { + cb(err); + return; + } + + var formId = this.getFormId(footerName); + + if(_.isUndefined(this.viewControllers[footerName])) { + var menuLoadOpts = { + callingMenu : this, + formId : formId, + mciMap : artData.mciMap + }; + + this.addViewController( + footerName, + new ViewController( { client : this.client, formId : formId } ) + ).loadFromMenuConfig(menuLoadOpts, err => { + cb(err); + }); + } else { + this.viewControllers[footerName].redrawAll(); + cb(null); + } + }); + } + + initSequence() { + var mciData = { }; + const self = this; + var art = self.menuConfig.config.art; + + assert(_.isObject(art)); + + async.series( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function displayHeaderAndBodyArt(callback) { + async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) { + theme.displayThemedAsset( + art[n], + self.client, + { font : self.menuConfig.font, acsCondMember : 'art' }, + function displayed(err, artData) { + if(artData) { + mciData[n] = artData; + self[n] = { height : artData.height }; + } + + next(err); + } + ); + }, function complete(err) { + callback(err); + }); + }, + function displayFooter(callback) { + self.setInitialFooterMode(); + + var footerName = self.getFooterName(); + + self.redrawFooter( { footerName : footerName }, function artDisplayed(err, artData) { + mciData[footerName] = artData; + callback(err); + }); + }, + function afterArtDisplayed(callback) { + self.mciReady(mciData, callback); + } + ], + function complete(err) { + if(err) { + self.client.log.warn( { error : err.message }, 'FSE init error'); + } else { + self.isReady = true; + self.finishedLoading(); + } + } + ); + } + + createInitialViews(mciData, cb) { + const self = this; + var menuLoadOpts = { callingMenu : self }; + + async.series( + [ + function header(callback) { + menuLoadOpts.formId = self.getFormId('header'); + menuLoadOpts.mciMap = mciData.header.mciMap; + + self.addViewController( + 'header', + new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) + ).loadFromMenuConfig(menuLoadOpts, function headerReady(err) { + callback(err); + }); + }, + function body(callback) { + menuLoadOpts.formId = self.getFormId('body'); + menuLoadOpts.mciMap = mciData.body.mciMap; + + self.addViewController( + 'body', + new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) + ).loadFromMenuConfig(menuLoadOpts, function bodyReady(err) { + callback(err); + }); + }, + function footer(callback) { + var footerName = self.getFooterName(); + + menuLoadOpts.formId = self.getFormId(footerName); + menuLoadOpts.mciMap = mciData[footerName].mciMap; + + self.addViewController( + footerName, + new ViewController( { client : self.client, formId : menuLoadOpts.formId } ) + ).loadFromMenuConfig(menuLoadOpts, function footerReady(err) { + callback(err); + }); + }, + function prepareViewStates(callback) { + var header = self.viewControllers.header; + var from = header.getView(MciViewIds.header.from); + from.acceptsFocus = false; + //from.setText(self.client.user.username); + + // :TODO: make this a method + var body = self.viewControllers.body.getView(MciViewIds.body.message); + self.updateTextEditMode(body.getTextEditMode()); + self.updateEditModePosition(body.getEditPosition()); + + // :TODO: If view mode, set body to read only... which needs an impl... + + callback(null); + }, + function setInitialData(callback) { + + switch(self.editorMode) { + case 'view' : + if(self.message) { + self.initHeaderViewMode(); + self.initFooterViewMode(); + + var bodyMessageView = self.viewControllers.body.getView(MciViewIds.body.message); + if(bodyMessageView && _.has(self, 'message.message')) { + //self.setBodyMessageViewText(); + bodyMessageView.setText(cleanControlCodes(self.message.message)); + } + } + break; + + case 'edit' : + { + const fromView = self.viewControllers.header.getView(MciViewIds.header.from); + const area = getMessageAreaByTag(self.messageAreaTag); + if(area && area.realNames) { + fromView.setText(self.client.user.properties.real_name || self.client.user.username); + } else { + fromView.setText(self.client.user.username); + } + + if(self.replyToMessage) { + self.initHeaderReplyEditMode(); + } + } + break; + } + + callback(null); + }, + function setInitialFocus(callback) { + + switch(self.editorMode) { + case 'edit' : + self.switchToHeader(); + break; + + case 'view' : + self.switchToFooter(); + //self.observeViewPosition(); + break; + } + + callback(null); + } + ], + function complete(err) { + return cb(err); + } + ); + } + + mciReadyHandler(mciData, cb) { + + this.createInitialViews(mciData, err => { + // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in + // place - if this is for existing usernames else validate spec + + /* self.viewControllers.header.on('leave', function headerViewLeave(view) { if(2 === view.id) { // "to" field @@ -784,181 +784,181 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } });*/ - cb(err); - }); - } + cb(err); + }); + } - updateEditModePosition(pos) { - if(this.isEditMode()) { - var posView = this.viewControllers.footerEditor.getView(1); - if(posView) { - this.client.term.rawWrite(ansi.savePos()); - // :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat - posView.setText(_.padStart(String(pos.row + 1), 2, '0') + ',' + _.padEnd(String(pos.col + 1), 2, '0')); - this.client.term.rawWrite(ansi.restorePos()); - } - } - } + updateEditModePosition(pos) { + if(this.isEditMode()) { + var posView = this.viewControllers.footerEditor.getView(1); + if(posView) { + this.client.term.rawWrite(ansi.savePos()); + // :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat + posView.setText(_.padStart(String(pos.row + 1), 2, '0') + ',' + _.padEnd(String(pos.col + 1), 2, '0')); + this.client.term.rawWrite(ansi.restorePos()); + } + } + } - updateTextEditMode(mode) { - if(this.isEditMode()) { - var modeView = this.viewControllers.footerEditor.getView(2); - if(modeView) { - this.client.term.rawWrite(ansi.savePos()); - modeView.setText('insert' === mode ? 'INS' : 'OVR'); - this.client.term.rawWrite(ansi.restorePos()); - } - } - } + updateTextEditMode(mode) { + if(this.isEditMode()) { + var modeView = this.viewControllers.footerEditor.getView(2); + if(modeView) { + this.client.term.rawWrite(ansi.savePos()); + modeView.setText('insert' === mode ? 'INS' : 'OVR'); + this.client.term.rawWrite(ansi.restorePos()); + } + } + } - setHeaderText(id, text) { - this.setViewText('header', id, text); - } + setHeaderText(id, text) { + this.setViewText('header', id, text); + } - initHeaderViewMode() { - this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); - this.setHeaderText(MciViewIds.header.to, this.message.toUserName); - this.setHeaderText(MciViewIds.header.subject, this.message.subject); - this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); - this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); - this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); + initHeaderViewMode() { + this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + this.setHeaderText(MciViewIds.header.to, this.message.toUserName); + this.setHeaderText(MciViewIds.header.subject, this.message.subject); + this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); + this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); + this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); - this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); + this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); - // if we changed conf/area we need to update any related standard MCI view - this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] ); - } + // if we changed conf/area we need to update any related standard MCI view + this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] ); + } - initHeaderReplyEditMode() { - assert(_.isObject(this.replyToMessage)); + initHeaderReplyEditMode() { + assert(_.isObject(this.replyToMessage)); - this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName); + this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName); - // - // We want to prefix the subject with "RE: " only if it's not already - // that way -- avoid RE: RE: RE: RE: ... - // - let newSubj = this.replyToMessage.subject; - if(false === /^RE:\s+/i.test(newSubj)) { - newSubj = `RE: ${newSubj}`; - } + // + // We want to prefix the subject with "RE: " only if it's not already + // that way -- avoid RE: RE: RE: RE: ... + // + let newSubj = this.replyToMessage.subject; + if(false === /^RE:\s+/i.test(newSubj)) { + newSubj = `RE: ${newSubj}`; + } - this.setHeaderText(MciViewIds.header.subject, newSubj); - } + this.setHeaderText(MciViewIds.header.subject, newSubj); + } - initFooterViewMode() { - this.setViewText('footerView', MciViewIds.ViewModeFooter.msgNum, (this.messageIndex + 1).toString() ); - this.setViewText('footerView', MciViewIds.ViewModeFooter.msgTotal, this.messageTotal.toString() ); - } + initFooterViewMode() { + this.setViewText('footerView', MciViewIds.ViewModeFooter.msgNum, (this.messageIndex + 1).toString() ); + this.setViewText('footerView', MciViewIds.ViewModeFooter.msgTotal, this.messageTotal.toString() ); + } - displayHelp(cb) { - this.client.term.rawWrite(ansi.resetScreen()); + displayHelp(cb) { + this.client.term.rawWrite(ansi.resetScreen()); - theme.displayThemeArt( - { name : this.menuConfig.config.art.help, client : this.client }, - () => { - this.client.waitForKeyPress( () => { - this.redrawScreen( () => { - this.viewControllers[this.getFooterName()].setFocus(true); - return cb(null); - }); - }); - } - ); - } + theme.displayThemeArt( + { name : this.menuConfig.config.art.help, client : this.client }, + () => { + this.client.waitForKeyPress( () => { + this.redrawScreen( () => { + this.viewControllers[this.getFooterName()].setFocus(true); + return cb(null); + }); + }); + } + ); + } - displayQuoteBuilder() { - // - // Clear body area - // - this.newQuoteBlock = true; - const self = this; + displayQuoteBuilder() { + // + // Clear body area + // + this.newQuoteBlock = true; + const self = this; - async.waterfall( - [ - function clearAndDisplayArt(callback) { - // :TODO: NetRunner does NOT support delete line, so this does not work: - self.client.term.rawWrite( - ansi.goto(self.header.height + 1, 1) + + async.waterfall( + [ + function clearAndDisplayArt(callback) { + // :TODO: NetRunner does NOT support delete line, so this does not work: + self.client.term.rawWrite( + ansi.goto(self.header.height + 1, 1) + ansi.deleteLine((self.client.term.termHeight - self.header.height) - 1)); - theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) { - callback(err, artData); - }); - }, - function createViewsIfNecessary(artData, callback) { - var formId = self.getFormId('quoteBuilder'); + theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) { + callback(err, artData); + }); + }, + function createViewsIfNecessary(artData, callback) { + var formId = self.getFormId('quoteBuilder'); - if(_.isUndefined(self.viewControllers.quoteBuilder)) { - var menuLoadOpts = { - callingMenu : self, - formId : formId, - mciMap : artData.mciMap, - }; + if(_.isUndefined(self.viewControllers.quoteBuilder)) { + var menuLoadOpts = { + callingMenu : self, + formId : formId, + mciMap : artData.mciMap, + }; - self.addViewController( - 'quoteBuilder', - new ViewController( { client : self.client, formId : formId } ) - ).loadFromMenuConfig(menuLoadOpts, function quoteViewsReady(err) { - callback(err); - }); - } else { - self.viewControllers.quoteBuilder.redrawAll(); - callback(null); - } - }, - function loadQuoteLines(callback) { - const quoteView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); - const bodyView = self.viewControllers.body.getView(MciViewIds.body.message); + self.addViewController( + 'quoteBuilder', + new ViewController( { client : self.client, formId : formId } ) + ).loadFromMenuConfig(menuLoadOpts, function quoteViewsReady(err) { + callback(err); + }); + } else { + self.viewControllers.quoteBuilder.redrawAll(); + callback(null); + } + }, + function loadQuoteLines(callback) { + const quoteView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); + const bodyView = self.viewControllers.body.getView(MciViewIds.body.message); - self.replyToMessage.getQuoteLines( - { - termWidth : self.client.term.termWidth, - termHeight : self.client.term.termHeight, - cols : quoteView.dimens.width, - startCol : quoteView.position.col, - ansiResetSgr : bodyView.styleSGR1, - ansiFocusPrefixSgr : quoteView.styleSGR2, - }, - (err, quoteLines, focusQuoteLines, replyIsAnsi) => { - if(err) { - return callback(err); - } + 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; + self.replyIsAnsi = replyIsAnsi; - quoteView.setItems(quoteLines); - quoteView.setFocusItems(focusQuoteLines); + quoteView.setItems(quoteLines); + quoteView.setFocusItems(focusQuoteLines); - self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg).setFocus(false); - self.viewControllers.quoteBuilder.switchFocus(MciViewIds.quoteBuilder.quoteLines); + self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg).setFocus(false); + self.viewControllers.quoteBuilder.switchFocus(MciViewIds.quoteBuilder.quoteLines); - return callback(null); - } - ); - }, - ], - function complete(err) { - if(err) { - self.client.log.warn( { error : err.message }, 'Error displaying quote builder'); - } - } - ); - } + return callback(null); + } + ); + }, + ], + function complete(err) { + if(err) { + self.client.log.warn( { error : err.message }, 'Error displaying quote builder'); + } + } + ); + } - observeEditorEvents() { - const bodyView = this.viewControllers.body.getView(MciViewIds.body.message); + observeEditorEvents() { + const bodyView = this.viewControllers.body.getView(MciViewIds.body.message); - bodyView.on('edit position', pos => { - this.updateEditModePosition(pos); - }); + bodyView.on('edit position', pos => { + this.updateEditModePosition(pos); + }); - bodyView.on('text edit mode', mode => { - this.updateTextEditMode(mode); - }); - } + bodyView.on('text edit mode', mode => { + this.updateTextEditMode(mode); + }); + } - /* + /* this.observeViewPosition = function() { self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) { console.log(pos.percent + ' / ' + pos.below) @@ -966,93 +966,93 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }; */ - switchToHeader() { - this.viewControllers.body.setFocus(false); - this.viewControllers.header.switchFocus(2); // to - } + switchToHeader() { + this.viewControllers.body.setFocus(false); + this.viewControllers.header.switchFocus(2); // to + } - switchToBody() { - this.viewControllers.header.setFocus(false); - this.viewControllers.body.switchFocus(1); + switchToBody() { + this.viewControllers.header.setFocus(false); + this.viewControllers.body.switchFocus(1); - this.observeEditorEvents(); - } + this.observeEditorEvents(); + } - switchToFooter() { - this.viewControllers.header.setFocus(false); - this.viewControllers.body.setFocus(false); + switchToFooter() { + this.viewControllers.header.setFocus(false); + this.viewControllers.body.setFocus(false); - this.viewControllers[this.getFooterName()].switchFocus(1); // HM1 - } + this.viewControllers[this.getFooterName()].switchFocus(1); // HM1 + } - switchFromQuoteBuilderToBody() { - this.viewControllers.quoteBuilder.setFocus(false); - var body = this.viewControllers.body.getView(MciViewIds.body.message); - body.redraw(); - this.viewControllers.body.switchFocus(1); + switchFromQuoteBuilderToBody() { + this.viewControllers.quoteBuilder.setFocus(false); + var body = this.viewControllers.body.getView(MciViewIds.body.message); + body.redraw(); + this.viewControllers.body.switchFocus(1); - // :TODO: create method (DRY) + // :TODO: create method (DRY) - this.updateTextEditMode(body.getTextEditMode()); - this.updateEditModePosition(body.getEditPosition()); + this.updateTextEditMode(body.getTextEditMode()); + this.updateEditModePosition(body.getEditPosition()); - this.observeEditorEvents(); - } + this.observeEditorEvents(); + } - quoteBuilderFinalize() { - // :TODO: fix magic #'s - const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); - const msgView = this.viewControllers.body.getView(MciViewIds.body.message); + quoteBuilderFinalize() { + // :TODO: fix magic #'s + const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); + const msgView = this.viewControllers.body.getView(MciViewIds.body.message); - let quoteLines = quoteMsgView.getData().trim(); + let quoteLines = quoteMsgView.getData().trim(); - if(quoteLines.length > 0) { - if(this.replyIsAnsi) { - const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); - quoteLines += `${ansi.normal()}${bodyMessageView.getSGRFor('text')}`; - } - msgView.addText(`${quoteLines}\n\n`); - } + if(quoteLines.length > 0) { + if(this.replyIsAnsi) { + const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); + quoteLines += `${ansi.normal()}${bodyMessageView.getSGRFor('text')}`; + } + msgView.addText(`${quoteLines}\n\n`); + } - quoteMsgView.setText(''); + quoteMsgView.setText(''); - this.footerMode = 'editor'; + this.footerMode = 'editor'; - this.switchFooter( () => { - this.switchFromQuoteBuilderToBody(); - }); - } + this.switchFooter( () => { + this.switchFromQuoteBuilderToBody(); + }); + } - getQuoteByHeader() { - let quoteFormat = this.menuConfig.config.quoteFormats; + getQuoteByHeader() { + let quoteFormat = this.menuConfig.config.quoteFormats; - if(Array.isArray(quoteFormat)) { - quoteFormat = quoteFormat[ Math.floor(Math.random() * quoteFormat.length) ]; - } else if(!_.isString(quoteFormat)) { - quoteFormat = 'On {dateTime} {userName} said...'; - } + if(Array.isArray(quoteFormat)) { + quoteFormat = quoteFormat[ Math.floor(Math.random() * quoteFormat.length) ]; + } else if(!_.isString(quoteFormat)) { + quoteFormat = 'On {dateTime} {userName} said...'; + } - const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); - return stringFormat(quoteFormat, { - dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), - userName : this.replyToMessage.fromUserName, - }); - } + const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); + return stringFormat(quoteFormat, { + dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), + userName : this.replyToMessage.fromUserName, + }); + } - enter() { - if(this.messageAreaTag) { - this.tempMessageConfAndAreaSwitch(this.messageAreaTag); - } + enter() { + if(this.messageAreaTag) { + this.tempMessageConfAndAreaSwitch(this.messageAreaTag); + } - super.enter(); - } + super.enter(); + } - leave() { - this.tempMessageConfAndAreaRestore(); - super.leave(); - } + leave() { + this.tempMessageConfAndAreaRestore(); + super.leave(); + } - mciReady(mciData, cb) { - return this.mciReadyHandler(mciData, cb); - } + mciReady(mciData, cb) { + return this.mciReadyHandler(mciData, cb); + } }; diff --git a/core/ftn_address.js b/core/ftn_address.js index 6b1e57e0..92c37557 100644 --- a/core/ftn_address.js +++ b/core/ftn_address.js @@ -7,94 +7,94 @@ const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\- const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; module.exports = class Address { - constructor(addr) { - if(addr) { - if(_.isObject(addr)) { - Object.assign(this, addr); - } else if(_.isString(addr)) { - const temp = Address.fromString(addr); - if(temp) { - Object.assign(this, temp); - } - } - } - } + constructor(addr) { + if(addr) { + if(_.isObject(addr)) { + Object.assign(this, addr); + } else if(_.isString(addr)) { + const temp = Address.fromString(addr); + if(temp) { + Object.assign(this, temp); + } + } + } + } - static isValidAddress(addr) { - return addr && addr.isValid(); - } + static isValidAddress(addr) { + return addr && addr.isValid(); + } - isValid() { - // FTN address is valid if we have at least a net/node - return _.isNumber(this.net) && _.isNumber(this.node); - } + isValid() { + // FTN address is valid if we have at least a net/node + return _.isNumber(this.net) && _.isNumber(this.node); + } - isEqual(other) { - if(_.isString(other)) { - other = Address.fromString(other); - } + isEqual(other) { + if(_.isString(other)) { + other = Address.fromString(other); + } - return ( - this.net === other.net && + return ( + this.net === other.net && this.node === other.node && this.zone === other.zone && this.point === other.point && this.domain === other.domain - ); - } + ); + } - getMatchAddr(pattern) { - const m = FTN_PATTERN_REGEXP.exec(pattern); - if(m) { - let addr = { }; + getMatchAddr(pattern) { + const m = FTN_PATTERN_REGEXP.exec(pattern); + if(m) { + let addr = { }; - if(m[1]) { - addr.zone = m[1].slice(0, -1); - if('*' !== addr.zone) { - addr.zone = parseInt(addr.zone); - } - } else { - addr.zone = '*'; - } + if(m[1]) { + addr.zone = m[1].slice(0, -1); + if('*' !== addr.zone) { + addr.zone = parseInt(addr.zone); + } + } else { + addr.zone = '*'; + } - if(m[2]) { - addr.net = m[2]; - if('*' !== addr.net) { - addr.net = parseInt(addr.net); - } - } else { - addr.net = '*'; - } + if(m[2]) { + addr.net = m[2]; + if('*' !== addr.net) { + addr.net = parseInt(addr.net); + } + } else { + addr.net = '*'; + } - if(m[3]) { - addr.node = m[3].substr(1); - if('*' !== addr.node) { - addr.node = parseInt(addr.node); - } - } else { - addr.node = '*'; - } + if(m[3]) { + addr.node = m[3].substr(1); + if('*' !== addr.node) { + addr.node = parseInt(addr.node); + } + } else { + addr.node = '*'; + } - if(m[4]) { - addr.point = m[4].substr(1); - if('*' !== addr.point) { - addr.point = parseInt(addr.point); - } - } else { - addr.point = '*'; - } + if(m[4]) { + addr.point = m[4].substr(1); + if('*' !== addr.point) { + addr.point = parseInt(addr.point); + } + } else { + addr.point = '*'; + } - if(m[5]) { - addr.domain = m[5].substr(1); - } else { - addr.domain = '*'; - } + if(m[5]) { + addr.domain = m[5].substr(1); + } else { + addr.domain = '*'; + } - return addr; - } - } + return addr; + } + } - /* + /* getMatchScore(pattern) { let score = 0; const addr = this.getMatchAddr(pattern); @@ -116,92 +116,92 @@ module.exports = class Address { } */ - isPatternMatch(pattern) { - const addr = this.getMatchAddr(pattern); - if(addr) { - return ( - ('*' === addr.net || this.net === addr.net) && + isPatternMatch(pattern) { + const addr = this.getMatchAddr(pattern); + if(addr) { + return ( + ('*' === addr.net || this.net === addr.net) && ('*' === addr.node || this.node === addr.node) && ('*' === addr.zone || this.zone === addr.zone) && ('*' === addr.point || this.point === addr.point) && ('*' === addr.domain || this.domain === addr.domain) - ); - } + ); + } - return false; - } + return false; + } - static fromString(addrStr) { - const m = FTN_ADDRESS_REGEXP.exec(addrStr); + static fromString(addrStr) { + const m = FTN_ADDRESS_REGEXP.exec(addrStr); - if(m) { - // start with a 2D - let addr = { - net : parseInt(m[2]), - node : parseInt(m[3].substr(1)), - }; + if(m) { + // start with a 2D + let addr = { + net : parseInt(m[2]), + node : parseInt(m[3].substr(1)), + }; - // 3D: Addition of zone if present - if(m[1]) { - addr.zone = parseInt(m[1].slice(0, -1)); - } + // 3D: Addition of zone if present + if(m[1]) { + addr.zone = parseInt(m[1].slice(0, -1)); + } - // 4D if optional point is present - if(m[4]) { - addr.point = parseInt(m[4].substr(1)); - } + // 4D if optional point is present + if(m[4]) { + addr.point = parseInt(m[4].substr(1)); + } - // 5D with @domain - if(m[5]) { - addr.domain = m[5].substr(1); - } + // 5D with @domain + if(m[5]) { + addr.domain = m[5].substr(1); + } - return new Address(addr); - } - } + return new Address(addr); + } + } - toString(dimensions) { - dimensions = dimensions || '5D'; + toString(dimensions) { + dimensions = dimensions || '5D'; - let addrStr = `${this.zone}:${this.net}`; + let addrStr = `${this.zone}:${this.net}`; - // allow for e.g. '4D' or 5 - const dim = parseInt(dimensions.toString()[0]); + // allow for e.g. '4D' or 5 + const dim = parseInt(dimensions.toString()[0]); - if(dim >= 3) { - addrStr += `/${this.node}`; - } + if(dim >= 3) { + addrStr += `/${this.node}`; + } - // missing & .0 are equiv for point - if(dim >= 4 && this.point) { - addrStr += `.${this.point}`; - } + // missing & .0 are equiv for point + if(dim >= 4 && this.point) { + addrStr += `.${this.point}`; + } - if(5 === dim && this.domain) { - addrStr += `@${this.domain.toLowerCase()}`; - } + if(5 === dim && this.domain) { + addrStr += `@${this.domain.toLowerCase()}`; + } - return addrStr; - } + return addrStr; + } - static getComparator() { - return function(left, right) { - let c = (left.zone || 0) - (right.zone || 0); - if(0 !== c) { - return c; - } + static getComparator() { + return function(left, right) { + let c = (left.zone || 0) - (right.zone || 0); + if(0 !== c) { + return c; + } - c = (left.net || 0) - (right.net || 0); - if(0 !== c) { - return c; - } + c = (left.net || 0) - (right.net || 0); + if(0 !== c) { + return c; + } - c = (left.node || 0) - (right.node || 0); - if(0 !== c) { - return c; - } + c = (left.node || 0) - (right.node || 0); + if(0 !== c) { + return c; + } - return (left.domain || '').localeCompare(right.domain || ''); - }; - } + return (left.domain || '').localeCompare(right.domain || ''); + }; + } }; diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index c42d859b..6c49c1c6 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -31,60 +31,60 @@ const FTN_MESSAGE_SAUCE_HEADER = Buffer.from('SAUCE00'); const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; class PacketHeader { - constructor(origAddr, destAddr, version, createdMoment) { - const EMPTY_ADDRESS = { - node : 0, - net : 0, - zone : 0, - point : 0, - }; + constructor(origAddr, destAddr, version, createdMoment) { + const EMPTY_ADDRESS = { + node : 0, + net : 0, + zone : 0, + point : 0, + }; - this.version = version || '2+'; - this.origAddress = origAddr || EMPTY_ADDRESS; - this.destAddress = destAddr || EMPTY_ADDRESS; - this.created = createdMoment || moment(); + this.version = version || '2+'; + this.origAddress = origAddr || EMPTY_ADDRESS; + this.destAddress = destAddr || EMPTY_ADDRESS; + this.created = createdMoment || moment(); - // uncommon to set the following explicitly - this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 - this.prodRevLo = 0; - this.baud = 0; - this.packetType = FTN_PACKET_HEADER_TYPE; - this.password = ''; - this.prodData = 0x47694e45; // "ENiG" + // uncommon to set the following explicitly + this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 + this.prodRevLo = 0; + this.baud = 0; + this.packetType = FTN_PACKET_HEADER_TYPE; + this.password = ''; + this.prodData = 0x47694e45; // "ENiG" - this.capWord = 0x0001; - this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap + this.capWord = 0x0001; + this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap - this.prodCodeHi = 0xfe; // see above - this.prodRevHi = 0; - } + this.prodCodeHi = 0xfe; // see above + this.prodRevHi = 0; + } - get origAddress() { - let addr = new Address({ - node : this.origNode, - zone : this.origZone, - }); + get origAddress() { + let addr = new Address({ + node : this.origNode, + zone : this.origZone, + }); - if(this.origPoint) { - addr.point = this.origPoint; - addr.net = this.auxNet; - } else { - addr.net = this.origNet; - } + if(this.origPoint) { + addr.point = this.origPoint; + addr.net = this.auxNet; + } else { + addr.net = this.origNet; + } - return addr; - } + return addr; + } - set origAddress(address) { - if(_.isString(address)) { - address = Address.fromString(address); - } + set origAddress(address) { + if(_.isString(address)) { + address = Address.fromString(address); + } - this.origNode = address.node; + this.origNode = address.node; - // See FSC-48 - // :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2 - /*if(address.point) { + // See FSC-48 + // :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2 + /*if(address.point) { this.auxNet = address.origNet; this.origNet = -1; } else { @@ -92,63 +92,63 @@ class PacketHeader { this.auxNet = 0; } */ - this.origNet = address.net; - this.auxNet = 0; + this.origNet = address.net; + this.auxNet = 0; - this.origZone = address.zone; - this.origZone2 = address.zone; - this.origPoint = address.point || 0; - } + this.origZone = address.zone; + this.origZone2 = address.zone; + this.origPoint = address.point || 0; + } - get destAddress() { - let addr = new Address({ - node : this.destNode, - net : this.destNet, - zone : this.destZone, - }); + get destAddress() { + let addr = new Address({ + node : this.destNode, + net : this.destNet, + zone : this.destZone, + }); - if(this.destPoint) { - addr.point = this.destPoint; - } + if(this.destPoint) { + addr.point = this.destPoint; + } - return addr; - } + return addr; + } - set destAddress(address) { - if(_.isString(address)) { - address = Address.fromString(address); - } + set destAddress(address) { + if(_.isString(address)) { + address = Address.fromString(address); + } - this.destNode = address.node; - this.destNet = address.net; - this.destZone = address.zone; - this.destZone2 = address.zone; - this.destPoint = address.point || 0; - } + this.destNode = address.node; + this.destNet = address.net; + this.destZone = address.zone; + this.destZone2 = address.zone; + this.destPoint = address.point || 0; + } - get created() { - return moment({ - year : this.year, - month : this.month - 1, // moment uses 0 indexed months - date : this.day, - hour : this.hour, - minute : this.minute, - second : this.second - }); - } + get created() { + return moment({ + year : this.year, + month : this.month - 1, // moment uses 0 indexed months + date : this.day, + hour : this.hour, + minute : this.minute, + second : this.second + }); + } - set created(momentCreated) { - if(!moment.isMoment(momentCreated)) { - momentCreated = moment(momentCreated); - } + set created(momentCreated) { + if(!moment.isMoment(momentCreated)) { + momentCreated = moment(momentCreated); + } - this.year = momentCreated.year(); - this.month = momentCreated.month() + 1; // moment uses 0 indexed months - this.day = momentCreated.date(); // day of month - this.hour = momentCreated.hour(); - this.minute = momentCreated.minute(); - this.second = momentCreated.second(); - } + this.year = momentCreated.year(); + this.month = momentCreated.month() + 1; // moment uses 0 indexed months + this.day = momentCreated.date(); // day of month + this.hour = momentCreated.hour(); + this.minute = momentCreated.minute(); + this.second = momentCreated.second(); + } } exports.PacketHeader = PacketHeader; @@ -166,501 +166,501 @@ exports.PacketHeader = PacketHeader; // http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt // function Packet(options) { - var self = this; + var self = this; - this.options = options || {}; + this.options = options || {}; - this.parsePacketHeader = function(packetBuffer, cb) { - assert(Buffer.isBuffer(packetBuffer)); + this.parsePacketHeader = function(packetBuffer, cb) { + assert(Buffer.isBuffer(packetBuffer)); - let packetHeader; - try { - packetHeader = new Parser() - .uint16le('origNode') - .uint16le('destNode') - .uint16le('year') - .uint16le('month') - .uint16le('day') - .uint16le('hour') - .uint16le('minute') - .uint16le('second') - .uint16le('baud') - .uint16le('packetType') - .uint16le('origNet') - .uint16le('destNet') - .int8('prodCodeLo') - .int8('prodRevLo') // aka serialNo - .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 - .uint16le('origZone') - .uint16le('destZone') - // - // The following is "filler" in FTS-0001, specifics in - // FSC-0045 and FSC-0048 - // - .uint16le('auxNet') - .uint16le('capWordValidate') - .int8('prodCodeHi') - .int8('prodRevHi') - .uint16le('capWord') - .uint16le('origZone2') - .uint16le('destZone2') - .uint16le('origPoint') - .uint16le('destPoint') - .uint32le('prodData') - .parse(packetBuffer); - } catch(e) { - return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`); - } + let packetHeader; + try { + packetHeader = new Parser() + .uint16le('origNode') + .uint16le('destNode') + .uint16le('year') + .uint16le('month') + .uint16le('day') + .uint16le('hour') + .uint16le('minute') + .uint16le('second') + .uint16le('baud') + .uint16le('packetType') + .uint16le('origNet') + .uint16le('destNet') + .int8('prodCodeLo') + .int8('prodRevLo') // aka serialNo + .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 + .uint16le('origZone') + .uint16le('destZone') + // + // The following is "filler" in FTS-0001, specifics in + // FSC-0045 and FSC-0048 + // + .uint16le('auxNet') + .uint16le('capWordValidate') + .int8('prodCodeHi') + .int8('prodRevHi') + .uint16le('capWord') + .uint16le('origZone2') + .uint16le('destZone2') + .uint16le('origPoint') + .uint16le('destPoint') + .uint32le('prodData') + .parse(packetBuffer); + } catch(e) { + return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`); + } - // Convert password from NULL padded array to string - packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); + // Convert password from NULL padded array to string + packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); - if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { - return cb(Errors.Invalid(`Unsupported FTN packet header type: ${packetHeader.packetType}`)); - } + if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { + return cb(Errors.Invalid(`Unsupported FTN packet header type: ${packetHeader.packetType}`)); + } - // - // What kind of packet do we really have here? - // - // :TODO: adjust values based on version discovered - if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { - packetHeader.version = '2.2'; + // + // What kind of packet do we really have here? + // + // :TODO: adjust values based on version discovered + if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { + packetHeader.version = '2.2'; - // See FSC-0045 - packetHeader.origPoint = packetHeader.year; - packetHeader.destPoint = packetHeader.month; + // See FSC-0045 + packetHeader.origPoint = packetHeader.year; + packetHeader.destPoint = packetHeader.month; - packetHeader.destDomain = packetHeader.origZone2; - packetHeader.origDomain = packetHeader.auxNet; - } else { - // - // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" - // - const capWordValidateSwapped = + packetHeader.destDomain = packetHeader.origZone2; + packetHeader.origDomain = packetHeader.auxNet; + } else { + // + // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" + // + const capWordValidateSwapped = ((packetHeader.capWordValidate & 0xff) << 8) | ((packetHeader.capWordValidate >> 8) & 0xff); - if(capWordValidateSwapped === packetHeader.capWord && + if(capWordValidateSwapped === packetHeader.capWord && 0 != packetHeader.capWord && packetHeader.capWord & 0x0001) - { - packetHeader.version = '2+'; + { + packetHeader.version = '2+'; - // See FSC-0048 - if(-1 === packetHeader.origNet) { - packetHeader.origNet = packetHeader.auxNet; - } - } else { - packetHeader.version = '2'; + // See FSC-0048 + if(-1 === packetHeader.origNet) { + packetHeader.origNet = packetHeader.auxNet; + } + } else { + packetHeader.version = '2'; - // :TODO: should fill bytes be 0? - } - } + // :TODO: should fill bytes be 0? + } + } - packetHeader.created = moment({ - year : packetHeader.year, - month : packetHeader.month - 1, // moment uses 0 indexed months - date : packetHeader.day, - hour : packetHeader.hour, - minute : packetHeader.minute, - second : packetHeader.second - }); + packetHeader.created = moment({ + year : packetHeader.year, + month : packetHeader.month - 1, // moment uses 0 indexed months + date : packetHeader.day, + hour : packetHeader.hour, + minute : packetHeader.minute, + second : packetHeader.second + }); - const ph = new PacketHeader(); - _.assign(ph, packetHeader); + const ph = new PacketHeader(); + _.assign(ph, packetHeader); - return cb(null, ph); - }; + return cb(null, ph); + }; - this.getPacketHeaderBuffer = function(packetHeader) { - let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); + this.getPacketHeaderBuffer = function(packetHeader) { + let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); - buffer.writeUInt16LE(packetHeader.origNode, 0); - buffer.writeUInt16LE(packetHeader.destNode, 2); - buffer.writeUInt16LE(packetHeader.year, 4); - buffer.writeUInt16LE(packetHeader.month, 6); - buffer.writeUInt16LE(packetHeader.day, 8); - buffer.writeUInt16LE(packetHeader.hour, 10); - buffer.writeUInt16LE(packetHeader.minute, 12); - buffer.writeUInt16LE(packetHeader.second, 14); + buffer.writeUInt16LE(packetHeader.origNode, 0); + buffer.writeUInt16LE(packetHeader.destNode, 2); + buffer.writeUInt16LE(packetHeader.year, 4); + buffer.writeUInt16LE(packetHeader.month, 6); + buffer.writeUInt16LE(packetHeader.day, 8); + buffer.writeUInt16LE(packetHeader.hour, 10); + buffer.writeUInt16LE(packetHeader.minute, 12); + buffer.writeUInt16LE(packetHeader.second, 14); - buffer.writeUInt16LE(packetHeader.baud, 16); - buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); - buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); - buffer.writeUInt16LE(packetHeader.destNet, 22); - buffer.writeUInt8(packetHeader.prodCodeLo, 24); - buffer.writeUInt8(packetHeader.prodRevHi, 25); + buffer.writeUInt16LE(packetHeader.baud, 16); + buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); + buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); + buffer.writeUInt16LE(packetHeader.destNet, 22); + buffer.writeUInt8(packetHeader.prodCodeLo, 24); + buffer.writeUInt8(packetHeader.prodRevHi, 25); - const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); - pass.copy(buffer, 26); + const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); + pass.copy(buffer, 26); - buffer.writeUInt16LE(packetHeader.origZone, 34); - buffer.writeUInt16LE(packetHeader.destZone, 36); - buffer.writeUInt16LE(packetHeader.auxNet, 38); - buffer.writeUInt16LE(packetHeader.capWordValidate, 40); - buffer.writeUInt8(packetHeader.prodCodeHi, 42); - buffer.writeUInt8(packetHeader.prodRevLo, 43); - buffer.writeUInt16LE(packetHeader.capWord, 44); - buffer.writeUInt16LE(packetHeader.origZone2, 46); - buffer.writeUInt16LE(packetHeader.destZone2, 48); - buffer.writeUInt16LE(packetHeader.origPoint, 50); - buffer.writeUInt16LE(packetHeader.destPoint, 52); - buffer.writeUInt32LE(packetHeader.prodData, 54); + buffer.writeUInt16LE(packetHeader.origZone, 34); + buffer.writeUInt16LE(packetHeader.destZone, 36); + buffer.writeUInt16LE(packetHeader.auxNet, 38); + buffer.writeUInt16LE(packetHeader.capWordValidate, 40); + buffer.writeUInt8(packetHeader.prodCodeHi, 42); + buffer.writeUInt8(packetHeader.prodRevLo, 43); + buffer.writeUInt16LE(packetHeader.capWord, 44); + buffer.writeUInt16LE(packetHeader.origZone2, 46); + buffer.writeUInt16LE(packetHeader.destZone2, 48); + buffer.writeUInt16LE(packetHeader.origPoint, 50); + buffer.writeUInt16LE(packetHeader.destPoint, 52); + buffer.writeUInt32LE(packetHeader.prodData, 54); - return buffer; - }; + return buffer; + }; - this.writePacketHeader = function(packetHeader, ws) { - let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); + this.writePacketHeader = function(packetHeader, ws) { + let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); - buffer.writeUInt16LE(packetHeader.origNode, 0); - buffer.writeUInt16LE(packetHeader.destNode, 2); - buffer.writeUInt16LE(packetHeader.year, 4); - buffer.writeUInt16LE(packetHeader.month, 6); - buffer.writeUInt16LE(packetHeader.day, 8); - buffer.writeUInt16LE(packetHeader.hour, 10); - buffer.writeUInt16LE(packetHeader.minute, 12); - buffer.writeUInt16LE(packetHeader.second, 14); + buffer.writeUInt16LE(packetHeader.origNode, 0); + buffer.writeUInt16LE(packetHeader.destNode, 2); + buffer.writeUInt16LE(packetHeader.year, 4); + buffer.writeUInt16LE(packetHeader.month, 6); + buffer.writeUInt16LE(packetHeader.day, 8); + buffer.writeUInt16LE(packetHeader.hour, 10); + buffer.writeUInt16LE(packetHeader.minute, 12); + buffer.writeUInt16LE(packetHeader.second, 14); - buffer.writeUInt16LE(packetHeader.baud, 16); - buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); - buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); - buffer.writeUInt16LE(packetHeader.destNet, 22); - buffer.writeUInt8(packetHeader.prodCodeLo, 24); - buffer.writeUInt8(packetHeader.prodRevHi, 25); + buffer.writeUInt16LE(packetHeader.baud, 16); + buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); + buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); + buffer.writeUInt16LE(packetHeader.destNet, 22); + buffer.writeUInt8(packetHeader.prodCodeLo, 24); + buffer.writeUInt8(packetHeader.prodRevHi, 25); - const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); - pass.copy(buffer, 26); + const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); + pass.copy(buffer, 26); - buffer.writeUInt16LE(packetHeader.origZone, 34); - buffer.writeUInt16LE(packetHeader.destZone, 36); - buffer.writeUInt16LE(packetHeader.auxNet, 38); - buffer.writeUInt16LE(packetHeader.capWordValidate, 40); - buffer.writeUInt8(packetHeader.prodCodeHi, 42); - buffer.writeUInt8(packetHeader.prodRevLo, 43); - buffer.writeUInt16LE(packetHeader.capWord, 44); - buffer.writeUInt16LE(packetHeader.origZone2, 46); - buffer.writeUInt16LE(packetHeader.destZone2, 48); - buffer.writeUInt16LE(packetHeader.origPoint, 50); - buffer.writeUInt16LE(packetHeader.destPoint, 52); - buffer.writeUInt32LE(packetHeader.prodData, 54); + buffer.writeUInt16LE(packetHeader.origZone, 34); + buffer.writeUInt16LE(packetHeader.destZone, 36); + buffer.writeUInt16LE(packetHeader.auxNet, 38); + buffer.writeUInt16LE(packetHeader.capWordValidate, 40); + buffer.writeUInt8(packetHeader.prodCodeHi, 42); + buffer.writeUInt8(packetHeader.prodRevLo, 43); + buffer.writeUInt16LE(packetHeader.capWord, 44); + buffer.writeUInt16LE(packetHeader.origZone2, 46); + buffer.writeUInt16LE(packetHeader.destZone2, 48); + buffer.writeUInt16LE(packetHeader.origPoint, 50); + buffer.writeUInt16LE(packetHeader.destPoint, 52); + buffer.writeUInt32LE(packetHeader.prodData, 54); - ws.write(buffer); + ws.write(buffer); - return buffer.length; - }; + return buffer.length; + }; - this.processMessageBody = function(messageBodyBuffer, cb) { - // - // From FTS-0001.16: - // "Message text is unbounded and null terminated (note exception below). - // - // A 'hard' carriage return, 0DH, marks the end of a paragraph, and must - // be preserved. - // - // So called 'soft' carriage returns, 8DH, may mark a previous - // processor's automatic line wrap, and should be ignored. Beware that - // they may be followed by linefeeds, or may not. - // - // All linefeeds, 0AH, should be ignored. Systems which display message - // text should wrap long lines to suit their application." - // - // This can be a bit tricky: - // * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that - // * Many kludge lines specify an encoding. If we find one of such lines, we'll - // likely need to re-decode as the specified encoding - // * SAUCE is binary-ish data, so we need to inspect for it before any - // decoding occurs - // - let messageBodyData = { - message : [], - kludgeLines : {}, // KLUDGE:[value1, value2, ...] map - seenBy : [], - }; + this.processMessageBody = function(messageBodyBuffer, cb) { + // + // From FTS-0001.16: + // "Message text is unbounded and null terminated (note exception below). + // + // A 'hard' carriage return, 0DH, marks the end of a paragraph, and must + // be preserved. + // + // So called 'soft' carriage returns, 8DH, may mark a previous + // processor's automatic line wrap, and should be ignored. Beware that + // they may be followed by linefeeds, or may not. + // + // All linefeeds, 0AH, should be ignored. Systems which display message + // text should wrap long lines to suit their application." + // + // This can be a bit tricky: + // * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that + // * Many kludge lines specify an encoding. If we find one of such lines, we'll + // likely need to re-decode as the specified encoding + // * SAUCE is binary-ish data, so we need to inspect for it before any + // decoding occurs + // + let messageBodyData = { + message : [], + kludgeLines : {}, // KLUDGE:[value1, value2, ...] map + seenBy : [], + }; - function addKludgeLine(line) { - // - // We have to special case INTL/TOPT/FMPT as they don't contain - // a ':' name/value separator like the rest of the kludge lines... because stupdity. - // - let key = line.substr(0, 4).trim(); - let value; - if( ['INTL', 'TOPT', 'FMPT', 'Via' ].includes(key)) { - value = line.substr(key.length).trim(); - } else { - const sepIndex = line.indexOf(':'); - key = line.substr(0, sepIndex).toUpperCase(); - value = line.substr(sepIndex + 1).trim(); - } + function addKludgeLine(line) { + // + // We have to special case INTL/TOPT/FMPT as they don't contain + // a ':' name/value separator like the rest of the kludge lines... because stupdity. + // + let key = line.substr(0, 4).trim(); + let value; + if( ['INTL', 'TOPT', 'FMPT', 'Via' ].includes(key)) { + value = line.substr(key.length).trim(); + } else { + const sepIndex = line.indexOf(':'); + key = line.substr(0, sepIndex).toUpperCase(); + value = line.substr(sepIndex + 1).trim(); + } - // - // Allow mapped value to be either a key:value if there is only - // one entry, or key:[value1, value2,...] if there are more - // - if(messageBodyData.kludgeLines[key]) { - if(!_.isArray(messageBodyData.kludgeLines[key])) { - messageBodyData.kludgeLines[key] = [ messageBodyData.kludgeLines[key] ]; - } - messageBodyData.kludgeLines[key].push(value); - } else { - messageBodyData.kludgeLines[key] = value; - } - } + // + // Allow mapped value to be either a key:value if there is only + // one entry, or key:[value1, value2,...] if there are more + // + if(messageBodyData.kludgeLines[key]) { + if(!_.isArray(messageBodyData.kludgeLines[key])) { + messageBodyData.kludgeLines[key] = [ messageBodyData.kludgeLines[key] ]; + } + messageBodyData.kludgeLines[key].push(value); + } else { + messageBodyData.kludgeLines[key] = value; + } + } - let encoding = 'cp437'; + let encoding = 'cp437'; - async.series( - [ - function extractSauce(callback) { - // :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's - // present, we need to extract it but keep the rest of hte message intact as it likely - // has SEEN-BY, PATH, and other kludge information *appended* - const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER); - if(sauceHeaderPosition > -1) { - sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition, sauceHeaderPosition + sauce.SAUCE_SIZE), (err, theSauce) => { - if(!err) { - // we read some SAUCE - don't re-process that portion into the body - messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); - // messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); - messageBodyData.sauce = theSauce; - } else { - Log.warn( { error : err.message }, 'Found what looks like to be a SAUCE record, but failed to read'); - } - return callback(null); // failure to read SAUCE is OK - }); - } else { - callback(null); - } - }, - function extractChrsAndDetermineEncoding(callback) { - // - // From FTS-5003.001: - // "The CHRS control line is formatted as follows: - // - // ^ACHRS: - // - // Where is a character string of no more than eight (8) - // ASCII characters identifying the character set or character encoding - // scheme used, and level is a positive integer value describing what - // level of CHRS the message is written in." - // - // Also according to the spec, the deprecated "CHARSET" value may be used - // :TODO: Look into CHARSET more - should we bother supporting it? - // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam - const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" - const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] ); + async.series( + [ + function extractSauce(callback) { + // :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's + // present, we need to extract it but keep the rest of hte message intact as it likely + // has SEEN-BY, PATH, and other kludge information *appended* + const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER); + if(sauceHeaderPosition > -1) { + sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition, sauceHeaderPosition + sauce.SAUCE_SIZE), (err, theSauce) => { + if(!err) { + // we read some SAUCE - don't re-process that portion into the body + messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); + // messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); + messageBodyData.sauce = theSauce; + } else { + Log.warn( { error : err.message }, 'Found what looks like to be a SAUCE record, but failed to read'); + } + return callback(null); // failure to read SAUCE is OK + }); + } else { + callback(null); + } + }, + function extractChrsAndDetermineEncoding(callback) { + // + // From FTS-5003.001: + // "The CHRS control line is formatted as follows: + // + // ^ACHRS: + // + // Where is a character string of no more than eight (8) + // ASCII characters identifying the character set or character encoding + // scheme used, and level is a positive integer value describing what + // level of CHRS the message is written in." + // + // Also according to the spec, the deprecated "CHARSET" value may be used + // :TODO: Look into CHARSET more - should we bother supporting it? + // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam + const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" + const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] ); - let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); - if(chrsPrefixIndex < 0) { - return callback(null); - } + let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); + if(chrsPrefixIndex < 0) { + return callback(null); + } - chrsPrefixIndex += FTN_CHRS_PREFIX.length; + chrsPrefixIndex += FTN_CHRS_PREFIX.length; - const chrsEndIndex = messageBodyBuffer.indexOf(FTN_CHRS_SUFFIX, chrsPrefixIndex); - if(chrsEndIndex < 0) { - return callback(null); - } + const chrsEndIndex = messageBodyBuffer.indexOf(FTN_CHRS_SUFFIX, chrsPrefixIndex); + if(chrsEndIndex < 0) { + return callback(null); + } - let chrsContent = messageBodyBuffer.slice(chrsPrefixIndex, chrsEndIndex); - if(0 === chrsContent.length) { - return callback(null); - } + let chrsContent = messageBodyBuffer.slice(chrsPrefixIndex, chrsEndIndex); + if(0 === chrsContent.length) { + return callback(null); + } - chrsContent = iconv.decode(chrsContent, 'CP437'); - const chrsEncoding = ftn.getEncodingFromCharacterSetIdentifier(chrsContent); - if(chrsEncoding) { - encoding = chrsEncoding; - } - return callback(null); - }, - function extractMessageData(callback) { - // - // Decode |messageBodyBuffer| using |encoding| defaulted or detected above - // - // :TODO: Look into \xec thing more - document - let decoded; - try { - decoded = iconv.decode(messageBodyBuffer, encoding); - } catch(e) { - Log.debug( { encoding : encoding, error : e.toString() }, 'Error decoding. Falling back to ASCII'); - decoded = iconv.decode(messageBodyBuffer, 'ascii'); - } + chrsContent = iconv.decode(chrsContent, 'CP437'); + const chrsEncoding = ftn.getEncodingFromCharacterSetIdentifier(chrsContent); + if(chrsEncoding) { + encoding = chrsEncoding; + } + return callback(null); + }, + function extractMessageData(callback) { + // + // Decode |messageBodyBuffer| using |encoding| defaulted or detected above + // + // :TODO: Look into \xec thing more - document + let decoded; + try { + decoded = iconv.decode(messageBodyBuffer, encoding); + } catch(e) { + Log.debug( { encoding : encoding, error : e.toString() }, 'Error decoding. Falling back to ASCII'); + decoded = iconv.decode(messageBodyBuffer, 'ascii'); + } - const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, '')); - let endOfMessage = false; + const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, '')); + let endOfMessage = false; - messageLines.forEach(line => { - if(0 === line.length) { - messageBodyData.message.push(''); - return; - } + messageLines.forEach(line => { + if(0 === line.length) { + messageBodyData.message.push(''); + return; + } - if(line.startsWith('AREA:')) { - messageBodyData.area = line.substring(line.indexOf(':') + 1).trim(); - } else if(line.startsWith('--- ')) { - // Tear Lines are tracked allowing for specialized display/etc. - messageBodyData.tearLine = line; - } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." - messageBodyData.originLine = line; - endOfMessage = true; // Anything past origin is not part of the message body - } else if(line.startsWith('SEEN-BY:')) { - endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body - messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim()); - } else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) { - if('PATH:' === line.slice(1, 6)) { - endOfMessage = true; // Anything pats the first PATH is not part of the message body - } - addKludgeLine(line.slice(1)); - } else if(!endOfMessage) { - // regular ol' message line - messageBodyData.message.push(line); - } - }); + if(line.startsWith('AREA:')) { + messageBodyData.area = line.substring(line.indexOf(':') + 1).trim(); + } else if(line.startsWith('--- ')) { + // Tear Lines are tracked allowing for specialized display/etc. + messageBodyData.tearLine = line; + } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." + messageBodyData.originLine = line; + endOfMessage = true; // Anything past origin is not part of the message body + } else if(line.startsWith('SEEN-BY:')) { + endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body + messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim()); + } else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) { + if('PATH:' === line.slice(1, 6)) { + endOfMessage = true; // Anything pats the first PATH is not part of the message body + } + addKludgeLine(line.slice(1)); + } else if(!endOfMessage) { + // regular ol' message line + messageBodyData.message.push(line); + } + }); - return callback(null); - } - ], - () => { - messageBodyData.message = messageBodyData.message.join('\n'); - return cb(messageBodyData); - } - ); - }; + return callback(null); + } + ], + () => { + messageBodyData.message = messageBodyData.message.join('\n'); + return cb(messageBodyData); + } + ); + }; - this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { - // - // Check for end-of-messages marker up front before parse so we can easily - // tell the difference between end and bad header - // - if(packetBuffer.length < 3) { - const peek = packetBuffer.slice(0, 2); - if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) { - // end marker - no more messages - return cb(null); - } - // else fall through & hit exception below to log error - } + this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { + // + // Check for end-of-messages marker up front before parse so we can easily + // tell the difference between end and bad header + // + if(packetBuffer.length < 3) { + const peek = packetBuffer.slice(0, 2); + if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) { + // end marker - no more messages + return cb(null); + } + // else fall through & hit exception below to log error + } - let msgData; - try { - msgData = new Parser() - .uint16le('messageType') - .uint16le('ftn_msg_orig_node') - .uint16le('ftn_msg_dest_node') - .uint16le('ftn_msg_orig_net') - .uint16le('ftn_msg_dest_net') - .uint16le('ftn_attr_flags') - .uint16le('ftn_cost') - // :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved - .array('modDateTime', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .array('toUserName', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .array('fromUserName', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .array('subject', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .array('message', { - type : 'uint8', - readUntil : b => 0x00 === b, - }) - .parse(packetBuffer); - } catch(e) { - return cb(Errors.Invalid(`Failed to parse FTN message header: ${e.message}`)); - } + let msgData; + try { + msgData = new Parser() + .uint16le('messageType') + .uint16le('ftn_msg_orig_node') + .uint16le('ftn_msg_dest_node') + .uint16le('ftn_msg_orig_net') + .uint16le('ftn_msg_dest_net') + .uint16le('ftn_attr_flags') + .uint16le('ftn_cost') + // :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved + .array('modDateTime', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('toUserName', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('fromUserName', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('subject', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .array('message', { + type : 'uint8', + readUntil : b => 0x00 === b, + }) + .parse(packetBuffer); + } catch(e) { + return cb(Errors.Invalid(`Failed to parse FTN message header: ${e.message}`)); + } - if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { - return cb(Errors.Invalid(`Unsupported FTN message type: ${msgData.messageType}`)); - } + if(FTN_PACKET_MESSAGE_TYPE != msgData.messageType) { + return cb(Errors.Invalid(`Unsupported FTN message type: ${msgData.messageType}`)); + } - // - // Convert null terminated arrays to strings - // - [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { - msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437'); - }); + // + // Convert null terminated arrays to strings + // + [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { + msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437'); + }); - // Technically the following fields have length limits as per fts-0001.016: - // * modDateTime : 20 bytes - // * toUserName : 36 bytes - // * fromUserName : 36 bytes - // * subject : 72 bytes + // Technically the following fields have length limits as per fts-0001.016: + // * modDateTime : 20 bytes + // * toUserName : 36 bytes + // * fromUserName : 36 bytes + // * subject : 72 bytes - // - // The message body itself is a special beast as it may - // contain an origin line, kludges, SAUCE in the case - // of ANSI files, etc. - // - const msg = new Message( { - toUserName : msgData.toUserName, - fromUserName : msgData.fromUserName, - subject : msgData.subject, - modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), - }); + // + // The message body itself is a special beast as it may + // contain an origin line, kludges, SAUCE in the case + // of ANSI files, etc. + // + const msg = new Message( { + toUserName : msgData.toUserName, + fromUserName : msgData.fromUserName, + subject : msgData.subject, + modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), + }); - // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) - msg.meta.FtnProperty = { - ftn_orig_node : header.origNode, - ftn_dest_node : header.destNode, - ftn_orig_network : header.origNet, - ftn_dest_network : header.destNet, + // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) + msg.meta.FtnProperty = { + ftn_orig_node : header.origNode, + ftn_dest_node : header.destNode, + ftn_orig_network : header.origNet, + ftn_dest_network : header.destNet, - ftn_attr_flags : msgData.ftn_attr_flags, - ftn_cost : msgData.ftn_cost, + ftn_attr_flags : msgData.ftn_attr_flags, + ftn_cost : msgData.ftn_cost, - ftn_msg_orig_node : msgData.ftn_msg_orig_node, - ftn_msg_dest_node : msgData.ftn_msg_dest_node, - ftn_msg_orig_net : msgData.ftn_msg_orig_net, - ftn_msg_dest_net : msgData.ftn_msg_dest_net, - }; + ftn_msg_orig_node : msgData.ftn_msg_orig_node, + ftn_msg_dest_node : msgData.ftn_msg_dest_node, + ftn_msg_orig_net : msgData.ftn_msg_orig_net, + ftn_msg_dest_net : msgData.ftn_msg_dest_net, + }; - self.processMessageBody(msgData.message, messageBodyData => { - msg.message = messageBodyData.message; - msg.meta.FtnKludge = messageBodyData.kludgeLines; + self.processMessageBody(msgData.message, messageBodyData => { + msg.message = messageBodyData.message; + msg.meta.FtnKludge = messageBodyData.kludgeLines; - if(messageBodyData.tearLine) { - msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; + if(messageBodyData.tearLine) { + msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; - if(self.options.keepTearAndOrigin) { - msg.message += `\r\n${messageBodyData.tearLine}\r\n`; - } - } + if(self.options.keepTearAndOrigin) { + msg.message += `\r\n${messageBodyData.tearLine}\r\n`; + } + } - if(messageBodyData.seenBy.length > 0) { - msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; - } + if(messageBodyData.seenBy.length > 0) { + msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; + } - if(messageBodyData.area) { - msg.meta.FtnProperty.ftn_area = messageBodyData.area; - } + if(messageBodyData.area) { + msg.meta.FtnProperty.ftn_area = messageBodyData.area; + } - if(messageBodyData.originLine) { - msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; + if(messageBodyData.originLine) { + msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; - if(self.options.keepTearAndOrigin) { - msg.message += `${messageBodyData.originLine}\r\n`; - } - } + if(self.options.keepTearAndOrigin) { + msg.message += `${messageBodyData.originLine}\r\n`; + } + } - // - // If we have a UTC offset kludge (e.g. TZUTC) then update - // modDateTime with it - // - if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { - msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); - } + // + // If we have a UTC offset kludge (e.g. TZUTC) then update + // modDateTime with it + // + if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { + msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); + } - // :TODO: Parser should give is this info: - const bytesRead = + // :TODO: Parser should give is this info: + const bytesRead = 14 + // fixed header size msgData.modDateTime.length + 1 + // +1 = NULL msgData.toUserName.length + 1 + // +1 = NULL @@ -668,322 +668,322 @@ function Packet(options) { msgData.subject.length + 1 + // +1 = NULL msgData.message.length; // includes NULL - const nextBuf = packetBuffer.slice(bytesRead); - if(nextBuf.length > 0) { - const next = function(e) { - if(e) { - cb(e); - } else { - self.parsePacketMessages(header, nextBuf, iterator, cb); - } - }; + const nextBuf = packetBuffer.slice(bytesRead); + if(nextBuf.length > 0) { + const next = function(e) { + if(e) { + cb(e); + } else { + self.parsePacketMessages(header, nextBuf, iterator, cb); + } + }; - iterator('message', msg, next); - } else { - cb(null); - } - }); - }; + iterator('message', msg, next); + } else { + cb(null); + } + }); + }; - this.sanatizeFtnProperties = function(message) { - [ - Message.FtnPropertyNames.FtnOrigNode, - Message.FtnPropertyNames.FtnDestNode, - Message.FtnPropertyNames.FtnOrigNetwork, - Message.FtnPropertyNames.FtnDestNetwork, - Message.FtnPropertyNames.FtnAttrFlags, - Message.FtnPropertyNames.FtnCost, - Message.FtnPropertyNames.FtnOrigZone, - Message.FtnPropertyNames.FtnDestZone, - Message.FtnPropertyNames.FtnOrigPoint, - Message.FtnPropertyNames.FtnDestPoint, - Message.FtnPropertyNames.FtnAttribute, - Message.FtnPropertyNames.FtnMsgOrigNode, - Message.FtnPropertyNames.FtnMsgDestNode, - Message.FtnPropertyNames.FtnMsgOrigNet, - Message.FtnPropertyNames.FtnMsgDestNet, - ].forEach( propName => { - if(message.meta.FtnProperty[propName]) { - message.meta.FtnProperty[propName] = parseInt(message.meta.FtnProperty[propName]) || 0; - } - }); - }; + this.sanatizeFtnProperties = function(message) { + [ + Message.FtnPropertyNames.FtnOrigNode, + Message.FtnPropertyNames.FtnDestNode, + Message.FtnPropertyNames.FtnOrigNetwork, + Message.FtnPropertyNames.FtnDestNetwork, + Message.FtnPropertyNames.FtnAttrFlags, + Message.FtnPropertyNames.FtnCost, + Message.FtnPropertyNames.FtnOrigZone, + Message.FtnPropertyNames.FtnDestZone, + Message.FtnPropertyNames.FtnOrigPoint, + Message.FtnPropertyNames.FtnDestPoint, + Message.FtnPropertyNames.FtnAttribute, + Message.FtnPropertyNames.FtnMsgOrigNode, + Message.FtnPropertyNames.FtnMsgDestNode, + Message.FtnPropertyNames.FtnMsgOrigNet, + Message.FtnPropertyNames.FtnMsgDestNet, + ].forEach( propName => { + if(message.meta.FtnProperty[propName]) { + message.meta.FtnProperty[propName] = parseInt(message.meta.FtnProperty[propName]) || 0; + } + }); + }; - this.writeMessageHeader = function(message, buf) { - // ensure address FtnProperties are numbers - self.sanatizeFtnProperties(message); + this.writeMessageHeader = function(message, buf) { + // ensure address FtnProperties are numbers + self.sanatizeFtnProperties(message); - const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node; - const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network; + const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node; + const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network; - buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); - buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); - buf.writeUInt16LE(destNode, 4); - buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6); - buf.writeUInt16LE(destNet, 8); - buf.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); - buf.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); + buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); + buf.writeUInt16LE(destNode, 4); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6); + buf.writeUInt16LE(destNet, 8); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); + buf.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); - const dateTimeBuffer = Buffer.from(ftn.getDateTimeString(message.modTimestamp) + '\0'); - dateTimeBuffer.copy(buf, 14); - }; + const dateTimeBuffer = Buffer.from(ftn.getDateTimeString(message.modTimestamp) + '\0'); + dateTimeBuffer.copy(buf, 14); + }; - this.getMessageEntryBuffer = function(message, options, cb) { + this.getMessageEntryBuffer = function(message, options, cb) { - function getAppendMeta(k, m, sepChar=':') { - let append = ''; - if(m) { - let a = m; - if(!_.isArray(a)) { - a = [ a ]; - } - a.forEach(v => { - append += `${k}${sepChar} ${v}\r`; - }); - } - return append; - } + function getAppendMeta(k, m, sepChar=':') { + let append = ''; + if(m) { + let a = m; + if(!_.isArray(a)) { + a = [ a ]; + } + a.forEach(v => { + append += `${k}${sepChar} ${v}\r`; + }); + } + return append; + } - async.waterfall( - [ - function prepareHeaderAndKludges(callback) { - const basicHeader = Buffer.alloc(34); - self.writeMessageHeader(message, basicHeader); + async.waterfall( + [ + function prepareHeaderAndKludges(callback) { + const basicHeader = Buffer.alloc(34); + self.writeMessageHeader(message, basicHeader); - // - // 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 } ); + // + // 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 = ''; + // + // 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) - } + // + // 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) + } - // :TODO: DRY with similar function in this file! - Object.keys(message.meta.FtnKludge).forEach(k => { - switch(k) { - case 'PATH' : - break; // skip & save for last + // :TODO: DRY with similar function in this file! + Object.keys(message.meta.FtnKludge).forEach(k => { + switch(k) { + case 'PATH' : + break; // skip & save for last - case 'Via' : - case 'FMPT' : - case 'TOPT' : - case 'INTL' : - msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar - break; + case 'Via' : + case 'FMPT' : + case 'TOPT' : + case 'INTL' : + msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar + break; - default : - msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); - break; - } - }); + default : + msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); + break; + } + }); - 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); - } + 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'; + 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`; - } + // + // 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`; - } + // + // 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']); + // + // 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'); - } + 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); - } - ); - }; + return callback( + null, + Buffer.concat( [ + basicHeader, + toUserNameBuf, + fromUserNameBuf, + subjectBuf, + msgBodyEncoded + ]) + ); + } + ], + (err, msgEntryBuffer) => { + return cb(err, msgEntryBuffer); + } + ); + }; - this.writeMessage = function(message, ws, options) { - const basicHeader = Buffer.alloc(34); - self.writeMessageHeader(message, basicHeader); + this.writeMessage = function(message, ws, options) { + const basicHeader = Buffer.alloc(34); + self.writeMessageHeader(message, basicHeader); - ws.write(basicHeader); + ws.write(basicHeader); - // toUserName & fromUserName: up to 36 bytes in length, NULL term'd - // :TODO: DRY... - let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd - ws.write(encBuf); + // toUserName & fromUserName: up to 36 bytes in length, NULL term'd + // :TODO: DRY... + let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36); + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + ws.write(encBuf); - encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd - ws.write(encBuf); + encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36); + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + ws.write(encBuf); - // subject: up to 72 bytes in length, NULL term'd - encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd - ws.write(encBuf); + // subject: up to 72 bytes in length, NULL term'd + encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72); + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + ws.write(encBuf); - // - // 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 = ''; + // + // 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, sepChar=':') { - if(m) { - let a = m; - if(!_.isArray(a)) { - a = [ a ]; - } - a.forEach(v => { - msgBody += `${k}${sepChar} ${v}\r`; - }); - } - } + function appendMeta(k, m, sepChar=':') { + if(m) { + let a = m; + if(!_.isArray(a)) { + a = [ a ]; + } + a.forEach(v => { + msgBody += `${k}${sepChar} ${v}\r`; + }); + } + } - // - // 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) - } + // + // 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 => { - switch(k) { - case 'PATH' : break; // skip & save for last + Object.keys(message.meta.FtnKludge).forEach(k => { + switch(k) { + case 'PATH' : break; // skip & save for last - case 'Via' : - case 'FMPT' : - case 'TOPT' : - case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar + case 'Via' : + case 'FMPT' : + case 'TOPT' : + case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar - default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; - } - }); + default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; + } + }); - msgBody += message.message + '\r'; + msgBody += message.message + '\r'; - // - // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 - // 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`; - } + // + // 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`; - } + // + // 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) + // + // 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']); + appendMeta('\x01PATH', message.meta.FtnKludge['PATH']); - // - // :TODO: We should encode based on config and add the proper kludge here! - ws.write(iconv.encode(msgBody + '\0', options.encoding)); - }; + // + // :TODO: We should encode based on config and add the proper kludge here! + ws.write(iconv.encode(msgBody + '\0', options.encoding)); + }; - this.parsePacketBuffer = function(packetBuffer, iterator, cb) { - async.waterfall( - [ - function processHeader(callback) { - self.parsePacketHeader(packetBuffer, (err, header) => { - if(err) { - return callback(err); - } + this.parsePacketBuffer = function(packetBuffer, iterator, cb) { + async.waterfall( + [ + function processHeader(callback) { + self.parsePacketHeader(packetBuffer, (err, header) => { + if(err) { + return callback(err); + } - const next = function(e) { - return callback(e, header); - }; + const next = function(e) { + return callback(e, header); + }; - iterator('header', header, next); - }); - }, - function processMessages(header, callback) { - self.parsePacketMessages( - header, - packetBuffer.slice(FTN_PACKET_HEADER_SIZE), - iterator, - callback); - } - ], - cb // complete - ); - }; + iterator('header', header, next); + }); + }, + function processMessages(header, callback) { + self.parsePacketMessages( + header, + packetBuffer.slice(FTN_PACKET_HEADER_SIZE), + iterator, + callback); + } + ], + cb // complete + ); + }; } // @@ -994,100 +994,100 @@ function Packet(options) { // * http://www.skepticfiles.org/aj/basics03.htm // Packet.Attribute = { - Private : 0x0001, // Private message / NetMail - Crash : 0x0002, - Received : 0x0004, - Sent : 0x0008, - FileAttached : 0x0010, - InTransit : 0x0020, - Orphan : 0x0040, - KillSent : 0x0080, - Local : 0x0100, // Message is from *this* system - Hold : 0x0200, - Reserved0 : 0x0400, - FileRequest : 0x0800, - ReturnReceiptRequest : 0x1000, - ReturnReceipt : 0x2000, - AuditRequest : 0x4000, - FileUpdateRequest : 0x8000, + Private : 0x0001, // Private message / NetMail + Crash : 0x0002, + Received : 0x0004, + Sent : 0x0008, + FileAttached : 0x0010, + InTransit : 0x0020, + Orphan : 0x0040, + KillSent : 0x0080, + Local : 0x0100, // Message is from *this* system + Hold : 0x0200, + Reserved0 : 0x0400, + FileRequest : 0x0800, + ReturnReceiptRequest : 0x1000, + ReturnReceipt : 0x2000, + AuditRequest : 0x4000, + FileUpdateRequest : 0x8000, }; Object.freeze(Packet.Attribute); Packet.prototype.read = function(pathOrBuffer, iterator, cb) { - var self = this; + var self = this; - async.series( - [ - function getBufferIfPath(callback) { - if(_.isString(pathOrBuffer)) { - fs.readFile(pathOrBuffer, (err, data) => { - pathOrBuffer = data; - callback(err); - }); - } else { - callback(null); - } - }, - function parseBuffer(callback) { - self.parsePacketBuffer(pathOrBuffer, iterator, err => { - callback(err); - }); - } - ], - err => { - cb(err); - } - ); + async.series( + [ + function getBufferIfPath(callback) { + if(_.isString(pathOrBuffer)) { + fs.readFile(pathOrBuffer, (err, data) => { + pathOrBuffer = data; + callback(err); + }); + } else { + callback(null); + } + }, + function parseBuffer(callback) { + self.parsePacketBuffer(pathOrBuffer, iterator, err => { + callback(err); + }); + } + ], + err => { + cb(err); + } + ); }; Packet.prototype.writeHeader = function(ws, packetHeader) { - return this.writePacketHeader(packetHeader, ws); + return this.writePacketHeader(packetHeader, ws); }; Packet.prototype.writeMessageEntry = function(ws, msgEntry) { - ws.write(msgEntry); - return msgEntry.length; + ws.write(msgEntry); + return msgEntry.length; }; Packet.prototype.writeTerminator = function(ws) { - // - // From FTS-0001.016: - // "A pseudo-message beginning with the word 0000H signifies the end of the packet." - // - ws.write(Buffer.from( [ 0x00, 0x00 ] )); // final extra null term - return 2; + // + // From FTS-0001.016: + // "A pseudo-message beginning with the word 0000H signifies the end of the packet." + // + ws.write(Buffer.from( [ 0x00, 0x00 ] )); // final extra null term + return 2; }; Packet.prototype.writeStream = function(ws, messages, options) { - if(!_.isBoolean(options.terminatePacket)) { - options.terminatePacket = true; - } + if(!_.isBoolean(options.terminatePacket)) { + options.terminatePacket = true; + } - if(_.isObject(options.packetHeader)) { - this.writePacketHeader(options.packetHeader, ws); - } + if(_.isObject(options.packetHeader)) { + this.writePacketHeader(options.packetHeader, ws); + } - options.encoding = options.encoding || 'utf8'; + options.encoding = options.encoding || 'utf8'; - messages.forEach(msg => { - this.writeMessage(msg, ws, options); - }); + messages.forEach(msg => { + this.writeMessage(msg, ws, options); + }); - if(true === options.terminatePacket) { - ws.write(Buffer.from( [ 0 ] )); // final extra null term - } + if(true === options.terminatePacket) { + ws.write(Buffer.from( [ 0 ] )); // final extra null term + } }; Packet.prototype.write = function(path, packetHeader, messages, options) { - if(!_.isArray(messages)) { - messages = [ messages ]; - } + if(!_.isArray(messages)) { + messages = [ messages ]; + } - options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4' + options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4' - this.writeStream( - fs.createWriteStream(path), // :TODO: specify mode/etc. - messages, - Object.assign( { packetHeader : packetHeader, terminatePacket : true }, options) - ); + this.writeStream( + fs.createWriteStream(path), // :TODO: specify mode/etc. + messages, + Object.assign( { packetHeader : packetHeader, terminatePacket : true }, options) + ); }; diff --git a/core/ftn_util.js b/core/ftn_util.js index 3fc51cd3..0f65e127 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -45,12 +45,12 @@ exports.getQuotePrefix = getQuotePrefix; // See list here: https://github.com/Mithgol/node-fidonet-jam function stringToNullPaddedBuffer(s, bufLen) { - let buffer = Buffer.alloc(bufLen); - let enc = iconv.encode(s, 'CP437').slice(0, bufLen); - for(let i = 0; i < enc.length; ++i) { - buffer[i] = enc[i]; - } - return buffer; + let buffer = Buffer.alloc(bufLen); + let enc = iconv.encode(s, 'CP437').slice(0, bufLen); + for(let i = 0; i < enc.length; ++i) { + buffer[i] = enc[i]; + } + return buffer; } // @@ -58,45 +58,45 @@ function stringToNullPaddedBuffer(s, bufLen) { // // :TODO: Name the next couple methods better - for FTN *packets* function getDateFromFtnDateTime(dateTime) { - // - // Examples seen in the wild (Working): - // "12 Sep 88 18:17:59" - // "Tue 01 Jan 80 00:00" - // "27 Feb 15 00:00:03" - // - // :TODO: Use moment.js here - return moment(Date.parse(dateTime)); // Date.parse() allows funky formats + // + // Examples seen in the wild (Working): + // "12 Sep 88 18:17:59" + // "Tue 01 Jan 80 00:00" + // "27 Feb 15 00:00:03" + // + // :TODO: Use moment.js here + return moment(Date.parse(dateTime)); // Date.parse() allows funky formats // return (new Date(Date.parse(dateTime))).toISOString(); } function getDateTimeString(m) { - // - // From http://ftsc.org/docs/fts-0001.016: - // DateTime = (* a character string 20 characters long *) - // (* 01 Jan 86 02:34:56 *) - // DayOfMonth " " Month " " Year " " - // " " HH ":" MM ":" SS - // Null - // - // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) - // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | - // "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" - // Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00" - // HH = "00" | .. | "23" - // MM = "00" | .. | "59" - // SS = "00" | .. | "59" - // - if(!moment.isMoment(m)) { - m = moment(m); - } + // + // From http://ftsc.org/docs/fts-0001.016: + // DateTime = (* a character string 20 characters long *) + // (* 01 Jan 86 02:34:56 *) + // DayOfMonth " " Month " " Year " " + // " " HH ":" MM ":" SS + // Null + // + // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) + // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | + // "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" + // Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00" + // HH = "00" | .. | "23" + // MM = "00" | .. | "59" + // SS = "00" | .. | "59" + // + if(!moment.isMoment(m)) { + m = moment(m); + } - return m.format('DD MMM YY HH:mm:ss'); + return m.format('DD MMM YY HH:mm:ss'); } function getMessageSerialNumber(messageId) { - const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); - const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); - return `00000000${hash}`.substr(-8); + const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); + const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); + return `00000000${hash}`.substr(-8); } // @@ -143,11 +143,11 @@ function getMessageSerialNumber(messageId) { // format, but that will only help when using newer Mystic versions. // function getMessageIdentifier(message, address, isNetMail = false) { - const addrStr = new Address(address).toString('5D'); - return isNetMail ? - `${addrStr} ${getMessageSerialNumber(message.messageId)}` : - `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}` - ; + const addrStr = new Address(address).toString('5D'); + return isNetMail ? + `${addrStr} ${getMessageSerialNumber(message.messageId)}` : + `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}` + ; } // @@ -158,10 +158,10 @@ function getMessageIdentifier(message, address, isNetMail = false) { // in which (; ; ) is used instead // function getProductIdentifier() { - const version = getCleanEnigmaVersion(); - const nodeVer = process.version.substr(1); // remove 'v' prefix + const version = getCleanEnigmaVersion(); + const nodeVer = process.version.substr(1); // remove 'v' prefix - return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; + return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } // @@ -171,7 +171,7 @@ function getProductIdentifier() { // http://ftsc.org/docs/frl-1004.002 // function getUTCTimeZoneOffset() { - return moment().format('ZZ').replace(/\+/, ''); + return moment().format('ZZ').replace(/\+/, ''); } // @@ -179,18 +179,18 @@ function getUTCTimeZoneOffset() { // http://ftsc.org/docs/fsc-0032.001 // function getQuotePrefix(name) { - let initials; + let initials; - const parts = name.split(' '); - if(parts.length > 1) { - // First & Last initials - (Bryan Ashby -> BA) - initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase(); - } else { - // Just use the first two - (NuSkooler -> Nu) - initials = _.capitalize(name.slice(0, 2)); - } + const parts = name.split(' '); + if(parts.length > 1) { + // First & Last initials - (Bryan Ashby -> BA) + initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase(); + } else { + // Just use the first two - (NuSkooler -> Nu) + initials = _.capitalize(name.slice(0, 2)); + } - return ` ${initials}> `; + return ` ${initials}> `; } // @@ -198,18 +198,18 @@ function getQuotePrefix(name) { // http://ftsc.org/docs/fts-0004.001 // function getOrigin(address) { - const config = Config(); - const origin = _.has(config, 'messageNetworks.originLine') ? - config.messageNetworks.originLine : - config.general.boardName; + const config = Config(); + const origin = _.has(config, 'messageNetworks.originLine') ? + config.messageNetworks.originLine : + config.general.boardName; - const addrStr = new Address(address).toString('5D'); - return ` * Origin: ${origin} (${addrStr})`; + const addrStr = new Address(address).toString('5D'); + return ` * Origin: ${origin} (${addrStr})`; } function getTearLine() { - const nodeVer = process.version.substr(1); // remove 'v' prefix - return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; + const nodeVer = process.version.substr(1); // remove 'v' prefix + return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } // @@ -217,17 +217,17 @@ function getTearLine() { // http://ftsc.org/docs/frl-1005.001 // function getVia(address) { - /* + /* FRL-1005.001 states teh following format: ^AVia: @YYYYMMDD.HHMMSS[.Precise][.Time Zone] [Serial Number] */ - const addrStr = new Address(address).toString('5D'); - const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); - const version = getCleanEnigmaVersion(); + const addrStr = new Address(address).toString('5D'); + const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); + const version = getCleanEnigmaVersion(); - return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; + return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; } // @@ -235,50 +235,50 @@ function getVia(address) { // http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac // function getIntl(toAddress, fromAddress) { - // - // INTL differs from 'standard' kludges in that there is no ':' after "INTL" - // - // ""INTL "" "" - // "...These addresses shall be given on the form :/" - // - return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`; + // + // INTL differs from 'standard' kludges in that there is no ':' after "INTL" + // + // ""INTL "" "" + // "...These addresses shall be given on the form :/" + // + return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`; } function getAbbreviatedNetNodeList(netNodes) { - let abbrList = ''; - let currNet; - netNodes.forEach(netNode => { - if(_.isString(netNode)) { - netNode = Address.fromString(netNode); - } - if(currNet !== netNode.net) { - abbrList += `${netNode.net}/`; - currNet = netNode.net; - } - abbrList += `${netNode.node} `; - }); + let abbrList = ''; + let currNet; + netNodes.forEach(netNode => { + if(_.isString(netNode)) { + netNode = Address.fromString(netNode); + } + if(currNet !== netNode.net) { + abbrList += `${netNode.net}/`; + currNet = netNode.net; + } + abbrList += `${netNode.node} `; + }); - return abbrList.trim(); // remove trailing space + return abbrList.trim(); // remove trailing space } // // Parse an abbreviated net/node list commonly used for SEEN-BY and PATH // function parseAbbreviatedNetNodeList(netNodes) { - const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g; - let net; - let m; - let results = []; - while(null !== (m = re.exec(netNodes))) { - if(m[1] && m[2]) { - net = parseInt(m[1]); - results.push(new Address( { net : net, node : parseInt(m[2]) } )); - } else if(net) { - results.push(new Address( { net : net, node : parseInt(m[3]) } )); - } - } + const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g; + let net; + let m; + let results = []; + while(null !== (m = re.exec(netNodes))) { + if(m[1] && m[2]) { + net = parseInt(m[1]); + results.push(new Address( { net : net, node : parseInt(m[2]) } )); + } else if(net) { + results.push(new Address( { net : net, node : parseInt(m[3]) } )); + } + } - return results; + return results; } // @@ -295,7 +295,7 @@ function parseAbbreviatedNetNodeList(netNodes) { // not the "SEEN-BY" prefix itself // function getUpdatedSeenByEntries(existingEntries, additions) { - /* + /* From FTS-0004: "There can be many seen-by lines at the end of Conference @@ -316,37 +316,37 @@ function getUpdatedSeenByEntries(existingEntries, additions) { this field is not put in place by other Echomail compatible programs." */ - existingEntries = existingEntries || []; - if(!_.isArray(existingEntries)) { - existingEntries = [ existingEntries ]; - } + existingEntries = existingEntries || []; + if(!_.isArray(existingEntries)) { + existingEntries = [ existingEntries ]; + } - if(!_.isString(additions)) { - additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); - } + if(!_.isString(additions)) { + additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); + } - additions = additions.sort(Address.getComparator()); + additions = additions.sort(Address.getComparator()); - // - // For now, we'll just append a new SEEN-BY entry - // - // :TODO: we should at least try and update what is already there in a smart way - existingEntries.push(getAbbreviatedNetNodeList(additions)); - return existingEntries; + // + // For now, we'll just append a new SEEN-BY entry + // + // :TODO: we should at least try and update what is already there in a smart way + existingEntries.push(getAbbreviatedNetNodeList(additions)); + return existingEntries; } function getUpdatedPathEntries(existingEntries, localAddress) { - // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line + // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line - existingEntries = existingEntries || []; - if(!_.isArray(existingEntries)) { - existingEntries = [ existingEntries ]; - } + existingEntries = existingEntries || []; + if(!_.isArray(existingEntries)) { + existingEntries = [ existingEntries ]; + } - existingEntries.push(getAbbreviatedNetNodeList( - parseAbbreviatedNetNodeList(localAddress))); + existingEntries.push(getAbbreviatedNetNodeList( + parseAbbreviatedNetNodeList(localAddress))); - return existingEntries; + return existingEntries; } // @@ -354,71 +354,71 @@ function getUpdatedPathEntries(existingEntries, localAddress) { // http://ftsc.org/docs/fts-5003.001 // const ENCODING_TO_FTS_5003_001_CHARS = { - // level 1 - generally should not be used - ascii : [ 'ASCII', 1 ], - 'us-ascii' : [ 'ASCII', 1 ], + // level 1 - generally should not be used + ascii : [ 'ASCII', 1 ], + 'us-ascii' : [ 'ASCII', 1 ], - // level 2 - 8 bit, ASCII based - cp437 : [ 'CP437', 2 ], - cp850 : [ 'CP850', 2 ], + // level 2 - 8 bit, ASCII based + cp437 : [ 'CP437', 2 ], + cp850 : [ 'CP850', 2 ], - // level 3 - reserved + // level 3 - reserved - // level 4 - utf8 : [ 'UTF-8', 4 ], - 'utf-8' : [ 'UTF-8', 4 ], + // level 4 + utf8 : [ 'UTF-8', 4 ], + 'utf-8' : [ 'UTF-8', 4 ], }; function getCharacterSetIdentifierByEncoding(encodingName) { - const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()]; - return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase(); + const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()]; + return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase(); } function getEncodingFromCharacterSetIdentifier(chrs) { - const ident = chrs.split(' ')[0].toUpperCase(); + const ident = chrs.split(' ')[0].toUpperCase(); - // :TODO: fill in the rest!!! - return { - // level 1 - 'ASCII' : 'iso-646-1', - 'DUTCH' : 'iso-646', - 'FINNISH' : 'iso-646-10', - 'FRENCH' : 'iso-646', - 'CANADIAN' : 'iso-646', - 'GERMAN' : 'iso-646', - 'ITALIAN' : 'iso-646', - 'NORWEIG' : 'iso-646', - 'PORTU' : 'iso-646', - 'SPANISH' : 'iso-656', - 'SWEDISH' : 'iso-646-10', - 'SWISS' : 'iso-646', - 'UK' : 'iso-646', - 'ISO-10' : 'iso-646-10', + // :TODO: fill in the rest!!! + return { + // level 1 + 'ASCII' : 'iso-646-1', + 'DUTCH' : 'iso-646', + 'FINNISH' : 'iso-646-10', + 'FRENCH' : 'iso-646', + 'CANADIAN' : 'iso-646', + 'GERMAN' : 'iso-646', + 'ITALIAN' : 'iso-646', + 'NORWEIG' : 'iso-646', + 'PORTU' : 'iso-646', + 'SPANISH' : 'iso-656', + 'SWEDISH' : 'iso-646-10', + 'SWISS' : 'iso-646', + 'UK' : 'iso-646', + 'ISO-10' : 'iso-646-10', - // level 2 - 'CP437' : 'cp437', - 'CP850' : 'cp850', - 'CP852' : 'cp852', - 'CP866' : 'cp866', - 'CP848' : 'cp848', - 'CP1250' : 'cp1250', - 'CP1251' : 'cp1251', - 'CP1252' : 'cp1252', - 'CP10000' : 'macroman', - 'LATIN-1' : 'iso-8859-1', - 'LATIN-2' : 'iso-8859-2', - 'LATIN-5' : 'iso-8859-9', - 'LATIN-9' : 'iso-8859-15', + // level 2 + 'CP437' : 'cp437', + 'CP850' : 'cp850', + 'CP852' : 'cp852', + 'CP866' : 'cp866', + 'CP848' : 'cp848', + 'CP1250' : 'cp1250', + 'CP1251' : 'cp1251', + 'CP1252' : 'cp1252', + 'CP10000' : 'macroman', + 'LATIN-1' : 'iso-8859-1', + 'LATIN-2' : 'iso-8859-2', + 'LATIN-5' : 'iso-8859-9', + 'LATIN-9' : 'iso-8859-15', - // level 4 - 'UTF-8' : 'utf8', + // level 4 + 'UTF-8' : 'utf8', - // deprecated stuff - 'IBMPC' : 'cp1250', // :TODO: validate - '+7_FIDO' : 'cp866', - '+7' : 'cp866', - 'MAC' : 'macroman', // :TODO: validate + // deprecated stuff + 'IBMPC' : 'cp1250', // :TODO: validate + '+7_FIDO' : 'cp866', + '+7' : 'cp866', + 'MAC' : 'macroman', // :TODO: validate - }[ident]; + }[ident]; } \ No newline at end of file diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index d9921c96..eb45d993 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -15,154 +15,154 @@ exports.HorizontalMenuView = HorizontalMenuView; // :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView) function HorizontalMenuView(options) { - options.cursor = options.cursor || 'hide'; + options.cursor = options.cursor || 'hide'; - if(!_.isNumber(options.itemSpacing)) { - options.itemSpacing = 1; - } + if(!_.isNumber(options.itemSpacing)) { + options.itemSpacing = 1; + } - MenuView.call(this, options); + MenuView.call(this, options); - this.dimens.height = 1; // always the case + this.dimens.height = 1; // always the case - var self = this; + var self = this; - this.getSpacer = function() { - return new Array(self.itemSpacing + 1).join(' '); - }; + this.getSpacer = function() { + return new Array(self.itemSpacing + 1).join(' '); + }; - this.performAutoScale = function() { - if(self.autoScale.width) { - var spacer = self.getSpacer(); - var width = self.items.join(spacer).length + (spacer.length * 2); - assert(width <= self.client.term.termWidth - self.position.col); - self.dimens.width = width; - } - }; + this.performAutoScale = function() { + if(self.autoScale.width) { + var spacer = self.getSpacer(); + var width = self.items.join(spacer).length + (spacer.length * 2); + assert(width <= self.client.term.termWidth - self.position.col); + self.dimens.width = width; + } + }; - this.performAutoScale(); + this.performAutoScale(); - this.cachePositions = function() { - if(this.positionCacheExpired) { - var col = self.position.col; - var spacer = self.getSpacer(); + this.cachePositions = function() { + if(this.positionCacheExpired) { + var col = self.position.col; + var spacer = self.getSpacer(); - for(var i = 0; i < self.items.length; ++i) { - self.items[i].col = col; - col += spacer.length + self.items[i].text.length + spacer.length; - } - } + for(var i = 0; i < self.items.length; ++i) { + self.items[i].col = col; + col += spacer.length + self.items[i].text.length + spacer.length; + } + } - this.positionCacheExpired = false; - }; + this.positionCacheExpired = false; + }; - this.drawItem = function(index) { - assert(!this.positionCacheExpired); + this.drawItem = function(index) { + assert(!this.positionCacheExpired); - const item = self.items[index]; - if(!item) { - return; - } + const item = self.items[index]; + if(!item) { + return; + } - let text; - let sgr; - if(item.focused && self.hasFocusItems()) { - const focusItem = self.focusItems[index]; - text = focusItem ? focusItem.text : item.text; - sgr = ''; - } else if(this.complexItems) { - text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); - sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); - } else { - text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); - sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); - } + let text; + let sgr; + if(item.focused && self.hasFocusItems()) { + const focusItem = self.focusItems[index]; + text = focusItem ? focusItem.text : item.text; + sgr = ''; + } else if(this.complexItems) { + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } - const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2); + const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2); - self.client.term.write( - `${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}` - ); - }; + self.client.term.write( + `${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}` + ); + }; } require('util').inherits(HorizontalMenuView, MenuView); HorizontalMenuView.prototype.setHeight = function(height) { - height = parseInt(height, 10); - assert(1 === height); // nothing else allowed here - HorizontalMenuView.super_.prototype.setHeight(this, height); + height = parseInt(height, 10); + assert(1 === height); // nothing else allowed here + HorizontalMenuView.super_.prototype.setHeight(this, height); }; HorizontalMenuView.prototype.redraw = function() { - HorizontalMenuView.super_.prototype.redraw.call(this); + HorizontalMenuView.super_.prototype.redraw.call(this); - this.cachePositions(); + this.cachePositions(); - for(var i = 0; i < this.items.length; ++i) { - this.items[i].focused = this.focusedItemIndex === i; - this.drawItem(i); - } + for(var i = 0; i < this.items.length; ++i) { + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } }; HorizontalMenuView.prototype.setPosition = function(pos) { - HorizontalMenuView.super_.prototype.setPosition.call(this, pos); + HorizontalMenuView.super_.prototype.setPosition.call(this, pos); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; HorizontalMenuView.prototype.setFocus = function(focused) { - HorizontalMenuView.super_.prototype.setFocus.call(this, focused); + HorizontalMenuView.super_.prototype.setFocus.call(this, focused); - this.redraw(); + this.redraw(); }; HorizontalMenuView.prototype.setItems = function(items) { - HorizontalMenuView.super_.prototype.setItems.call(this, items); + HorizontalMenuView.super_.prototype.setItems.call(this, items); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; HorizontalMenuView.prototype.focusNext = function() { - if(this.items.length - 1 === this.focusedItemIndex) { - this.focusedItemIndex = 0; - } else { - this.focusedItemIndex++; - } + if(this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + } else { + this.focusedItemIndex++; + } - // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes - this.redraw(); + // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes + this.redraw(); - HorizontalMenuView.super_.prototype.focusNext.call(this); + HorizontalMenuView.super_.prototype.focusNext.call(this); }; HorizontalMenuView.prototype.focusPrevious = function() { - if(0 === this.focusedItemIndex) { - this.focusedItemIndex = this.items.length - 1; - } else { - this.focusedItemIndex--; - } + if(0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + } else { + this.focusedItemIndex--; + } - // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes - this.redraw(); + // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes + this.redraw(); - HorizontalMenuView.super_.prototype.focusPrevious.call(this); + HorizontalMenuView.super_.prototype.focusPrevious.call(this); }; HorizontalMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('left', key.name)) { - this.focusPrevious(); - } else if(this.isKeyMapped('right', key.name)) { - this.focusNext(); - } - } + if(key) { + if(this.isKeyMapped('left', key.name)) { + this.focusPrevious(); + } else if(this.isKeyMapped('right', key.name)) { + this.focusNext(); + } + } - HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key); + HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; HorizontalMenuView.prototype.getData = function() { - const item = this.getItem(this.focusedItemIndex); - return _.isString(item.data) ? item.data : this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; \ No newline at end of file diff --git a/core/key_entry_view.js b/core/key_entry_view.js index 304f8ef3..1d7ca905 100644 --- a/core/key_entry_view.js +++ b/core/key_entry_view.js @@ -9,69 +9,69 @@ const stylizeString = require('./string_util.js').stylizeString; const _ = require('lodash'); module.exports = class KeyEntryView extends View { - constructor(options) { - options.acceptsFocus = valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = valueWithDefault(options.acceptsInput, true); + constructor(options) { + options.acceptsFocus = valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = valueWithDefault(options.acceptsInput, true); - super(options); + super(options); - this.eatTabKey = options.eatTabKey || true; - this.caseInsensitive = options.caseInsensitive || true; + this.eatTabKey = options.eatTabKey || true; + this.caseInsensitive = options.caseInsensitive || true; - if(Array.isArray(options.keys)) { - if(this.caseInsensitive) { - this.keys = options.keys.map( k => k.toUpperCase() ); - } else { - this.keys = options.keys; - } - } - } + if(Array.isArray(options.keys)) { + if(this.caseInsensitive) { + this.keys = options.keys.map( k => k.toUpperCase() ); + } else { + this.keys = options.keys; + } + } + } - onKeyPress(ch, key) { - const drawKey = ch; + onKeyPress(ch, key) { + const drawKey = ch; - if(ch && this.caseInsensitive) { - ch = ch.toUpperCase(); - } + if(ch && this.caseInsensitive) { + ch = ch.toUpperCase(); + } - if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { - this.redraw(); // sets position - this.client.term.write(stylizeString(ch, this.textStyle)); - } + if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { + this.redraw(); // sets position + this.client.term.write(stylizeString(ch, this.textStyle)); + } - this.keyEntered = ch || key.name; + this.keyEntered = ch || key.name; - if(key && 'tab' === key.name && !this.eatTabKey) { - return this.emit('action', 'next', key); - } + if(key && 'tab' === key.name && !this.eatTabKey) { + return this.emit('action', 'next', key); + } - this.emit('action', 'accept'); - // NOTE: we don't call super here. KeyEntryView is a special snowflake. - } + this.emit('action', 'accept'); + // NOTE: we don't call super here. KeyEntryView is a special snowflake. + } - setPropertyValue(propName, propValue) { - switch(propName) { - case 'eatTabKey' : - if(_.isBoolean(propValue)) { - this.eatTabKey = propValue; - } - break; + setPropertyValue(propName, propValue) { + switch(propName) { + case 'eatTabKey' : + if(_.isBoolean(propValue)) { + this.eatTabKey = propValue; + } + break; - case 'caseInsensitive' : - if(_.isBoolean(propValue)) { - this.caseInsensitive = propValue; - } - break; + case 'caseInsensitive' : + if(_.isBoolean(propValue)) { + this.caseInsensitive = propValue; + } + break; - case 'keys' : - if(Array.isArray(propValue)) { - this.keys = propValue; - } - break; - } + case 'keys' : + if(Array.isArray(propValue)) { + this.keys = propValue; + } + break; + } - super.setPropertyValue(propName, propValue); - } + super.setPropertyValue(propName, propValue); + } - getData() { return this.keyEntered; } + getData() { return this.keyEntered; } }; \ No newline at end of file diff --git a/core/last_callers.js b/core/last_callers.js index 1bc1f422..88b0b716 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -24,128 +24,128 @@ const _ = require('lodash'); */ exports.moduleInfo = { - name : 'Last Callers', - desc : 'Last callers to the system', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.lastcallers' + name : 'Last Callers', + desc : 'Last callers to the system', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.lastcallers' }; const MciCodeIds = { - CallerList : 1, + CallerList : 1, }; exports.getModule = class LastCallersModule extends MenuModule { - constructor(options) { - super(options); - } + constructor(options) { + super(options); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - let loginHistory; - let callersView; + let loginHistory; + let callersView; - async.series( - [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, - }; + async.series( + [ + function loadFromConfig(callback) { + const loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + noInput : true, + }; - vc.loadFromMenuConfig(loadOpts, callback); - }, - function fetchHistory(callback) { - callersView = vc.getView(MciCodeIds.CallerList); + vc.loadFromMenuConfig(loadOpts, callback); + }, + function fetchHistory(callback) { + callersView = vc.getView(MciCodeIds.CallerList); - // fetch up - StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { - loginHistory = lh; + // fetch up + StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { + loginHistory = lh; - if(self.menuConfig.config.hideSysOpLogin) { - const noOpLoginHistory = loginHistory.filter(lh => { - return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId - }); + if(self.menuConfig.config.hideSysOpLogin) { + const noOpLoginHistory = loginHistory.filter(lh => { + return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId + }); - // - // If we have enough items to display, or hideSysOpLogin is set to 'always', - // then set loginHistory to our filtered list. Else, we'll leave it be. - // - if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) { - loginHistory = noOpLoginHistory; - } - } + // + // If we have enough items to display, or hideSysOpLogin is set to 'always', + // then set loginHistory to our filtered list. Else, we'll leave it be. + // + if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) { + loginHistory = noOpLoginHistory; + } + } - // - // Finally, we need to trim up the list to the needed size - // - loginHistory = loginHistory.slice(0, callersView.dimens.height); + // + // Finally, we need to trim up the list to the needed size + // + loginHistory = loginHistory.slice(0, callersView.dimens.height); - return callback(err); - }); - }, - function getUserNamesAndProperties(callback) { - const getPropOpts = { - names : [ 'location', 'affiliation' ] - }; + return callback(err); + }); + }, + function getUserNamesAndProperties(callback) { + const getPropOpts = { + names : [ 'location', 'affiliation' ] + }; - const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; + const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; - async.each( - loginHistory, - (item, next) => { - item.userId = parseInt(item.log_value); - item.ts = moment(item.timestamp).format(dateTimeFormat); + async.each( + loginHistory, + (item, next) => { + item.userId = parseInt(item.log_value); + item.ts = moment(item.timestamp).format(dateTimeFormat); - User.getUserName(item.userId, (err, userName) => { - if(err) { - item.deleted = true; - return next(null); - } else { - item.userName = userName || 'N/A'; + User.getUserName(item.userId, (err, userName) => { + if(err) { + item.deleted = true; + return next(null); + } else { + item.userName = userName || 'N/A'; - User.loadProperties(item.userId, getPropOpts, (err, props) => { - if(!err && props) { - item.location = props.location || 'N/A'; - item.affiliation = item.affils = (props.affiliation || 'N/A'); - } else { - item.location = 'N/A'; - item.affiliation = item.affils = 'N/A'; - } - return next(null); - }); - } - }); - }, - err => { - loginHistory = loginHistory.filter(lh => true !== lh.deleted); - return callback(err); - } - ); - }, - function populateList(callback) { - const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}'; + User.loadProperties(item.userId, getPropOpts, (err, props) => { + if(!err && props) { + item.location = props.location || 'N/A'; + item.affiliation = item.affils = (props.affiliation || 'N/A'); + } else { + item.location = 'N/A'; + item.affiliation = item.affils = 'N/A'; + } + return next(null); + }); + } + }); + }, + err => { + loginHistory = loginHistory.filter(lh => true !== lh.deleted); + return callback(err); + } + ); + }, + function populateList(callback) { + const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}'; - callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) ); + callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) ); - callersView.redraw(); - return callback(null); - } - ], - (err) => { - if(err) { - self.client.log.error( { error : err.toString() }, 'Error loading last callers'); - } - cb(err); - } - ); - }); - } + callersView.redraw(); + return callback(null); + } + ], + (err) => { + if(err) { + self.client.log.error( { error : err.toString() }, 'Error loading last callers'); + } + cb(err); + } + ); + }); + } }; diff --git a/core/listening_server.js b/core/listening_server.js index 94efd475..3678ff3f 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -14,51 +14,51 @@ exports.shutdown = shutdown; exports.getServer = getServer; function startup(cb) { - return startListening(cb); + return startListening(cb); } function shutdown(cb) { - return cb(null); + return cb(null); } function getServer(packageName) { - return listeningServers[packageName]; + return listeningServers[packageName]; } function startListening(cb) { - const moduleUtil = require('./module_util.js'); // late load so we get Config + const moduleUtil = require('./module_util.js'); // late load so we get Config - async.each( [ 'login', 'content' ], (category, next) => { - moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { - // :TODO: use enig error here! - if(err) { - if('EENIGMODDISABLED' === err.code) { - logger.log.debug(err.message); - } else { - logger.log.info( { err : err }, 'Failed loading module'); - } - return; - } + async.each( [ 'login', 'content' ], (category, next) => { + moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { + // :TODO: use enig error here! + if(err) { + if('EENIGMODDISABLED' === err.code) { + logger.log.debug(err.message); + } else { + logger.log.info( { err : err }, 'Failed loading module'); + } + return; + } - const moduleInst = new module.getModule(); - try { - moduleInst.createServer(); - if(!moduleInst.listen()) { - throw new Error('Failed listening'); - } + const moduleInst = new module.getModule(); + try { + moduleInst.createServer(); + if(!moduleInst.listen()) { + throw new Error('Failed listening'); + } - listeningServers[module.moduleInfo.packageName] = { - instance : moduleInst, - info : module.moduleInfo, - }; + listeningServers[module.moduleInfo.packageName] = { + instance : moduleInst, + info : module.moduleInfo, + }; - } catch(e) { - logger.log.error(e, 'Exception caught creating server!'); - } - }, err => { - return next(err); - }); - }, err => { - return cb(err); - }); + } catch(e) { + logger.log.error(e, 'Exception caught creating server!'); + } + }, err => { + return next(err); + }); + }, err => { + return cb(err); + }); } diff --git a/core/logger.js b/core/logger.js index c9a75faf..8b95c821 100644 --- a/core/logger.js +++ b/core/logger.js @@ -9,66 +9,66 @@ const _ = require('lodash'); module.exports = class Log { - static init() { - const Config = require('./config.js').get(); - const logPath = Config.paths.logs; + static init() { + const Config = require('./config.js').get(); + const logPath = Config.paths.logs; - const err = this.checkLogPath(logPath); - if(err) { - console.error(err.message); // eslint-disable-line no-console - return process.exit(); - } + const err = this.checkLogPath(logPath); + if(err) { + console.error(err.message); // eslint-disable-line no-console + return process.exit(); + } - const logStreams = []; - if(_.isObject(Config.logging.rotatingFile)) { - Config.logging.rotatingFile.path = paths.join(logPath, Config.logging.rotatingFile.fileName); - logStreams.push(Config.logging.rotatingFile); - } + const logStreams = []; + if(_.isObject(Config.logging.rotatingFile)) { + Config.logging.rotatingFile.path = paths.join(logPath, Config.logging.rotatingFile.fileName); + logStreams.push(Config.logging.rotatingFile); + } - const serializers = { - err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. - }; + const serializers = { + err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. + }; - // try to remove sensitive info by default, e.g. 'password' fields - [ 'formData', 'formValue' ].forEach(keyName => { - serializers[keyName] = (fd) => Log.hideSensitive(fd); - }); + // try to remove sensitive info by default, e.g. 'password' fields + [ 'formData', 'formValue' ].forEach(keyName => { + serializers[keyName] = (fd) => Log.hideSensitive(fd); + }); - this.log = bunyan.createLogger({ - name : 'ENiGMA½ BBS', - streams : logStreams, - serializers : serializers, - }); - } + this.log = bunyan.createLogger({ + name : 'ENiGMA½ BBS', + streams : logStreams, + serializers : serializers, + }); + } - static checkLogPath(logPath) { - try { - if(!fs.statSync(logPath).isDirectory()) { - return new Error(`${logPath} is not a directory`); - } + static checkLogPath(logPath) { + try { + if(!fs.statSync(logPath).isDirectory()) { + return new Error(`${logPath} is not a directory`); + } - return null; - } catch(e) { - if('ENOENT' === e.code) { - return new Error(`${logPath} does not exist`); - } - return e; - } - } + return null; + } catch(e) { + if('ENOENT' === e.code) { + return new Error(`${logPath} does not exist`); + } + return e; + } + } - static hideSensitive(obj) { - try { - // - // Use a regexp -- we don't know how nested fields we want to seek and destroy may be - // - return JSON.parse( - JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { - return `"${valueName}":"********"`; - }) - ); - } catch(e) { - // be safe and return empty obj! - return {}; - } - } + static hideSensitive(obj) { + try { + // + // Use a regexp -- we don't know how nested fields we want to seek and destroy may be + // + return JSON.parse( + JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { + return `"${valueName}":"********"`; + }) + ); + } catch(e) { + // be safe and return empty obj! + return {}; + } + } }; diff --git a/core/login_server_module.js b/core/login_server_module.js index 72958a0c..ef4e712e 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -11,77 +11,77 @@ const clientConns = require('./client_connections.js'); const _ = require('lodash'); module.exports = class LoginServerModule extends ServerModule { - constructor() { - super(); - } + constructor() { + super(); + } - // :TODO: we need to max connections -- e.g. from config 'maxConnections' + // :TODO: we need to max connections -- e.g. from config 'maxConnections' - prepareClient(client, cb) { - const theme = require('./theme.js'); + prepareClient(client, cb) { + const theme = require('./theme.js'); - // - // Choose initial theme before we have user context - // - if('*' === conf.config.preLoginTheme) { - client.user.properties.theme_id = theme.getRandomTheme() || ''; - } else { - client.user.properties.theme_id = conf.config.preLoginTheme; - } + // + // Choose initial theme before we have user context + // + if('*' === conf.config.preLoginTheme) { + client.user.properties.theme_id = theme.getRandomTheme() || ''; + } else { + client.user.properties.theme_id = conf.config.preLoginTheme; + } - theme.setClientTheme(client, client.user.properties.theme_id); - return cb(null); // note: currently useless to use cb here - but this may change...again... - } + theme.setClientTheme(client, client.user.properties.theme_id); + return cb(null); // note: currently useless to use cb here - but this may change...again... + } - handleNewClient(client, clientSock, modInfo) { - // - // Start tracking the client. We'll assign it an ID which is - // just the index in our connections array. - // - if(_.isUndefined(client.session)) { - client.session = {}; - } + handleNewClient(client, clientSock, modInfo) { + // + // Start tracking the client. We'll assign it an ID which is + // just the index in our connections array. + // + if(_.isUndefined(client.session)) { + client.session = {}; + } - client.session.serverName = modInfo.name; - client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); + client.session.serverName = modInfo.name; + client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); - clientConns.addNewClient(client, clientSock); + clientConns.addNewClient(client, clientSock); - client.on('ready', readyOptions => { + client.on('ready', readyOptions => { - client.startIdleMonitor(); + client.startIdleMonitor(); - // Go to module -- use default error handler - this.prepareClient(client, () => { - require('./connect.js').connectEntry(client, readyOptions.firstMenu); - }); - }); + // Go to module -- use default error handler + this.prepareClient(client, () => { + require('./connect.js').connectEntry(client, readyOptions.firstMenu); + }); + }); - client.on('end', () => { - clientConns.removeClient(client); - }); + client.on('end', () => { + clientConns.removeClient(client); + }); - client.on('error', err => { - logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message); - }); + client.on('error', err => { + logger.log.info({ clientId : client.session.id }, 'Connection error: %s' % err.message); + }); - client.on('close', err => { - const logFunc = err ? logger.log.info : logger.log.debug; - logFunc( { clientId : client.session.id }, 'Connection closed'); + client.on('close', err => { + const logFunc = err ? logger.log.info : logger.log.debug; + logFunc( { clientId : client.session.id }, 'Connection closed'); - clientConns.removeClient(client); - }); + clientConns.removeClient(client); + }); - client.on('idle timeout', () => { - client.log.info('User idle timeout expired'); + client.on('idle timeout', () => { + client.log.info('User idle timeout expired'); - client.menuStack.goto('idleLogoff', err => { - if(err) { - // likely just doesn't exist - client.term.write('\nIdle timeout expired. Goodbye!\n'); - client.end(); - } - }); - }); - } + client.menuStack.goto('idleLogoff', err => { + if(err) { + // likely just doesn't exist + client.term.write('\nIdle timeout expired. Goodbye!\n'); + client.end(); + } + }); + }); + } }; diff --git a/core/mail_packet.js b/core/mail_packet.js index fbbb3e76..32b85a06 100644 --- a/core/mail_packet.js +++ b/core/mail_packet.js @@ -8,29 +8,29 @@ var _ = require('lodash'); module.exports = MailPacket; function MailPacket(options) { - events.EventEmitter.call(this); + events.EventEmitter.call(this); - // map of network name -> address obj ( { zone, net, node, point, domain } ) - this.nodeAddresses = options.nodeAddresses || {}; + // map of network name -> address obj ( { zone, net, node, point, domain } ) + this.nodeAddresses = options.nodeAddresses || {}; } require('util').inherits(MailPacket, events.EventEmitter); MailPacket.prototype.read = function(options) { - // - // options.packetPath | opts.packetBuffer: supplies a path-to-file - // or a buffer containing packet data - // - // emits 'message' event per message read - // - assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer)); + // + // options.packetPath | opts.packetBuffer: supplies a path-to-file + // or a buffer containing packet data + // + // emits 'message' event per message read + // + assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer)); }; MailPacket.prototype.write = function(options) { - // - // options.messages[]: array of message(s) to create packets from - // - // emits 'packet' event per packet constructed - // - assert(_.isArray(options.messages)); + // + // options.messages[]: array of message(s) to create packets from + // + // emits 'packet' event per packet constructed + // + assert(_.isArray(options.messages)); }; \ No newline at end of file diff --git a/core/mail_util.js b/core/mail_util.js index 4e959389..6822e78e 100644 --- a/core/mail_util.js +++ b/core/mail_util.js @@ -22,60 +22,60 @@ const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+")) Bar { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' } */ function getAddressedToInfo(input) { - input = input.trim(); + input = input.trim(); - const firstAtPos = input.indexOf('@'); + const firstAtPos = input.indexOf('@'); - if(firstAtPos < 0) { - let addr = Address.fromString(input); - if(Address.isValidAddress(addr)) { - return { flavor : Message.AddressFlavor.FTN, remote : input }; - } + if(firstAtPos < 0) { + let addr = Address.fromString(input); + if(Address.isValidAddress(addr)) { + return { flavor : Message.AddressFlavor.FTN, remote : input }; + } - const lessThanPos = input.indexOf('<'); - if(lessThanPos < 0) { - return { name : input, flavor : Message.AddressFlavor.Local }; - } + const lessThanPos = input.indexOf('<'); + if(lessThanPos < 0) { + return { name : input, flavor : Message.AddressFlavor.Local }; + } - const greaterThanPos = input.indexOf('>'); - if(greaterThanPos < lessThanPos) { - return { name : input, flavor : Message.AddressFlavor.Local }; - } + const greaterThanPos = input.indexOf('>'); + if(greaterThanPos < lessThanPos) { + return { name : input, flavor : Message.AddressFlavor.Local }; + } - addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos)); - if(Address.isValidAddress(addr)) { - return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; - } + addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos)); + if(Address.isValidAddress(addr)) { + return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; + } - return { name : input, flavor : Message.AddressFlavor.Local }; - } + return { name : input, flavor : Message.AddressFlavor.Local }; + } - const lessThanPos = input.indexOf('<'); - const greaterThanPos = input.indexOf('>'); - if(lessThanPos > 0 && greaterThanPos > lessThanPos) { - const addr = input.slice(lessThanPos + 1, greaterThanPos); - const m = addr.match(EMAIL_REGEX); - if(m) { - return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr }; - } + const lessThanPos = input.indexOf('<'); + const greaterThanPos = input.indexOf('>'); + if(lessThanPos > 0 && greaterThanPos > lessThanPos) { + const addr = input.slice(lessThanPos + 1, greaterThanPos); + const m = addr.match(EMAIL_REGEX); + if(m) { + return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr }; + } - return { name : input, flavor : Message.AddressFlavor.Local }; - } + return { name : input, flavor : Message.AddressFlavor.Local }; + } - let m = input.match(EMAIL_REGEX); - if(m) { - return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input }; - } + let m = input.match(EMAIL_REGEX); + if(m) { + return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input }; + } - let addr = Address.fromString(input); // 5D? - if(Address.isValidAddress(addr)) { - return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ; - } + let addr = Address.fromString(input); // 5D? + if(Address.isValidAddress(addr)) { + return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ; + } - addr = Address.fromString(input.slice(firstAtPos + 1).trim()); - if(Address.isValidAddress(addr)) { - return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; - } + addr = Address.fromString(input.slice(firstAtPos + 1).trim()); + if(Address.isValidAddress(addr)) { + return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; + } - return { name : input, flavor : Message.AddressFlavor.Local }; + return { name : input, flavor : Message.AddressFlavor.Local }; } diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index 2c0b6021..417b7928 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -28,181 +28,181 @@ exports.MaskEditTextView = MaskEditTextView; // function MaskEditTextView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); - options.resizable = false; + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); + options.resizable = false; - TextView.call(this, options); + TextView.call(this, options); - this.cursorPos = { x : 0 }; - this.patternArrayPos = 0; + this.cursorPos = { x : 0 }; + this.patternArrayPos = 0; - var self = this; + var self = this; - this.maskPattern = options.maskPattern || ''; + this.maskPattern = options.maskPattern || ''; - this.clientBackspace = function() { - var fillCharSGR = this.getStyleSGR(3) || this.getSGR(); - this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR()); - }; + this.clientBackspace = function() { + var fillCharSGR = this.getStyleSGR(3) || this.getSGR(); + this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR()); + }; - this.drawText = function(s) { - var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.drawText = function(s) { + var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - assert(textToDraw.length <= self.patternArray.length); + assert(textToDraw.length <= self.patternArray.length); - // draw out the text we have so far - var i = 0; - var t = 0; - while(i < self.patternArray.length) { - if(_.isRegExp(self.patternArray[i])) { - if(t < textToDraw.length) { - self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]); - t++; - } else { - self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar); - } - } else { - var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || ''); - self.client.term.write(styleSgr + self.maskPattern[i]); - } - i++; - } - }; + // draw out the text we have so far + var i = 0; + var t = 0; + while(i < self.patternArray.length) { + if(_.isRegExp(self.patternArray[i])) { + if(t < textToDraw.length) { + self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]); + t++; + } else { + self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar); + } + } else { + var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || ''); + self.client.term.write(styleSgr + self.maskPattern[i]); + } + i++; + } + }; - this.buildPattern = function() { - self.patternArray = []; - self.maxLength = 0; + this.buildPattern = function() { + self.patternArray = []; + self.maxLength = 0; - for(var i = 0; i < self.maskPattern.length; i++) { - // :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark! - if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) { - self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]); - ++self.maxLength; - } else { - self.patternArray.push(self.maskPattern[i]); - } - } - }; + for(var i = 0; i < self.maskPattern.length; i++) { + // :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark! + if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) { + self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]); + ++self.maxLength; + } else { + self.patternArray.push(self.maskPattern[i]); + } + } + }; - this.getEndOfTextColumn = function() { - return this.position.col + this.patternArrayPos; - }; + this.getEndOfTextColumn = function() { + return this.position.col + this.patternArrayPos; + }; - this.buildPattern(); + this.buildPattern(); } require('util').inherits(MaskEditTextView, TextView); MaskEditTextView.maskPatternCharacterRegEx = { - '#' : /[0-9]/, // Numeric - 'A' : /[a-zA-Z]/, // Alpha - '@' : /[0-9a-zA-Z]/, // Alphanumeric - '&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 + '#' : /[0-9]/, // Numeric + 'A' : /[a-zA-Z]/, // Alpha + '@' : /[0-9a-zA-Z]/, // Alphanumeric + '&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 }; MaskEditTextView.prototype.setText = function(text) { - MaskEditTextView.super_.prototype.setText.call(this, text); + MaskEditTextView.super_.prototype.setText.call(this, text); - if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() - this.patternArrayPos = this.patternArray.length; - } + if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() + this.patternArrayPos = this.patternArray.length; + } }; MaskEditTextView.prototype.setMaskPattern = function(pattern) { - this.dimens.width = pattern.length; + this.dimens.width = pattern.length; - this.maskPattern = pattern; - this.buildPattern(); + this.maskPattern = pattern; + this.buildPattern(); }; MaskEditTextView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('backspace', key.name)) { - if(this.text.length > 0) { - this.patternArrayPos--; - assert(this.patternArrayPos >= 0); + if(key) { + if(this.isKeyMapped('backspace', key.name)) { + if(this.text.length > 0) { + this.patternArrayPos--; + assert(this.patternArrayPos >= 0); - if(_.isRegExp(this.patternArray[this.patternArrayPos])) { - this.text = this.text.substr(0, this.text.length - 1); - this.clientBackspace(); - } else { - while(this.patternArrayPos > 0) { - if(_.isRegExp(this.patternArray[this.patternArrayPos])) { - this.text = this.text.substr(0, this.text.length - 1); - this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); - this.clientBackspace(); - break; - } - this.patternArrayPos--; - } - } - } + if(_.isRegExp(this.patternArray[this.patternArrayPos])) { + this.text = this.text.substr(0, this.text.length - 1); + this.clientBackspace(); + } else { + while(this.patternArrayPos > 0) { + if(_.isRegExp(this.patternArray[this.patternArrayPos])) { + this.text = this.text.substr(0, this.text.length - 1); + this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); + this.clientBackspace(); + break; + } + this.patternArrayPos--; + } + } + } - return; - } else if(this.isKeyMapped('clearLine', key.name)) { - this.text = ''; - this.patternArrayPos = 0; - this.setFocus(true); // redraw + adjust cursor + return; + } else if(this.isKeyMapped('clearLine', key.name)) { + this.text = ''; + this.patternArrayPos = 0; + this.setFocus(true); // redraw + adjust cursor - return; - } - } + return; + } + } - if(ch && strUtil.isPrintable(ch)) { - if(this.text.length < this.maxLength) { - ch = strUtil.stylizeString(ch, this.textStyle); + if(ch && strUtil.isPrintable(ch)) { + if(this.text.length < this.maxLength) { + ch = strUtil.stylizeString(ch, this.textStyle); - if(!ch.match(this.patternArray[this.patternArrayPos])) { - return; - } + if(!ch.match(this.patternArray[this.patternArrayPos])) { + return; + } - this.text += ch; - this.patternArrayPos++; + this.text += ch; + this.patternArrayPos++; - while(this.patternArrayPos < this.patternArray.length && + while(this.patternArrayPos < this.patternArray.length && !_.isRegExp(this.patternArray[this.patternArrayPos])) - { - this.patternArrayPos++; - } + { + this.patternArrayPos++; + } - this.redraw(); - this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); - } - } + this.redraw(); + this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); + } + } - MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key); + MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key); }; MaskEditTextView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'maskPattern' : this.setMaskPattern(value); break; - } + switch(propName) { + case 'maskPattern' : this.setMaskPattern(value); break; + } - MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); + MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); }; MaskEditTextView.prototype.getData = function() { - var rawData = MaskEditTextView.super_.prototype.getData.call(this); + var rawData = MaskEditTextView.super_.prototype.getData.call(this); - if(!rawData || 0 === rawData.length) { - return rawData; - } + if(!rawData || 0 === rawData.length) { + return rawData; + } - var data = ''; + var data = ''; - assert(rawData.length <= this.patternArray.length); + assert(rawData.length <= this.patternArray.length); - var p = 0; - for(var i = 0; i < this.patternArray.length; ++i) { - if(_.isRegExp(this.patternArray[i])) { - data += rawData[p++]; - } else { - data += this.patternArray[i]; - } - } + var p = 0; + for(var i = 0; i < this.patternArray.length; ++i) { + if(_.isRegExp(this.patternArray[i])) { + data += rawData[p++]; + } else { + data += this.patternArray[i]; + } + } - return data; + return data; }; diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index eab2787c..3bc333ae 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -22,186 +22,186 @@ const _ = require('lodash'); exports.MCIViewFactory = MCIViewFactory; function MCIViewFactory(client) { - this.client = client; + this.client = client; } MCIViewFactory.UserViewCodes = [ - 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', + 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', - // - // XY is a special MCI code that allows finding positions - // and counts for key lookup, but does not explicitly - // represent a visible View on it's own - // - 'XY', + // + // XY is a special MCI code that allows finding positions + // and counts for key lookup, but does not explicitly + // represent a visible View on it's own + // + 'XY', ]; MCIViewFactory.prototype.createFromMCI = function(mci) { - assert(mci.code); - assert(mci.id > 0); - assert(mci.position); + assert(mci.code); + assert(mci.id > 0); + assert(mci.position); - var view; - var options = { - client : this.client, - id : mci.id, - ansiSGR : mci.SGR, - ansiFocusSGR : mci.focusSGR, - position : { row : mci.position[0], col : mci.position[1] }, - }; + var view; + var options = { + client : this.client, + id : mci.id, + ansiSGR : mci.SGR, + ansiFocusSGR : mci.focusSGR, + position : { row : mci.position[0], col : mci.position[1] }, + }; - // :TODO: These should use setPropertyValue()! - function setOption(pos, name) { - if(mci.args.length > pos && mci.args[pos].length > 0) { - options[name] = mci.args[pos]; - } - } + // :TODO: These should use setPropertyValue()! + function setOption(pos, name) { + if(mci.args.length > pos && mci.args[pos].length > 0) { + options[name] = mci.args[pos]; + } + } - function setWidth(pos) { - if(mci.args.length > pos && mci.args[pos].length > 0) { - if(!_.isObject(options.dimens)) { - options.dimens = {}; - } - options.dimens.width = parseInt(mci.args[pos], 10); - } - } + function setWidth(pos) { + if(mci.args.length > pos && mci.args[pos].length > 0) { + if(!_.isObject(options.dimens)) { + options.dimens = {}; + } + options.dimens.width = parseInt(mci.args[pos], 10); + } + } - function setFocusOption(pos, name) { - if(mci.focusArgs && mci.focusArgs.length > pos && mci.focusArgs[pos].length > 0) { - options[name] = mci.focusArgs[pos]; - } - } + function setFocusOption(pos, name) { + if(mci.focusArgs && mci.focusArgs.length > pos && mci.focusArgs[pos].length > 0) { + options[name] = mci.focusArgs[pos]; + } + } - // - // Note: Keep this in sync with UserViewCodes above! - // - switch(mci.code) { - // Text Label (Text View) - case 'TL' : - setOption(0, 'textStyle'); - setOption(1, 'justify'); - setWidth(2); + // + // Note: Keep this in sync with UserViewCodes above! + // + switch(mci.code) { + // Text Label (Text View) + case 'TL' : + setOption(0, 'textStyle'); + setOption(1, 'justify'); + setWidth(2); - view = new TextView(options); - break; + view = new TextView(options); + break; - // Edit Text - case 'ET' : - setWidth(0); + // Edit Text + case 'ET' : + setWidth(0); - setOption(1, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setOption(1, 'textStyle'); + setFocusOption(0, 'focusTextStyle'); - view = new EditTextView(options); - break; + view = new EditTextView(options); + break; - // Masked Edit Text - case 'ME' : - setOption(0, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + // Masked Edit Text + case 'ME' : + setOption(0, 'textStyle'); + setFocusOption(0, 'focusTextStyle'); - view = new MaskEditTextView(options); - break; + view = new MaskEditTextView(options); + break; - // Multi Line Edit Text - case 'MT' : - // :TODO: apply params - view = new MultiLineEditTextView(options); - break; + // Multi Line Edit Text + case 'MT' : + // :TODO: apply params + view = new MultiLineEditTextView(options); + break; - // Pre-defined Label (Text View) - // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove - case 'PL' : - if(mci.args.length > 0) { - options.text = getPredefinedMCIValue(this.client, mci.args[0]); - if(options.text) { - setOption(1, 'textStyle'); - setOption(2, 'justify'); - setWidth(3); + // Pre-defined Label (Text View) + // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove + case 'PL' : + if(mci.args.length > 0) { + options.text = getPredefinedMCIValue(this.client, mci.args[0]); + if(options.text) { + setOption(1, 'textStyle'); + setOption(2, 'justify'); + setWidth(3); - view = new TextView(options); - } - } - break; + view = new TextView(options); + } + } + break; - // Button - case 'BT' : - if(mci.args.length > 0) { - options.dimens = { width : parseInt(mci.args[0], 10) }; - } + // Button + case 'BT' : + if(mci.args.length > 0) { + options.dimens = { width : parseInt(mci.args[0], 10) }; + } - setOption(1, 'textStyle'); - setOption(2, 'justify'); + setOption(1, 'textStyle'); + setOption(2, 'justify'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); - view = new ButtonView(options); - break; + view = new ButtonView(options); + break; - // Vertial Menu - case 'VM' : - setOption(0, 'itemSpacing'); - setOption(1, 'justify'); - setOption(2, 'textStyle'); + // Vertial Menu + case 'VM' : + setOption(0, 'itemSpacing'); + setOption(1, 'justify'); + setOption(2, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); - view = new VerticalMenuView(options); - break; + view = new VerticalMenuView(options); + break; - // Horizontal Menu - case 'HM' : - setOption(0, 'itemSpacing'); - setOption(1, 'textStyle'); + // Horizontal Menu + case 'HM' : + setOption(0, 'itemSpacing'); + setOption(1, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); - view = new HorizontalMenuView(options); - break; + view = new HorizontalMenuView(options); + break; - case 'SM' : - setOption(0, 'textStyle'); - setOption(1, 'justify'); + case 'SM' : + setOption(0, 'textStyle'); + setOption(1, 'justify'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); - view = new SpinnerMenuView(options); - break; + view = new SpinnerMenuView(options); + break; - case 'TM' : - if(mci.args.length > 0) { - var styleSG1 = { fg : parseInt(mci.args[0], 10) }; - if(mci.args.length > 1) { - styleSG1.bg = parseInt(mci.args[1], 10); - } - options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true); - } + case 'TM' : + if(mci.args.length > 0) { + var styleSG1 = { fg : parseInt(mci.args[0], 10) }; + if(mci.args.length > 1) { + styleSG1.bg = parseInt(mci.args[1], 10); + } + options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true); + } - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); - view = new ToggleMenuView(options); - break; + view = new ToggleMenuView(options); + break; - case 'KE' : - view = new KeyEntryView(options); - break; + case 'KE' : + view = new KeyEntryView(options); + break; - default : - options.text = getPredefinedMCIValue(this.client, mci.code); - if(_.isString(options.text)) { - setWidth(0); + default : + options.text = getPredefinedMCIValue(this.client, mci.code); + if(_.isString(options.text)) { + setWidth(0); - setOption(1, 'textStyle'); - setOption(2, 'justify'); + setOption(1, 'textStyle'); + setOption(2, 'justify'); - view = new TextView(options); - } - break; - } + view = new TextView(options); + } + break; + } - if(view) { - view.mciCode = mci.code; - } + if(view) { + view.mciCode = mci.code; + } - return view; + return view; }; diff --git a/core/menu_module.js b/core/menu_module.js index 20680354..520d30e9 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -19,358 +19,358 @@ const _ = require('lodash'); exports.MenuModule = class MenuModule extends PluginModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.menuName = options.menuName; - this.menuConfig = options.menuConfig; - this.client = options.client; - this.menuConfig.options = options.menuConfig.options || {}; - this.menuMethods = {}; // methods called from @method's - this.menuConfig.config = this.menuConfig.config || {}; + this.menuName = options.menuName; + this.menuConfig = options.menuConfig; + this.client = options.client; + this.menuConfig.options = options.menuConfig.options || {}; + this.menuMethods = {}; // methods called from @method's + this.menuConfig.config = this.menuConfig.config || {}; - this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; + this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; - this.viewControllers = {}; - } + this.viewControllers = {}; + } - enter() { - this.initSequence(); - } + enter() { + this.initSequence(); + } - leave() { - this.detachViewControllers(); - } + leave() { + this.detachViewControllers(); + } - initSequence() { - const self = this; - const mciData = {}; - let pausePosition; + initSequence() { + const self = this; + const mciData = {}; + let pausePosition; - async.series( - [ - function beforeDisplayArt(callback) { - self.beforeArt(callback); - }, - function displayMenuArt(callback) { - if(!_.isString(self.menuConfig.art)) { - return callback(null); - } + async.series( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function displayMenuArt(callback) { + if(!_.isString(self.menuConfig.art)) { + return callback(null); + } - self.displayAsset( - self.menuConfig.art, - self.menuConfig.options, - (err, artData) => { - if(err) { - self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } ); - } else { - mciData.menu = artData.mciMap; - } + self.displayAsset( + self.menuConfig.art, + self.menuConfig.options, + (err, artData) => { + if(err) { + self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } ); + } else { + mciData.menu = artData.mciMap; + } - return callback(null); // any errors are non-fatal - } - ); - }, - function moveToPromptLocation(callback) { - if(self.menuConfig.prompt) { - // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements - } + return callback(null); // any errors are non-fatal + } + ); + }, + function moveToPromptLocation(callback) { + if(self.menuConfig.prompt) { + // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements + } - return callback(null); - }, - function displayPromptArt(callback) { - if(!_.isString(self.menuConfig.prompt)) { - return callback(null); - } + return callback(null); + }, + function displayPromptArt(callback) { + if(!_.isString(self.menuConfig.prompt)) { + return callback(null); + } - if(!_.isObject(self.menuConfig.promptConfig)) { - return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found')); - } + if(!_.isObject(self.menuConfig.promptConfig)) { + return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found')); + } - self.displayAsset( - self.menuConfig.promptConfig.art, - self.menuConfig.options, - (err, artData) => { - if(artData) { - mciData.prompt = artData.mciMap; - } - return callback(err); // pass err here; prompts *must* have art - } - ); - }, - function recordCursorPosition(callback) { - if(!self.shouldPause()) { - return callback(null); // cursor position not needed - } + self.displayAsset( + self.menuConfig.promptConfig.art, + self.menuConfig.options, + (err, artData) => { + if(artData) { + mciData.prompt = artData.mciMap; + } + return callback(err); // pass err here; prompts *must* have art + } + ); + }, + function recordCursorPosition(callback) { + if(!self.shouldPause()) { + return callback(null); // cursor position not needed + } - self.client.once('cursor position report', pos => { - pausePosition = { row : pos[0], col : 1 }; - self.client.log.trace('After art position recorded', pausePosition ); - return callback(null); - }); + self.client.once('cursor position report', pos => { + pausePosition = { row : pos[0], col : 1 }; + self.client.log.trace('After art position recorded', pausePosition ); + return callback(null); + }); - self.client.term.rawWrite(ansi.queryPos()); - }, - function afterArtDisplayed(callback) { - return self.mciReady(mciData, callback); - }, - function displayPauseIfRequested(callback) { - if(!self.shouldPause()) { - return callback(null); - } + self.client.term.rawWrite(ansi.queryPos()); + }, + function afterArtDisplayed(callback) { + return self.mciReady(mciData, callback); + }, + function displayPauseIfRequested(callback) { + if(!self.shouldPause()) { + return callback(null); + } - return self.pausePrompt(pausePosition, callback); - }, - function finishAndNext(callback) { - self.finishedLoading(); - return self.autoNextMenu(callback); - } - ], - err => { - if(err) { - self.client.log.warn('Error during init sequence', { error : err.message } ); + return self.pausePrompt(pausePosition, callback); + }, + function finishAndNext(callback) { + self.finishedLoading(); + return self.autoNextMenu(callback); + } + ], + err => { + if(err) { + self.client.log.warn('Error during init sequence', { error : err.message } ); - return self.prevMenu( () => { /* dummy */ } ); - } - } - ); - } + return self.prevMenu( () => { /* dummy */ } ); + } + } + ); + } - beforeArt(cb) { - if(_.isNumber(this.menuConfig.options.baudRate)) { - // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here - this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); - } + beforeArt(cb) { + if(_.isNumber(this.menuConfig.options.baudRate)) { + // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here + this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); + } - if(this.cls) { - this.client.term.rawWrite(ansi.resetScreen()); - } + if(this.cls) { + this.client.term.rawWrite(ansi.resetScreen()); + } - return cb(null); - } + return cb(null); + } - mciReady(mciData, cb) { - // available for sub-classes - return cb(null); - } + mciReady(mciData, cb) { + // available for sub-classes + return cb(null); + } - finishedLoading() { - // nothing in base - } + finishedLoading() { + // nothing in base + } - getSaveState() { - // nothing in base - } + getSaveState() { + // nothing in base + } - restoreSavedState(/*savedState*/) { - // nothing in base - } + restoreSavedState(/*savedState*/) { + // nothing in base + } - getMenuResult() { - // default to the formData that was provided @ a submit, if any - return this.submitFormData; - } + getMenuResult() { + // default to the formData that was provided @ a submit, if any + return this.submitFormData; + } - nextMenu(cb) { - if(!this.haveNext()) { - return this.prevMenu(cb); // no next, go to prev - } + nextMenu(cb) { + if(!this.haveNext()) { + return this.prevMenu(cb); // no next, go to prev + } - return this.client.menuStack.next(cb); - } + return this.client.menuStack.next(cb); + } - prevMenu(cb) { - return this.client.menuStack.prev(cb); - } + prevMenu(cb) { + return this.client.menuStack.prev(cb); + } - gotoMenu(name, options, cb) { - return this.client.menuStack.goto(name, options, cb); - } + gotoMenu(name, options, cb) { + return this.client.menuStack.goto(name, options, cb); + } - addViewController(name, vc) { - assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); + addViewController(name, vc) { + assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); - this.viewControllers[name] = vc; - return vc; - } + this.viewControllers[name] = vc; + return vc; + } - detachViewControllers() { - Object.keys(this.viewControllers).forEach( name => { - this.viewControllers[name].detachClientEvents(); - }); - } + detachViewControllers() { + Object.keys(this.viewControllers).forEach( name => { + this.viewControllers[name].detachClientEvents(); + }); + } - shouldPause() { - return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause); - } + shouldPause() { + return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause); + } - hasNextTimeout() { - return _.isNumber(this.menuConfig.options.nextTimeout); - } + hasNextTimeout() { + return _.isNumber(this.menuConfig.options.nextTimeout); + } - haveNext() { - return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); - } + haveNext() { + return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); + } - autoNextMenu(cb) { - const self = this; + autoNextMenu(cb) { + const self = this; - function gotoNextMenu() { - if(self.haveNext()) { - return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb); - } else { - return self.prevMenu(cb); - } - } + function gotoNextMenu() { + if(self.haveNext()) { + return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb); + } else { + return self.prevMenu(cb); + } + } - if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { - if(this.hasNextTimeout()) { - setTimeout( () => { - return gotoNextMenu(); - }, this.menuConfig.options.nextTimeout); - } else { - return gotoNextMenu(); - } - } - } + if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { + if(this.hasNextTimeout()) { + setTimeout( () => { + return gotoNextMenu(); + }, this.menuConfig.options.nextTimeout); + } else { + return gotoNextMenu(); + } + } + } - standardMCIReadyHandler(mciData, cb) { - // - // A quick rundown: - // * We may have mciData.menu, mciData.prompt, or both. - // * Prompt form is favored over menu form if both are present. - // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) - // - const self = this; + standardMCIReadyHandler(mciData, cb) { + // + // A quick rundown: + // * We may have mciData.menu, mciData.prompt, or both. + // * Prompt form is favored over menu form if both are present. + // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) + // + const self = this; - async.series( - [ - function addViewControllers(callback) { - _.forEach(mciData, (mciMap, name) => { - assert('menu' === name || 'prompt' === name); - self.addViewController(name, new ViewController( { client : self.client } ) ); - }); + async.series( + [ + function addViewControllers(callback) { + _.forEach(mciData, (mciMap, name) => { + assert('menu' === name || 'prompt' === name); + self.addViewController(name, new ViewController( { client : self.client } ) ); + }); - return callback(null); - }, - function createMenu(callback) { - if(!self.viewControllers.menu) { - return callback(null); - } + return callback(null); + }, + function createMenu(callback) { + if(!self.viewControllers.menu) { + return callback(null); + } - const menuLoadOpts = { - mciMap : mciData.menu, - callingMenu : self, - withoutForm : _.isObject(mciData.prompt), - }; + const menuLoadOpts = { + mciMap : mciData.menu, + callingMenu : self, + withoutForm : _.isObject(mciData.prompt), + }; - self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { - return callback(err); - }); - }, - function createPrompt(callback) { - if(!self.viewControllers.prompt) { - return callback(null); - } + self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { + return callback(err); + }); + }, + function createPrompt(callback) { + if(!self.viewControllers.prompt) { + return callback(null); + } - const promptLoadOpts = { - callingMenu : self, - mciMap : mciData.prompt, - }; + const promptLoadOpts = { + callingMenu : self, + mciMap : mciData.prompt, + }; - self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); - } + self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } - displayAsset(name, options, cb) { - if(_.isFunction(options)) { - cb = options; - options = {}; - } + displayAsset(name, options, cb) { + if(_.isFunction(options)) { + cb = options; + options = {}; + } - if(options.clearScreen) { - this.client.term.rawWrite(ansi.resetScreen()); - } + if(options.clearScreen) { + this.client.term.rawWrite(ansi.resetScreen()); + } - return theme.displayThemedAsset( - name, - this.client, - Object.assign( { font : this.menuConfig.config.font }, options ), - (err, artData) => { - if(cb) { - return cb(err, artData); - } - } - ); - } + return theme.displayThemedAsset( + name, + this.client, + Object.assign( { font : this.menuConfig.config.font }, options ), + (err, artData) => { + if(cb) { + return cb(err, artData); + } + } + ); + } - prepViewController(name, formId, mciMap, cb) { - if(_.isUndefined(this.viewControllers[name])) { - const vcOpts = { - client : this.client, - formId : formId, - }; + prepViewController(name, formId, mciMap, cb) { + if(_.isUndefined(this.viewControllers[name])) { + const vcOpts = { + client : this.client, + formId : formId, + }; - const vc = this.addViewController(name, new ViewController(vcOpts)); + const vc = this.addViewController(name, new ViewController(vcOpts)); - const loadOpts = { - callingMenu : this, - mciMap : mciMap, - formId : formId, - }; + const loadOpts = { + callingMenu : this, + mciMap : mciMap, + formId : formId, + }; - return vc.loadFromMenuConfig(loadOpts, err => { - return cb(err, vc); - }); - } + return vc.loadFromMenuConfig(loadOpts, err => { + return cb(err, vc); + }); + } - this.viewControllers[name].setFocus(true); + this.viewControllers[name].setFocus(true); - return cb(null, this.viewControllers[name]); - } + return cb(null, this.viewControllers[name]); + } - prepViewControllerWithArt(name, formId, options, cb) { - this.displayAsset( - this.menuConfig.config.art[name], - options, - (err, artData) => { - if(err) { - return cb(err); - } + prepViewControllerWithArt(name, formId, options, cb) { + this.displayAsset( + this.menuConfig.config.art[name], + options, + (err, artData) => { + if(err) { + return cb(err); + } - return this.prepViewController(name, formId, artData.mciMap, cb); - } - ); - } + return this.prepViewController(name, formId, artData.mciMap, cb); + } + ); + } - optionalMoveToPosition(position) { - if(position) { - position.x = position.row || position.x || 1; - position.y = position.col || position.y || 1; + optionalMoveToPosition(position) { + if(position) { + position.x = position.row || position.x || 1; + position.y = position.col || position.y || 1; - this.client.term.rawWrite(ansi.goto(position.x, position.y)); - } - } + this.client.term.rawWrite(ansi.goto(position.x, position.y)); + } + } - pausePrompt(position, cb) { - if(!cb && _.isFunction(position)) { - cb = position; - position = null; - } + pausePrompt(position, cb) { + if(!cb && _.isFunction(position)) { + cb = position; + position = null; + } - this.optionalMoveToPosition(position); + this.optionalMoveToPosition(position); - return theme.displayThemedPause(this.client, cb); - } + return theme.displayThemedPause(this.client, cb); + } - /* + /* :TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... ) promptForInput(formName, name, options, cb) { if(!cb && _.isFunction(options)) { @@ -386,55 +386,55 @@ exports.MenuModule = class MenuModule extends PluginModule { } */ - setViewText(formName, mciId, text, appendMultiLine) { - const view = this.viewControllers[formName].getView(mciId); - if(!view) { - return; - } + setViewText(formName, mciId, text, appendMultiLine) { + const view = this.viewControllers[formName].getView(mciId); + if(!view) { + return; + } - if(appendMultiLine && (view instanceof MultiLineEditTextView)) { - view.addText(text); - } else { - view.setText(text); - } - } + if(appendMultiLine && (view instanceof MultiLineEditTextView)) { + view.addText(text); + } else { + view.setText(text); + } + } - updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) { - options = options || {}; + updateCustomViewTextsWithFilter(formName, startId, fmtObj, options) { + options = options || {}; - let textView; - let customMciId = startId; - const config = this.menuConfig.config; - const endId = options.endId || 99; // we'll fail to get a view before 99 + let textView; + let customMciId = startId; + const config = this.menuConfig.config; + const endId = options.endId || 99; // we'll fail to get a view before 99 - while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { - const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" - const format = config[key]; + while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { + const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" + const format = config[key]; - if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { - const text = stringFormat(format, fmtObj); + if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { + const text = stringFormat(format, fmtObj); - if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { - textView.addText(text); - } else { - textView.setText(text); - } - } + if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { + textView.addText(text); + } else { + textView.setText(text); + } + } - ++customMciId; - } - } + ++customMciId; + } + } - refreshPredefinedMciViewsByCode(formName, mciCodes) { - const form = _.get(this, [ 'viewControllers', formName] ); - if(form) { - form.getViewsByMciCode(mciCodes).forEach(v => { - if(!v.setText) { - return; - } + refreshPredefinedMciViewsByCode(formName, mciCodes) { + const form = _.get(this, [ 'viewControllers', formName] ); + if(form) { + form.getViewsByMciCode(mciCodes).forEach(v => { + if(!v.setText) { + return; + } - v.setText(getPredefinedMCIValue(this.client, v.mciCode)); - }); - } - } + v.setText(getPredefinedMCIValue(this.client, v.mciCode)); + }); + } + } }; diff --git a/core/menu_stack.js b/core/menu_stack.js index 3775c065..90269bcb 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -12,180 +12,180 @@ const assert = require('assert'); // :TODO: Stack is backwards.... top should be most recent! :) module.exports = class MenuStack { - constructor(client) { - this.client = client; - this.stack = []; - } + constructor(client) { + this.client = client; + this.stack = []; + } - push(moduleInfo) { - return this.stack.push(moduleInfo); - } + push(moduleInfo) { + return this.stack.push(moduleInfo); + } - pop() { - return this.stack.pop(); - } + pop() { + return this.stack.pop(); + } - peekPrev() { - if(this.stackSize > 1) { - return this.stack[this.stack.length - 2]; - } - } + peekPrev() { + if(this.stackSize > 1) { + return this.stack[this.stack.length - 2]; + } + } - top() { - if(this.stackSize > 0) { - return this.stack[this.stack.length - 1]; - } - } + top() { + if(this.stackSize > 0) { + return this.stack[this.stack.length - 1]; + } + } - get stackSize() { - return this.stack.length; - } + get stackSize() { + return this.stack.length; + } - get currentModule() { - const top = this.top(); - if(top) { - return top.instance; - } - } + get currentModule() { + const top = this.top(); + if(top) { + return top.instance; + } + } - next(cb) { - const currentModuleInfo = this.top(); - assert(currentModuleInfo, 'Empty menu stack!'); + next(cb) { + const currentModuleInfo = this.top(); + assert(currentModuleInfo, 'Empty menu stack!'); - const menuConfig = currentModuleInfo.instance.menuConfig; - const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); - if(!nextMenu) { - return cb(Array.isArray(menuConfig.next) ? - Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') : - Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT') - ); - } + const menuConfig = currentModuleInfo.instance.menuConfig; + const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); + if(!nextMenu) { + return cb(Array.isArray(menuConfig.next) ? + Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') : + Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT') + ); + } - if(nextMenu === currentModuleInfo.name) { - return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE')); - } + if(nextMenu === currentModuleInfo.name) { + return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE')); + } - this.goto(nextMenu, { }, cb); - } + this.goto(nextMenu, { }, cb); + } - prev(cb) { - const menuResult = this.top().instance.getMenuResult(); + prev(cb) { + const menuResult = this.top().instance.getMenuResult(); - // :TODO: leave() should really take a cb... - this.pop().instance.leave(); // leave & remove current + // :TODO: leave() should really take a cb... + this.pop().instance.leave(); // leave & remove current - const previousModuleInfo = this.pop(); // get previous + const previousModuleInfo = this.pop(); // get previous - if(previousModuleInfo) { - const opts = { - extraArgs : previousModuleInfo.extraArgs, - savedState : previousModuleInfo.savedState, - lastMenuResult : menuResult, - }; + if(previousModuleInfo) { + const opts = { + extraArgs : previousModuleInfo.extraArgs, + savedState : previousModuleInfo.savedState, + lastMenuResult : menuResult, + }; - return this.goto(previousModuleInfo.name, opts, cb); - } + return this.goto(previousModuleInfo.name, opts, cb); + } - return cb(Errors.MenuStack('No previous menu available', 'NOPREV')); - } + return cb(Errors.MenuStack('No previous menu available', 'NOPREV')); + } - goto(name, options, cb) { - const currentModuleInfo = this.top(); + goto(name, options, cb) { + const currentModuleInfo = this.top(); - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } - options = options || {}; - const self = this; + options = options || {}; + const self = this; - if(currentModuleInfo && name === currentModuleInfo.name) { - if(cb) { - cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); - } - return; - } + if(currentModuleInfo && name === currentModuleInfo.name) { + if(cb) { + cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); + } + return; + } - const loadOpts = { - name : name, - client : self.client, - }; + const loadOpts = { + name : name, + client : self.client, + }; - if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { - loadOpts.extraArgs = currentModuleInfo.extraArgs; - } else { - loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); - } - loadOpts.lastMenuResult = options.lastMenuResult; + if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { + loadOpts.extraArgs = currentModuleInfo.extraArgs; + } else { + loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); + } + loadOpts.lastMenuResult = options.lastMenuResult; - loadMenu(loadOpts, (err, modInst) => { - if(err) { - // :TODO: probably should just require a cb... - const errCb = cb || self.client.defaultHandlerMissingMod(); - errCb(err); - } else { - self.client.log.debug( { menuName : name }, 'Goto menu module'); + loadMenu(loadOpts, (err, modInst) => { + if(err) { + // :TODO: probably should just require a cb... + const errCb = cb || self.client.defaultHandlerMissingMod(); + errCb(err); + } else { + self.client.log.debug( { menuName : name }, 'Goto menu module'); - // - // If menuFlags were supplied in menu.hjson, they should win over - // anything supplied in code. - // - let menuFlags; - if(0 === modInst.menuConfig.options.menuFlags.length) { - menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : []; - } else { - menuFlags = modInst.menuConfig.options.menuFlags; + // + // If menuFlags were supplied in menu.hjson, they should win over + // anything supplied in code. + // + let menuFlags; + if(0 === modInst.menuConfig.options.menuFlags.length) { + menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : []; + } else { + menuFlags = modInst.menuConfig.options.menuFlags; - // in code we can ask to merge in - if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { - menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); - } - } + // in code we can ask to merge in + if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { + menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); + } + } - if(currentModuleInfo) { - // save stack state - currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); + if(currentModuleInfo) { + // save stack state + currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); - currentModuleInfo.instance.leave(); + currentModuleInfo.instance.leave(); - if(currentModuleInfo.menuFlags.includes('noHistory')) { - this.pop(); - } + if(currentModuleInfo.menuFlags.includes('noHistory')) { + this.pop(); + } - if(menuFlags.includes('popParent')) { - this.pop().instance.leave(); // leave & remove current - } - } + if(menuFlags.includes('popParent')) { + this.pop().instance.leave(); // leave & remove current + } + } - self.push({ - name : name, - instance : modInst, - extraArgs : loadOpts.extraArgs, - menuFlags : menuFlags, - }); + self.push({ + name : name, + instance : modInst, + extraArgs : loadOpts.extraArgs, + menuFlags : menuFlags, + }); - // restore previous state if requested - if(options.savedState) { - modInst.restoreSavedState(options.savedState); - } + // restore previous state if requested + if(options.savedState) { + modInst.restoreSavedState(options.savedState); + } - const stackEntries = self.stack.map(stackEntry => { - let name = stackEntry.name; - if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) { - name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`; - } - return name; - }); + const stackEntries = self.stack.map(stackEntry => { + let name = stackEntry.name; + if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) { + name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`; + } + return name; + }); - self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' ); + self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' ); - modInst.enter(); + modInst.enter(); - if(cb) { - cb(null); - } - } - }); - } + if(cb) { + cb(null); + } + } + }); + } }; diff --git a/core/menu_util.js b/core/menu_util.js index c6ad3a85..76205d3f 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -19,243 +19,243 @@ exports.handleAction = handleAction; exports.handleNext = handleNext; function getMenuConfig(client, name, cb) { - var menuConfig; + var menuConfig; - async.waterfall( - [ - function locateMenuConfig(callback) { - if(_.has(client.currentTheme, [ 'menus', name ])) { - menuConfig = client.currentTheme.menus[name]; - callback(null); - } else { - callback(new Error('No menu entry for \'' + name + '\'')); - } - }, - function locatePromptConfig(callback) { - if(_.isString(menuConfig.prompt)) { - if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { - menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; - callback(null); - } else { - callback(new Error('No prompt entry for \'' + menuConfig.prompt + '\'')); - } - } else { - callback(null); - } - } - ], - function complete(err) { - cb(err, menuConfig); - } - ); + async.waterfall( + [ + function locateMenuConfig(callback) { + if(_.has(client.currentTheme, [ 'menus', name ])) { + menuConfig = client.currentTheme.menus[name]; + callback(null); + } else { + callback(new Error('No menu entry for \'' + name + '\'')); + } + }, + function locatePromptConfig(callback) { + if(_.isString(menuConfig.prompt)) { + if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { + menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; + callback(null); + } else { + callback(new Error('No prompt entry for \'' + menuConfig.prompt + '\'')); + } + } else { + callback(null); + } + } + ], + function complete(err) { + cb(err, menuConfig); + } + ); } function loadMenu(options, cb) { - assert(_.isObject(options)); - assert(_.isString(options.name)); - assert(_.isObject(options.client)); + assert(_.isObject(options)); + assert(_.isString(options.name)); + assert(_.isObject(options.client)); - async.waterfall( - [ - function getMenuConfiguration(callback) { - getMenuConfig(options.client, options.name, (err, menuConfig) => { - return callback(err, menuConfig); - }); - }, - function loadMenuModule(menuConfig, callback) { + async.waterfall( + [ + function getMenuConfiguration(callback) { + getMenuConfig(options.client, options.name, (err, menuConfig) => { + return callback(err, menuConfig); + }); + }, + function loadMenuModule(menuConfig, callback) { - menuConfig.options = menuConfig.options || {}; - menuConfig.options.menuFlags = menuConfig.options.menuFlags || []; - if(!Array.isArray(menuConfig.options.menuFlags)) { - menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ]; - } + menuConfig.options = menuConfig.options || {}; + menuConfig.options.menuFlags = menuConfig.options.menuFlags || []; + if(!Array.isArray(menuConfig.options.menuFlags)) { + menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ]; + } - const modAsset = asset.getModuleAsset(menuConfig.module); - const modSupplied = null !== modAsset; + const modAsset = asset.getModuleAsset(menuConfig.module); + const modSupplied = null !== modAsset; - const modLoadOpts = { - name : modSupplied ? modAsset.asset : 'standard_menu', - path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, - category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', - }; + const modLoadOpts = { + name : modSupplied ? modAsset.asset : 'standard_menu', + path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, + category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', + }; - moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => { - const modData = { - name : modLoadOpts.name, - config : menuConfig, - mod : mod, - }; + moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => { + const modData = { + name : modLoadOpts.name, + config : menuConfig, + mod : mod, + }; - return callback(err, modData); - }); - }, - function createModuleInstance(modData, callback) { - Log.trace( - { moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, - 'Creating menu module instance'); + return callback(err, modData); + }); + }, + function createModuleInstance(modData, callback) { + Log.trace( + { moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, + 'Creating menu module instance'); - let moduleInstance; - try { - moduleInstance = new modData.mod.getModule({ - menuName : options.name, - menuConfig : modData.config, - extraArgs : options.extraArgs, - client : options.client, - lastMenuResult : options.lastMenuResult, - }); - } catch(e) { - return callback(e); - } + let moduleInstance; + try { + moduleInstance = new modData.mod.getModule({ + menuName : options.name, + menuConfig : modData.config, + extraArgs : options.extraArgs, + client : options.client, + lastMenuResult : options.lastMenuResult, + }); + } catch(e) { + return callback(e); + } - return callback(null, moduleInstance); - } - ], - (err, modInst) => { - return cb(err, modInst); - } - ); + return callback(null, moduleInstance); + } + ], + (err, modInst) => { + return cb(err, modInst); + } + ); } function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { - assert(_.isObject(menuConfig)); + assert(_.isObject(menuConfig)); - if(!_.isObject(menuConfig.form)) { - cb(new Error('Invalid or missing \'form\' member for menu')); - return; - } + if(!_.isObject(menuConfig.form)) { + cb(new Error('Invalid or missing \'form\' member for menu')); + return; + } - if(!_.isObject(menuConfig.form[formId])) { - cb(new Error('No form found for formId ' + formId)); - return; - } + if(!_.isObject(menuConfig.form[formId])) { + cb(new Error('No form found for formId ' + formId)); + return; + } - const formForId = menuConfig.form[formId]; - const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => { - return MCIViewFactory.UserViewCodes.indexOf(mci) > -1; - }).join(''); + const formForId = menuConfig.form[formId]; + const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => { + return MCIViewFactory.UserViewCodes.indexOf(mci) > -1; + }).join(''); - Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key'); + Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key'); - // - // Exact, explicit match? - // - if(_.isObject(formForId[mciReqKey])) { - Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); - cb(null, formForId[mciReqKey]); - return; - } + // + // Exact, explicit match? + // + if(_.isObject(formForId[mciReqKey])) { + Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); + cb(null, formForId[mciReqKey]); + return; + } - // - // Generic match - // - if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { - Log.trace('Using generic configuration'); - return cb(null, formForId); - } + // + // Generic match + // + if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { + Log.trace('Using generic configuration'); + return cb(null, formForId); + } - cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\'')); + cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\'')); } // :TODO: Most of this should be moved elsewhere .... DRY... function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) { - if('' === paths.extname(path)) { - path += '.js'; - } + if('' === paths.extname(path)) { + path += '.js'; + } - try { - client.log.trace( - { path : path, methodName : asset.asset, formData : formData, extraArgs : extraArgs }, - 'Calling menu method'); + try { + client.log.trace( + { path : path, methodName : asset.asset, formData : formData, extraArgs : extraArgs }, + 'Calling menu method'); - const methodMod = require(path); - return methodMod[asset.asset](client.currentMenuModule, formData || { }, extraArgs, cb); - } catch(e) { - client.log.error( { error : e.toString(), methodName : asset.asset }, 'Failed to execute asset method'); - return cb(e); - } + const methodMod = require(path); + return methodMod[asset.asset](client.currentMenuModule, formData || { }, extraArgs, cb); + } catch(e) { + client.log.error( { error : e.toString(), methodName : asset.asset }, 'Failed to execute asset method'); + return cb(e); + } } function handleAction(client, formData, conf, cb) { - assert(_.isObject(conf)); - assert(_.isString(conf.action)); + assert(_.isObject(conf)); + assert(_.isString(conf.action)); - const actionAsset = asset.parseAsset(conf.action); - assert(_.isObject(actionAsset)); + const actionAsset = asset.parseAsset(conf.action); + assert(_.isObject(actionAsset)); - switch(actionAsset.type) { - case 'method' : - case 'systemMethod' : - if(_.isString(actionAsset.location)) { - return callModuleMenuMethod( - client, - actionAsset, - paths.join(Config().paths.mods, actionAsset.location), - formData, - conf.extraArgs, - cb); - } else if('systemMethod' === actionAsset.type) { - // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () - // :TODO: Probably better as system_method.js - return callModuleMenuMethod( - client, - actionAsset, - paths.join(__dirname, 'system_menu_method.js'), - formData, - conf.extraArgs, - cb); - } else { - // local to current module - const currentModule = client.currentMenuModule; - if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { - return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); - } + switch(actionAsset.type) { + case 'method' : + case 'systemMethod' : + if(_.isString(actionAsset.location)) { + return callModuleMenuMethod( + client, + actionAsset, + paths.join(Config().paths.mods, actionAsset.location), + formData, + conf.extraArgs, + cb); + } else if('systemMethod' === actionAsset.type) { + // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () + // :TODO: Probably better as system_method.js + return callModuleMenuMethod( + client, + actionAsset, + paths.join(__dirname, 'system_menu_method.js'), + formData, + conf.extraArgs, + cb); + } else { + // local to current module + const currentModule = client.currentMenuModule; + if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { + return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); + } - const err = new Error('Method does not exist'); - client.log.warn( { method : actionAsset.asset }, err.message); - return cb(err); - } + const err = new Error('Method does not exist'); + client.log.warn( { method : actionAsset.asset }, err.message); + return cb(err); + } - case 'menu' : - return client.currentMenuModule.gotoMenu(actionAsset.asset, { formData : formData, extraArgs : conf.extraArgs }, cb ); - } + case 'menu' : + return client.currentMenuModule.gotoMenu(actionAsset.asset, { formData : formData, extraArgs : conf.extraArgs }, cb ); + } } function handleNext(client, nextSpec, conf, cb) { - nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals + nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals - const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); - // :TODO: getAssetWithShorthand() can return undefined - handle it! + const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); + // :TODO: getAssetWithShorthand() can return undefined - handle it! - conf = conf || {}; - const extraArgs = conf.extraArgs || {}; + conf = conf || {}; + const extraArgs = conf.extraArgs || {}; - // :TODO: DRY this with handleAction() - switch(nextAsset.type) { - case 'method' : - case 'systemMethod' : - if(_.isString(nextAsset.location)) { - return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb); - } else if('systemMethod' === nextAsset.type) { - // :TODO: see other notes about system_menu_method.js here - return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb); - } else { - // local to current module - const currentModule = client.currentMenuModule; - if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) { - const formData = {}; // we don't have any - return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb ); - } + // :TODO: DRY this with handleAction() + switch(nextAsset.type) { + case 'method' : + case 'systemMethod' : + if(_.isString(nextAsset.location)) { + return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb); + } else if('systemMethod' === nextAsset.type) { + // :TODO: see other notes about system_menu_method.js here + return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb); + } else { + // local to current module + const currentModule = client.currentMenuModule; + if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) { + const formData = {}; // we don't have any + return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb ); + } - const err = new Error('Method does not exist'); - client.log.warn( { method : nextAsset.asset }, err.message); - return cb(err); - } + const err = new Error('Method does not exist'); + client.log.warn( { method : nextAsset.asset }, err.message); + return cb(err); + } - case 'menu' : - return client.currentMenuModule.gotoMenu(nextAsset.asset, { extraArgs : extraArgs }, cb ); - } + case 'menu' : + return client.currentMenuModule.gotoMenu(nextAsset.asset, { extraArgs : extraArgs }, cb ); + } - const err = new Error('Invalid asset type for "next"'); - client.log.error( { nextSpec : nextSpec }, err.message); - return cb(err); + const err = new Error('Invalid asset type for "next"'); + client.log.error( { nextSpec : nextSpec }, err.message); + return cb(err); } diff --git a/core/menu_view.js b/core/menu_view.js index 598e04cd..78bf2c87 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -14,264 +14,264 @@ const _ = require('lodash'); exports.MenuView = MenuView; function MenuView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - View.call(this, options); + View.call(this, options); - this.disablePipe = options.disablePipe || false; + this.disablePipe = options.disablePipe || false; - const self = this; + const self = this; - if(options.items) { - this.setItems(options.items); - } else { - this.items = []; - } + if(options.items) { + this.setItems(options.items); + } else { + this.items = []; + } - this.renderCache = {}; + this.renderCache = {}; - this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); - this.setHotKeys(options.hotKeys); + this.setHotKeys(options.hotKeys); - this.focusedItemIndex = options.focusedItemIndex || 0; - this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; + this.focusedItemIndex = options.focusedItemIndex || 0; + this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; + // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + this.justify = options.justify || 'none'; - this.hasFocusItems = function() { - return !_.isUndefined(self.focusItems); - }; + this.hasFocusItems = function() { + return !_.isUndefined(self.focusItems); + }; - this.getHotKeyItemIndex = function(ch) { - if(ch && self.hotKeys) { - const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; - if(_.isNumber(keyIndex)) { - return keyIndex; - } - } - return -1; - }; + this.getHotKeyItemIndex = function(ch) { + if(ch && self.hotKeys) { + const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; + if(_.isNumber(keyIndex)) { + return keyIndex; + } + } + return -1; + }; - this.emitIndexUpdate = function() { - self.emit('index update', self.focusedItemIndex); - } + this.emitIndexUpdate = function() { + self.emit('index update', self.focusedItemIndex); + }; } util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { - if(Array.isArray(items)) { - this.sorted = false; - this.renderCache = {}; + if(Array.isArray(items)) { + this.sorted = false; + this.renderCache = {}; - // - // Items can be an array of strings or an array of objects. - // - // In the case of objects, items are considered complex and - // may have one or more members that can later be formatted - // against. The default member is 'text'. The member 'data' - // may be overridden to provide a form value other than the - // item's index. - // - // Items can be formatted with 'itemFormat' and 'focusItemFormat' - // - let text; - let stringItem; - this.items = items.map(item => { - stringItem = _.isString(item); - if(stringItem) { - text = item; - } else { - text = item.text || ''; - this.complexItems = true; - } + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if(stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others - }); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + }); - if(this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; - } - } + if(this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; + } + } }; MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { - const item = this.renderCache[index]; - return item && item[focusItem ? 'focus' : 'standard']; + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; }; MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { - this.renderCache[index] = this.renderCache[index] || {}; - this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; MenuView.prototype.setSort = function(sort) { - if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { - return; - } + if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; + } - const key = true === sort ? 'text' : sort; - if('text' !== sort && !this.complexItems) { - return; // need a valid sort key - } + const key = true === sort ? 'text' : sort; + if('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } - this.items.sort( (a, b) => { - const a1 = a[key]; - const b1 = b[key]; - if(!a1) { - return -1; - } - if(!b1) { - return 1; - } - return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); - }); + this.items.sort( (a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if(!a1) { + return -1; + } + if(!b1) { + return 1; + } + return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); + }); - this.sorted = true; + this.sorted = true; }; MenuView.prototype.removeItem = function(index) { - this.sorted = false; - this.items.splice(index, 1); + this.sorted = false; + this.items.splice(index, 1); - if(this.focusItems) { - this.focusItems.splice(index, 1); - } + if(this.focusItems) { + this.focusItems.splice(index, 1); + } - if(this.focusedItemIndex >= index) { - this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); - } + if(this.focusedItemIndex >= index) { + this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); + } - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; MenuView.prototype.getCount = function() { - return this.items.length; + return this.items.length; }; MenuView.prototype.getItems = function() { - if(this.complexItems) { - return this.items; - } + if(this.complexItems) { + return this.items; + } - return this.items.map( item => { - return item.text; - }); + return this.items.map( item => { + return item.text; + }); }; MenuView.prototype.getItem = function(index) { - if(this.complexItems) { - return this.items[index]; - } + if(this.complexItems) { + return this.items[index]; + } - return this.items[index].text; + return this.items[index].text; }; MenuView.prototype.focusNext = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusFirst = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusLast = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { - this.focusedItemIndex = index; + this.focusedItemIndex = index; }; MenuView.prototype.onKeyPress = function(ch, key) { - const itemIndex = this.getHotKeyItemIndex(ch); - if(itemIndex >= 0) { - this.setFocusItemIndex(itemIndex); + const itemIndex = this.getHotKeyItemIndex(ch); + if(itemIndex >= 0) { + this.setFocusItemIndex(itemIndex); - if(true === this.hotKeySubmit) { - this.emit('action', 'accept'); - } - } + if(true === this.hotKeySubmit) { + this.emit('action', 'accept'); + } + } - MenuView.super_.prototype.onKeyPress.call(this, ch, key); + MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; MenuView.prototype.setFocusItems = function(items) { - const self = this; + const self = this; - if(items) { - this.focusItems = []; - items.forEach( itemText => { - this.focusItems.push( - { - text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); - }); - } + if(items) { + this.focusItems = []; + items.forEach( itemText => { + this.focusItems.push( + { + text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) + } + ); + }); + } }; MenuView.prototype.setItemSpacing = function(itemSpacing) { - itemSpacing = parseInt(itemSpacing); - assert(_.isNumber(itemSpacing)); + itemSpacing = parseInt(itemSpacing); + assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'itemSpacing' : this.setItemSpacing(value); break; - case 'items' : this.setItems(value); break; - case 'focusItems' : this.setFocusItems(value); break; - case 'hotKeys' : this.setHotKeys(value); break; - case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; - case 'focusItemIndex' : this.focusedItemIndex = value; break; + switch(propName) { + case 'itemSpacing' : this.setItemSpacing(value); break; + case 'items' : this.setItems(value); break; + case 'focusItems' : this.setFocusItems(value); break; + case 'hotKeys' : this.setHotKeys(value); break; + case 'hotKeySubmit' : this.hotKeySubmit = value; break; + case 'justify' : this.justify = value; break; + case 'focusItemIndex' : this.focusedItemIndex = value; break; - case 'itemFormat' : - case 'focusItemFormat' : - this[propName] = value; - break; + case 'itemFormat' : + case 'focusItemFormat' : + this[propName] = value; + break; - case 'sort' : this.setSort(value); break; - } + case 'sort' : this.setSort(value); break; + } - MenuView.super_.prototype.setPropertyValue.call(this, propName, value); + MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; MenuView.prototype.setHotKeys = function(hotKeys) { - if(_.isObject(hotKeys)) { - if(this.caseInsensitiveHotKeys) { - this.hotKeys = {}; - for(var key in hotKeys) { - this.hotKeys[key.toLowerCase()] = hotKeys[key]; - } - } else { - this.hotKeys = hotKeys; - } - } + if(_.isObject(hotKeys)) { + if(this.caseInsensitiveHotKeys) { + this.hotKeys = {}; + for(var key in hotKeys) { + this.hotKeys[key.toLowerCase()] = hotKeys[key]; + } + } else { + this.hotKeys = hotKeys; + } + } }; diff --git a/core/message.js b/core/message.js index f1fa2db8..277f5781 100644 --- a/core/message.js +++ b/core/message.js @@ -8,13 +8,13 @@ const createNamedUUID = require('./uuid_util.js').createNamedUUID; const Errors = require('./enig_error.js').Errors; const ANSI = require('./ansi_term.js'); const { - sanatizeString, - getISOTimestampString } = require('./database.js'); + sanatizeString, + getISOTimestampString } = require('./database.js'); const { - isAnsi, isFormattedLine, - splitTextAtTerms, - renderSubstr + isAnsi, isFormattedLine, + splitTextAtTerms, + renderSubstr } = require('./string_util.js'); const ansiPrep = require('./ansi_prep.js'); @@ -30,182 +30,182 @@ const iconvEncode = require('iconv-lite').encode; const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); const WELL_KNOWN_AREA_TAGS = { - Invalid : '', - Private : 'private_mail', - Bulletin : 'local_bulletin', + Invalid : '', + Private : 'private_mail', + Bulletin : 'local_bulletin', }; const SYSTEM_META_NAMES = { - LocalToUserID : 'local_to_user_id', - LocalFromUserID : 'local_from_user_id', - StateFlags0 : 'state_flags0', // See Message.StateFlags0 - ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. - ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor - RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address - RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address + LocalToUserID : 'local_to_user_id', + LocalFromUserID : 'local_from_user_id', + StateFlags0 : 'state_flags0', // See Message.StateFlags0 + ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. + ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor + RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address + RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address }; // Types for Message.SystemMetaNames.ExternalFlavor meta const ADDRESS_FLAVOR = { - Local : 'local', // local / non-remote addressing - FTN : 'ftn', // FTN style - Email : 'email', + Local : 'local', // local / non-remote addressing + FTN : 'ftn', // FTN style + Email : 'email', }; const STATE_FLAGS0 = { - None : 0x00000000, - Imported : 0x00000001, // imported from foreign system - Exported : 0x00000002, // exported to foreign system + None : 0x00000000, + Imported : 0x00000001, // imported from foreign system + Exported : 0x00000002, // exported to foreign system }; // :TODO: these should really live elsewhere... const FTN_PROPERTY_NAMES = { - // packet header oriented - FtnOrigNode : 'ftn_orig_node', - FtnDestNode : 'ftn_dest_node', - // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping - FtnOrigNetwork : 'ftn_orig_network', - FtnDestNetwork : 'ftn_dest_network', - FtnAttrFlags : 'ftn_attr_flags', - FtnCost : 'ftn_cost', - FtnOrigZone : 'ftn_orig_zone', - FtnDestZone : 'ftn_dest_zone', - FtnOrigPoint : 'ftn_orig_point', - FtnDestPoint : 'ftn_dest_point', + // packet header oriented + FtnOrigNode : 'ftn_orig_node', + FtnDestNode : 'ftn_dest_node', + // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping + FtnOrigNetwork : 'ftn_orig_network', + FtnDestNetwork : 'ftn_dest_network', + FtnAttrFlags : 'ftn_attr_flags', + FtnCost : 'ftn_cost', + FtnOrigZone : 'ftn_orig_zone', + FtnDestZone : 'ftn_dest_zone', + FtnOrigPoint : 'ftn_orig_point', + FtnDestPoint : 'ftn_dest_point', - // message header oriented - FtnMsgOrigNode : 'ftn_msg_orig_node', - FtnMsgDestNode : 'ftn_msg_dest_node', - FtnMsgOrigNet : 'ftn_msg_orig_net', - FtnMsgDestNet : 'ftn_msg_dest_net', + // message header oriented + FtnMsgOrigNode : 'ftn_msg_orig_node', + FtnMsgDestNode : 'ftn_msg_dest_node', + FtnMsgOrigNet : 'ftn_msg_orig_net', + FtnMsgDestNet : 'ftn_msg_dest_net', - FtnAttribute : 'ftn_attribute', + FtnAttribute : 'ftn_attribute', - FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 - FtnOrigin : 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 - FtnArea : 'ftn_area', // http://ftsc.org/docs/fts-0004.001 - FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 + FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 + FtnOrigin : 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 + FtnArea : 'ftn_area', // http://ftsc.org/docs/fts-0004.001 + FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 }; // :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! const MESSAGE_ROW_MAP = { - reply_to_message_id : 'replyToMsgId', - modified_timestamp : 'modTimestamp' + reply_to_message_id : 'replyToMsgId', + modified_timestamp : 'modTimestamp' }; module.exports = class Message { - constructor( - { - messageId = 0, areaTag = Message.WellKnownAreaTags.Invalid, uuid, replyToMsgId = 0, - toUserName = '', fromUserName = '', subject = '', message = '', modTimestamp = moment(), - meta, hashTags = [], - } = { } - ) - { - this.messageId = messageId; - this.areaTag = areaTag; - this.uuid = uuid; - this.replyToMsgId = replyToMsgId; - this.toUserName = toUserName; - this.fromUserName = fromUserName; - this.subject = subject; - this.message = message; + constructor( + { + messageId = 0, areaTag = Message.WellKnownAreaTags.Invalid, uuid, replyToMsgId = 0, + toUserName = '', fromUserName = '', subject = '', message = '', modTimestamp = moment(), + meta, hashTags = [], + } = { } + ) + { + this.messageId = messageId; + this.areaTag = areaTag; + this.uuid = uuid; + this.replyToMsgId = replyToMsgId; + this.toUserName = toUserName; + this.fromUserName = fromUserName; + this.subject = subject; + this.message = message; - if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { - modTimestamp = moment(modTimestamp); - } + if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { + modTimestamp = moment(modTimestamp); + } - this.modTimestamp = modTimestamp; + this.modTimestamp = modTimestamp; - this.meta = {}; - _.defaultsDeep(this.meta, { System : {} }, meta); + this.meta = {}; + _.defaultsDeep(this.meta, { System : {} }, meta); - this.hashTags = hashTags; - } + this.hashTags = hashTags; + } - isValid() { return true; } // :TODO: obviously useless; look into this or remove it + isValid() { return true; } // :TODO: obviously useless; look into this or remove it - static isPrivateAreaTag(areaTag) { - return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; - } + static isPrivateAreaTag(areaTag) { + return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; + } - isPrivate() { - return Message.isPrivateAreaTag(this.areaTag); - } + isPrivate() { + return Message.isPrivateAreaTag(this.areaTag); + } - isFromRemoteUser() { - return null !== _.get(this, 'meta.System.remote_from_user', null); - } + isFromRemoteUser() { + return null !== _.get(this, 'meta.System.remote_from_user', null); + } - static get WellKnownAreaTags() { - return WELL_KNOWN_AREA_TAGS; - } + static get WellKnownAreaTags() { + return WELL_KNOWN_AREA_TAGS; + } - static get SystemMetaNames() { - return SYSTEM_META_NAMES; - } + static get SystemMetaNames() { + return SYSTEM_META_NAMES; + } - static get AddressFlavor() { - return ADDRESS_FLAVOR; - } + static get AddressFlavor() { + return ADDRESS_FLAVOR; + } - static get StateFlags0() { - return STATE_FLAGS0; - } + static get StateFlags0() { + return STATE_FLAGS0; + } - static get FtnPropertyNames() { - return FTN_PROPERTY_NAMES; - } + static get FtnPropertyNames() { + return FTN_PROPERTY_NAMES; + } - setLocalToUserId(userId) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; - } + setLocalToUserId(userId) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalToUserID] = userId; + } - setLocalFromUserId(userId) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; - } + setLocalFromUserId(userId) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.LocalFromUserID] = userId; + } - setRemoteToUser(remoteTo) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo; - } + setRemoteToUser(remoteTo) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.RemoteToUser] = remoteTo; + } - setExternalFlavor(flavor) { - this.meta.System = this.meta.System || {}; - this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor; - } + setExternalFlavor(flavor) { + this.meta.System = this.meta.System || {}; + this.meta.System[Message.SystemMetaNames.ExternalFlavor] = flavor; + } - static createMessageUUID(areaTag, modTimestamp, subject, body) { - assert(_.isString(areaTag)); - assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp)); - assert(_.isString(subject)); - assert(_.isString(body)); + static createMessageUUID(areaTag, modTimestamp, subject, body) { + assert(_.isString(areaTag)); + assert(_.isDate(modTimestamp) || moment.isMoment(modTimestamp)); + assert(_.isString(subject)); + assert(_.isString(body)); - if(!moment.isMoment(modTimestamp)) { - modTimestamp = moment(modTimestamp); - } + if(!moment.isMoment(modTimestamp)) { + modTimestamp = moment(modTimestamp); + } - areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); - modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); - subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); - body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); + areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); + modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); + subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); + body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); - return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); - } + return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); + } - static getMessageFromRow(row) { - const msg = {}; - _.each(row, (v, k) => { - // :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()! - k = MESSAGE_ROW_MAP[k] || _.camelCase(k); - msg[k] = v; - }); - return msg; - } + static getMessageFromRow(row) { + const msg = {}; + _.each(row, (v, k) => { + // :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()! + k = MESSAGE_ROW_MAP[k] || _.camelCase(k); + msg[k] = v; + }); + return msg; + } - /* + /* Find message IDs or UUIDs by filter. Available filters/options: filter.uuids - use with resultType='id' @@ -229,237 +229,237 @@ module.exports = class Message { filter.privateTagUserId = - if set, only private messages belonging to are processed - any other areaTag or confTag filters will be ignored - - if NOT present, private areas are skipped + - if NOT present, private areas are skipped *=NYI */ - static findMessages(filter, cb) { - filter = filter || {}; + static findMessages(filter, cb) { + filter = filter || {}; - filter.resultType = filter.resultType || 'id'; - filter.extraFields = filter.extraFields || []; + filter.resultType = filter.resultType || 'id'; + filter.extraFields = filter.extraFields || []; - if('messageList' === filter.resultType) { - filter.extraFields = _.uniq(filter.extraFields.concat( - [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ] - )); - } + if('messageList' === filter.resultType) { + filter.extraFields = _.uniq(filter.extraFields.concat( + [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ] + )); + } - const field = 'uuid' === filter.resultType ? 'message_uuid' : 'message_id'; + const field = 'uuid' === filter.resultType ? 'message_uuid' : 'message_id'; - if(moment.isMoment(filter.newerThanTimestamp)) { - filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); - } + if(moment.isMoment(filter.newerThanTimestamp)) { + filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); + } - let sql; - if('count' === filter.resultType) { - sql = + let sql; + if('count' === filter.resultType) { + sql = `SELECT COUNT() AS count FROM message m`; - } else { - sql = + } else { + sql = `SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} FROM message m`; - } + } - const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; - let sqlOrderBy; - let sqlWhere = ''; + const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; + let sqlOrderBy; + let sqlWhere = ''; - function appendWhereClause(clause) { - if(sqlWhere) { - sqlWhere += ' AND '; - } else { - sqlWhere += ' WHERE '; - } - sqlWhere += clause; - } + function appendWhereClause(clause) { + if(sqlWhere) { + sqlWhere += ' AND '; + } else { + sqlWhere += ' WHERE '; + } + sqlWhere += clause; + } - // currently only avail sort - if('modTimestamp' === filter.sort) { - sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`; - } else { - sqlOrderBy = `ORDER BY m.message_id ${sqlOrderDir}`; - } + // currently only avail sort + if('modTimestamp' === filter.sort) { + sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`; + } else { + sqlOrderBy = `ORDER BY m.message_id ${sqlOrderDir}`; + } - if(Array.isArray(filter.ids)) { - appendWhereClause(`m.message_id IN (${filter.ids.join(', ')})`); - } + if(Array.isArray(filter.ids)) { + appendWhereClause(`m.message_id IN (${filter.ids.join(', ')})`); + } - if(Array.isArray(filter.uuids)) { - const uuidList = filter.uuids.map(u => `"${u}"`).join(', '); - appendWhereClause(`m.message_id IN (${uuidList})`); - } + if(Array.isArray(filter.uuids)) { + const uuidList = filter.uuids.map(u => `"${u}"`).join(', '); + appendWhereClause(`m.message_id IN (${uuidList})`); + } - if(_.isNumber(filter.privateTagUserId)) { - appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); - appendWhereClause( - `m.message_id IN ( + if(_.isNumber(filter.privateTagUserId)) { + appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); + appendWhereClause( + `m.message_id IN ( SELECT message_id FROM message_meta WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} )`); - } else { - if(filter.areaTag && filter.areaTag.length > 0) { - if(Array.isArray(filter.areaTag)) { - const areaList = filter.areaTag - .filter(t => t != Message.WellKnownAreaTags.Private) - .map(t => `"${t}"`).join(', '); - if(areaList.length > 0) { - appendWhereClause(`m.area_tag IN(${areaList})`); - } - } else if(_.isString(filter.areaTag) && Message.WellKnownAreaTags.Private !== filter.areaTag) { - appendWhereClause(`m.area_tag = "${filter.areaTag}"`); - } - } + } else { + if(filter.areaTag && filter.areaTag.length > 0) { + if(Array.isArray(filter.areaTag)) { + const areaList = filter.areaTag + .filter(t => t != Message.WellKnownAreaTags.Private) + .map(t => `"${t}"`).join(', '); + if(areaList.length > 0) { + appendWhereClause(`m.area_tag IN(${areaList})`); + } + } else if(_.isString(filter.areaTag) && Message.WellKnownAreaTags.Private !== filter.areaTag) { + appendWhereClause(`m.area_tag = "${filter.areaTag}"`); + } + } - // explicit exclude of Private - appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`); - } + // explicit exclude of Private + appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`); + } - if(_.isNumber(filter.replyToMessageId)) { - appendWhereClause(`m.reply_to_message_id=${filter.replyToMessageId}`); - } + if(_.isNumber(filter.replyToMessageId)) { + appendWhereClause(`m.reply_to_message_id=${filter.replyToMessageId}`); + } - [ 'toUserName', 'fromUserName' ].forEach(field => { - if(_.isString(filter[field]) && filter[field].length > 0) { - appendWhereClause(`m.${_.snakeCase(field)} LIKE "${sanatizeString(filter[field])}"`); - } - }); + [ 'toUserName', 'fromUserName' ].forEach(field => { + if(_.isString(filter[field]) && filter[field].length > 0) { + appendWhereClause(`m.${_.snakeCase(field)} LIKE "${sanatizeString(filter[field])}"`); + } + }); - if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { - appendWhereClause(`DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); - } + if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { + appendWhereClause(`DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); + } - if(_.isNumber(filter.newerThanMessageId)) { - appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); - } + if(_.isNumber(filter.newerThanMessageId)) { + appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); + } - if(filter.terms && filter.terms.length > 0) { - // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex - appendWhereClause( - `m.message_id IN ( + if(filter.terms && filter.terms.length > 0) { + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + appendWhereClause( + `m.message_id IN ( SELECT rowid FROM message_fts WHERE message_fts MATCH ":${sanatizeString(filter.terms)}" )` - ); - } + ); + } - sql += `${sqlWhere} ${sqlOrderBy}`; + sql += `${sqlWhere} ${sqlOrderBy}`; - if(_.isNumber(filter.limit)) { - sql += ` LIMIT ${filter.limit}`; - } + if(_.isNumber(filter.limit)) { + sql += ` LIMIT ${filter.limit}`; + } - sql += ';'; + sql += ';'; - if('count' === filter.resultType) { - msgDb.get(sql, (err, row) => { - return cb(err, row ? row.count : 0); - }); - } else { - const matches = []; - const extra = filter.extraFields.length > 0; + if('count' === filter.resultType) { + msgDb.get(sql, (err, row) => { + return cb(err, row ? row.count : 0); + }); + } else { + const matches = []; + const extra = filter.extraFields.length > 0; - const rowConv = 'messageList' === filter.resultType ? Message.getMessageFromRow : row => row; + const rowConv = 'messageList' === filter.resultType ? Message.getMessageFromRow : row => row; - msgDb.each(sql, (err, row) => { - if(_.isObject(row)) { - matches.push(extra ? rowConv(row) : row[field]); - } - }, err => { - return cb(err, matches); - }); - } - } + msgDb.each(sql, (err, row) => { + if(_.isObject(row)) { + matches.push(extra ? rowConv(row) : row[field]); + } + }, err => { + return cb(err, matches); + }); + } + } - // :TODO: use findMessages, by uuid, limit=1 - static getMessageIdByUuid(uuid, cb) { - msgDb.get( - `SELECT message_id + // :TODO: use findMessages, by uuid, limit=1 + static getMessageIdByUuid(uuid, cb) { + msgDb.get( + `SELECT message_id FROM message WHERE message_uuid = ? LIMIT 1;`, - [ uuid ], - (err, row) => { - if(err) { - return cb(err); - } + [ uuid ], + (err, row) => { + if(err) { + return cb(err); + } - const success = (row && row.message_id); - return cb( - success ? null : Errors.DoesNotExist(`No message for UUID ${uuid}`), - success ? row.message_id : null - ); - } - ); - } + const success = (row && row.message_id); + return cb( + success ? null : Errors.DoesNotExist(`No message for UUID ${uuid}`), + success ? row.message_id : null + ); + } + ); + } - // :TODO: use findMessages - static getMessageIdsByMetaValue(category, name, value, cb) { - msgDb.all( - `SELECT message_id + // :TODO: use findMessages + static getMessageIdsByMetaValue(category, name, value, cb) { + msgDb.all( + `SELECT message_id FROM message_meta WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, - [ category, name, value ], - (err, rows) => { - if(err) { - return cb(err); - } - return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) - } - ); - } + [ category, name, value ], + (err, rows) => { + if(err) { + return cb(err); + } + return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) + } + ); + } - static getMetaValuesByMessageId(messageId, category, name, cb) { - const sql = + static getMetaValuesByMessageId(messageId, category, name, cb) { + const sql = `SELECT meta_value FROM message_meta WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; - msgDb.all(sql, [ messageId, category, name ], (err, rows) => { - if(err) { - return cb(err); - } + msgDb.all(sql, [ messageId, category, name ], (err, rows) => { + if(err) { + return cb(err); + } - if(0 === rows.length) { - return cb(Errors.DoesNotExist('No value for category/name')); - } + if(0 === rows.length) { + return cb(Errors.DoesNotExist('No value for category/name')); + } - // single values are returned without an array - if(1 === rows.length) { - return cb(null, rows[0].meta_value); - } + // single values are returned without an array + if(1 === rows.length) { + return cb(null, rows[0].meta_value); + } - return cb(null, rows.map(r => r.meta_value)); // map to array of values only - }); - } + return cb(null, rows.map(r => r.meta_value)); // map to array of values only + }); + } - static getMetaValuesByMessageUuid(uuid, category, name, cb) { - async.waterfall( - [ - function getMessageId(callback) { - Message.getMessageIdByUuid(uuid, (err, messageId) => { - return callback(err, messageId); - }); - }, - function getMetaValues(messageId, callback) { - Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { - return callback(err, values); - }); - } - ], - (err, values) => { - return cb(err, values); - } - ); - } + static getMetaValuesByMessageUuid(uuid, category, name, cb) { + async.waterfall( + [ + function getMessageId(callback) { + Message.getMessageIdByUuid(uuid, (err, messageId) => { + return callback(err, messageId); + }); + }, + function getMetaValues(messageId, callback) { + Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { + return callback(err, values); + }); + } + ], + (err, values) => { + return cb(err, values); + } + ); + } - loadMeta(cb) { - /* + loadMeta(cb) { + /* Example of loaded this.meta: meta: { @@ -471,154 +471,154 @@ module.exports = class Message { } } */ - const sql = + const sql = `SELECT meta_category, meta_name, meta_value FROM message_meta WHERE message_id = ?;`; - const self = this; // :TODO: not required - arrow functions below: - msgDb.each(sql, [ this.messageId ], (err, row) => { - if(!(row.meta_category in self.meta)) { - self.meta[row.meta_category] = { }; - self.meta[row.meta_category][row.meta_name] = row.meta_value; - } else { - if(!(row.meta_name in self.meta[row.meta_category])) { - self.meta[row.meta_category][row.meta_name] = row.meta_value; - } else { - if(_.isString(self.meta[row.meta_category][row.meta_name])) { - self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; - } + const self = this; // :TODO: not required - arrow functions below: + msgDb.each(sql, [ this.messageId ], (err, row) => { + if(!(row.meta_category in self.meta)) { + self.meta[row.meta_category] = { }; + self.meta[row.meta_category][row.meta_name] = row.meta_value; + } else { + if(!(row.meta_name in self.meta[row.meta_category])) { + self.meta[row.meta_category][row.meta_name] = row.meta_value; + } else { + if(_.isString(self.meta[row.meta_category][row.meta_name])) { + self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; + } - self.meta[row.meta_category][row.meta_name].push(row.meta_value); - } - } - }, err => { - return cb(err); - }); - } + self.meta[row.meta_category][row.meta_name].push(row.meta_value); + } + } + }, err => { + return cb(err); + }); + } - // :TODO: this should only take a UUID... - load(options, cb) { - assert(_.isString(options.uuid)); + // :TODO: this should only take a UUID... + load(options, cb) { + assert(_.isString(options.uuid)); - const self = this; + const self = this; - async.series( - [ - function loadMessage(callback) { - msgDb.get( - `SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, + async.series( + [ + function loadMessage(callback) { + msgDb.get( + `SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp, view_count FROM message WHERE message_uuid=? LIMIT 1;`, - [ options.uuid ], - (err, msgRow) => { - if(err) { - return callback(err); - } + [ options.uuid ], + (err, msgRow) => { + if(err) { + return callback(err); + } - if(!msgRow) { - return callback(Errors.DoesNotExist('Message (no longer) available')); - } + if(!msgRow) { + return callback(Errors.DoesNotExist('Message (no longer) available')); + } - self.messageId = msgRow.message_id; - self.areaTag = msgRow.area_tag; - self.messageUuid = msgRow.message_uuid; - self.replyToMsgId = msgRow.reply_to_message_id; - self.toUserName = msgRow.to_user_name; - self.fromUserName = msgRow.from_user_name; - self.subject = msgRow.subject; - self.message = msgRow.message; - self.modTimestamp = moment(msgRow.modified_timestamp); + self.messageId = msgRow.message_id; + self.areaTag = msgRow.area_tag; + self.messageUuid = msgRow.message_uuid; + self.replyToMsgId = msgRow.reply_to_message_id; + self.toUserName = msgRow.to_user_name; + self.fromUserName = msgRow.from_user_name; + self.subject = msgRow.subject; + self.message = msgRow.message; + self.modTimestamp = moment(msgRow.modified_timestamp); - return callback(err); - } - ); - }, - function loadMessageMeta(callback) { - self.loadMeta(err => { - return callback(err); - }); - }, - function loadHashTags(callback) { - // :TODO: - return callback(null); - } - ], - err => { - return cb(err); - } - ); - } + return callback(err); + } + ); + }, + function loadMessageMeta(callback) { + self.loadMeta(err => { + return callback(err); + }); + }, + function loadHashTags(callback) { + // :TODO: + return callback(null); + } + ], + err => { + return cb(err); + } + ); + } - persistMetaValue(category, name, value, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = msgDb; - } + persistMetaValue(category, name, value, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = msgDb; + } - const metaStmt = transOrDb.prepare( - `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) + const metaStmt = transOrDb.prepare( + `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) VALUES (?, ?, ?, ?);`); - if(!_.isArray(value)) { - value = [ value ]; - } + if(!_.isArray(value)) { + value = [ value ]; + } - const self = this; + const self = this; - async.each(value, (v, next) => { - metaStmt.run(self.messageId, category, name, v, err => { - return next(err); - }); - }, err => { - return cb(err); - }); - } + async.each(value, (v, next) => { + metaStmt.run(self.messageId, category, name, v, err => { + return next(err); + }); + }, err => { + return cb(err); + }); + } - persist(cb) { - if(!this.isValid()) { - return cb(Errors.Invalid('Cannot persist invalid message!')); - } + persist(cb) { + if(!this.isValid()) { + return cb(Errors.Invalid('Cannot persist invalid message!')); + } - const self = this; + const self = this; - async.waterfall( - [ - function beginTransaction(callback) { - return msgDb.beginTransaction(callback); - }, - function storeMessage(trans, callback) { - // generate a UUID for this message if required (general case) - const msgTimestamp = moment(); - if(!self.uuid) { - self.uuid = Message.createMessageUUID( - self.areaTag, - msgTimestamp, - self.subject, - self.message - ); - } + async.waterfall( + [ + function beginTransaction(callback) { + return msgDb.beginTransaction(callback); + }, + function storeMessage(trans, callback) { + // generate a UUID for this message if required (general case) + const msgTimestamp = moment(); + if(!self.uuid) { + self.uuid = Message.createMessageUUID( + self.areaTag, + msgTimestamp, + self.subject, + self.message + ); + } - trans.run( - `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) + trans.run( + `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, - [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], - function inserted(err) { // use non-arrow function for 'this' scope - if(!err) { - self.messageId = this.lastID; - } + [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], + function inserted(err) { // use non-arrow function for 'this' scope + if(!err) { + self.messageId = this.lastID; + } - return callback(err, trans); - } - ); - }, - function storeMeta(trans, callback) { - if(!self.meta) { - return callback(null, trans); - } - /* + return callback(err, trans); + } + ); + }, + function storeMeta(trans, callback) { + if(!self.meta) { + return callback(null, trans); + } + /* Example of self.meta: meta: { @@ -630,60 +630,60 @@ module.exports = class Message { } } */ - async.each(Object.keys(self.meta), (category, nextCat) => { - async.each(Object.keys(self.meta[category]), (name, nextName) => { - self.persistMetaValue(category, name, self.meta[category][name], trans, err => { - return nextName(err); - }); - }, err => { - return nextCat(err); - }); + async.each(Object.keys(self.meta), (category, nextCat) => { + async.each(Object.keys(self.meta[category]), (name, nextName) => { + self.persistMetaValue(category, name, self.meta[category][name], trans, err => { + return nextName(err); + }); + }, err => { + return nextCat(err); + }); - }, err => { - return callback(err, trans); - }); - }, - function storeHashTags(trans, callback) { - // :TODO: hash tag support - return callback(null, trans); - } - ], - (err, trans) => { - if(trans) { - trans[err ? 'rollback' : 'commit'](transErr => { - return cb(err ? err : transErr, self.messageId); - }); - } else { - return cb(err); - } - } - ); - } + }, err => { + return callback(err, trans); + }); + }, + function storeHashTags(trans, callback) { + // :TODO: hash tag support + return callback(null, trans); + } + ], + (err, trans) => { + if(trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(err ? err : transErr, self.messageId); + }); + } else { + return cb(err); + } + } + ); + } - // :TODO: FTN stuff doesn't have any business here - getFTNQuotePrefix(source) { - source = source || 'fromUserName'; + // :TODO: FTN stuff doesn't have any business here + getFTNQuotePrefix(source) { + source = source || 'fromUserName'; - return ftnUtil.getQuotePrefix(this[source]); - } + return ftnUtil.getQuotePrefix(this[source]); + } - getTearLinePosition(input) { - const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); - return m ? m.index : -1; - } + getTearLinePosition(input) { + const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); + return m ? m.index : -1; + } - getQuoteLines(options, cb) { - if(!options.termWidth || !options.termHeight || !options.cols) { - return cb(Errors.MissingParam()); - } + getQuoteLines(options, cb) { + if(!options.termWidth || !options.termHeight || !options.cols) { + return cb(Errors.MissingParam()); + } - options.startCol = options.startCol || 1; - options.includePrefix = _.get(options, 'includePrefix', true); - options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); - options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); - options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting + options.startCol = options.startCol || 1; + options.includePrefix = _.get(options, 'includePrefix', true); + options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); + options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); + options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting - /* + /* Some long text that needs to be wrapped and quoted should look right after doing so, don't ya think? yeah I think so @@ -694,166 +694,166 @@ module.exports = class Message { Ot> Nu> right after doing so, don't ya think? yeah I think so */ - const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; + const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; - function getWrapped(text, extraPrefix) { - extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; + function getWrapped(text, extraPrefix) { + extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; - const wrapOpts = { - width : options.cols - (quotePrefix.length + extraPrefix.length), - tabHandling : 'expand', - tabWidth : 4, - }; + const wrapOpts = { + width : options.cols - (quotePrefix.length + extraPrefix.length), + tabHandling : 'expand', + tabWidth : 4, + }; - return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { - return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; - }); - } + return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { + return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; + }); + } - function getFormattedLine(line) { - // for pre-formatted text, we just append a line truncated to fit - let newLen; - const total = line.length + quotePrefix.length; + 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; - } + if(total > options.cols) { + newLen = options.cols - total; + } else { + newLen = total; + } - return `${quotePrefix}${line.slice(0, newLen)}`; - } + 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; + 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); + let lastSgr = ''; + const split = splitTextAtTerms(prepped); - const quoteLines = []; - const focusQuoteLines = []; + const quoteLines = []; + const focusQuoteLines = []; - // - // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) - // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to - // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do - // the trick and allow them to leave them alone! - // - split.forEach(l => { - quoteLines.push(`${lastSgr}${l}`); + // + // 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 - }); + 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; + 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, ''); + 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 + // 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; + 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); - } + 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); - } + 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); + 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; + 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; + 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; - } - }); + 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)); - }); + quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); + }); - input.slice(tearLinePos).split(/\r?\n/).forEach(l => { - quoted.push(...getWrapped(l)); - }); + input.slice(tearLinePos).split(/\r?\n/).forEach(l => { + quoted.push(...getWrapped(l)); + }); - return cb(null, quoted, null, false); - } - } + return cb(null, quoted, null, false); + } + } }; diff --git a/core/message_area.js b/core/message_area.js index 44a94c9a..03cc914c 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -35,252 +35,252 @@ exports.persistMessage = persistMessage; exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; function getAvailableMessageConferences(client, options) { - options = options || { includeSystemInternal : false }; + options = options || { includeSystemInternal : false }; - assert(client || true === options.noClient); + assert(client || true === options.noClient); - // perform ACS check per conf & omit system_internal if desired - return _.omitBy(Config().messageConferences, (conf, confTag) => { - if(!options.includeSystemInternal && 'system_internal' === confTag) { - return true; - } + // perform ACS check per conf & omit system_internal if desired + return _.omitBy(Config().messageConferences, (conf, confTag) => { + if(!options.includeSystemInternal && 'system_internal' === confTag) { + return true; + } - return client && !client.acs.hasMessageConfRead(conf); - }); + return client && !client.acs.hasMessageConfRead(conf); + }); } function getSortedAvailMessageConferences(client, options) { - const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => { - return { - confTag : k, - conf : v, - }; - }); + const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => { + return { + confTag : k, + conf : v, + }; + }); - sortAreasOrConfs(confs, 'conf'); + sortAreasOrConfs(confs, 'conf'); - return confs; + return confs; } // Return an *object* of available areas within |confTag| function getAvailableMessageAreasByConfTag(confTag, options) { - options = options || {}; + options = options || {}; - // :TODO: confTag === "" then find default + // :TODO: confTag === "" then find default - const config = Config(); - if(_.has(config.messageConferences, [ confTag, 'areas' ])) { - const areas = config.messageConferences[confTag].areas; + const config = Config(); + if(_.has(config.messageConferences, [ confTag, 'areas' ])) { + const areas = config.messageConferences[confTag].areas; - if(!options.client || true === options.noAcsCheck) { - // everything - no ACS checks - return areas; - } else { - // perform ACS check per area - return _.omitBy(areas, area => { - return !options.client.acs.hasMessageAreaRead(area); - }); - } - } + if(!options.client || true === options.noAcsCheck) { + // everything - no ACS checks + return areas; + } else { + // perform ACS check per area + return _.omitBy(areas, area => { + return !options.client.acs.hasMessageAreaRead(area); + }); + } + } } function getSortedAvailMessageAreasByConfTag(confTag, options) { - const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => { - return { - areaTag : k, - area : v, - }; - }); + const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => { + return { + areaTag : k, + area : v, + }; + }); - sortAreasOrConfs(areas, 'area'); + sortAreasOrConfs(areas, 'area'); - return areas; + return areas; } function getDefaultMessageConferenceTag(client, disableAcsCheck) { - // - // Find the first conference marked 'default'. If found, - // inspect |client| against *read* ACS using defaults if not - // specified. - // - // If the above fails, just go down the list until we get one - // that passes. - // - // It's possible that we end up with nothing here! - // - // Note that built in 'system_internal' is always ommited here - // - const config = Config(); - let defaultConf = _.findKey(config.messageConferences, o => o.default); - if(defaultConf) { - const conf = config.messageConferences[defaultConf]; - if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { - return defaultConf; - } - } + // + // Find the first conference marked 'default'. If found, + // inspect |client| against *read* ACS using defaults if not + // specified. + // + // If the above fails, just go down the list until we get one + // that passes. + // + // It's possible that we end up with nothing here! + // + // Note that built in 'system_internal' is always ommited here + // + const config = Config(); + let defaultConf = _.findKey(config.messageConferences, o => o.default); + if(defaultConf) { + const conf = config.messageConferences[defaultConf]; + if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { + return defaultConf; + } + } - // just use anything we can - defaultConf = _.findKey(config.messageConferences, (conf, confTag) => { - return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)); - }); + // just use anything we can + defaultConf = _.findKey(config.messageConferences, (conf, confTag) => { + return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)); + }); - return defaultConf; + return defaultConf; } function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { - // - // Similar to finding the default conference: - // Find the first entry marked 'default', if any. If found, check | client| against - // *read* ACS. If this fails, just find the first one we can that passes checks. - // - // It's possible that we end up with nothing! - // - confTag = confTag || getDefaultMessageConferenceTag(client); + // + // Similar to finding the default conference: + // Find the first entry marked 'default', if any. If found, check | client| against + // *read* ACS. If this fails, just find the first one we can that passes checks. + // + // It's possible that we end up with nothing! + // + confTag = confTag || getDefaultMessageConferenceTag(client); - const config = Config(); - if(confTag && _.has(config.messageConferences, [ confTag, 'areas' ])) { - const areaPool = config.messageConferences[confTag].areas; - let defaultArea = _.findKey(areaPool, o => o.default); - if(defaultArea) { - const area = areaPool[defaultArea]; - if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) { - return defaultArea; - } - } + const config = Config(); + if(confTag && _.has(config.messageConferences, [ confTag, 'areas' ])) { + const areaPool = config.messageConferences[confTag].areas; + let defaultArea = _.findKey(areaPool, o => o.default); + if(defaultArea) { + const area = areaPool[defaultArea]; + if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) { + return defaultArea; + } + } - defaultArea = _.findKey(areaPool, (area) => { - return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); - }); + defaultArea = _.findKey(areaPool, (area) => { + return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); + }); - return defaultArea; - } + return defaultArea; + } } function getMessageConferenceByTag(confTag) { - return Config().messageConferences[confTag]; + return Config().messageConferences[confTag]; } function getMessageConfTagByAreaTag(areaTag) { - const confs = Config().messageConferences; - return Object.keys(confs).find( (confTag) => { - return _.has(confs, [ confTag, 'areas', areaTag]); - }); + const confs = Config().messageConferences; + return Object.keys(confs).find( (confTag) => { + return _.has(confs, [ confTag, 'areas', areaTag]); + }); } function getMessageAreaByTag(areaTag, optionalConfTag) { - const confs = Config().messageConferences; + const confs = Config().messageConferences; - // :TODO: this could be cached - if(_.isString(optionalConfTag)) { - if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { - return confs[optionalConfTag].areas[areaTag]; - } - } else { - // - // No confTag to work with - we'll have to search through them all - // - let area; - _.forEach(confs, (v) => { - if(_.has(v, [ 'areas', areaTag ])) { - area = v.areas[areaTag]; - return false; // stop iteration - } - }); + // :TODO: this could be cached + if(_.isString(optionalConfTag)) { + if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { + return confs[optionalConfTag].areas[areaTag]; + } + } else { + // + // No confTag to work with - we'll have to search through them all + // + let area; + _.forEach(confs, (v) => { + if(_.has(v, [ 'areas', areaTag ])) { + area = v.areas[areaTag]; + return false; // stop iteration + } + }); - return area; - } + return area; + } } function changeMessageConference(client, confTag, cb) { - async.waterfall( - [ - function getConf(callback) { - const conf = getMessageConferenceByTag(confTag); + async.waterfall( + [ + function getConf(callback) { + const conf = getMessageConferenceByTag(confTag); - if(conf) { - callback(null, conf); - } else { - callback(new Error('Invalid message conference tag')); - } - }, - function getDefaultAreaInConf(conf, callback) { - const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); - const area = getMessageAreaByTag(areaTag, confTag); + if(conf) { + callback(null, conf); + } else { + callback(new Error('Invalid message conference tag')); + } + }, + function getDefaultAreaInConf(conf, callback) { + const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); + const area = getMessageAreaByTag(areaTag, confTag); - if(area) { - callback(null, conf, { areaTag : areaTag, area : area } ); - } else { - callback(new Error('No available areas for this user in conference')); - } - }, - function validateAccess(conf, areaInfo, callback) { - if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(areaInfo.area)) { - return callback(new Error('Access denied to message area and/or conference')); - } else { - return callback(null, conf, areaInfo); - } - }, - function changeConferenceAndArea(conf, areaInfo, callback) { - const newProps = { - message_conf_tag : confTag, - message_area_tag : areaInfo.areaTag, - }; - client.user.persistProperties(newProps, err => { - callback(err, conf, areaInfo); - }); - }, - ], - function complete(err, conf, areaInfo) { - if(!err) { - client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed'); - } else { - client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference'); - } - cb(err); - } - ); + if(area) { + callback(null, conf, { areaTag : areaTag, area : area } ); + } else { + callback(new Error('No available areas for this user in conference')); + } + }, + function validateAccess(conf, areaInfo, callback) { + if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(areaInfo.area)) { + return callback(new Error('Access denied to message area and/or conference')); + } else { + return callback(null, conf, areaInfo); + } + }, + function changeConferenceAndArea(conf, areaInfo, callback) { + const newProps = { + message_conf_tag : confTag, + message_area_tag : areaInfo.areaTag, + }; + client.user.persistProperties(newProps, err => { + callback(err, conf, areaInfo); + }); + }, + ], + function complete(err, conf, areaInfo) { + if(!err) { + client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed'); + } else { + client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference'); + } + cb(err); + } + ); } function changeMessageAreaWithOptions(client, areaTag, options, cb) { - options = options || {}; // :TODO: this is currently pointless... cb is required... + options = options || {}; // :TODO: this is currently pointless... cb is required... - async.waterfall( - [ - function getArea(callback) { - const area = getMessageAreaByTag(areaTag); - return callback(area ? null : new Error('Invalid message areaTag'), area); - }, - function validateAccess(area, callback) { - // - // Need at least *read* to access the area - // - if(!client.acs.hasMessageAreaRead(area)) { - return callback(new Error('Access denied to message area')); - } else { - return callback(null, area); - } - }, - function changeArea(area, callback) { - if(true === options.persist) { - client.user.persistProperty('message_area_tag', areaTag, function persisted(err) { - return callback(err, area); - }); - } else { - client.user.properties['message_area_tag'] = areaTag; - return callback(null, area); - } - } - ], - function complete(err, area) { - if(!err) { - client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed'); - } else { - client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area'); - } + async.waterfall( + [ + function getArea(callback) { + const area = getMessageAreaByTag(areaTag); + return callback(area ? null : new Error('Invalid message areaTag'), area); + }, + function validateAccess(area, callback) { + // + // Need at least *read* to access the area + // + if(!client.acs.hasMessageAreaRead(area)) { + return callback(new Error('Access denied to message area')); + } else { + return callback(null, area); + } + }, + function changeArea(area, callback) { + if(true === options.persist) { + client.user.persistProperty('message_area_tag', areaTag, function persisted(err) { + return callback(err, area); + }); + } else { + client.user.properties['message_area_tag'] = areaTag; + return callback(null, area); + } + } + ], + function complete(err, area) { + if(!err) { + client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed'); + } else { + client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area'); + } - return cb(err); - } - ); + return cb(err); + } + ); } // @@ -290,185 +290,185 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { // This is useful for example when doing a new scan // function tempChangeMessageConfAndArea(client, areaTag) { - const area = getMessageAreaByTag(areaTag); - const confTag = getMessageConfTagByAreaTag(areaTag); + const area = getMessageAreaByTag(areaTag); + const confTag = getMessageConfTagByAreaTag(areaTag); - if(!area || !confTag) { - return false; - } + if(!area || !confTag) { + return false; + } - const conf = getMessageConferenceByTag(confTag); + const conf = getMessageConferenceByTag(confTag); - if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { - return false; - } + if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { + return false; + } - client.user.properties.message_conf_tag = confTag; - client.user.properties.message_area_tag = areaTag; + client.user.properties.message_conf_tag = confTag; + client.user.properties.message_area_tag = areaTag; - return true; + return true; } function changeMessageArea(client, areaTag, cb) { - changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); + changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); } function getNewMessageCountInAreaForUser(userId, areaTag, cb) { - getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { - lastMessageId = lastMessageId || 0; + getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { + lastMessageId = lastMessageId || 0; - const filter = { - areaTag, - newerThanMessageId : lastMessageId, - resultType : 'count', - }; + const filter = { + areaTag, + newerThanMessageId : lastMessageId, + resultType : 'count', + }; - if(Message.isPrivateAreaTag(areaTag)) { - filter.privateTagUserId = userId; - } + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = userId; + } - Message.findMessages(filter, (err, count) => { - return cb(err, count); - }); - }); + Message.findMessages(filter, (err, count) => { + return cb(err, count); + }); + }); } function getNewMessagesInAreaForUser(userId, areaTag, cb) { - getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { - lastMessageId = lastMessageId || 0; + getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { + lastMessageId = lastMessageId || 0; - const filter = { - areaTag, - resultType : 'messageList', - newerThanMessageId : lastMessageId, - sort : 'messageId', - order : 'ascending', - }; + const filter = { + areaTag, + resultType : 'messageList', + newerThanMessageId : lastMessageId, + sort : 'messageId', + order : 'ascending', + }; - if(Message.isPrivateAreaTag(areaTag)) { - filter.privateTagUserId = userId; - } + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = userId; + } - return Message.findMessages(filter, cb); - }); + return Message.findMessages(filter, cb); + }); } function getMessageListForArea(client, areaTag, cb) { - const filter = { - areaTag, - resultType : 'messageList', - sort : 'messageId', - order : 'ascending', - }; + const filter = { + areaTag, + resultType : 'messageList', + sort : 'messageId', + order : 'ascending', + }; - if(Message.isPrivateAreaTag(areaTag)) { - filter.privateTagUserId = client.user.userId; - } + if(Message.isPrivateAreaTag(areaTag)) { + filter.privateTagUserId = client.user.userId; + } - return Message.findMessages(filter, cb); + return Message.findMessages(filter, cb); } function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { - Message.findMessages( - { - areaTag, - newerThanTimestamp, - sort : 'modTimestamp', - order : 'ascending', - limit : 1, - }, - (err, id) => { - if(err) { - return cb(err); - } - return cb(null, id ? id[0] : null); - } - ); + Message.findMessages( + { + areaTag, + newerThanTimestamp, + sort : 'modTimestamp', + order : 'ascending', + limit : 1, + }, + (err, id) => { + if(err) { + return cb(err); + } + return cb(null, id ? id[0] : null); + } + ); } function getMessageAreaLastReadId(userId, areaTag, cb) { - msgDb.get( - 'SELECT message_id ' + + msgDb.get( + 'SELECT message_id ' + 'FROM user_message_area_last_read ' + 'WHERE user_id = ? AND area_tag = ?;', - [ userId, areaTag.toLowerCase() ], - function complete(err, row) { - cb(err, row ? row.message_id : 0); - } - ); + [ userId, areaTag.toLowerCase() ], + function complete(err, row) { + cb(err, row ? row.message_id : 0); + } + ); } function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) { - if(!cb && _.isFunction(allowOlder)) { - cb = allowOlder; - allowOlder = false; - } + if(!cb && _.isFunction(allowOlder)) { + cb = allowOlder; + allowOlder = false; + } - // :TODO: likely a better way to do this... - async.waterfall( - [ - function getCurrent(callback) { - getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) { - lastId = lastId || 0; - callback(null, lastId); // ignore errors as we default to 0 - }); - }, - function update(lastId, callback) { - if(allowOlder || messageId > lastId) { - msgDb.run( - 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + + // :TODO: likely a better way to do this... + async.waterfall( + [ + function getCurrent(callback) { + getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) { + lastId = lastId || 0; + callback(null, lastId); // ignore errors as we default to 0 + }); + }, + function update(lastId, callback) { + if(allowOlder || messageId > lastId) { + msgDb.run( + 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + 'VALUES (?, ?, ?);', - [ userId, areaTag, messageId ], - function written(err) { - callback(err, true); // true=didUpdate - } - ); - } else { - callback(null); - } - } - ], - function complete(err, didUpdate) { - if(err) { - Log.debug( - { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, - 'Failed updating area last read ID'); - } else { - if(true === didUpdate) { - Log.trace( - { userId : userId, areaTag : areaTag, messageId : messageId }, - 'Area last read ID updated'); - } - } - cb(err); - } - ); + [ userId, areaTag, messageId ], + function written(err) { + callback(err, true); // true=didUpdate + } + ); + } else { + callback(null); + } + } + ], + function complete(err, didUpdate) { + if(err) { + Log.debug( + { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, + 'Failed updating area last read ID'); + } else { + if(true === didUpdate) { + Log.trace( + { userId : userId, areaTag : areaTag, messageId : messageId }, + 'Area last read ID updated'); + } + } + cb(err); + } + ); } function persistMessage(message, cb) { - async.series( - [ - function persistMessageToDisc(callback) { - return message.persist(callback); - }, - function recordToMessageNetworks(callback) { - return msgNetRecord(message, callback); - } - ], - cb - ); + async.series( + [ + function persistMessageToDisc(callback) { + return message.persist(callback); + }, + function recordToMessageNetworks(callback) { + return msgNetRecord(message, callback); + } + ], + cb + ); } // method exposed for event scheduler function trimMessageAreasScheduledEvent(args, cb) { - function trimMessageAreaByMaxMessages(areaInfo, cb) { - if(0 === areaInfo.maxMessages) { - return cb(null); - } + function trimMessageAreaByMaxMessages(areaInfo, cb) { + if(0 === areaInfo.maxMessages) { + return cb(null); + } - msgDb.run( - `DELETE FROM message + msgDb.run( + `DELETE FROM message WHERE message_id IN( SELECT message_id FROM message @@ -476,124 +476,124 @@ function trimMessageAreasScheduledEvent(args, cb) { ORDER BY message_id DESC LIMIT -1 OFFSET ${areaInfo.maxMessages} );`, - [ areaInfo.areaTag.toLowerCase() ], - function result(err) { // no arrow func; need this - if(err) { - Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area'); - } else { - Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully'); - } - return cb(err); - } - ); - } + [ areaInfo.areaTag.toLowerCase() ], + function result(err) { // no arrow func; need this + if(err) { + Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area'); + } else { + Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully'); + } + return cb(err); + } + ); + } - function trimMessageAreaByMaxAgeDays(areaInfo, cb) { - if(0 === areaInfo.maxAgeDays) { - return cb(null); - } + function trimMessageAreaByMaxAgeDays(areaInfo, cb) { + if(0 === areaInfo.maxAgeDays) { + return cb(null); + } - msgDb.run( - `DELETE FROM message + msgDb.run( + `DELETE FROM message WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`, - [ areaInfo.areaTag ], - function result(err) { // no arrow func; need this - if(err) { - Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area'); - } else { - Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays', count : this.changes }, 'Area trimmed successfully'); - } - return cb(err); - } - ); - } + [ areaInfo.areaTag ], + function result(err) { // no arrow func; need this + if(err) { + Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area'); + } else { + Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays', count : this.changes }, 'Area trimmed successfully'); + } + return cb(err); + } + ); + } - async.waterfall( - [ - function getAreaTags(callback) { - const areaTags = []; + async.waterfall( + [ + function getAreaTags(callback) { + const areaTags = []; - // - // We use SQL here vs API such that no-longer-used tags are picked up - // - msgDb.each( - `SELECT DISTINCT area_tag + // + // We use SQL here vs API such that no-longer-used tags are picked up + // + msgDb.each( + `SELECT DISTINCT area_tag FROM message;`, - (err, row) => { - if(err) { - return callback(err); - } + (err, row) => { + if(err) { + return callback(err); + } - // We treat private mail special - if(!Message.isPrivateAreaTag(row.area_tag)) { - areaTags.push(row.area_tag); - } - }, - err => { - return callback(err, areaTags); - } - ); - }, - function prepareAreaInfo(areaTags, callback) { - let areaInfos = []; + // We treat private mail special + if(!Message.isPrivateAreaTag(row.area_tag)) { + areaTags.push(row.area_tag); + } + }, + err => { + return callback(err, areaTags); + } + ); + }, + function prepareAreaInfo(areaTags, callback) { + let areaInfos = []; - // determine maxMessages & maxAgeDays per area - const config = Config(); - areaTags.forEach(areaTag => { + // determine maxMessages & maxAgeDays per area + const config = Config(); + areaTags.forEach(areaTag => { - let maxMessages = config.messageAreaDefaults.maxMessages; - let maxAgeDays = config.messageAreaDefaults.maxAgeDays; + let maxMessages = config.messageAreaDefaults.maxMessages; + let maxAgeDays = config.messageAreaDefaults.maxAgeDays; - const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here - if(area) { - maxMessages = area.maxMessages || maxMessages; - maxAgeDays = area.maxAgeDays || maxAgeDays; - } + const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here + if(area) { + maxMessages = area.maxMessages || maxMessages; + maxAgeDays = area.maxAgeDays || maxAgeDays; + } - areaInfos.push( { - areaTag : areaTag, - maxMessages : maxMessages, - maxAgeDays : maxAgeDays, - } ); - }); + areaInfos.push( { + areaTag : areaTag, + maxMessages : maxMessages, + maxAgeDays : maxAgeDays, + } ); + }); - return callback(null, areaInfos); - }, - function trimGeneralAreas(areaInfos, callback) { - async.each( - areaInfos, - (areaInfo, next) => { - trimMessageAreaByMaxMessages(areaInfo, err => { - if(err) { - return next(err); - } + return callback(null, areaInfos); + }, + function trimGeneralAreas(areaInfos, callback) { + async.each( + areaInfos, + (areaInfo, next) => { + trimMessageAreaByMaxMessages(areaInfo, err => { + if(err) { + return next(err); + } - trimMessageAreaByMaxAgeDays(areaInfo, err => { - return next(err); - }); - }); - }, - callback - ); - }, - function trimExternalPrivateSentMail(callback) { - // - // *External* (FTN, email, ...) outgoing is cleaned up *after export* - // if it is older than the configured |maxExternalSentAgeDays| days - // - // Outgoing externally exported private mail is: - // - In the 'private_mail' area - // - Marked exported (state_flags0 exported bit set) - // - Marked with any external flavor (we don't mark local) - // - const maxExternalSentAgeDays = _.get( - Config, - 'messageConferences.system_internal.areas.private_mail.maxExternalSentAgeDays', - 30 - ); + trimMessageAreaByMaxAgeDays(areaInfo, err => { + return next(err); + }); + }); + }, + callback + ); + }, + function trimExternalPrivateSentMail(callback) { + // + // *External* (FTN, email, ...) outgoing is cleaned up *after export* + // if it is older than the configured |maxExternalSentAgeDays| days + // + // Outgoing externally exported private mail is: + // - In the 'private_mail' area + // - Marked exported (state_flags0 exported bit set) + // - Marked with any external flavor (we don't mark local) + // + const maxExternalSentAgeDays = _.get( + Config, + 'messageConferences.system_internal.areas.private_mail.maxExternalSentAgeDays', + 30 + ); - msgDb.run( - `DELETE FROM message + msgDb.run( + `DELETE FROM message WHERE message_id IN ( SELECT m.message_id FROM message m @@ -605,20 +605,20 @@ function trimMessageAreasScheduledEvent(args, cb) { (mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}') WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days') );`, - function results(err) { // no arrow func; need this - if(err) { - Log.warn( { error : err.message }, 'Error trimming private externally sent messages'); - } else { - Log.debug( { count : this.changes }, 'Private externally sent messages trimmed successfully'); - } - } - ); + function results(err) { // no arrow func; need this + if(err) { + Log.warn( { error : err.message }, 'Error trimming private externally sent messages'); + } else { + Log.debug( { count : this.changes }, 'Private externally sent messages trimmed successfully'); + } + } + ); - return callback(null); - } - ], - err => { - return cb(err); - } - ); + return callback(null); + } + ], + err => { + return cb(err); + } + ); } \ No newline at end of file diff --git a/core/message_base_search.js b/core/message_base_search.js index fdb17859..98f78552 100644 --- a/core/message_base_search.js +++ b/core/message_base_search.js @@ -4,9 +4,9 @@ // ENiGMA½ const MenuModule = require('./menu_module.js').MenuModule; const { - getSortedAvailMessageConferences, - getAvailableMessageAreasByConfTag, - getSortedAvailMessageAreasByConfTag, + getSortedAvailMessageConferences, + getAvailableMessageAreasByConfTag, + getSortedAvailMessageAreasByConfTag, } = require('./message_area.js'); const Errors = require('./enig_error.js').Errors; const Message = require('./message.js'); @@ -15,134 +15,134 @@ const Message = require('./message.js'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Base Search', - desc : 'Module for quickly searching the message base', - author : 'NuSkooler', + name : 'Message Base Search', + desc : 'Module for quickly searching the message base', + author : 'NuSkooler', }; const MciViewIds = { - search : { - searchTerms : 1, - search : 2, - conf : 3, - area : 4, - to : 5, - from : 6, - advSearch : 7, - } + search : { + searchTerms : 1, + search : 2, + conf : 3, + area : 4, + to : 5, + from : 6, + advSearch : 7, + } }; exports.getModule = class MessageBaseSearch extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.menuMethods = { - search : (formData, extraArgs, cb) => { - return this.searchNow(formData, cb); - } - }; - } + this.menuMethods = { + search : (formData, extraArgs, cb) => { + return this.searchNow(formData, cb); + } + }; + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - this.prepViewController('search', 0, mciData.menu, (err, vc) => { - if(err) { - return cb(err); - } + this.prepViewController('search', 0, mciData.menu, (err, vc) => { + if(err) { + return cb(err); + } - const confView = vc.getView(MciViewIds.search.conf); - const areaView = vc.getView(MciViewIds.search.area); + const confView = vc.getView(MciViewIds.search.conf); + const areaView = vc.getView(MciViewIds.search.area); - if(!confView || !areaView) { - return cb(Errors.DoesNotExist('Missing one or more required views')); - } + if(!confView || !areaView) { + return cb(Errors.DoesNotExist('Missing one or more required views')); + } - const availConfs = [ { text : '-ALL-', data : '' } ].concat( - getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || [] - ); + const availConfs = [ { text : '-ALL-', data : '' } ].concat( + getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || [] + ); - let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL + let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL - confView.setItems(availConfs); - areaView.setItems(availAreas); + confView.setItems(availConfs); + areaView.setItems(availAreas); - confView.setFocusItemIndex(0); - areaView.setFocusItemIndex(0); + confView.setFocusItemIndex(0); + areaView.setFocusItemIndex(0); - confView.on('index update', idx => { - availAreas = [ { text : '-ALL-', data : '' } ].concat( - getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map( - area => Object.assign(area, { text : area.area.name, data : area.areaTag } ) - ) - ); - areaView.setItems(availAreas); - areaView.setFocusItemIndex(0); - }); + confView.on('index update', idx => { + availAreas = [ { text : '-ALL-', data : '' } ].concat( + getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map( + area => Object.assign(area, { text : area.area.name, data : area.areaTag } ) + ) + ); + areaView.setItems(availAreas); + areaView.setFocusItemIndex(0); + }); - vc.switchFocus(MciViewIds.search.searchTerms); - return cb(null); - }); - }); - } + vc.switchFocus(MciViewIds.search.searchTerms); + return cb(null); + }); + }); + } - searchNow(formData, cb) { - const isAdvanced = formData.submitId === MciViewIds.search.advSearch; - const value = formData.value; + searchNow(formData, cb) { + const isAdvanced = formData.submitId === MciViewIds.search.advSearch; + const value = formData.value; - const filter = { - resultType : 'messageList', - sort : 'modTimestamp', - terms : value.searchTerms, - //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], - limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned - }; + const filter = { + resultType : 'messageList', + sort : 'modTimestamp', + terms : value.searchTerms, + //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned + }; - if(isAdvanced) { - filter.toUserName = value.toUserName; - filter.fromUserName = value.fromUserName; + if(isAdvanced) { + filter.toUserName = value.toUserName; + filter.fromUserName = value.fromUserName; - if(value.confTag && !value.areaTag) { - // areaTag may be a string or array of strings - // getAvailableMessageAreasByConfTag() returns a obj - we only need tags - filter.areaTag = _.map( - getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ), - (area, areaTag) => areaTag - ); - } else if(value.areaTag) { - filter.areaTag = value.areaTag; // specific conf + area - } - } + if(value.confTag && !value.areaTag) { + // areaTag may be a string or array of strings + // getAvailableMessageAreasByConfTag() returns a obj - we only need tags + filter.areaTag = _.map( + getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ), + (area, areaTag) => areaTag + ); + } else if(value.areaTag) { + filter.areaTag = value.areaTag; // specific conf + area + } + } - Message.findMessages(filter, (err, messageList) => { - if(err) { - return cb(err); - } + Message.findMessages(filter, (err, messageList) => { + if(err) { + return cb(err); + } - if(0 === messageList.length) { - return this.gotoMenu( - this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', - { menuFlags : [ 'popParent' ] }, - cb - ); - } + if(0 === messageList.length) { + return this.gotoMenu( + this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', + { menuFlags : [ 'popParent' ] }, + cb + ); + } - const menuOpts = { - extraArgs : { - messageList, - noUpdateLastReadId : true - }, - menuFlags : [ 'popParent' ], - }; + const menuOpts = { + extraArgs : { + messageList, + noUpdateLastReadId : true + }, + menuFlags : [ 'popParent' ], + }; - return this.gotoMenu( - this.menuConfig.config.messageListMenu || 'messageAreaMessageList', - menuOpts, - cb - ); - }); - } + return this.gotoMenu( + this.menuConfig.config.messageListMenu || 'messageAreaMessageList', + menuOpts, + cb + ); + }); + } }; diff --git a/core/mime_util.js b/core/mime_util.js index b9a7c5af..d6631077 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -10,33 +10,33 @@ exports.startup = startup; exports.resolveMimeType = resolveMimeType; function startup(cb) { - // - // Add in types (not yet) supported by mime-db -- and therefor, mime-types - // - const ADDITIONAL_EXT_MIMETYPES = { - ans : 'text/x-ansi', - gz : 'application/gzip', // not in mime-types 2.1.15 :( - lzx : 'application/x-lzx', // :TODO: submit to mime-types - }; + // + // Add in types (not yet) supported by mime-db -- and therefor, mime-types + // + const ADDITIONAL_EXT_MIMETYPES = { + ans : 'text/x-ansi', + gz : 'application/gzip', // not in mime-types 2.1.15 :( + lzx : 'application/x-lzx', // :TODO: submit to mime-types + }; - _.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => { - // don't override any entries - if(!_.isString(mimeTypes.types[ext])) { - mimeTypes[ext] = mimeType; - } + _.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => { + // don't override any entries + if(!_.isString(mimeTypes.types[ext])) { + mimeTypes[ext] = mimeType; + } - if(!mimeTypes.extensions[mimeType]) { - mimeTypes.extensions[mimeType] = [ ext ]; - } - }); + if(!mimeTypes.extensions[mimeType]) { + mimeTypes.extensions[mimeType] = [ ext ]; + } + }); - return cb(null); + return cb(null); } function resolveMimeType(query) { - if(mimeTypes.extensions[query]) { - return query; // alreaed a mime-type - } + if(mimeTypes.extensions[query]) { + return query; // alreaed a mime-type + } - return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined + return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined } \ No newline at end of file diff --git a/core/misc_util.js b/core/misc_util.js index 70bbb5e2..3a75d065 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -14,39 +14,39 @@ exports.getCleanEnigmaVersion = getCleanEnigmaVersion; exports.getEnigmaUserAgent = getEnigmaUserAgent; function isProduction() { - var env = process.env.NODE_ENV || 'dev'; - return 'production' === env; + var env = process.env.NODE_ENV || 'dev'; + return 'production' === env; } function isDevelopment() { - return (!(isProduction())); + return (!(isProduction())); } function valueWithDefault(val, defVal) { - return (typeof val !== 'undefined' ? val : defVal); + return (typeof val !== 'undefined' ? val : defVal); } function resolvePath(path) { - if(path.substr(0, 2) === '~/') { - var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH; - path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1); - } - return paths.resolve(path); + if(path.substr(0, 2) === '~/') { + var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH; + path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1); + } + return paths.resolve(path); } function getCleanEnigmaVersion() { - return packageJson.version - .replace(/-/g, '.') - .replace(/alpha/,'a') - .replace(/beta/,'b') - ; + return packageJson.version + .replace(/-/g, '.') + .replace(/alpha/,'a') + .replace(/beta/,'b') + ; } // See also ftn_util.js getTearLine() & getProductIdentifier() function getEnigmaUserAgent() { - // can't have 1/2 or ½ in User-Agent according to RFC 1945 :( - const version = getCleanEnigmaVersion(); - const nodeVer = process.version.substr(1); // remove 'v' prefix + // can't have 1/2 or ½ in User-Agent according to RFC 1945 :( + const version = getCleanEnigmaVersion(); + const nodeVer = process.version.substr(1); // remove 'v' prefix - return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; + return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } \ No newline at end of file diff --git a/core/mod_mixins.js b/core/mod_mixins.js index c830813a..fd9db771 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -7,28 +7,28 @@ const { get } = require('lodash'); exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { - tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { - messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); - if(!messageAreaTag) { - return; // nothing to do! - } + tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { + messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); + if(!messageAreaTag) { + return; // nothing to do! + } - if(recordPrevious) { - this.prevMessageConfAndArea = { - confTag : this.client.user.properties.message_conf_tag, - areaTag : this.client.user.properties.message_area_tag, - }; - } + if(recordPrevious) { + this.prevMessageConfAndArea = { + confTag : this.client.user.properties.message_conf_tag, + areaTag : this.client.user.properties.message_area_tag, + }; + } - if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) { - this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch'); - } - } + if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) { + this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch'); + } + } - tempMessageConfAndAreaRestore() { - if(this.prevMessageConfAndArea) { - this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; - this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; - } - } + tempMessageConfAndAreaRestore() { + if(this.prevMessageConfAndArea) { + this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; + this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; + } + } }; diff --git a/core/module_util.js b/core/module_util.js index 3bfd88d2..26a1ec53 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -18,93 +18,93 @@ exports.loadModulesForCategory = loadModulesForCategory; exports.getModulePaths = getModulePaths; function loadModuleEx(options, cb) { - assert(_.isObject(options)); - assert(_.isString(options.name)); - assert(_.isString(options.path)); + assert(_.isObject(options)); + assert(_.isString(options.name)); + assert(_.isString(options.path)); - const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; + const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; - if(_.isObject(modConfig) && false === modConfig.enabled) { - const err = new Error(`Module "${options.name}" is disabled`); - err.code = 'EENIGMODDISABLED'; - return cb(err); - } + if(_.isObject(modConfig) && false === modConfig.enabled) { + const err = new Error(`Module "${options.name}" is disabled`); + err.code = 'EENIGMODDISABLED'; + return cb(err); + } - // - // Modules are allowed to live in /path/to//.js or - // simply in /path/to/.js. This allows for more advanced modules - // to have their own containing folder, package.json & dependencies, etc. - // - let mod; - let modPath = paths.join(options.path, `${options.name}.js`); // general case first - try { - mod = require(modPath); - } catch(e) { - if('MODULE_NOT_FOUND' === e.code) { - modPath = paths.join(options.path, options.name, `${options.name}.js`); - try { - mod = require(modPath); - } catch(e) { - return cb(e); - } - } else { - return cb(e); - } - } + // + // Modules are allowed to live in /path/to//.js or + // simply in /path/to/.js. This allows for more advanced modules + // to have their own containing folder, package.json & dependencies, etc. + // + let mod; + let modPath = paths.join(options.path, `${options.name}.js`); // general case first + try { + mod = require(modPath); + } catch(e) { + if('MODULE_NOT_FOUND' === e.code) { + modPath = paths.join(options.path, options.name, `${options.name}.js`); + try { + mod = require(modPath); + } catch(e) { + return cb(e); + } + } else { + return cb(e); + } + } - if(!_.isObject(mod.moduleInfo)) { - return cb(new Error('Module is missing "moduleInfo" section')); - } + if(!_.isObject(mod.moduleInfo)) { + return cb(new Error('Module is missing "moduleInfo" section')); + } - if(!_.isFunction(mod.getModule)) { - return cb(new Error('Invalid or missing "getModule" method for module!')); - } + if(!_.isFunction(mod.getModule)) { + return cb(new Error('Invalid or missing "getModule" method for module!')); + } - return cb(null, mod); + return cb(null, mod); } function loadModule(name, category, cb) { - const path = Config().paths[category]; + const path = Config().paths[category]; - if(!_.isString(path)) { - return cb(new Error(`Not sure where to look for "${name}" of category "${category}"`)); - } + if(!_.isString(path)) { + return cb(new Error(`Not sure where to look for "${name}" of category "${category}"`)); + } - loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { - return cb(err, mod); - }); + loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { + return cb(err, mod); + }); } function loadModulesForCategory(category, iterator, complete) { - fs.readdir(Config().paths[category], (err, files) => { - if(err) { - return iterator(err); - } + fs.readdir(Config().paths[category], (err, files) => { + if(err) { + return iterator(err); + } - const jsModules = files.filter(file => { - return '.js' === paths.extname(file); - }); + const jsModules = files.filter(file => { + return '.js' === paths.extname(file); + }); - async.each(jsModules, (file, next) => { - loadModule(paths.basename(file, '.js'), category, (err, mod) => { - iterator(err, mod); - return next(); - }); - }, err => { - if(complete) { - return complete(err); - } - }); - }); + async.each(jsModules, (file, next) => { + loadModule(paths.basename(file, '.js'), category, (err, mod) => { + iterator(err, mod); + return next(); + }); + }, err => { + if(complete) { + return complete(err); + } + }); + }); } function getModulePaths() { - const config = Config(); - return [ - config.paths.mods, - config.paths.loginServers, - config.paths.contentServers, - config.paths.scannerTossers, - ]; + const config = Config(); + return [ + config.paths.mods, + config.paths.loginServers, + config.paths.contentServers, + config.paths.scannerTossers, + ]; } diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 8238e4b6..2d61044d 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -15,9 +15,9 @@ const async = require('async'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Area List', - desc : 'Module for listing / choosing message areas', - author : 'NuSkooler', + name : 'Message Area List', + desc : 'Module for listing / choosing message areas', + author : 'NuSkooler', }; /* @@ -35,73 +35,73 @@ exports.moduleInfo = { */ const MciViewIds = { - AreaList : 1, - SelAreaInfo1 : 2, - SelAreaInfo2 : 3, + AreaList : 1, + SelAreaInfo1 : 2, + SelAreaInfo2 : 3, }; exports.getModule = class MessageAreaListModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( - this.client.user.properties.message_conf_tag, - { client : this.client } - ); + this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( + this.client.user.properties.message_conf_tag, + { client : this.client } + ); - const self = this; - this.menuMethods = { - changeArea : function(formData, extraArgs, cb) { - if(1 === formData.submitId) { - let area = self.messageAreas[formData.value.area]; - const areaTag = area.areaTag; - area = area.area; // what we want is actually embedded + const self = this; + this.menuMethods = { + changeArea : function(formData, extraArgs, cb) { + if(1 === formData.submitId) { + let area = self.messageAreas[formData.value.area]; + const areaTag = area.areaTag; + area = area.area; // what we want is actually embedded - messageArea.changeMessageArea(self.client, areaTag, err => { - if(err) { - self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); + messageArea.changeMessageArea(self.client, areaTag, err => { + if(err) { + self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); - self.prevMenuOnTimeout(1000, cb); - } else { - if(_.isString(area.art)) { - const dispOptions = { - client : self.client, - name : area.art, - }; + self.prevMenuOnTimeout(1000, cb); + } else { + if(_.isString(area.art)) { + const dispOptions = { + client : self.client, + name : area.art, + }; - self.client.term.rawWrite(resetScreen()); + self.client.term.rawWrite(resetScreen()); - displayThemeArt(dispOptions, () => { - // pause by default, unless explicitly told not to - if(_.has(area, 'options.pause') && false === area.options.pause) { - return self.prevMenuOnTimeout(1000, cb); - } else { - self.pausePrompt( () => { - return self.prevMenu(cb); - }); - } - }); - } else { - return self.prevMenu(cb); - } - } - }); - } else { - return cb(null); - } - } - }; - } + displayThemeArt(dispOptions, () => { + // pause by default, unless explicitly told not to + if(_.has(area, 'options.pause') && false === area.options.pause) { + return self.prevMenuOnTimeout(1000, cb); + } else { + self.pausePrompt( () => { + return self.prevMenu(cb); + }); + } + }); + } else { + return self.prevMenu(cb); + } + } + }); + } else { + return cb(null); + } + } + }; + } - prevMenuOnTimeout(timeout, cb) { - setTimeout( () => { - return this.prevMenu(cb); - }, timeout); - } + prevMenuOnTimeout(timeout, cb) { + setTimeout( () => { + return this.prevMenu(cb); + }, timeout); + } - // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! - updateGeneralAreaInfoViews(areaIndex) { - /* + // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! + updateGeneralAreaInfoViews(areaIndex) { + /* const areaInfo = self.messageAreas[areaIndex]; [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { @@ -111,71 +111,71 @@ exports.getModule = class MessageAreaListModule extends MenuModule { } }); */ - } + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); - async.series( - [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - formId : 0, - }; + async.series( + [ + function loadFromConfig(callback) { + const loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + formId : 0, + }; - vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) { - callback(err); - }); - }, - function populateAreaListView(callback) { - const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; - const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; + vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) { + callback(err); + }); + }, + function populateAreaListView(callback) { + const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; + const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; - const areaListView = vc.getView(MciViewIds.AreaList); - if(!areaListView) { - return callback(Errors.MissingMci('A MenuView compatible MCI code is required')); - } - let i = 1; - areaListView.setItems(_.map(self.messageAreas, v => { - return stringFormat(listFormat, { - index : i++, - areaTag : v.area.areaTag, - name : v.area.name, - desc : v.area.desc, - }); - })); + const areaListView = vc.getView(MciViewIds.AreaList); + if(!areaListView) { + return callback(Errors.MissingMci('A MenuView compatible MCI code is required')); + } + let i = 1; + areaListView.setItems(_.map(self.messageAreas, v => { + return stringFormat(listFormat, { + index : i++, + areaTag : v.area.areaTag, + name : v.area.name, + desc : v.area.desc, + }); + })); - i = 1; - areaListView.setFocusItems(_.map(self.messageAreas, v => { - return stringFormat(focusListFormat, { - index : i++, - areaTag : v.area.areaTag, - name : v.area.name, - desc : v.area.desc, - }); - })); + i = 1; + areaListView.setFocusItems(_.map(self.messageAreas, v => { + return stringFormat(focusListFormat, { + index : i++, + areaTag : v.area.areaTag, + name : v.area.name, + desc : v.area.desc, + }); + })); - areaListView.on('index update', areaIndex => { - self.updateGeneralAreaInfoViews(areaIndex); - }); + areaListView.on('index update', areaIndex => { + self.updateGeneralAreaInfoViews(areaIndex); + }); - areaListView.redraw(); + areaListView.redraw(); - callback(null); - } - ], - function complete(err) { - return cb(err); - } - ); - }); - } + callback(null); + } + ], + function complete(err) { + return cb(err); + } + ); + }); + } }; diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 3ffef698..8c2136c7 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -8,60 +8,60 @@ const _ = require('lodash'); const async = require('async'); exports.moduleInfo = { - name : 'Message Area Post', - desc : 'Module for posting a new message to an area', - author : 'NuSkooler', + name : 'Message Area Post', + desc : 'Module for posting a new message to an area', + author : 'NuSkooler', }; exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - const self = this; + const self = this; - // we're posting, so always start with 'edit' mode - this.editorMode = 'edit'; + // we're posting, so always start with 'edit' mode + this.editorMode = 'edit'; - this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) { + this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) { - var msg; - async.series( - [ - function getMessageObject(callback) { - self.getMessage(function gotMsg(err, msgObj) { - msg = msgObj; - return callback(err); - }); - }, - function saveMessage(callback) { - return persistMessage(msg, callback); - }, - function updateStats(callback) { - self.updateUserStats(callback); - } - ], - function complete(err) { - if(err) { - // :TODO:... sooooo now what? - } else { - // note: not logging 'from' here as it's part of client.log.xxxx() - self.client.log.info( - { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, - 'Message persisted' - ); - } + var msg; + async.series( + [ + function getMessageObject(callback) { + self.getMessage(function gotMsg(err, msgObj) { + msg = msgObj; + return callback(err); + }); + }, + function saveMessage(callback) { + return persistMessage(msg, callback); + }, + function updateStats(callback) { + self.updateUserStats(callback); + } + ], + function complete(err) { + if(err) { + // :TODO:... sooooo now what? + } else { + // note: not logging 'from' here as it's part of client.log.xxxx() + self.client.log.info( + { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, + 'Message persisted' + ); + } - return self.nextMenu(cb); - } - ); - }; - } + return self.nextMenu(cb); + } + ); + }; + } - enter() { - if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) { - this.messageAreaTag = this.client.user.properties.message_area_tag; - } + enter() { + if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) { + this.messageAreaTag = this.client.user.properties.message_area_tag; + } - super.enter(); - } + super.enter(); + } }; \ No newline at end of file diff --git a/core/msg_area_reply_fse.js b/core/msg_area_reply_fse.js index 24ee5377..83cb99c7 100644 --- a/core/msg_area_reply_fse.js +++ b/core/msg_area_reply_fse.js @@ -6,13 +6,13 @@ var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; exports.getModule = AreaReplyFSEModule; exports.moduleInfo = { - name : 'Message Area Reply', - desc : 'Module for replying to an area message', - author : 'NuSkooler', + name : 'Message Area Reply', + desc : 'Module for replying to an area message', + author : 'NuSkooler', }; function AreaReplyFSEModule(options) { - FullScreenEditorModule.call(this, options); + FullScreenEditorModule.call(this, options); } require('util').inherits(AreaReplyFSEModule, FullScreenEditorModule); diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index 7452d9d2..af0cbb78 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -9,137 +9,137 @@ const Message = require('./message.js'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Area View', - desc : 'Module for viewing an area message', - author : 'NuSkooler', + name : 'Message Area View', + desc : 'Module for viewing an area message', + author : 'NuSkooler', }; exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.editorType = 'area'; - this.editorMode = 'view'; + this.editorType = 'area'; + this.editorMode = 'view'; - if(_.isObject(options.extraArgs)) { - this.messageList = options.extraArgs.messageList; - this.messageIndex = options.extraArgs.messageIndex; - this.lastMessageNextExit = options.extraArgs.lastMessageNextExit; - } + if(_.isObject(options.extraArgs)) { + this.messageList = options.extraArgs.messageList; + this.messageIndex = options.extraArgs.messageIndex; + this.lastMessageNextExit = options.extraArgs.lastMessageNextExit; + } - this.messageList = this.messageList || []; - this.messageIndex = this.messageIndex || 0; - this.messageTotal = this.messageList.length; + this.messageList = this.messageList || []; + this.messageIndex = this.messageIndex || 0; + this.messageTotal = this.messageList.length; - if(this.messageList.length > 0) { - this.messageAreaTag = this.messageList[this.messageIndex].areaTag; - } + if(this.messageList.length > 0) { + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + } - const self = this; + const self = this; - // assign *additional* menuMethods - Object.assign(this.menuMethods, { - nextMessage : (formData, extraArgs, cb) => { - if(self.messageIndex + 1 < self.messageList.length) { - self.messageIndex++; + // assign *additional* menuMethods + Object.assign(this.menuMethods, { + nextMessage : (formData, extraArgs, cb) => { + if(self.messageIndex + 1 < self.messageList.length) { + self.messageIndex++; - this.messageAreaTag = this.messageList[this.messageIndex].areaTag; - this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with - return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); - } + return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); + } - // auto-exit if no more to go? - if(self.lastMessageNextExit) { - self.lastMessageReached = true; - return self.prevMenu(cb); - } + // auto-exit if no more to go? + if(self.lastMessageNextExit) { + self.lastMessageReached = true; + return self.prevMenu(cb); + } - return cb(null); - }, + return cb(null); + }, - prevMessage : (formData, extraArgs, cb) => { - if(self.messageIndex > 0) { - self.messageIndex--; + prevMessage : (formData, extraArgs, cb) => { + if(self.messageIndex > 0) { + self.messageIndex--; - this.messageAreaTag = this.messageList[this.messageIndex].areaTag; - this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + this.messageAreaTag = this.messageList[this.messageIndex].areaTag; + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with - return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); - } + return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); + } - return cb(null); - }, + return cb(null); + }, - movementKeyPressed : (formData, extraArgs, cb) => { - const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic # + movementKeyPressed : (formData, extraArgs, cb) => { + const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic # - // :TODO: Create methods for up/down vs using keyPressXXXXX - switch(formData.key.name) { - case 'down arrow' : bodyView.scrollDocumentUp(); break; - case 'up arrow' : bodyView.scrollDocumentDown(); break; - case 'page up' : bodyView.keyPressPageUp(); break; - case 'page down' : bodyView.keyPressPageDown(); break; - } + // :TODO: Create methods for up/down vs using keyPressXXXXX + switch(formData.key.name) { + case 'down arrow' : bodyView.scrollDocumentUp(); break; + case 'up arrow' : bodyView.scrollDocumentDown(); break; + case 'page up' : bodyView.keyPressPageUp(); break; + case 'page down' : bodyView.keyPressPageDown(); break; + } - // :TODO: need to stop down/page down if doing so would push the last - // visible page off the screen at all .... this should be handled by MLTEV though... + // :TODO: need to stop down/page down if doing so would push the last + // visible page off the screen at all .... this should be handled by MLTEV though... - return cb(null); - }, + return cb(null); + }, - replyMessage : (formData, extraArgs, cb) => { - if(_.isString(extraArgs.menu)) { - const modOpts = { - extraArgs : { - messageAreaTag : self.messageAreaTag, - replyToMessage : self.message, - } - }; + replyMessage : (formData, extraArgs, cb) => { + if(_.isString(extraArgs.menu)) { + const modOpts = { + extraArgs : { + messageAreaTag : self.messageAreaTag, + replyToMessage : self.message, + } + }; - return self.gotoMenu(extraArgs.menu, modOpts, cb); - } + return self.gotoMenu(extraArgs.menu, modOpts, cb); + } - self.client.log(extraArgs, 'Missing extraArgs.menu'); - return cb(null); - } - }); - } + self.client.log(extraArgs, 'Missing extraArgs.menu'); + return cb(null); + } + }); + } - loadMessageByUuid(uuid, cb) { - const msg = new Message(); - msg.load( { uuid : uuid, user : this.client.user }, () => { - this.setMessage(msg); + loadMessageByUuid(uuid, cb) { + const msg = new Message(); + msg.load( { uuid : uuid, user : this.client.user }, () => { + this.setMessage(msg); - if(cb) { - return cb(null); - } - }); - } + if(cb) { + return cb(null); + } + }); + } - finishedLoading() { - this.loadMessageByUuid(this.messageList[this.messageIndex].messageUuid); - } + finishedLoading() { + this.loadMessageByUuid(this.messageList[this.messageIndex].messageUuid); + } - getSaveState() { - return { - messageList : this.messageList, - messageIndex : this.messageIndex, - messageTotal : this.messageList.length, - }; - } + getSaveState() { + return { + messageList : this.messageList, + messageIndex : this.messageIndex, + messageTotal : this.messageList.length, + }; + } - restoreSavedState(savedState) { - this.messageList = savedState.messageList; - this.messageIndex = savedState.messageIndex; - this.messageTotal = savedState.messageTotal; - } + restoreSavedState(savedState) { + this.messageList = savedState.messageList; + this.messageIndex = savedState.messageIndex; + this.messageTotal = savedState.messageTotal; + } - getMenuResult() { - return { - messageIndex : this.messageIndex, - lastMessageReached : this.lastMessageReached, - }; - } + getMenuResult() { + return { + messageIndex : this.messageIndex, + lastMessageReached : this.lastMessageReached, + }; + } }; diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index 43e57820..0876f89a 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -14,135 +14,135 @@ const async = require('async'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Conference List', - desc : 'Module for listing / choosing message conferences', - author : 'NuSkooler', + name : 'Message Conference List', + desc : 'Module for listing / choosing message conferences', + author : 'NuSkooler', }; const MciViewIds = { - ConfList : 1, + ConfList : 1, - // :TODO: - // # areas in conf .... see Obv/2, iNiQ, ... - // + // :TODO: + // # areas in conf .... see Obv/2, iNiQ, ... + // }; exports.getModule = class MessageConfListModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client); - const self = this; + this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client); + const self = this; - this.menuMethods = { - changeConference : function(formData, extraArgs, cb) { - if(1 === formData.submitId) { - let conf = self.messageConfs[formData.value.conf]; - const confTag = conf.confTag; - conf = conf.conf; // what we want is embedded + this.menuMethods = { + changeConference : function(formData, extraArgs, cb) { + if(1 === formData.submitId) { + let conf = self.messageConfs[formData.value.conf]; + const confTag = conf.confTag; + conf = conf.conf; // what we want is embedded - messageArea.changeMessageConference(self.client, confTag, err => { - if(err) { - self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); + messageArea.changeMessageConference(self.client, confTag, err => { + if(err) { + self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); - setTimeout( () => { - return self.prevMenu(cb); - }, 1000); - } else { - if(_.isString(conf.art)) { - const dispOptions = { - client : self.client, - name : conf.art, - }; + setTimeout( () => { + return self.prevMenu(cb); + }, 1000); + } else { + if(_.isString(conf.art)) { + const dispOptions = { + client : self.client, + name : conf.art, + }; - self.client.term.rawWrite(resetScreen()); + self.client.term.rawWrite(resetScreen()); - displayThemeArt(dispOptions, () => { - // pause by default, unless explicitly told not to - if(_.has(conf, 'options.pause') && false === conf.options.pause) { - return self.prevMenuOnTimeout(1000, cb); - } else { - self.pausePrompt( () => { - return self.prevMenu(cb); - }); - } - }); - } else { - return self.prevMenu(cb); - } - } - }); - } else { - return cb(null); - } - } - }; - } + displayThemeArt(dispOptions, () => { + // pause by default, unless explicitly told not to + if(_.has(conf, 'options.pause') && false === conf.options.pause) { + return self.prevMenuOnTimeout(1000, cb); + } else { + self.pausePrompt( () => { + return self.prevMenu(cb); + }); + } + }); + } else { + return self.prevMenu(cb); + } + } + }); + } else { + return cb(null); + } + } + }; + } - prevMenuOnTimeout(timeout, cb) { - setTimeout( () => { - return this.prevMenu(cb); - }, timeout); - } + prevMenuOnTimeout(timeout, cb) { + setTimeout( () => { + return this.prevMenu(cb); + }, timeout); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); - async.series( - [ - function loadFromConfig(callback) { - let loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - formId : 0, - }; + async.series( + [ + function loadFromConfig(callback) { + let loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + formId : 0, + }; - vc.loadFromMenuConfig(loadOpts, callback); - }, - function populateConfListView(callback) { - const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; - const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; + vc.loadFromMenuConfig(loadOpts, callback); + }, + function populateConfListView(callback) { + const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; + const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; - const confListView = vc.getView(MciViewIds.ConfList); - let i = 1; - confListView.setItems(_.map(self.messageConfs, v => { - return stringFormat(listFormat, { - index : i++, - confTag : v.conf.confTag, - name : v.conf.name, - desc : v.conf.desc, - }); - })); + const confListView = vc.getView(MciViewIds.ConfList); + let i = 1; + confListView.setItems(_.map(self.messageConfs, v => { + return stringFormat(listFormat, { + index : i++, + confTag : v.conf.confTag, + name : v.conf.name, + desc : v.conf.desc, + }); + })); - i = 1; - confListView.setFocusItems(_.map(self.messageConfs, v => { - return stringFormat(focusListFormat, { - index : i++, - confTag : v.conf.confTag, - name : v.conf.name, - desc : v.conf.desc, - }); - })); + i = 1; + confListView.setFocusItems(_.map(self.messageConfs, v => { + return stringFormat(focusListFormat, { + index : i++, + confTag : v.conf.confTag, + name : v.conf.name, + desc : v.conf.desc, + }); + })); - confListView.redraw(); + confListView.redraw(); - callback(null); - }, - function populateTextViews(callback) { - // :TODO: populate other avail MCI, e.g. current conf name - callback(null); - } - ], - function complete(err) { - cb(err); - } - ); - }); - } + callback(null); + }, + function populateTextViews(callback) { + // :TODO: populate other avail MCI, e.g. current conf name + callback(null); + } + ], + function complete(err) { + cb(err); + } + ); + }); + } }; diff --git a/core/msg_list.js b/core/msg_list.js index 701542d6..5c8624ab 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -30,229 +30,229 @@ const moment = require('moment'); */ exports.moduleInfo = { - name : 'Message List', - desc : 'Module for listing/browsing available messages', - author : 'NuSkooler', + name : 'Message List', + desc : 'Module for listing/browsing available messages', + author : 'NuSkooler', }; const MciViewIds = { - msgList : 1, // VM1 - msgInfo1 : 2, // TL2 + msgList : 1, // VM1 + msgInfo1 : 2, // TL2 }; exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) { - constructor(options) { - super(options); + constructor(options) { + super(options); - // :TODO: consider this pattern in base MenuModule - clean up code all over - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + // :TODO: consider this pattern in base MenuModule - clean up code all over + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); + this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); - this.menuMethods = { - selectMessage : (formData, extraArgs, cb) => { - if(MciViewIds.msgList === formData.submitId) { - this.initialFocusIndex = formData.value.message; + this.menuMethods = { + selectMessage : (formData, extraArgs, cb) => { + if(MciViewIds.msgList === formData.submitId) { + this.initialFocusIndex = formData.value.message; - const modOpts = { - extraArgs : { - messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, - messageList : this.config.messageList, - messageIndex : formData.value.message, - lastMessageNextExit : true, - } - }; + const modOpts = { + extraArgs : { + messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, + messageList : this.config.messageList, + messageIndex : formData.value.message, + lastMessageNextExit : true, + } + }; - if(_.isBoolean(this.config.noUpdateLastReadId)) { - modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId; - } + if(_.isBoolean(this.config.noUpdateLastReadId)) { + modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId; + } - // - // Provide a serializer so we don't dump *huge* bits of information to the log - // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 - // - const self = this; - modOpts.extraArgs.toJSON = function() { - const logMsgList = (self.config.messageList.length <= 4) ? - self.config.messageList : - self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); + // + // Provide a serializer so we don't dump *huge* bits of information to the log + // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 + // + const self = this; + modOpts.extraArgs.toJSON = function() { + const logMsgList = (self.config.messageList.length <= 4) ? + self.config.messageList : + self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); - return { - // note |this| is scope of toJSON()! - messageAreaTag : this.messageAreaTag, - apprevMessageList : logMsgList, - messageCount : this.messageList.length, - messageIndex : this.messageIndex, - }; - }; + return { + // note |this| is scope of toJSON()! + messageAreaTag : this.messageAreaTag, + apprevMessageList : logMsgList, + messageCount : this.messageList.length, + messageIndex : this.messageIndex, + }; + }; - return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb); - } else { - return cb(null); - } - }, + return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb); + } else { + return cb(null); + } + }, - fullExit : (formData, extraArgs, cb) => { - this.menuResult = { fullExit : true }; - return this.prevMenu(cb); - } - }; - } + fullExit : (formData, extraArgs, cb) => { + this.menuResult = { fullExit : true }; + return this.prevMenu(cb); + } + }; + } - getSelectedAreaTag(listIndex) { - return this.config.messageList[listIndex].areaTag || this.config.messageAreaTag; - } + getSelectedAreaTag(listIndex) { + return this.config.messageList[listIndex].areaTag || this.config.messageAreaTag; + } - enter() { - if(this.lastMessageReachedExit) { - return this.prevMenu(); - } + enter() { + if(this.lastMessageReachedExit) { + return this.prevMenu(); + } - super.enter(); + super.enter(); - // - // Config can specify |messageAreaTag| else it comes from - // the user's current area. If |messageList| is supplied, - // each item is expected to contain |areaTag|, so we use that - // instead in those cases. - // - if(!Array.isArray(this.config.messageList)) { - if(this.config.messageAreaTag) { - this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); - } else { - this.config.messageAreaTag = this.client.user.properties.message_area_tag; - } - } - } + // + // Config can specify |messageAreaTag| else it comes from + // the user's current area. If |messageList| is supplied, + // each item is expected to contain |areaTag|, so we use that + // instead in those cases. + // + if(!Array.isArray(this.config.messageList)) { + if(this.config.messageAreaTag) { + this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); + } else { + this.config.messageAreaTag = this.client.user.properties.message_area_tag; + } + } + } - leave() { - this.tempMessageConfAndAreaRestore(); - super.leave(); - } + leave() { + this.tempMessageConfAndAreaRestore(); + super.leave(); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - let configProvidedMessageList = false; + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + let configProvidedMessageList = false; - async.series( - [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu - }; + async.series( + [ + function loadFromConfig(callback) { + const loadOpts = { + callingMenu : self, + mciMap : mciData.menu + }; - return vc.loadFromMenuConfig(loadOpts, callback); - }, - function fetchMessagesInArea(callback) { - // - // Config can supply messages else we'll need to populate the list now - // - if(_.isArray(self.config.messageList)) { - configProvidedMessageList = true; - return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); - } + return vc.loadFromMenuConfig(loadOpts, callback); + }, + function fetchMessagesInArea(callback) { + // + // Config can supply messages else we'll need to populate the list now + // + if(_.isArray(self.config.messageList)) { + configProvidedMessageList = true; + return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); + } - messageArea.getMessageListForArea(self.client, self.config.messageAreaTag, function msgs(err, msgList) { - if(!msgList || 0 === msgList.length) { - return callback(new Error('No messages in area')); - } + messageArea.getMessageListForArea(self.client, self.config.messageAreaTag, function msgs(err, msgList) { + if(!msgList || 0 === msgList.length) { + return callback(new Error('No messages in area')); + } - self.config.messageList = msgList; - return callback(err); - }); - }, - function getLastReadMesageId(callback) { - // messageList entries can contain |isNew| if they want to be considered new - if(configProvidedMessageList) { - self.lastReadId = 0; - return callback(null); - } + self.config.messageList = msgList; + return callback(err); + }); + }, + function getLastReadMesageId(callback) { + // messageList entries can contain |isNew| if they want to be considered new + if(configProvidedMessageList) { + self.lastReadId = 0; + return callback(null); + } - messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { - self.lastReadId = lastReadId || 0; - return callback(null); // ignore any errors, e.g. missing value - }); - }, - function updateMessageListObjects(callback) { - const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); - const newIndicator = self.menuConfig.config.newIndicator || '*'; - const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues + messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { + self.lastReadId = lastReadId || 0; + return callback(null); // ignore any errors, e.g. missing value + }); + }, + function updateMessageListObjects(callback) { + const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); + const newIndicator = self.menuConfig.config.newIndicator || '*'; + const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues - let msgNum = 1; - self.config.messageList.forEach( (listItem, index) => { - listItem.msgNum = msgNum++; - listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); - const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; - listItem.newIndicator = isNew ? newIndicator : regIndicator; + let msgNum = 1; + self.config.messageList.forEach( (listItem, index) => { + listItem.msgNum = msgNum++; + listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); + const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; + listItem.newIndicator = isNew ? newIndicator : regIndicator; - if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { - self.initialFocusIndex = index; - } + if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { + self.initialFocusIndex = index; + } - listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text - }); - return callback(null); - }, - function populateList(callback) { - const msgListView = vc.getView(MciViewIds.msgList); - // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... - const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; + listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text + }); + return callback(null); + }, + function populateList(callback) { + const msgListView = vc.getView(MciViewIds.msgList); + // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... + const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; - msgListView.setItems(self.config.messageList); + msgListView.setItems(self.config.messageList); - msgListView.on('index update', idx => { - self.setViewText( - 'allViews', - MciViewIds.msgInfo1, - stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.config.messageList.length } )); - }); + msgListView.on('index update', idx => { + self.setViewText( + 'allViews', + MciViewIds.msgInfo1, + stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.config.messageList.length } )); + }); - if(self.initialFocusIndex > 0) { - // note: causes redraw() - msgListView.setFocusItemIndex(self.initialFocusIndex); - } else { - msgListView.redraw(); - } + if(self.initialFocusIndex > 0) { + // note: causes redraw() + msgListView.setFocusItemIndex(self.initialFocusIndex); + } else { + msgListView.redraw(); + } - return callback(null); - }, - function drawOtherViews(callback) { - const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; - self.setViewText( - 'allViews', - MciViewIds.msgInfo1, - stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.config.messageList.length } )); - return callback(null); - }, - ], - err => { - if(err) { - self.client.log.error( { error : err.message }, 'Error loading message list'); - } - return cb(err); - } - ); - }); - } + return callback(null); + }, + function drawOtherViews(callback) { + const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; + self.setViewText( + 'allViews', + MciViewIds.msgInfo1, + stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.config.messageList.length } )); + return callback(null); + }, + ], + err => { + if(err) { + self.client.log.error( { error : err.message }, 'Error loading message list'); + } + return cb(err); + } + ); + }); + } - getSaveState() { - return { initialFocusIndex : this.initialFocusIndex }; - } + getSaveState() { + return { initialFocusIndex : this.initialFocusIndex }; + } - restoreSavedState(savedState) { - if(savedState) { - this.initialFocusIndex = savedState.initialFocusIndex; - } - } + restoreSavedState(savedState) { + if(savedState) { + this.initialFocusIndex = savedState.initialFocusIndex; + } + } - getMenuResult() { - return this.menuResult; - } + getMenuResult() { + return this.menuResult; + } }; diff --git a/core/msg_network.js b/core/msg_network.js index 890d1bcf..721ebba4 100644 --- a/core/msg_network.js +++ b/core/msg_network.js @@ -14,53 +14,53 @@ exports.recordMessage = recordMessage; let msgNetworkModules = []; function startup(cb) { - async.series( - [ - function loadModules(callback) { - loadModulesForCategory('scannerTossers', (err, module) => { - if(!err) { - const modInst = new module.getModule(); + async.series( + [ + function loadModules(callback) { + loadModulesForCategory('scannerTossers', (err, module) => { + if(!err) { + const modInst = new module.getModule(); - modInst.startup(err => { - if(!err) { - msgNetworkModules.push(modInst); - } - }); - } - }, err => { - callback(err); - }); - } - ], - cb - ); + modInst.startup(err => { + if(!err) { + msgNetworkModules.push(modInst); + } + }); + } + }, err => { + callback(err); + }); + } + ], + cb + ); } function shutdown(cb) { - async.each( - msgNetworkModules, - (msgNetModule, next) => { - msgNetModule.shutdown( () => { - return next(); - }); - }, - () => { - msgNetworkModules = []; - return cb(null); - } - ); + async.each( + msgNetworkModules, + (msgNetModule, next) => { + msgNetModule.shutdown( () => { + return next(); + }); + }, + () => { + msgNetworkModules = []; + return cb(null); + } + ); } function recordMessage(message, cb) { - // - // Give all message network modules (scanner/tossers) - // a chance to do something with |message|. Any or all can - // choose to ignore it. - // - async.each(msgNetworkModules, (modInst, next) => { - modInst.record(message); - next(); - }, err => { - cb(err); - }); + // + // Give all message network modules (scanner/tossers) + // a chance to do something with |message|. Any or all can + // choose to ignore it. + // + async.each(msgNetworkModules, (modInst, next) => { + modInst.record(message); + next(); + }, err => { + cb(err); + }); } \ No newline at end of file diff --git a/core/msg_scan_toss_module.js b/core/msg_scan_toss_module.js index 9b3598c1..002c2cc3 100644 --- a/core/msg_scan_toss_module.js +++ b/core/msg_scan_toss_module.js @@ -7,17 +7,17 @@ var PluginModule = require('./plugin_module.js').PluginModule; exports.MessageScanTossModule = MessageScanTossModule; function MessageScanTossModule() { - PluginModule.call(this); + PluginModule.call(this); } require('util').inherits(MessageScanTossModule, PluginModule); MessageScanTossModule.prototype.startup = function(cb) { - return cb(null); + return cb(null); }; MessageScanTossModule.prototype.shutdown = function(cb) { - return cb(null); + return cb(null); }; MessageScanTossModule.prototype.record = function(/*message*/) { diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 5757e708..3a7f29d9 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -63,155 +63,155 @@ const _ = require('lodash'); const SPECIAL_KEY_MAP_DEFAULT = { - 'line feed' : [ 'return' ], - exit : [ 'esc' ], - backspace : [ 'backspace' ], - delete : [ 'delete' ], - tab : [ 'tab' ], - up : [ 'up arrow' ], - down : [ 'down arrow' ], - end : [ 'end' ], - home : [ 'home' ], - left : [ 'left arrow' ], - right : [ 'right arrow' ], - 'delete line' : [ 'ctrl + y' ], - 'page up' : [ 'page up' ], - 'page down' : [ 'page down' ], - insert : [ 'insert', 'ctrl + v' ], + 'line feed' : [ 'return' ], + exit : [ 'esc' ], + backspace : [ 'backspace' ], + delete : [ 'delete' ], + tab : [ 'tab' ], + up : [ 'up arrow' ], + down : [ 'down arrow' ], + end : [ 'end' ], + home : [ 'home' ], + left : [ 'left arrow' ], + right : [ 'right arrow' ], + 'delete line' : [ 'ctrl + y' ], + 'page up' : [ 'page up' ], + 'page down' : [ 'page down' ], + insert : [ 'insert', 'ctrl + v' ], }; exports.MultiLineEditTextView = MultiLineEditTextView; function MultiLineEditTextView(options) { - if(!_.isBoolean(options.acceptsFocus)) { - options.acceptsFocus = true; - } + if(!_.isBoolean(options.acceptsFocus)) { + options.acceptsFocus = true; + } - if(!_.isBoolean(this.acceptsInput)) { - options.acceptsInput = true; - } + if(!_.isBoolean(this.acceptsInput)) { + options.acceptsInput = true; + } - if(!_.isObject(options.specialKeyMap)) { - options.specialKeyMap = SPECIAL_KEY_MAP_DEFAULT; - } + if(!_.isObject(options.specialKeyMap)) { + options.specialKeyMap = SPECIAL_KEY_MAP_DEFAULT; + } - View.call(this, options); + View.call(this, options); - var self = this; + var self = this; - // - // ANSI seems to want tabs to default to 8 characters. See the following: - // * http://www.ansi-bbs.org/ansi-bbs2/control_chars/ - // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt - // - // This seems overkill though, so let's default to 4 :) - // :TODO: what shoudl this really be? Maybe 8 is OK - // - this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4; + // + // ANSI seems to want tabs to default to 8 characters. See the following: + // * http://www.ansi-bbs.org/ansi-bbs2/control_chars/ + // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt + // + // This seems overkill though, so let's default to 4 :) + // :TODO: what shoudl this really be? Maybe 8 is OK + // + this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4; - this.textLines = [ ]; - this.topVisibleIndex = 0; - this.mode = options.mode || 'edit'; // edit | preview | read-only + this.textLines = [ ]; + this.topVisibleIndex = 0; + this.mode = options.mode || 'edit'; // edit | preview | read-only - if ('preview' === this.mode) { - this.autoScroll = options.autoScroll || true; - this.tabSwitchesView = true; - } else { - this.autoScroll = options.autoScroll || false; - this.tabSwitchesView = options.tabSwitchesView || false; - } - // - // cursorPos represents zero-based row, col positions - // within the editor itself - // - this.cursorPos = { col : 0, row : 0 }; + if ('preview' === this.mode) { + this.autoScroll = options.autoScroll || true; + this.tabSwitchesView = true; + } else { + this.autoScroll = options.autoScroll || false; + this.tabSwitchesView = options.tabSwitchesView || false; + } + // + // cursorPos represents zero-based row, col positions + // within the editor itself + // + this.cursorPos = { col : 0, row : 0 }; - this.getSGRFor = function(sgrFor) { - return { - text : self.getSGR(), - }[sgrFor] || self.getSGR(); - }; + this.getSGRFor = function(sgrFor) { + return { + text : self.getSGR(), + }[sgrFor] || self.getSGR(); + }; - this.isEditMode = function() { - return 'edit' === self.mode; - }; + this.isEditMode = function() { + return 'edit' === self.mode; + }; - this.isPreviewMode = function() { - return 'preview' === self.mode; - }; + this.isPreviewMode = function() { + return 'preview' === self.mode; + }; - // :TODO: Most of the calls to this could be avoided via incrementRow(), decrementRow() that keeps track or such - this.getTextLinesIndex = function(row) { - if(!_.isNumber(row)) { - row = self.cursorPos.row; - } - var index = self.topVisibleIndex + row; - return index; - }; + // :TODO: Most of the calls to this could be avoided via incrementRow(), decrementRow() that keeps track or such + this.getTextLinesIndex = function(row) { + if(!_.isNumber(row)) { + row = self.cursorPos.row; + } + var index = self.topVisibleIndex + row; + return index; + }; - this.getRemainingLinesBelowRow = function(row) { - if(!_.isNumber(row)) { - row = self.cursorPos.row; - } - return self.textLines.length - (self.topVisibleIndex + row) - 1; - }; + this.getRemainingLinesBelowRow = function(row) { + if(!_.isNumber(row)) { + row = self.cursorPos.row; + } + return self.textLines.length - (self.topVisibleIndex + row) - 1; + }; - this.getNextEndOfLineIndex = function(startIndex) { - for(var i = startIndex; i < self.textLines.length; i++) { - if(self.textLines[i].eol) { - return i; - } - } - return self.textLines.length; - }; + this.getNextEndOfLineIndex = function(startIndex) { + for(var i = startIndex; i < self.textLines.length; i++) { + if(self.textLines[i].eol) { + return i; + } + } + return self.textLines.length; + }; - this.toggleTextCursor = function(action) { - self.client.term.rawWrite(`${self.getSGRFor('text')}${'hide' === action ? ansi.hideCursor() : ansi.showCursor()}`); - }; + this.toggleTextCursor = function(action) { + self.client.term.rawWrite(`${self.getSGRFor('text')}${'hide' === action ? ansi.hideCursor() : ansi.showCursor()}`); + }; - this.redrawRows = function(startRow, endRow) { - self.toggleTextCursor('hide'); + this.redrawRows = function(startRow, endRow) { + self.toggleTextCursor('hide'); - const startIndex = self.getTextLinesIndex(startRow); - const endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length); - const absPos = self.getAbsolutePosition(startRow, 0); + const startIndex = self.getTextLinesIndex(startRow); + const endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length); + const absPos = self.getAbsolutePosition(startRow, 0); - for(let i = startIndex; i < endIndex; ++i) { - //${self.getSGRFor('text')} - self.client.term.write( - `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, - false // convertLineFeeds - ); - } + for(let i = startIndex; i < endIndex; ++i) { + //${self.getSGRFor('text')} + self.client.term.write( + `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, + false // convertLineFeeds + ); + } - self.toggleTextCursor('show'); + self.toggleTextCursor('show'); - return absPos.row - self.position.row; // row we ended on - }; + return absPos.row - self.position.row; // row we ended on + }; - this.eraseRows = function(startRow, endRow) { - self.toggleTextCursor('hide'); + this.eraseRows = function(startRow, endRow) { + self.toggleTextCursor('hide'); - const absPos = self.getAbsolutePosition(startRow, 0); - const absPosEnd = self.getAbsolutePosition(endRow, 0); - const eraseFiller = ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' '); + const absPos = self.getAbsolutePosition(startRow, 0); + const absPosEnd = self.getAbsolutePosition(endRow, 0); + const eraseFiller = ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' '); - while(absPos.row < absPosEnd.row) { - self.client.term.write( - `${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`, - false // convertLineFeeds - ); - } + while(absPos.row < absPosEnd.row) { + self.client.term.write( + `${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`, + false // convertLineFeeds + ); + } - self.toggleTextCursor('show'); - }; + self.toggleTextCursor('show'); + }; - this.redrawVisibleArea = function() { - assert(self.topVisibleIndex <= self.textLines.length); - const lastRow = self.redrawRows(0, self.dimens.height); + this.redrawVisibleArea = function() { + assert(self.topVisibleIndex <= self.textLines.length); + const lastRow = self.redrawRows(0, self.dimens.height); - self.eraseRows(lastRow, self.dimens.height); - /* + self.eraseRows(lastRow, self.dimens.height); + /* // :TOOD: create eraseRows(startRow, endRow) if(lastRow < self.dimens.height) { @@ -223,100 +223,100 @@ function MultiLineEditTextView(options) { } } */ - }; + }; - this.getVisibleText = function(index) { - if(!_.isNumber(index)) { - index = self.getTextLinesIndex(); - } - return self.textLines[index].text.replace(/\t/g, ' '); - }; + this.getVisibleText = function(index) { + if(!_.isNumber(index)) { + index = self.getTextLinesIndex(); + } + return self.textLines[index].text.replace(/\t/g, ' '); + }; - this.getText = function(index) { - if(!_.isNumber(index)) { - index = self.getTextLinesIndex(); - } - return self.textLines.length > index ? self.textLines[index].text : ''; - }; + this.getText = function(index) { + if(!_.isNumber(index)) { + index = self.getTextLinesIndex(); + } + return self.textLines.length > index ? self.textLines[index].text : ''; + }; - this.getTextLength = function(index) { - if(!_.isNumber(index)) { - index = self.getTextLinesIndex(); - } - return self.textLines.length > index ? self.textLines[index].text.length : 0; - }; + this.getTextLength = function(index) { + if(!_.isNumber(index)) { + index = self.getTextLinesIndex(); + } + return self.textLines.length > index ? self.textLines[index].text.length : 0; + }; - this.getCharacter = function(index, col) { - if(!_.isNumber(col)) { - col = self.cursorPos.col; - } - return self.getText(index).charAt(col); - }; + this.getCharacter = function(index, col) { + if(!_.isNumber(col)) { + col = self.cursorPos.col; + } + return self.getText(index).charAt(col); + }; - this.isTab = function(index, col) { - return '\t' === self.getCharacter(index, col); - }; + this.isTab = function(index, col) { + return '\t' === self.getCharacter(index, col); + }; - this.getTextEndOfLineColumn = function(index) { - return Math.max(0, self.getTextLength(index)); - }; + this.getTextEndOfLineColumn = function(index) { + return Math.max(0, self.getTextLength(index)); + }; - this.getRenderText = function(index) { - let text = self.getVisibleText(index); - const remain = self.dimens.width - text.length; + this.getRenderText = function(index) { + let text = self.getVisibleText(index); + const remain = self.dimens.width - text.length; - if(remain > 0) { - text += ' '.repeat(remain + 1); - // text += new Array(remain + 1).join(' '); - } + if(remain > 0) { + text += ' '.repeat(remain + 1); + // text += new Array(remain + 1).join(' '); + } - return text; - }; + return text; + }; - this.getTextLines = function(startIndex, endIndex) { - var lines; - if(startIndex === endIndex) { - lines = [ self.textLines[startIndex] ]; - } else { - lines = self.textLines.slice(startIndex, endIndex + 1); // "slice extracts up to but not including end." - } - return lines; - }; + this.getTextLines = function(startIndex, endIndex) { + var lines; + if(startIndex === endIndex) { + lines = [ self.textLines[startIndex] ]; + } else { + lines = self.textLines.slice(startIndex, endIndex + 1); // "slice extracts up to but not including end." + } + return lines; + }; - this.getOutputText = function(startIndex, endIndex, eolMarker, options) { - const lines = self.getTextLines(startIndex, endIndex); - let text = ''; - const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); + this.getOutputText = function(startIndex, endIndex, eolMarker, options) { + const lines = self.getTextLines(startIndex, endIndex); + let text = ''; + const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); - lines.forEach(line => { - text += line.text.replace(re, '\t'); + lines.forEach(line => { + text += line.text.replace(re, '\t'); - if(options.forceLineTerms || (eolMarker && line.eol)) { - text += eolMarker; - } - }); + if(options.forceLineTerms || (eolMarker && line.eol)) { + text += eolMarker; + } + }); - return text; - }; + return text; + }; - this.getContiguousText = function(startIndex, endIndex, includeEol) { - var lines = self.getTextLines(startIndex, endIndex); - var text = ''; - for(var i = 0; i < lines.length; ++i) { - text += lines[i].text; - if(includeEol && lines[i].eol) { - text += '\n'; - } - } - return text; - }; + this.getContiguousText = function(startIndex, endIndex, includeEol) { + var lines = self.getTextLines(startIndex, endIndex); + var text = ''; + for(var i = 0; i < lines.length; ++i) { + text += lines[i].text; + if(includeEol && lines[i].eol) { + text += '\n'; + } + } + return text; + }; - this.replaceCharacterInText = function(c, index, col) { - self.textLines[index].text = strUtil.replaceAt( - self.textLines[index].text, col, c); - }; + this.replaceCharacterInText = function(c, index, col) { + self.textLines[index].text = strUtil.replaceAt( + self.textLines[index].text, col, c); + }; - /* + /* this.editTextAtPosition = function(editAction, text, index, col) { switch(editAction) { case 'insert' : @@ -335,669 +335,669 @@ function MultiLineEditTextView(options) { }; */ - this.updateTextWordWrap = function(index) { - const nextEolIndex = self.getNextEndOfLineIndex(index); - const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); - const newLines = wrapped.wrapped.map(l => { return { text : l }; } ); + this.updateTextWordWrap = function(index) { + const nextEolIndex = self.getNextEndOfLineIndex(index); + const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); + const newLines = wrapped.wrapped.map(l => { return { text : l }; } ); - newLines[newLines.length - 1].eol = true; + newLines[newLines.length - 1].eol = true; - Array.prototype.splice.apply( - self.textLines, - [ index, (nextEolIndex - index) + 1 ].concat(newLines)); + Array.prototype.splice.apply( + self.textLines, + [ index, (nextEolIndex - index) + 1 ].concat(newLines)); - return wrapped.firstWrapRange; - }; + return wrapped.firstWrapRange; + }; - this.removeCharactersFromText = function(index, col, operation, count) { - if('delete' === operation) { - self.textLines[index].text = + this.removeCharactersFromText = function(index, col, operation, count) { + if('delete' === operation) { + self.textLines[index].text = self.textLines[index].text.slice(0, col) + self.textLines[index].text.slice(col + count); - self.updateTextWordWrap(index); - self.redrawRows(self.cursorPos.row, self.dimens.height); - self.moveClientCursorToCursorPos(); - } else if ('backspace' === operation) { - // :TODO: method for splicing text - self.textLines[index].text = + self.updateTextWordWrap(index); + self.redrawRows(self.cursorPos.row, self.dimens.height); + self.moveClientCursorToCursorPos(); + } else if ('backspace' === operation) { + // :TODO: method for splicing text + self.textLines[index].text = self.textLines[index].text.slice(0, col - (count - 1)) + self.textLines[index].text.slice(col + 1); - self.cursorPos.col -= (count - 1); - - self.updateTextWordWrap(index); - self.redrawRows(self.cursorPos.row, self.dimens.height); - - self.moveClientCursorToCursorPos(); - } else if('delete line' === operation) { - // - // Delete a visible line. Note that this is *not* the "physical" line, or - // 1:n entries up to eol! This is to keep consistency with home/end, and - // some other text editors such as nano. Sublime for example want to - // treat all of these things using the physical approach, but this seems - // a bit odd in this context. - // - var isLastLine = (index === self.textLines.length - 1); - var hadEol = self.textLines[index].eol; - - self.textLines.splice(index, 1); - if(hadEol && self.textLines.length > index && !self.textLines[index].eol) { - self.textLines[index].eol = true; - } - - // - // Create a empty edit buffer if necessary - // :TODO: Make this a method - if(self.textLines.length < 1) { - self.textLines = [ { text : '', eol : true } ]; - isLastLine = false; // resetting - } - - self.cursorPos.col = 0; - - var lastRow = self.redrawRows(self.cursorPos.row, self.dimens.height); - self.eraseRows(lastRow, self.dimens.height); - - // - // If we just deleted the last line in the buffer, move up - // - if(isLastLine) { - self.cursorEndOfPreviousLine(); - } else { - self.moveClientCursorToCursorPos(); - } - } - }; - - this.insertCharactersInText = function(c, index, col) { - const prevTextLength = self.getTextLength(index); - let editingEol = self.cursorPos.col === prevTextLength; - - self.textLines[index].text = [ - self.textLines[index].text.slice(0, col), - c, - self.textLines[index].text.slice(col) - ].join(''); - - self.cursorPos.col += c.length; - - if(self.getTextLength(index) > self.dimens.width) { - // - // Update word wrapping and |cursorOffset| if the cursor - // was within the bounds of the wrapped text - // - let cursorOffset; - const lastCol = self.cursorPos.col - c.length; - const firstWrapRange = self.updateTextWordWrap(index); - if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { - cursorOffset = self.cursorPos.col - firstWrapRange.start; - editingEol = true; //override - } else { - cursorOffset = firstWrapRange.end; - } - - // redraw from current row to end of visible area - self.redrawRows(self.cursorPos.row, self.dimens.height); - - // If we're editing mid, we're done here. Else, we need to - // move the cursor to the new editing position after a wrap - if(editingEol) { - self.cursorBeginOfNextLine(); - self.cursorPos.col += cursorOffset; - self.client.term.rawWrite(ansi.right(cursorOffset)); - } else { - // adjust cursor after drawing new rows - const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); - self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col)); - } - } else { - // - // We must only redraw from col -> end of current visible line - // - const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); - const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length); - - self.client.term.write( - `${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto(absPos.row, absPos.col)}${ansi.showCursor()}`, - false // convertLineFeeds - ); - } - }; - - this.getRemainingTabWidth = function(col) { - if(!_.isNumber(col)) { - col = self.cursorPos.col; - } - return self.tabWidth - (col % self.tabWidth); - }; - - this.calculateTabStops = function() { - self.tabStops = [ 0 ]; - var col = 0; - while(col < self.dimens.width) { - col += self.getRemainingTabWidth(col); - self.tabStops.push(col); - } - }; - - this.getNextTabStop = function(col) { - var i = self.tabStops.length; - while(self.tabStops[--i] > col); - return self.tabStops[++i]; - }; - - this.getPrevTabStop = function(col) { - var i = self.tabStops.length; - while(self.tabStops[--i] >= col); - return self.tabStops[i]; - }; - - this.expandTab = function(col, expandChar) { - expandChar = expandChar || ' '; - return new Array(self.getRemainingTabWidth(col)).join(expandChar); - }; - - this.wordWrapSingleLine = function(line, tabHandling = 'expand') { - return wordWrapText( - line, - { - width : self.dimens.width, - tabHandling : tabHandling, - tabWidth : self.tabWidth, - tabChar : '\t', - } - ); - }; - - this.setTextLines = function(lines, index, termWithEol) { - if(0 === index && (0 === self.textLines.length || (self.textLines.length === 1 && '' === self.textLines[0].text) )) { - // quick path: just set the things - 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); - } - ); - }; - - this.insertRawText = function(text, index, col) { - // - // Perform the following on |text|: - // * Normalize various line feed formats -> \n - // * Remove some control characters (e.g. \b) - // * Word wrap lines such that they fit in the visible workspace. - // Each actual line will then take 1:n elements in textLines[]. - // * Each tab will be appropriately expanded and take 1:n \t - // characters. This allows us to know when we're in tab space - // when doing cursor movement/etc. - // - // - // Try to handle any possible newline that can be fed to us. - // See http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line - // - // :TODO: support index/col insertion point - - if(_.isNumber(index)) { - if(_.isNumber(col)) { - // - // Modify text to have information from index - // before and and after column - // - // :TODO: Need to clean this string (e.g. collapse tabs) - text = self.textLines; - - // :TODO: Remove original line @ index - } - } else { - index = self.textLines.length; - } - - text = strUtil.splitTextAtTerms(text); - - let wrapped; - text.forEach(line => { - wrapped = self.wordWrapSingleLine(line, 'expand').wrapped; - - self.setTextLines(wrapped, index, true); // true=termWithEol - index += wrapped.length; - }); - }; - - this.getAbsolutePosition = function(row, col) { - return { - row : self.position.row + row, - col : self.position.col + col, - }; - }; - - this.moveClientCursorToCursorPos = function() { - var absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); - self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col)); - }; - - - this.keyPressCharacter = function(c) { - var index = self.getTextLinesIndex(); - - // - // :TODO: stuff that needs to happen - // * Break up into smaller methods - // * Even in overtype mode, word wrapping must apply if past bounds - // * A lot of this can be used for backspacing also - // * See how Sublime treats tabs in *non* overtype mode... just overwrite them? - // - // - - if(self.overtypeMode) { - // :TODO: special handing for insert over eol mark? - self.replaceCharacterInText(c, index, self.cursorPos.col); - self.cursorPos.col++; - self.client.term.write(c); - } else { - self.insertCharactersInText(c, index, self.cursorPos.col); - } - - self.emitEditPosition(); - }; - - this.keyPressUp = function() { - if(self.cursorPos.row > 0) { - self.cursorPos.row--; - self.client.term.rawWrite(ansi.up()); - - if(!self.adjustCursorToNextTab('up')) { - self.adjustCursorIfPastEndOfLine(false); - } - } else { - self.scrollDocumentDown(); - self.adjustCursorIfPastEndOfLine(true); - } - - self.emitEditPosition(); - }; - - this.keyPressDown = function() { - var lastVisibleRow = Math.min( - self.dimens.height, - (self.textLines.length - self.topVisibleIndex)) - 1; - - if(self.cursorPos.row < lastVisibleRow) { - self.cursorPos.row++; - self.client.term.rawWrite(ansi.down()); - - if(!self.adjustCursorToNextTab('down')) { - self.adjustCursorIfPastEndOfLine(false); - } - } else { - self.scrollDocumentUp(); - self.adjustCursorIfPastEndOfLine(true); - } - - self.emitEditPosition(); - }; - - this.keyPressLeft = function() { - if(self.cursorPos.col > 0) { - var prevCharIsTab = self.isTab(); - - self.cursorPos.col--; - self.client.term.rawWrite(ansi.left()); - - if(prevCharIsTab) { - self.adjustCursorToNextTab('left'); - } - } else { - self.cursorEndOfPreviousLine(); - } - - self.emitEditPosition(); - }; - - this.keyPressRight = function() { - var eolColumn = self.getTextEndOfLineColumn(); - if(self.cursorPos.col < eolColumn) { - var prevCharIsTab = self.isTab(); - - self.cursorPos.col++; - self.client.term.rawWrite(ansi.right()); - - if(prevCharIsTab) { - self.adjustCursorToNextTab('right'); - } - } else { - self.cursorBeginOfNextLine(); - } - - self.emitEditPosition(); - }; - - this.keyPressHome = function() { - var firstNonWhitespace = self.getVisibleText().search(/\S/); - if(-1 !== firstNonWhitespace) { - self.cursorPos.col = firstNonWhitespace; - } else { - self.cursorPos.col = 0; - } - self.moveClientCursorToCursorPos(); - - self.emitEditPosition(); - }; - - this.keyPressEnd = function() { - self.cursorPos.col = self.getTextEndOfLineColumn(); - self.moveClientCursorToCursorPos(); - self.emitEditPosition(); - }; - - this.keyPressPageUp = function() { - if(self.topVisibleIndex > 0) { - self.topVisibleIndex = Math.max(0, self.topVisibleIndex - self.dimens.height); - self.redraw(); - self.adjustCursorIfPastEndOfLine(true); - } else { - self.cursorPos.row = 0; - self.moveClientCursorToCursorPos(); // :TODO: ajust if eol, etc. - } - - self.emitEditPosition(); - }; - - this.keyPressPageDown = function() { - var linesBelow = self.getRemainingLinesBelowRow(); - if(linesBelow > 0) { - self.topVisibleIndex += Math.min(linesBelow, self.dimens.height); - self.redraw(); - self.adjustCursorIfPastEndOfLine(true); - } - - self.emitEditPosition(); - }; - - this.keyPressLineFeed = function() { - // - // Break up text from cursor position, redraw, and update cursor - // position to start of next line - // - var index = self.getTextLinesIndex(); - var nextEolIndex = self.getNextEndOfLineIndex(index); - var text = self.getContiguousText(index, nextEolIndex); - const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; - - newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); - for(var i = 1; i < newLines.length; ++i) { - newLines[i] = { text : newLines[i] }; - } - newLines[newLines.length - 1].eol = true; - - Array.prototype.splice.apply( - self.textLines, - [ index, (nextEolIndex - index) + 1 ].concat(newLines)); - - // redraw from current row to end of visible area - self.redrawRows(self.cursorPos.row, self.dimens.height); - self.cursorBeginOfNextLine(); - - self.emitEditPosition(); - }; - - this.keyPressInsert = function() { - self.toggleTextEditMode(); - }; - - this.keyPressTab = function() { - var index = self.getTextLinesIndex(); - self.insertCharactersInText(self.expandTab(self.cursorPos.col, '\t') + '\t', index, self.cursorPos.col); - - self.emitEditPosition(); - }; - - this.keyPressBackspace = function() { - if(self.cursorPos.col >= 1) { - // - // Don't want to delete character at cursor, but rather the character - // to the left of the cursor! - // - self.cursorPos.col -= 1; - - var index = self.getTextLinesIndex(); - var count; - - if(self.isTab()) { - var col = self.cursorPos.col; - var prevTabStop = self.getPrevTabStop(self.cursorPos.col); - while(col >= prevTabStop) { - if(!self.isTab(index, col)) { - break; - } - --col; - } - - count = (self.cursorPos.col - col); - } else { - count = 1; - } - - self.removeCharactersFromText( - index, - self.cursorPos.col, - 'backspace', - count); - } else { - // - // Delete character at end of line previous. - // * This may be a eol marker - // * Word wrapping will need re-applied - // - // :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev - self.keyPressLeft(); // same as hitting left - jump to previous line - //self.keyPressBackspace(); - } - - self.emitEditPosition(); - }; - - this.keyPressDelete = function() { - const lineIndex = self.getTextLinesIndex(); - - if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) { - // - // Start of line and nothing left. Just delete the line - // - self.removeCharactersFromText( - lineIndex, - 0, - 'delete line' - ); - } else { - self.removeCharactersFromText( - lineIndex, - self.cursorPos.col, - 'delete', - 1 - ); - } - - self.emitEditPosition(); - }; - - this.keyPressDeleteLine = function() { - if(self.textLines.length > 0) { - self.removeCharactersFromText( - self.getTextLinesIndex(), - 0, - 'delete line'); - } - - self.emitEditPosition(); - }; - - this.adjustCursorIfPastEndOfLine = function(forceUpdate) { - var eolColumn = self.getTextEndOfLineColumn(); - if(self.cursorPos.col > eolColumn) { - self.cursorPos.col = eolColumn; - forceUpdate = true; - } - - if(forceUpdate) { - self.moveClientCursorToCursorPos(); - } - }; - - this.adjustCursorToNextTab = function(direction) { - if(self.isTab()) { - var move; - switch(direction) { - // - // Next tabstop to the right - // - case 'right' : - move = self.getNextTabStop(self.cursorPos.col) - self.cursorPos.col; - self.cursorPos.col += move; - self.client.term.rawWrite(ansi.right(move)); - break; - - // - // Next tabstop to the left - // - case 'left' : - move = self.cursorPos.col - self.getPrevTabStop(self.cursorPos.col); - self.cursorPos.col -= move; - self.client.term.rawWrite(ansi.left(move)); - break; - - case 'up' : - case 'down' : - // - // Jump to the tabstop nearest the cursor - // - var newCol = self.tabStops.reduce(function r(prev, curr) { - return (Math.abs(curr - self.cursorPos.col) < Math.abs(prev - self.cursorPos.col) ? curr : prev); - }); - - if(newCol > self.cursorPos.col) { - move = newCol - self.cursorPos.col; - self.cursorPos.col += move; - self.client.term.rawWrite(ansi.right(move)); - } else if(newCol < self.cursorPos.col) { - move = self.cursorPos.col - newCol; - self.cursorPos.col -= move; - self.client.term.rawWrite(ansi.left(move)); - } - break; - } - - return true; - } - return false; // did not fall on a tab - }; - - this.cursorStartOfDocument = function() { - self.topVisibleIndex = 0; - self.cursorPos = { row : 0, col : 0 }; - - self.redraw(); - self.moveClientCursorToCursorPos(); - }; - - this.cursorEndOfDocument = function() { - self.topVisibleIndex = Math.max(self.textLines.length - self.dimens.height, 0); - self.cursorPos.row = (self.textLines.length - self.topVisibleIndex) - 1; - self.cursorPos.col = self.getTextEndOfLineColumn(); - - self.redraw(); - self.moveClientCursorToCursorPos(); - }; - - this.cursorBeginOfNextLine = function() { - // e.g. when scrolling right past eol - var linesBelow = self.getRemainingLinesBelowRow(); - - if(linesBelow > 0) { - var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; - if(self.cursorPos.row < lastVisibleRow) { - self.cursorPos.row++; - } else { - self.scrollDocumentUp(); - } - self.keyPressHome(); // same as pressing 'home' - } - }; - - this.cursorEndOfPreviousLine = function() { - // e.g. when scrolling left past start of line - var moveToEnd; - if(self.cursorPos.row > 0) { - self.cursorPos.row--; - moveToEnd = true; - } else if(self.topVisibleIndex > 0) { - self.scrollDocumentDown(); - moveToEnd = true; - } - - if(moveToEnd) { - self.keyPressEnd(); // same as pressing 'end' - } - }; - - /* + self.cursorPos.col -= (count - 1); + + self.updateTextWordWrap(index); + self.redrawRows(self.cursorPos.row, self.dimens.height); + + self.moveClientCursorToCursorPos(); + } else if('delete line' === operation) { + // + // Delete a visible line. Note that this is *not* the "physical" line, or + // 1:n entries up to eol! This is to keep consistency with home/end, and + // some other text editors such as nano. Sublime for example want to + // treat all of these things using the physical approach, but this seems + // a bit odd in this context. + // + var isLastLine = (index === self.textLines.length - 1); + var hadEol = self.textLines[index].eol; + + self.textLines.splice(index, 1); + if(hadEol && self.textLines.length > index && !self.textLines[index].eol) { + self.textLines[index].eol = true; + } + + // + // Create a empty edit buffer if necessary + // :TODO: Make this a method + if(self.textLines.length < 1) { + self.textLines = [ { text : '', eol : true } ]; + isLastLine = false; // resetting + } + + self.cursorPos.col = 0; + + var lastRow = self.redrawRows(self.cursorPos.row, self.dimens.height); + self.eraseRows(lastRow, self.dimens.height); + + // + // If we just deleted the last line in the buffer, move up + // + if(isLastLine) { + self.cursorEndOfPreviousLine(); + } else { + self.moveClientCursorToCursorPos(); + } + } + }; + + this.insertCharactersInText = function(c, index, col) { + const prevTextLength = self.getTextLength(index); + let editingEol = self.cursorPos.col === prevTextLength; + + self.textLines[index].text = [ + self.textLines[index].text.slice(0, col), + c, + self.textLines[index].text.slice(col) + ].join(''); + + self.cursorPos.col += c.length; + + if(self.getTextLength(index) > self.dimens.width) { + // + // Update word wrapping and |cursorOffset| if the cursor + // was within the bounds of the wrapped text + // + let cursorOffset; + const lastCol = self.cursorPos.col - c.length; + const firstWrapRange = self.updateTextWordWrap(index); + if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { + cursorOffset = self.cursorPos.col - firstWrapRange.start; + editingEol = true; //override + } else { + cursorOffset = firstWrapRange.end; + } + + // redraw from current row to end of visible area + self.redrawRows(self.cursorPos.row, self.dimens.height); + + // If we're editing mid, we're done here. Else, we need to + // move the cursor to the new editing position after a wrap + if(editingEol) { + self.cursorBeginOfNextLine(); + self.cursorPos.col += cursorOffset; + self.client.term.rawWrite(ansi.right(cursorOffset)); + } else { + // adjust cursor after drawing new rows + const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col)); + } + } else { + // + // We must only redraw from col -> end of current visible line + // + const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length); + + self.client.term.write( + `${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto(absPos.row, absPos.col)}${ansi.showCursor()}`, + false // convertLineFeeds + ); + } + }; + + this.getRemainingTabWidth = function(col) { + if(!_.isNumber(col)) { + col = self.cursorPos.col; + } + return self.tabWidth - (col % self.tabWidth); + }; + + this.calculateTabStops = function() { + self.tabStops = [ 0 ]; + var col = 0; + while(col < self.dimens.width) { + col += self.getRemainingTabWidth(col); + self.tabStops.push(col); + } + }; + + this.getNextTabStop = function(col) { + var i = self.tabStops.length; + while(self.tabStops[--i] > col); + return self.tabStops[++i]; + }; + + this.getPrevTabStop = function(col) { + var i = self.tabStops.length; + while(self.tabStops[--i] >= col); + return self.tabStops[i]; + }; + + this.expandTab = function(col, expandChar) { + expandChar = expandChar || ' '; + return new Array(self.getRemainingTabWidth(col)).join(expandChar); + }; + + this.wordWrapSingleLine = function(line, tabHandling = 'expand') { + return wordWrapText( + line, + { + width : self.dimens.width, + tabHandling : tabHandling, + tabWidth : self.tabWidth, + tabChar : '\t', + } + ); + }; + + this.setTextLines = function(lines, index, termWithEol) { + if(0 === index && (0 === self.textLines.length || (self.textLines.length === 1 && '' === self.textLines[0].text) )) { + // quick path: just set the things + 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); + } + ); + }; + + this.insertRawText = function(text, index, col) { + // + // Perform the following on |text|: + // * Normalize various line feed formats -> \n + // * Remove some control characters (e.g. \b) + // * Word wrap lines such that they fit in the visible workspace. + // Each actual line will then take 1:n elements in textLines[]. + // * Each tab will be appropriately expanded and take 1:n \t + // characters. This allows us to know when we're in tab space + // when doing cursor movement/etc. + // + // + // Try to handle any possible newline that can be fed to us. + // See http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line + // + // :TODO: support index/col insertion point + + if(_.isNumber(index)) { + if(_.isNumber(col)) { + // + // Modify text to have information from index + // before and and after column + // + // :TODO: Need to clean this string (e.g. collapse tabs) + text = self.textLines; + + // :TODO: Remove original line @ index + } + } else { + index = self.textLines.length; + } + + text = strUtil.splitTextAtTerms(text); + + let wrapped; + text.forEach(line => { + wrapped = self.wordWrapSingleLine(line, 'expand').wrapped; + + self.setTextLines(wrapped, index, true); // true=termWithEol + index += wrapped.length; + }); + }; + + this.getAbsolutePosition = function(row, col) { + return { + row : self.position.row + row, + col : self.position.col + col, + }; + }; + + this.moveClientCursorToCursorPos = function() { + var absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); + self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col)); + }; + + + this.keyPressCharacter = function(c) { + var index = self.getTextLinesIndex(); + + // + // :TODO: stuff that needs to happen + // * Break up into smaller methods + // * Even in overtype mode, word wrapping must apply if past bounds + // * A lot of this can be used for backspacing also + // * See how Sublime treats tabs in *non* overtype mode... just overwrite them? + // + // + + if(self.overtypeMode) { + // :TODO: special handing for insert over eol mark? + self.replaceCharacterInText(c, index, self.cursorPos.col); + self.cursorPos.col++; + self.client.term.write(c); + } else { + self.insertCharactersInText(c, index, self.cursorPos.col); + } + + self.emitEditPosition(); + }; + + this.keyPressUp = function() { + if(self.cursorPos.row > 0) { + self.cursorPos.row--; + self.client.term.rawWrite(ansi.up()); + + if(!self.adjustCursorToNextTab('up')) { + self.adjustCursorIfPastEndOfLine(false); + } + } else { + self.scrollDocumentDown(); + self.adjustCursorIfPastEndOfLine(true); + } + + self.emitEditPosition(); + }; + + this.keyPressDown = function() { + var lastVisibleRow = Math.min( + self.dimens.height, + (self.textLines.length - self.topVisibleIndex)) - 1; + + if(self.cursorPos.row < lastVisibleRow) { + self.cursorPos.row++; + self.client.term.rawWrite(ansi.down()); + + if(!self.adjustCursorToNextTab('down')) { + self.adjustCursorIfPastEndOfLine(false); + } + } else { + self.scrollDocumentUp(); + self.adjustCursorIfPastEndOfLine(true); + } + + self.emitEditPosition(); + }; + + this.keyPressLeft = function() { + if(self.cursorPos.col > 0) { + var prevCharIsTab = self.isTab(); + + self.cursorPos.col--; + self.client.term.rawWrite(ansi.left()); + + if(prevCharIsTab) { + self.adjustCursorToNextTab('left'); + } + } else { + self.cursorEndOfPreviousLine(); + } + + self.emitEditPosition(); + }; + + this.keyPressRight = function() { + var eolColumn = self.getTextEndOfLineColumn(); + if(self.cursorPos.col < eolColumn) { + var prevCharIsTab = self.isTab(); + + self.cursorPos.col++; + self.client.term.rawWrite(ansi.right()); + + if(prevCharIsTab) { + self.adjustCursorToNextTab('right'); + } + } else { + self.cursorBeginOfNextLine(); + } + + self.emitEditPosition(); + }; + + this.keyPressHome = function() { + var firstNonWhitespace = self.getVisibleText().search(/\S/); + if(-1 !== firstNonWhitespace) { + self.cursorPos.col = firstNonWhitespace; + } else { + self.cursorPos.col = 0; + } + self.moveClientCursorToCursorPos(); + + self.emitEditPosition(); + }; + + this.keyPressEnd = function() { + self.cursorPos.col = self.getTextEndOfLineColumn(); + self.moveClientCursorToCursorPos(); + self.emitEditPosition(); + }; + + this.keyPressPageUp = function() { + if(self.topVisibleIndex > 0) { + self.topVisibleIndex = Math.max(0, self.topVisibleIndex - self.dimens.height); + self.redraw(); + self.adjustCursorIfPastEndOfLine(true); + } else { + self.cursorPos.row = 0; + self.moveClientCursorToCursorPos(); // :TODO: ajust if eol, etc. + } + + self.emitEditPosition(); + }; + + this.keyPressPageDown = function() { + var linesBelow = self.getRemainingLinesBelowRow(); + if(linesBelow > 0) { + self.topVisibleIndex += Math.min(linesBelow, self.dimens.height); + self.redraw(); + self.adjustCursorIfPastEndOfLine(true); + } + + self.emitEditPosition(); + }; + + this.keyPressLineFeed = function() { + // + // Break up text from cursor position, redraw, and update cursor + // position to start of next line + // + var index = self.getTextLinesIndex(); + var nextEolIndex = self.getNextEndOfLineIndex(index); + var text = self.getContiguousText(index, nextEolIndex); + const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; + + newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); + for(var i = 1; i < newLines.length; ++i) { + newLines[i] = { text : newLines[i] }; + } + newLines[newLines.length - 1].eol = true; + + Array.prototype.splice.apply( + self.textLines, + [ index, (nextEolIndex - index) + 1 ].concat(newLines)); + + // redraw from current row to end of visible area + self.redrawRows(self.cursorPos.row, self.dimens.height); + self.cursorBeginOfNextLine(); + + self.emitEditPosition(); + }; + + this.keyPressInsert = function() { + self.toggleTextEditMode(); + }; + + this.keyPressTab = function() { + var index = self.getTextLinesIndex(); + self.insertCharactersInText(self.expandTab(self.cursorPos.col, '\t') + '\t', index, self.cursorPos.col); + + self.emitEditPosition(); + }; + + this.keyPressBackspace = function() { + if(self.cursorPos.col >= 1) { + // + // Don't want to delete character at cursor, but rather the character + // to the left of the cursor! + // + self.cursorPos.col -= 1; + + var index = self.getTextLinesIndex(); + var count; + + if(self.isTab()) { + var col = self.cursorPos.col; + var prevTabStop = self.getPrevTabStop(self.cursorPos.col); + while(col >= prevTabStop) { + if(!self.isTab(index, col)) { + break; + } + --col; + } + + count = (self.cursorPos.col - col); + } else { + count = 1; + } + + self.removeCharactersFromText( + index, + self.cursorPos.col, + 'backspace', + count); + } else { + // + // Delete character at end of line previous. + // * This may be a eol marker + // * Word wrapping will need re-applied + // + // :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev + self.keyPressLeft(); // same as hitting left - jump to previous line + //self.keyPressBackspace(); + } + + self.emitEditPosition(); + }; + + this.keyPressDelete = function() { + const lineIndex = self.getTextLinesIndex(); + + if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) { + // + // Start of line and nothing left. Just delete the line + // + self.removeCharactersFromText( + lineIndex, + 0, + 'delete line' + ); + } else { + self.removeCharactersFromText( + lineIndex, + self.cursorPos.col, + 'delete', + 1 + ); + } + + self.emitEditPosition(); + }; + + this.keyPressDeleteLine = function() { + if(self.textLines.length > 0) { + self.removeCharactersFromText( + self.getTextLinesIndex(), + 0, + 'delete line'); + } + + self.emitEditPosition(); + }; + + this.adjustCursorIfPastEndOfLine = function(forceUpdate) { + var eolColumn = self.getTextEndOfLineColumn(); + if(self.cursorPos.col > eolColumn) { + self.cursorPos.col = eolColumn; + forceUpdate = true; + } + + if(forceUpdate) { + self.moveClientCursorToCursorPos(); + } + }; + + this.adjustCursorToNextTab = function(direction) { + if(self.isTab()) { + var move; + switch(direction) { + // + // Next tabstop to the right + // + case 'right' : + move = self.getNextTabStop(self.cursorPos.col) - self.cursorPos.col; + self.cursorPos.col += move; + self.client.term.rawWrite(ansi.right(move)); + break; + + // + // Next tabstop to the left + // + case 'left' : + move = self.cursorPos.col - self.getPrevTabStop(self.cursorPos.col); + self.cursorPos.col -= move; + self.client.term.rawWrite(ansi.left(move)); + break; + + case 'up' : + case 'down' : + // + // Jump to the tabstop nearest the cursor + // + var newCol = self.tabStops.reduce(function r(prev, curr) { + return (Math.abs(curr - self.cursorPos.col) < Math.abs(prev - self.cursorPos.col) ? curr : prev); + }); + + if(newCol > self.cursorPos.col) { + move = newCol - self.cursorPos.col; + self.cursorPos.col += move; + self.client.term.rawWrite(ansi.right(move)); + } else if(newCol < self.cursorPos.col) { + move = self.cursorPos.col - newCol; + self.cursorPos.col -= move; + self.client.term.rawWrite(ansi.left(move)); + } + break; + } + + return true; + } + return false; // did not fall on a tab + }; + + this.cursorStartOfDocument = function() { + self.topVisibleIndex = 0; + self.cursorPos = { row : 0, col : 0 }; + + self.redraw(); + self.moveClientCursorToCursorPos(); + }; + + this.cursorEndOfDocument = function() { + self.topVisibleIndex = Math.max(self.textLines.length - self.dimens.height, 0); + self.cursorPos.row = (self.textLines.length - self.topVisibleIndex) - 1; + self.cursorPos.col = self.getTextEndOfLineColumn(); + + self.redraw(); + self.moveClientCursorToCursorPos(); + }; + + this.cursorBeginOfNextLine = function() { + // e.g. when scrolling right past eol + var linesBelow = self.getRemainingLinesBelowRow(); + + if(linesBelow > 0) { + var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; + if(self.cursorPos.row < lastVisibleRow) { + self.cursorPos.row++; + } else { + self.scrollDocumentUp(); + } + self.keyPressHome(); // same as pressing 'home' + } + }; + + this.cursorEndOfPreviousLine = function() { + // e.g. when scrolling left past start of line + var moveToEnd; + if(self.cursorPos.row > 0) { + self.cursorPos.row--; + moveToEnd = true; + } else if(self.topVisibleIndex > 0) { + self.scrollDocumentDown(); + moveToEnd = true; + } + + if(moveToEnd) { + self.keyPressEnd(); // same as pressing 'end' + } + }; + + /* this.cusorEndOfNextLine = function() { var linesBelow = self.getRemainingLinesBelowRow(); @@ -1013,66 +1013,66 @@ function MultiLineEditTextView(options) { }; */ - this.scrollDocumentUp = function() { - // - // Note: We scroll *up* when the cursor goes *down* beyond - // the visible area! - // - var linesBelow = self.getRemainingLinesBelowRow(); - if(linesBelow > 0) { - self.topVisibleIndex++; - self.redraw(); - } - }; + this.scrollDocumentUp = function() { + // + // Note: We scroll *up* when the cursor goes *down* beyond + // the visible area! + // + var linesBelow = self.getRemainingLinesBelowRow(); + if(linesBelow > 0) { + self.topVisibleIndex++; + self.redraw(); + } + }; - this.scrollDocumentDown = function() { - // - // Note: We scroll *down* when the cursor goes *up* beyond - // the visible area! - // - if(self.topVisibleIndex > 0) { - self.topVisibleIndex--; - self.redraw(); - } - }; + this.scrollDocumentDown = function() { + // + // Note: We scroll *down* when the cursor goes *up* beyond + // the visible area! + // + if(self.topVisibleIndex > 0) { + self.topVisibleIndex--; + self.redraw(); + } + }; - this.emitEditPosition = function() { - self.emit('edit position', self.getEditPosition()); - }; + this.emitEditPosition = function() { + self.emit('edit position', self.getEditPosition()); + }; - this.toggleTextEditMode = function() { - self.overtypeMode = !self.overtypeMode; - self.emit('text edit mode', self.getTextEditMode()); - }; + this.toggleTextEditMode = function() { + self.overtypeMode = !self.overtypeMode; + self.emit('text edit mode', self.getTextEditMode()); + }; - this.insertRawText(''); // init to blank/empty + this.insertRawText(''); // init to blank/empty } require('util').inherits(MultiLineEditTextView, View); MultiLineEditTextView.prototype.setWidth = function(width) { - MultiLineEditTextView.super_.prototype.setWidth.call(this, width); + MultiLineEditTextView.super_.prototype.setWidth.call(this, width); - this.calculateTabStops(); + this.calculateTabStops(); }; MultiLineEditTextView.prototype.redraw = function() { - MultiLineEditTextView.super_.prototype.redraw.call(this); + MultiLineEditTextView.super_.prototype.redraw.call(this); - this.redrawVisibleArea(); + this.redrawVisibleArea(); }; MultiLineEditTextView.prototype.setFocus = function(focused) { - this.client.term.rawWrite(this.getSGRFor('text')); - this.moveClientCursorToCursorPos(); + this.client.term.rawWrite(this.getSGRFor('text')); + this.moveClientCursorToCursorPos(); - MultiLineEditTextView.super_.prototype.setFocus.call(this, focused); + MultiLineEditTextView.super_.prototype.setFocus.call(this, focused); }; MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode : 'default' } ) { - this.textLines = [ ]; - this.addText(text, options); - /*this.insertRawText(text); + this.textLines = [ ]; + this.addText(text, options); + /*this.insertRawText(text); if(this.isEditMode()) { this.cursorEndOfDocument(); @@ -1082,132 +1082,132 @@ MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode }; MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : false }, cb) { - this.textLines = [ ]; - return this.setAnsiWithOptions(ansi, options, cb); + this.textLines = [ ]; + return this.setAnsiWithOptions(ansi, options, cb); }; MultiLineEditTextView.prototype.addText = function(text, options = { scrollMode : 'default' }) { - this.insertRawText(text); + this.insertRawText(text); - switch(options.scrollMode) { - case 'default' : - if(this.isEditMode() || this.autoScroll) { - this.cursorEndOfDocument(); - } else { - this.cursorStartOfDocument(); - } - break; + switch(options.scrollMode) { + case 'default' : + if(this.isEditMode() || this.autoScroll) { + this.cursorEndOfDocument(); + } else { + this.cursorStartOfDocument(); + } + break; - case 'top' : - case 'start' : - this.cursorStartOfDocument(); - break; + case 'top' : + case 'start' : + this.cursorStartOfDocument(); + break; - case 'end' : - case 'bottom' : - this.cursorEndOfDocument(); - break; - } + case 'end' : + case 'bottom' : + this.cursorEndOfDocument(); + break; + } }; MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms : false } ) { - return this.getOutputText(0, this.textLines.length, '\r\n', options); + return this.getOutputText(0, this.textLines.length, '\r\n', options); }; MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'mode' : - this.mode = value; - if('preview' === value && !this.specialKeyMap.next) { - this.specialKeyMap.next = [ 'tab' ]; - } - break; + switch(propName) { + case 'mode' : + this.mode = value; + if('preview' === value && !this.specialKeyMap.next) { + this.specialKeyMap.next = [ 'tab' ]; + } + break; - case 'autoScroll' : - this.autoScroll = value; - break; + case 'autoScroll' : + this.autoScroll = value; + break; - case 'tabSwitchesView' : - this.tabSwitchesView = value; - this.specialKeyMap.next = this.specialKeyMap.next || []; - this.specialKeyMap.next.push('tab'); - break; - } + case 'tabSwitchesView' : + this.tabSwitchesView = value; + this.specialKeyMap.next = this.specialKeyMap.next || []; + this.specialKeyMap.next.push('tab'); + break; + } - MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); + MultiLineEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); }; const HANDLED_SPECIAL_KEYS = [ - 'up', 'down', 'left', 'right', - 'home', 'end', - 'page up', 'page down', - 'line feed', - 'insert', - 'tab', - 'backspace', 'delete', - 'delete line', + 'up', 'down', 'left', 'right', + 'home', 'end', + 'page up', 'page down', + 'line feed', + 'insert', + 'tab', + 'backspace', 'delete', + 'delete line', ]; const PREVIEW_MODE_KEYS = [ - 'up', 'down', 'page up', 'page down' + 'up', 'down', 'page up', 'page down' ]; MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { - const self = this; - let handled; + const self = this; + let handled; - if(key) { - HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) { - if(self.isKeyMapped(specialKey, key.name)) { + if(key) { + HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) { + if(self.isKeyMapped(specialKey, key.name)) { - if(self.isPreviewMode() && -1 === PREVIEW_MODE_KEYS.indexOf(specialKey)) { - return; - } + if(self.isPreviewMode() && -1 === PREVIEW_MODE_KEYS.indexOf(specialKey)) { + return; + } - if('tab' !== key.name || !self.tabSwitchesView) { - self[_.camelCase('keyPress ' + specialKey)](); - handled = true; - } - } - }); - } + if('tab' !== key.name || !self.tabSwitchesView) { + self[_.camelCase('keyPress ' + specialKey)](); + handled = true; + } + } + }); + } - if(self.isEditMode() && ch && strUtil.isPrintable(ch)) { - this.keyPressCharacter(ch); - } + if(self.isEditMode() && ch && strUtil.isPrintable(ch)) { + this.keyPressCharacter(ch); + } - if(!handled) { - MultiLineEditTextView.super_.prototype.onKeyPress.call(this, ch, key); - } + if(!handled) { + MultiLineEditTextView.super_.prototype.onKeyPress.call(this, ch, key); + } }; MultiLineEditTextView.prototype.scrollUp = function() { - this.scrollDocumentUp(); + this.scrollDocumentUp(); }; MultiLineEditTextView.prototype.scrollDown = function() { - this.scrollDocumentDown(); + this.scrollDocumentDown(); }; MultiLineEditTextView.prototype.deleteLine = function(line) { - this.textLines.splice(line, 1); + this.textLines.splice(line, 1); }; MultiLineEditTextView.prototype.getLineCount = function() { - return this.textLines.length; + return this.textLines.length; }; MultiLineEditTextView.prototype.getTextEditMode = function() { - return this.overtypeMode ? 'overtype' : 'insert'; + return this.overtypeMode ? 'overtype' : 'insert'; }; MultiLineEditTextView.prototype.getEditPosition = function() { - var currentIndex = this.getTextLinesIndex() + 1; + var currentIndex = this.getTextLinesIndex() + 1; - return { - row : this.getTextLinesIndex(this.cursorPos.row), - col : this.cursorPos.col, - percent : Math.floor(((currentIndex / this.textLines.length) * 100)), - below : this.getRemainingLinesBelowRow(), - }; + return { + row : this.getTextLinesIndex(this.cursorPos.row), + col : this.cursorPos.col, + percent : Math.floor(((currentIndex / this.textLines.length) * 100)), + below : this.getRemainingLinesBelowRow(), + }; }; diff --git a/core/new_scan.js b/core/new_scan.js index b088e47f..b84eab52 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -16,9 +16,9 @@ const _ = require('lodash'); const async = require('async'); exports.moduleInfo = { - name : 'New Scan', - desc : 'Performs a new scan against various areas of the system', - author : 'NuSkooler', + name : 'New Scan', + desc : 'Performs a new scan against various areas of the system', + author : 'NuSkooler', }; /* @@ -30,239 +30,239 @@ exports.moduleInfo = { */ const MciCodeIds = { - ScanStatusLabel : 1, // TL1 - ScanStatusList : 2, // VM2 (appends) + ScanStatusLabel : 1, // TL1 + ScanStatusList : 2, // VM2 (appends) }; const Steps = { - MessageConfs : 'messageConferences', - FileBase : 'fileBase', + MessageConfs : 'messageConferences', + FileBase : 'fileBase', - Finished : 'finished', + Finished : 'finished', }; exports.getModule = class NewScanModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false); + this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false); - this.currentStep = Steps.MessageConfs; - this.currentScanAux = {}; + this.currentStep = Steps.MessageConfs; + this.currentScanAux = {}; - // :TODO: Make this conf/area specific: - const config = this.menuConfig.config; - this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; - this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; - this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; - this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; - } + // :TODO: Make this conf/area specific: + const config = this.menuConfig.config; + this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; + this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; + this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; + this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; + } - updateScanStatus(statusText) { - this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText); - } + updateScanStatus(statusText) { + this.setViewText('allViews', MciCodeIds.ScanStatusLabel, statusText); + } - newScanMessageConference(cb) { - // lazy init - if(!this.sortedMessageConfs) { - const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. + newScanMessageConference(cb) { + // lazy init + if(!this.sortedMessageConfs) { + const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. - this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => { - return { - confTag : k, - conf : v, - }; - }); + this.sortedMessageConfs = _.map(msgArea.getAvailableMessageConferences(this.client, getAvailOpts), (v, k) => { + return { + confTag : k, + conf : v, + }; + }); - // - // Sort conferences by name, other than 'system_internal' which should - // always come first such that we display private mails/etc. before - // other conferences & areas - // - this.sortedMessageConfs.sort((a, b) => { - if('system_internal' === a.confTag) { - return -1; - } else { - return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } ); - } - }); + // + // Sort conferences by name, other than 'system_internal' which should + // always come first such that we display private mails/etc. before + // other conferences & areas + // + this.sortedMessageConfs.sort((a, b) => { + if('system_internal' === a.confTag) { + return -1; + } else { + return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } ); + } + }); - this.currentScanAux.conf = this.currentScanAux.conf || 0; - this.currentScanAux.area = this.currentScanAux.area || 0; - } + this.currentScanAux.conf = this.currentScanAux.conf || 0; + this.currentScanAux.area = this.currentScanAux.area || 0; + } - const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; + const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; - this.newScanMessageArea(currentConf, () => { - if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) { - this.currentScanAux.conf += 1; - this.currentScanAux.area = 0; + this.newScanMessageArea(currentConf, () => { + if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) { + this.currentScanAux.conf += 1; + this.currentScanAux.area = 0; - return this.newScanMessageConference(cb); // recursive to next conf - } + return this.newScanMessageConference(cb); // recursive to next conf + } - this.updateScanStatus(this.scanCompleteMsg); - return cb(Errors.DoesNotExist('No more conferences')); - }); - } + this.updateScanStatus(this.scanCompleteMsg); + return cb(Errors.DoesNotExist('No more conferences')); + }); + } - newScanMessageArea(conf, cb) { - // :TODO: it would be nice to cache this - must be done by conf! - const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); - const currentArea = sortedAreas[this.currentScanAux.area]; + newScanMessageArea(conf, cb) { + // :TODO: it would be nice to cache this - must be done by conf! + const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); + const currentArea = sortedAreas[this.currentScanAux.area]; - // - // Scan and update index until we find something. If results are found, - // we'll goto the list module & show them. - // - const self = this; - async.waterfall( - [ - function checkAndUpdateIndex(callback) { - // Advance to next area if possible - if(sortedAreas.length >= self.currentScanAux.area + 1) { - self.currentScanAux.area += 1; - return callback(null); - } else { - self.updateScanStatus(self.scanCompleteMsg); - return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan - } - }, - function updateStatusScanStarted(callback) { - self.updateScanStatus(stringFormat(self.scanStartFmt, { - confName : conf.conf.name, - confDesc : conf.conf.desc, - areaName : currentArea.area.name, - areaDesc : currentArea.area.desc - })); - return callback(null); - }, - function getNewMessagesCountInArea(callback) { - msgArea.getNewMessageCountInAreaForUser( - self.client.user.userId, currentArea.areaTag, (err, newMessageCount) => { - callback(err, newMessageCount); - } - ); - }, - function displayMessageList(newMessageCount) { - if(newMessageCount <= 0) { - return self.newScanMessageArea(conf, cb); // next area, if any - } + // + // Scan and update index until we find something. If results are found, + // we'll goto the list module & show them. + // + const self = this; + async.waterfall( + [ + function checkAndUpdateIndex(callback) { + // Advance to next area if possible + if(sortedAreas.length >= self.currentScanAux.area + 1) { + self.currentScanAux.area += 1; + return callback(null); + } else { + self.updateScanStatus(self.scanCompleteMsg); + return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan + } + }, + function updateStatusScanStarted(callback) { + self.updateScanStatus(stringFormat(self.scanStartFmt, { + confName : conf.conf.name, + confDesc : conf.conf.desc, + areaName : currentArea.area.name, + areaDesc : currentArea.area.desc + })); + return callback(null); + }, + function getNewMessagesCountInArea(callback) { + msgArea.getNewMessageCountInAreaForUser( + self.client.user.userId, currentArea.areaTag, (err, newMessageCount) => { + callback(err, newMessageCount); + } + ); + }, + function displayMessageList(newMessageCount) { + if(newMessageCount <= 0) { + return self.newScanMessageArea(conf, cb); // next area, if any + } - const nextModuleOpts = { - extraArgs: { - messageAreaTag : currentArea.areaTag, - } - }; + const nextModuleOpts = { + extraArgs: { + messageAreaTag : currentArea.areaTag, + } + }; - return self.gotoMenu(self.menuConfig.config.newScanMessageList || 'newScanMessageList', nextModuleOpts); - } - ], - err => { - return cb(err); - } - ); - } + return self.gotoMenu(self.menuConfig.config.newScanMessageList || 'newScanMessageList', nextModuleOpts); + } + ], + err => { + return cb(err); + } + ); + } - newScanFileBase(cb) { - // :TODO: add in steps - const filterCriteria = { - newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), - areaTag : getAvailableFileAreaTags(this.client), - order : 'ascending', // oldest first - }; + newScanFileBase(cb) { + // :TODO: add in steps + const filterCriteria = { + newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), + areaTag : getAvailableFileAreaTags(this.client), + order : 'ascending', // oldest first + }; - FileEntry.findFiles( - filterCriteria, - (err, fileIds) => { - if(err || 0 === fileIds.length) { - return cb(err ? err : Errors.DoesNotExist('No more new files')); - } + FileEntry.findFiles( + filterCriteria, + (err, fileIds) => { + if(err || 0 === fileIds.length) { + return cb(err ? err : Errors.DoesNotExist('No more new files')); + } - FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[fileIds.length - 1] ); + FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, fileIds[fileIds.length - 1] ); - const menuOpts = { - extraArgs : { - fileList : fileIds, - }, - }; + const menuOpts = { + extraArgs : { + fileList : fileIds, + }, + }; - return this.gotoMenu(this.menuConfig.config.newScanFileBaseList || 'newScanFileBaseList', menuOpts); - } - ); - } + return this.gotoMenu(this.menuConfig.config.newScanFileBaseList || 'newScanFileBaseList', menuOpts); + } + ); + } - getSaveState() { - return { - currentStep : this.currentStep, - currentScanAux : this.currentScanAux, - }; - } + getSaveState() { + return { + currentStep : this.currentStep, + currentScanAux : this.currentScanAux, + }; + } - restoreSavedState(savedState) { - this.currentStep = savedState.currentStep; - this.currentScanAux = savedState.currentScanAux; - } + restoreSavedState(savedState) { + this.currentStep = savedState.currentStep; + this.currentScanAux = savedState.currentScanAux; + } - performScanCurrentStep(cb) { - switch(this.currentStep) { - case Steps.MessageConfs : - this.newScanMessageConference( () => { - this.currentStep = Steps.FileBase; - return this.performScanCurrentStep(cb); - }); - break; + performScanCurrentStep(cb) { + switch(this.currentStep) { + case Steps.MessageConfs : + this.newScanMessageConference( () => { + this.currentStep = Steps.FileBase; + return this.performScanCurrentStep(cb); + }); + break; - case Steps.FileBase : - this.newScanFileBase( () => { - this.currentStep = Steps.Finished; - return this.performScanCurrentStep(cb); - }); - break; + case Steps.FileBase : + this.newScanFileBase( () => { + this.currentStep = Steps.Finished; + return this.performScanCurrentStep(cb); + }); + break; - default : return cb(null); - } - } + default : return cb(null); + } + } - mciReady(mciData, cb) { - if(this.newScanFullExit) { - // user has canceled the entire scan @ message list view - return cb(null); - } + mciReady(mciData, cb) { + if(this.newScanFullExit) { + // user has canceled the entire scan @ message list view + return cb(null); + } - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - // :TODO: display scan step/etc. + // :TODO: display scan step/etc. - async.series( - [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, - }; + async.series( + [ + function loadFromConfig(callback) { + const loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + noInput : true, + }; - vc.loadFromMenuConfig(loadOpts, callback); - }, - function performCurrentStepScan(callback) { - return self.performScanCurrentStep(callback); - } - ], - err => { - if(err) { - self.client.log.error( { error : err.toString() }, 'Error during new scan'); - } - return cb(err); - } - ); - }); - } + vc.loadFromMenuConfig(loadOpts, callback); + }, + function performCurrentStepScan(callback) { + return self.performScanCurrentStep(callback); + } + ], + err => { + if(err) { + self.client.log.error( { error : err.toString() }, 'Error during new scan'); + } + return cb(err); + } + ); + }); + } }; diff --git a/core/nua.js b/core/nua.js index cb7e16a7..011ad943 100644 --- a/core/nua.js +++ b/core/nua.js @@ -10,136 +10,136 @@ const Config = require('./config.js').get; const messageArea = require('./message_area.js'); exports.moduleInfo = { - name : 'NUA', - desc : 'New User Application', + name : 'NUA', + desc : 'New User Application', }; const MciViewIds = { - userName : 1, - password : 9, - confirm : 10, - errMsg : 11, + userName : 1, + password : 9, + confirm : 10, + errMsg : 11, }; exports.getModule = class NewUserAppModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - const self = this; + const self = this; - this.menuMethods = { - // - // Validation stuff - // - validatePassConfirmMatch : function(data, cb) { - const passwordView = self.viewControllers.menu.getView(MciViewIds.password); - return cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); - }, + this.menuMethods = { + // + // Validation stuff + // + validatePassConfirmMatch : function(data, cb) { + const passwordView = self.viewControllers.menu.getView(MciViewIds.password); + return cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); + }, - viewValidationListener : function(err, cb) { - const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); - let newFocusId; + viewValidationListener : function(err, cb) { + const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); + let newFocusId; - if(err) { - errMsgView.setText(err.message); - err.view.clearText(); + if(err) { + errMsgView.setText(err.message); + err.view.clearText(); - if(err.view.getId() === MciViewIds.confirm) { - newFocusId = MciViewIds.password; - self.viewControllers.menu.getView(MciViewIds.password).clearText(); - } - } else { - errMsgView.clearText(); - } + if(err.view.getId() === MciViewIds.confirm) { + newFocusId = MciViewIds.password; + self.viewControllers.menu.getView(MciViewIds.password).clearText(); + } + } else { + errMsgView.clearText(); + } - return cb(newFocusId); - }, + return cb(newFocusId); + }, - // - // Submit handlers - // - submitApplication : function(formData, extraArgs, cb) { - const newUser = new User(); - const config = Config(); + // + // Submit handlers + // + submitApplication : function(formData, extraArgs, cb) { + const newUser = new User(); + const config = Config(); - newUser.username = formData.value.username; + newUser.username = formData.value.username; - // - // We have to disable ACS checks for initial default areas as the user is not yet ready - // - let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck - let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck + // + // We have to disable ACS checks for initial default areas as the user is not yet ready + // + let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck + let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck - // can't store undefined! - confTag = confTag || ''; - areaTag = areaTag || ''; + // can't store undefined! + confTag = confTag || ''; + areaTag = areaTag || ''; - newUser.properties = { - real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format - sex : formData.value.sex, - location : formData.value.location, - affiliation : formData.value.affils, - email_address : formData.value.email, - web_address : formData.value.web, - account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format + newUser.properties = { + real_name : formData.value.realName, + birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format + sex : formData.value.sex, + location : formData.value.location, + affiliation : formData.value.affils, + email_address : formData.value.email, + web_address : formData.value.web, + account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format - message_conf_tag : confTag, - message_area_tag : areaTag, + message_conf_tag : confTag, + message_area_tag : areaTag, - term_height : self.client.term.termHeight, - term_width : self.client.term.termWidth, + term_height : self.client.term.termHeight, + term_width : self.client.term.termWidth, - // :TODO: Other defaults - // :TODO: should probably have a place to create defaults/etc. - }; + // :TODO: Other defaults + // :TODO: should probably have a place to create defaults/etc. + }; - if('*' === config.defaults.theme) { - newUser.properties.theme_id = theme.getRandomTheme(); - } else { - newUser.properties.theme_id = config.defaults.theme; - } + if('*' === config.defaults.theme) { + newUser.properties.theme_id = theme.getRandomTheme(); + } else { + newUser.properties.theme_id = config.defaults.theme; + } - // :TODO: User.create() should validate email uniqueness! - newUser.create(formData.value.password, err => { - if(err) { - self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); + // :TODO: User.create() should validate email uniqueness! + newUser.create(formData.value.password, err => { + if(err) { + self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); - self.gotoMenu(extraArgs.error, err => { - if(err) { - return self.prevMenu(cb); - } - return cb(null); - }); - } else { - self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created'); + self.gotoMenu(extraArgs.error, err => { + if(err) { + return self.prevMenu(cb); + } + return cb(null); + }); + } else { + self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created'); - // Cache SysOp information now - // :TODO: Similar to bbs.js. DRY - if(newUser.isSysOp()) { - config.general.sysOp = { - username : formData.value.username, - properties : newUser.properties, - }; - } + // Cache SysOp information now + // :TODO: Similar to bbs.js. DRY + if(newUser.isSysOp()) { + config.general.sysOp = { + username : formData.value.username, + properties : newUser.properties, + }; + } - if(User.AccountStatus.inactive === self.client.user.properties.account_status) { - return self.gotoMenu(extraArgs.inactive, cb); - } else { - // - // If active now, we need to call login() to authenticate - // - return login(self, formData, extraArgs, cb); - } - } - }); - }, - }; - } + if(User.AccountStatus.inactive === self.client.user.properties.account_status) { + return self.gotoMenu(extraArgs.inactive, cb); + } else { + // + // If active now, we need to call login() to authenticate + // + return login(self, formData, extraArgs, cb); + } + } + }); + }, + }; + } - mciReady(mciData, cb) { - return this.standardMCIReadyHandler(mciData, cb); - } + mciReady(mciData, cb) { + return this.standardMCIReadyHandler(mciData, cb); + } }; diff --git a/core/onelinerz.js b/core/onelinerz.js index 65599ba9..d54d7f4f 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -5,8 +5,8 @@ const MenuModule = require('./menu_module.js').MenuModule; const { - getModDatabasePath, - getTransactionDatabase + getModDatabasePath, + getTransactionDatabase } = require('./database.js'); const ViewController = require('./view_controller.js').ViewController; @@ -30,136 +30,136 @@ const moment = require('moment'); exports.moduleInfo = { - name : 'Onelinerz', - desc : 'Standard local onelinerz', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.onelinerz', + name : 'Onelinerz', + desc : 'Standard local onelinerz', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.onelinerz', }; const MciViewIds = { - ViewForm : { - Entries : 1, - AddPrompt : 2, - }, - AddForm : { - NewEntry : 1, - EntryPreview : 2, - AddPrompt : 3, - } + ViewForm : { + Entries : 1, + AddPrompt : 2, + }, + AddForm : { + NewEntry : 1, + EntryPreview : 2, + AddPrompt : 3, + } }; const FormIds = { - View : 0, - Add : 1, + View : 0, + Add : 1, }; exports.getModule = class OnelinerzModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - const self = this; + const self = this; - this.menuMethods = { - viewAddScreen : function(formData, extraArgs, cb) { - return self.displayAddScreen(cb); - }, + this.menuMethods = { + viewAddScreen : function(formData, extraArgs, cb) { + return self.displayAddScreen(cb); + }, - addEntry : function(formData, extraArgs, cb) { - if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) { - const oneliner = formData.value.oneliner.trim(); // remove any trailing ws + addEntry : function(formData, extraArgs, cb) { + if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) { + const oneliner = formData.value.oneliner.trim(); // remove any trailing ws - self.storeNewOneliner(oneliner, err => { - if(err) { - self.client.log.warn( { error : err.message }, 'Failed saving oneliner'); - } + self.storeNewOneliner(oneliner, err => { + if(err) { + self.client.log.warn( { error : err.message }, 'Failed saving oneliner'); + } - self.clearAddForm(); - return self.displayViewScreen(true, cb); // true=cls - }); + self.clearAddForm(); + return self.displayViewScreen(true, cb); // true=cls + }); - } else { - // empty message - treat as if cancel was hit - return self.displayViewScreen(true, cb); // true=cls - } - }, + } else { + // empty message - treat as if cancel was hit + return self.displayViewScreen(true, cb); // true=cls + } + }, - cancelAdd : function(formData, extraArgs, cb) { - self.clearAddForm(); - return self.displayViewScreen(true, cb); // true=cls - } - }; - } + cancelAdd : function(formData, extraArgs, cb) { + self.clearAddForm(); + return self.displayViewScreen(true, cb); // true=cls + } + }; + } - initSequence() { - const self = this; - async.series( - [ - function beforeDisplayArt(callback) { - return self.beforeArt(callback); - }, - function display(callback) { - return self.displayViewScreen(false, callback); - } - ], - err => { - if(err) { - // :TODO: Handle me -- initSequence() should really take a completion callback - } - self.finishedLoading(); - } - ); - } + initSequence() { + const self = this; + async.series( + [ + function beforeDisplayArt(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + return self.displayViewScreen(false, callback); + } + ], + err => { + if(err) { + // :TODO: Handle me -- initSequence() should really take a completion callback + } + self.finishedLoading(); + } + ); + } - displayViewScreen(clearScreen, cb) { - const self = this; + displayViewScreen(clearScreen, cb) { + const self = this; - async.waterfall( - [ - function clearAndDisplayArt(callback) { - if(self.viewControllers.add) { - self.viewControllers.add.setFocus(false); - } + async.waterfall( + [ + function clearAndDisplayArt(callback) { + if(self.viewControllers.add) { + self.viewControllers.add.setFocus(false); + } - if(clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } + if(clearScreen) { + self.client.term.rawWrite(ansi.resetScreen()); + } - theme.displayThemedAsset( - self.menuConfig.config.art.entries, - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'view', - new ViewController( { client : self.client, formId : FormIds.View } ) - ); + theme.displayThemedAsset( + self.menuConfig.config.art.entries, + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'view', + new ViewController( { client : self.client, formId : FormIds.View } ) + ); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); - return callback(null); - } - }, - function fetchEntries(callback) { - const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries); - const limit = entriesView.dimens.height; - let entries = []; + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.view.setFocus(true); + self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); + return callback(null); + } + }, + function fetchEntries(callback) { + const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries); + const limit = entriesView.dimens.height; + let entries = []; - self.db.each( - `SELECT * + self.db.each( + `SELECT * FROM ( SELECT * FROM onelinerz @@ -167,172 +167,172 @@ exports.getModule = class OnelinerzModule extends MenuModule { LIMIT ${limit} ) ORDER BY timestamp ASC;`, - (err, row) => { - if(!err) { - row.timestamp = moment(row.timestamp); // convert -> moment - entries.push(row); - } - }, - err => { - return callback(err, entriesView, entries); - } - ); - }, - function populateEntries(entriesView, entries, callback) { - const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent - const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma'; + (err, row) => { + if(!err) { + row.timestamp = moment(row.timestamp); // convert -> moment + entries.push(row); + } + }, + err => { + return callback(err, entriesView, entries); + } + ); + }, + function populateEntries(entriesView, entries, callback) { + const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent + const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma'; - entriesView.setItems(entries.map( e => { - return stringFormat(listFormat, { - userId : e.user_id, - username : e.user_name, - oneliner : e.oneliner, - ts : e.timestamp.format(tsFormat), - } ); - })); + entriesView.setItems(entries.map( e => { + return stringFormat(listFormat, { + userId : e.user_id, + username : e.user_name, + oneliner : e.oneliner, + ts : e.timestamp.format(tsFormat), + } ); + })); - entriesView.redraw(); + entriesView.redraw(); - return callback(null); - }, - function finalPrep(callback) { - const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt); - promptView.setFocusItemIndex(1); // default to NO - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + return callback(null); + }, + function finalPrep(callback) { + const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt); + promptView.setFocusItemIndex(1); // default to NO + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - displayAddScreen(cb) { - const self = this; + displayAddScreen(cb) { + const self = this; - async.waterfall( - [ - function clearAndDisplayArt(callback) { - self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(ansi.resetScreen()); + async.waterfall( + [ + function clearAndDisplayArt(callback) { + self.viewControllers.view.setFocus(false); + self.client.term.rawWrite(ansi.resetScreen()); - theme.displayThemedAsset( - self.menuConfig.config.art.add, - self.client, - { font : self.menuConfig.font }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'add', - new ViewController( { client : self.client, formId : FormIds.Add } ) - ); + theme.displayThemedAsset( + self.menuConfig.config.art.add, + self.client, + { font : self.menuConfig.font }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'add', + new ViewController( { client : self.client, formId : FormIds.Add } ) + ); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.add.setFocus(true); - self.viewControllers.add.redrawAll(); - self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry); - return callback(null); - } - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.add.setFocus(true); + self.viewControllers.add.redrawAll(); + self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry); + return callback(null); + } + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - clearAddForm() { - this.setViewText('add', MciViewIds.AddForm.NewEntry, ''); - this.setViewText('add', MciViewIds.AddForm.EntryPreview, ''); - } + clearAddForm() { + this.setViewText('add', MciViewIds.AddForm.NewEntry, ''); + this.setViewText('add', MciViewIds.AddForm.EntryPreview, ''); + } - initDatabase(cb) { - const self = this; + initDatabase(cb) { + const self = this; - async.series( - [ - function openDatabase(callback) { - self.db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(exports.moduleInfo), - err => { - return callback(err); - } - )); - }, - function createTables(callback) { - self.db.run( - `CREATE TABLE IF NOT EXISTS onelinerz ( + async.series( + [ + function openDatabase(callback) { + self.db = getTransactionDatabase(new sqlite3.Database( + getModDatabasePath(exports.moduleInfo), + err => { + return callback(err); + } + )); + }, + function createTables(callback) { + self.db.run( + `CREATE TABLE IF NOT EXISTS onelinerz ( id INTEGER PRIMARY KEY, user_id INTEGER_NOT NULL, user_name VARCHAR NOT NULL, oneliner VARCHAR NOT NULL, timestamp DATETIME NOT NULL );` - , - err => { - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); - } + , + err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } - storeNewOneliner(oneliner, cb) { - const self = this; - const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + storeNewOneliner(oneliner, cb) { + const self = this; + const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); - async.series( - [ - function addRec(callback) { - self.db.run( - `INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp) + async.series( + [ + function addRec(callback) { + self.db.run( + `INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp) VALUES (?, ?, ?, ?);`, - [ self.client.user.userId, self.client.user.username, oneliner, ts ], - callback - ); - }, - function removeOld(callback) { - // keep 25 max most recent items - remove the older ones - self.db.run( - `DELETE FROM onelinerz + [ self.client.user.userId, self.client.user.username, oneliner, ts ], + callback + ); + }, + function removeOld(callback) { + // keep 25 max most recent items - remove the older ones + self.db.run( + `DELETE FROM onelinerz WHERE id IN ( SELECT id FROM onelinerz ORDER BY id DESC LIMIT -1 OFFSET 25 );`, - callback - ); - } - ], - err => { - return cb(err); - } - ); - } + callback + ); + } + ], + err => { + return cb(err); + } + ); + } - beforeArt(cb) { - super.beforeArt(err => { - return err ? cb(err) : this.initDatabase(cb); - }); - } + beforeArt(cb) { + super.beforeArt(err => { + return err ? cb(err) : this.initDatabase(cb); + }); + } }; diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index 18546c98..13f9ec91 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -16,83 +16,83 @@ exports.getAreaAndStorage = getAreaAndStorage; exports.looksLikePattern = looksLikePattern; const exitCodes = exports.ExitCodes = { - SUCCESS : 0, - ERROR : -1, - BAD_COMMAND : -2, - BAD_ARGS : -3, + SUCCESS : 0, + ERROR : -1, + BAD_COMMAND : -2, + BAD_ARGS : -3, }; const argv = exports.argv = require('minimist')(process.argv.slice(2), { - alias : { - h : 'help', - v : 'version', - c : 'config', - n : 'no-prompt', - } + alias : { + h : 'help', + v : 'version', + c : 'config', + n : 'no-prompt', + } }); function printUsageAndSetExitCode(errMsg, exitCode) { - if(_.isUndefined(exitCode)) { - exitCode = exitCodes.ERROR; - } + if(_.isUndefined(exitCode)) { + exitCode = exitCodes.ERROR; + } - process.exitCode = exitCode; + process.exitCode = exitCode; - if(errMsg) { - console.error(errMsg); - } + if(errMsg) { + console.error(errMsg); + } } function getDefaultConfigPath() { - return './config/'; + return './config/'; } function getConfigPath() { - const baseConfigPath = argv.config ? argv.config : config.getDefaultPath(); - return baseConfigPath + 'config.hjson'; + const baseConfigPath = argv.config ? argv.config : config.getDefaultPath(); + return baseConfigPath + 'config.hjson'; } function initConfig(cb) { - const configPath = getConfigPath(); + const configPath = getConfigPath(); - config.init(configPath, { keepWsc : true, noWatch : true }, cb); + config.init(configPath, { keepWsc : true, noWatch : true }, cb); } function initConfigAndDatabases(cb) { - async.series( - [ - function init(callback) { - initConfig(callback); - }, - function initDb(callback) { - db.initializeDatabases(callback); - }, - ], - err => { - return cb(err); - } - ); + async.series( + [ + function init(callback) { + initConfig(callback); + }, + function initDb(callback) { + db.initializeDatabases(callback); + }, + ], + err => { + return cb(err); + } + ); } function getAreaAndStorage(tags) { - return tags.map(tag => { - const parts = tag.toString().split('@'); - const entry = { - areaTag : parts[0], - }; - entry.pattern = entry.areaTag; // handy - if(parts[1]) { - entry.storageTag = parts[1]; - } - return entry; - }); + return tags.map(tag => { + const parts = tag.toString().split('@'); + const entry = { + areaTag : parts[0], + }; + entry.pattern = entry.areaTag; // handy + if(parts[1]) { + entry.storageTag = parts[1]; + } + return entry; + }); } function looksLikePattern(tag) { - // globs can start with @ - if(tag.indexOf('@') > 0) { - return false; - } + // globs can start with @ + if(tag.indexOf('@') > 0) { + return false; + } - return /[*?[\]!()+|^]/.test(tag); + return /[*?[\]!()+|^]/.test(tag); } \ No newline at end of file diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 3edfc1f3..d28c977f 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -25,536 +25,536 @@ exports.handleConfigCommand = handleConfigCommand; function getAnswers(questions, cb) { - inq.prompt(questions).then( answers => { - return cb(answers); - }); + inq.prompt(questions).then( answers => { + return cb(answers); + }); } const QUESTIONS = { - Intro : [ - { - name : 'createNewConfig', - message : 'Create a new configuration?', - type : 'confirm', - default : false, - }, - { - name : 'configPath', - message : 'Configuration path:', - default : getConfigPath(), - when : answers => answers.createNewConfig - }, - ], - - OverwriteConfig : [ - { - name : 'overwriteConfig', - message : 'Config file exists. Overwrite?', - type : 'confirm', - default : false, - } - ], - - Basic : [ - { - name : 'boardName', - message : 'BBS name:', - default : 'New ENiGMA½ BBS', - }, - ], - - Misc : [ - { - name : 'loggingLevel', - message : 'Logging level:', - type : 'list', - choices : [ 'Error', 'Warn', 'Info', 'Debug', 'Trace' ], - default : 2, - filter : s => s.toLowerCase(), - }, - { - name : 'sevenZipExe', - message : '7-Zip executable:', - type : 'list', - choices : [ '7z', '7za', 'None' ] - } - ], - - MessageConfAndArea : [ - { - name : 'msgConfName', - message : 'First message conference:', - default : 'Local', - }, - { - name : 'msgConfDesc', - message : 'Conference description:', - default : 'Local Areas', - }, - { - name : 'msgAreaName', - message : 'First area in message conference:', - default : 'General', - }, - { - name : 'msgAreaDesc', - message : 'Area description:', - default : 'General chit-chat', - } - ] + Intro : [ + { + name : 'createNewConfig', + message : 'Create a new configuration?', + type : 'confirm', + default : false, + }, + { + name : 'configPath', + message : 'Configuration path:', + default : getConfigPath(), + when : answers => answers.createNewConfig + }, + ], + + OverwriteConfig : [ + { + name : 'overwriteConfig', + message : 'Config file exists. Overwrite?', + type : 'confirm', + default : false, + } + ], + + Basic : [ + { + name : 'boardName', + message : 'BBS name:', + default : 'New ENiGMA½ BBS', + }, + ], + + Misc : [ + { + name : 'loggingLevel', + message : 'Logging level:', + type : 'list', + choices : [ 'Error', 'Warn', 'Info', 'Debug', 'Trace' ], + default : 2, + filter : s => s.toLowerCase(), + }, + { + name : 'sevenZipExe', + message : '7-Zip executable:', + type : 'list', + choices : [ '7z', '7za', 'None' ] + } + ], + + MessageConfAndArea : [ + { + name : 'msgConfName', + message : 'First message conference:', + default : 'Local', + }, + { + name : 'msgConfDesc', + message : 'Conference description:', + default : 'Local Areas', + }, + { + name : 'msgAreaName', + message : 'First area in message conference:', + default : 'General', + }, + { + name : 'msgAreaDesc', + message : 'Area description:', + default : 'General chit-chat', + } + ] }; function makeMsgConfAreaName(s) { - return s.toLowerCase().replace(/\s+/g, '_'); + return s.toLowerCase().replace(/\s+/g, '_'); } function askNewConfigQuestions(cb) { - - const ui = new inq.ui.BottomBar(); - - let configPath; - let config; - - async.waterfall( - [ - function intro(callback) { - getAnswers(QUESTIONS.Intro, answers => { - if(!answers.createNewConfig) { - return callback('exit'); - } - - // adjust for ~ and the like - configPath = resolvePath(answers.configPath); - - const configDir = paths.dirname(configPath); - mkdirsSync(configDir); - - // - // Check if the file exists and can be written to - // - fs.access(configPath, fs.F_OK | fs.W_OK, err => { - if(err) { - if('EACCES' === err.code) { - ui.log.write(`${configPath} cannot be written to`); - callback('exit'); - } else if('ENOENT' === err.code) { - callback(null, false); - } - } else { - callback(null, true); // exists + writable - } - }); - }); - }, - function promptOverwrite(needPrompt, callback) { - if(needPrompt) { - getAnswers(QUESTIONS.OverwriteConfig, answers => { - callback(answers.overwriteConfig ? null : 'exit'); - }); - } else { - callback(null); - } - }, - function basic(callback) { - getAnswers(QUESTIONS.Basic, answers => { - config = { - general : { - boardName : answers.boardName, - }, - }; - - callback(null); - }); - }, - function msgConfAndArea(callback) { - getAnswers(QUESTIONS.MessageConfAndArea, answers => { - config.messageConferences = {}; - - const confName = makeMsgConfAreaName(answers.msgConfName); - const areaName = makeMsgConfAreaName(answers.msgAreaName); - - config.messageConferences[confName] = { - name : answers.msgConfName, - desc : answers.msgConfDesc, - sort : 1, - default : true, - }; - - config.messageConferences.another_sample_conf = { - name : 'Another Sample Conference', - desc : 'Another conference example. Change me!', - sort : 2, - }; - - config.messageConferences[confName].areas = {}; - config.messageConferences[confName].areas[areaName] = { - name : answers.msgAreaName, - desc : answers.msgAreaDesc, - sort : 1, - default : true, - }; - - config.messageConferences.another_sample_conf = { - name : 'Another Sample Conference', - desc : 'Another conf sample. Change me!', - areas : { - another_sample_area : { - name : 'Another Sample Area', - desc : 'Another area example. Change me!', - sort : 2 - } - } - }; - - callback(null); - }); - }, - function misc(callback) { - getAnswers(QUESTIONS.Misc, answers => { - if('None' !== answers.sevenZipExe) { - config.archivers = { - zip : { - compressCmd : answers.sevenZipExe, - decompressCmd : answers.sevenZipExe, - } - }; - } - - config.logging = { - level : answers.loggingLevel, - }; - - callback(null); - }); - } - ], - err => { - cb(err, configPath, config); - } - ); + const ui = new inq.ui.BottomBar(); + + let configPath; + let config; + + async.waterfall( + [ + function intro(callback) { + getAnswers(QUESTIONS.Intro, answers => { + if(!answers.createNewConfig) { + return callback('exit'); + } + + // adjust for ~ and the like + configPath = resolvePath(answers.configPath); + + const configDir = paths.dirname(configPath); + mkdirsSync(configDir); + + // + // Check if the file exists and can be written to + // + fs.access(configPath, fs.F_OK | fs.W_OK, err => { + if(err) { + if('EACCES' === err.code) { + ui.log.write(`${configPath} cannot be written to`); + callback('exit'); + } else if('ENOENT' === err.code) { + callback(null, false); + } + } else { + callback(null, true); // exists + writable + } + }); + }); + }, + function promptOverwrite(needPrompt, callback) { + if(needPrompt) { + getAnswers(QUESTIONS.OverwriteConfig, answers => { + callback(answers.overwriteConfig ? null : 'exit'); + }); + } else { + callback(null); + } + }, + function basic(callback) { + getAnswers(QUESTIONS.Basic, answers => { + config = { + general : { + boardName : answers.boardName, + }, + }; + + callback(null); + }); + }, + function msgConfAndArea(callback) { + getAnswers(QUESTIONS.MessageConfAndArea, answers => { + config.messageConferences = {}; + + const confName = makeMsgConfAreaName(answers.msgConfName); + const areaName = makeMsgConfAreaName(answers.msgAreaName); + + config.messageConferences[confName] = { + name : answers.msgConfName, + desc : answers.msgConfDesc, + sort : 1, + default : true, + }; + + config.messageConferences.another_sample_conf = { + name : 'Another Sample Conference', + desc : 'Another conference example. Change me!', + sort : 2, + }; + + config.messageConferences[confName].areas = {}; + config.messageConferences[confName].areas[areaName] = { + name : answers.msgAreaName, + desc : answers.msgAreaDesc, + sort : 1, + default : true, + }; + + config.messageConferences.another_sample_conf = { + name : 'Another Sample Conference', + desc : 'Another conf sample. Change me!', + + areas : { + another_sample_area : { + name : 'Another Sample Area', + desc : 'Another area example. Change me!', + sort : 2 + } + } + }; + + callback(null); + }); + }, + function misc(callback) { + getAnswers(QUESTIONS.Misc, answers => { + if('None' !== answers.sevenZipExe) { + config.archivers = { + zip : { + compressCmd : answers.sevenZipExe, + decompressCmd : answers.sevenZipExe, + } + }; + } + + config.logging = { + level : answers.loggingLevel, + }; + + callback(null); + }); + } + ], + err => { + cb(err, configPath, config); + } + ); } function writeConfig(config, path) { - config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t', keepWsc : true, quotes : 'strings' } ); - - try { - fs.writeFileSync(path, config, 'utf8'); - return true; - } catch(e) { - return false; - } + config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t', keepWsc : true, quotes : 'strings' } ); + + try { + fs.writeFileSync(path, config, 'utf8'); + return true; + } catch(e) { + return false; + } } function buildNewConfig() { - askNewConfigQuestions( (err, configPath, config) => { - if(err) { - return; - } + askNewConfigQuestions( (err, configPath, config) => { + if(err) { + return; + } - if(writeConfig(config, configPath)) { - console.info('Configuration generated'); - } else { - console.error('Failed writing configuration'); - } - }); + if(writeConfig(config, configPath)) { + console.info('Configuration generated'); + } else { + console.error('Failed writing configuration'); + } + }); } function validateUplinks(uplinks) { - const ftnAddress = require('../../core/ftn_address.js'); - const valid = uplinks.every(ul => { - const addr = ftnAddress.fromString(ul); - return addr; - }); - return valid; + const ftnAddress = require('../../core/ftn_address.js'); + const valid = uplinks.every(ul => { + const addr = ftnAddress.fromString(ul); + return addr; + }); + return valid; } function getMsgAreaImportType(path) { - if(argv.type) { - return argv.type.toLowerCase(); - } + if(argv.type) { + return argv.type.toLowerCase(); + } - const ext = paths.extname(path).toLowerCase().substr(1); - return ext; // .bbs|.na|... + const ext = paths.extname(path).toLowerCase().substr(1); + return ext; // .bbs|.na|... } function importAreas() { - const importPath = argv._[argv._.length - 1]; - if(argv._.length < 3 || !importPath || 0 === importPath.length) { - return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); - } + const importPath = argv._[argv._.length - 1]; + if(argv._.length < 3 || !importPath || 0 === importPath.length) { + return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); + } - const importType = getMsgAreaImportType(importPath); - if('na' !== importType && 'bbs' !== importType) { - return console.error(`"${importType}" is not a recognized import file type`); - } + const importType = getMsgAreaImportType(importPath); + if('na' !== importType && 'bbs' !== importType) { + return console.error(`"${importType}" is not a recognized import file type`); + } - // optional data - we'll prompt if for anything not found - let confTag = argv.conf; - let networkName = argv.network; - let uplinks = argv.uplinks; - if(uplinks) { - uplinks = uplinks.split(/[\s,]+/); - } + // optional data - we'll prompt if for anything not found + let confTag = argv.conf; + let networkName = argv.network; + let uplinks = argv.uplinks; + if(uplinks) { + uplinks = uplinks.split(/[\s,]+/); + } - let importEntries; + let importEntries; - async.waterfall( - [ - function readImportFile(callback) { - fs.readFile(importPath, 'utf8', (err, importData) => { - if(err) { - return callback(err); - } + async.waterfall( + [ + function readImportFile(callback) { + fs.readFile(importPath, 'utf8', (err, importData) => { + if(err) { + return callback(err); + } - importEntries = getImportEntries(importType, importData); - if(0 === importEntries.length) { - return callback(Errors.Invalid('Invalid or empty import file')); - } + importEntries = getImportEntries(importType, importData); + if(0 === importEntries.length) { + return callback(Errors.Invalid('Invalid or empty import file')); + } - // We should have enough to validate uplinks - if('bbs' === importType) { - for(let i = 0; i < importEntries.length; ++i) { - if(!validateUplinks(importEntries[i].uplinks)) { - return callback(Errors.Invalid('Invalid uplink(s)')); - } - } - } else { - if(!validateUplinks(uplinks)) { - return callback(Errors.Invalid('Invalid uplink(s)')); - } - } + // We should have enough to validate uplinks + if('bbs' === importType) { + for(let i = 0; i < importEntries.length; ++i) { + if(!validateUplinks(importEntries[i].uplinks)) { + return callback(Errors.Invalid('Invalid uplink(s)')); + } + } + } else { + if(!validateUplinks(uplinks)) { + return callback(Errors.Invalid('Invalid uplink(s)')); + } + } - return callback(null); - }); - }, - function init(callback) { - return initConfigAndDatabases(callback); - }, - function validateAndCollectInput(callback) { - const msgArea = require('../../core/message_area.js'); - const sysConfig = require('../../core/config.js').get(); + return callback(null); + }); + }, + function init(callback) { + return initConfigAndDatabases(callback); + }, + function validateAndCollectInput(callback) { + const msgArea = require('../../core/message_area.js'); + const sysConfig = require('../../core/config.js').get(); - let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } ); - if(!msgConfs) { - return callback(Errors.DoesNotExist('No conferences exist in your configuration')); - } + let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } ); + if(!msgConfs) { + return callback(Errors.DoesNotExist('No conferences exist in your configuration')); + } - msgConfs = msgConfs.map(mc => { - return { - name : mc.conf.name, - value : mc.confTag, - }; - }); + msgConfs = msgConfs.map(mc => { + return { + name : mc.conf.name, + value : mc.confTag, + }; + }); - if(confTag && !msgConfs.find(mc => { - return confTag === mc.value; - })) - { - return callback(Errors.DoesNotExist(`Conference "${confTag}" does not exist`)); - } + if(confTag && !msgConfs.find(mc => { + return confTag === mc.value; + })) + { + return callback(Errors.DoesNotExist(`Conference "${confTag}" does not exist`)); + } - let existingNetworkNames = []; - if(_.has(sysConfig, 'messageNetworks.ftn.networks')) { - existingNetworkNames = Object.keys(sysConfig.messageNetworks.ftn.networks); - } + let existingNetworkNames = []; + if(_.has(sysConfig, 'messageNetworks.ftn.networks')) { + existingNetworkNames = Object.keys(sysConfig.messageNetworks.ftn.networks); + } - if(0 === existingNetworkNames.length) { - return callback(Errors.DoesNotExist('No FTN style networks exist in your configuration')); - } + if(0 === existingNetworkNames.length) { + return callback(Errors.DoesNotExist('No FTN style networks exist in your configuration')); + } - if(networkName && !existingNetworkNames.find(net => networkName === net)) { - return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`)); - } + if(networkName && !existingNetworkNames.find(net => networkName === net)) { + return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`)); + } - getAnswers([ - { - name : 'confTag', - message : 'Message conference:', - type : 'list', - choices : msgConfs, - pageSize : 10, - when : !confTag, - }, - { - name : 'networkName', - message : 'Network name:', - type : 'list', - choices : existingNetworkNames, - when : !networkName, - }, - { - name : 'uplinks', - message : 'Uplink(s) (comma separated):', - type : 'input', - validate : (input) => { - const inputUplinks = input.split(/[\s,]+/); - return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)'; - }, - when : !uplinks && 'bbs' !== importType, - } - ], - answers => { - confTag = confTag || answers.confTag; - networkName = networkName || answers.networkName; - uplinks = uplinks || answers.uplinks; + getAnswers([ + { + name : 'confTag', + message : 'Message conference:', + type : 'list', + choices : msgConfs, + pageSize : 10, + when : !confTag, + }, + { + name : 'networkName', + message : 'Network name:', + type : 'list', + choices : existingNetworkNames, + when : !networkName, + }, + { + name : 'uplinks', + message : 'Uplink(s) (comma separated):', + type : 'input', + validate : (input) => { + const inputUplinks = input.split(/[\s,]+/); + return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)'; + }, + when : !uplinks && 'bbs' !== importType, + } + ], + answers => { + confTag = confTag || answers.confTag; + networkName = networkName || answers.networkName; + uplinks = uplinks || answers.uplinks; - importEntries.forEach(ie => { - ie.areaTag = ie.ftnTag.toLowerCase(); - }); + importEntries.forEach(ie => { + ie.areaTag = ie.ftnTag.toLowerCase(); + }); - return callback(null); - }); - }, - function confirmWithUser(callback) { - const sysConfig = require('../../core/config.js').get(); + return callback(null); + }); + }, + function confirmWithUser(callback) { + const sysConfig = require('../../core/config.js').get(); - console.info(`Importing the following for "${confTag}" - (${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`); - importEntries.forEach(ie => { - console.info(` ${ie.ftnTag} - ${ie.name}`); - }); + console.info(`Importing the following for "${confTag}" - (${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`); + importEntries.forEach(ie => { + console.info(` ${ie.ftnTag} - ${ie.name}`); + }); - console.info(''); - console.info('Importing will NOT create required FTN network configurations.'); - console.info('If you have not yet done this, you will need to complete additional steps after importing.'); - console.info('See docs/msg_networks.md for details.'); - console.info(''); + console.info(''); + console.info('Importing will NOT create required FTN network configurations.'); + console.info('If you have not yet done this, you will need to complete additional steps after importing.'); + console.info('See docs/msg_networks.md for details.'); + console.info(''); - getAnswers([ - { - name : 'proceed', - message : 'Proceed?', - type : 'confirm', - } - ], - answers => { - return callback(answers.proceed ? null : Errors.General('User canceled')); - }); + getAnswers([ + { + name : 'proceed', + message : 'Proceed?', + type : 'confirm', + } + ], + answers => { + return callback(answers.proceed ? null : Errors.General('User canceled')); + }); - }, - function loadConfigHjson(callback) { - const configPath = getConfigPath(); - fs.readFile(configPath, 'utf8', (err, confData) => { - if(err) { - return callback(err); - } + }, + function loadConfigHjson(callback) { + const configPath = getConfigPath(); + fs.readFile(configPath, 'utf8', (err, confData) => { + if(err) { + return callback(err); + } - let config; - try { - config = hjson.parse(confData, { keepWsc : true } ); - } catch(e) { - return callback(e); - } - return callback(null, config); + let config; + try { + config = hjson.parse(confData, { keepWsc : true } ); + } catch(e) { + return callback(e); + } + return callback(null, config); - }); - }, - function performImport(config, callback) { - const confAreas = { messageConferences : {} }; - confAreas.messageConferences[confTag] = { areas : {} }; + }); + }, + function performImport(config, callback) { + const confAreas = { messageConferences : {} }; + confAreas.messageConferences[confTag] = { areas : {} }; - const msgNetworks = { messageNetworks : { ftn : { areas : {} } } }; + const msgNetworks = { messageNetworks : { ftn : { areas : {} } } }; - importEntries.forEach(ie => { - const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area + importEntries.forEach(ie => { + const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area - confAreas.messageConferences[confTag].areas[ie.areaTag] = { - name : ie.name, - desc : ie.name, - }; + confAreas.messageConferences[confTag].areas[ie.areaTag] = { + name : ie.name, + desc : ie.name, + }; - msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = { - network : networkName, - tag : ie.ftnTag, - uplinks : specificUplinks - }; - }); - + msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = { + network : networkName, + tag : ie.ftnTag, + uplinks : specificUplinks + }; + }); - const newConfig = _.defaultsDeep(config, confAreas, msgNetworks); - const configPath = getConfigPath(); - if(!writeConfig(newConfig, configPath)) { - return callback(Errors.UnexpectedState('Failed writing configuration')); - } + const newConfig = _.defaultsDeep(config, confAreas, msgNetworks); + const configPath = getConfigPath(); + + if(!writeConfig(newConfig, configPath)) { + return callback(Errors.UnexpectedState('Failed writing configuration')); + } + + return callback(null); + } + ], + err => { + if(err) { + console.error(err.reason ? err.reason : err.message); + } else { + const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"'; + console.info('Configuration generated.'); + console.info(`You may wish to validate changes made to ${getConfigPath()}`); + console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`); + console.info(''); + } + } + ); - return callback(null); - } - ], - err => { - if(err) { - console.error(err.reason ? err.reason : err.message); - } else { - const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"'; - console.info('Configuration generated.'); - console.info(`You may wish to validate changes made to ${getConfigPath()}`); - console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`); - console.info(''); - } - } - ); - } function getImportEntries(importType, importData) { - let importEntries = []; + let importEntries = []; - if('na' === importType) { - // - // parse out - // TAG DESC - // - const re = /^([^\s]+)\s+([^\r\n]+)/gm; - let m; - - while( (m = re.exec(importData) )) { - importEntries.push({ - ftnTag : m[1], - name : m[2], - }); - } - } else if ('bbs' === importType) { - // - // Various formats for AREAS.BBS seem to exist. We want to support as much as possible. - // - // SBBS http://www.synchro.net/docs/sbbsecho.html#AREAS.BBS - // CODE TAG UPLINKS - // - // VADV https://www.vadvbbs.com/products/vadv/support/docs/docs_vfido.php#AREAS.BBS - // TAG UPLINKS - // - // Misc - // PATH|OTHER TAG UPLINKS - // - // Assume the second item is TAG and 1:n UPLINKS (space and/or comma sep) after (at the end) - // - const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm; - let m; - while ( (m = re.exec(importData) )) { - const tag = m[1]; + if('na' === importType) { + // + // parse out + // TAG DESC + // + const re = /^([^\s]+)\s+([^\r\n]+)/gm; + let m; - importEntries.push({ - ftnTag : tag, - name : `Area: ${tag}`, - uplinks : m[2].split(/[\s,]+/), - }); - } - } + while( (m = re.exec(importData) )) { + importEntries.push({ + ftnTag : m[1], + name : m[2], + }); + } + } else if ('bbs' === importType) { + // + // Various formats for AREAS.BBS seem to exist. We want to support as much as possible. + // + // SBBS http://www.synchro.net/docs/sbbsecho.html#AREAS.BBS + // CODE TAG UPLINKS + // + // VADV https://www.vadvbbs.com/products/vadv/support/docs/docs_vfido.php#AREAS.BBS + // TAG UPLINKS + // + // Misc + // PATH|OTHER TAG UPLINKS + // + // Assume the second item is TAG and 1:n UPLINKS (space and/or comma sep) after (at the end) + // + const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm; + let m; + while ( (m = re.exec(importData) )) { + const tag = m[1]; - return importEntries; + importEntries.push({ + ftnTag : tag, + name : `Area: ${tag}`, + uplinks : m[2].split(/[\s,]+/), + }); + } + } + + return importEntries; } function handleConfigCommand() { - if(true === argv.help) { - return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); - } + if(true === argv.help) { + return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); + } - const action = argv._[1]; + const action = argv._[1]; - switch(action) { - case 'new' : return buildNewConfig(); - case 'import-areas' : return importAreas(); + switch(action) { + case 'new' : return buildNewConfig(); + case 'import-areas' : return importAreas(); - default : return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); - } + default : return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); + } } diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index b9ae8aeb..8568372c 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -8,8 +8,8 @@ const argv = require('./oputil_common.js').argv; const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; const getHelpFor = require('./oputil_help.js').getHelpFor; const { - getAreaAndStorage, - looksLikePattern + getAreaAndStorage, + looksLikePattern } = require('./oputil_common.js'); const Errors = require('../enig_error.js').Errors; @@ -39,684 +39,684 @@ exports.handleFileBaseCommand = handleFileBaseCommand; let fileArea; // required during init function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { - async.series( - [ - 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 - } + async.series( + [ + 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! - } + 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(fileEntry.desc && fileEntry.desc.length > 0 ) { - return callback(null); - } + const desc = descHandler.getDescription(fileEntry.fileName); + if(desc) { + fileEntry.desc = desc; + } + return callback(null); + }, + function getDescFromUserIfNeeded(callback) { + if(fileEntry.desc && fileEntry.desc.length > 0 ) { + return callback(null); + } - const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName; - const descFromFile = getDescFromFileName(fileEntry.fileName); + const getDescFromFileName = require('../../core/file_base_area.js').getDescFromFileName; + const descFromFile = getDescFromFileName(fileEntry.fileName); - if(false === argv.prompt) { - fileEntry.desc = descFromFile; - return callback(null); - } + if(false === argv.prompt) { + fileEntry.desc = descFromFile; + return callback(null); + } - const questions = [ - { - name : 'desc', - message : `Description for ${fileEntry.fileName}:`, - type : 'input', - default : descFromFile, - } - ]; + const questions = [ + { + name : 'desc', + message : `Description for ${fileEntry.fileName}:`, + type : 'input', + default : descFromFile, + } + ]; - inq.prompt(questions).then( answers => { - fileEntry.desc = answers.desc; - return callback(null); - }); - }, - function persist(callback) { - fileEntry.persist(isUpdate, err => { - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); + inq.prompt(questions).then( answers => { + fileEntry.desc = answers.desc; + return callback(null); + }); + }, + function persist(callback) { + fileEntry.persist(isUpdate, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); } const SCAN_EXCLUDE_FILENAMES = [ 'DESCRIPT.ION', 'FILES.BBS' ]; function loadDescHandler(path, cb) { - const DescIon = require('../../core/descript_ion_file.js'); + const DescIon = require('../../core/descript_ion_file.js'); - // :TODO: support FILES.BBS also + // :TODO: support FILES.BBS also - DescIon.createFromFile(path, (err, descHandler) => { - return cb(err, descHandler); - }); + DescIon.createFromFile(path, (err, descHandler) => { + return cb(err, descHandler); + }); } function scanFileAreaForChanges(areaInfo, options, cb) { - const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => { - return options.areaAndStorageInfo.find(asi => { - return !asi.storageTag || sl.storageTag === asi.storageTag; - }); - }); + const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => { + return options.areaAndStorageInfo.find(asi => { + return !asi.storageTag || sl.storageTag === asi.storageTag; + }); + }); - function updateTags(fe) { - if(Array.isArray(options.tags)) { - fe.hashTags = new Set(options.tags); - } - } + function updateTags(fe) { + if(Array.isArray(options.tags)) { + fe.hashTags = new Set(options.tags); + } + } - const FileEntry = require('../file_entry.js'); + const FileEntry = require('../file_entry.js'); - const readDir = options.glob ? - (dir, next) => { - return glob(options.glob, { cwd : dir, nodir : true }, next); - } : - (dir, next) => { - return fs.readdir(dir, next); - }; + const readDir = options.glob ? + (dir, next) => { + return glob(options.glob, { cwd : dir, nodir : true }, next); + } : + (dir, next) => { + return fs.readdir(dir, next); + }; - async.eachSeries(storageLocations, (storageLoc, nextLocation) => { - async.waterfall( - [ - function initDescFile(callback) { - if(options.descFileHandler) { - return callback(null, options.descFileHandler); // we're going to use the global handler - } + async.eachSeries(storageLocations, (storageLoc, nextLocation) => { + async.waterfall( + [ + 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; + loadDescHandler(paths.join(storageLoc.dir, 'DESCRIPT.ION'), (err, descHandler) => { + return callback(null, descHandler); + }); + }, + function scanPhysFiles(descHandler, callback) { + const physDir = storageLoc.dir; - readDir(physDir, (err, files) => { - if(err) { - return callback(err); - } + readDir(physDir, (err, files) => { + if(err) { + return callback(err); + } - async.eachSeries(files, (fileName, nextFile) => { - const fullPath = paths.join(physDir, fileName); + async.eachSeries(files, (fileName, nextFile) => { + const fullPath = paths.join(physDir, fileName); - if(SCAN_EXCLUDE_FILENAMES.includes(fileName.toUpperCase())) { - console.info(`Excluding ${fullPath}`); - return nextFile(null); - } + if(SCAN_EXCLUDE_FILENAMES.includes(fileName.toUpperCase())) { + console.info(`Excluding ${fullPath}`); + return nextFile(null); + } - fs.stat(fullPath, (err, stats) => { - if(err) { - // :TODO: Log me! - return nextFile(null); // always try next file - } + fs.stat(fullPath, (err, stats) => { + if(err) { + // :TODO: Log me! + return nextFile(null); // always try next file + } - if(!stats.isFile()) { - return nextFile(null); - } + if(!stats.isFile()) { + return nextFile(null); + } - process.stdout.write(`Scanning ${fullPath}... `); + process.stdout.write(`Scanning ${fullPath}... `); - async.series( - [ - function quickCheck(next) { - if(!options.quick) { - return next(null); - } + async.series( + [ + function quickCheck(next) { + if(!options.quick) { + return next(null); + } - FileEntry.quickCheckExistsByPath(fullPath, (err, exists) => { - if(exists) { - console.info('Dupe'); - return nextFile(null); - } + FileEntry.quickCheckExistsByPath(fullPath, (err, exists) => { + if(exists) { + console.info('Dupe'); + return nextFile(null); + } - return next(null); - }); - }, - function fullScan() { - fileArea.scanFile( - fullPath, - { - areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag - }, - (err, fileEntry, dupeEntries) => { - if(err) { - console.info(`Error: ${err.message}`); - return nextFile(null); // try next anyway - } + return next(null); + }); + }, + function fullScan() { + fileArea.scanFile( + fullPath, + { + areaTag : areaInfo.areaTag, + storageTag : storageLoc.storageTag + }, + (err, fileEntry, dupeEntries) => { + if(err) { + console.info(`Error: ${err.message}`); + return nextFile(null); // try next anyway + } - // - // We'll update the entry if the following conditions are met: - // * We have a single duplicate, and: - // * --update was passed or the existing entry's desc, - // longDesc, or est_release_year meta are blank/empty - // - if(argv.update && 1 === dupeEntries.length) { - const FileEntry = require('../../core/file_entry.js'); - const existingEntry = new FileEntry(); + // + // We'll update the entry if the following conditions are met: + // * We have a single duplicate, and: + // * --update was passed or the existing entry's desc, + // longDesc, or est_release_year meta are blank/empty + // + if(argv.update && 1 === dupeEntries.length) { + const FileEntry = require('../../core/file_entry.js'); + const existingEntry = new FileEntry(); - return existingEntry.load(dupeEntries[0].fileId, err => { - if(err) { - console.info('Dupe (cannot update)'); - return nextFile(null); - } + return existingEntry.load(dupeEntries[0].fileId, err => { + if(err) { + console.info('Dupe (cannot update)'); + return nextFile(null); + } - // - // Update only if tags or desc changed - // - const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; - const tagsEq = _.isEqual(optTags, existingEntry.hashTags); + // + // Update only if tags or desc changed + // + const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; + const tagsEq = _.isEqual(optTags, existingEntry.hashTags); - if( tagsEq && + if( tagsEq && fileEntry.desc === existingEntry.desc && fileEntry.descLong == existingEntry.descLong && fileEntry.meta.est_release_year == existingEntry.meta.est_release_year) - { - console.info('Dupe'); - return nextFile(null); - } + { + console.info('Dupe'); + return nextFile(null); + } - console.info('Dupe (updating)'); + console.info('Dupe (updating)'); - // don't allow overwrite of values if new version is blank - existingEntry.desc = fileEntry.desc || existingEntry.desc; - existingEntry.descLong = fileEntry.descLong || existingEntry.descLong; + // don't allow overwrite of values if new version is blank + existingEntry.desc = fileEntry.desc || existingEntry.desc; + existingEntry.descLong = fileEntry.descLong || existingEntry.descLong; - if(fileEntry.meta.est_release_year) { - existingEntry.meta.est_release_year = fileEntry.meta.est_release_year; - } + if(fileEntry.meta.est_release_year) { + existingEntry.meta.est_release_year = fileEntry.meta.est_release_year; + } - updateTags(existingEntry); + updateTags(existingEntry); - finalizeEntryAndPersist(true, existingEntry, descHandler, err => { - return nextFile(err); - }); - }); - } else if(dupeEntries.length > 0) { - console.info('Dupe'); - return nextFile(null); - } + finalizeEntryAndPersist(true, existingEntry, descHandler, err => { + return nextFile(err); + }); + }); + } else if(dupeEntries.length > 0) { + console.info('Dupe'); + return nextFile(null); + } - console.info('Done!'); - updateTags(fileEntry); + console.info('Done!'); + updateTags(fileEntry); - finalizeEntryAndPersist(false, fileEntry, descHandler, err => { - return nextFile(err); - }); - } - ); - } - ] - ); - }); - }, err => { - return callback(err); - }); - }); - }, - function scanDbEntries(callback) { - // :TODO: Look @ db entries for area that were *not* processed above - return callback(null); - } - ], - err => { - return nextLocation(err); - } - ); - }, - err => { - return cb(err); - }); + finalizeEntryAndPersist(false, fileEntry, descHandler, err => { + return nextFile(err); + }); + } + ); + } + ] + ); + }); + }, err => { + return callback(err); + }); + }); + }, + function scanDbEntries(callback) { + // :TODO: Look @ db entries for area that were *not* processed above + return callback(null); + } + ], + err => { + return nextLocation(err); + } + ); + }, + err => { + return cb(err); + }); } function dumpAreaInfo(areaInfo, areaAndStorageInfo, cb) { - console.info(`areaTag: ${areaInfo.areaTag}`); - console.info(`name: ${areaInfo.name}`); - console.info(`desc: ${areaInfo.desc}`); + console.info(`areaTag: ${areaInfo.areaTag}`); + console.info(`name: ${areaInfo.name}`); + console.info(`desc: ${areaInfo.desc}`); - areaInfo.storage.forEach(si => { - console.info(`storageTag: ${si.storageTag} => ${si.dir}`); - }); - console.info(''); + areaInfo.storage.forEach(si => { + console.info(`storageTag: ${si.storageTag} => ${si.dir}`); + }); + console.info(''); - return cb(null); + return cb(null); } function getFileEntries(pattern, cb) { - // spec: FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA - const FileEntry = require('../../core/file_entry.js'); + // spec: FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA + const FileEntry = require('../../core/file_entry.js'); - async.waterfall( - [ - function tryByFileId(callback) { - const fileId = parseInt(pattern); - if(!/^[0-9]+$/.test(pattern) || isNaN(fileId)) { - return callback(null, null); // try SHA - } + async.waterfall( + [ + function tryByFileId(callback) { + const fileId = parseInt(pattern); + if(!/^[0-9]+$/.test(pattern) || isNaN(fileId)) { + return callback(null, null); // try SHA + } - const fileEntry = new FileEntry(); - fileEntry.load(fileId, err => { - return callback(null, err ? null : [ fileEntry ] ); - }); - }, - function tryByShaOrPartialSha(entries, callback) { - if(entries) { - return callback(null, entries); // already got it by FILE_ID - } + const fileEntry = new FileEntry(); + fileEntry.load(fileId, err => { + return callback(null, err ? null : [ fileEntry ] ); + }); + }, + function tryByShaOrPartialSha(entries, callback) { + if(entries) { + return callback(null, entries); // already got it by FILE_ID + } - FileEntry.findFileBySha(pattern, (err, fileEntry) => { - return callback(null, fileEntry ? [ fileEntry ] : null ); - }); - }, - function tryByFileNameWildcard(entries, callback) { - if(entries) { - return callback(null, entries); // already got by FILE_ID|SHA - } + FileEntry.findFileBySha(pattern, (err, fileEntry) => { + return callback(null, fileEntry ? [ fileEntry ] : null ); + }); + }, + function tryByFileNameWildcard(entries, callback) { + if(entries) { + return callback(null, entries); // already got by FILE_ID|SHA + } - return FileEntry.findByFileNameWildcard(pattern, callback); - } - ], - (err, entries) => { - return cb(err, entries); - } - ); + return FileEntry.findByFileNameWildcard(pattern, callback); + } + ], + (err, entries) => { + return cb(err, entries); + } + ); } function dumpFileInfo(shaOrFileId, cb) { - async.waterfall( - [ - function getEntry(callback) { - getFileEntries(shaOrFileId, (err, entries) => { - if(err) { - return callback(err); - } + async.waterfall( + [ + function getEntry(callback) { + getFileEntries(shaOrFileId, (err, entries) => { + if(err) { + return callback(err); + } - return callback(null, entries[0]); - }); - }, - function dumpInfo(fileEntry, callback) { - const fullPath = paths.join(fileArea.getAreaStorageDirectoryByTag(fileEntry.storageTag), fileEntry.fileName); + return callback(null, entries[0]); + }); + }, + function dumpInfo(fileEntry, callback) { + const fullPath = paths.join(fileArea.getAreaStorageDirectoryByTag(fileEntry.storageTag), fileEntry.fileName); - console.info(`file_id: ${fileEntry.fileId}`); - console.info(`sha_256: ${fileEntry.fileSha256}`); - console.info(`area_tag: ${fileEntry.areaTag}`); - console.info(`storage_tag: ${fileEntry.storageTag}`); - console.info(`path: ${fullPath}`); - console.info(`hashTags: ${Array.from(fileEntry.hashTags).join(', ')}`); - console.info(`uploaded: ${moment(fileEntry.uploadTimestamp).format()}`); + console.info(`file_id: ${fileEntry.fileId}`); + console.info(`sha_256: ${fileEntry.fileSha256}`); + console.info(`area_tag: ${fileEntry.areaTag}`); + console.info(`storage_tag: ${fileEntry.storageTag}`); + console.info(`path: ${fullPath}`); + console.info(`hashTags: ${Array.from(fileEntry.hashTags).join(', ')}`); + console.info(`uploaded: ${moment(fileEntry.uploadTimestamp).format()}`); - _.each(fileEntry.meta, (metaValue, metaName) => { - console.info(`${metaName}: ${metaValue}`); - }); + _.each(fileEntry.meta, (metaValue, metaName) => { + console.info(`${metaName}: ${metaValue}`); + }); - if(argv['show-desc']) { - console.info(`${fileEntry.desc}`); - } - console.info(''); + if(argv['show-desc']) { + console.info(`${fileEntry.desc}`); + } + console.info(''); - return callback(null); - } - ], - err => { - return cb(err); - } - ); + return callback(null); + } + ], + err => { + return cb(err); + } + ); } function displayFileAreaInfo() { - // AREA_TAG[@STORAGE_TAG] - // SHA256|PARTIAL - // if sha: dump file info - // if area/stoarge dump area(s) + + // AREA_TAG[@STORAGE_TAG] + // SHA256|PARTIAL + // if sha: dump file info + // if area/stoarge dump area(s) + - async.series( - [ - function init(callback) { - return initConfigAndDatabases(callback); - }, - function dumpInfo(callback) { - const sysConfig = require('../../core/config.js').get(); - let suppliedAreas = argv._.slice(2); - if(!suppliedAreas || 0 === suppliedAreas.length) { - suppliedAreas = _.map(sysConfig.fileBase.areas, (areaInfo, areaTag) => areaTag); - } + async.series( + [ + function init(callback) { + return initConfigAndDatabases(callback); + }, + function dumpInfo(callback) { + const sysConfig = require('../../core/config.js').get(); + let suppliedAreas = argv._.slice(2); + if(!suppliedAreas || 0 === suppliedAreas.length) { + suppliedAreas = _.map(sysConfig.fileBase.areas, (areaInfo, areaTag) => areaTag); + } - const areaAndStorageInfo = getAreaAndStorage(suppliedAreas); + const areaAndStorageInfo = getAreaAndStorage(suppliedAreas); - fileArea = require('../../core/file_base_area.js'); + fileArea = require('../../core/file_base_area.js'); - async.eachSeries(areaAndStorageInfo, (areaAndStorage, nextArea) => { - const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); - if(areaInfo) { - return dumpAreaInfo(areaInfo, areaAndStorageInfo, nextArea); - } else { - return dumpFileInfo(areaAndStorage.areaTag, nextArea); - } - }, - err => { - return callback(err); - }); - } - ], - err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(err.message); - } - } - ); + async.eachSeries(areaAndStorageInfo, (areaAndStorage, nextArea) => { + const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); + if(areaInfo) { + return dumpAreaInfo(areaInfo, areaAndStorageInfo, nextArea); + } else { + return dumpFileInfo(areaAndStorage.areaTag, nextArea); + } + }, + err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); } function scanFileAreas() { - const options = {}; + const options = {}; - const tags = argv.tags; - if(tags) { - options.tags = tags.split(','); - } + const tags = argv.tags; + if(tags) { + options.tags = tags.split(','); + } - options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH - options.quick = argv.quick; + options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH + options.quick = argv.quick; - options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); + options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); - const last = argv._[argv._.length - 1]; - if(options.areaAndStorageInfo.length > 1 && looksLikePattern(last)) { - options.glob = last; - options.areaAndStorageInfo.length -= 1; - } + const last = argv._[argv._.length - 1]; + if(options.areaAndStorageInfo.length > 1 && looksLikePattern(last)) { + options.glob = last; + options.areaAndStorageInfo.length -= 1; + } - async.series( - [ - function init(callback) { - return initConfigAndDatabases(callback); - }, - function initMime(callback) { - return require('../../core/mime_util.js').startup(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); - } + async.series( + [ + function init(callback) { + return initConfigAndDatabases(callback); + }, + function initMime(callback) { + return require('../../core/mime_util.js').startup(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) { - fileArea = require('../../core/file_base_area.js'); + loadDescHandler(options.descFile, (err, descHandler) => { + options.descFileHandler = descHandler; + return callback(null); + }); + }, + function scanAreas(callback) { + fileArea = require('../../core/file_base_area.js'); - async.eachSeries(options.areaAndStorageInfo, (areaAndStorage, nextAreaTag) => { - const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); - if(!areaInfo) { - return nextAreaTag(new Error(`Invalid file base area tag: ${areaAndStorage.areaTag}`)); - } + async.eachSeries(options.areaAndStorageInfo, (areaAndStorage, nextAreaTag) => { + const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); + if(!areaInfo) { + return nextAreaTag(new Error(`Invalid file base area tag: ${areaAndStorage.areaTag}`)); + } - console.info(`Processing area "${areaInfo.name}":`); + console.info(`Processing area "${areaInfo.name}":`); - scanFileAreaForChanges(areaInfo, options, err => { - return callback(err); - }); - }, err => { - return callback(err); - }); - } - ], - err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(err.message); - } - } - ); + scanFileAreaForChanges(areaInfo, options, err => { + return callback(err); + }); + }, err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); } function expandFileTargets(targets, cb) { - let entries = []; + let entries = []; - // Each entry may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] - const FileEntry = require('../../core/file_entry.js'); + // Each entry may be PATH|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] + const FileEntry = require('../../core/file_entry.js'); - async.eachSeries(targets, (areaAndStorage, next) => { - const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); + async.eachSeries(targets, (areaAndStorage, next) => { + const areaInfo = fileArea.getFileAreaByTag(areaAndStorage.areaTag); - if(areaInfo) { - // AREA_TAG[@STORAGE_TAG] - all files in area@tag - const findFilter = { - areaTag : areaAndStorage.areaTag, - }; + if(areaInfo) { + // AREA_TAG[@STORAGE_TAG] - all files in area@tag + const findFilter = { + areaTag : areaAndStorage.areaTag, + }; - if(areaAndStorage.storageTag) { - findFilter.storageTag = areaAndStorage.storageTag; - } + if(areaAndStorage.storageTag) { + findFilter.storageTag = areaAndStorage.storageTag; + } - FileEntry.findFiles(findFilter, (err, fileIds) => { - if(err) { - return next(err); - } + FileEntry.findFiles(findFilter, (err, fileIds) => { + if(err) { + return next(err); + } - async.each(fileIds, (fileId, nextFileId) => { - const fileEntry = new FileEntry(); - fileEntry.load(fileId, err => { - if(!err) { - entries.push(fileEntry); - } - return nextFileId(err); - }); - }, - err => { - return next(err); - }); - }); + async.each(fileIds, (fileId, nextFileId) => { + const fileEntry = new FileEntry(); + fileEntry.load(fileId, err => { + if(!err) { + entries.push(fileEntry); + } + return nextFileId(err); + }); + }, + err => { + return next(err); + }); + }); - } else { - // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA - // :TODO: FULL_PATH -> entries - getFileEntries(areaAndStorage.pattern, (err, fileEntries) => { - if(err) { - return next(err); - } + } else { + // FILENAME_WC|FILE_ID|SHA|PARTIAL_SHA + // :TODO: FULL_PATH -> entries + getFileEntries(areaAndStorage.pattern, (err, fileEntries) => { + if(err) { + return next(err); + } - entries = entries.concat(fileEntries); - return next(null); - }); - } - }, - err => { - return cb(err, entries); - }); + entries = entries.concat(fileEntries); + return next(null); + }); + } + }, + err => { + return cb(err, entries); + }); } function moveFiles() { - // - // oputil fb move SRC [SRC2 ...] DST - // - // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] - // DST: AREA_TAG[@STORAGE_TAG] - // - if(argv._.length < 4) { - return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); - } + // + // oputil fb move SRC [SRC2 ...] DST + // + // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] + // DST: AREA_TAG[@STORAGE_TAG] + // + if(argv._.length < 4) { + return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); + } - const moveArgs = argv._.slice(2); - const src = getAreaAndStorage(moveArgs.slice(0, -1)); - const dst = getAreaAndStorage(moveArgs.slice(-1))[0]; + const moveArgs = argv._.slice(2); + const src = getAreaAndStorage(moveArgs.slice(0, -1)); + const dst = getAreaAndStorage(moveArgs.slice(-1))[0]; - let FileEntry; + let FileEntry; - async.waterfall( - [ - function init(callback) { - return initConfigAndDatabases( err => { - if(!err) { - fileArea = require('../../core/file_base_area.js'); - } - return callback(err); - }); - }, - function validateAndExpandSourceAndDest(callback) { - const areaInfo = fileArea.getFileAreaByTag(dst.areaTag); - if(areaInfo) { - dst.areaInfo = areaInfo; - } else { - return callback(Errors.DoesNotExist('Invalid or unknown destination area')); - } + async.waterfall( + [ + function init(callback) { + return initConfigAndDatabases( err => { + if(!err) { + fileArea = require('../../core/file_base_area.js'); + } + return callback(err); + }); + }, + function validateAndExpandSourceAndDest(callback) { + const areaInfo = fileArea.getFileAreaByTag(dst.areaTag); + if(areaInfo) { + dst.areaInfo = areaInfo; + } else { + return callback(Errors.DoesNotExist('Invalid or unknown destination area')); + } - FileEntry = require('../../core/file_entry.js'); + FileEntry = require('../../core/file_entry.js'); - expandFileTargets(src, (err, srcEntries) => { - return callback(err, srcEntries); - }); - }, - function moveEntries(srcEntries, callback) { + expandFileTargets(src, (err, srcEntries) => { + return callback(err, srcEntries); + }); + }, + function moveEntries(srcEntries, callback) { - if(!dst.storageTag) { - dst.storageTag = dst.areaInfo.storageTags[0]; - } + if(!dst.storageTag) { + dst.storageTag = dst.areaInfo.storageTags[0]; + } - const destDir = FileEntry.getAreaStorageDirectoryByTag(dst.storageTag); + const destDir = FileEntry.getAreaStorageDirectoryByTag(dst.storageTag); - async.eachSeries(srcEntries, (entry, nextEntry) => { - const srcPath = entry.filePath; - const dstPath = paths.join(destDir, entry.fileName); + async.eachSeries(srcEntries, (entry, nextEntry) => { + const srcPath = entry.filePath; + const dstPath = paths.join(destDir, entry.fileName); - process.stdout.write(`Moving ${srcPath} => ${dstPath}... `); + process.stdout.write(`Moving ${srcPath} => ${dstPath}... `); - FileEntry.moveEntry(entry, dst.areaTag, dst.storageTag, err => { - if(err) { - console.info(`Failed: ${err.message}`); - } else { - console.info('Done'); - } - return nextEntry(null); // always try next - }); - }, - err => { - return callback(err); - }); - } - ], - err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(err.message); - } - } - ); + FileEntry.moveEntry(entry, dst.areaTag, dst.storageTag, err => { + if(err) { + console.info(`Failed: ${err.message}`); + } else { + console.info('Done'); + } + return nextEntry(null); // always try next + }); + }, + err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); } function removeFiles() { - // - // oputil fb rm|remove|del|delete SRC [SRC2 ...] - // - // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] - // - // AREA_TAG[@STORAGE_TAG] remove all entries matching - // supplied area/storage tags - // - // --phys-file removes backing physical file(s) - // - if(argv._.length < 3) { - return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); - } + // + // oputil fb rm|remove|del|delete SRC [SRC2 ...] + // + // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] + // + // AREA_TAG[@STORAGE_TAG] remove all entries matching + // supplied area/storage tags + // + // --phys-file removes backing physical file(s) + // + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); + } - const removePhysFile = argv['phys-file']; + const removePhysFile = argv['phys-file']; - const src = getAreaAndStorage(argv._.slice(2)); + const src = getAreaAndStorage(argv._.slice(2)); - async.waterfall( - [ - function init(callback) { - return initConfigAndDatabases( err => { - if(!err) { - fileArea = require('../../core/file_base_area.js'); - } - return callback(err); - }); - }, - function expandSources(callback) { - expandFileTargets(src, (err, srcEntries) => { - return callback(err, srcEntries); - }); - }, - function removeEntries(srcEntries, callback) { - const FileEntry = require('../../core/file_entry.js'); + async.waterfall( + [ + function init(callback) { + return initConfigAndDatabases( err => { + if(!err) { + fileArea = require('../../core/file_base_area.js'); + } + return callback(err); + }); + }, + function expandSources(callback) { + expandFileTargets(src, (err, srcEntries) => { + return callback(err, srcEntries); + }); + }, + function removeEntries(srcEntries, callback) { + const FileEntry = require('../../core/file_entry.js'); - const extraOutput = removePhysFile ? ' (including physical file)' : ''; + const extraOutput = removePhysFile ? ' (including physical file)' : ''; - async.eachSeries(srcEntries, (entry, nextEntry) => { + async.eachSeries(srcEntries, (entry, nextEntry) => { - process.stdout.write(`Removing ${entry.filePath}${extraOutput}... `); + process.stdout.write(`Removing ${entry.filePath}${extraOutput}... `); - FileEntry.removeEntry(entry, { removePhysFile }, err => { - if(err) { - console.info(`Failed: ${err.message}`); - } else { - console.info('Done'); - } + FileEntry.removeEntry(entry, { removePhysFile }, err => { + if(err) { + console.info(`Failed: ${err.message}`); + } else { + console.info('Done'); + } - return nextEntry(err); - }); - }, err => { - return callback(err); - }); - } - ], - err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(err.message); - } - } - ); + return nextEntry(err); + }); + }, err => { + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } + } + ); } function handleFileBaseCommand() { - function errUsage() { - return printUsageAndSetExitCode( - getHelpFor('FileBase') + getHelpFor('FileOpsInfo'), - ExitCodes.ERROR - ); - } + function errUsage() { + return printUsageAndSetExitCode( + getHelpFor('FileBase') + getHelpFor('FileOpsInfo'), + ExitCodes.ERROR + ); + } - if(true === argv.help) { - return errUsage(); - } + if(true === argv.help) { + return errUsage(); + } - const action = argv._[1]; + const action = argv._[1]; - return ({ - info : displayFileAreaInfo, - scan : scanFileAreas, + return ({ + info : displayFileAreaInfo, + scan : scanFileAreas, - mv : moveFiles, - move : moveFiles, + mv : moveFiles, + move : moveFiles, - rm : removeFiles, - remove : removeFiles, - del : removeFiles, - delete : removeFiles, - }[action] || errUsage)(); + rm : removeFiles, + remove : removeFiles, + del : removeFiles, + delete : removeFiles, + }[action] || errUsage)(); } \ No newline at end of file diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index a560b17e..6f86cdf0 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -7,7 +7,7 @@ const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPat exports.getHelpFor = getHelpFor; const usageHelp = exports.USAGE_HELP = { - General : + General : `usage: optutil.js [--version] [--help] [] @@ -21,7 +21,7 @@ commands: fb file base management mb message base management `, - User : + User : `usage: optutil.js user [] actions: @@ -33,7 +33,7 @@ actions: group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP `, - Config : + Config : `usage: optutil.js config [] actions: @@ -46,7 +46,7 @@ import-areas args: --uplinks UL1,UL2,... specify one or more comma separated uplinks --type TYPE specifies area import type. valid options are "bbs" and "na" `, - FileBase : + FileBase : `usage: oputil.js fb [] actions: @@ -80,7 +80,7 @@ info args: remove args: --phys-file also remove underlying physical file `, - FileOpsInfo : + FileOpsInfo : ` general information: AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag @@ -90,7 +90,7 @@ general information: SHA full or partial SHA-256 FILE_ID a file identifier. see file.sqlite3 `, - MessageBase : + MessageBase : `usage: oputil.js mb [] actions: @@ -101,5 +101,5 @@ general information: }; function getHelpFor(command) { - return usageHelp[command]; + return usageHelp[command]; } diff --git a/core/oputil/oputil_main.js b/core/oputil/oputil_main.js index aa373ef2..d9492c0d 100644 --- a/core/oputil/oputil_main.js +++ b/core/oputil/oputil_main.js @@ -14,23 +14,23 @@ const getHelpFor = require('./oputil_help.js').getHelpFor; module.exports = function() { - process.exitCode = ExitCodes.SUCCESS; + process.exitCode = ExitCodes.SUCCESS; - if(true === argv.version) { - return console.info(require('../package.json').version); - } + if(true === argv.version) { + return console.info(require('../package.json').version); + } - if(0 === argv._.length || + if(0 === argv._.length || 'help' === argv._[0]) - { - return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS); - } + { + return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.SUCCESS); + } - switch(argv._[0]) { - case 'user' : return handleUserCommand(); - case 'config' : return handleConfigCommand(); - case 'fb' : return handleFileBaseCommand(); - case 'mb' : return handleMessageBaseCommand(); - default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND); - } + switch(argv._[0]) { + case 'user' : return handleUserCommand(); + case 'config' : return handleConfigCommand(); + case 'fb' : return handleFileBaseCommand(); + case 'mb' : return handleMessageBaseCommand(); + default : return printUsageAndSetExitCode(getHelpFor('General'), ExitCodes.BAD_COMMAND); + } }; diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index 6e89cf73..60a99a2a 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -16,127 +16,127 @@ const async = require('async'); exports.handleMessageBaseCommand = handleMessageBaseCommand; function areaFix() { - // - // oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS] - // - if(argv._.length < 3) { - return printUsageAndSetExitCode( - getHelpFor('MessageBase'), - ExitCodes.ERROR - ); - } + // + // oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS] + // + if(argv._.length < 3) { + return printUsageAndSetExitCode( + getHelpFor('MessageBase'), + ExitCodes.ERROR + ); + } - async.waterfall( - [ - function init(callback) { - return initConfigAndDatabases(callback); - }, - function validateAddress(callback) { - const addrArg = argv._.slice(-1)[0]; - const ftnAddr = Address.fromString(addrArg); + async.waterfall( + [ + function init(callback) { + return initConfigAndDatabases(callback); + }, + function validateAddress(callback) { + const addrArg = argv._.slice(-1)[0]; + const ftnAddr = Address.fromString(addrArg); - if(!ftnAddr) { - return callback(Errors.Invalid(`"${addrArg}" is not a valid FTN address`)); - } + if(!ftnAddr) { + return callback(Errors.Invalid(`"${addrArg}" is not a valid FTN address`)); + } - // - // We need to validate the address targets a system we know unless - // the --force option is used - // - // :TODO: - return callback(null, ftnAddr); - }, - function fetchFromUser(ftnAddr, callback) { - // - // --from USER || +op from system - // - // If possible, we want the user ID of the supplied user as well - // - const User = require('../user.js'); + // + // We need to validate the address targets a system we know unless + // the --force option is used + // + // :TODO: + return callback(null, ftnAddr); + }, + function fetchFromUser(ftnAddr, callback) { + // + // --from USER || +op from system + // + // If possible, we want the user ID of the supplied user as well + // + const User = require('../user.js'); - if(argv.from) { - User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => { - if(err) { - return callback(null, ftnAddr, argv.from, 0); - } + if(argv.from) { + User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => { + if(err) { + return callback(null, ftnAddr, argv.from, 0); + } - // fromName is the same as argv.from, but case may be differnet (yet correct) - return callback(null, ftnAddr, fromName, userId); - }); - } else { - User.getUserName(User.RootUserID, (err, fromName) => { - return callback(null, ftnAddr, fromName || 'SysOp', err ? 0 : User.RootUserID); - }); - } - }, - function createMessage(ftnAddr, fromName, fromUserId, callback) { - // - // Build message as commands separated by line feed - // - // We need to remove quotes from arguments. These are required - // in the case of e.g. removing an area: "-SOME_AREA" would end - // up confusing minimist, therefor they must be quoted: "'-SOME_AREA'" - // - const messageBody = argv._.slice(2, -1).map(arg => { - return arg.replace(/["']/g, ''); - }).join('\r\n') + '\n'; + // fromName is the same as argv.from, but case may be differnet (yet correct) + return callback(null, ftnAddr, fromName, userId); + }); + } else { + User.getUserName(User.RootUserID, (err, fromName) => { + return callback(null, ftnAddr, fromName || 'SysOp', err ? 0 : User.RootUserID); + }); + } + }, + function createMessage(ftnAddr, fromName, fromUserId, callback) { + // + // Build message as commands separated by line feed + // + // We need to remove quotes from arguments. These are required + // in the case of e.g. removing an area: "-SOME_AREA" would end + // up confusing minimist, therefor they must be quoted: "'-SOME_AREA'" + // + const messageBody = argv._.slice(2, -1).map(arg => { + return arg.replace(/["']/g, ''); + }).join('\r\n') + '\n'; - const Message = require('../message.js'); + const Message = require('../message.js'); - const message = new Message({ - toUserName : argv.to || 'AreaFix', - fromUserName : fromName, - subject : argv.password || '', - message : messageBody, - areaTag : Message.WellKnownAreaTags.Private, // mark private - meta : { - System : { - [ Message.SystemMetaNames.RemoteToUser ] : ftnAddr.toString(), // where to send it - [ Message.SystemMetaNames.ExternalFlavor ] : Message.AddressFlavor.FTN, // on FTN-style network - } - } - }); + const message = new Message({ + toUserName : argv.to || 'AreaFix', + fromUserName : fromName, + subject : argv.password || '', + message : messageBody, + areaTag : Message.WellKnownAreaTags.Private, // mark private + meta : { + System : { + [ Message.SystemMetaNames.RemoteToUser ] : ftnAddr.toString(), // where to send it + [ Message.SystemMetaNames.ExternalFlavor ] : Message.AddressFlavor.FTN, // on FTN-style network + } + } + }); - if(0 !== fromUserId) { - message.setLocalFromUserId(fromUserId); - } + if(0 !== fromUserId) { + message.setLocalFromUserId(fromUserId); + } - return callback(null, message); - }, - function persistMessage(message, callback) { - message.persist(err => { - if(!err) { - console.log('AreaFix message persisted and will be exported at next scheduled scan'); - } - return callback(err); - }); - } - ], - err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`); - } - } - ); + return callback(null, message); + }, + function persistMessage(message, callback) { + message.persist(err => { + if(!err) { + console.log('AreaFix message persisted and will be exported at next scheduled scan'); + } + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`); + } + } + ); } function handleMessageBaseCommand() { - function errUsage() { - return printUsageAndSetExitCode( - getHelpFor('MessageBase'), - ExitCodes.ERROR - ); - } + function errUsage() { + return printUsageAndSetExitCode( + getHelpFor('MessageBase'), + ExitCodes.ERROR + ); + } - if(true === argv.help) { - return errUsage(); - } + if(true === argv.help) { + return errUsage(); + } - const action = argv._[1]; + const action = argv._[1]; - return({ - areafix : areaFix, - }[action] || errUsage)(); + return({ + areafix : areaFix, + }[action] || errUsage)(); } \ No newline at end of file diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 59813b3b..60d3888d 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -15,191 +15,191 @@ const _ = require('lodash'); exports.handleUserCommand = handleUserCommand; function getUser(userName, cb) { - const User = require('../../core/user.js'); - User.getUserIdAndName(userName, (err, userId) => { - if(err) { - process.exitCode = ExitCodes.BAD_ARGS; - return cb(err); - } - const u = new User(); - u.userId = userId; - return cb(null, u); - }); + const User = require('../../core/user.js'); + User.getUserIdAndName(userName, (err, userId) => { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + return cb(err); + } + const u = new User(); + u.userId = userId; + return cb(null, u); + }); } function initAndGetUser(userName, cb) { - async.waterfall( - [ - function init(callback) { - initConfigAndDatabases(callback); - }, - function getUserObject(callback) { - getUser(userName, (err, user) => { - if(err) { - process.exitCode = ExitCodes.BAD_ARGS; - return callback(err); - } - return callback(null, user); - }); - } - ], - (err, user) => { - return cb(err, user); - } - ); + async.waterfall( + [ + function init(callback) { + initConfigAndDatabases(callback); + }, + function getUserObject(callback) { + getUser(userName, (err, user) => { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + return callback(err); + } + return callback(null, user); + }); + } + ], + (err, user) => { + return cb(err, user); + } + ); } function setAccountStatus(user, status) { - if(argv._.length < 3) { - return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); - } + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } - const AccountStatus = require('../../core/user.js').AccountStatus; - const statusDesc = _.invert(AccountStatus)[status]; - user.persistProperty('account_status', status, err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(err.message); - } else { - console.info(`User status set to ${statusDesc}`); - } - }); + const AccountStatus = require('../../core/user.js').AccountStatus; + const statusDesc = _.invert(AccountStatus)[status]; + user.persistProperty('account_status', status, err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } else { + console.info(`User status set to ${statusDesc}`); + } + }); } function setUserPassword(user) { - if(argv._.length < 4) { - return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); - } + if(argv._.length < 4) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } - async.waterfall( - [ - function validate(callback) { - // :TODO: prompt if no password provided (more secure, no history, etc.) - const password = argv._[argv._.length - 1]; - if(0 === password.length) { - return callback(Errors.Invalid('Invalid password')); - } - return callback(null, password); - }, - function set(password, callback) { - user.setNewAuthCredentials(password, err => { - if(err) { - process.exitCode = ExitCodes.BAD_ARGS; - } - return callback(err); - }); - } - ], - err => { - if(err) { - console.error(err.message); - } else { - console.info('New password set'); - } - } - ); + async.waterfall( + [ + function validate(callback) { + // :TODO: prompt if no password provided (more secure, no history, etc.) + const password = argv._[argv._.length - 1]; + if(0 === password.length) { + return callback(Errors.Invalid('Invalid password')); + } + return callback(null, password); + }, + function set(password, callback) { + user.setNewAuthCredentials(password, err => { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + } + return callback(err); + }); + } + ], + err => { + if(err) { + console.error(err.message); + } else { + console.info('New password set'); + } + } + ); } -function removeUser(user) { - console.error('NOT YET IMPLEMENTED'); +function removeUser() { + console.error('NOT YET IMPLEMENTED'); } function modUserGroups(user) { - if(argv._.length < 3) { - return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); - } + if(argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } - let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" - let action = groupName[0]; // + or - + let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" + let action = groupName[0]; // + or - - if('-' === action || '+' === action) { - groupName = groupName.substr(1); - } + if('-' === action || '+' === action) { + groupName = groupName.substr(1); + } - action = action || '+'; + action = action || '+'; - if(0 === groupName.length) { - return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); - } + if(0 === groupName.length) { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } - // - // Groups are currently arbritary, so do a slight validation - // - if(!/[A-Za-z0-9]+/.test(groupName)) { - process.exitCode = ExitCodes.BAD_ARGS; - return console.error('Bad group name'); - } + // + // Groups are currently arbritary, so do a slight validation + // + if(!/[A-Za-z0-9]+/.test(groupName)) { + process.exitCode = ExitCodes.BAD_ARGS; + return console.error('Bad group name'); + } - function done(err) { - if(err) { - process.exitCode = ExitCodes.BAD_ARGS; - console.error(err.message); - } else { - console.info('User groups modified'); - } - } + function done(err) { + if(err) { + process.exitCode = ExitCodes.BAD_ARGS; + console.error(err.message); + } else { + console.info('User groups modified'); + } + } - const UserGroup = require('../../core/user_group.js'); - if('-' === action) { - UserGroup.removeUserFromGroup(user.userId, groupName, done); - } else { - UserGroup.addUserToGroup(user.userId, groupName, done); - } + const UserGroup = require('../../core/user_group.js'); + if('-' === action) { + UserGroup.removeUserFromGroup(user.userId, groupName, done); + } else { + UserGroup.addUserToGroup(user.userId, groupName, done); + } } function activateUser(user) { - const AccountStatus = require('../../core/user.js').AccountStatus; - return setAccountStatus(user, AccountStatus.active); + const AccountStatus = require('../../core/user.js').AccountStatus; + return setAccountStatus(user, AccountStatus.active); } function deactivateUser(user) { - const AccountStatus = require('../../core/user.js').AccountStatus; - return setAccountStatus(user, AccountStatus.inactive); + const AccountStatus = require('../../core/user.js').AccountStatus; + return setAccountStatus(user, AccountStatus.inactive); } function disableUser(user) { - const AccountStatus = require('../../core/user.js').AccountStatus; - return setAccountStatus(user, AccountStatus.disabled); + const AccountStatus = require('../../core/user.js').AccountStatus; + return setAccountStatus(user, AccountStatus.disabled); } function handleUserCommand() { - function errUsage() { - return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); - } + function errUsage() { + return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); + } - if(true === argv.help) { - return errUsage(); - } + if(true === argv.help) { + return errUsage(); + } - const action = argv._[1]; - const usernameIdx = [ 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1; - const userName = argv._[usernameIdx]; + const action = argv._[1]; + const usernameIdx = [ 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1; + const userName = argv._[usernameIdx]; - if(!userName) { - return errUsage(); - } + if(!userName) { + return errUsage(); + } - initAndGetUser(userName, (err, user) => { - if(err) { - process.exitCode = ExitCodes.ERROR; - return console.error(err.message); - } + initAndGetUser(userName, (err, user) => { + if(err) { + process.exitCode = ExitCodes.ERROR; + return console.error(err.message); + } - return ({ - pass : setUserPassword, - passwd : setUserPassword, - password : setUserPassword, + return ({ + pass : setUserPassword, + passwd : setUserPassword, + password : setUserPassword, - rm : removeUser, - remove : removeUser, - del : removeUser, - delete : removeUser, + rm : removeUser, + remove : removeUser, + del : removeUser, + delete : removeUser, - activate : activateUser, - deactivate : deactivateUser, - disable : disableUser, + activate : activateUser, + deactivate : deactivateUser, + disable : disableUser, - group : modUserGroups, - }[action] || errUsage)(user); - }); + group : modUserGroups, + }[action] || errUsage)(user); + }); } \ No newline at end of file diff --git a/core/predefined_mci.js b/core/predefined_mci.js index c1f7e9fb..584feffa 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -21,230 +21,230 @@ exports.getPredefinedMCIValue = getPredefinedMCIValue; exports.init = init; function init(cb) { - setNextRandomRumor(cb); + setNextRandomRumor(cb); } function setNextRandomRumor(cb) { - StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => { - if(entry) { - entry = entry[0]; - } - const randRumor = entry && entry.log_value ? entry.log_value : ''; - StatLog.setNonPeristentSystemStat('random_rumor', randRumor); - if(cb) { - return cb(null); - } - }); + StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => { + if(entry) { + entry = entry[0]; + } + const randRumor = entry && entry.log_value ? entry.log_value : ''; + StatLog.setNonPeristentSystemStat('random_rumor', randRumor); + if(cb) { + return cb(null); + } + }); } function getUserRatio(client, propA, propB) { - const a = StatLog.getUserStatNum(client.user, propA); - const b = StatLog.getUserStatNum(client.user, propB); - const ratio = ~~((a / b) * 100); - return `${ratio}%`; + const a = StatLog.getUserStatNum(client.user, propA); + const b = StatLog.getUserStatNum(client.user, propB); + const ratio = ~~((a / b) * 100); + return `${ratio}%`; } function userStatAsString(client, statName, defaultValue) { - return (StatLog.getUserStat(client.user, statName) || defaultValue).toLocaleString(); + return (StatLog.getUserStat(client.user, statName) || defaultValue).toLocaleString(); } function sysStatAsString(statName, defaultValue) { - return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString(); + return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString(); } const PREDEFINED_MCI_GENERATORS = { - // - // Board - // - BN : function boardName() { return Config().general.boardName; }, + // + // Board + // + BN : function boardName() { return Config().general.boardName; }, - // ENiGMA - VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, - VN : function version() { return packageJson.version; }, + // ENiGMA + VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, + VN : function version() { return packageJson.version; }, - // +op info - SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, - SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, - SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, - SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, - SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, - SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, - // :TODO: op age, web, ????? + // +op info + SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, + SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, + SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, + SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, + SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, + SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, + // :TODO: op age, web, ????? - // - // Current user / session - // - UN : function userName(client) { return client.user.username; }, - UI : function userId(client) { return client.user.userId.toString(); }, - UG : function groups(client) { return _.values(client.user.groups).join(', '); }, - UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, - LO : function location(client) { return userStatAsString(client, 'location', ''); }, - UA : function age(client) { return client.user.getAge().toString(); }, - BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY - US : function sex(client) { return userStatAsString(client, 'sex', ''); }, - UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, - UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, - UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, - UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, - UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, - ND : function connectedNode(client) { return client.node.toString(); }, - IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version - ST : function serverName(client) { return client.session.serverName; }, - FN : function activeFileBaseFilterName(client) { - const activeFilter = FileBaseFilters.getActiveFilter(client); - return activeFilter ? activeFilter.name : ''; - }, - DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 - DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes - const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr - }, - UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 - UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes - const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr - }, - NR : function userUpDownRatio(client) { // Obv/2 - return getUserRatio(client, 'ul_total_count', 'dl_total_count'); - }, - KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio - return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes'); - }, + // + // Current user / session + // + UN : function userName(client) { return client.user.username; }, + UI : function userId(client) { return client.user.userId.toString(); }, + UG : function groups(client) { return _.values(client.user.groups).join(', '); }, + UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, + LO : function location(client) { return userStatAsString(client, 'location', ''); }, + UA : function age(client) { return client.user.getAge().toString(); }, + BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY + US : function sex(client) { return userStatAsString(client, 'sex', ''); }, + UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, + UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, + UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, + UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, + UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, + ND : function connectedNode(client) { return client.node.toString(); }, + IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version + ST : function serverName(client) { return client.session.serverName; }, + FN : function activeFileBaseFilterName(client) { + const activeFilter = FileBaseFilters.getActiveFilter(client); + return activeFilter ? activeFilter.name : ''; + }, + DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 + DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes + const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 + UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes + const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + NR : function userUpDownRatio(client) { // Obv/2 + return getUserRatio(client, 'ul_total_count', 'dl_total_count'); + }, + KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio + return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes'); + }, - MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, - PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, - PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, + MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, + PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, + PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, - MD : function currentMenuDescription(client) { - return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; - }, + MD : function currentMenuDescription(client) { + return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; + }, - MA : function messageAreaName(client) { - const area = getMessageAreaByTag(client.user.properties.message_area_tag); - return area ? area.name : ''; - }, - MC : function messageConfName(client) { - const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); - return conf ? conf.name : ''; - }, - ML : function messageAreaDescription(client) { - const area = getMessageAreaByTag(client.user.properties.message_area_tag); - return area ? area.desc : ''; - }, - CM : function messageConfDescription(client) { - const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); - return conf ? conf.desc : ''; - }, + MA : function messageAreaName(client) { + const area = getMessageAreaByTag(client.user.properties.message_area_tag); + return area ? area.name : ''; + }, + MC : function messageConfName(client) { + const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); + return conf ? conf.name : ''; + }, + ML : function messageAreaDescription(client) { + const area = getMessageAreaByTag(client.user.properties.message_area_tag); + return area ? area.desc : ''; + }, + CM : function messageConfDescription(client) { + const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); + return conf ? conf.desc : ''; + }, - SH : function termHeight(client) { return client.term.termHeight.toString(); }, - SW : function termWidth(client) { return client.term.termWidth.toString(); }, + SH : function termHeight(client) { return client.term.termHeight.toString(); }, + SW : function termWidth(client) { return client.term.termWidth.toString(); }, - // - // Date/Time - // - // :TODO: change to CD for 'Current Date' - DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, - CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, + // + // Date/Time + // + // :TODO: change to CD for 'Current Date' + DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, + CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, - // - // OS/System Info - // - OS : function operatingSystem() { - return { - linux : 'Linux', - darwin : 'Mac OS X', - win32 : 'Windows', - sunos : 'SunOS', - freebsd : 'FreeBSD', - }[os.platform()] || os.type(); - }, + // + // OS/System Info + // + OS : function operatingSystem() { + return { + linux : 'Linux', + darwin : 'Mac OS X', + win32 : 'Windows', + sunos : 'SunOS', + freebsd : 'FreeBSD', + }[os.platform()] || os.type(); + }, - OA : function systemArchitecture() { return os.arch(); }, + OA : function systemArchitecture() { return os.arch(); }, - SC : function systemCpuModel() { - // - // Clean up CPU strings a bit for better display - // - return os.cpus()[0].model - .replace(/\(R\)|\(TM\)|processor|CPU/g, '') - .replace(/\s+(?= )/g, ''); - }, + SC : function systemCpuModel() { + // + // Clean up CPU strings a bit for better display + // + return os.cpus()[0].model + .replace(/\(R\)|\(TM\)|processor|CPU/g, '') + .replace(/\s+(?= )/g, ''); + }, - // :TODO: MCI for core count, e.g. os.cpus().length + // :TODO: MCI for core count, e.g. os.cpus().length - // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage - NV : function nodeVersion() { return process.version; }, + // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage + NV : function nodeVersion() { return process.version; }, - AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, + AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, - TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); }, + TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); }, - RR : function randomRumor() { - // start the process of picking another random one - setNextRandomRumor(); + RR : function randomRumor() { + // start the process of picking another random one + setNextRandomRumor(); - return StatLog.getSystemStat('random_rumor'); - }, + return StatLog.getSystemStat('random_rumor'); + }, - // - // System File Base, Up/Download Info - // - // :TODO: DD - Today's # of downloads (iNiQUiTY) - // - SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, - SO : function systemByteDownload() { - const byteSize = StatLog.getSystemStatNum('dl_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr - }, - SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); }, - SP : function systemByteUpload() { - const byteSize = StatLog.getSystemStatNum('ul_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr - }, - TF : function totalFilesOnSystem() { - const areaStats = StatLog.getSystemStat('file_base_area_stats'); - return _.get(areaStats, 'totalFiles', 0).toLocaleString(); - }, - TB : function totalBytesOnSystem() { - const areaStats = StatLog.getSystemStat('file_base_area_stats'); - const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); - return formatByteSize(totalBytes, true); // true=withAbbr - }, + // + // System File Base, Up/Download Info + // + // :TODO: DD - Today's # of downloads (iNiQUiTY) + // + SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, + SO : function systemByteDownload() { + const byteSize = StatLog.getSystemStatNum('dl_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); }, + SP : function systemByteUpload() { + const byteSize = StatLog.getSystemStatNum('ul_total_bytes'); + return formatByteSize(byteSize, true); // true=withAbbr + }, + TF : function totalFilesOnSystem() { + const areaStats = StatLog.getSystemStat('file_base_area_stats'); + return _.get(areaStats, 'totalFiles', 0).toLocaleString(); + }, + TB : function totalBytesOnSystem() { + const areaStats = StatLog.getSystemStat('file_base_area_stats'); + const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); + return formatByteSize(totalBytes, true); // true=withAbbr + }, - // :TODO: PT - Messages posted *today* (Obv/2) - // -> Include FTN/etc. - // :TODO: NT - New users today (Obv/2) - // :TODO: CT - Calls *today* (Obv/2) - // :TODO: FT - Files uploaded/added *today* (Obv/2) - // :TODO: DD - Files downloaded *today* (iNiQUiTY) - // :TODO: TP - total message/posts on the system (Obv/2) - // -> Include FTN/etc. - // :TODO: LC - name of last caller to system (Obv/2) - // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) + // :TODO: PT - Messages posted *today* (Obv/2) + // -> Include FTN/etc. + // :TODO: NT - New users today (Obv/2) + // :TODO: CT - Calls *today* (Obv/2) + // :TODO: FT - Files uploaded/added *today* (Obv/2) + // :TODO: DD - Files downloaded *today* (iNiQUiTY) + // :TODO: TP - total message/posts on the system (Obv/2) + // -> Include FTN/etc. + // :TODO: LC - name of last caller to system (Obv/2) + // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) - // - // Special handling for XY - // - XY : function xyHack() { return; /* nothing */ }, + // + // Special handling for XY + // + XY : function xyHack() { return; /* nothing */ }, }; function getPredefinedMCIValue(client, code) { - if(!client || !code) { - return; - } + if(!client || !code) { + return; + } - const generator = PREDEFINED_MCI_GENERATORS[code]; + const generator = PREDEFINED_MCI_GENERATORS[code]; - if(generator) { - let value; - try { - value = generator(client); - } catch(e) { - Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' ); - } + if(generator) { + let value; + try { + value = generator(client); + } catch(e) { + Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' ); + } - return value; - } + return value; + } } diff --git a/core/rumorz.js b/core/rumorz.js index da1ab5f6..d51e33e8 100644 --- a/core/rumorz.js +++ b/core/rumorz.js @@ -15,233 +15,233 @@ const async = require('async'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Rumorz', - desc : 'Standard local rumorz', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.rumorz', + name : 'Rumorz', + desc : 'Standard local rumorz', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.rumorz', }; const STATLOG_KEY_RUMORZ = 'system_rumorz'; const FormIds = { - View : 0, - Add : 1, + View : 0, + Add : 1, }; const MciCodeIds = { - ViewForm : { - Entries : 1, - AddPrompt : 2, - }, - AddForm : { - NewEntry : 1, - EntryPreview : 2, - AddPrompt : 3, - } + ViewForm : { + Entries : 1, + AddPrompt : 2, + }, + AddForm : { + NewEntry : 1, + EntryPreview : 2, + AddPrompt : 3, + } }; exports.getModule = class RumorzModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.menuMethods = { - viewAddScreen : (formData, extraArgs, cb) => { - return this.displayAddScreen(cb); - }, + this.menuMethods = { + viewAddScreen : (formData, extraArgs, cb) => { + return this.displayAddScreen(cb); + }, - addEntry : (formData, extraArgs, cb) => { - if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { - const rumor = formData.value.rumor.trim(); // remove any trailing ws + addEntry : (formData, extraArgs, cb) => { + if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { + const rumor = formData.value.rumor.trim(); // remove any trailing ws - StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { - this.clearAddForm(); - return this.displayViewScreen(true, cb); // true=cls - }); - } else { - // empty message - treat as if cancel was hit - return this.displayViewScreen(true, cb); // true=cls - } - }, + StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { + this.clearAddForm(); + return this.displayViewScreen(true, cb); // true=cls + }); + } else { + // empty message - treat as if cancel was hit + return this.displayViewScreen(true, cb); // true=cls + } + }, - cancelAdd : (formData, extraArgs, cb) => { - this.clearAddForm(); - return this.displayViewScreen(true, cb); // true=cls - } - }; - } + cancelAdd : (formData, extraArgs, cb) => { + this.clearAddForm(); + return this.displayViewScreen(true, cb); // true=cls + } + }; + } - get config() { return this.menuConfig.config; } + get config() { return this.menuConfig.config; } - clearAddForm() { - const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); - const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); + clearAddForm() { + const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); + const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); - newEntryView.setText(''); + newEntryView.setText(''); - // preview is optional - if(previewView) { - previewView.setText(''); - } - } + // preview is optional + if(previewView) { + previewView.setText(''); + } + } - initSequence() { - const self = this; + initSequence() { + const self = this; - async.series( - [ - function beforeDisplayArt(callback) { - self.beforeArt(callback); - }, - function display(callback) { - self.displayViewScreen(false, callback); - } - ], - err => { - if(err) { - // :TODO: Handle me -- initSequence() should really take a completion callback - } - self.finishedLoading(); - } - ); - } + async.series( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function display(callback) { + self.displayViewScreen(false, callback); + } + ], + err => { + if(err) { + // :TODO: Handle me -- initSequence() should really take a completion callback + } + self.finishedLoading(); + } + ); + } - displayViewScreen(clearScreen, cb) { - const self = this; - async.waterfall( - [ - function clearAndDisplayArt(callback) { - if(self.viewControllers.add) { - self.viewControllers.add.setFocus(false); - } + displayViewScreen(clearScreen, cb) { + const self = this; + async.waterfall( + [ + function clearAndDisplayArt(callback) { + if(self.viewControllers.add) { + self.viewControllers.add.setFocus(false); + } - if(clearScreen) { - self.client.term.rawWrite(resetScreen()); - } + if(clearScreen) { + self.client.term.rawWrite(resetScreen()); + } - theme.displayThemedAsset( - self.config.art.entries, - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'view', - new ViewController( { client : self.client, formId : FormIds.View } ) - ); + theme.displayThemedAsset( + self.config.art.entries, + self.client, + { font : self.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'view', + new ViewController( { client : self.client, formId : FormIds.View } ) + ); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); - return callback(null); - } - }, - function fetchEntries(callback) { - const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries); + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.view.setFocus(true); + self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt).redraw(); + return callback(null); + } + }, + function fetchEntries(callback) { + const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries); - StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => { - return callback(err, entriesView, entries); - }); - }, - function populateEntries(entriesView, entries, callback) { - const config = self.config; - const listFormat = config.listFormat || '{rumor}'; - const focusListFormat = config.focusListFormat || listFormat; + StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => { + return callback(err, entriesView, entries); + }); + }, + function populateEntries(entriesView, entries, callback) { + const config = self.config; + const listFormat = config.listFormat || '{rumor}'; + const focusListFormat = config.focusListFormat || listFormat; - entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) ); - entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) ); - entriesView.redraw(); + entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) ); + entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) ); + entriesView.redraw(); - return callback(null); - }, - function finalPrep(callback) { - const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt); - promptView.setFocusItemIndex(1); // default to NO - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + return callback(null); + }, + function finalPrep(callback) { + const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt); + promptView.setFocusItemIndex(1); // default to NO + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - displayAddScreen(cb) { - const self = this; + displayAddScreen(cb) { + const self = this; - async.waterfall( - [ - function clearAndDisplayArt(callback) { - self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(resetScreen()); + async.waterfall( + [ + function clearAndDisplayArt(callback) { + self.viewControllers.view.setFocus(false); + self.client.term.rawWrite(resetScreen()); - theme.displayThemedAsset( - self.config.art.add, - self.client, - { font : self.menuConfig.font }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'add', - new ViewController( { client : self.client, formId : FormIds.Add } ) - ); + theme.displayThemedAsset( + self.config.art.add, + self.client, + { font : self.menuConfig.font }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + function initOrRedrawViewController(artData, callback) { + if(_.isUndefined(self.viewControllers.add)) { + const vc = self.addViewController( + 'add', + new ViewController( { client : self.client, formId : FormIds.Add } ) + ); - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, - }; + const loadOpts = { + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, + }; - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.add.setFocus(true); - self.viewControllers.add.redrawAll(); - self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry); - return callback(null); - } - }, - function initPreviewUpdates(callback) { - const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); - const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); - if(previewView) { - let timerId; - entryView.on('key press', () => { - clearTimeout(timerId); - timerId = setTimeout( () => { - const focused = self.viewControllers.add.getFocusedView(); - if(focused === entryView) { - previewView.setText(entryView.getData()); - focused.setFocus(true); - } - }, 500); - }); - } - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + return vc.loadFromMenuConfig(loadOpts, callback); + } else { + self.viewControllers.add.setFocus(true); + self.viewControllers.add.redrawAll(); + self.viewControllers.add.switchFocus(MciCodeIds.AddForm.NewEntry); + return callback(null); + } + }, + function initPreviewUpdates(callback) { + const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); + const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); + if(previewView) { + let timerId; + entryView.on('key press', () => { + clearTimeout(timerId); + timerId = setTimeout( () => { + const focused = self.viewControllers.add.getFocusedView(); + if(focused === entryView) { + previewView.setText(entryView.getData()); + focused.setFocus(true); + } + }, 500); + }); + } + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } }; diff --git a/core/sauce.js b/core/sauce.js index e0182ae7..29176d8d 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -27,100 +27,100 @@ exports.SAUCE_SIZE = SAUCE_SIZE; const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; function readSAUCE(data, cb) { - if(data.length < SAUCE_SIZE) { - return cb(Errors.DoesNotExist('No SAUCE record present')); - } + if(data.length < SAUCE_SIZE) { + return cb(Errors.DoesNotExist('No SAUCE record present')); + } - let sauceRec; - try { - sauceRec = new Parser() - .buffer('id', { length : 5 } ) - .buffer('version', { length : 2 } ) - .buffer('title', { length: 35 } ) - .buffer('author', { length : 20 } ) - .buffer('group', { length: 20 } ) - .buffer('date', { length: 8 } ) - .uint32le('fileSize') - .int8('dataType') - .int8('fileType') - .uint16le('tinfo1') - .uint16le('tinfo2') - .uint16le('tinfo3') - .uint16le('tinfo4') - .int8('numComments') - .int8('flags') - // :TODO: does this need to be optional? - .buffer('tinfos', { length: 22 } ) // SAUCE 00.5 - .parse(data.slice(data.length - SAUCE_SIZE)); - } catch(e) { - return cb(Errors.Invalid('Invalid SAUCE record')); - } + let sauceRec; + try { + sauceRec = new Parser() + .buffer('id', { length : 5 } ) + .buffer('version', { length : 2 } ) + .buffer('title', { length: 35 } ) + .buffer('author', { length : 20 } ) + .buffer('group', { length: 20 } ) + .buffer('date', { length: 8 } ) + .uint32le('fileSize') + .int8('dataType') + .int8('fileType') + .uint16le('tinfo1') + .uint16le('tinfo2') + .uint16le('tinfo3') + .uint16le('tinfo4') + .int8('numComments') + .int8('flags') + // :TODO: does this need to be optional? + .buffer('tinfos', { length: 22 } ) // SAUCE 00.5 + .parse(data.slice(data.length - SAUCE_SIZE)); + } catch(e) { + return cb(Errors.Invalid('Invalid SAUCE record')); + } - if(!SAUCE_ID.equals(sauceRec.id)) { - return cb(Errors.DoesNotExist('No SAUCE record present')); - } + if(!SAUCE_ID.equals(sauceRec.id)) { + return cb(Errors.DoesNotExist('No SAUCE record present')); + } - const ver = iconv.decode(sauceRec.version, 'cp437'); + const ver = iconv.decode(sauceRec.version, 'cp437'); - if('00' !== ver) { - return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`)); - } + if('00' !== ver) { + return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`)); + } - if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) { - return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`)); - } + if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(sauceRec.dataType)) { + return cb(Errors.Invalid(`Unsupported SAUCE DataType: ${sauceRec.dataType}`)); + } - const sauce = { - id : iconv.decode(sauceRec.id, 'cp437'), - version : iconv.decode(sauceRec.version, 'cp437').trim(), - title : iconv.decode(sauceRec.title, 'cp437').trim(), - author : iconv.decode(sauceRec.author, 'cp437').trim(), - group : iconv.decode(sauceRec.group, 'cp437').trim(), - date : iconv.decode(sauceRec.date, 'cp437').trim(), - fileSize : sauceRec.fileSize, - dataType : sauceRec.dataType, - fileType : sauceRec.fileType, - tinfo1 : sauceRec.tinfo1, - tinfo2 : sauceRec.tinfo2, - tinfo3 : sauceRec.tinfo3, - tinfo4 : sauceRec.tinfo4, - numComments : sauceRec.numComments, - flags : sauceRec.flags, - tinfos : sauceRec.tinfos, - }; + const sauce = { + id : iconv.decode(sauceRec.id, 'cp437'), + version : iconv.decode(sauceRec.version, 'cp437').trim(), + title : iconv.decode(sauceRec.title, 'cp437').trim(), + author : iconv.decode(sauceRec.author, 'cp437').trim(), + group : iconv.decode(sauceRec.group, 'cp437').trim(), + date : iconv.decode(sauceRec.date, 'cp437').trim(), + fileSize : sauceRec.fileSize, + dataType : sauceRec.dataType, + fileType : sauceRec.fileType, + tinfo1 : sauceRec.tinfo1, + tinfo2 : sauceRec.tinfo2, + tinfo3 : sauceRec.tinfo3, + tinfo4 : sauceRec.tinfo4, + numComments : sauceRec.numComments, + flags : sauceRec.flags, + tinfos : sauceRec.tinfos, + }; - const dt = SAUCE_DATA_TYPES[sauce.dataType]; - if(dt && dt.parser) { - sauce[dt.name] = dt.parser(sauce); - } + const dt = SAUCE_DATA_TYPES[sauce.dataType]; + if(dt && dt.parser) { + sauce[dt.name] = dt.parser(sauce); + } - return cb(null, sauce); + return cb(null, sauce); } // :TODO: These need completed: const SAUCE_DATA_TYPES = { - 0 : { name : 'None' }, - 1 : { name : 'Character', parser : parseCharacterSAUCE }, - 2 : 'Bitmap', - 3 : 'Vector', - 4 : 'Audio', - 5 : 'BinaryText', - 6 : 'XBin', - 7 : 'Archive', - 8 : 'Executable', + 0 : { name : 'None' }, + 1 : { name : 'Character', parser : parseCharacterSAUCE }, + 2 : 'Bitmap', + 3 : 'Vector', + 4 : 'Audio', + 5 : 'BinaryText', + 6 : 'XBin', + 7 : 'Archive', + 8 : 'Executable', }; const SAUCE_CHARACTER_FILE_TYPES = { - 0 : 'ASCII', - 1 : 'ANSi', - 2 : 'ANSiMation', - 3 : 'RIP script', - 4 : 'PCBoard', - 5 : 'Avatar', - 6 : 'HTML', - 7 : 'Source', - 8 : 'TundraDraw', + 0 : 'ASCII', + 1 : 'ANSi', + 2 : 'ANSiMation', + 3 : 'RIP script', + 4 : 'PCBoard', + 5 : 'Avatar', + 6 : 'HTML', + 7 : 'Source', + 8 : 'TundraDraw', }; // @@ -129,53 +129,53 @@ const SAUCE_CHARACTER_FILE_TYPES = { // Note that this is the same mapping that x84 uses. Be compatible! // const SAUCE_FONT_TO_ENCODING_HINT = { - 'Amiga MicroKnight' : 'amiga', - 'Amiga MicroKnight+' : 'amiga', - 'Amiga mOsOul' : 'amiga', - 'Amiga P0T-NOoDLE' : 'amiga', - 'Amiga Topaz 1' : 'amiga', - 'Amiga Topaz 1+' : 'amiga', - 'Amiga Topaz 2' : 'amiga', - 'Amiga Topaz 2+' : 'amiga', - 'Atari ATASCII' : 'atari', - 'IBM EGA43' : 'cp437', - 'IBM EGA' : 'cp437', - 'IBM VGA25G' : 'cp437', - 'IBM VGA50' : 'cp437', - 'IBM VGA' : 'cp437', + 'Amiga MicroKnight' : 'amiga', + 'Amiga MicroKnight+' : 'amiga', + 'Amiga mOsOul' : 'amiga', + 'Amiga P0T-NOoDLE' : 'amiga', + 'Amiga Topaz 1' : 'amiga', + 'Amiga Topaz 1+' : 'amiga', + 'Amiga Topaz 2' : 'amiga', + 'Amiga Topaz 2+' : 'amiga', + 'Atari ATASCII' : 'atari', + 'IBM EGA43' : 'cp437', + 'IBM EGA' : 'cp437', + 'IBM VGA25G' : 'cp437', + 'IBM VGA50' : 'cp437', + 'IBM VGA' : 'cp437', }; [ - '437', '720', '737', '775', '819', '850', '852', '855', '857', '858', - '860', '861', '862', '863', '864', '865', '866', '869', '872' + '437', '720', '737', '775', '819', '850', '852', '855', '857', '858', + '860', '861', '862', '863', '864', '865', '866', '869', '872' ].forEach( page => { - const codec = 'cp' + page; - SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; + const codec = 'cp' + page; + SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; }); function parseCharacterSAUCE(sauce) { - const result = {}; + const result = {}; - result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; + result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; - if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { - // convience: create ansiFlags - sauce.ansiFlags = sauce.flags; + if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { + // convience: create ansiFlags + sauce.ansiFlags = sauce.flags; - let i = 0; - while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { - ++i; - } + let i = 0; + while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { + ++i; + } - const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); - if(fontName.length > 0) { - result.fontName = fontName; - } - } + const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); + if(fontName.length > 0) { + result.fontName = fontName; + } + } - return result; + return result; } \ No newline at end of file diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 2d978852..c5fa3f57 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -37,9 +37,9 @@ const iconv = require('iconv-lite'); const uuidV4 = require('uuid/v4'); exports.moduleInfo = { - name : 'FTN BSO', - desc : 'BSO style message scanner/tosser for FTN networks', - author : 'NuSkooler', + name : 'FTN BSO', + desc : 'BSO style message scanner/tosser for FTN networks', + author : 'NuSkooler', }; /* @@ -54,75 +54,75 @@ exports.getModule = FTNMessageScanTossModule; const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:|@immediate)([^\0]+)?$/; function FTNMessageScanTossModule() { - MessageScanTossModule.call(this); + MessageScanTossModule.call(this); - const self = this; + const self = this; - this.archUtil = ArchiveUtil.getInstance(); + this.archUtil = ArchiveUtil.getInstance(); - const config = Config(); - if(_.has(config, 'scannerTossers.ftn_bso')) { - this.moduleConfig = config.scannerTossers.ftn_bso; - } + const config = Config(); + if(_.has(config, 'scannerTossers.ftn_bso')) { + this.moduleConfig = config.scannerTossers.ftn_bso; + } - this.getDefaultNetworkName = function() { - if(this.moduleConfig.defaultNetwork) { - return this.moduleConfig.defaultNetwork.toLowerCase(); - } + this.getDefaultNetworkName = function() { + if(this.moduleConfig.defaultNetwork) { + return this.moduleConfig.defaultNetwork.toLowerCase(); + } - const networkNames = Object.keys(config.messageNetworks.ftn.networks); - if(1 === networkNames.length) { - return networkNames[0].toLowerCase(); - } - }; + const networkNames = Object.keys(config.messageNetworks.ftn.networks); + if(1 === networkNames.length) { + return networkNames[0].toLowerCase(); + } + }; - this.getDefaultZone = function(networkName) { - const config = Config(); - if(_.isNumber(config.messageNetworks.ftn.networks[networkName].defaultZone)) { - return config.messageNetworks.ftn.networks[networkName].defaultZone; - } + this.getDefaultZone = function(networkName) { + const config = Config(); + if(_.isNumber(config.messageNetworks.ftn.networks[networkName].defaultZone)) { + return config.messageNetworks.ftn.networks[networkName].defaultZone; + } - // non-explicit: default to local address zone - const networkLocalAddress = config.messageNetworks.ftn.networks[networkName].localAddress; - if(networkLocalAddress) { - const addr = Address.fromString(networkLocalAddress); - return addr.zone; - } - }; + // non-explicit: default to local address zone + const networkLocalAddress = config.messageNetworks.ftn.networks[networkName].localAddress; + if(networkLocalAddress) { + const addr = Address.fromString(networkLocalAddress); + return addr.zone; + } + }; - /* + /* this.isDefaultDomainZone = function(networkName, address) { const defaultNetworkName = this.getDefaultNetworkName(); return(networkName === defaultNetworkName && address.zone === this.moduleConfig.defaultZone); }; */ - this.getNetworkNameByAddress = function(remoteAddress) { - return _.findKey(Config().messageNetworks.ftn.networks, network => { - const localAddress = Address.fromString(network.localAddress); - return !_.isUndefined(localAddress) && localAddress.isEqual(remoteAddress); - }); - }; + this.getNetworkNameByAddress = function(remoteAddress) { + return _.findKey(Config().messageNetworks.ftn.networks, network => { + const localAddress = Address.fromString(network.localAddress); + return !_.isUndefined(localAddress) && localAddress.isEqual(remoteAddress); + }); + }; - this.getNetworkNameByAddressPattern = function(remoteAddressPattern) { - return _.findKey(Config().messageNetworks.ftn.networks, network => { - const localAddress = Address.fromString(network.localAddress); - return !_.isUndefined(localAddress) && localAddress.isPatternMatch(remoteAddressPattern); - }); - }; + this.getNetworkNameByAddressPattern = function(remoteAddressPattern) { + return _.findKey(Config().messageNetworks.ftn.networks, network => { + const localAddress = Address.fromString(network.localAddress); + return !_.isUndefined(localAddress) && localAddress.isPatternMatch(remoteAddressPattern); + }); + }; - this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) { - ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper - return _.findKey(Config().messageNetworks.ftn.areas, areaConf => { - return areaConf.tag.toUpperCase() === ftnAreaTag; - }); - }; + this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) { + ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper + return _.findKey(Config().messageNetworks.ftn.areas, areaConf => { + return areaConf.tag.toUpperCase() === ftnAreaTag; + }); + }; - this.getExportType = function(nodeConfig) { - return _.isString(nodeConfig.exportType) ? nodeConfig.exportType.toLowerCase() : 'crash'; - }; + this.getExportType = function(nodeConfig) { + return _.isString(nodeConfig.exportType) ? nodeConfig.exportType.toLowerCase() : 'crash'; + }; - /* + /* this.getSeenByAddresses = function(messageSeenBy) { if(!_.isArray(messageSeenBy)) { messageSeenBy = [ messageSeenBy ]; @@ -136,11 +136,11 @@ function FTNMessageScanTossModule() { }; */ - this.messageHasValidMSGID = function(msg) { - return _.isString(msg.meta.FtnKludge.MSGID) && msg.meta.FtnKludge.MSGID.length > 0; - }; + this.messageHasValidMSGID = function(msg) { + return _.isString(msg.meta.FtnKludge.MSGID) && msg.meta.FtnKludge.MSGID.length > 0; + }; - /* + /* this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { let dir = this.moduleConfig.paths.outbound; if(!this.isDefaultDomainZone(networkName, destAddress)) { @@ -151,230 +151,230 @@ function FTNMessageScanTossModule() { }; */ - this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { - networkName = networkName.toLowerCase(); + this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { + networkName = networkName.toLowerCase(); - let dir = this.moduleConfig.paths.outbound; + let dir = this.moduleConfig.paths.outbound; - const defaultNetworkName = this.getDefaultNetworkName(); - const defaultZone = this.getDefaultZone(networkName); + const defaultNetworkName = this.getDefaultNetworkName(); + const defaultZone = this.getDefaultZone(networkName); - let zoneExt; - if(defaultZone !== destAddress.zone) { - zoneExt = '.' + `000${destAddress.zone.toString(16)}`.substr(-3); - } else { - zoneExt = ''; - } + let zoneExt; + if(defaultZone !== destAddress.zone) { + zoneExt = '.' + `000${destAddress.zone.toString(16)}`.substr(-3); + } else { + zoneExt = ''; + } - if(defaultNetworkName === networkName) { - dir = paths.join(dir, `outbound${zoneExt}`); - } else { - dir = paths.join(dir, `${networkName}${zoneExt}`); - } + if(defaultNetworkName === networkName) { + dir = paths.join(dir, `outbound${zoneExt}`); + } else { + dir = paths.join(dir, `${networkName}${zoneExt}`); + } - return dir; - }; + return dir; + }; - this.getOutgoingPacketFileName = function(basePath, messageId, isTemp, fileCase) { - // - // Generating an outgoing packet file name comes with a few issues: - // * We must use DOS 8.3 filenames due to legacy systems that receive - // the packet not understanding LFNs - // * We need uniqueness; This is especially important with packets that - // end up in bundles and on the receiving/remote system where conflicts - // with other systems could also occur - // - // There are a lot of systems in use here for the name: - // * HEX CRC16/32 of data - // * HEX UNIX timestamp - // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second) - // See https://groups.google.com/forum/#!searchin/alt.bbs.mystic/netmail$20filename/alt.bbs.mystic/m1xLnY8i1pU/YnG2excdl6MJ - // * SBBSEcho uses DDHHMMSS - see https://github.com/ftnapps/pkg-sbbs/blob/master/docs/fidonet.txt - // * We already have a system for 8-character serial number gernation that is - // used for e.g. in FTS-0009.001 MSGIDs... let's use that! - // - const name = ftnUtil.getMessageSerialNumber(messageId); - const ext = (true === isTemp) ? 'pk_' : 'pkt'; + this.getOutgoingPacketFileName = function(basePath, messageId, isTemp, fileCase) { + // + // Generating an outgoing packet file name comes with a few issues: + // * We must use DOS 8.3 filenames due to legacy systems that receive + // the packet not understanding LFNs + // * We need uniqueness; This is especially important with packets that + // end up in bundles and on the receiving/remote system where conflicts + // with other systems could also occur + // + // There are a lot of systems in use here for the name: + // * HEX CRC16/32 of data + // * HEX UNIX timestamp + // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second) + // See https://groups.google.com/forum/#!searchin/alt.bbs.mystic/netmail$20filename/alt.bbs.mystic/m1xLnY8i1pU/YnG2excdl6MJ + // * SBBSEcho uses DDHHMMSS - see https://github.com/ftnapps/pkg-sbbs/blob/master/docs/fidonet.txt + // * We already have a system for 8-character serial number gernation that is + // used for e.g. in FTS-0009.001 MSGIDs... let's use that! + // + const name = ftnUtil.getMessageSerialNumber(messageId); + const ext = (true === isTemp) ? 'pk_' : 'pkt'; - let fileName = `${name}.${ext}`; - if('upper' === fileCase) { - fileName = fileName.toUpperCase(); - } + let fileName = `${name}.${ext}`; + if('upper' === fileCase) { + fileName = fileName.toUpperCase(); + } - return paths.join(basePath, fileName); - }; + return paths.join(basePath, fileName); + }; - this.getOutgoingFlowFileExtension = function(destAddress, flowType, exportType, fileCase) { - let ext; + this.getOutgoingFlowFileExtension = function(destAddress, flowType, exportType, fileCase) { + let ext; - switch(flowType) { - case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break; - case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break; - case 'busy' : ext = 'bsy'; break; - case 'request' : ext = 'req'; break; - case 'requests' : ext = 'hrq'; break; - } + switch(flowType) { + case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break; + case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break; + case 'busy' : ext = 'bsy'; break; + case 'request' : ext = 'req'; break; + case 'requests' : ext = 'hrq'; break; + } - if('upper' === fileCase) { - ext = ext.toUpperCase(); - } + if('upper' === fileCase) { + ext = ext.toUpperCase(); + } - return ext; - }; + return ext; + }; - this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) { - // - // Refs - // * http://ftsc.org/docs/fts-5005.003 - // * http://wiki.synchro.net/ref:fidonet_files#flow_files - // - let controlFileBaseName; - let pointDir; + this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) { + // + // Refs + // * http://ftsc.org/docs/fts-5005.003 + // * http://wiki.synchro.net/ref:fidonet_files#flow_files + // + let controlFileBaseName; + let pointDir; - const ext = self.getOutgoingFlowFileExtension( - destAddress, - flowType, - exportType, - fileCase - ); + const ext = self.getOutgoingFlowFileExtension( + destAddress, + flowType, + exportType, + fileCase + ); - const netComponent = `0000${destAddress.net.toString(16)}`.substr(-4); - const nodeComponent = `0000${destAddress.node.toString(16)}`.substr(-4); + const netComponent = `0000${destAddress.net.toString(16)}`.substr(-4); + const nodeComponent = `0000${destAddress.node.toString(16)}`.substr(-4); - if(destAddress.point) { - // point's go in an extra subdir, e.g. outbound/NNNNnnnn.pnt/00000001.pnt (for a point of 1) - pointDir = `${netComponent}${nodeComponent}.pnt`; - controlFileBaseName = `00000000${destAddress.point.toString(16)}`.substr(-8); - } else { - pointDir = ''; + if(destAddress.point) { + // point's go in an extra subdir, e.g. outbound/NNNNnnnn.pnt/00000001.pnt (for a point of 1) + pointDir = `${netComponent}${nodeComponent}.pnt`; + controlFileBaseName = `00000000${destAddress.point.toString(16)}`.substr(-8); + } else { + pointDir = ''; - // - // Use |destAddress| nnnnNNNN.??? where nnnn is dest net and NNNN is dest - // node. This seems to match what Mystic does - // - controlFileBaseName = `${netComponent}${nodeComponent}`; - } + // + // Use |destAddress| nnnnNNNN.??? where nnnn is dest net and NNNN is dest + // node. This seems to match what Mystic does + // + controlFileBaseName = `${netComponent}${nodeComponent}`; + } - // - // From FTS-5005.003: "Lower case filenames are prefered if supported by the file system." - // ...but we let the user override. - // - if('upper' === fileCase) { - controlFileBaseName = controlFileBaseName.toUpperCase(); - pointDir = pointDir.toUpperCase(); - } + // + // From FTS-5005.003: "Lower case filenames are prefered if supported by the file system." + // ...but we let the user override. + // + if('upper' === fileCase) { + controlFileBaseName = controlFileBaseName.toUpperCase(); + pointDir = pointDir.toUpperCase(); + } - return paths.join(basePath, pointDir, `${controlFileBaseName}.${ext}`); - }; + return paths.join(basePath, pointDir, `${controlFileBaseName}.${ext}`); + }; - this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) { - // - // We have to ensure the *directory* of |filePath| exists here esp. - // for cases such as point destinations where a subdir may be - // present in the path that doesn't yet exist. - // - const flowFileDir = paths.dirname(filePath); - fse.mkdirs(flowFileDir, () => { // note not checking err; let's try appendFile - const appendLines = fileRefs.reduce( (content, ref) => { - return content + `${directive}${ref}\n`; - }, ''); + this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) { + // + // We have to ensure the *directory* of |filePath| exists here esp. + // for cases such as point destinations where a subdir may be + // present in the path that doesn't yet exist. + // + const flowFileDir = paths.dirname(filePath); + fse.mkdirs(flowFileDir, () => { // note not checking err; let's try appendFile + const appendLines = fileRefs.reduce( (content, ref) => { + return content + `${directive}${ref}\n`; + }, ''); - fs.appendFile(filePath, appendLines, err => { - return cb(err); - }); - }); - }; + fs.appendFile(filePath, appendLines, err => { + return cb(err); + }); + }); + }; - this.getOutgoingBundleFileName = function(basePath, sourceAddress, destAddress, cb) { - // - // Base filename is constructed as such: - // * If this |destAddress| is *not* a point address, we use NNNNnnnn where - // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded - // hex of dest node - source node. - // * If |destAddress| is a point, NNNN becomes 0000 and nnnn becomes 'p' + - // 3 digit 0 padded hex point - // - // Extension is dd? where dd is Su...Mo and ? is 0...Z as collisions arise - // - let basename; - if(destAddress.point) { - const pointHex = `000${destAddress.point}`.substr(-3); - basename = `0000p${pointHex}`; - } else { - basename = + this.getOutgoingBundleFileName = function(basePath, sourceAddress, destAddress, cb) { + // + // Base filename is constructed as such: + // * If this |destAddress| is *not* a point address, we use NNNNnnnn where + // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded + // hex of dest node - source node. + // * If |destAddress| is a point, NNNN becomes 0000 and nnnn becomes 'p' + + // 3 digit 0 padded hex point + // + // Extension is dd? where dd is Su...Mo and ? is 0...Z as collisions arise + // + let basename; + if(destAddress.point) { + const pointHex = `000${destAddress.point}`.substr(-3); + basename = `0000p${pointHex}`; + } else { + basename = `0000${Math.abs(sourceAddress.net - destAddress.net).toString(16)}`.substr(-4) + `0000${Math.abs(sourceAddress.node - destAddress.node).toString(16)}`.substr(-4); - } + } - // - // We need to now find the first entry that does not exist starting - // with dd0 to ddz - // - const EXT_SUFFIXES = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); - let fileName = `${basename}.${moment().format('dd').toLowerCase()}`; - async.detectSeries(EXT_SUFFIXES, (suffix, callback) => { - const checkFileName = fileName + suffix; - fs.stat(paths.join(basePath, checkFileName), err => { - callback(null, (err && 'ENOENT' === err.code) ? true : false); - }); - }, (err, finalSuffix) => { - if(finalSuffix) { - return cb(null, paths.join(basePath, fileName + finalSuffix)); - } + // + // We need to now find the first entry that does not exist starting + // with dd0 to ddz + // + const EXT_SUFFIXES = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); + let fileName = `${basename}.${moment().format('dd').toLowerCase()}`; + async.detectSeries(EXT_SUFFIXES, (suffix, callback) => { + const checkFileName = fileName + suffix; + fs.stat(paths.join(basePath, checkFileName), err => { + callback(null, (err && 'ENOENT' === err.code) ? true : false); + }); + }, (err, finalSuffix) => { + if(finalSuffix) { + return cb(null, paths.join(basePath, fileName + finalSuffix)); + } - return cb(new Error('Could not acquire a bundle filename!')); - }); - }; + return cb(new Error('Could not acquire a bundle filename!')); + }); + }; - this.prepareMessage = function(message, options) { - // - // Set various FTN kludges/etc. - // - const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version + this.prepareMessage = function(message, options) { + // + // Set various FTN kludges/etc. + // + const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version - // :TODO: create Address.toMeta() / similar - message.meta.FtnProperty = message.meta.FtnProperty || {}; - message.meta.FtnKludge = message.meta.FtnKludge || {}; + // :TODO: create Address.toMeta() / similar + message.meta.FtnProperty = message.meta.FtnProperty || {}; + message.meta.FtnKludge = message.meta.FtnKludge || {}; - message.meta.FtnProperty.ftn_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_orig_network = localAddress.net; - message.meta.FtnProperty.ftn_cost = 0; - message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; + message.meta.FtnProperty.ftn_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_orig_network = localAddress.net; + message.meta.FtnProperty.ftn_cost = 0; + message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; - const destAddress = options.routeAddress || options.destAddress; - message.meta.FtnProperty.ftn_dest_node = destAddress.node; - message.meta.FtnProperty.ftn_dest_network = destAddress.net; + const destAddress = options.routeAddress || options.destAddress; + message.meta.FtnProperty.ftn_dest_node = destAddress.node; + message.meta.FtnProperty.ftn_dest_network = destAddress.net; - if(destAddress.zone) { - message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; - } - if(destAddress.point) { - message.meta.FtnProperty.ftn_dest_point = destAddress.point; - } + if(destAddress.zone) { + message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; + } + if(destAddress.point) { + message.meta.FtnProperty.ftn_dest_point = destAddress.point; + } - // tear line and origin can both go in EchoMail & NetMail - message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); - message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(localAddress); + // tear line and origin can both go in EchoMail & NetMail + message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); + message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(localAddress); - let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system + let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system - const config = Config(); - if(self.isNetMailMessage(message)) { - // - // Set route and message destination properties -- they may differ - // - message.meta.FtnProperty.ftn_msg_dest_node = options.destAddress.node; - message.meta.FtnProperty.ftn_msg_dest_net = options.destAddress.net; + const config = Config(); + if(self.isNetMailMessage(message)) { + // + // Set route and message destination properties -- they may differ + // + message.meta.FtnProperty.ftn_msg_dest_node = options.destAddress.node; + message.meta.FtnProperty.ftn_msg_dest_net = options.destAddress.net; - ftnAttribute |= ftnMailPacket.Packet.Attribute.Private; + ftnAttribute |= ftnMailPacket.Packet.Attribute.Private; - // - // NetMail messages need a FRL-1005.001 "Via" line - // http://ftsc.org/docs/frl-1005.001 - // - // :TODO: We need to do this when FORWARDING NetMail - /* + // + // NetMail messages need a FRL-1005.001 "Via" line + // http://ftsc.org/docs/frl-1005.001 + // + // :TODO: We need to do this when FORWARDING NetMail + /* if(_.isString(message.meta.FtnKludge.Via)) { message.meta.FtnKludge.Via = [ message.meta.FtnKludge.Via ]; } @@ -382,1546 +382,1546 @@ function FTNMessageScanTossModule() { message.meta.FtnKludge.Via.push(ftnUtil.getVia(options.network.localAddress)); */ - // - // We need to set INTL, and possibly FMPT and/or TOPT - // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac - // - message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.destAddress, localAddress); + // + // We need to set INTL, and possibly FMPT and/or TOPT + // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac + // + message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.destAddress, localAddress); - if(_.isNumber(localAddress.point) && localAddress.point > 0) { - message.meta.FtnKludge.FMPT = localAddress.point; - } + if(_.isNumber(localAddress.point) && localAddress.point > 0) { + message.meta.FtnKludge.FMPT = localAddress.point; + } - if(_.isNumber(options.destAddress.point) && options.destAddress.point > 0) { - message.meta.FtnKludge.TOPT = options.destAddress.point; - } - } else { - // - // Set appropriate attribute flag for export type - // - switch(this.getExportType(options.nodeConfig)) { - case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break; - case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break; + if(_.isNumber(options.destAddress.point) && options.destAddress.point > 0) { + message.meta.FtnKludge.TOPT = options.destAddress.point; + } + } else { + // + // Set appropriate attribute flag for export type + // + switch(this.getExportType(options.nodeConfig)) { + case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break; + case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break; // :TODO: Others? - } + } - // - // EchoMail requires some additional properties & kludges - // - message.meta.FtnProperty.ftn_area = config.messageNetworks.ftn.areas[message.areaTag].tag; + // + // EchoMail requires some additional properties & kludges + // + message.meta.FtnProperty.ftn_area = config.messageNetworks.ftn.areas[message.areaTag].tag; - // - // When exporting messages, we should create/update SEEN-BY - // with remote address(s) we are exporting to. - // - const seenByAdditions = + // + // When exporting messages, we should create/update SEEN-BY + // with remote address(s) we are exporting to. + // + const seenByAdditions = [ `${localAddress.net}/${localAddress.node}` ].concat(config.messageNetworks.ftn.areas[message.areaTag].uplinks); - message.meta.FtnProperty.ftn_seen_by = + message.meta.FtnProperty.ftn_seen_by = ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions); - // - // And create/update PATH for ourself - // - message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, localAddress); - } + // + // And create/update PATH for ourself + // + message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, localAddress); + } - message.meta.FtnProperty.ftn_attr_flags = ftnAttribute; + message.meta.FtnProperty.ftn_attr_flags = ftnAttribute; - // - // Additional kludges - // - // Check for existence of MSGID as we may already have stored it from a previous - // export that failed to finish - // - if(!message.meta.FtnKludge.MSGID) { - message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier( - message, - localAddress, - message.isPrivate() // true = isNetMail - ); - } + // + // Additional kludges + // + // Check for existence of MSGID as we may already have stored it from a previous + // export that failed to finish + // + if(!message.meta.FtnKludge.MSGID) { + message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier( + message, + localAddress, + message.isPrivate() // true = isNetMail + ); + } - message.meta.FtnKludge.TZUTC = ftnUtil.getUTCTimeZoneOffset(); + message.meta.FtnKludge.TZUTC = ftnUtil.getUTCTimeZoneOffset(); - // - // According to FSC-0046: - // - // "When a Conference Mail processor adds a TID to a message, it may not - // add a PID. An existing TID should, however, be replaced. TIDs follow - // the same format used for PIDs, as explained above." - // - message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier(); + // + // According to FSC-0046: + // + // "When a Conference Mail processor adds a TID to a message, it may not + // add a PID. An existing TID should, however, be replaced. TIDs follow + // the same format used for PIDs, as explained above." + // + message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier(); - // - // Determine CHRS and actual internal encoding name. If the message has an - // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set. - // - let encoding = options.nodeConfig.encoding || config.scannerTossers.ftn_bso.packetMsgEncoding || 'utf8'; - 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); - if(encFromChars) { - encoding = encFromChars; - } - } + // + // Determine CHRS and actual internal encoding name. If the message has an + // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set. + // + let encoding = options.nodeConfig.encoding || config.scannerTossers.ftn_bso.packetMsgEncoding || 'utf8'; + 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); + if(encFromChars) { + encoding = encFromChars; + } + } - // - // Ensure we ended up with something useable. If not, back to utf8! - // - if(!iconv.encodingExists(encoding)) { - Log.debug( { encoding : encoding }, 'Unknown encoding. Falling back to utf8'); - encoding = 'utf8'; - } + // + // Ensure we ended up with something useable. If not, back to utf8! + // + if(!iconv.encodingExists(encoding)) { + Log.debug( { encoding : encoding }, 'Unknown encoding. Falling back to utf8'); + encoding = 'utf8'; + } - options.encoding = encoding; // save for later - message.meta.FtnKludge.CHRS = ftnUtil.getCharacterSetIdentifierByEncoding(encoding); - }; + options.encoding = encoding; // save for later + message.meta.FtnKludge.CHRS = ftnUtil.getCharacterSetIdentifierByEncoding(encoding); + }; - this.setReplyKludgeFromReplyToMsgId = function(message, cb) { - // - // Look up MSGID kludge for |message.replyToMsgId|, if any. - // If found, we can create a REPLY kludge with the previously - // discovered MSGID. - // + this.setReplyKludgeFromReplyToMsgId = function(message, cb) { + // + // Look up MSGID kludge for |message.replyToMsgId|, if any. + // If found, we can create a REPLY kludge with the previously + // discovered MSGID. + // - if(0 === message.replyToMsgId) { - return cb(null); // nothing to do - } + if(0 === message.replyToMsgId) { + return cb(null); // nothing to do + } - Message.getMetaValuesByMessageId(message.replyToMsgId, 'FtnKludge', 'MSGID', (err, msgIdVal) => { - if(!err) { - assert(_.isString(msgIdVal), 'Expected string but got ' + (typeof msgIdVal) + ' (' + msgIdVal + ')'); - // got a MSGID - create a REPLY - message.meta.FtnKludge.REPLY = msgIdVal; - } + Message.getMetaValuesByMessageId(message.replyToMsgId, 'FtnKludge', 'MSGID', (err, msgIdVal) => { + if(!err) { + assert(_.isString(msgIdVal), 'Expected string but got ' + (typeof msgIdVal) + ' (' + msgIdVal + ')'); + // got a MSGID - create a REPLY + message.meta.FtnKludge.REPLY = msgIdVal; + } - cb(null); // this method always passes - }); - }; + cb(null); // this method always passes + }); + }; - // check paths, Addresses, etc. - this.isAreaConfigValid = function(areaConfig) { - if(!areaConfig || !_.isString(areaConfig.tag) || !_.isString(areaConfig.network)) { - return false; - } + // check paths, Addresses, etc. + this.isAreaConfigValid = function(areaConfig) { + if(!areaConfig || !_.isString(areaConfig.tag) || !_.isString(areaConfig.network)) { + return false; + } - if(_.isString(areaConfig.uplinks)) { - areaConfig.uplinks = areaConfig.uplinks.split(' '); - } + if(_.isString(areaConfig.uplinks)) { + areaConfig.uplinks = areaConfig.uplinks.split(' '); + } - return (_.isArray(areaConfig.uplinks)); - }; + return (_.isArray(areaConfig.uplinks)); + }; - this.hasValidConfiguration = function() { - if(!_.has(this, 'moduleConfig.nodes') || !_.has(Config(), 'messageNetworks.ftn.areas')) { - return false; - } + this.hasValidConfiguration = function() { + if(!_.has(this, 'moduleConfig.nodes') || !_.has(Config(), 'messageNetworks.ftn.areas')) { + return false; + } - // :TODO: need to check more! + // :TODO: need to check more! - return true; - }; + return true; + }; - this.parseScheduleString = function(schedStr) { - if(!schedStr) { - return; // nothing to parse! - } + this.parseScheduleString = function(schedStr) { + if(!schedStr) { + return; // nothing to parse! + } - let schedule = {}; + let schedule = {}; - const m = SCHEDULE_REGEXP.exec(schedStr); - if(m) { - schedStr = schedStr.substr(0, m.index).trim(); + const m = SCHEDULE_REGEXP.exec(schedStr); + if(m) { + schedStr = schedStr.substr(0, m.index).trim(); - if('@watch:' === m[1]) { - schedule.watchFile = m[2]; - } else if('@immediate' === m[1]) { - schedule.immediate = true; - } - } + if('@watch:' === m[1]) { + schedule.watchFile = m[2]; + } else if('@immediate' === m[1]) { + schedule.immediate = true; + } + } - if(schedStr.length > 0) { - const sched = later.parse.text(schedStr); - if(-1 === sched.error) { - schedule.sched = sched; - } - } + if(schedStr.length > 0) { + const sched = later.parse.text(schedStr); + if(-1 === sched.error) { + schedule.sched = sched; + } + } - // return undefined if we couldn't parse out anything useful - if(!_.isEmpty(schedule)) { - return schedule; - } - }; + // return undefined if we couldn't parse out anything useful + if(!_.isEmpty(schedule)) { + return schedule; + } + }; - this.getAreaLastScanId = function(areaTag, cb) { - const sql = + this.getAreaLastScanId = function(areaTag, cb) { + const sql = `SELECT area_tag, message_id FROM message_area_last_scan WHERE scan_toss = "ftn_bso" AND area_tag = ? LIMIT 1;`; - msgDb.get(sql, [ areaTag ], (err, row) => { - return cb(err, row ? row.message_id : 0); - }); - }; + msgDb.get(sql, [ areaTag ], (err, row) => { + return cb(err, row ? row.message_id : 0); + }); + }; - this.setAreaLastScanId = function(areaTag, lastScanId, cb) { - const sql = + this.setAreaLastScanId = function(areaTag, lastScanId, cb) { + const sql = `REPLACE INTO message_area_last_scan (scan_toss, area_tag, message_id) VALUES ("ftn_bso", ?, ?);`; - msgDb.run(sql, [ areaTag, lastScanId ], err => { - return cb(err); - }); - }; - - this.getNodeConfigByAddress = function(addr) { - addr = _.isString(addr) ? Address.fromString(addr) : addr; - - // :TODO: sort wildcard nodes{} entries by most->least explicit according to FTN hierarchy - return _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { - return addr.isPatternMatch(nodeAddrWildcard); - }); - }; - - this.exportNetMailMessagePacket = function(message, exportOpts, cb) { - // - // For NetMail, we always create a *single* packet per message. - // - async.series( - [ - function generalPrep(callback) { - self.prepareMessage(message, exportOpts); - - return self.setReplyKludgeFromReplyToMsgId(message, callback); - }, - function createPacket(callback) { - const packet = new ftnMailPacket.Packet(); - - const packetHeader = new ftnMailPacket.PacketHeader( - exportOpts.network.localAddress, - exportOpts.routeAddress, - exportOpts.nodeConfig.packetType - ); - - packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - - // use current message ID for filename seed - exportOpts.pktFileName = self.getOutgoingPacketFileName( - self.exportTempDir, - message.messageId, - false, // createTempPacket=false - exportOpts.fileCase - ); - - const ws = fs.createWriteStream(exportOpts.pktFileName); - - packet.writeHeader(ws, packetHeader); - - packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => { - if(err) { - return callback(err); - } - - ws.write(msgBuf); - - packet.writeTerminator(ws); - - ws.end(); - ws.once('finish', () => { - return callback(null); - }); - }); - } - ], - err => { - return cb(err); - } - ); - }; - - this.exportMessagesByUuid = function(messageUuids, exportOpts, cb) { - // - // This method has a lot of madness going on: - // - Try to stuff messages into packets until we've hit the target size - // - We need to wait for write streams to finish before proceeding in many cases - // or data will be cut off when closing and creating a new stream - // - let exportedFiles = []; - let currPacketSize = self.moduleConfig.packetTargetByteSize; - let packet; - let ws; - let remainMessageBuf; - let remainMessageId; - const createTempPacket = !_.isString(exportOpts.nodeConfig.archiveType) || 0 === exportOpts.nodeConfig.archiveType.length; - - function finalizePacket(cb) { - packet.writeTerminator(ws); - ws.end(); - ws.once('finish', () => { - return cb(null); - }); - } - - async.each(messageUuids, (msgUuid, nextUuid) => { - let message = new Message(); - - async.series( - [ - function finalizePrevious(callback) { - if(packet && currPacketSize >= self.moduleConfig.packetTargetByteSize) { - return finalizePacket(callback); - } else { - callback(null); - } - }, - function loadMessage(callback) { - message.load( { uuid : msgUuid }, err => { - if(err) { - return callback(err); - } - - // General preperation - self.prepareMessage(message, exportOpts); - - self.setReplyKludgeFromReplyToMsgId(message, err => { - callback(err); - }); - }); - }, - function createNewPacket(callback) { - if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { - packet = new ftnMailPacket.Packet(); - - const packetHeader = new ftnMailPacket.PacketHeader( - exportOpts.network.localAddress, - exportOpts.destAddress, - exportOpts.nodeConfig.packetType); - - packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - - // use current message ID for filename seed - const pktFileName = self.getOutgoingPacketFileName( - self.exportTempDir, - message.messageId, - createTempPacket, - exportOpts.fileCase - ); - - exportedFiles.push(pktFileName); - - ws = fs.createWriteStream(pktFileName); - - currPacketSize = packet.writeHeader(ws, packetHeader); - - if(remainMessageBuf) { - currPacketSize += packet.writeMessageEntry(ws, remainMessageBuf); - remainMessageBuf = null; - } - } - - callback(null); - }, - function appendMessage(callback) { - packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => { - if(err) { - return callback(err); - } - - currPacketSize += msgBuf.length; - - if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { - remainMessageBuf = msgBuf; // save for next packet - remainMessageId = message.messageId; - } else { - ws.write(msgBuf); - } - - return callback(null); - }); - }, - function storeStateFlags0Meta(callback) { - message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), err => { - callback(err); - }); - }, - function storeMsgIdMeta(callback) { - // - // We want to store some meta as if we had imported - // this message for later reference - // - if(message.meta.FtnKludge.MSGID) { - message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, err => { - callback(err); - }); - } else { - callback(null); - } - } - ], - err => { - nextUuid(err); - } - ); - }, err => { - if(err) { - cb(err); - } else { - async.series( - [ - function terminateLast(callback) { - if(packet) { - return finalizePacket(callback); - } else { - callback(null); - } - }, - function writeRemainPacket(callback) { - if(remainMessageBuf) { - // :TODO: DRY this with the code above -- they are basically identical - packet = new ftnMailPacket.Packet(); - - const packetHeader = new ftnMailPacket.PacketHeader( - exportOpts.network.localAddress, - exportOpts.destAddress, - exportOpts.nodeConfig.packetType); - - packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - - // use current message ID for filename seed - const pktFileName = self.getOutgoingPacketFileName( - self.exportTempDir, - remainMessageId, - createTempPacket, - exportOpts.filleCase - ); - - exportedFiles.push(pktFileName); - - ws = fs.createWriteStream(pktFileName); - - packet.writeHeader(ws, packetHeader); - ws.write(remainMessageBuf); - return finalizePacket(callback); - } else { - callback(null); - } - } - ], - err => { - cb(err, exportedFiles); - } - ); - } - }); - }; - - this.getNetMailRoute = function(dstAddr) { - // - // Route full|wildcard -> full adddress/network lookup - // - const routes = _.get(Config(), 'scannerTossers.ftn_bso.netMail.routes'); - if(!routes) { - return; - } - - return _.find(routes, (route, addrWildcard) => { - return dstAddr.isPatternMatch(addrWildcard); - }); - }; - - this.getNetMailRouteInfoFromAddress = function(destAddress, cb) { - // - // Attempt to find route information for |destAddress|: - // - // 1) Routes: scannerTossers.ftn_bso.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config - // - Where we send may not be where destAddress is (it's routed!) - // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config - // - Where we send is direct to destAddress - // - // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address - // falling back to Config.scannerTossers.ftn_bso.defaultNetwork - // - const route = this.getNetMailRoute(destAddress); - - let routeAddress; - let networkName; - let isRouted; - if(route) { - routeAddress = Address.fromString(route.address); - networkName = route.network; - isRouted = true; - } else { - routeAddress = destAddress; - isRouted = false; - } - - networkName = networkName || this.getNetworkNameByAddress(routeAddress); - - const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { - return routeAddress.isPatternMatch(nodeAddrWildcard); - }) || { packetType : '2+', encoding : Config().scannerTossers.ftn_bso.packetMsgEncoding }; - - // we should never be failing here; we may just be using defaults. - return cb( - networkName ? null : Errors.DoesNotExist(`No NetMail route for ${destAddress.toString()}`), - { destAddress, routeAddress, networkName, config, isRouted } - ); - }; - - this.exportNetMailMessagesToUplinks = function(messagesOrMessageUuids, cb) { - // for each message/UUID, find where to send the thing - async.each(messagesOrMessageUuids, (msgOrUuid, nextMessageOrUuid) => { - - const exportOpts = {}; - const message = new Message(); - - async.series( - [ - function loadMessage(callback) { - if(_.isString(msgOrUuid)) { - message.load( { uuid : msgOrUuid }, err => { - return callback(err, message); - }); - } else { - return callback(null, msgOrUuid); - } - }, - function discoverUplink(callback) { - const dstAddr = new Address(message.meta.System[Message.SystemMetaNames.RemoteToUser]); - - self.getNetMailRouteInfoFromAddress(dstAddr, (err, routeInfo) => { - if(err) { - return callback(err); - } - - exportOpts.nodeConfig = routeInfo.config; - exportOpts.destAddress = dstAddr; - exportOpts.routeAddress = routeInfo.routeAddress; - exportOpts.fileCase = routeInfo.config.fileCase || 'lower'; - exportOpts.network = Config().messageNetworks.ftn.networks[routeInfo.networkName]; - exportOpts.networkName = routeInfo.networkName; - exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); - exportOpts.exportType = self.getExportType(routeInfo.config); - - if(!exportOpts.network) { - return callback(Errors.DoesNotExist(`No configuration found for network ${routeInfo.networkName}`)); - } - - return callback(null); - }); - }, - function createOutgoingDir(callback) { - // ensure outgoing NetMail directory exists - return fse.mkdirs(exportOpts.outgoingDir, callback); - }, - function exportPacket(callback) { - return self.exportNetMailMessagePacket(message, exportOpts, callback); - }, - function moveToOutgoing(callback) { - const newExt = exportOpts.fileCase === 'lower' ? '.pkt' : '.PKT'; - exportOpts.exportedToPath = paths.join( - exportOpts.outgoingDir, - `${paths.basename(exportOpts.pktFileName, paths.extname(exportOpts.pktFileName))}${newExt}` - ); - - return fse.move(exportOpts.pktFileName, exportOpts.exportedToPath, callback); - }, - function prepareFloFile(callback) { - const flowFilePath = self.getOutgoingFlowFileName( - exportOpts.outgoingDir, - exportOpts.routeAddress, - 'ref', - exportOpts.exportType, - exportOpts.fileCase - ); - - return self.flowFileAppendRefs(flowFilePath, [ exportOpts.exportedToPath ], '^', callback); - }, - function storeStateFlags0Meta(callback) { - return message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), callback); - }, - function storeMsgIdMeta(callback) { - // Store meta as if we had imported this message -- for later reference - if(message.meta.FtnKludge.MSGID) { - return message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, callback); - } - - return callback(null); - } - ], - err => { - if(err) { - Log.warn( { error : err.message }, 'Error exporting message' ); - } - return nextMessageOrUuid(null); - } - ); - }, err => { - if(err) { - Log.warn( { error : err.message }, 'Error(s) during NetMail export'); - } - return cb(err); - }); - }; - - this.exportEchoMailMessagesToUplinks = function(messageUuids, areaConfig, cb) { - const config = Config(); - async.each(areaConfig.uplinks, (uplink, nextUplink) => { - const nodeConfig = self.getNodeConfigByAddress(uplink); - if(!nodeConfig) { - return nextUplink(); - } - - const exportOpts = { - nodeConfig, - network : config.messageNetworks.ftn.networks[areaConfig.network], - destAddress : Address.fromString(uplink), - networkName : areaConfig.network, - fileCase : nodeConfig.fileCase || 'lower', - }; - - if(_.isString(exportOpts.network.localAddress)) { - exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); - } - - const outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); - const exportType = self.getExportType(exportOpts.nodeConfig); - - async.waterfall( - [ - function createOutgoingDir(callback) { - fse.mkdirs(outgoingDir, err => { - callback(err); - }); - }, - function exportToTempArea(callback) { - self.exportMessagesByUuid(messageUuids, exportOpts, callback); - }, - function createArcMailBundle(exportedFileNames, callback) { - if(self.archUtil.haveArchiver(exportOpts.nodeConfig.archiveType)) { - // :TODO: support bundleTargetByteSize: - // - // Compress to a temp location then we'll move it in the next step - // - // Note that we must use the *final* output dir for getOutgoingBundleFileName() - // as it checks for collisions in bundle names! - // - self.getOutgoingBundleFileName(outgoingDir, exportOpts.network.localAddress, exportOpts.destAddress, (err, bundlePath) => { - if(err) { - return callback(err); - } - - // adjust back to temp path - const tempBundlePath = paths.join(self.exportTempDir, paths.basename(bundlePath)); - - self.archUtil.compressTo( - exportOpts.nodeConfig.archiveType, - tempBundlePath, - exportedFileNames, err => { - callback(err, [ tempBundlePath ] ); - } - ); - }); - } else { - callback(null, exportedFileNames); - } - }, - function moveFilesToOutgoing(exportedFileNames, callback) { - async.each(exportedFileNames, (oldPath, nextFile) => { - const ext = paths.extname(oldPath).toLowerCase(); - if('.pk_' === ext.toLowerCase()) { - // - // For a given temporary .pk_ file, we need to move it to the outoing - // directory with the appropriate BSO style filename. - // - const newExt = self.getOutgoingFlowFileExtension( - exportOpts.destAddress, - 'mail', - exportType, - exportOpts.fileCase - ); - - const newPath = paths.join( - outgoingDir, - `${paths.basename(oldPath, ext)}${newExt}`); - - fse.move(oldPath, newPath, nextFile); - } else { - const newPath = paths.join(outgoingDir, paths.basename(oldPath)); - fse.move(oldPath, newPath, err => { - if(err) { - Log.warn( - { oldPath : oldPath, newPath : newPath, error : err.toString() }, - 'Failed moving temporary bundle file!'); - - return nextFile(); - } - - // - // For bundles, we need to append to the appropriate flow file - // - const flowFilePath = self.getOutgoingFlowFileName( - outgoingDir, - exportOpts.destAddress, - 'ref', - exportType, - exportOpts.fileCase - ); - - // directive of '^' = delete file after transfer - self.flowFileAppendRefs(flowFilePath, [ newPath ], '^', err => { - if(err) { - Log.warn( { path : flowFilePath }, 'Failed appending flow reference record!'); - } - nextFile(); - }); - }); - } - }, callback); - } - ], - err => { - // :TODO: do something with |err| ? - if(err) { - Log.warn(err.message); - } - nextUplink(); - } - ); - }, cb); // complete - }; - - this.setReplyToMsgIdFtnReplyKludge = function(message, cb) { - // - // Given a FTN REPLY kludge, set |message.replyToMsgId|, if possible, - // by looking up an associated MSGID kludge meta. - // - // See also: http://ftsc.org/docs/fts-0009.001 - // - if(!_.isString(message.meta.FtnKludge.REPLY)) { - // nothing to do - return cb(); - } - - Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => { - if(msgIds && msgIds.length > 0) { - // expect a single match, but dupe checking is not perfect - warn otherwise - if(1 === msgIds.length) { - message.replyToMsgId = msgIds[0]; - } else { - Log.warn( { msgIds : msgIds, replyKludge : message.meta.FtnKludge.REPLY }, 'Found 2:n MSGIDs matching REPLY kludge!'); - } - } - cb(); - }); - }; - - this.getLocalUserNameFromAlias = function(lookup) { - lookup = lookup.toLowerCase(); - - const aliases = _.get(Config(), 'messageNetworks.ftn.netMail.aliases'); - if(!aliases) { - return lookup; // keep orig - } - - const alias = _.find(aliases, (localName, alias) => { - return alias.toLowerCase() === lookup; - }); - - return alias || lookup; - }; - - this.getAddressesFromNetMailMessage = function(message) { - const intlKludge = _.get(message, 'meta.FtnKludge.INTL'); - - if(!intlKludge) { - return {}; - } - - let [ to, from ] = intlKludge.split(' '); - if(!to || !from) { - return {}; - } - - const fromPoint = _.get(message, 'meta.FtnKludge.FMPT'); - const toPoint = _.get(message, 'meta.FtnKludge.TOPT'); - - if(fromPoint) { - from += `.${fromPoint}`; - } - - if(toPoint) { - to += `.${toPoint}`; - } - - return { to : Address.fromString(to), from : Address.fromString(from) }; - }; - - this.importMailToArea = function(config, header, message, cb) { - async.series( - [ - function validateDestinationAddress(callback) { - const localNetworkPattern = `${message.meta.FtnProperty.ftn_dest_network}/${message.meta.FtnProperty.ftn_dest_node}`; - const localNetworkName = self.getNetworkNameByAddressPattern(localNetworkPattern); - - return callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us')); - }, - function checkForDupeMSGID(callback) { - // - // If we have a MSGID, don't allow a dupe - // - if(!_.has(message.meta, 'FtnKludge.MSGID')) { - return callback(null); - } - - Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, (err, msgIds) => { - if(msgIds && msgIds.length > 0) { - const err = new Error('Duplicate MSGID'); - err.code = 'DUPE_MSGID'; - return callback(err); - } - - return callback(null); - }); - }, - function basicSetup(callback) { - message.areaTag = config.localAreaTag; - - // indicate this was imported from FTN - message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.FTN; - - // - // If we *allow* dupes (disabled by default), then just generate - // a random UUID. Otherwise, don't assign the UUID just yet. It will be - // generated at persist() time and should be consistent across import/exports - // - if(true === _.get(Config(), [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) { - // just generate a UUID & therefor always allow for dupes - message.uuid = uuidV4(); - } - - return callback(null); - }, - function setReplyToMessageId(callback) { - self.setReplyToMsgIdFtnReplyKludge(message, () => { - return callback(null); - }); - }, - function setupPrivateMessage(callback) { - // - // If this is a private message (e.g. NetMail) we set the local user ID - // - if(Message.WellKnownAreaTags.Private !== config.localAreaTag) { - return callback(null); - } - - // - // Create a meta value for the *remote* from user. In the case here with FTN, - // their fully qualified FTN from address - // - const { from } = self.getAddressesFromNetMailMessage(message); - - if(!from) { - return callback(Errors.Invalid('Cannot import FTN NetMail without valid INTL line')); - } - - message.meta.System[Message.SystemMetaNames.RemoteFromUser] = from.toString(); - - const lookupName = self.getLocalUserNameFromAlias(message.toUserName); - - User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { - if(err) { - // - // Couldn't find a local username. If the toUserName itself is a FTN address - // we can only assume the message is to the +op, else we'll have to fail. - // - const toUserNameAsAddress = Address.fromString(message.toUserName); - if(toUserNameAsAddress.isValid()) { - - Log.info( - { toUserName : message.toUserName, fromUserName : message.fromUserName }, - 'No local "to" username for FTN message. Appears to be a FTN address only; assuming addressed to SysOp' - ); - - User.getUserName(User.RootUserID, (err, sysOpUserName) => { - if(err) { - return callback(Errors.UnexpectedState('Failed to get SysOp user information')); - } - - message.meta.System[Message.SystemMetaNames.LocalToUserID] = User.RootUserID; - message.toUserName = sysOpUserName; - return callback(null); - }); - } else { - return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); - } - } - - // we do this after such that error cases can be preseved above - if(lookupName !== message.toUserName) { - message.toUserName = localUserName; - } - - // set the meta information - used elsehwere for retrieval - message.meta.System[Message.SystemMetaNames.LocalToUserID] = localToUserId; - return callback(null); - }); - }, - function persistImport(callback) { - // mark as imported - message.meta.System.state_flags0 = Message.StateFlags0.Imported.toString(); - - // save to disc - message.persist(err => { - return callback(err); - }); - } - ], - err => { - cb(err); - } - ); - }; - - this.appendTearAndOrigin = function(message) { - if(message.meta.FtnProperty.ftn_tear_line) { - message.message += `\r\n${message.meta.FtnProperty.ftn_tear_line}\r\n`; - } - - if(message.meta.FtnProperty.ftn_origin) { - message.message += `${message.meta.FtnProperty.ftn_origin}\r\n`; - } - }; - - // - // Ref. implementations on import: - // * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c - // https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/handle.c - // - this.importMessagesFromPacketFile = function(packetPath, password, cb) { - let packetHeader; - - const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later - - let importStats = { - areaSuccess : {}, // areaTag->count - areaFail : {}, // areaTag->count - otherFail : 0, - }; - - new ftnMailPacket.Packet(packetOpts).read(packetPath, (entryType, entryData, next) => { - if('header' === entryType) { - packetHeader = entryData; - - const localNetworkName = self.getNetworkNameByAddress(packetHeader.destAddress); - if(!_.isString(localNetworkName)) { - const addrString = new Address(packetHeader.destAddress).toString(); - return next(new Error(`No local configuration for packet addressed to ${addrString}`)); - } else { - - // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?! - return next(null); - } - - } else if('message' === entryType) { - const message = entryData; - const areaTag = message.meta.FtnProperty.ftn_area; - - let localAreaTag; - if(areaTag) { - localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag); - - if(!localAreaTag) { - // - // No local area configured for this import - // - // :TODO: Handle the "catch all" area bucket case if configured - Log.warn( { areaTag : areaTag }, 'No local area configured for this packet file!'); - - // bump generic failure - importStats.otherFail += 1; - - return next(null); - } - } else { - // - // No area tag: If marked private in attributes, this is a NetMail - // - if(message.meta.FtnProperty.ftn_attr_flags & ftnMailPacket.Packet.Attribute.Private) { - localAreaTag = Message.WellKnownAreaTags.Private; - } else { - Log.warn('Non-private message without area tag'); - importStats.otherFail += 1; - return next(null); - } - } - - message.uuid = Message.createMessageUUID( - localAreaTag, - message.modTimestamp, - message.subject, - message.message); - - self.appendTearAndOrigin(message); - - const importConfig = { - localAreaTag : localAreaTag, - }; - - self.importMailToArea(importConfig, packetHeader, message, err => { - if(err) { - // bump area fail stats - importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1; - - if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) { - const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A'; - Log.info( - { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId }, - 'Not importing non-unique message'); - - return next(null); - } - } else { - // bump area success - importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1; - } - - return next(err); - }); - } - }, err => { - // - // try to produce something helpful in the log - // - const finalStats = Object.assign(importStats, { packetPath : packetPath } ); - if(err || Object.keys(finalStats.areaFail).length > 0) { - if(err) { - Object.assign(finalStats, { error : err.message } ); - } - - Log.warn(finalStats, 'Import completed with error(s)'); - } else { - Log.info(finalStats, 'Import complete'); - } - - cb(err); - }); - }; - - this.maybeArchiveImportFile = function(origPath, type, status, cb) { - // - // type : pkt|tic|bundle - // status : good|reject - // - // Status of "good" is only applied to pkt files & placed - // in |retain| if set. This is generally used for debugging only. - // - let archivePath; - const ts = moment().format('YYYY-MM-DDTHH.mm.ss.SSS'); - const fn = paths.basename(origPath); - - if('good' === status && type === 'pkt') { - if(!_.isString(self.moduleConfig.paths.retain)) { - return cb(null); - } - - archivePath = paths.join(self.moduleConfig.paths.retain, `good-pkt-${ts}--${fn}`); - } else if('good' !== status) { - archivePath = paths.join(self.moduleConfig.paths.reject, `${status}-${type}--${ts}-${fn}`); - } else { - return cb(null); // don't archive non-good/pkt files - } - - Log.debug( { origPath : origPath, archivePath : archivePath, type : type, status : status }, 'Archiving import file'); - - fse.copy(origPath, archivePath, err => { - if(err) { - Log.warn( { error : err.message, origPath : origPath, archivePath : archivePath, type : type, status : status }, 'Failed to archive packet file'); - } - - return cb(null); // never fatal - }); - }; - - this.importPacketFilesFromDirectory = function(importDir, password, cb) { - async.waterfall( - [ - function getPacketFiles(callback) { - fs.readdir(importDir, (err, files) => { - if(err) { - return callback(err); - } - callback(null, files.filter(f => '.pkt' === paths.extname(f).toLowerCase())); - }); - }, - function importPacketFiles(packetFiles, callback) { - let rejects = []; - async.eachSeries(packetFiles, (packetFile, nextFile) => { - self.importMessagesFromPacketFile(paths.join(importDir, packetFile), '', err => { - if(err) { - Log.debug( - { path : paths.join(importDir, packetFile), error : err.toString() }, - 'Failed to import packet file'); - - rejects.push(packetFile); - } - nextFile(); - }); - }, err => { - // :TODO: Handle err! we should try to keep going though... - callback(err, packetFiles, rejects); - }); - }, - function handleProcessedFiles(packetFiles, rejects, callback) { - async.each(packetFiles, (packetFile, nextFile) => { - // possibly archive, then remove original - const fullPath = paths.join(importDir, packetFile); - self.maybeArchiveImportFile( - fullPath, - 'pkt', - rejects.includes(packetFile) ? 'reject' : 'good', - () => { - fs.unlink(fullPath, () => { - return nextFile(null); - }); - } - ); - }, err => { - callback(err); - }); - } - ], - err => { - cb(err); - } - ); - }; - - this.importFromDirectory = function(inboundType, importDir, cb) { - async.waterfall( - [ - // start with .pkt files - function importPacketFiles(callback) { - self.importPacketFilesFromDirectory(importDir, '', err => { - callback(err); - }); - }, - function discoverBundles(callback) { - fs.readdir(importDir, (err, files) => { - // :TODO: if we do much more of this, probably just use the glob module - const bundleRegExp = /\.(su|mo|tu|we|th|fr|sa)[0-9a-z]/i; - files = files.filter(f => { - const fext = paths.extname(f); - return bundleRegExp.test(fext); - }); - - async.map(files, (file, transform) => { - const fullPath = paths.join(importDir, file); - self.archUtil.detectType(fullPath, (err, archName) => { - transform(null, { path : fullPath, archName : archName } ); - }); - }, (err, bundleFiles) => { - callback(err, bundleFiles); - }); - }); - }, - function importBundles(bundleFiles, callback) { - let rejects = []; - - async.each(bundleFiles, (bundleFile, nextFile) => { - if(_.isUndefined(bundleFile.archName)) { - Log.warn( - { fileName : bundleFile.path }, - 'Unknown bundle archive type'); - - rejects.push(bundleFile.path); - - return nextFile(); // unknown archive type - } - - Log.debug( { bundleFile : bundleFile }, 'Processing bundle' ); - - self.archUtil.extractTo( - bundleFile.path, - self.importTempDir, - bundleFile.archName, - err => { - if(err) { - Log.warn( - { path : bundleFile.path, error : err.message }, - 'Failed to extract bundle'); - - rejects.push(bundleFile.path); - } - - nextFile(); - } - ); - }, err => { - if(err) { - return callback(err); - } - - // - // All extracted - import .pkt's - // - self.importPacketFilesFromDirectory(self.importTempDir, '', err => { - // :TODO: handle |err| - callback(null, bundleFiles, rejects); - }); - }); - }, - function handleProcessedBundleFiles(bundleFiles, rejects, callback) { - async.each(bundleFiles, (bundleFile, nextFile) => { - self.maybeArchiveImportFile( - bundleFile.path, - 'bundle', - rejects.includes(bundleFile.path) ? 'reject' : 'good', - () => { - fs.unlink(bundleFile.path, err => { - if(err) { - Log.error( { path : bundleFile.path, error : err.message }, 'Failed unlinking bundle'); - } - return nextFile(null); - }); - } - ); - }, err => { - callback(err); - }); - }, - function importTicFiles(callback) { - self.processTicFilesInDirectory(importDir, err => { - return callback(err); - }); - } - ], - err => { - cb(err); - } - ); - }; - - this.createTempDirectories = function(cb) { - temptmp.mkdir( { prefix : 'enigftnexport-' }, (err, tempDir) => { - if(err) { - return cb(err); - } - - self.exportTempDir = tempDir; - - temptmp.mkdir( { prefix : 'enigftnimport-' }, (err, tempDir) => { - self.importTempDir = tempDir; - - cb(err); - }); - }); - }; - - // Starts an export block - returns true if we can proceed - this.exportingStart = function() { - if(!this.exportRunning) { - this.exportRunning = true; - return true; - } - - return false; - }; - - // ends an export block - this.exportingEnd = function(cb) { - this.exportRunning = false; - - if(cb) { - return cb(null); - } - }; - - this.copyTicAttachment = function(src, dst, isUpdate, cb) { - if(isUpdate) { - fse.copy(src, dst, { overwrite : true }, err => { - return cb(err, dst); - }); - } else { - copyFileWithCollisionHandling(src, dst, (err, finalPath) => { - return cb(err, finalPath); - }); - } - }; - - this.getLocalAreaTagsForTic = function() { - const config = Config(); - return _.union(Object.keys(config.scannerTossers.ftn_bso.ticAreas || {} ), Object.keys(config.fileBase.areas)); - }; - - this.processSingleTicFile = function(ticFileInfo, cb) { - Log.debug( { tic : ticFileInfo.path, file : ticFileInfo.getAsString('File') }, 'Processing TIC file'); - - async.waterfall( - [ - function generalValidation(callback) { - const sysConfig = Config(); - const config = { - nodes : sysConfig.scannerTossers.ftn_bso.nodes, - defaultPassword : sysConfig.scannerTossers.ftn_bso.tic.password, - localAreaTags : self.getLocalAreaTagsForTic(), - }; - - return ticFileInfo.validate(config, (err, localInfo) => { - if(err) { - Log.trace( { reason : err.message }, 'Validation failure'); - return callback(err); - } - - // We may need to map |localAreaTag| back to real areaTag if it's a mapping/alias - const mappedLocalAreaTag = _.get(Config().scannerTossers.ftn_bso, [ 'ticAreas', localInfo.areaTag ]); - - if(mappedLocalAreaTag) { - if(_.isString(mappedLocalAreaTag.areaTag)) { - localInfo.areaTag = mappedLocalAreaTag.areaTag; - localInfo.hashTags = mappedLocalAreaTag.hashTags; // override default for node - localInfo.storageTag = mappedLocalAreaTag.storageTag; // override default - } else if(_.isString(mappedLocalAreaTag)) { - localInfo.areaTag = mappedLocalAreaTag; - } - } - - return callback(null, localInfo); - }); - }, - function findExistingItem(localInfo, callback) { - // - // We will need to look for an existing item to replace/update if: - // a) The TIC file has a "Replaces" field - // b) The general or node specific |allowReplace| is true - // - // Replace specifies a DOS 8.3 *pattern* which is allowed to have - // ? and * characters. For example, RETRONET.* - // - // Lastly, we will only replace if the item is in the same/specified area - // and that come from the same origin as a previous entry. - // - const allowReplace = _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config().scannerTossers.ftn_bso.tic.allowReplace); - const replaces = ticFileInfo.getAsString('Replaces'); - - if(!allowReplace || !replaces) { - return callback(null, localInfo); - } - - const metaPairs = [ - { - name : 'short_file_name', - value : replaces.toUpperCase(), // we store upper as well - wildcards : true, // value may contain wildcards - }, - { - name : 'tic_origin', - value : ticFileInfo.getAsString('Origin'), - } - ]; - - FileEntry.findFiles( { metaPairs : metaPairs, areaTag : localInfo.areaTag }, (err, fileIds) => { - if(err) { - return callback(err); - } - - // 0:1 allowed - if(1 === fileIds.length) { - localInfo.existingFileId = fileIds[0]; - - // fetch old filename - we may need to remove it if replacing with a new name - FileEntry.loadBasicEntry(localInfo.existingFileId, {}, (err, info) => { - if(info) { - Log.trace( - { fileId : localInfo.existingFileId, oldFileName : info.fileName, oldStorageTag : info.storageTag }, - 'Existing TIC file target to be replaced' - ); - - localInfo.oldFileName = info.fileName; - localInfo.oldStorageTag = info.storageTag; - } - return callback(null, localInfo); // continue even if we couldn't find an old match - }); - } else if(fileIds.legnth > 1) { - return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`)); - } else { - return callback(null, localInfo); - } - }); - }, - function scan(localInfo, callback) { - const scanOpts = { - sha256 : localInfo.sha256, // *may* have already been calculated - meta : { - // some TIC-related metadata we always want - short_file_name : ticFileInfo.getAsString('File').toUpperCase(), // upper to ensure no case issues later; this should be a DOS 8.3 name - tic_origin : ticFileInfo.getAsString('Origin'), - tic_desc : ticFileInfo.getAsString('Desc'), - upload_by_username : _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config().scannerTossers.ftn_bso.tic.uploadBy), - } - }; - - const ldesc = ticFileInfo.getAsString('Ldesc', '\n'); - if(ldesc) { - scanOpts.meta.tic_ldesc = ldesc; - } - - // - // We may have TIC auto-tagging for this node and/or specific (remote) area - // - const hashTags = + msgDb.run(sql, [ areaTag, lastScanId ], err => { + return cb(err); + }); + }; + + this.getNodeConfigByAddress = function(addr) { + addr = _.isString(addr) ? Address.fromString(addr) : addr; + + // :TODO: sort wildcard nodes{} entries by most->least explicit according to FTN hierarchy + return _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { + return addr.isPatternMatch(nodeAddrWildcard); + }); + }; + + this.exportNetMailMessagePacket = function(message, exportOpts, cb) { + // + // For NetMail, we always create a *single* packet per message. + // + async.series( + [ + function generalPrep(callback) { + self.prepareMessage(message, exportOpts); + + return self.setReplyKludgeFromReplyToMsgId(message, callback); + }, + function createPacket(callback) { + const packet = new ftnMailPacket.Packet(); + + const packetHeader = new ftnMailPacket.PacketHeader( + exportOpts.network.localAddress, + exportOpts.routeAddress, + exportOpts.nodeConfig.packetType + ); + + packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; + + // use current message ID for filename seed + exportOpts.pktFileName = self.getOutgoingPacketFileName( + self.exportTempDir, + message.messageId, + false, // createTempPacket=false + exportOpts.fileCase + ); + + const ws = fs.createWriteStream(exportOpts.pktFileName); + + packet.writeHeader(ws, packetHeader); + + packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => { + if(err) { + return callback(err); + } + + ws.write(msgBuf); + + packet.writeTerminator(ws); + + ws.end(); + ws.once('finish', () => { + return callback(null); + }); + }); + } + ], + err => { + return cb(err); + } + ); + }; + + this.exportMessagesByUuid = function(messageUuids, exportOpts, cb) { + // + // This method has a lot of madness going on: + // - Try to stuff messages into packets until we've hit the target size + // - We need to wait for write streams to finish before proceeding in many cases + // or data will be cut off when closing and creating a new stream + // + let exportedFiles = []; + let currPacketSize = self.moduleConfig.packetTargetByteSize; + let packet; + let ws; + let remainMessageBuf; + let remainMessageId; + const createTempPacket = !_.isString(exportOpts.nodeConfig.archiveType) || 0 === exportOpts.nodeConfig.archiveType.length; + + function finalizePacket(cb) { + packet.writeTerminator(ws); + ws.end(); + ws.once('finish', () => { + return cb(null); + }); + } + + async.each(messageUuids, (msgUuid, nextUuid) => { + let message = new Message(); + + async.series( + [ + function finalizePrevious(callback) { + if(packet && currPacketSize >= self.moduleConfig.packetTargetByteSize) { + return finalizePacket(callback); + } else { + callback(null); + } + }, + function loadMessage(callback) { + message.load( { uuid : msgUuid }, err => { + if(err) { + return callback(err); + } + + // General preperation + self.prepareMessage(message, exportOpts); + + self.setReplyKludgeFromReplyToMsgId(message, err => { + callback(err); + }); + }); + }, + function createNewPacket(callback) { + if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { + packet = new ftnMailPacket.Packet(); + + const packetHeader = new ftnMailPacket.PacketHeader( + exportOpts.network.localAddress, + exportOpts.destAddress, + exportOpts.nodeConfig.packetType); + + packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; + + // use current message ID for filename seed + const pktFileName = self.getOutgoingPacketFileName( + self.exportTempDir, + message.messageId, + createTempPacket, + exportOpts.fileCase + ); + + exportedFiles.push(pktFileName); + + ws = fs.createWriteStream(pktFileName); + + currPacketSize = packet.writeHeader(ws, packetHeader); + + if(remainMessageBuf) { + currPacketSize += packet.writeMessageEntry(ws, remainMessageBuf); + remainMessageBuf = null; + } + } + + callback(null); + }, + function appendMessage(callback) { + packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => { + if(err) { + return callback(err); + } + + currPacketSize += msgBuf.length; + + if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { + remainMessageBuf = msgBuf; // save for next packet + remainMessageId = message.messageId; + } else { + ws.write(msgBuf); + } + + return callback(null); + }); + }, + function storeStateFlags0Meta(callback) { + message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), err => { + callback(err); + }); + }, + function storeMsgIdMeta(callback) { + // + // We want to store some meta as if we had imported + // this message for later reference + // + if(message.meta.FtnKludge.MSGID) { + message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, err => { + callback(err); + }); + } else { + callback(null); + } + } + ], + err => { + nextUuid(err); + } + ); + }, err => { + if(err) { + cb(err); + } else { + async.series( + [ + function terminateLast(callback) { + if(packet) { + return finalizePacket(callback); + } else { + callback(null); + } + }, + function writeRemainPacket(callback) { + if(remainMessageBuf) { + // :TODO: DRY this with the code above -- they are basically identical + packet = new ftnMailPacket.Packet(); + + const packetHeader = new ftnMailPacket.PacketHeader( + exportOpts.network.localAddress, + exportOpts.destAddress, + exportOpts.nodeConfig.packetType); + + packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; + + // use current message ID for filename seed + const pktFileName = self.getOutgoingPacketFileName( + self.exportTempDir, + remainMessageId, + createTempPacket, + exportOpts.filleCase + ); + + exportedFiles.push(pktFileName); + + ws = fs.createWriteStream(pktFileName); + + packet.writeHeader(ws, packetHeader); + ws.write(remainMessageBuf); + return finalizePacket(callback); + } else { + callback(null); + } + } + ], + err => { + cb(err, exportedFiles); + } + ); + } + }); + }; + + this.getNetMailRoute = function(dstAddr) { + // + // Route full|wildcard -> full adddress/network lookup + // + const routes = _.get(Config(), 'scannerTossers.ftn_bso.netMail.routes'); + if(!routes) { + return; + } + + return _.find(routes, (route, addrWildcard) => { + return dstAddr.isPatternMatch(addrWildcard); + }); + }; + + this.getNetMailRouteInfoFromAddress = function(destAddress, cb) { + // + // Attempt to find route information for |destAddress|: + // + // 1) Routes: scannerTossers.ftn_bso.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config + // - Where we send may not be where destAddress is (it's routed!) + // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config + // - Where we send is direct to destAddress + // + // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address + // falling back to Config.scannerTossers.ftn_bso.defaultNetwork + // + const route = this.getNetMailRoute(destAddress); + + let routeAddress; + let networkName; + let isRouted; + if(route) { + routeAddress = Address.fromString(route.address); + networkName = route.network; + isRouted = true; + } else { + routeAddress = destAddress; + isRouted = false; + } + + networkName = networkName || this.getNetworkNameByAddress(routeAddress); + + const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { + return routeAddress.isPatternMatch(nodeAddrWildcard); + }) || { packetType : '2+', encoding : Config().scannerTossers.ftn_bso.packetMsgEncoding }; + + // we should never be failing here; we may just be using defaults. + return cb( + networkName ? null : Errors.DoesNotExist(`No NetMail route for ${destAddress.toString()}`), + { destAddress, routeAddress, networkName, config, isRouted } + ); + }; + + this.exportNetMailMessagesToUplinks = function(messagesOrMessageUuids, cb) { + // for each message/UUID, find where to send the thing + async.each(messagesOrMessageUuids, (msgOrUuid, nextMessageOrUuid) => { + + const exportOpts = {}; + const message = new Message(); + + async.series( + [ + function loadMessage(callback) { + if(_.isString(msgOrUuid)) { + message.load( { uuid : msgOrUuid }, err => { + return callback(err, message); + }); + } else { + return callback(null, msgOrUuid); + } + }, + function discoverUplink(callback) { + const dstAddr = new Address(message.meta.System[Message.SystemMetaNames.RemoteToUser]); + + self.getNetMailRouteInfoFromAddress(dstAddr, (err, routeInfo) => { + if(err) { + return callback(err); + } + + exportOpts.nodeConfig = routeInfo.config; + exportOpts.destAddress = dstAddr; + exportOpts.routeAddress = routeInfo.routeAddress; + exportOpts.fileCase = routeInfo.config.fileCase || 'lower'; + exportOpts.network = Config().messageNetworks.ftn.networks[routeInfo.networkName]; + exportOpts.networkName = routeInfo.networkName; + exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); + exportOpts.exportType = self.getExportType(routeInfo.config); + + if(!exportOpts.network) { + return callback(Errors.DoesNotExist(`No configuration found for network ${routeInfo.networkName}`)); + } + + return callback(null); + }); + }, + function createOutgoingDir(callback) { + // ensure outgoing NetMail directory exists + return fse.mkdirs(exportOpts.outgoingDir, callback); + }, + function exportPacket(callback) { + return self.exportNetMailMessagePacket(message, exportOpts, callback); + }, + function moveToOutgoing(callback) { + const newExt = exportOpts.fileCase === 'lower' ? '.pkt' : '.PKT'; + exportOpts.exportedToPath = paths.join( + exportOpts.outgoingDir, + `${paths.basename(exportOpts.pktFileName, paths.extname(exportOpts.pktFileName))}${newExt}` + ); + + return fse.move(exportOpts.pktFileName, exportOpts.exportedToPath, callback); + }, + function prepareFloFile(callback) { + const flowFilePath = self.getOutgoingFlowFileName( + exportOpts.outgoingDir, + exportOpts.routeAddress, + 'ref', + exportOpts.exportType, + exportOpts.fileCase + ); + + return self.flowFileAppendRefs(flowFilePath, [ exportOpts.exportedToPath ], '^', callback); + }, + function storeStateFlags0Meta(callback) { + return message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), callback); + }, + function storeMsgIdMeta(callback) { + // Store meta as if we had imported this message -- for later reference + if(message.meta.FtnKludge.MSGID) { + return message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, callback); + } + + return callback(null); + } + ], + err => { + if(err) { + Log.warn( { error : err.message }, 'Error exporting message' ); + } + return nextMessageOrUuid(null); + } + ); + }, err => { + if(err) { + Log.warn( { error : err.message }, 'Error(s) during NetMail export'); + } + return cb(err); + }); + }; + + this.exportEchoMailMessagesToUplinks = function(messageUuids, areaConfig, cb) { + const config = Config(); + async.each(areaConfig.uplinks, (uplink, nextUplink) => { + const nodeConfig = self.getNodeConfigByAddress(uplink); + if(!nodeConfig) { + return nextUplink(); + } + + const exportOpts = { + nodeConfig, + network : config.messageNetworks.ftn.networks[areaConfig.network], + destAddress : Address.fromString(uplink), + networkName : areaConfig.network, + fileCase : nodeConfig.fileCase || 'lower', + }; + + if(_.isString(exportOpts.network.localAddress)) { + exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); + } + + const outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); + const exportType = self.getExportType(exportOpts.nodeConfig); + + async.waterfall( + [ + function createOutgoingDir(callback) { + fse.mkdirs(outgoingDir, err => { + callback(err); + }); + }, + function exportToTempArea(callback) { + self.exportMessagesByUuid(messageUuids, exportOpts, callback); + }, + function createArcMailBundle(exportedFileNames, callback) { + if(self.archUtil.haveArchiver(exportOpts.nodeConfig.archiveType)) { + // :TODO: support bundleTargetByteSize: + // + // Compress to a temp location then we'll move it in the next step + // + // Note that we must use the *final* output dir for getOutgoingBundleFileName() + // as it checks for collisions in bundle names! + // + self.getOutgoingBundleFileName(outgoingDir, exportOpts.network.localAddress, exportOpts.destAddress, (err, bundlePath) => { + if(err) { + return callback(err); + } + + // adjust back to temp path + const tempBundlePath = paths.join(self.exportTempDir, paths.basename(bundlePath)); + + self.archUtil.compressTo( + exportOpts.nodeConfig.archiveType, + tempBundlePath, + exportedFileNames, err => { + callback(err, [ tempBundlePath ] ); + } + ); + }); + } else { + callback(null, exportedFileNames); + } + }, + function moveFilesToOutgoing(exportedFileNames, callback) { + async.each(exportedFileNames, (oldPath, nextFile) => { + const ext = paths.extname(oldPath).toLowerCase(); + if('.pk_' === ext.toLowerCase()) { + // + // For a given temporary .pk_ file, we need to move it to the outoing + // directory with the appropriate BSO style filename. + // + const newExt = self.getOutgoingFlowFileExtension( + exportOpts.destAddress, + 'mail', + exportType, + exportOpts.fileCase + ); + + const newPath = paths.join( + outgoingDir, + `${paths.basename(oldPath, ext)}${newExt}`); + + fse.move(oldPath, newPath, nextFile); + } else { + const newPath = paths.join(outgoingDir, paths.basename(oldPath)); + fse.move(oldPath, newPath, err => { + if(err) { + Log.warn( + { oldPath : oldPath, newPath : newPath, error : err.toString() }, + 'Failed moving temporary bundle file!'); + + return nextFile(); + } + + // + // For bundles, we need to append to the appropriate flow file + // + const flowFilePath = self.getOutgoingFlowFileName( + outgoingDir, + exportOpts.destAddress, + 'ref', + exportType, + exportOpts.fileCase + ); + + // directive of '^' = delete file after transfer + self.flowFileAppendRefs(flowFilePath, [ newPath ], '^', err => { + if(err) { + Log.warn( { path : flowFilePath }, 'Failed appending flow reference record!'); + } + nextFile(); + }); + }); + } + }, callback); + } + ], + err => { + // :TODO: do something with |err| ? + if(err) { + Log.warn(err.message); + } + nextUplink(); + } + ); + }, cb); // complete + }; + + this.setReplyToMsgIdFtnReplyKludge = function(message, cb) { + // + // Given a FTN REPLY kludge, set |message.replyToMsgId|, if possible, + // by looking up an associated MSGID kludge meta. + // + // See also: http://ftsc.org/docs/fts-0009.001 + // + if(!_.isString(message.meta.FtnKludge.REPLY)) { + // nothing to do + return cb(); + } + + Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => { + if(msgIds && msgIds.length > 0) { + // expect a single match, but dupe checking is not perfect - warn otherwise + if(1 === msgIds.length) { + message.replyToMsgId = msgIds[0]; + } else { + Log.warn( { msgIds : msgIds, replyKludge : message.meta.FtnKludge.REPLY }, 'Found 2:n MSGIDs matching REPLY kludge!'); + } + } + cb(); + }); + }; + + this.getLocalUserNameFromAlias = function(lookup) { + lookup = lookup.toLowerCase(); + + const aliases = _.get(Config(), 'messageNetworks.ftn.netMail.aliases'); + if(!aliases) { + return lookup; // keep orig + } + + const alias = _.find(aliases, (localName, alias) => { + return alias.toLowerCase() === lookup; + }); + + return alias || lookup; + }; + + this.getAddressesFromNetMailMessage = function(message) { + const intlKludge = _.get(message, 'meta.FtnKludge.INTL'); + + if(!intlKludge) { + return {}; + } + + let [ to, from ] = intlKludge.split(' '); + if(!to || !from) { + return {}; + } + + const fromPoint = _.get(message, 'meta.FtnKludge.FMPT'); + const toPoint = _.get(message, 'meta.FtnKludge.TOPT'); + + if(fromPoint) { + from += `.${fromPoint}`; + } + + if(toPoint) { + to += `.${toPoint}`; + } + + return { to : Address.fromString(to), from : Address.fromString(from) }; + }; + + this.importMailToArea = function(config, header, message, cb) { + async.series( + [ + function validateDestinationAddress(callback) { + const localNetworkPattern = `${message.meta.FtnProperty.ftn_dest_network}/${message.meta.FtnProperty.ftn_dest_node}`; + const localNetworkName = self.getNetworkNameByAddressPattern(localNetworkPattern); + + return callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us')); + }, + function checkForDupeMSGID(callback) { + // + // If we have a MSGID, don't allow a dupe + // + if(!_.has(message.meta, 'FtnKludge.MSGID')) { + return callback(null); + } + + Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, (err, msgIds) => { + if(msgIds && msgIds.length > 0) { + const err = new Error('Duplicate MSGID'); + err.code = 'DUPE_MSGID'; + return callback(err); + } + + return callback(null); + }); + }, + function basicSetup(callback) { + message.areaTag = config.localAreaTag; + + // indicate this was imported from FTN + message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.FTN; + + // + // If we *allow* dupes (disabled by default), then just generate + // a random UUID. Otherwise, don't assign the UUID just yet. It will be + // generated at persist() time and should be consistent across import/exports + // + if(true === _.get(Config(), [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) { + // just generate a UUID & therefor always allow for dupes + message.uuid = uuidV4(); + } + + return callback(null); + }, + function setReplyToMessageId(callback) { + self.setReplyToMsgIdFtnReplyKludge(message, () => { + return callback(null); + }); + }, + function setupPrivateMessage(callback) { + // + // If this is a private message (e.g. NetMail) we set the local user ID + // + if(Message.WellKnownAreaTags.Private !== config.localAreaTag) { + return callback(null); + } + + // + // Create a meta value for the *remote* from user. In the case here with FTN, + // their fully qualified FTN from address + // + const { from } = self.getAddressesFromNetMailMessage(message); + + if(!from) { + return callback(Errors.Invalid('Cannot import FTN NetMail without valid INTL line')); + } + + message.meta.System[Message.SystemMetaNames.RemoteFromUser] = from.toString(); + + const lookupName = self.getLocalUserNameFromAlias(message.toUserName); + + User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { + if(err) { + // + // Couldn't find a local username. If the toUserName itself is a FTN address + // we can only assume the message is to the +op, else we'll have to fail. + // + const toUserNameAsAddress = Address.fromString(message.toUserName); + if(toUserNameAsAddress.isValid()) { + + Log.info( + { toUserName : message.toUserName, fromUserName : message.fromUserName }, + 'No local "to" username for FTN message. Appears to be a FTN address only; assuming addressed to SysOp' + ); + + User.getUserName(User.RootUserID, (err, sysOpUserName) => { + if(err) { + return callback(Errors.UnexpectedState('Failed to get SysOp user information')); + } + + message.meta.System[Message.SystemMetaNames.LocalToUserID] = User.RootUserID; + message.toUserName = sysOpUserName; + return callback(null); + }); + } else { + return callback(Errors.DoesNotExist(`Could not get local user ID for "${message.toUserName}": ${err.message}`)); + } + } + + // we do this after such that error cases can be preseved above + if(lookupName !== message.toUserName) { + message.toUserName = localUserName; + } + + // set the meta information - used elsehwere for retrieval + message.meta.System[Message.SystemMetaNames.LocalToUserID] = localToUserId; + return callback(null); + }); + }, + function persistImport(callback) { + // mark as imported + message.meta.System.state_flags0 = Message.StateFlags0.Imported.toString(); + + // save to disc + message.persist(err => { + return callback(err); + }); + } + ], + err => { + cb(err); + } + ); + }; + + this.appendTearAndOrigin = function(message) { + if(message.meta.FtnProperty.ftn_tear_line) { + message.message += `\r\n${message.meta.FtnProperty.ftn_tear_line}\r\n`; + } + + if(message.meta.FtnProperty.ftn_origin) { + message.message += `${message.meta.FtnProperty.ftn_origin}\r\n`; + } + }; + + // + // Ref. implementations on import: + // * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c + // https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/handle.c + // + this.importMessagesFromPacketFile = function(packetPath, password, cb) { + let packetHeader; + + const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later + + let importStats = { + areaSuccess : {}, // areaTag->count + areaFail : {}, // areaTag->count + otherFail : 0, + }; + + new ftnMailPacket.Packet(packetOpts).read(packetPath, (entryType, entryData, next) => { + if('header' === entryType) { + packetHeader = entryData; + + const localNetworkName = self.getNetworkNameByAddress(packetHeader.destAddress); + if(!_.isString(localNetworkName)) { + const addrString = new Address(packetHeader.destAddress).toString(); + return next(new Error(`No local configuration for packet addressed to ${addrString}`)); + } else { + + // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?! + return next(null); + } + + } else if('message' === entryType) { + const message = entryData; + const areaTag = message.meta.FtnProperty.ftn_area; + + let localAreaTag; + if(areaTag) { + localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag); + + if(!localAreaTag) { + // + // No local area configured for this import + // + // :TODO: Handle the "catch all" area bucket case if configured + Log.warn( { areaTag : areaTag }, 'No local area configured for this packet file!'); + + // bump generic failure + importStats.otherFail += 1; + + return next(null); + } + } else { + // + // No area tag: If marked private in attributes, this is a NetMail + // + if(message.meta.FtnProperty.ftn_attr_flags & ftnMailPacket.Packet.Attribute.Private) { + localAreaTag = Message.WellKnownAreaTags.Private; + } else { + Log.warn('Non-private message without area tag'); + importStats.otherFail += 1; + return next(null); + } + } + + message.uuid = Message.createMessageUUID( + localAreaTag, + message.modTimestamp, + message.subject, + message.message); + + self.appendTearAndOrigin(message); + + const importConfig = { + localAreaTag : localAreaTag, + }; + + self.importMailToArea(importConfig, packetHeader, message, err => { + if(err) { + // bump area fail stats + importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1; + + if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) { + const msgId = _.has(message.meta, 'FtnKludge.MSGID') ? message.meta.FtnKludge.MSGID : 'N/A'; + Log.info( + { area : localAreaTag, subject : message.subject, uuid : message.uuid, MSGID : msgId }, + 'Not importing non-unique message'); + + return next(null); + } + } else { + // bump area success + importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1; + } + + return next(err); + }); + } + }, err => { + // + // try to produce something helpful in the log + // + const finalStats = Object.assign(importStats, { packetPath : packetPath } ); + if(err || Object.keys(finalStats.areaFail).length > 0) { + if(err) { + Object.assign(finalStats, { error : err.message } ); + } + + Log.warn(finalStats, 'Import completed with error(s)'); + } else { + Log.info(finalStats, 'Import complete'); + } + + cb(err); + }); + }; + + this.maybeArchiveImportFile = function(origPath, type, status, cb) { + // + // type : pkt|tic|bundle + // status : good|reject + // + // Status of "good" is only applied to pkt files & placed + // in |retain| if set. This is generally used for debugging only. + // + let archivePath; + const ts = moment().format('YYYY-MM-DDTHH.mm.ss.SSS'); + const fn = paths.basename(origPath); + + if('good' === status && type === 'pkt') { + if(!_.isString(self.moduleConfig.paths.retain)) { + return cb(null); + } + + archivePath = paths.join(self.moduleConfig.paths.retain, `good-pkt-${ts}--${fn}`); + } else if('good' !== status) { + archivePath = paths.join(self.moduleConfig.paths.reject, `${status}-${type}--${ts}-${fn}`); + } else { + return cb(null); // don't archive non-good/pkt files + } + + Log.debug( { origPath : origPath, archivePath : archivePath, type : type, status : status }, 'Archiving import file'); + + fse.copy(origPath, archivePath, err => { + if(err) { + Log.warn( { error : err.message, origPath : origPath, archivePath : archivePath, type : type, status : status }, 'Failed to archive packet file'); + } + + return cb(null); // never fatal + }); + }; + + this.importPacketFilesFromDirectory = function(importDir, password, cb) { + async.waterfall( + [ + function getPacketFiles(callback) { + fs.readdir(importDir, (err, files) => { + if(err) { + return callback(err); + } + callback(null, files.filter(f => '.pkt' === paths.extname(f).toLowerCase())); + }); + }, + function importPacketFiles(packetFiles, callback) { + let rejects = []; + async.eachSeries(packetFiles, (packetFile, nextFile) => { + self.importMessagesFromPacketFile(paths.join(importDir, packetFile), '', err => { + if(err) { + Log.debug( + { path : paths.join(importDir, packetFile), error : err.toString() }, + 'Failed to import packet file'); + + rejects.push(packetFile); + } + nextFile(); + }); + }, err => { + // :TODO: Handle err! we should try to keep going though... + callback(err, packetFiles, rejects); + }); + }, + function handleProcessedFiles(packetFiles, rejects, callback) { + async.each(packetFiles, (packetFile, nextFile) => { + // possibly archive, then remove original + const fullPath = paths.join(importDir, packetFile); + self.maybeArchiveImportFile( + fullPath, + 'pkt', + rejects.includes(packetFile) ? 'reject' : 'good', + () => { + fs.unlink(fullPath, () => { + return nextFile(null); + }); + } + ); + }, err => { + callback(err); + }); + } + ], + err => { + cb(err); + } + ); + }; + + this.importFromDirectory = function(inboundType, importDir, cb) { + async.waterfall( + [ + // start with .pkt files + function importPacketFiles(callback) { + self.importPacketFilesFromDirectory(importDir, '', err => { + callback(err); + }); + }, + function discoverBundles(callback) { + fs.readdir(importDir, (err, files) => { + // :TODO: if we do much more of this, probably just use the glob module + const bundleRegExp = /\.(su|mo|tu|we|th|fr|sa)[0-9a-z]/i; + files = files.filter(f => { + const fext = paths.extname(f); + return bundleRegExp.test(fext); + }); + + async.map(files, (file, transform) => { + const fullPath = paths.join(importDir, file); + self.archUtil.detectType(fullPath, (err, archName) => { + transform(null, { path : fullPath, archName : archName } ); + }); + }, (err, bundleFiles) => { + callback(err, bundleFiles); + }); + }); + }, + function importBundles(bundleFiles, callback) { + let rejects = []; + + async.each(bundleFiles, (bundleFile, nextFile) => { + if(_.isUndefined(bundleFile.archName)) { + Log.warn( + { fileName : bundleFile.path }, + 'Unknown bundle archive type'); + + rejects.push(bundleFile.path); + + return nextFile(); // unknown archive type + } + + Log.debug( { bundleFile : bundleFile }, 'Processing bundle' ); + + self.archUtil.extractTo( + bundleFile.path, + self.importTempDir, + bundleFile.archName, + err => { + if(err) { + Log.warn( + { path : bundleFile.path, error : err.message }, + 'Failed to extract bundle'); + + rejects.push(bundleFile.path); + } + + nextFile(); + } + ); + }, err => { + if(err) { + return callback(err); + } + + // + // All extracted - import .pkt's + // + self.importPacketFilesFromDirectory(self.importTempDir, '', err => { + // :TODO: handle |err| + callback(null, bundleFiles, rejects); + }); + }); + }, + function handleProcessedBundleFiles(bundleFiles, rejects, callback) { + async.each(bundleFiles, (bundleFile, nextFile) => { + self.maybeArchiveImportFile( + bundleFile.path, + 'bundle', + rejects.includes(bundleFile.path) ? 'reject' : 'good', + () => { + fs.unlink(bundleFile.path, err => { + if(err) { + Log.error( { path : bundleFile.path, error : err.message }, 'Failed unlinking bundle'); + } + return nextFile(null); + }); + } + ); + }, err => { + callback(err); + }); + }, + function importTicFiles(callback) { + self.processTicFilesInDirectory(importDir, err => { + return callback(err); + }); + } + ], + err => { + cb(err); + } + ); + }; + + this.createTempDirectories = function(cb) { + temptmp.mkdir( { prefix : 'enigftnexport-' }, (err, tempDir) => { + if(err) { + return cb(err); + } + + self.exportTempDir = tempDir; + + temptmp.mkdir( { prefix : 'enigftnimport-' }, (err, tempDir) => { + self.importTempDir = tempDir; + + cb(err); + }); + }); + }; + + // Starts an export block - returns true if we can proceed + this.exportingStart = function() { + if(!this.exportRunning) { + this.exportRunning = true; + return true; + } + + return false; + }; + + // ends an export block + this.exportingEnd = function(cb) { + this.exportRunning = false; + + if(cb) { + return cb(null); + } + }; + + this.copyTicAttachment = function(src, dst, isUpdate, cb) { + if(isUpdate) { + fse.copy(src, dst, { overwrite : true }, err => { + return cb(err, dst); + }); + } else { + copyFileWithCollisionHandling(src, dst, (err, finalPath) => { + return cb(err, finalPath); + }); + } + }; + + this.getLocalAreaTagsForTic = function() { + const config = Config(); + return _.union(Object.keys(config.scannerTossers.ftn_bso.ticAreas || {} ), Object.keys(config.fileBase.areas)); + }; + + this.processSingleTicFile = function(ticFileInfo, cb) { + Log.debug( { tic : ticFileInfo.path, file : ticFileInfo.getAsString('File') }, 'Processing TIC file'); + + async.waterfall( + [ + function generalValidation(callback) { + const sysConfig = Config(); + const config = { + nodes : sysConfig.scannerTossers.ftn_bso.nodes, + defaultPassword : sysConfig.scannerTossers.ftn_bso.tic.password, + localAreaTags : self.getLocalAreaTagsForTic(), + }; + + return ticFileInfo.validate(config, (err, localInfo) => { + if(err) { + Log.trace( { reason : err.message }, 'Validation failure'); + return callback(err); + } + + // We may need to map |localAreaTag| back to real areaTag if it's a mapping/alias + const mappedLocalAreaTag = _.get(Config().scannerTossers.ftn_bso, [ 'ticAreas', localInfo.areaTag ]); + + if(mappedLocalAreaTag) { + if(_.isString(mappedLocalAreaTag.areaTag)) { + localInfo.areaTag = mappedLocalAreaTag.areaTag; + localInfo.hashTags = mappedLocalAreaTag.hashTags; // override default for node + localInfo.storageTag = mappedLocalAreaTag.storageTag; // override default + } else if(_.isString(mappedLocalAreaTag)) { + localInfo.areaTag = mappedLocalAreaTag; + } + } + + return callback(null, localInfo); + }); + }, + function findExistingItem(localInfo, callback) { + // + // We will need to look for an existing item to replace/update if: + // a) The TIC file has a "Replaces" field + // b) The general or node specific |allowReplace| is true + // + // Replace specifies a DOS 8.3 *pattern* which is allowed to have + // ? and * characters. For example, RETRONET.* + // + // Lastly, we will only replace if the item is in the same/specified area + // and that come from the same origin as a previous entry. + // + const allowReplace = _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config().scannerTossers.ftn_bso.tic.allowReplace); + const replaces = ticFileInfo.getAsString('Replaces'); + + if(!allowReplace || !replaces) { + return callback(null, localInfo); + } + + const metaPairs = [ + { + name : 'short_file_name', + value : replaces.toUpperCase(), // we store upper as well + wildcards : true, // value may contain wildcards + }, + { + name : 'tic_origin', + value : ticFileInfo.getAsString('Origin'), + } + ]; + + FileEntry.findFiles( { metaPairs : metaPairs, areaTag : localInfo.areaTag }, (err, fileIds) => { + if(err) { + return callback(err); + } + + // 0:1 allowed + if(1 === fileIds.length) { + localInfo.existingFileId = fileIds[0]; + + // fetch old filename - we may need to remove it if replacing with a new name + FileEntry.loadBasicEntry(localInfo.existingFileId, {}, (err, info) => { + if(info) { + Log.trace( + { fileId : localInfo.existingFileId, oldFileName : info.fileName, oldStorageTag : info.storageTag }, + 'Existing TIC file target to be replaced' + ); + + localInfo.oldFileName = info.fileName; + localInfo.oldStorageTag = info.storageTag; + } + return callback(null, localInfo); // continue even if we couldn't find an old match + }); + } else if(fileIds.legnth > 1) { + return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`)); + } else { + return callback(null, localInfo); + } + }); + }, + function scan(localInfo, callback) { + const scanOpts = { + sha256 : localInfo.sha256, // *may* have already been calculated + meta : { + // some TIC-related metadata we always want + short_file_name : ticFileInfo.getAsString('File').toUpperCase(), // upper to ensure no case issues later; this should be a DOS 8.3 name + tic_origin : ticFileInfo.getAsString('Origin'), + tic_desc : ticFileInfo.getAsString('Desc'), + upload_by_username : _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config().scannerTossers.ftn_bso.tic.uploadBy), + } + }; + + const ldesc = ticFileInfo.getAsString('Ldesc', '\n'); + if(ldesc) { + scanOpts.meta.tic_ldesc = ldesc; + } + + // + // We may have TIC auto-tagging for this node and/or specific (remote) area + // + const hashTags = localInfo.hashTags || _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/ - if(hashTags) { - scanOpts.hashTags = new Set(hashTags.split(/[\s,]+/)); - } + if(hashTags) { + scanOpts.hashTags = new Set(hashTags.split(/[\s,]+/)); + } - if(localInfo.crc32) { - scanOpts.meta.file_crc32 = localInfo.crc32.toString(16); // again, *may* have already been calculated - } + if(localInfo.crc32) { + scanOpts.meta.file_crc32 = localInfo.crc32.toString(16); // again, *may* have already been calculated + } - scanFile( - ticFileInfo.filePath, - scanOpts, - (err, fileEntry) => { - if(err) { - Log.trace( { reason : err.message }, 'Scanning failed'); - } + scanFile( + ticFileInfo.filePath, + scanOpts, + (err, fileEntry) => { + if(err) { + Log.trace( { reason : err.message }, 'Scanning failed'); + } - localInfo.fileEntry = fileEntry; - return callback(err, localInfo); - } - ); - }, - function store(localInfo, callback) { - // - // Move file to final area storage and persist to DB - // - const areaInfo = getFileAreaByTag(localInfo.areaTag); - if(!areaInfo) { - return callback(Errors.UnexpectedState(`Could not get area for tag ${localInfo.areaTag}`)); - } + localInfo.fileEntry = fileEntry; + return callback(err, localInfo); + } + ); + }, + function store(localInfo, callback) { + // + // Move file to final area storage and persist to DB + // + const areaInfo = getFileAreaByTag(localInfo.areaTag); + if(!areaInfo) { + return callback(Errors.UnexpectedState(`Could not get area for tag ${localInfo.areaTag}`)); + } - const storageTag = localInfo.storageTag || areaInfo.storageTags[0]; - if(!isValidStorageTag(storageTag)) { - return callback(Errors.Invalid(`Invalid storage tag: ${storageTag}`)); - } + const storageTag = localInfo.storageTag || areaInfo.storageTags[0]; + if(!isValidStorageTag(storageTag)) { + return callback(Errors.Invalid(`Invalid storage tag: ${storageTag}`)); + } - localInfo.fileEntry.storageTag = storageTag; - localInfo.fileEntry.areaTag = localInfo.areaTag; - localInfo.fileEntry.fileName = ticFileInfo.longFileName; + localInfo.fileEntry.storageTag = storageTag; + localInfo.fileEntry.areaTag = localInfo.areaTag; + localInfo.fileEntry.fileName = ticFileInfo.longFileName; - // - // We may now have two descriptions: from .DIZ/etc. or the TIC itself. - // Determine which one to use using |descPriority| and availability. - // - // We will still fallback as needed from -> -> - // - const descPriority = _.get( - Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ], - Config().scannerTossers.ftn_bso.tic.descPriority - ); + // + // We may now have two descriptions: from .DIZ/etc. or the TIC itself. + // Determine which one to use using |descPriority| and availability. + // + // We will still fallback as needed from -> -> + // + const descPriority = _.get( + Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ], + Config().scannerTossers.ftn_bso.tic.descPriority + ); - if('tic' === descPriority) { - const origDesc = localInfo.fileEntry.desc; - localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || origDesc || getDescFromFileName(ticFileInfo.filePath); - } else { - // see if we got desc from .DIZ/etc. - const fromDescFile = 'descFile' === localInfo.fileEntry.descSrc; - localInfo.fileEntry.desc = fromDescFile ? localInfo.fileEntry.desc : ticFileInfo.getAsString('Ldesc'); - localInfo.fileEntry.desc = localInfo.fileEntry.desc || getDescFromFileName(ticFileInfo.filePath); - } + if('tic' === descPriority) { + const origDesc = localInfo.fileEntry.desc; + localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || origDesc || getDescFromFileName(ticFileInfo.filePath); + } else { + // see if we got desc from .DIZ/etc. + const fromDescFile = 'descFile' === localInfo.fileEntry.descSrc; + localInfo.fileEntry.desc = fromDescFile ? localInfo.fileEntry.desc : ticFileInfo.getAsString('Ldesc'); + localInfo.fileEntry.desc = localInfo.fileEntry.desc || getDescFromFileName(ticFileInfo.filePath); + } - const areaStorageDir = getAreaStorageDirectoryByTag(storageTag); - if(!areaStorageDir) { - return callback(Errors.UnexpectedState(`Could not get storage directory for tag ${localInfo.areaTag}`)); - } + const areaStorageDir = getAreaStorageDirectoryByTag(storageTag); + if(!areaStorageDir) { + return callback(Errors.UnexpectedState(`Could not get storage directory for tag ${localInfo.areaTag}`)); + } - const isUpdate = localInfo.existingFileId ? true : false; + const isUpdate = localInfo.existingFileId ? true : false; - if(isUpdate) { - // we need to *update* an existing record/file - localInfo.fileEntry.fileId = localInfo.existingFileId; - } + if(isUpdate) { + // we need to *update* an existing record/file + localInfo.fileEntry.fileId = localInfo.existingFileId; + } - const dst = paths.join(areaStorageDir, localInfo.fileEntry.fileName); + const dst = paths.join(areaStorageDir, localInfo.fileEntry.fileName); - self.copyTicAttachment(ticFileInfo.filePath, dst, isUpdate, (err, finalPath) => { - if(err) { - Log.info( { reason : err.message }, 'Failed to copy TIC attachment'); - return callback(err); - } + self.copyTicAttachment(ticFileInfo.filePath, dst, isUpdate, (err, finalPath) => { + if(err) { + Log.info( { reason : err.message }, 'Failed to copy TIC attachment'); + return callback(err); + } - if(dst !== finalPath) { - localInfo.fileEntry.fileName = paths.basename(finalPath); - } + if(dst !== finalPath) { + localInfo.fileEntry.fileName = paths.basename(finalPath); + } - localInfo.fileEntry.persist(isUpdate, err => { - return callback(err, localInfo); - }); - }); - }, - // :TODO: from here, we need to re-toss files if needed, before they are removed - function cleanupOldFile(localInfo, callback) { - if(!localInfo.existingFileId) { - return callback(null, localInfo); - } + localInfo.fileEntry.persist(isUpdate, err => { + return callback(err, localInfo); + }); + }); + }, + // :TODO: from here, we need to re-toss files if needed, before they are removed + function cleanupOldFile(localInfo, callback) { + if(!localInfo.existingFileId) { + return callback(null, localInfo); + } - const oldStorageDir = getAreaStorageDirectoryByTag(localInfo.oldStorageTag); - const oldPath = paths.join(oldStorageDir, localInfo.oldFileName); + const oldStorageDir = getAreaStorageDirectoryByTag(localInfo.oldStorageTag); + const oldPath = paths.join(oldStorageDir, localInfo.oldFileName); - fs.unlink(oldPath, err => { - if(err) { - Log.warn( { error : err.message, oldPath : oldPath }, 'Failed removing old physical file during TIC replacement'); - } else { - Log.debug( { oldPath : oldPath }, 'Removed old physical file during TIC replacement'); - } - return callback(null, localInfo); // continue even if err - }); - }, - ], - (err, localInfo) => { - if(err) { - Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed import/update TIC record' ); - } else { - Log.debug( - { tic : ticFileInfo.path, file : ticFileInfo.filePath, area : localInfo.areaTag }, - 'TIC imported successfully' - ); - } - return cb(err); - } - ); - }; + fs.unlink(oldPath, err => { + if(err) { + Log.warn( { error : err.message, oldPath : oldPath }, 'Failed removing old physical file during TIC replacement'); + } else { + Log.debug( { oldPath : oldPath }, 'Removed old physical file during TIC replacement'); + } + return callback(null, localInfo); // continue even if err + }); + }, + ], + (err, localInfo) => { + if(err) { + Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed import/update TIC record' ); + } else { + Log.debug( + { tic : ticFileInfo.path, file : ticFileInfo.filePath, area : localInfo.areaTag }, + 'TIC imported successfully' + ); + } + return cb(err); + } + ); + }; - this.removeAssocTicFiles = function(ticFileInfo, cb) { - async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { - fs.unlink(path, err => { - if(err && 'ENOENT' !== err.code) { // don't log when the file doesn't exist - Log.warn( { error : err.message, path : path }, 'Failed unlinking TIC file'); - } - return nextPath(null); - }); - }, err => { - return cb(err); - }); - }; + this.removeAssocTicFiles = function(ticFileInfo, cb) { + async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { + fs.unlink(path, err => { + if(err && 'ENOENT' !== err.code) { // don't log when the file doesn't exist + Log.warn( { error : err.message, path : path }, 'Failed unlinking TIC file'); + } + return nextPath(null); + }); + }, err => { + return cb(err); + }); + }; - this.performEchoMailExport = function(cb) { - // - // Select all messages with a |message_id| > |lastScanId|. - // Additionally exclude messages with the System state_flags0 which will be present for - // imported or already exported messages - // - // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! - // - const getNewUuidsSql = + this.performEchoMailExport = function(cb) { + // + // Select all messages with a |message_id| > |lastScanId|. + // Additionally exclude messages with the System state_flags0 which will be present for + // imported or already exported messages + // + // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! + // + const getNewUuidsSql = `SELECT message_id, message_uuid FROM message m WHERE area_tag = ? AND message_id > ? AND @@ -1931,79 +1931,79 @@ function FTNMessageScanTossModule() { ORDER BY message_id;` ; - // we shouldn't, but be sure we don't try to pick up private mail here - const config = Config(); - const areaTags = Object.keys(config.messageNetworks.ftn.areas) - .filter(areaTag => Message.WellKnownAreaTags.Private !== areaTag); + // we shouldn't, but be sure we don't try to pick up private mail here + const config = Config(); + const areaTags = Object.keys(config.messageNetworks.ftn.areas) + .filter(areaTag => Message.WellKnownAreaTags.Private !== areaTag); - async.each(areaTags, (areaTag, nextArea) => { - const areaConfig = config.messageNetworks.ftn.areas[areaTag]; - if(!this.isAreaConfigValid(areaConfig)) { - return nextArea(); - } + async.each(areaTags, (areaTag, nextArea) => { + const areaConfig = config.messageNetworks.ftn.areas[areaTag]; + if(!this.isAreaConfigValid(areaConfig)) { + return nextArea(); + } - // - // For each message that is newer than that of the last scan - // we need to export to each configured associated uplink(s) - // - async.waterfall( - [ - function getLastScanId(callback) { - self.getAreaLastScanId(areaTag, callback); - }, - function getNewUuids(lastScanId, callback) { - msgDb.all(getNewUuidsSql, [ areaTag, lastScanId ], (err, rows) => { - if(err) { - callback(err); - } else { - if(0 === rows.length) { - let nothingToDoErr = new Error('Nothing to do!'); - nothingToDoErr.noRows = true; - callback(nothingToDoErr); - } else { - callback(null, rows); - } - } - }); - }, - function exportToConfiguredUplinks(msgRows, callback) { - const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only - self.exportEchoMailMessagesToUplinks(uuidsOnly, areaConfig, err => { - const newLastScanId = msgRows[msgRows.length - 1].message_id; + // + // For each message that is newer than that of the last scan + // we need to export to each configured associated uplink(s) + // + async.waterfall( + [ + function getLastScanId(callback) { + self.getAreaLastScanId(areaTag, callback); + }, + function getNewUuids(lastScanId, callback) { + msgDb.all(getNewUuidsSql, [ areaTag, lastScanId ], (err, rows) => { + if(err) { + callback(err); + } else { + if(0 === rows.length) { + let nothingToDoErr = new Error('Nothing to do!'); + nothingToDoErr.noRows = true; + callback(nothingToDoErr); + } else { + callback(null, rows); + } + } + }); + }, + function exportToConfiguredUplinks(msgRows, callback) { + const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only + self.exportEchoMailMessagesToUplinks(uuidsOnly, areaConfig, err => { + const newLastScanId = msgRows[msgRows.length - 1].message_id; - Log.info( - { areaTag : areaTag, messagesExported : msgRows.length, newLastScanId : newLastScanId }, - 'Export complete'); + Log.info( + { areaTag : areaTag, messagesExported : msgRows.length, newLastScanId : newLastScanId }, + 'Export complete'); - callback(err, newLastScanId); - }); - }, - function updateLastScanId(newLastScanId, callback) { - self.setAreaLastScanId(areaTag, newLastScanId, callback); - } - ], - () => { - return nextArea(); - } - ); - }, - err => { - return cb(err); - }); - }; + callback(err, newLastScanId); + }); + }, + function updateLastScanId(newLastScanId, callback) { + self.setAreaLastScanId(areaTag, newLastScanId, callback); + } + ], + () => { + return nextArea(); + } + ); + }, + err => { + return cb(err); + }); + }; - this.performNetMailExport = function(cb) { - // - // Select all messages with a |message_id| > |lastScanId| in the private area - // that are schedule for export to FTN-style networks. - // - // Just like EchoMail, we additionally exclude messages with the System state_flags0 - // which will be present for imported or already exported messages - // - // - // :TODO: fill out the rest of the consts here - // :TODO: this statement is crazy ugly -- use JOIN / NOT EXISTS for state_flags & 0x02 - const getNewUuidsSql = + this.performNetMailExport = function(cb) { + // + // Select all messages with a |message_id| > |lastScanId| in the private area + // that are schedule for export to FTN-style networks. + // + // Just like EchoMail, we additionally exclude messages with the System state_flags0 + // which will be present for imported or already exported messages + // + // + // :TODO: fill out the rest of the consts here + // :TODO: this statement is crazy ugly -- use JOIN / NOT EXISTS for state_flags & 0x02 + const getNewUuidsSql = `SELECT message_id, message_uuid FROM message m WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND @@ -2024,40 +2024,40 @@ function FTNMessageScanTossModule() { ORDER BY message_id; `; - async.waterfall( - [ - function getLastScanId(callback) { - return self.getAreaLastScanId(Message.WellKnownAreaTags.Private, callback); - }, - function getNewUuids(lastScanId, callback) { - msgDb.all(getNewUuidsSql, [ lastScanId ], (err, rows) => { - if(err) { - return callback(err); - } + async.waterfall( + [ + function getLastScanId(callback) { + return self.getAreaLastScanId(Message.WellKnownAreaTags.Private, callback); + }, + function getNewUuids(lastScanId, callback) { + msgDb.all(getNewUuidsSql, [ lastScanId ], (err, rows) => { + if(err) { + return callback(err); + } - if(0 === rows.length) { - return cb(null); // note |cb| -- early bail out! - } + if(0 === rows.length) { + return cb(null); // note |cb| -- early bail out! + } - return callback(null, rows); - }); - }, - function exportMessages(rows, callback) { - const messageUuids = rows.map(r => r.message_uuid); - return self.exportNetMailMessagesToUplinks(messageUuids, callback); - } - ], - err => { - return cb(err); - } - ); - }; + return callback(null, rows); + }); + }, + function exportMessages(rows, callback) { + const messageUuids = rows.map(r => r.message_uuid); + return self.exportNetMailMessagesToUplinks(messageUuids, callback); + } + ], + err => { + return cb(err); + } + ); + }; - this.isNetMailMessage = function(message) { - return message.isPrivate() && + this.isNetMailMessage = function(message) { + return message.isPrivate() && null === _.get(message, 'meta.System.LocalToUserID', null) && Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null); - }; + }; } require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule); @@ -2065,293 +2065,293 @@ require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule); // :TODO: *scheduled* portion of this stuff should probably use event_scheduler - @immediate would still use record(). FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importDir, cb) { - // :TODO: pass in 'inbound' vs 'secInbound' -- pass along to processSingleTicFile() where password will be checked + // :TODO: pass in 'inbound' vs 'secInbound' -- pass along to processSingleTicFile() where password will be checked - const self = this; - async.waterfall( - [ - function findTicFiles(callback) { - fs.readdir(importDir, (err, files) => { - if(err) { - return callback(err); - } + const self = this; + async.waterfall( + [ + function findTicFiles(callback) { + fs.readdir(importDir, (err, files) => { + if(err) { + return callback(err); + } - return callback(null, files.filter(f => '.tic' === paths.extname(f).toLowerCase())); - }); - }, - function gatherInfo(ticFiles, callback) { - const ticFilesInfo = []; + return callback(null, files.filter(f => '.tic' === paths.extname(f).toLowerCase())); + }); + }, + function gatherInfo(ticFiles, callback) { + const ticFilesInfo = []; - async.each(ticFiles, (fileName, nextFile) => { - const fullPath = paths.join(importDir, fileName); + async.each(ticFiles, (fileName, nextFile) => { + const fullPath = paths.join(importDir, fileName); - TicFileInfo.createFromFile(fullPath, (err, ticInfo) => { - if(err) { - Log.warn( { error : err.message, path : fullPath }, 'Failed reading TIC file'); - } else { - ticFilesInfo.push(ticInfo); - } + TicFileInfo.createFromFile(fullPath, (err, ticInfo) => { + if(err) { + Log.warn( { error : err.message, path : fullPath }, 'Failed reading TIC file'); + } else { + ticFilesInfo.push(ticInfo); + } - return nextFile(null); - }); - }, - err => { - return callback(err, ticFilesInfo); - }); - }, - function process(ticFilesInfo, callback) { - async.eachSeries(ticFilesInfo, (ticFileInfo, nextTicInfo) => { - self.processSingleTicFile(ticFileInfo, err => { - if(err) { - // archive rejected TIC stuff (.TIC + attach) - async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { - if(!path) { // possibly rejected due to "File" not existing/etc. - return nextPath(null); - } + return nextFile(null); + }); + }, + err => { + return callback(err, ticFilesInfo); + }); + }, + function process(ticFilesInfo, callback) { + async.eachSeries(ticFilesInfo, (ticFileInfo, nextTicInfo) => { + self.processSingleTicFile(ticFileInfo, err => { + if(err) { + // archive rejected TIC stuff (.TIC + attach) + async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { + if(!path) { // possibly rejected due to "File" not existing/etc. + return nextPath(null); + } - self.maybeArchiveImportFile( - path, - 'tic', - 'reject', - () => { - return nextPath(null); - } - ); - }, - () => { - self.removeAssocTicFiles(ticFileInfo, () => { - return nextTicInfo(null); - }); - }); - } else { - self.removeAssocTicFiles(ticFileInfo, () => { - return nextTicInfo(null); - }); - } - }); - }, err => { - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); + self.maybeArchiveImportFile( + path, + 'tic', + 'reject', + () => { + return nextPath(null); + } + ); + }, + () => { + self.removeAssocTicFiles(ticFileInfo, () => { + return nextTicInfo(null); + }); + }); + } else { + self.removeAssocTicFiles(ticFileInfo, () => { + return nextTicInfo(null); + }); + } + }); + }, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); }; FTNMessageScanTossModule.prototype.startup = function(cb) { - Log.info(`${exports.moduleInfo.name} Scanner/Tosser starting up`); + Log.info(`${exports.moduleInfo.name} Scanner/Tosser starting up`); - let importing = false; + let importing = false; - let self = this; + let self = this; - function tryImportNow(reasonDesc, extraInfo) { - if(!importing) { - importing = true; + function tryImportNow(reasonDesc, extraInfo) { + if(!importing) { + importing = true; - Log.info( Object.assign({ module : exports.moduleInfo.name }, extraInfo), reasonDesc); + Log.info( Object.assign({ module : exports.moduleInfo.name }, extraInfo), reasonDesc); - self.performImport( () => { - importing = false; - }); - } - } + self.performImport( () => { + importing = false; + }); + } + } - this.createTempDirectories(err => { - if(err) { - Log.warn( { error : err.toStrong() }, 'Failed creating temporary directories!'); - return cb(err); - } + this.createTempDirectories(err => { + if(err) { + Log.warn( { error : err.toStrong() }, 'Failed creating temporary directories!'); + return cb(err); + } - if(_.isObject(this.moduleConfig.schedule)) { - const exportSchedule = this.parseScheduleString(this.moduleConfig.schedule.export); - if(exportSchedule) { - Log.debug( - { - schedule : this.moduleConfig.schedule.export, - schedOK : -1 === exportSchedule.sched.error, - next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), - immediate : exportSchedule.immediate ? true : false, - }, - 'Export schedule loaded' - ); + if(_.isObject(this.moduleConfig.schedule)) { + const exportSchedule = this.parseScheduleString(this.moduleConfig.schedule.export); + if(exportSchedule) { + Log.debug( + { + schedule : this.moduleConfig.schedule.export, + schedOK : -1 === exportSchedule.sched.error, + next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), + immediate : exportSchedule.immediate ? true : false, + }, + 'Export schedule loaded' + ); - if(exportSchedule.sched) { - this.exportTimer = later.setInterval( () => { - if(this.exportingStart()) { - Log.info( { module : exports.moduleInfo.name }, 'Performing scheduled message scan/export...'); + if(exportSchedule.sched) { + this.exportTimer = later.setInterval( () => { + if(this.exportingStart()) { + Log.info( { module : exports.moduleInfo.name }, 'Performing scheduled message scan/export...'); - this.performExport( () => { - this.exportingEnd(); - }); - } - }, exportSchedule.sched); - } + this.performExport( () => { + this.exportingEnd(); + }); + } + }, exportSchedule.sched); + } - if(_.isBoolean(exportSchedule.immediate)) { - this.exportImmediate = exportSchedule.immediate; - } - } + if(_.isBoolean(exportSchedule.immediate)) { + this.exportImmediate = exportSchedule.immediate; + } + } - const importSchedule = this.parseScheduleString(this.moduleConfig.schedule.import); - if(importSchedule) { - Log.debug( - { - schedule : this.moduleConfig.schedule.import, - schedOK : -1 === importSchedule.sched.error, - next : moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), - watchFile : _.isString(importSchedule.watchFile) ? importSchedule.watchFile : 'None', - }, - 'Import schedule loaded' - ); + const importSchedule = this.parseScheduleString(this.moduleConfig.schedule.import); + if(importSchedule) { + Log.debug( + { + schedule : this.moduleConfig.schedule.import, + schedOK : -1 === importSchedule.sched.error, + next : moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), + watchFile : _.isString(importSchedule.watchFile) ? importSchedule.watchFile : 'None', + }, + 'Import schedule loaded' + ); - if(importSchedule.sched) { - this.importTimer = later.setInterval( () => { - tryImportNow('Performing scheduled message import/toss...'); - }, importSchedule.sched); - } + if(importSchedule.sched) { + this.importTimer = later.setInterval( () => { + tryImportNow('Performing scheduled message import/toss...'); + }, importSchedule.sched); + } - if(_.isString(importSchedule.watchFile)) { - const watcher = sane( - paths.dirname(importSchedule.watchFile), - { - glob : `**/${paths.basename(importSchedule.watchFile)}` - } - ); + if(_.isString(importSchedule.watchFile)) { + const watcher = sane( + paths.dirname(importSchedule.watchFile), + { + glob : `**/${paths.basename(importSchedule.watchFile)}` + } + ); - [ 'change', 'add', 'delete' ].forEach(event => { - watcher.on(event, (fileName, fileRoot) => { - const eventPath = paths.join(fileRoot, fileName); - if(paths.join(fileRoot, fileName) === importSchedule.watchFile) { - tryImportNow('Performing import/toss due to @watch', { eventPath, event } ); - } - }); - }); + [ 'change', 'add', 'delete' ].forEach(event => { + watcher.on(event, (fileName, fileRoot) => { + const eventPath = paths.join(fileRoot, fileName); + if(paths.join(fileRoot, fileName) === importSchedule.watchFile) { + tryImportNow('Performing import/toss due to @watch', { eventPath, event } ); + } + }); + }); - // - // If the watch file already exists, kick off now - // https://github.com/NuSkooler/enigma-bbs/issues/122 - // - fse.exists(importSchedule.watchFile, exists => { - if(exists) { - tryImportNow('Performing import/toss due to @watch', { eventPath : importSchedule.watchFile, event : 'initial exists' } ); - } - }); - } - } - } + // + // If the watch file already exists, kick off now + // https://github.com/NuSkooler/enigma-bbs/issues/122 + // + fse.exists(importSchedule.watchFile, exists => { + if(exists) { + tryImportNow('Performing import/toss due to @watch', { eventPath : importSchedule.watchFile, event : 'initial exists' } ); + } + }); + } + } + } - FTNMessageScanTossModule.super_.prototype.startup.call(this, cb); - }); + FTNMessageScanTossModule.super_.prototype.startup.call(this, cb); + }); }; FTNMessageScanTossModule.prototype.shutdown = function(cb) { - Log.info('FidoNet Scanner/Tosser shutting down'); + Log.info('FidoNet Scanner/Tosser shutting down'); - if(this.exportTimer) { - this.exportTimer.clear(); - } + if(this.exportTimer) { + this.exportTimer.clear(); + } - if(this.importTimer) { - this.importTimer.clear(); - } + if(this.importTimer) { + this.importTimer.clear(); + } - // - // Clean up temp dir/files we created - // - temptmp.cleanup( paths => { - const fullStats = { - exportDir : this.exportTempDir, - importTemp : this.importTempDir, - paths : paths, - sessionId : temptmp.sessionId, - }; + // + // Clean up temp dir/files we created + // + temptmp.cleanup( paths => { + const fullStats = { + exportDir : this.exportTempDir, + importTemp : this.importTempDir, + paths : paths, + sessionId : temptmp.sessionId, + }; - Log.trace(fullStats, 'Temporary directories cleaned up'); + Log.trace(fullStats, 'Temporary directories cleaned up'); - FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); - }); + FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); + }); - FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); + FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); }; FTNMessageScanTossModule.prototype.performImport = function(cb) { - if(!this.hasValidConfiguration()) { - return cb(new Error('Missing or invalid configuration')); - } + if(!this.hasValidConfiguration()) { + return cb(new Error('Missing or invalid configuration')); + } - const self = this; + const self = this; - async.each( [ 'inbound', 'secInbound' ], (inboundType, nextDir) => { - self.importFromDirectory(inboundType, self.moduleConfig.paths[inboundType], () => { - return nextDir(null); - }); - }, cb); + async.each( [ 'inbound', 'secInbound' ], (inboundType, nextDir) => { + self.importFromDirectory(inboundType, self.moduleConfig.paths[inboundType], () => { + return nextDir(null); + }); + }, cb); }; FTNMessageScanTossModule.prototype.performExport = function(cb) { - // - // We're only concerned with areas related to FTN. For each area, loop though - // and let's find out what messages need exported. - // - if(!this.hasValidConfiguration()) { - return cb(new Error('Missing or invalid configuration')); - } + // + // We're only concerned with areas related to FTN. For each area, loop though + // and let's find out what messages need exported. + // + if(!this.hasValidConfiguration()) { + return cb(new Error('Missing or invalid configuration')); + } - const self = this; + const self = this; - async.eachSeries( [ 'EchoMail', 'NetMail' ], (type, nextType) => { - self[`perform${type}Export`]( err => { - if(err) { - Log.warn( { error : err.message, type : type }, 'Error(s) during export' ); - } - return nextType(null); // try next, always - }); - }, () => { - return cb(null); - }); + async.eachSeries( [ 'EchoMail', 'NetMail' ], (type, nextType) => { + self[`perform${type}Export`]( err => { + if(err) { + Log.warn( { error : err.message, type : type }, 'Error(s) during export' ); + } + return nextType(null); // try next, always + }); + }, () => { + return cb(null); + }); }; FTNMessageScanTossModule.prototype.record = function(message) { - // - // This module works off schedules, but we do support @immediate for export - // - if(true !== this.exportImmediate || !this.hasValidConfiguration()) { - return; - } + // + // This module works off schedules, but we do support @immediate for export + // + if(true !== this.exportImmediate || !this.hasValidConfiguration()) { + return; + } - const info = { uuid : message.uuid, subject : message.subject }; + const info = { uuid : message.uuid, subject : message.subject }; - function exportLog(err) { - if(err) { - Log.warn(info, 'Failed exporting message'); - } else { - Log.info(info, 'Message exported'); - } - } + function exportLog(err) { + if(err) { + Log.warn(info, 'Failed exporting message'); + } else { + Log.info(info, 'Message exported'); + } + } - if(this.isNetMailMessage(message)) { - Object.assign(info, { type : 'NetMail' } ); + if(this.isNetMailMessage(message)) { + Object.assign(info, { type : 'NetMail' } ); - if(this.exportingStart()) { - this.exportNetMailMessagesToUplinks( [ message.uuid ], err => { - this.exportingEnd( () => exportLog(err) ); - }); - } - } else if(message.areaTag) { - Object.assign(info, { type : 'EchoMail' } ); + if(this.exportingStart()) { + this.exportNetMailMessagesToUplinks( [ message.uuid ], err => { + this.exportingEnd( () => exportLog(err) ); + }); + } + } else if(message.areaTag) { + Object.assign(info, { type : 'EchoMail' } ); - const areaConfig = Config().messageNetworks.ftn.areas[message.areaTag]; - if(!this.isAreaConfigValid(areaConfig)) { - return; - } + const areaConfig = Config().messageNetworks.ftn.areas[message.areaTag]; + if(!this.isAreaConfigValid(areaConfig)) { + return; + } - if(this.exportingStart()) { - this.exportEchoMailMessagesToUplinks( [ message.uuid ], areaConfig, err => { - this.exportingEnd( () => exportLog(err) ); - }); - } - } + if(this.exportingStart()) { + this.exportEchoMailMessagesToUplinks( [ message.uuid ], areaConfig, err => { + this.exportingEnd( () => exportLog(err) ); + }); + } + } }; diff --git a/core/server_module.js b/core/server_module.js index d1b8ccc6..26000c1b 100644 --- a/core/server_module.js +++ b/core/server_module.js @@ -6,7 +6,7 @@ var PluginModule = require('./plugin_module.js').PluginModule; exports.ServerModule = ServerModule; function ServerModule() { - PluginModule.call(this); + PluginModule.call(this); } require('util').inherits(ServerModule, PluginModule); diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index d613052a..bc221b84 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -6,14 +6,14 @@ const Log = require('../../logger.js').log; const { ServerModule } = require('../../server_module.js'); const Config = require('../../config.js').get; const { - splitTextAtTerms, - isAnsi, - cleanControlCodes + splitTextAtTerms, + isAnsi, + cleanControlCodes } = require('../../string_util.js'); const { - getMessageConferenceByTag, - getMessageAreaByTag, - getMessageListForArea, + getMessageConferenceByTag, + getMessageAreaByTag, + getMessageListForArea, } = require('../../message_area.js'); const { sortAreasOrConfs } = require('../../conf_area_util.js'); const AnsiPrep = require('../../ansi_prep.js'); @@ -26,221 +26,221 @@ const paths = require('path'); const moment = require('moment'); const ModuleInfo = exports.moduleInfo = { - name : 'Gopher', - desc : 'Gopher Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.gopher.server', + name : 'Gopher', + desc : 'Gopher Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.gopher.server', }; const Message = require('../../message.js'); const ItemTypes = { - Invalid : '', // not really a type, of course! + Invalid : '', // not really a type, of course! - // Canonical, RFC-1436 - TextFile : '0', - SubMenu : '1', - CCSONameserver : '2', - Error : '3', - BinHexFile : '4', - DOSFile : '5', - UuEncodedFile : '6', - FullTextSearch : '7', - Telnet : '8', - BinaryFile : '9', - AltServer : '+', - GIFFile : 'g', - ImageFile : 'I', - Telnet3270 : 'T', + // Canonical, RFC-1436 + TextFile : '0', + SubMenu : '1', + CCSONameserver : '2', + Error : '3', + BinHexFile : '4', + DOSFile : '5', + UuEncodedFile : '6', + FullTextSearch : '7', + Telnet : '8', + BinaryFile : '9', + AltServer : '+', + GIFFile : 'g', + ImageFile : 'I', + Telnet3270 : 'T', - // Non-canonical - HtmlFile : 'h', - InfoMessage : 'i', - SoundFile : 's', + // Non-canonical + HtmlFile : 'h', + InfoMessage : 'i', + SoundFile : 's', }; exports.getModule = class GopherModule extends ServerModule { - constructor() { - super(); + constructor() { + super(); - this.routes = new Map(); // selector->generator => gopher item - this.log = Log.child( { server : 'Gopher' } ); - } + this.routes = new Map(); // selector->generator => gopher item + this.log = Log.child( { server : 'Gopher' } ); + } - createServer() { - if(!this.enabled) { - return; - } + createServer() { + if(!this.enabled) { + return; + } - const config = Config(); - this.publicHostname = config.contentServers.gopher.publicHostname; - this.publicPort = config.contentServers.gopher.publicPort; + const config = Config(); + this.publicHostname = config.contentServers.gopher.publicHostname; + this.publicPort = config.contentServers.gopher.publicPort; - this.addRoute(/^\/?\r\n$/, this.defaultGenerator); - this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); + this.addRoute(/^\/?\r\n$/, this.defaultGenerator); + this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); - this.server = net.createServer( socket => { - socket.setEncoding('ascii'); + this.server = net.createServer( socket => { + socket.setEncoding('ascii'); - socket.on('data', data => { - this.routeRequest(data, socket); - }); + socket.on('data', data => { + this.routeRequest(data, socket); + }); - socket.on('error', err => { - if('ECONNRESET' !== err.code) { // normal - this.log.trace( { error : err.message }, 'Socket error'); - } - }); - }); - } + socket.on('error', err => { + if('ECONNRESET' !== err.code) { // normal + this.log.trace( { error : err.message }, 'Socket error'); + } + }); + }); + } - listen() { - if(!this.enabled) { - return true; // nothing to do, but not an error - } + listen() { + if(!this.enabled) { + return true; // nothing to do, but not an error + } - const config = Config(); - const port = parseInt(config.contentServers.gopher.port); - if(isNaN(port)) { - this.log.warn( { port : config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); - return false; - } + const config = Config(); + const port = parseInt(config.contentServers.gopher.port); + if(isNaN(port)) { + this.log.warn( { port : config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); + return false; + } - return this.server.listen(port); - } + return this.server.listen(port); + } - get enabled() { - return _.get(Config(), 'contentServers.gopher.enabled', false) && this.isConfigured(); - } + get enabled() { + return _.get(Config(), 'contentServers.gopher.enabled', false) && this.isConfigured(); + } - isConfigured() { - // public hostname & port must be set; responses contain them! - const config = Config(); - return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) && + isConfigured() { + // public hostname & port must be set; responses contain them! + const config = Config(); + return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) && _.isNumber(_.get(config, 'contentServers.gopher.publicPort')); - } + } - addRoute(selectorRegExp, generatorHandler) { - if(_.isString(selectorRegExp)) { - try { - selectorRegExp = new RegExp(`${selectorRegExp}\r\n`); - } catch(e) { - this.log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' ); - return false; - } - } - this.routes.set(selectorRegExp, generatorHandler.bind(this)); - } + addRoute(selectorRegExp, generatorHandler) { + if(_.isString(selectorRegExp)) { + try { + selectorRegExp = new RegExp(`${selectorRegExp}\r\n`); + } catch(e) { + this.log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' ); + return false; + } + } + this.routes.set(selectorRegExp, generatorHandler.bind(this)); + } - routeRequest(selector, socket) { - let match; - for(let [regex, gen] of this.routes) { - match = selector.match(regex); - if(match) { - return gen(match, res => { - return socket.end(`${res}`); - }); - } - } - this.notFoundGenerator(selector, res => { - return socket.end(`${res}`); - }); - } + routeRequest(selector, socket) { + let match; + for(let [regex, gen] of this.routes) { + match = selector.match(regex); + if(match) { + return gen(match, res => { + return socket.end(`${res}`); + }); + } + } + this.notFoundGenerator(selector, res => { + return socket.end(`${res}`); + }); + } - makeItem(itemType, text, selector, hostname, port) { - selector = selector || ''; // e.g. for info - hostname = hostname || this.publicHostname; - port = port || this.publicPort; - return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; - } + makeItem(itemType, text, selector, hostname, port) { + selector = selector || ''; // e.g. for info + hostname = hostname || this.publicHostname; + port = port || this.publicPort; + return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; + } - defaultGenerator(selectorMatch, cb) { - this.log.trace( { selector : selectorMatch[0] }, 'Serving default content'); + defaultGenerator(selectorMatch, cb) { + this.log.trace( { selector : selectorMatch[0] }, 'Serving default content'); - let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc'); - bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); - fs.readFile(bannerFile, 'utf8', (err, banner) => { - if(err) { - return cb('You have reached an ENiGMA½ Gopher server!'); - } + let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc'); + bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); + fs.readFile(bannerFile, 'utf8', (err, banner) => { + if(err) { + return cb('You have reached an ENiGMA½ Gopher server!'); + } - banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join(''); - banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); - return cb(banner); - }); - } + banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join(''); + banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); + return cb(banner); + }); + } - notFoundGenerator(selector, cb) { - this.log.trace( { selector }, 'Serving not found content'); - return cb('Not found'); - } + notFoundGenerator(selector, cb) { + this.log.trace( { selector }, 'Serving not found content'); + return cb('Not found'); + } - isAreaAndConfExposed(confTag, areaTag) { - const conf = _.get(Config(), [ 'contentServers', 'gopher', 'messageConferences', confTag ]); - return Array.isArray(conf) && conf.includes(areaTag); - } + isAreaAndConfExposed(confTag, areaTag) { + const conf = _.get(Config(), [ 'contentServers', 'gopher', 'messageConferences', confTag ]); + return Array.isArray(conf) && conf.includes(areaTag); + } - prepareMessageBody(body, cb) { - if(isAnsi(body)) { - AnsiPrep( - body, - { - cols : 79, // Gopher std. wants 70, but we'll have to deal with it. - forceLineTerm : true, // ensure each line is term'd - asciiMode : true, // export to ASCII - fillLines : false, // don't fill up to |cols| - }, - (err, prepped) => { - return cb(prepped || body); - } - ); - } else { - return cb(cleanControlCodes(body, { all : true } )); - } - } + prepareMessageBody(body, cb) { + if(isAnsi(body)) { + AnsiPrep( + body, + { + cols : 79, // Gopher std. wants 70, but we'll have to deal with it. + forceLineTerm : true, // ensure each line is term'd + asciiMode : true, // export to ASCII + fillLines : false, // don't fill up to |cols| + }, + (err, prepped) => { + return cb(prepped || body); + } + ); + } else { + return cb(cleanControlCodes(body, { all : true } )); + } + } - shortenSubject(subject) { - return _.truncate(subject, { length : 30 } ); - } + shortenSubject(subject) { + return _.truncate(subject, { length : 30 } ); + } - messageAreaGenerator(selectorMatch, cb) { - this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content'); - // - // Selector should be: - // /msgarea - list confs - // /msgarea/conftag - list areas in conf - // /msgarea/conftag/areatag - list messages in area - // /msgarea/conftag/areatag/ - message as text - // /msgarea/conftag/areatag/_raw - full message as text + headers - // - if(selectorMatch[3] || selectorMatch[4]) { - // message - //const raw = selectorMatch[4] ? true : false; - // :TODO: support 'raw' - const msgUuid = selectorMatch[3].replace(/\r\n|\//g, ''); - const confTag = selectorMatch[1].substr(1).split('/')[0]; - const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); - const message = new Message(); + messageAreaGenerator(selectorMatch, cb) { + this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content'); + // + // Selector should be: + // /msgarea - list confs + // /msgarea/conftag - list areas in conf + // /msgarea/conftag/areatag - list messages in area + // /msgarea/conftag/areatag/ - message as text + // /msgarea/conftag/areatag/_raw - full message as text + headers + // + if(selectorMatch[3] || selectorMatch[4]) { + // message + //const raw = selectorMatch[4] ? true : false; + // :TODO: support 'raw' + const msgUuid = selectorMatch[3].replace(/\r\n|\//g, ''); + const confTag = selectorMatch[1].substr(1).split('/')[0]; + const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); + const message = new Message(); - return message.load( { uuid : msgUuid }, err => { - if(err) { - this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existant message UUID!'); - return this.notFoundGenerator(selectorMatch, cb); - } + return message.load( { uuid : msgUuid }, err => { + if(err) { + this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existant message UUID!'); + return this.notFoundGenerator(selectorMatch, cb); + } - if(message.areaTag !== areaTag || !this.isAreaAndConfExposed(confTag, areaTag)) { - this.log.warn( { areaTag }, 'Attempted access to non-exposed conference and/or area!'); - return this.notFoundGenerator(selectorMatch, cb); - } + if(message.areaTag !== areaTag || !this.isAreaAndConfExposed(confTag, areaTag)) { + this.log.warn( { areaTag }, 'Attempted access to non-exposed conference and/or area!'); + return this.notFoundGenerator(selectorMatch, cb); + } - if(Message.isPrivateAreaTag(areaTag)) { - this.log.warn( { areaTag }, 'Attempted access to message in private area!'); - return this.notFoundGenerator(selectorMatch, cb); - } + if(Message.isPrivateAreaTag(areaTag)) { + this.log.warn( { areaTag }, 'Attempted access to message in private area!'); + return this.notFoundGenerator(selectorMatch, cb); + } - this.prepareMessageBody(message.message, msgBody => { - const response = `${'-'.repeat(70)} + this.prepareMessageBody(message.message, msgBody => { + const response = `${'-'.repeat(70)} To : ${message.toUserName} From : ${message.fromUserName} When : ${moment(message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')} @@ -249,87 +249,87 @@ ID : ${message.messageUuid} (${message.messageId}) ${'-'.repeat(70)} ${msgBody} `; - return cb(response); - }); - }); - } else if(selectorMatch[2]) { - // list messages in area - const confTag = selectorMatch[1].substr(1).split('/')[0]; - const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); - const area = getMessageAreaByTag(areaTag); + return cb(response); + }); + }); + } else if(selectorMatch[2]) { + // list messages in area + const confTag = selectorMatch[1].substr(1).split('/')[0]; + const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); + const area = getMessageAreaByTag(areaTag); - if(Message.isPrivateAreaTag(areaTag)) { - this.log.warn( { areaTag }, 'Attempted access to private area!'); - return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private')); - } + if(Message.isPrivateAreaTag(areaTag)) { + this.log.warn( { areaTag }, 'Attempted access to private area!'); + return cb(this.makeItem(ItemTypes.InfoMessage, 'Area is private')); + } - if(!area || !this.isAreaAndConfExposed(confTag, areaTag)) { - this.log.warn( { confTag, areaTag }, 'Attempted access to non-exposed conference and/or area!'); - return this.notFoundGenerator(selectorMatch, cb); - } + if(!area || !this.isAreaAndConfExposed(confTag, areaTag)) { + this.log.warn( { confTag, areaTag }, 'Attempted access to non-exposed conference and/or area!'); + return this.notFoundGenerator(selectorMatch, cb); + } - return getMessageListForArea(null, areaTag, (err, msgList) => { - const response = [ - this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`), - this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - ...msgList.map(msg => this.makeItem( - ItemTypes.TextFile, - `${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(msg.subject)} (${msg.fromUserName} to ${msg.toUserName})`, - `/msgarea/${confTag}/${areaTag}/${msg.messageUuid}` - )) - ].join(''); + return getMessageListForArea(null, areaTag, (err, msgList) => { + const response = [ + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`), + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + ...msgList.map(msg => this.makeItem( + ItemTypes.TextFile, + `${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(msg.subject)} (${msg.fromUserName} to ${msg.toUserName})`, + `/msgarea/${confTag}/${areaTag}/${msg.messageUuid}` + )) + ].join(''); - return cb(response); - }); - } else if(selectorMatch[1]) { - // list areas in conf - const sysConfig = Config(); - const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); - const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); - if(!conf) { - return this.notFoundGenerator(selectorMatch, cb); - } + return cb(response); + }); + } else if(selectorMatch[1]) { + // list areas in conf + const sysConfig = Config(); + const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); + const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); + if(!conf) { + return this.notFoundGenerator(selectorMatch, cb); + } - const areas = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ], {}) - .map(areaTag => Object.assign( { areaTag }, getMessageAreaByTag(areaTag))) - .filter(area => area && !Message.isPrivateAreaTag(area.areaTag)); + const areas = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ], {}) + .map(areaTag => Object.assign( { areaTag }, getMessageAreaByTag(areaTag))) + .filter(area => area && !Message.isPrivateAreaTag(area.areaTag)); - if(0 === areas.length) { - return cb(this.makeItem(ItemTypes.InfoMessage, 'No message areas available')); - } + if(0 === areas.length) { + return cb(this.makeItem(ItemTypes.InfoMessage, 'No message areas available')); + } - sortAreasOrConfs(areas); + sortAreasOrConfs(areas); - const response = [ - this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`), - this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - ...areas.map(area => this.makeItem(ItemTypes.SubMenu, area.name, `/msgarea/${confTag}/${area.areaTag}`)) - ].join(''); + const response = [ + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`), + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + ...areas.map(area => this.makeItem(ItemTypes.SubMenu, area.name, `/msgarea/${confTag}/${area.areaTag}`)) + ].join(''); - return cb(response); - } else { - // message area base (list confs) - const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {})) - .map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag))) - .filter(conf => conf); // remove any baddies + return cb(response); + } else { + // message area base (list confs) + const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {})) + .map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag))) + .filter(conf => conf); // remove any baddies - if(0 === confs.length) { - return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available')); - } + if(0 === confs.length) { + return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available')); + } - sortAreasOrConfs(confs); + sortAreasOrConfs(confs); - const response = [ - this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'), - this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - this.makeItem(ItemTypes.InfoMessage, ''), - ...confs.map(conf => this.makeItem(ItemTypes.SubMenu, conf.name, `/msgarea/${conf.confTag}`)) - ].join(''); + const response = [ + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'), + this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), + this.makeItem(ItemTypes.InfoMessage, ''), + ...confs.map(conf => this.makeItem(ItemTypes.SubMenu, conf.name, `/msgarea/${conf.confTag}`)) + ].join(''); - return cb(response); - } - } + return cb(response); + } + } }; \ No newline at end of file diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 47e0661f..c31a3720 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -15,169 +15,169 @@ const paths = require('path'); const mimeTypes = require('mime-types'); const ModuleInfo = exports.moduleInfo = { - name : 'Web', - desc : 'Web Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.web.server', + name : 'Web', + desc : 'Web Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.web.server', }; class Route { - constructor(route) { - Object.assign(this, route); - - if(this.method) { - this.method = this.method.toUpperCase(); - } + constructor(route) { + Object.assign(this, route); - try { - this.pathRegExp = new RegExp(this.path); - } catch(e) { - Log.debug( { route : route }, 'Invalid regular expression for route path' ); - } - } + if(this.method) { + this.method = this.method.toUpperCase(); + } - isValid() { - return ( - this.pathRegExp instanceof RegExp && - ( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) || + try { + this.pathRegExp = new RegExp(this.path); + } catch(e) { + Log.debug( { route : route }, 'Invalid regular expression for route path' ); + } + } + + isValid() { + return ( + this.pathRegExp instanceof RegExp && + ( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) || !_.isFunction(this.handler) - ); - } + ); + } - matchesRequest(req) { - return req.method === this.method && this.pathRegExp.test(req.url); - } + matchesRequest(req) { + return req.method === this.method && this.pathRegExp.test(req.url); + } - getRouteKey() { return `${this.method}:${this.path}`; } + getRouteKey() { return `${this.method}:${this.path}`; } } exports.getModule = class WebServerModule extends ServerModule { - constructor() { - super(); + constructor() { + super(); - const config = Config(); - this.enableHttp = config.contentServers.web.http.enabled || false; - this.enableHttps = config.contentServers.web.https.enabled || false; + const config = Config(); + this.enableHttp = config.contentServers.web.http.enabled || false; + this.enableHttps = config.contentServers.web.https.enabled || false; - this.routes = {}; + this.routes = {}; - if(this.isEnabled() && config.contentServers.web.staticRoot) { - this.addRoute({ - method : 'GET', - path : '/static/.*$', - handler : this.routeStaticFile.bind(this), - }); - } - } + if(this.isEnabled() && config.contentServers.web.staticRoot) { + this.addRoute({ + method : 'GET', + path : '/static/.*$', + handler : this.routeStaticFile.bind(this), + }); + } + } - buildUrl(pathAndQuery) { - // - // Create a URL such as - // https://l33t.codes:44512/ + |pathAndQuery| - // - // Prefer HTTPS over HTTP. Be explicit about the port - // only if non-standard. Allow users to override full prefix in config. - // - const config = Config(); - if(_.isString(config.contentServers.web.overrideUrlPrefix)) { - return `${config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`; - } + buildUrl(pathAndQuery) { + // + // Create a URL such as + // https://l33t.codes:44512/ + |pathAndQuery| + // + // Prefer HTTPS over HTTP. Be explicit about the port + // only if non-standard. Allow users to override full prefix in config. + // + const config = Config(); + if(_.isString(config.contentServers.web.overrideUrlPrefix)) { + return `${config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`; + } - let schema; - let port; - if(config.contentServers.web.https.enabled) { - schema = 'https://'; - port = (443 === config.contentServers.web.https.port) ? - '' : - `:${config.contentServers.web.https.port}`; - } else { - schema = 'http://'; - port = (80 === config.contentServers.web.http.port) ? - '' : - `:${config.contentServers.web.http.port}`; - } - - return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`; - } + let schema; + let port; + if(config.contentServers.web.https.enabled) { + schema = 'https://'; + port = (443 === config.contentServers.web.https.port) ? + '' : + `:${config.contentServers.web.https.port}`; + } else { + schema = 'http://'; + port = (80 === config.contentServers.web.http.port) ? + '' : + `:${config.contentServers.web.http.port}`; + } - isEnabled() { - return this.enableHttp || this.enableHttps; - } + return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`; + } - createServer() { - if(this.enableHttp) { - this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) ); - } + isEnabled() { + return this.enableHttp || this.enableHttps; + } - const config = Config(); - if(this.enableHttps) { - const options = { - cert : fs.readFileSync(config.contentServers.web.https.certPem), - key : fs.readFileSync(config.contentServers.web.https.keyPem), - }; + createServer() { + if(this.enableHttp) { + this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) ); + } - // additional options - Object.assign(options, config.contentServers.web.https.options || {} ); + const config = Config(); + if(this.enableHttps) { + const options = { + cert : fs.readFileSync(config.contentServers.web.https.certPem), + key : fs.readFileSync(config.contentServers.web.https.keyPem), + }; - this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) ); - } - } + // additional options + Object.assign(options, config.contentServers.web.https.options || {} ); - listen() { - let ok = true; + this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) ); + } + } - const config = Config(); - [ 'http', 'https' ].forEach(service => { - const name = `${service}Server`; - if(this[name]) { - const port = parseInt(config.contentServers.web[service].port); - if(isNaN(port)) { - ok = false; - return Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); - } - return this[name].listen(port); - } - }); + listen() { + let ok = true; - return ok; - } + const config = Config(); + [ 'http', 'https' ].forEach(service => { + const name = `${service}Server`; + if(this[name]) { + const port = parseInt(config.contentServers.web[service].port); + if(isNaN(port)) { + ok = false; + return Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); + } + return this[name].listen(port); + } + }); - addRoute(route) { - route = new Route(route); + return ok; + } - if(!route.isValid()) { - Log.warn( { route : route }, 'Cannot add route: missing or invalid required members' ); - return false; - } + addRoute(route) { + route = new Route(route); - const routeKey = route.getRouteKey(); - if(routeKey in this.routes) { - Log.warn( { route : route, routeKey : routeKey }, 'Cannot add route: duplicate method/path combination exists' ); - return false; - } + if(!route.isValid()) { + Log.warn( { route : route }, 'Cannot add route: missing or invalid required members' ); + return false; + } - this.routes[routeKey] = route; - return true; - } + const routeKey = route.getRouteKey(); + if(routeKey in this.routes) { + Log.warn( { route : route, routeKey : routeKey }, 'Cannot add route: duplicate method/path combination exists' ); + return false; + } - routeRequest(req, resp) { - const route = _.find(this.routes, r => r.matchesRequest(req) ); + this.routes[routeKey] = route; + return true; + } - if(!route && '/' === req.url) { - return this.routeIndex(req, resp); - } + routeRequest(req, resp) { + const route = _.find(this.routes, r => r.matchesRequest(req) ); - return route ? route.handler(req, resp) : this.accessDenied(resp); - } + if(!route && '/' === req.url) { + return this.routeIndex(req, resp); + } - respondWithError(resp, code, bodyText, title) { - const customErrorPage = paths.join(Config().contentServers.web.staticRoot, `${code}.html`); + return route ? route.handler(req, resp) : this.accessDenied(resp); + } - fs.readFile(customErrorPage, 'utf8', (err, data) => { - resp.writeHead(code, { 'Content-Type' : 'text/html' } ); + respondWithError(resp, code, bodyText, title) { + const customErrorPage = paths.join(Config().contentServers.web.staticRoot, `${code}.html`); - if(err) { - return resp.end(` + fs.readFile(customErrorPage, 'utf8', (err, data) => { + resp.writeHead(code, { 'Content-Type' : 'text/html' } ); + + if(err) { + return resp.end(` @@ -190,74 +190,74 @@ exports.getModule = class WebServerModule extends ServerModule { ` - ); - } + ); + } - return resp.end(data); - }); - } + return resp.end(data); + }); + } - accessDenied(resp) { - return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied'); - } + accessDenied(resp) { + return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied'); + } - fileNotFound(resp) { - return this.respondWithError(resp, 404, 'File not found.', 'File Not Found'); - } + fileNotFound(resp) { + return this.respondWithError(resp, 404, 'File not found.', 'File Not Found'); + } - routeIndex(req, resp) { - const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html'); + routeIndex(req, resp) { + const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html'); - return this.returnStaticPage(filePath, resp); - } + return this.returnStaticPage(filePath, resp); + } - routeStaticFile(req, resp) { - const fileName = req.url.substr(req.url.indexOf('/', 1)); - const filePath = paths.join(Config().contentServers.web.staticRoot, fileName); + routeStaticFile(req, resp) { + const fileName = req.url.substr(req.url.indexOf('/', 1)); + const filePath = paths.join(Config().contentServers.web.staticRoot, fileName); - return this.returnStaticPage(filePath, resp); - } + return this.returnStaticPage(filePath, resp); + } - returnStaticPage(filePath, resp) { - const self = this; + returnStaticPage(filePath, resp) { + const self = this; - fs.stat(filePath, (err, stats) => { - if(err || !stats.isFile()) { - return self.fileNotFound(resp); - } + fs.stat(filePath, (err, stats) => { + if(err || !stats.isFile()) { + return self.fileNotFound(resp); + } - const headers = { - 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'), - 'Content-Length' : stats.size, - }; + const headers = { + 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'), + 'Content-Length' : stats.size, + }; - const readStream = fs.createReadStream(filePath); - resp.writeHead(200, headers); - return readStream.pipe(resp); - }); - } + const readStream = fs.createReadStream(filePath); + resp.writeHead(200, headers); + return readStream.pipe(resp); + }); + } - routeTemplateFilePage(templatePath, preprocessCallback, resp) { - const self = this; + routeTemplateFilePage(templatePath, preprocessCallback, resp) { + const self = this; - fs.readFile(templatePath, 'utf8', (err, templateData) => { - if(err) { - return self.fileNotFound(resp); - } + fs.readFile(templatePath, 'utf8', (err, templateData) => { + if(err) { + return self.fileNotFound(resp); + } - preprocessCallback(templateData, (err, finalPage, contentType) => { - if(err || !finalPage) { - return self.respondWithError(resp, 500, 'Internal Server Error.', 'Internal Server Error'); - } + preprocessCallback(templateData, (err, finalPage, contentType) => { + if(err || !finalPage) { + return self.respondWithError(resp, 500, 'Internal Server Error.', 'Internal Server Error'); + } - const headers = { - 'Content-Type' : contentType || mimeTypes.contentType('.html'), - 'Content-Length' : finalPage.length, - }; + const headers = { + 'Content-Type' : contentType || mimeTypes.contentType('.html'), + 'Content-Length' : finalPage.length, + }; - resp.writeHead(200, headers); - return resp.end(finalPage); - }); - }); - } + resp.writeHead(200, headers); + return resp.end(finalPage); + }); + }); + } }; diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 0b8e3082..982ab0a1 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -19,215 +19,215 @@ const _ = require('lodash'); const assert = require('assert'); const ModuleInfo = exports.moduleInfo = { - name : 'SSH', - desc : 'SSH Server', - author : 'NuSkooler', - isSecure : true, - packageName : 'codes.l33t.enigma.ssh.server', + name : 'SSH', + desc : 'SSH Server', + author : 'NuSkooler', + isSecure : true, + packageName : 'codes.l33t.enigma.ssh.server', }; function SSHClient(clientConn) { - baseClient.Client.apply(this, arguments); + baseClient.Client.apply(this, arguments); - // - // WARNING: Until we have emit 'ready', self.input, and self.output and - // not yet defined! - // + // + // WARNING: Until we have emit 'ready', self.input, and self.output and + // not yet defined! + // - const self = this; + const self = this; - let loginAttempts = 0; + let loginAttempts = 0; - clientConn.on('authentication', function authAttempt(ctx) { - const username = ctx.username || ''; - const password = ctx.password || ''; + clientConn.on('authentication', function authAttempt(ctx) { + const username = ctx.username || ''; + const password = ctx.password || ''; - const config = Config(); - self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; + const config = Config(); + self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; - self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); + self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); - function terminateConnection() { - ctx.reject(); - return clientConn.end(); - } + function terminateConnection() { + ctx.reject(); + return clientConn.end(); + } - function alreadyLoggedIn(username) { - ctx.prompt(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); - return terminateConnection(); - } + function alreadyLoggedIn(username) { + ctx.prompt(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); + return terminateConnection(); + } - // - // If the system is open and |isNewUser| is true, the login - // sequence is hijacked in order to start the applicaiton process. - // - if(false === config.general.closedSystem && self.isNewUser) { - return ctx.accept(); - } + // + // If the system is open and |isNewUser| is true, the login + // sequence is hijacked in order to start the applicaiton process. + // + if(false === config.general.closedSystem && self.isNewUser) { + return ctx.accept(); + } - if(username.length > 0 && password.length > 0) { - loginAttempts += 1; + if(username.length > 0 && password.length > 0) { + loginAttempts += 1; - userLogin(self, ctx.username, ctx.password, function authResult(err) { - if(err) { - if(err.existingConn) { - return alreadyLoggedIn(username); - } + userLogin(self, ctx.username, ctx.password, function authResult(err) { + if(err) { + if(err.existingConn) { + return alreadyLoggedIn(username); + } - return ctx.reject(SSHClient.ValidAuthMethods); - } + return ctx.reject(SSHClient.ValidAuthMethods); + } - ctx.accept(); - }); - } else { - if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { - return ctx.reject(SSHClient.ValidAuthMethods); - } + ctx.accept(); + }); + } else { + if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { + return ctx.reject(SSHClient.ValidAuthMethods); + } - if(0 === username.length) { - // :TODO: can we display something here? - return ctx.reject(); - } + if(0 === username.length) { + // :TODO: can we display something here? + return ctx.reject(); + } - const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; + const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; - ctx.prompt(interactivePrompt, function retryPrompt(answers) { - loginAttempts += 1; + ctx.prompt(interactivePrompt, function retryPrompt(answers) { + loginAttempts += 1; - userLogin(self, username, (answers[0] || ''), err => { - if(err) { - if(err.existingConn) { - return alreadyLoggedIn(username); - } + userLogin(self, username, (answers[0] || ''), err => { + if(err) { + if(err.existingConn) { + return alreadyLoggedIn(username); + } - if(loginAttempts >= config.general.loginAttempts) { - return terminateConnection(); - } + if(loginAttempts >= config.general.loginAttempts) { + return terminateConnection(); + } - const artOpts = { - client : self, - name : 'SSHPMPT.ASC', - readSauce : false, - }; + const artOpts = { + client : self, + name : 'SSHPMPT.ASC', + readSauce : false, + }; - theme.getThemeArt(artOpts, (err, artInfo) => { - if(err) { - interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; - } else { - const newUserNameList = _.has(config, 'users.newUserNames') && config.users.newUserNames.length > 0 ? - config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : - '(No new user names enabled!)'; + theme.getThemeArt(artOpts, (err, artInfo) => { + if(err) { + interactivePrompt.prompt = `Access denied\n${ctx.username}'s password: `; + } else { + const newUserNameList = _.has(config, 'users.newUserNames') && config.users.newUserNames.length > 0 ? + config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : + '(No new user names enabled!)'; - interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; - } - return ctx.prompt(interactivePrompt, retryPrompt); - }); - } else { - ctx.accept(); - } - }); - }); - } - }); + interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; + } + return ctx.prompt(interactivePrompt, retryPrompt); + }); + } else { + ctx.accept(); + } + }); + }); + } + }); - this.dataHandler = function(data) { - self.emit('data', data); - }; + this.dataHandler = function(data) { + self.emit('data', data); + }; - this.updateTermInfo = function(info) { - // - // From ssh2 docs: - // "rows and cols override width and height when rows and cols are non-zero." - // - let termHeight; - let termWidth; + this.updateTermInfo = function(info) { + // + // From ssh2 docs: + // "rows and cols override width and height when rows and cols are non-zero." + // + let termHeight; + let termWidth; - if(info.rows > 0 && info.cols > 0) { - termHeight = info.rows; - termWidth = info.cols; - } else if(info.width > 0 && info.height > 0) { - termHeight = info.height; - termWidth = info.width; - } + if(info.rows > 0 && info.cols > 0) { + termHeight = info.rows; + termWidth = info.cols; + } else if(info.width > 0 && info.height > 0) { + termHeight = info.height; + termWidth = info.width; + } - assert(_.isObject(self.term)); + assert(_.isObject(self.term)); - // - // Note that if we fail here, connect.js attempts some non-standard - // queries/etc., and ultimately will default to 80x24 if all else fails - // - if(termHeight > 0 && termWidth > 0) { - self.term.termHeight = termHeight; - self.term.termWidth = termWidth; + // + // Note that if we fail here, connect.js attempts some non-standard + // queries/etc., and ultimately will default to 80x24 if all else fails + // + if(termHeight > 0 && termWidth > 0) { + self.term.termHeight = termHeight; + self.term.termWidth = termWidth; - self.clearMciCache(); // term size changes = invalidate cache - } + self.clearMciCache(); // term size changes = invalidate cache + } - if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { - self.setTermType(info.term); - } - }; + if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { + self.setTermType(info.term); + } + }; - clientConn.once('ready', function clientReady() { - self.log.info('SSH authentication success'); + clientConn.once('ready', function clientReady() { + self.log.info('SSH authentication success'); - clientConn.on('session', accept => { + clientConn.on('session', accept => { - const session = accept(); + const session = accept(); - session.on('pty', function pty(accept, reject, info) { - self.log.debug(info, 'SSH pty event'); + session.on('pty', function pty(accept, reject, info) { + self.log.debug(info, 'SSH pty event'); - if(_.isFunction(accept)) { - accept(); - } + if(_.isFunction(accept)) { + accept(); + } - if(self.input) { // do we have I/O? - self.updateTermInfo(info); - } else { - self.cachedTermInfo = info; - } - }); + if(self.input) { // do we have I/O? + self.updateTermInfo(info); + } else { + self.cachedTermInfo = info; + } + }); - session.on('shell', accept => { - self.log.debug('SSH shell event'); + session.on('shell', accept => { + self.log.debug('SSH shell event'); - const channel = accept(); + const channel = accept(); - self.setInputOutput(channel.stdin, channel.stdout); + self.setInputOutput(channel.stdin, channel.stdout); - channel.stdin.on('data', self.dataHandler); + channel.stdin.on('data', self.dataHandler); - if(self.cachedTermInfo) { - self.updateTermInfo(self.cachedTermInfo); - delete self.cachedTermInfo; - } + if(self.cachedTermInfo) { + self.updateTermInfo(self.cachedTermInfo); + delete self.cachedTermInfo; + } - // we're ready! - const firstMenu = self.isNewUser ? Config().loginServers.ssh.firstMenuNewUser : Config().loginServers.ssh.firstMenu; - self.emit('ready', { firstMenu : firstMenu } ); - }); + // we're ready! + const firstMenu = self.isNewUser ? Config().loginServers.ssh.firstMenuNewUser : Config().loginServers.ssh.firstMenu; + self.emit('ready', { firstMenu : firstMenu } ); + }); - session.on('window-change', (accept, reject, info) => { - self.log.debug(info, 'SSH window-change event'); + session.on('window-change', (accept, reject, info) => { + self.log.debug(info, 'SSH window-change event'); - if(self.input) { - self.updateTermInfo(info); - } else { - self.cachedTermInfo = info; - } - }); + if(self.input) { + self.updateTermInfo(info); + } else { + self.cachedTermInfo = info; + } + }); - }); - }); + }); + }); - clientConn.on('end', () => { - self.emit('end'); // remove client connection/tracking - }); + clientConn.on('end', () => { + self.emit('end'); // remove client connection/tracking + }); - clientConn.on('error', err => { - self.log.warn( { error : err.message, code : err.code }, 'SSH connection error'); - }); + clientConn.on('error', err => { + self.log.warn( { error : err.message, code : err.code }, 'SSH connection error'); + }); } util.inherits(SSHClient, baseClient.Client); @@ -235,47 +235,47 @@ util.inherits(SSHClient, baseClient.Client); SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ]; exports.getModule = class SSHServerModule extends LoginServerModule { - constructor() { - super(); - } + constructor() { + super(); + } - createServer() { - const config = Config(); - const serverConf = { - hostKeys : [ - { - key : fs.readFileSync(config.loginServers.ssh.privateKeyPem), - passphrase : config.loginServers.ssh.privateKeyPass, - } - ], - ident : 'enigma-bbs-' + enigVersion + '-srv', + createServer() { + const config = Config(); + const serverConf = { + hostKeys : [ + { + key : fs.readFileSync(config.loginServers.ssh.privateKeyPem), + passphrase : config.loginServers.ssh.privateKeyPass, + } + ], + ident : 'enigma-bbs-' + enigVersion + '-srv', - // Note that sending 'banner' breaks at least EtherTerm! - debug : (sshDebugLine) => { - if(true === config.loginServers.ssh.traceConnections) { - Log.trace(`SSH: ${sshDebugLine}`); - } - }, - algorithms: { compress: ['none'] }, - }; + // Note that sending 'banner' breaks at least EtherTerm! + debug : (sshDebugLine) => { + if(true === config.loginServers.ssh.traceConnections) { + Log.trace(`SSH: ${sshDebugLine}`); + } + }, + algorithms: { compress: ['none'] }, + }; - this.server = ssh2.Server(serverConf); - this.server.on('connection', (conn, info) => { - Log.info(info, 'New SSH connection'); - this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo); - }); - } + this.server = ssh2.Server(serverConf); + this.server.on('connection', (conn, info) => { + Log.info(info, 'New SSH connection'); + this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo); + }); + } - listen() { - const config = Config(); - const port = parseInt(config.loginServers.ssh.port); - if(isNaN(port)) { - Log.error( { server : ModuleInfo.name, port : config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); - return false; - } + listen() { + const config = Config(); + const port = parseInt(config.loginServers.ssh.port); + if(isNaN(port)) { + Log.error( { server : ModuleInfo.name, port : config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); + return false; + } - this.server.listen(port); - Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); - return true; - } + this.server.listen(port); + Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + return true; + } }; diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 3a58ae4e..ce854013 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -18,11 +18,11 @@ const util = require('util'); //var debug = require('debug')('telnet'); const ModuleInfo = exports.moduleInfo = { - name : 'Telnet', - desc : 'Telnet Server', - author : 'NuSkooler', - isSecure : false, - packageName : 'codes.l33t.enigma.telnet.server', + name : 'Telnet', + desc : 'Telnet Server', + author : 'NuSkooler', + isSecure : false, + packageName : 'codes.l33t.enigma.telnet.server', }; exports.TelnetClient = TelnetClient; @@ -56,22 +56,22 @@ exports.TelnetClient = TelnetClient; */ const COMMANDS = { - SE : 240, // End of Sub-Negotation Parameters - NOP : 241, // No Operation - DM : 242, // Data Mark - BRK : 243, // Break - IP : 244, // Interrupt Process - AO : 245, // Abort Output - AYT : 246, // Are You There? - EC : 247, // Erase Character - EL : 248, // Erase Line - GA : 249, // Go Ahead - SB : 250, // Start Sub-Negotiation Parameters - WILL : 251, // - WONT : 252, - DO : 253, - DONT : 254, - IAC : 255, // (Data Byte) + SE : 240, // End of Sub-Negotation Parameters + NOP : 241, // No Operation + DM : 242, // Data Mark + BRK : 243, // Break + IP : 244, // Interrupt Process + AO : 245, // Abort Output + AYT : 246, // Are You There? + EC : 247, // Erase Character + EL : 248, // Erase Line + GA : 249, // Go Ahead + SB : 250, // Start Sub-Negotiation Parameters + WILL : 251, // + WONT : 252, + DO : 253, + DONT : 254, + IAC : 255, // (Data Byte) }; // @@ -79,9 +79,9 @@ const COMMANDS = { // * http://www.faqs.org/rfcs/rfc1572.html // const SB_COMMANDS = { - IS : 0, - SEND : 1, - INFO : 2, + IS : 0, + SEND : 1, + INFO : 2, }; // @@ -92,104 +92,104 @@ const SB_COMMANDS = { // * http://www.networksorcery.com/enp/protocol/telnet.htm // const OPTIONS = { - TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 - ECHO : 1, // http://tools.ietf.org/html/rfc857 - // RECONNECTION : 2 - SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 - //APPROX_MESSAGE_SIZE : 4 - STATUS : 5, // http://tools.ietf.org/html/rfc859 - TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 - //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt - //OUPUT_LINE_WIDTH : 8, - //OUTPUT_PAGE_SIZE : 9, // - //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 - //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 - //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 - //OUTPUT_FORMFEED_DISP : 13, // RFC 655 - //OUTPUT_VERT_TABSTOPS : 14, // RFC 656 - //OUTPUT_VERT_TAB_DISP : 15, // RFC 657 - //OUTPUT_LF_DISP : 16, // RFC 658 - //EXTENDED_ASCII : 17, // RFC 659 - //LOGOUT : 18, // RFC 727 - //BYTE_MACRO : 19, // RFC 753 - //DATA_ENTRY_TERMINAL : 20, // RFC 1043 - //SUPDUP : 21, // RFC 736 - //SUPDUP_OUTPUT : 22, // RFC 749 - SEND_LOCATION : 23, // RFC 779 - TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 - //END_OF_RECORD : 25, // RFC 885 - //TACACS_USER_ID : 26, // RFC 927 - //OUTPUT_MARKING : 27, // RFC 933 - //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 - //TELNET_3270_REGIME : 29, // RFC 1041 - WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 - TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 - REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 - LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 - X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 - NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) - AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 - ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 - NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) - //TN3270E : 40, // RFC 2355 - //XAUTH : 41, - //CHARSET : 42, // RFC 2066 - //REMOTE_SERIAL_PORT : 43, - //COM_PORT_CONTROL : 44, // RFC 2217 - //SUPRESS_LOCAL_ECHO : 45, - //START_TLS : 46, - //KERMIT : 47, // RFC 2840 - //SEND_URL : 48, - //FORWARD_X : 49, + TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 + ECHO : 1, // http://tools.ietf.org/html/rfc857 + // RECONNECTION : 2 + SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 + //APPROX_MESSAGE_SIZE : 4 + STATUS : 5, // http://tools.ietf.org/html/rfc859 + TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 + //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt + //OUPUT_LINE_WIDTH : 8, + //OUTPUT_PAGE_SIZE : 9, // + //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 + //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 + //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 + //OUTPUT_FORMFEED_DISP : 13, // RFC 655 + //OUTPUT_VERT_TABSTOPS : 14, // RFC 656 + //OUTPUT_VERT_TAB_DISP : 15, // RFC 657 + //OUTPUT_LF_DISP : 16, // RFC 658 + //EXTENDED_ASCII : 17, // RFC 659 + //LOGOUT : 18, // RFC 727 + //BYTE_MACRO : 19, // RFC 753 + //DATA_ENTRY_TERMINAL : 20, // RFC 1043 + //SUPDUP : 21, // RFC 736 + //SUPDUP_OUTPUT : 22, // RFC 749 + SEND_LOCATION : 23, // RFC 779 + TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 + //END_OF_RECORD : 25, // RFC 885 + //TACACS_USER_ID : 26, // RFC 927 + //OUTPUT_MARKING : 27, // RFC 933 + //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 + //TELNET_3270_REGIME : 29, // RFC 1041 + WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 + TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 + REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 + LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 + X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 + NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) + AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 + ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 + NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) + //TN3270E : 40, // RFC 2355 + //XAUTH : 41, + //CHARSET : 42, // RFC 2066 + //REMOTE_SERIAL_PORT : 43, + //COM_PORT_CONTROL : 44, // RFC 2217 + //SUPRESS_LOCAL_ECHO : 45, + //START_TLS : 46, + //KERMIT : 47, // RFC 2840 + //SEND_URL : 48, + //FORWARD_X : 49, - //PRAGMA_LOGON : 138, - //SSPI_LOGON : 139, - //PRAGMA_HEARTBEAT : 140 + //PRAGMA_LOGON : 138, + //SSPI_LOGON : 139, + //PRAGMA_HEARTBEAT : 140 - ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854 + ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854 - EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) + EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) }; // Commands used within NEW_ENVIRONMENT[_DEP] const NEW_ENVIRONMENT_COMMANDS = { - VAR : 0, - VALUE : 1, - ESC : 2, - USERVAR : 3, + VAR : 0, + VALUE : 1, + ESC : 2, + USERVAR : 3, }; const IAC_BUF = Buffer.from([ COMMANDS.IAC ]); const IAC_SE_BUF = Buffer.from([ COMMANDS.IAC, COMMANDS.SE ]); const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { - names[COMMANDS[name]] = name.toLowerCase(); - return names; + names[COMMANDS[name]] = name.toLowerCase(); + return names; }, {}); const COMMAND_IMPLS = {}; [ 'do', 'dont', 'will', 'wont', 'sb' ].forEach(function(command) { - const code = COMMANDS[command.toUpperCase()]; - COMMAND_IMPLS[code] = function(bufs, i, event) { - if(bufs.length < (i + 1)) { - return MORE_DATA_REQUIRED; - } - return parseOption(bufs, i, event); - }; + const code = COMMANDS[command.toUpperCase()]; + COMMAND_IMPLS[code] = function(bufs, i, event) { + if(bufs.length < (i + 1)) { + return MORE_DATA_REQUIRED; + } + return parseOption(bufs, i, event); + }; }); // :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode // Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) { - names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' '); - return names; + names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' '); + return names; }, {}); function unknownOption(bufs, i, event) { - Log.warn( { bufs : bufs, i : i, event : event }, 'Unknown Telnet option'); - event.buf = bufs.splice(0, i).toBuffer(); - return event; + Log.warn( { bufs : bufs, i : i, event : event }, 'Unknown Telnet option'); + event.buf = bufs.splice(0, i).toBuffer(); + return event; } const OPTION_IMPLS = {}; @@ -206,371 +206,371 @@ OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = OPTION_IMPLS[OPTIONS.SEND_LOCATION] = OPTION_IMPLS[OPTIONS.ARE_YOU_THERE] = OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { - event.buf = bufs.splice(0, i).toBuffer(); - return event; + event.buf = bufs.splice(0, i).toBuffer(); + return event; }; OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // We need 4 bytes header + data + IAC SE - if(bufs.length < 7) { - return MORE_DATA_REQUIRED; - } + if(event.commandCode !== COMMANDS.SB) { + OPTION_IMPLS.NO_ARGS(bufs, i, event); + } else { + // We need 4 bytes header + data + IAC SE + if(bufs.length < 7) { + return MORE_DATA_REQUIRED; + } - const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } + const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes + if(-1 === end) { + return MORE_DATA_REQUIRED; + } - let ttypeCmd; - try { - ttypeCmd = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint8('is') - .array('ttype', { - type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC - }) - // note we read iac2 above - .uint8('se') - .parse(bufs.toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing TTYP telnet command'); - return event; - } + let ttypeCmd; + try { + ttypeCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint8('is') + .array('ttype', { + type : 'uint8', + readUntil : b => 255 === b, // 255=COMMANDS.IAC + }) + // note we read iac2 above + .uint8('se') + .parse(bufs.toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing TTYP telnet command'); + return event; + } - EnigAssert(COMMANDS.IAC === ttypeCmd.iac1); - EnigAssert(COMMANDS.SB === ttypeCmd.sb); - EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); - EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); - EnigAssert(ttypeCmd.ttype.length > 0); - // note we found IAC_SE above + EnigAssert(COMMANDS.IAC === ttypeCmd.iac1); + EnigAssert(COMMANDS.SB === ttypeCmd.sb); + EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); + EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); + EnigAssert(ttypeCmd.ttype.length > 0); + // note we found IAC_SE above - // some terminals such as NetRunner provide a NULL-terminated buffer - // slice to remove IAC - event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); + // some terminals such as NetRunner provide a NULL-terminated buffer + // slice to remove IAC + event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); - bufs.splice(0, end); - } + bufs.splice(0, end); + } - return event; + return event; }; OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // we need 9 bytes - if(bufs.length < 9) { - return MORE_DATA_REQUIRED; - } + if(event.commandCode !== COMMANDS.SB) { + OPTION_IMPLS.NO_ARGS(bufs, i, event); + } else { + // we need 9 bytes + if(bufs.length < 9) { + return MORE_DATA_REQUIRED; + } - let nawsCmd; - try { - nawsCmd = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint16be('width') - .uint16be('height') - .uint8('iac2') - .uint8('se') - .parse(bufs.splice(0, 9).toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing NAWS telnet command'); - return event; - } + let nawsCmd; + try { + nawsCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint16be('width') + .uint16be('height') + .uint8('iac2') + .uint8('se') + .parse(bufs.splice(0, 9).toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing NAWS telnet command'); + return event; + } - EnigAssert(COMMANDS.IAC === nawsCmd.iac1); - EnigAssert(COMMANDS.SB === nawsCmd.sb); - EnigAssert(OPTIONS.WINDOW_SIZE === nawsCmd.opt); - EnigAssert(COMMANDS.IAC === nawsCmd.iac2); - EnigAssert(COMMANDS.SE === nawsCmd.se); + EnigAssert(COMMANDS.IAC === nawsCmd.iac1); + EnigAssert(COMMANDS.SB === nawsCmd.sb); + EnigAssert(OPTIONS.WINDOW_SIZE === nawsCmd.opt); + EnigAssert(COMMANDS.IAC === nawsCmd.iac2); + EnigAssert(COMMANDS.SE === nawsCmd.se); - event.cols = event.columns = event.width = nawsCmd.width; - event.rows = event.height = nawsCmd.height; - } - return event; + event.cols = event.columns = event.width = nawsCmd.width; + event.rows = event.height = nawsCmd.height; + } + return event; }; // Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] const NEW_ENVIRONMENT_DELIMITERS = []; Object.keys(NEW_ENVIRONMENT_COMMANDS).forEach(function onKey(k) { - NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]); + NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]); }); // Handle the deprecated RFC 1408 & the updated RFC 1572: OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { - if(event.commandCode !== COMMANDS.SB) { - OPTION_IMPLS.NO_ARGS(bufs, i, event); - } else { - // - // We need 4 bytes header + + IAC SE - // Many terminals send a empty list: - // IAC SB NEW-ENVIRON IS IAC SE - // - if(bufs.length < 6) { - return MORE_DATA_REQUIRED; - } + if(event.commandCode !== COMMANDS.SB) { + OPTION_IMPLS.NO_ARGS(bufs, i, event); + } else { + // + // We need 4 bytes header + + IAC SE + // Many terminals send a empty list: + // IAC SB NEW-ENVIRON IS IAC SE + // + if(bufs.length < 6) { + return MORE_DATA_REQUIRED; + } - let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes - if(-1 === end) { - return MORE_DATA_REQUIRED; - } + let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes + if(-1 === end) { + return MORE_DATA_REQUIRED; + } - // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. + // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. - let envCmd; - try { - envCmd = new Parser() - .uint8('iac1') - .uint8('sb') - .uint8('opt') - .uint8('isOrInfo') // IS=initial, INFO=updates - .array('envBlock', { - type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC - }) - // note we consume IAC above - .uint8('se') - .parse(bufs.splice(0, bufs.length).toBuffer()); - } catch(e) { - Log.debug( { error : e }, 'Failed parsing NEW-ENVIRON telnet command'); - return event; - } + let envCmd; + try { + envCmd = new Parser() + .uint8('iac1') + .uint8('sb') + .uint8('opt') + .uint8('isOrInfo') // IS=initial, INFO=updates + .array('envBlock', { + type : 'uint8', + readUntil : b => 255 === b, // 255=COMMANDS.IAC + }) + // note we consume IAC above + .uint8('se') + .parse(bufs.splice(0, bufs.length).toBuffer()); + } catch(e) { + Log.debug( { error : e }, 'Failed parsing NEW-ENVIRON telnet command'); + return event; + } - EnigAssert(COMMANDS.IAC === envCmd.iac1); - EnigAssert(COMMANDS.SB === envCmd.sb); - EnigAssert(OPTIONS.NEW_ENVIRONMENT === envCmd.opt || OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt); - EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); + EnigAssert(COMMANDS.IAC === envCmd.iac1); + EnigAssert(COMMANDS.SB === envCmd.sb); + EnigAssert(OPTIONS.NEW_ENVIRONMENT === envCmd.opt || OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt); + EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); - if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { - // :TODO: we should probably support this for legacy clients? - Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); - } + if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { + // :TODO: we should probably support this for legacy clients? + Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); + } - const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC + const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC - if(envBuf.length < 4) { // TYPE + single char name + sep + single char value - // empty env block - return event; - } + if(envBuf.length < 4) { // TYPE + single char name + sep + single char value + // empty env block + return event; + } - const States = { - Name : 1, - Value : 2, - }; + const States = { + Name : 1, + Value : 2, + }; - let state = States.Name; - const setVars = {}; - const delVars = []; - let varName; - // :TODO: handle ESC type!!! - while(envBuf.length) { - switch(state) { - case States.Name : - { - const type = parseInt(envBuf.splice(0, 1)); - if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { - return event; // fail :( - } + let state = States.Name; + const setVars = {}; + const delVars = []; + let varName; + // :TODO: handle ESC type!!! + while(envBuf.length) { + switch(state) { + case States.Name : + { + const type = parseInt(envBuf.splice(0, 1)); + if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { + return event; // fail :( + } - let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); - if(-1 === nameEnd) { - nameEnd = envBuf.length; - } + let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); + if(-1 === nameEnd) { + nameEnd = envBuf.length; + } - varName = envBuf.splice(0, nameEnd); - if(!varName) { - return event; // something is wrong. - } + varName = envBuf.splice(0, nameEnd); + if(!varName) { + return event; // something is wrong. + } - varName = Buffer.from(varName).toString('ascii'); + varName = Buffer.from(varName).toString('ascii'); - const next = parseInt(envBuf.splice(0, 1)); - if(NEW_ENVIRONMENT_COMMANDS.VALUE === next) { - state = States.Value; - } else { - state = States.Name; - delVars.push(varName); // no value; del this var - } - } - break; + const next = parseInt(envBuf.splice(0, 1)); + if(NEW_ENVIRONMENT_COMMANDS.VALUE === next) { + state = States.Value; + } else { + state = States.Name; + delVars.push(varName); // no value; del this var + } + } + break; - case States.Value : - { - let valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VAR); - if(-1 === valueEnd) { - valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.USERVAR); - } - if(-1 === valueEnd) { - valueEnd = envBuf.length; - } + case States.Value : + { + let valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VAR); + if(-1 === valueEnd) { + valueEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.USERVAR); + } + if(-1 === valueEnd) { + valueEnd = envBuf.length; + } - let value = envBuf.splice(0, valueEnd); - if(value) { - value = Buffer.from(value).toString('ascii'); - setVars[varName] = value; - } - state = States.Name; - } - break; - } - } + let value = envBuf.splice(0, valueEnd); + if(value) { + value = Buffer.from(value).toString('ascii'); + setVars[varName] = value; + } + state = States.Name; + } + break; + } + } - // :TODO: Handle deleting previously set vars via delVars - event.type = envCmd.isOrInfo; - event.envVars = setVars; - } + // :TODO: Handle deleting previously set vars via delVars + event.type = envCmd.isOrInfo; + event.envVars = setVars; + } - return event; + return event; }; const MORE_DATA_REQUIRED = 0xfeedface; function parseBufs(bufs) { - EnigAssert(bufs.length >= 2); - EnigAssert(bufs.get(0) === COMMANDS.IAC); - return parseCommand(bufs, 1, {}); + EnigAssert(bufs.length >= 2); + EnigAssert(bufs.get(0) === COMMANDS.IAC); + return parseCommand(bufs, 1, {}); } function parseCommand(bufs, i, event) { - const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.commandCode = command; - event.command = COMMAND_NAMES[command]; + const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same + event.commandCode = command; + event.command = COMMAND_NAMES[command]; - const handler = COMMAND_IMPLS[command]; - if(handler) { - return handler(bufs, i + 1, event); - } else { - if(2 !== bufs.length) { - Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND - } + const handler = COMMAND_IMPLS[command]; + if(handler) { + return handler(bufs, i + 1, event); + } else { + if(2 !== bufs.length) { + Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND + } - event.buf = bufs.splice(0, 2).toBuffer(); - return event; - } + event.buf = bufs.splice(0, 2).toBuffer(); + return event; + } } function parseOption(bufs, i, event) { - const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.optionCode = option; - event.option = OPTION_NAMES[option]; + const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same + event.optionCode = option; + event.option = OPTION_NAMES[option]; - const handler = OPTION_IMPLS[option]; - return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event); + const handler = OPTION_IMPLS[option]; + return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event); } function TelnetClient(input, output) { - baseClient.Client.apply(this, arguments); + baseClient.Client.apply(this, arguments); - const self = this; + const self = this; - let bufs = buffers(); - this.bufs = bufs; + let bufs = buffers(); + this.bufs = bufs; - this.sentDont = {}; // DON'T's we've already sent + this.sentDont = {}; // DON'T's we've already sent - this.setInputOutput(input, output); + this.setInputOutput(input, output); - this.negotiationsComplete = false; // are we in the 'negotiation' phase? - this.didReady = false; // have we emit the 'ready' event? + this.negotiationsComplete = false; // are we in the 'negotiation' phase? + this.didReady = false; // have we emit the 'ready' event? - this.subNegotiationState = { - newEnvironRequested : false, - }; + this.subNegotiationState = { + newEnvironRequested : false, + }; - this.dataHandler = function(b) { - if(!Buffer.isBuffer(b)) { - EnigAssert(false, `Cannot push non-buffer ${typeof b}`); - return; - } + 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; - while((i = bufs.indexOf(IAC_BUF)) >= 0) { + let i; + while((i = bufs.indexOf(IAC_BUF)) >= 0) { - // - // Some clients will send even IAC separate from data - // - if(bufs.length <= (i + 1)) { - i = MORE_DATA_REQUIRED; - break; - } + // + // Some clients will send even IAC separate from data + // + if(bufs.length <= (i + 1)) { + i = MORE_DATA_REQUIRED; + break; + } - EnigAssert(bufs.length > (i + 1)); + EnigAssert(bufs.length > (i + 1)); - if(i > 0) { - self.emit('data', bufs.splice(0, i).toBuffer()); - } + if(i > 0) { + self.emit('data', bufs.splice(0, i).toBuffer()); + } - i = parseBufs(bufs); + i = parseBufs(bufs); - if(MORE_DATA_REQUIRED === i) { - break; - } else if(i) { - if(i.option) { - self.emit(i.option, i); // "transmit binary", "echo", ... - } + if(MORE_DATA_REQUIRED === i) { + break; + } else if(i) { + if(i.option) { + self.emit(i.option, i); // "transmit binary", "echo", ... + } - self.handleTelnetEvent(i); + self.handleTelnetEvent(i); - if(i.data) { - self.emit('data', i.data); - } - } - } + if(i.data) { + self.emit('data', i.data); + } + } + } - if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { - // - // Standard data payload. This can still be "non-user" data - // such as ANSI control, but we don't handle that here. - // - self.emit('data', bufs.splice(0).toBuffer()); - } - }; + if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { + // + // Standard data payload. This can still be "non-user" data + // such as ANSI control, but we don't handle that here. + // + self.emit('data', bufs.splice(0).toBuffer()); + } + }; - this.input.on('data', this.dataHandler); + this.input.on('data', this.dataHandler); - this.input.on('end', () => { - self.emit('end'); - }); + this.input.on('end', () => { + self.emit('end'); + }); - this.input.on('error', err => { - this.connectionDebug( { err : err }, 'Socket error' ); - return self.emit('end'); - }); + this.input.on('error', err => { + this.connectionDebug( { err : err }, 'Socket error' ); + return self.emit('end'); + }); - this.connectionTrace = (info, msg) => { - if(Config().loginServers.telnet.traceConnections) { - const logger = self.log || Log; - return logger.trace(info, `Telnet: ${msg}`); - } - }; + this.connectionTrace = (info, msg) => { + if(Config().loginServers.telnet.traceConnections) { + 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.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}`); - }; + this.connectionWarn = (info, msg) => { + const logger = self.log || Log; + return logger.warn(info, `Telnet: ${msg}`); + }; - this.readyNow = () => { - if(!this.didReady) { - this.didReady = true; - this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); - } - }; + this.readyNow = () => { + if(!this.didReady) { + this.didReady = true; + this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); + } + }; } util.inherits(TelnetClient, baseClient.Client); @@ -580,314 +580,314 @@ util.inherits(TelnetClient, baseClient.Client); /////////////////////////////////////////////////////////////////////////////// TelnetClient.prototype.handleTelnetEvent = function(evt) { - if(!evt.command) { - return this.connectionWarn( { evt : evt }, 'No command for event'); - } + if(!evt.command) { + return this.connectionWarn( { evt : evt }, 'No command for event'); + } - // handler name e.g. 'handleWontCommand' - const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; + // handler name e.g. 'handleWontCommand' + const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; - if(this[handlerName]) { - // specialized - this[handlerName](evt); - } else { - // generic-ish - this.handleMiscCommand(evt); - } + if(this[handlerName]) { + // specialized + this[handlerName](evt); + } else { + // generic-ish + this.handleMiscCommand(evt); + } }; TelnetClient.prototype.handleWillCommand = function(evt) { - if('terminal type' === evt.option) { - // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // - this.requestTerminalType(); - } else if('new environment' === evt.option) { - // - // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html - // - this.requestNewEnvironment(); - } else { - // :TODO: temporary: - this.connectionTrace(evt, 'WILL'); - } + if('terminal type' === evt.option) { + // + // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // + this.requestTerminalType(); + } else if('new environment' === evt.option) { + // + // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html + // + this.requestNewEnvironment(); + } else { + // :TODO: temporary: + this.connectionTrace(evt, 'WILL'); + } }; TelnetClient.prototype.handleWontCommand = function(evt) { - if(this.sentDont[evt.option]) { - return this.connectionTrace(evt, 'WONT - DON\'T already sent'); - } + if(this.sentDont[evt.option]) { + return this.connectionTrace(evt, 'WONT - DON\'T already sent'); + } - this.sentDont[evt.option] = true; + this.sentDont[evt.option] = true; - if('new environment' === evt.option) { - this.dont.new_environment(); - } else { - this.connectionTrace(evt, 'WONT'); - } + if('new environment' === evt.option) { + this.dont.new_environment(); + } else { + this.connectionTrace(evt, 'WONT'); + } }; TelnetClient.prototype.handleDoCommand = function(evt) { - // :TODO: handle the rest, e.g. echo nd the like + // :TODO: handle the rest, e.g. echo nd the like - if('linemode' === evt.option) { - // - // Client wants to enable linemode editing. Denied. - // - this.wont.linemode(); - } else if('encrypt' === evt.option) { - // - // Client wants to enable encryption. Denied. - // - this.wont.encrypt(); - } else { - // :TODO: temporary: - this.connectionTrace(evt, 'DO'); - } + if('linemode' === evt.option) { + // + // Client wants to enable linemode editing. Denied. + // + this.wont.linemode(); + } else if('encrypt' === evt.option) { + // + // Client wants to enable encryption. Denied. + // + this.wont.encrypt(); + } else { + // :TODO: temporary: + this.connectionTrace(evt, 'DO'); + } }; TelnetClient.prototype.handleDontCommand = function(evt) { - this.connectionTrace(evt, 'DONT'); + this.connectionTrace(evt, 'DONT'); }; TelnetClient.prototype.handleSbCommand = function(evt) { - const self = this; + const self = this; - if('terminal type' === evt.option) { - // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // - // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // We should keep asking until we see a repeat. From there, determine the best type/etc. - self.setTermType(evt.ttype); + if('terminal type' === evt.option) { + // + // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // + // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // We should keep asking until we see a repeat. From there, determine the best type/etc. + self.setTermType(evt.ttype); - self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout + self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout - self.readyNow(); - } else if('new environment' === evt.option) { - // - // Handling is as follows: - // * Map 'TERM' -> 'termType' and only update if ours is 'unknown' - // * Map COLUMNS -> 'termWidth' and only update if ours is 0 - // * Map ROWS -> 'termHeight' and only update if ours is 0 - // * Add any new variables, ignore any existing - // - Object.keys(evt.envVars || {} ).forEach(function onEnv(name) { - if('TERM' === name && 'unknown' === self.term.termType) { - self.setTermType(evt.envVars[name]); - } else if('COLUMNS' === name && 0 === self.term.termWidth) { - self.term.termWidth = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache - self.connectionDebug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); - } else if('ROWS' === name && 0 === self.term.termHeight) { - self.term.termHeight = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache - self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); - } else { - if(name in self.term.env) { + self.readyNow(); + } else if('new environment' === evt.option) { + // + // Handling is as follows: + // * Map 'TERM' -> 'termType' and only update if ours is 'unknown' + // * Map COLUMNS -> 'termWidth' and only update if ours is 0 + // * Map ROWS -> 'termHeight' and only update if ours is 0 + // * Add any new variables, ignore any existing + // + Object.keys(evt.envVars || {} ).forEach(function onEnv(name) { + if('TERM' === name && 'unknown' === self.term.termType) { + self.setTermType(evt.envVars[name]); + } else if('COLUMNS' === name && 0 === self.term.termWidth) { + self.term.termWidth = parseInt(evt.envVars[name]); + self.clearMciCache(); // term size changes = invalidate cache + self.connectionDebug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); + } else if('ROWS' === name && 0 === self.term.termHeight) { + self.term.termHeight = parseInt(evt.envVars[name]); + self.clearMciCache(); // term size changes = invalidate cache + self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); + } else { + if(name in self.term.env) { - EnigAssert( - SB_COMMANDS.INFO === evt.type || SB_COMMANDS.IS === evt.type, - 'Unexpected type: ' + evt.type - ); + 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] }, - 'Environment variable already exists' - ); - } else { - self.term.env[name] = evt.envVars[name]; - self.connectionDebug( { varName : name, value : evt.envVars[name] }, 'New environment variable' ); - } - } - }); + self.connectionWarn( + { varName : name, value : evt.envVars[name], existingValue : self.term.env[name] }, + 'Environment variable already exists' + ); + } else { + self.term.env[name] = evt.envVars[name]; + self.connectionDebug( { varName : name, value : evt.envVars[name] }, 'New environment variable' ); + } + } + }); - } else if('window size' === evt.option) { - // - // Update termWidth & termHeight. - // Set LINES and COLUMNS environment variables as well. - // - self.term.termWidth = evt.width; - self.term.termHeight = evt.height; + } else if('window size' === evt.option) { + // + // Update termWidth & termHeight. + // Set LINES and COLUMNS environment variables as well. + // + self.term.termWidth = evt.width; + self.term.termHeight = evt.height; - if(evt.width > 0) { - self.term.env.COLUMNS = evt.height; - } + if(evt.width > 0) { + self.term.env.COLUMNS = evt.height; + } - if(evt.height > 0) { - self.term.env.ROWS = evt.height; - } + if(evt.height > 0) { + self.term.env.ROWS = evt.height; + } - self.clearMciCache(); // term size changes = invalidate cache + self.clearMciCache(); // term size changes = invalidate cache - self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); - } else { - self.connectionDebug(evt, 'SB'); - } + self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); + } else { + self.connectionDebug(evt, 'SB'); + } }; const IGNORED_COMMANDS = []; [ COMMANDS.EL, COMMANDS.GA, COMMANDS.NOP, COMMANDS.DM, COMMANDS.BRK ].forEach(function onCommandCode(cc) { - IGNORED_COMMANDS.push(cc); + IGNORED_COMMANDS.push(cc); }); TelnetClient.prototype.handleMiscCommand = function(evt) { - EnigAssert(evt.command !== 'undefined' && evt.command.length > 0); + EnigAssert(evt.command !== 'undefined' && evt.command.length > 0); - // - // See: - // * RFC 854 @ http://tools.ietf.org/html/rfc854 - // - if('ip' === evt.command) { - // Interrupt Process (IP) - this.log.debug('Interrupt Process (IP) - Ending'); + // + // See: + // * RFC 854 @ http://tools.ietf.org/html/rfc854 + // + if('ip' === evt.command) { + // Interrupt Process (IP) + this.log.debug('Interrupt Process (IP) - Ending'); - this.input.end(); - } else if('ayt' === evt.command) { - this.output.write('\b'); + this.input.end(); + } else if('ayt' === evt.command) { + this.output.write('\b'); - this.log.debug('Are You There (AYT) - Replied "\\b"'); - } else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) { - this.log.debug({ evt : evt }, 'Ignoring command'); - } else { - this.log.warn({ evt : evt }, 'Unknown command'); - } + this.log.debug('Are You There (AYT) - Replied "\\b"'); + } else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) { + this.log.debug({ evt : evt }, 'Ignoring command'); + } else { + this.log.warn({ evt : evt }, 'Unknown command'); + } }; TelnetClient.prototype.requestTerminalType = function() { - const buf = Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.TERMINAL_TYPE, - SB_COMMANDS.SEND, - COMMANDS.IAC, - COMMANDS.SE ]); - this.output.write(buf); + const buf = Buffer.from( [ + COMMANDS.IAC, + COMMANDS.SB, + OPTIONS.TERMINAL_TYPE, + SB_COMMANDS.SEND, + COMMANDS.IAC, + COMMANDS.SE ]); + this.output.write(buf); }; const WANTED_ENVIRONMENT_VAR_BUFS = [ - Buffer.from( 'LINES' ), - Buffer.from( 'COLUMNS' ), - Buffer.from( 'TERM' ), - Buffer.from( 'TERM_PROGRAM' ) + Buffer.from( 'LINES' ), + Buffer.from( 'COLUMNS' ), + Buffer.from( 'TERM' ), + Buffer.from( 'TERM_PROGRAM' ) ]; TelnetClient.prototype.requestNewEnvironment = function() { - if(this.subNegotiationState.newEnvironRequested) { - this.log.debug('New environment already requested'); - return; - } + if(this.subNegotiationState.newEnvironRequested) { + this.log.debug('New environment already requested'); + return; + } - const self = this; + const self = this; - const bufs = buffers(); - bufs.push(Buffer.from( [ - COMMANDS.IAC, - COMMANDS.SB, - OPTIONS.NEW_ENVIRONMENT, - SB_COMMANDS.SEND ] - )); + const bufs = buffers(); + bufs.push(Buffer.from( [ + COMMANDS.IAC, + COMMANDS.SB, + OPTIONS.NEW_ENVIRONMENT, + SB_COMMANDS.SEND ] + )); - for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { - bufs.push(Buffer.from( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); - } + for(let i = 0; i < WANTED_ENVIRONMENT_VAR_BUFS.length; ++i) { + bufs.push(Buffer.from( [ NEW_ENVIRONMENT_COMMANDS.VAR ] ), WANTED_ENVIRONMENT_VAR_BUFS[i] ); + } - bufs.push(Buffer.from([ NEW_ENVIRONMENT_COMMANDS.USERVAR, COMMANDS.IAC, COMMANDS.SE ])); + bufs.push(Buffer.from([ NEW_ENVIRONMENT_COMMANDS.USERVAR, COMMANDS.IAC, COMMANDS.SE ])); - self.output.write(bufs.toBuffer()); + self.output.write(bufs.toBuffer()); - this.subNegotiationState.newEnvironRequested = true; + this.subNegotiationState.newEnvironRequested = true; }; TelnetClient.prototype.banner = function() { - this.will.echo(); + this.will.echo(); - this.will.suppress_go_ahead(); - this.do.suppress_go_ahead(); + this.will.suppress_go_ahead(); + this.do.suppress_go_ahead(); - this.do.transmit_binary(); - this.will.transmit_binary(); + this.do.transmit_binary(); + this.will.transmit_binary(); - this.do.terminal_type(); + this.do.terminal_type(); - this.do.window_size(); - this.do.new_environment(); + this.do.window_size(); + this.do.new_environment(); }; function Command(command, client) { - this.command = COMMANDS[command.toUpperCase()]; - this.client = client; + this.command = COMMANDS[command.toUpperCase()]; + this.client = client; } // Create Command objects with echo, transmit_binary, ... Object.keys(OPTIONS).forEach(function(name) { - const code = OPTIONS[name]; + const code = OPTIONS[name]; - Command.prototype[name.toLowerCase()] = function() { - const buf = Buffer.alloc(3); - buf[0] = COMMANDS.IAC; - buf[1] = this.command; - buf[2] = code; - return this.client.output.write(buf); - }; + Command.prototype[name.toLowerCase()] = function() { + const buf = Buffer.alloc(3); + buf[0] = COMMANDS.IAC; + buf[1] = this.command; + buf[2] = code; + return this.client.output.write(buf); + }; }); // Create do, dont, etc. methods on Client ['do', 'dont', 'will', 'wont'].forEach(function(command) { - const get = function() { - return new Command(command, this); - }; + const get = function() { + return new Command(command, this); + }; - Object.defineProperty(TelnetClient.prototype, command, { - get : get, - enumerable : true, - configurable : true - }); + Object.defineProperty(TelnetClient.prototype, command, { + get : get, + enumerable : true, + configurable : true + }); }); exports.getModule = class TelnetServerModule extends LoginServerModule { - constructor() { - super(); - } + constructor() { + super(); + } - createServer() { - this.server = net.createServer( sock => { - const client = new TelnetClient(sock, sock); + createServer() { + this.server = net.createServer( sock => { + const client = new TelnetClient(sock, sock); - client.banner(); + client.banner(); - this.handleNewClient(client, sock, ModuleInfo); + this.handleNewClient(client, sock, ModuleInfo); - // - // Set a timeout and attempt to proceed even if we don't know - // the term type yet, which is the preferred trigger - // for moving along - // - setTimeout( () => { - if(!client.didReady) { - Log.info('Proceeding after 3s without knowing term type'); - client.readyNow(); - } - }, 3000); - }); + // + // Set a timeout and attempt to proceed even if we don't know + // the term type yet, which is the preferred trigger + // for moving along + // + setTimeout( () => { + if(!client.didReady) { + Log.info('Proceeding after 3s without knowing term type'); + client.readyNow(); + } + }, 3000); + }); - this.server.on('error', err => { - Log.info( { error : err.message }, 'Telnet server error'); - }); - } + this.server.on('error', err => { + Log.info( { error : err.message }, 'Telnet server error'); + }); + } - listen() { - const config = Config(); - const port = parseInt(config.loginServers.telnet.port); - if(isNaN(port)) { - Log.error( { server : ModuleInfo.name, port : config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); - return false; - } + listen() { + const config = Config(); + const port = parseInt(config.loginServers.telnet.port); + if(isNaN(port)) { + Log.error( { server : ModuleInfo.name, port : config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); + return false; + } - this.server.listen(port); - Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); - return true; - } + this.server.listen(port); + Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + return true; + } }; diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index e30d0303..f7dac07d 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -16,93 +16,93 @@ const fs = require('graceful-fs'); const Writable = require('stream'); const ModuleInfo = exports.moduleInfo = { - name : 'WebSocket', - desc : 'WebSocket Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.websocket.server', + name : 'WebSocket', + desc : 'WebSocket Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.websocket.server', }; function WebSocketClient(ws, req, serverType) { - Object.defineProperty(this, 'isSecure', { - get : () => ('secure' === serverType || true === this.proxied) ? true : false, - }); + Object.defineProperty(this, 'isSecure', { + get : () => ('secure' === serverType || true === this.proxied) ? true : false, + }); - const self = this; + const self = this; - this.dataHandler = function(data) { - self.socketBridge.emit('data', data); - }; + this.dataHandler = function(data) { + self.socketBridge.emit('data', data); + }; - // - // This bridge makes accessible various calls that client sub classes - // want to access on I/O socket - // - this.socketBridge = new class SocketBridge extends Writable { - constructor(ws) { - super(); - this.ws = ws; - } + // + // This bridge makes accessible various calls that client sub classes + // want to access on I/O socket + // + this.socketBridge = new class SocketBridge extends Writable { + constructor(ws) { + super(); + this.ws = ws; + } - end() { - return ws.close(); - } + end() { + return ws.close(); + } - write(data, cb) { - cb = cb || ( () => { /* eat it up */} ); // handle data writes after close + 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()'); - } + // we need to fake some streaming work + unpipe() { + Log.trace('WebSocket SocketBridge unpipe()'); + } - resume() { - Log.trace('WebSocket SocketBridge resume()'); - } + resume() { + Log.trace('WebSocket SocketBridge resume()'); + } - get remoteAddress() { - // Support X-Forwarded-For and X-Real-IP headers for proxied connections - return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; - } - }(ws); + get remoteAddress() { + // Support X-Forwarded-For and X-Real-IP headers for proxied connections + return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; + } + }(ws); - ws.on('message', this.dataHandler); + ws.on('message', this.dataHandler); - ws.on('close', () => { - // we'll remove client connection which will in turn end() via our SocketBridge above - return this.emit('end'); - }); + ws.on('close', () => { + // we'll remove client connection which will in turn end() via our SocketBridge above + return this.emit('end'); + }); - // - // Montior connection status with ping/pong - // - ws.on('pong', () => { - Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); - ws.isConnectionAlive = true; - }); + // + // Montior connection status with ping/pong + // + ws.on('pong', () => { + Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); + ws.isConnectionAlive = true; + }); - TelnetClient.call(this, this.socketBridge, this.socketBridge); + TelnetClient.call(this, this.socketBridge, this.socketBridge); - Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); + Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); - // - // If the config allows it, look for 'x-forwarded-proto' as "https" - // to override |isSecure| - // - if(true === _.get(Config(), 'loginServers.webSocket.proxied') && + // + // If the config allows it, look for 'x-forwarded-proto' as "https" + // to override |isSecure| + // + if(true === _.get(Config(), 'loginServers.webSocket.proxied') && 'https' === req.headers['x-forwarded-proto']) - { - Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); - this.proxied = true; - } else { - this.proxied = false; - } + { + Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); + this.proxied = true; + } else { + this.proxied = false; + } - // start handshake process - this.banner(); + // start handshake process + this.banner(); } require('util').inherits(WebSocketClient, TelnetClient); @@ -110,101 +110,101 @@ require('util').inherits(WebSocketClient, TelnetClient); const WSS_SERVER_TYPES = [ 'insecure', 'secure' ]; exports.getModule = class WebSocketLoginServer extends LoginServerModule { - constructor() { - super(); - } + constructor() { + super(); + } - createServer() { - // - // We will actually create up to two servers: - // * insecure websocket (ws://) - // * secure (tls) websocket (wss://) - // - const config = _.get(Config(), 'loginServers.webSocket'); - if(!_.isObject(config)) { - return; - } + createServer() { + // + // We will actually create up to two servers: + // * insecure websocket (ws://) + // * secure (tls) websocket (wss://) + // + const config = _.get(Config(), 'loginServers.webSocket'); + if(!_.isObject(config)) { + return; + } - const wsPort = _.get(config, 'ws.port'); - const wssPort = _.get(config, 'wss.port'); + const wsPort = _.get(config, 'ws.port'); + const wssPort = _.get(config, 'wss.port'); - if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) { - const httpServer = http.createServer( (req, resp) => { - // dummy handler - resp.writeHead(200); - return resp.end('ENiGMA½ BBS WebSocket Server!'); - }); + if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) { + const httpServer = http.createServer( (req, resp) => { + // dummy handler + resp.writeHead(200); + return resp.end('ENiGMA½ BBS WebSocket Server!'); + }); - this.insecure = { - httpServer : httpServer, - wsServer : new WebSocketServer( { server : httpServer } ), - }; - } + this.insecure = { + httpServer : httpServer, + wsServer : new WebSocketServer( { server : httpServer } ), + }; + } - if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) { - const httpServer = https.createServer({ - key : fs.readFileSync(config.wss.keyPem), - cert : fs.readFileSync(config.wss.certPem), - }); + if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) { + const httpServer = https.createServer({ + key : fs.readFileSync(config.wss.keyPem), + cert : fs.readFileSync(config.wss.certPem), + }); - this.secure = { - httpServer : httpServer, - wsServer : new WebSocketServer( { server : httpServer } ), - }; - } - } + this.secure = { + httpServer : httpServer, + wsServer : new WebSocketServer( { server : httpServer } ), + }; + } + } - listen() { - WSS_SERVER_TYPES.forEach(serverType => { - const server = this[serverType]; - if(!server) { - return; - } + listen() { + WSS_SERVER_TYPES.forEach(serverType => { + const server = this[serverType]; + if(!server) { + return; + } - const serverName = `${ModuleInfo.name} (${serverType})`; - const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); + const serverName = `${ModuleInfo.name} (${serverType})`; + const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); - if(isNaN(port)) { - Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); - return; - } + if(isNaN(port)) { + Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); + return; + } - server.httpServer.listen(port); + server.httpServer.listen(port); - server.wsServer.on('connection', (ws, req) => { - const webSocketClient = new WebSocketClient(ws, req, serverType); - this.handleNewClient(webSocketClient, webSocketClient.socketBridge, ModuleInfo); - }); + server.wsServer.on('connection', (ws, req) => { + const webSocketClient = new WebSocketClient(ws, req, serverType); + this.handleNewClient(webSocketClient, webSocketClient.socketBridge, ModuleInfo); + }); - Log.info( { server : serverName, port : port }, 'Listening for connections' ); - }); + Log.info( { server : serverName, port : port }, 'Listening for connections' ); + }); - // - // Send pings every 30s - // - setInterval( () => { - WSS_SERVER_TYPES.forEach(serverType => { - if(this[serverType]) { - this[serverType].wsServer.clients.forEach(ws => { - if(false === ws.isConnectionAlive) { - Log.debug('WebSocket connection seems inactive. Terminating.'); - return ws.terminate(); - } + // + // Send pings every 30s + // + setInterval( () => { + WSS_SERVER_TYPES.forEach(serverType => { + if(this[serverType]) { + this[serverType].wsServer.clients.forEach(ws => { + if(false === ws.isConnectionAlive) { + Log.debug('WebSocket connection seems inactive. Terminating.'); + return ws.terminate(); + } - ws.isConnectionAlive = false; // pong will reset this + ws.isConnectionAlive = false; // pong will reset this - Log.trace('Ping to remote WebSocket client'); - return ws.ping('', false); // false=don't mask - }); - } - }); - }, 30000); + Log.trace('Ping to remote WebSocket client'); + return ws.ping('', false); // false=don't mask + }); + } + }); + }, 30000); - return true; - } + return true; + } - webSocketConnection(conn) { - const webSocketClient = new WebSocketClient(conn); - this.handleNewClient(webSocketClient, webSocketClient.socketShim, ModuleInfo); - } + webSocketConnection(conn) { + const webSocketClient = new WebSocketClient(conn); + this.handleNewClient(webSocketClient, webSocketClient.socketShim, ModuleInfo); + } }; diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js index efcb1f19..85b9cfcd 100644 --- a/core/set_newscan_date.js +++ b/core/set_newscan_date.js @@ -9,10 +9,10 @@ const FileEntry = require('./file_entry.js'); const FileBaseFilters = require('./file_base_filter.js'); const { getAvailableFileAreaTags } = require('./file_base_area.js'); const { - getSortedAvailMessageConferences, - getSortedAvailMessageAreasByConfTag, - updateMessageAreaLastReadId, - getMessageIdNewerThanTimestampByArea + getSortedAvailMessageConferences, + getSortedAvailMessageAreasByConfTag, + updateMessageAreaLastReadId, + getMessageIdNewerThanTimestampByArea } = require('./message_area.js'); const stringFormat = require('./string_format.js'); @@ -22,240 +22,240 @@ const moment = require('moment'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Set New Scan Date', - desc : 'Sets new scan date for applicable scans', - author : 'NuSkooler', + name : 'Set New Scan Date', + desc : 'Sets new scan date for applicable scans', + author : 'NuSkooler', }; const MciViewIds = { - main : { - scanDate : 1, - targetSelection : 2, - } + main : { + scanDate : 1, + targetSelection : 2, + } }; // :TODO: for messages, we could insert "conf - all areas" into targets, and allow such exports.getModule = class SetNewScanDate extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - const config = this.menuConfig.config; + const config = this.menuConfig.config; - this.target = config.target || 'message'; - this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; + this.target = config.target || 'message'; + this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; - this.menuMethods = { - scanDateSubmit : (formData, extraArgs, cb) => { - let scanDate = _.get(formData, 'value.scanDate'); - if(!scanDate) { - return cb(Errors.MissingParam('"scanDate" missing from form data')); - } + this.menuMethods = { + scanDateSubmit : (formData, extraArgs, cb) => { + let scanDate = _.get(formData, 'value.scanDate'); + if(!scanDate) { + return cb(Errors.MissingParam('"scanDate" missing from form data')); + } - scanDate = moment(scanDate, this.scanDateFormat); - if(!scanDate.isValid()) { - return cb(Errors.Invalid(`"${_.get(formData, 'value.scanDate')}" is not a valid date`)); - } + scanDate = moment(scanDate, this.scanDateFormat); + if(!scanDate.isValid()) { + return cb(Errors.Invalid(`"${_.get(formData, 'value.scanDate')}" is not a valid date`)); + } - const targetSelection = _.get(formData, 'value.targetSelection'); // may be undefined if N/A + const targetSelection = _.get(formData, 'value.targetSelection'); // may be undefined if N/A - this[`setNewScanDateFor${_.capitalize(this.target)}Base`](targetSelection, scanDate, () => { - return this.prevMenu(cb); - }); - }, - }; - } + this[`setNewScanDateFor${_.capitalize(this.target)}Base`](targetSelection, scanDate, () => { + return this.prevMenu(cb); + }); + }, + }; + } - setNewScanDateForMessageBase(targetSelection, scanDate, cb) { - const target = this.targetSelections[targetSelection]; - if(!target) { - return cb(Errors.UnexpectedState('Unable to get target in which to set new scan')); - } + setNewScanDateForMessageBase(targetSelection, scanDate, cb) { + const target = this.targetSelections[targetSelection]; + if(!target) { + return cb(Errors.UnexpectedState('Unable to get target in which to set new scan')); + } - // selected area, or all of 'em - let updateAreaTags; - if('' === target.area.areaTag) { - updateAreaTags = this.targetSelections - .map( targetSelection => targetSelection.area.areaTag ) - .filter( areaTag => areaTag ); // remove the blank 'all' entry - } else { - updateAreaTags = [ target.area.areaTag ]; - } + // selected area, or all of 'em + let updateAreaTags; + if('' === target.area.areaTag) { + updateAreaTags = this.targetSelections + .map( targetSelection => targetSelection.area.areaTag ) + .filter( areaTag => areaTag ); // remove the blank 'all' entry + } else { + updateAreaTags = [ target.area.areaTag ]; + } - async.each(updateAreaTags, (areaTag, nextAreaTag) => { - getMessageIdNewerThanTimestampByArea(areaTag, scanDate, (err, messageId) => { - if(err) { - return nextAreaTag(err); - } + async.each(updateAreaTags, (areaTag, nextAreaTag) => { + getMessageIdNewerThanTimestampByArea(areaTag, scanDate, (err, messageId) => { + if(err) { + return nextAreaTag(err); + } - if(!messageId) { - return nextAreaTag(null); // nothing to do - } + if(!messageId) { + return nextAreaTag(null); // nothing to do + } - messageId = Math.max(messageId - 1, 0); + messageId = Math.max(messageId - 1, 0); - return updateMessageAreaLastReadId( - this.client.user.userId, - areaTag, - messageId, - true, // allowOlder - nextAreaTag - ); - }); - }, err => { - return cb(err); - }); - } + return updateMessageAreaLastReadId( + this.client.user.userId, + areaTag, + messageId, + true, // allowOlder + nextAreaTag + ); + }); + }, err => { + return cb(err); + }); + } - setNewScanDateForFileBase(targetSelection, scanDate, cb) { - // - // ENiGMA doesn't currently have the concept of per-area - // scan pointers for users, so we use all areas avail - // to the user. - // - const filterCriteria = { - areaTag : getAvailableFileAreaTags(this.client), - newerThanTimestamp : scanDate, - limit : 1, - orderBy : 'upload_timestamp', - order : 'ascending', - }; + setNewScanDateForFileBase(targetSelection, scanDate, cb) { + // + // ENiGMA doesn't currently have the concept of per-area + // scan pointers for users, so we use all areas avail + // to the user. + // + const filterCriteria = { + areaTag : getAvailableFileAreaTags(this.client), + newerThanTimestamp : scanDate, + limit : 1, + orderBy : 'upload_timestamp', + order : 'ascending', + }; - FileEntry.findFiles(filterCriteria, (err, fileIds) => { - if(err) { - return cb(err); - } + FileEntry.findFiles(filterCriteria, (err, fileIds) => { + if(err) { + return cb(err); + } - if(!fileIds || 0 === fileIds.length) { - // nothing to do - return cb(null); - } + if(!fileIds || 0 === fileIds.length) { + // nothing to do + return cb(null); + } - const pointerFileId = Math.max(fileIds[0] - 1, 0); + const pointerFileId = Math.max(fileIds[0] - 1, 0); - return FileBaseFilters.setFileBaseLastViewedFileIdForUser( - this.client.user, - pointerFileId, - true, // allowOlder - cb - ); - }); - } + return FileBaseFilters.setFileBaseLastViewedFileIdForUser( + this.client.user, + pointerFileId, + true, // allowOlder + cb + ); + }); + } - loadAvailMessageBaseSelections(cb) { - // - // Create an array of objects with conf/area information per entry, - // sorted naturally or via the 'sort' member in config - // - const selections = []; - getSortedAvailMessageConferences(this.client).forEach(conf => { - getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).forEach(area => { - selections.push({ - conf : { - confTag : conf.confTag, - name : conf.conf.name, - desc : conf.conf.desc, - }, - area : { - areaTag : area.areaTag, - name : area.area.name, - desc : area.area.desc, - } - }); - }); - }); + loadAvailMessageBaseSelections(cb) { + // + // Create an array of objects with conf/area information per entry, + // sorted naturally or via the 'sort' member in config + // + const selections = []; + getSortedAvailMessageConferences(this.client).forEach(conf => { + getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).forEach(area => { + selections.push({ + conf : { + confTag : conf.confTag, + name : conf.conf.name, + desc : conf.conf.desc, + }, + area : { + areaTag : area.areaTag, + name : area.area.name, + desc : area.area.desc, + } + }); + }); + }); - selections.unshift({ - conf : { - confTag : '', - name : 'All conferences', - desc : 'All conferences', - }, - area : { - areaTag : '', - name : 'All areas', - desc : 'All areas', - } - }); + selections.unshift({ + conf : { + confTag : '', + name : 'All conferences', + desc : 'All conferences', + }, + area : { + areaTag : '', + name : 'All areas', + desc : 'All areas', + } + }); - // Find current conf/area & move it directly under "All" - const currConfTag = this.client.user.properties.message_conf_tag; - const currAreaTag = this.client.user.properties.message_area_tag; - if(currConfTag && currAreaTag) { - const confAreaIndex = selections.findIndex( confArea => { - return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag; - }); + // Find current conf/area & move it directly under "All" + const currConfTag = this.client.user.properties.message_conf_tag; + const currAreaTag = this.client.user.properties.message_area_tag; + if(currConfTag && currAreaTag) { + const confAreaIndex = selections.findIndex( confArea => { + return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag; + }); - if(confAreaIndex > -1) { - selections.splice(1, 0, selections.splice(confAreaIndex, 1)[0]); - } - } + if(confAreaIndex > -1) { + selections.splice(1, 0, selections.splice(confAreaIndex, 1)[0]); + } + } - this.targetSelections = selections; + this.targetSelections = selections; - return cb(null); - } + return cb(null); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.addViewController( 'main', new ViewController( { client : this.client } ) ); + const self = this; + const vc = self.addViewController( 'main', new ViewController( { client : this.client } ) ); - async.series( - [ - function validateConfig(callback) { - if(![ 'message', 'file' ].includes(self.target)) { - return callback(Errors.Invalid(`Invalid "target" in config: ${self.target}`)); - } - // :TOD0: validate scanDateFormat - return callback(null); - }, - function loadFromConfig(callback) { - return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); - }, - function loadAvailSelections(callback) { - switch(self.target) { - case 'message' : - return self.loadAvailMessageBaseSelections(callback); + async.series( + [ + function validateConfig(callback) { + if(![ 'message', 'file' ].includes(self.target)) { + return callback(Errors.Invalid(`Invalid "target" in config: ${self.target}`)); + } + // :TOD0: validate scanDateFormat + return callback(null); + }, + function loadFromConfig(callback) { + return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); + }, + function loadAvailSelections(callback) { + switch(self.target) { + case 'message' : + return self.loadAvailMessageBaseSelections(callback); - default : - return callback(null); - } - }, - function populateForm(callback) { - const today = moment(); + default : + return callback(null); + } + }, + function populateForm(callback) { + const today = moment(); - const scanDateView = vc.getView(MciViewIds.main.scanDate); + const scanDateView = vc.getView(MciViewIds.main.scanDate); - // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now - const scanDateFormat = self.scanDateFormat.replace(/[/\-. ]/g, ''); - scanDateView.setText(today.format(scanDateFormat)); + // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now + const scanDateFormat = self.scanDateFormat.replace(/[/\-. ]/g, ''); + scanDateView.setText(today.format(scanDateFormat)); - if('message' === self.target) { - const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}'; - const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat; + if('message' === self.target) { + const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}'; + const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat; - const targetSelectionView = vc.getView(MciViewIds.main.targetSelection); + const targetSelectionView = vc.getView(MciViewIds.main.targetSelection); - targetSelectionView.setItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); - targetSelectionView.setFocusItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); + targetSelectionView.setItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); + targetSelectionView.setFocusItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); - targetSelectionView.setFocusItemIndex(0); - } + targetSelectionView.setFocusItemIndex(0); + } - self.viewControllers.main.resetInitialFocus(); - //vc.switchFocus(MciViewIds.main.scanDate); - return callback(null); - } - ], - err => { - return cb(err); - } - ); - }); - } + self.viewControllers.main.resetInitialFocus(); + //vc.switchFocus(MciViewIds.main.scanDate); + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } }; diff --git a/core/show_art.js b/core/show_art.js index 6fb8d01b..bb917e91 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -12,159 +12,159 @@ const async = require('async'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Show Art', - desc : 'Module for more advanced methods of displaying art', - author : 'NuSkooler', + name : 'Show Art', + desc : 'Module for more advanced methods of displaying art', + author : 'NuSkooler', }; exports.getModule = class ShowArtModule extends MenuModule { - constructor(options) { - super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); - this.config.method = this.config.method || 'random'; - this.config.optional = _.get(this.config, 'optional', true); - } + this.config.method = this.config.method || 'random'; + this.config.optional = _.get(this.config, 'optional', true); + } - initSequence() { - const self = this; + initSequence() { + const self = this; - async.series( - [ - function before(callback) { - return self.beforeArt(callback); - }, - function showArt(callback) { - // - // How we show art depends on our configuration - // - let handler = { - extraArgs : self.showByExtraArgs, - sequence : self.showBySequence, - random : self.showByRandom, - fileBaseArea : self.showByFileBaseArea, - }[self.config.method] || self.showRandomArt; + async.series( + [ + function before(callback) { + return self.beforeArt(callback); + }, + function showArt(callback) { + // + // How we show art depends on our configuration + // + let handler = { + extraArgs : self.showByExtraArgs, + sequence : self.showBySequence, + random : self.showByRandom, + fileBaseArea : self.showByFileBaseArea, + }[self.config.method] || self.showRandomArt; - handler = handler.bind(self); + handler = handler.bind(self); - return handler(callback); - } - ], - err => { - if(err && !self.config.optional) { - self.client.log.warn('Error during init sequence', { error : err.message } ); - return self.prevMenu( () => { /* dummy */ } ); - } + return handler(callback); + } + ], + err => { + if(err && !self.config.optional) { + self.client.log.warn('Error during init sequence', { error : err.message } ); + return self.prevMenu( () => { /* dummy */ } ); + } - self.finishedLoading(); - return self.autoNextMenu( () => { /* dummy */ } ); - } - ); - } + self.finishedLoading(); + return self.autoNextMenu( () => { /* dummy */ } ); + } + ); + } - showByExtraArgs(cb) { - this.getArtKeyValue( (err, artSpec) => { - if(err) { - return cb(err); - } - const options = { - pause : this.shouldPause(), - desc : 'extraArgs', - }; - return this.displaySingleArtWithOptions(artSpec, options, cb); - }); - } + showByExtraArgs(cb) { + this.getArtKeyValue( (err, artSpec) => { + if(err) { + return cb(err); + } + const options = { + pause : this.shouldPause(), + desc : 'extraArgs', + }; + return this.displaySingleArtWithOptions(artSpec, options, cb); + }); + } - showBySequence(cb) { - return cb(null); - } + showBySequence(cb) { + return cb(null); + } - showByRandom(cb) { - return cb(null); - } + showByRandom(cb) { + return cb(null); + } - showByFileBaseArea(cb) { - this.getArtKeyValue( (err, key) => { - if(err) { - return cb(err); - } + showByFileBaseArea(cb) { + this.getArtKeyValue( (err, key) => { + if(err) { + return cb(err); + } - // further resolve key -> file base area art - const artSpec = _.get(Config(), [ 'fileBase', 'areas', key, 'art' ]); - if(!artSpec) { - return cb(Errors.MissingConfig(`No art defined for file base area "${key}"`)); - } - const options = { - pause : this.shouldPause(), - desc : 'fileBaseArea', - }; - return this.displaySingleArtWithOptions(artSpec, options, cb); - }); - } + // further resolve key -> file base area art + const artSpec = _.get(Config(), [ 'fileBase', 'areas', key, 'art' ]); + if(!artSpec) { + return cb(Errors.MissingConfig(`No art defined for file base area "${key}"`)); + } + const options = { + pause : this.shouldPause(), + desc : 'fileBaseArea', + }; + return this.displaySingleArtWithOptions(artSpec, options, cb); + }); + } - getArtKeyValue(cb) { - const key = this.config.key; - if(!_.isString(key)) { - return cb(Errors.MissingConfig('Config option "key" is required for method "extraArgs"')); - } + getArtKeyValue(cb) { + const key = this.config.key; + if(!_.isString(key)) { + return cb(Errors.MissingConfig('Config option "key" is required for method "extraArgs"')); + } - const path = key.split('.'); - const artKey = _.get(this.config, [ 'extraArgs' ].concat(path) ); - if(!_.isString(artKey)) { - return cb(Errors.MissingParam(`Invalid or missing "extraArgs.${key}" value`)); - } + const path = key.split('.'); + const artKey = _.get(this.config, [ 'extraArgs' ].concat(path) ); + if(!_.isString(artKey)) { + return cb(Errors.MissingParam(`Invalid or missing "extraArgs.${key}" value`)); + } - return cb(null, artKey); - } + return cb(null, artKey); + } - displaySingleArtWithOptions(artSpec, options, cb) { - const self = this; - async.waterfall( - [ - function art(callback) { - // :TODO: we really need a way to supply an explicit path to look in, e.g. general/area_art/ - self.displayAsset( - artSpec, - self.menuConfig.options, - (err, artData) => { - if(err) { - return callback(err); - } - const mciData = { menu : artData.mciMap }; - return callback(null, mciData); - } - ); - }, - function recordCursorPosition(mciData, callback) { - if(!options.pause) { - return callback(null, mciData, null); // cursor position not needed - } + displaySingleArtWithOptions(artSpec, options, cb) { + const self = this; + async.waterfall( + [ + function art(callback) { + // :TODO: we really need a way to supply an explicit path to look in, e.g. general/area_art/ + self.displayAsset( + artSpec, + self.menuConfig.options, + (err, artData) => { + if(err) { + return callback(err); + } + const mciData = { menu : artData.mciMap }; + return callback(null, mciData); + } + ); + }, + function recordCursorPosition(mciData, callback) { + if(!options.pause) { + return callback(null, mciData, null); // cursor position not needed + } - self.client.once('cursor position report', pos => { - const pausePosition = { row : pos[0], col : 1 }; - return callback(null, mciData, pausePosition); - }); + self.client.once('cursor position report', pos => { + const pausePosition = { row : pos[0], col : 1 }; + return callback(null, mciData, pausePosition); + }); - self.client.term.rawWrite(ANSI.queryPos()); - }, - function afterArtDisplayed(mciData, pausePosition, callback) { - self.mciReady(mciData, err => { - return callback(err, pausePosition); - }); - }, - function displayPauseIfRequested(pausePosition, callback) { - if(!options.pause) { - return callback(null); - } - return self.pausePrompt(pausePosition, callback); - }, - ], - err => { - if(err) { - self.client.log.warn( { artSpec, error : err.message }, `Failed to display "${options.desc}" art`); - } - return cb(err); - } - ); - } + self.client.term.rawWrite(ANSI.queryPos()); + }, + function afterArtDisplayed(mciData, pausePosition, callback) { + self.mciReady(mciData, err => { + return callback(err, pausePosition); + }); + }, + function displayPauseIfRequested(pausePosition, callback) { + if(!options.pause) { + return callback(null); + } + return self.pausePrompt(pausePosition, callback); + }, + ], + err => { + if(err) { + self.client.log.warn( { artSpec, error : err.message }, `Failed to display "${options.desc}" art`); + } + return cb(err); + } + ); + } }; diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index 829255ca..b46dda51 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -12,105 +12,105 @@ const _ = require('lodash'); exports.SpinnerMenuView = SpinnerMenuView; function SpinnerMenuView(options) { - options.justify = options.justify || 'left'; - options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; + options.cursor = options.cursor || 'hide'; - MenuView.call(this, options); + MenuView.call(this, options); - var self = this; + var self = this; - /* + /* this.cachePositions = function() { self.positionCacheExpired = false; }; */ - this.updateSelection = function() { - //assert(!self.positionCacheExpired); + this.updateSelection = function() { + //assert(!self.positionCacheExpired); - assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); + assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); - this.drawItem(this.focusedItemIndex); - this.emit('index update', this.focusedItemIndex); - }; + this.drawItem(this.focusedItemIndex); + this.emit('index update', this.focusedItemIndex); + }; - this.drawItem = function() { - var item = self.items[this.focusedItemIndex]; - if(!item) { - return; - } + this.drawItem = function() { + var item = self.items[this.focusedItemIndex]; + if(!item) { + return; + } - this.client.term.write(ansi.goto(this.position.row, this.position.col)); - this.client.term.write(self.hasFocus ? self.getFocusSGR() : self.getSGR()); + this.client.term.write(ansi.goto(this.position.row, this.position.col)); + this.client.term.write(self.hasFocus ? self.getFocusSGR() : self.getSGR()); - var text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + var text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); - self.client.term.write( - strUtil.pad(text, this.dimens.width + 1, this.fillChar, this.justify)); - }; + self.client.term.write( + strUtil.pad(text, this.dimens.width + 1, this.fillChar, this.justify)); + }; } util.inherits(SpinnerMenuView, MenuView); SpinnerMenuView.prototype.redraw = function() { - SpinnerMenuView.super_.prototype.redraw.call(this); + SpinnerMenuView.super_.prototype.redraw.call(this); - //this.cachePositions(); - this.drawItem(this.focusedItemIndex); + //this.cachePositions(); + this.drawItem(this.focusedItemIndex); }; SpinnerMenuView.prototype.setFocus = function(focused) { - SpinnerMenuView.super_.prototype.setFocus.call(this, focused); + SpinnerMenuView.super_.prototype.setFocus.call(this, focused); - this.redraw(); + this.redraw(); }; SpinnerMenuView.prototype.setFocusItemIndex = function(index) { - SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - this.updateSelection(); // will redraw + this.updateSelection(); // will redraw }; SpinnerMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('up', key.name)) { - if(0 === this.focusedItemIndex) { - this.focusedItemIndex = this.items.length - 1; - } else { - this.focusedItemIndex--; - } + if(key) { + if(this.isKeyMapped('up', key.name)) { + if(0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + } else { + this.focusedItemIndex--; + } - this.updateSelection(); - return; - } else if(this.isKeyMapped('down', key.name)) { - if(this.items.length - 1 === this.focusedItemIndex) { - this.focusedItemIndex = 0; - } else { - this.focusedItemIndex++; - } + this.updateSelection(); + return; + } else if(this.isKeyMapped('down', key.name)) { + if(this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + } else { + this.focusedItemIndex++; + } - this.updateSelection(); - return; - } - } + this.updateSelection(); + return; + } + } - SpinnerMenuView.super_.prototype.onKeyPress.call(this, ch, key); + SpinnerMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; SpinnerMenuView.prototype.getData = function() { - const item = this.getItem(this.focusedItemIndex); - return _.isString(item.data) ? item.data : this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; SpinnerMenuView.prototype.setItems = function(items) { - SpinnerMenuView.super_.prototype.setItems.call(this, items); + SpinnerMenuView.super_.prototype.setItems.call(this, items); - var longest = 0; - for(var i = 0; i < this.items.length; ++i) { - if(longest < this.items[i].text.length) { - longest = this.items[i].text.length; - } - } + var longest = 0; + for(var i = 0; i < this.items.length; ++i) { + if(longest < this.items[i].text.length) { + longest = this.items[i].text.length; + } + } - this.dimens.width = longest; + this.dimens.width = longest; }; \ No newline at end of file diff --git a/core/standard_menu.js b/core/standard_menu.js index ddaebff3..f22153b2 100644 --- a/core/standard_menu.js +++ b/core/standard_menu.js @@ -4,24 +4,24 @@ const MenuModule = require('./menu_module.js').MenuModule; exports.moduleInfo = { - name : 'Standard Menu Module', - desc : 'A Menu Module capable of handing standard configurations', - author : 'NuSkooler', + name : 'Standard Menu Module', + desc : 'A Menu Module capable of handing standard configurations', + author : 'NuSkooler', }; exports.getModule = class StandardMenuModule extends MenuModule { - constructor(options) { - super(options); - } + constructor(options) { + super(options); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - // we do this so other modules can be both customized and still perform standard tasks - return this.standardMCIReadyHandler(mciData, cb); - }); - } + // we do this so other modules can be both customized and still perform standard tasks + return this.standardMCIReadyHandler(mciData, cb); + }); + } }; diff --git a/core/stat_log.js b/core/stat_log.js index edb2d98c..849404c2 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -20,173 +20,173 @@ const moment = require('moment'); making them easily available for MCI codes for example. */ class StatLog { - constructor() { - this.systemStats = {}; - } + constructor() { + this.systemStats = {}; + } - init(cb) { - // - // Load previous state/values of |this.systemStats| - // - const self = this; + init(cb) { + // + // Load previous state/values of |this.systemStats| + // + const self = this; - sysDb.each( - `SELECT stat_name, stat_value + sysDb.each( + `SELECT stat_name, stat_value FROM system_stat;`, - (err, row) => { - if(row) { - self.systemStats[row.stat_name] = row.stat_value; - } - }, - err => { - return cb(err); - } - ); - } + (err, row) => { + if(row) { + self.systemStats[row.stat_name] = row.stat_value; + } + }, + err => { + return cb(err); + } + ); + } - get KeepDays() { - return { - Forever : -1, - }; - } + get KeepDays() { + return { + Forever : -1, + }; + } - get KeepType() { - return { - Forever : 'forever', - Days : 'days', - Max : 'max', - Count : 'max', - }; - } + get KeepType() { + return { + Forever : 'forever', + Days : 'days', + Max : 'max', + Count : 'max', + }; + } - get Order() { - return { - Timestamp : 'timestamp_asc', - TimestampAsc : 'timestamp_asc', - TimestampDesc : 'timestamp_desc', - Random : 'random', - }; - } + get Order() { + return { + Timestamp : 'timestamp_asc', + TimestampAsc : 'timestamp_asc', + TimestampDesc : 'timestamp_desc', + Random : 'random', + }; + } - setNonPeristentSystemStat(statName, statValue) { - this.systemStats[statName] = statValue; - } + setNonPeristentSystemStat(statName, statValue) { + this.systemStats[statName] = statValue; + } - setSystemStat(statName, statValue, cb) { - // live stats - this.systemStats[statName] = statValue; + setSystemStat(statName, statValue, cb) { + // live stats + this.systemStats[statName] = statValue; - // persisted stats - sysDb.run( - `REPLACE INTO system_stat (stat_name, stat_value) + // persisted stats + sysDb.run( + `REPLACE INTO system_stat (stat_name, stat_value) VALUES (?, ?);`, - [ statName, statValue ], - err => { - // cb optional - callers may fire & forget - if(cb) { - return cb(err); - } - } - ); - } + [ statName, statValue ], + err => { + // cb optional - callers may fire & forget + if(cb) { + return cb(err); + } + } + ); + } - getSystemStat(statName) { return this.systemStats[statName]; } + getSystemStat(statName) { return this.systemStats[statName]; } - getSystemStatNum(statName) { - return parseInt(this.getSystemStat(statName)) || 0; - } + getSystemStatNum(statName) { + return parseInt(this.getSystemStat(statName)) || 0; + } - incrementSystemStat(statName, incrementBy, cb) { - incrementBy = incrementBy || 1; + incrementSystemStat(statName, incrementBy, cb) { + incrementBy = incrementBy || 1; - let newValue = parseInt(this.systemStats[statName]); - if(newValue) { - if(!_.isNumber(newValue)) { - return cb(new Error(`Value for ${statName} is not a number!`)); - } + let newValue = parseInt(this.systemStats[statName]); + if(newValue) { + if(!_.isNumber(newValue)) { + return cb(new Error(`Value for ${statName} is not a number!`)); + } - newValue += incrementBy; - } else { - newValue = incrementBy; - } + newValue += incrementBy; + } else { + newValue = incrementBy; + } - return this.setSystemStat(statName, newValue, cb); - } + return this.setSystemStat(statName, newValue, cb); + } - // - // User specific stats - // These are simply convience methods to the user's properties - // - setUserStat(user, statName, statValue, cb) { - // note: cb is optional in PersistUserProperty - return user.persistProperty(statName, statValue, cb); - } + // + // User specific stats + // These are simply convience methods to the user's properties + // + setUserStat(user, statName, statValue, cb) { + // note: cb is optional in PersistUserProperty + return user.persistProperty(statName, statValue, cb); + } - getUserStat(user, statName) { - return user.properties[statName]; - } + getUserStat(user, statName) { + return user.properties[statName]; + } - getUserStatNum(user, statName) { - return parseInt(this.getUserStat(user, statName)) || 0; - } + getUserStatNum(user, statName) { + return parseInt(this.getUserStat(user, statName)) || 0; + } - incrementUserStat(user, statName, incrementBy, cb) { - incrementBy = incrementBy || 1; + incrementUserStat(user, statName, incrementBy, cb) { + incrementBy = incrementBy || 1; - let newValue = parseInt(user.properties[statName]); - if(newValue) { - if(!_.isNumber(newValue)) { - return cb(new Error(`Value for ${statName} is not a number!`)); - } + let newValue = parseInt(user.properties[statName]); + if(newValue) { + if(!_.isNumber(newValue)) { + return cb(new Error(`Value for ${statName} is not a number!`)); + } - newValue += incrementBy; - } else { - newValue = incrementBy; - } + newValue += incrementBy; + } else { + newValue = incrementBy; + } - return this.setUserStat(user, statName, newValue, cb); - } + return this.setUserStat(user, statName, newValue, cb); + } - // the time "now" in the ISO format we use and love :) - get now() { return moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } + // the time "now" in the ISO format we use and love :) + get now() { return moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } - appendSystemLogEntry(logName, logValue, keep, keepType, cb) { - sysDb.run( - `INSERT INTO system_event_log (timestamp, log_name, log_value) + appendSystemLogEntry(logName, logValue, keep, keepType, cb) { + sysDb.run( + `INSERT INTO system_event_log (timestamp, log_name, log_value) VALUES (?, ?, ?);`, - [ this.now, logName, logValue ], - () => { - // - // Handle keep - // - if(-1 === keep) { - if(cb) { - return cb(null); - } - return; - } + [ this.now, logName, logValue ], + () => { + // + // Handle keep + // + if(-1 === keep) { + if(cb) { + return cb(null); + } + return; + } - switch(keepType) { - // keep # of days - case 'days' : - sysDb.run( - `DELETE FROM system_event_log + switch(keepType) { + // keep # of days + case 'days' : + sysDb.run( + `DELETE FROM system_event_log WHERE log_name = ? AND timestamp <= DATETIME("now", "-${keep} day");`, - [ logName ], - err => { - // cb optional - callers may fire & forget - if(cb) { - return cb(err); - } - } - ); - break; + [ logName ], + err => { + // cb optional - callers may fire & forget + if(cb) { + return cb(err); + } + } + ); + break; - case 'count': - case 'max' : - // keep max of N/count - sysDb.run( - `DELETE FROM system_event_log + case 'count': + case 'max' : + // keep max of N/count + sysDb.run( + `DELETE FROM system_event_log WHERE id IN( SELECT id FROM system_event_log @@ -194,92 +194,92 @@ class StatLog { ORDER BY id DESC LIMIT -1 OFFSET ${keep} );`, - [ logName ], - err => { - if(cb) { - return cb(err); - } - } - ); - break; + [ logName ], + err => { + if(cb) { + return cb(err); + } + } + ); + break; - case 'forever' : - default : - // nop - break; - } - } - ); - } + case 'forever' : + default : + // nop + break; + } + } + ); + } - getSystemLogEntries(logName, order, limit, cb) { - let sql = + getSystemLogEntries(logName, order, limit, cb) { + let sql = `SELECT timestamp, log_value FROM system_event_log WHERE log_name = ?`; - switch(order) { - case 'timestamp' : - case 'timestamp_asc' : - sql += ' ORDER BY timestamp ASC'; - break; + switch(order) { + case 'timestamp' : + case 'timestamp_asc' : + sql += ' ORDER BY timestamp ASC'; + break; - case 'timestamp_desc' : - sql += ' ORDER BY timestamp DESC'; - break; + case 'timestamp_desc' : + sql += ' ORDER BY timestamp DESC'; + break; - case 'random' : - sql += ' ORDER BY RANDOM()'; - } + case 'random' : + sql += ' ORDER BY RANDOM()'; + } - if(!cb && _.isFunction(limit)) { - cb = limit; - limit = 0; - } else { - limit = limit || 0; - } + if(!cb && _.isFunction(limit)) { + cb = limit; + limit = 0; + } else { + limit = limit || 0; + } - if(0 !== limit) { - sql += ` LIMIT ${limit}`; - } + if(0 !== limit) { + sql += ` LIMIT ${limit}`; + } - sql += ';'; + sql += ';'; - sysDb.all(sql, [ logName ], (err, rows) => { - return cb(err, rows); - }); - } + sysDb.all(sql, [ logName ], (err, rows) => { + return cb(err, rows); + }); + } - appendUserLogEntry(user, logName, logValue, keepDays, cb) { - sysDb.run( - `INSERT INTO user_event_log (timestamp, user_id, log_name, log_value) + appendUserLogEntry(user, logName, logValue, keepDays, cb) { + sysDb.run( + `INSERT INTO user_event_log (timestamp, user_id, log_name, log_value) VALUES (?, ?, ?, ?);`, - [ this.now, user.userId, logName, logValue ], - () => { - // - // Handle keepDays - // - if(-1 === keepDays) { - if(cb) { - return cb(null); - } - return; - } + [ this.now, user.userId, logName, logValue ], + () => { + // + // Handle keepDays + // + if(-1 === keepDays) { + if(cb) { + return cb(null); + } + return; + } - sysDb.run( - `DELETE FROM user_event_log + sysDb.run( + `DELETE FROM user_event_log WHERE user_id = ? AND log_name = ? AND timestamp <= DATETIME("now", "-${keepDays} day");`, - [ user.userId, logName ], - err => { - // cb optional - callers may fire & forget - if(cb) { - return cb(err); - } - } - ); - } - ); - } + [ user.userId, logName ], + err => { + // cb optional - callers may fire & forget + if(cb) { + return cb(err); + } + } + ); + } + ); + } } module.exports = new StatLog(); diff --git a/core/string_format.js b/core/string_format.js index 38c2047f..7857f2b3 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -4,12 +4,12 @@ const EnigError = require('./enig_error.js').EnigError; const { - pad, - stylizeString, - renderStringLength, - renderSubstr, - formatByteSize, formatByteSizeAbbr, - formatCount, formatCountAbbr, + pad, + stylizeString, + renderStringLength, + renderSubstr, + formatByteSize, formatByteSizeAbbr, + formatCount, formatCountAbbr, } = require('./string_util.js'); // deps @@ -28,329 +28,329 @@ class ValueError extends EnigError { } class KeyError extends EnigError { } const SpecRegExp = { - FillAlign : /^(.)?([<>=^])/, - Sign : /^[ +-]/, - Width : /^\d*/, - Precision : /^\d+/, + FillAlign : /^(.)?([<>=^])/, + Sign : /^[ +-]/, + Width : /^\d*/, + Precision : /^\d+/, }; function tokenizeFormatSpec(spec) { - const tokens = { - fill : '', - align : '', - sign : '', - '#' : false, - '0' : false, - width : '', - ',' : false, - precision : '', - type : '', - }; + const tokens = { + fill : '', + align : '', + sign : '', + '#' : false, + '0' : false, + width : '', + ',' : false, + precision : '', + type : '', + }; - let index = 0; - let match; + let index = 0; + let match; - function incIndexByMatch() { - index += match[0].length; - } + function incIndexByMatch() { + index += match[0].length; + } - match = SpecRegExp.FillAlign.exec(spec); - if(match) { - if(match[1]) { - tokens.fill = match[1]; - } - tokens.align = match[2]; - incIndexByMatch(); - } + match = SpecRegExp.FillAlign.exec(spec); + if(match) { + if(match[1]) { + tokens.fill = match[1]; + } + tokens.align = match[2]; + incIndexByMatch(); + } - match = SpecRegExp.Sign.exec(spec.slice(index)); - if(match) { - tokens.sign = match[0]; - incIndexByMatch(); - } + match = SpecRegExp.Sign.exec(spec.slice(index)); + if(match) { + tokens.sign = match[0]; + incIndexByMatch(); + } - if('#' === spec.charAt(index)) { - tokens['#'] = true; - ++index; - } + if('#' === spec.charAt(index)) { + tokens['#'] = true; + ++index; + } - if('0' === spec.charAt(index)) { - tokens['0'] = true; - ++index; - } + if('0' === spec.charAt(index)) { + tokens['0'] = true; + ++index; + } - match = SpecRegExp.Width.exec(spec.slice(index)); - tokens.width = match[0]; - incIndexByMatch(); + match = SpecRegExp.Width.exec(spec.slice(index)); + tokens.width = match[0]; + incIndexByMatch(); - if(',' === spec.charAt(index)) { - tokens[','] = true; - ++index; - } + if(',' === spec.charAt(index)) { + tokens[','] = true; + ++index; + } - if('.' === spec.charAt(index)) { - ++index; + if('.' === spec.charAt(index)) { + ++index; - match = SpecRegExp.Precision.exec(spec.slice(index)); - if(!match) { - throw new ValueError('Format specifier missing precision'); - } + match = SpecRegExp.Precision.exec(spec.slice(index)); + if(!match) { + throw new ValueError('Format specifier missing precision'); + } - tokens.precision = match[0]; - incIndexByMatch(); - } + tokens.precision = match[0]; + incIndexByMatch(); + } - if(index < spec.length) { - tokens.type = spec.charAt(index); - ++index; - } + if(index < spec.length) { + tokens.type = spec.charAt(index); + ++index; + } - if(index < spec.length) { - throw new ValueError('Invalid conversion specification'); - } + if(index < spec.length) { + throw new ValueError('Invalid conversion specification'); + } - if(tokens[','] && 's' === tokens.type) { - throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes - } + if(tokens[','] && 's' === tokens.type) { + throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes + } - return tokens; + return tokens; } function quote(s) { - return `"${s.replace(/"/g, '\\"')}"`; + return `"${s.replace(/"/g, '\\"')}"`; } function getPadAlign(align) { - return { - '<' : 'left', - '>' : 'right', - '^' : 'center', - }[align] || '>'; + return { + '<' : 'left', + '>' : 'right', + '^' : 'center', + }[align] || '>'; } function formatString(value, tokens) { - const fill = tokens.fill || (tokens['0'] ? '0' : ' '); - const align = tokens.align || (tokens['0'] ? '=' : '<'); - const precision = Number(tokens.precision || renderStringLength(value) + 1); + const fill = tokens.fill || (tokens['0'] ? '0' : ' '); + const align = tokens.align || (tokens['0'] ? '=' : '<'); + const precision = Number(tokens.precision || renderStringLength(value) + 1); - if('' !== tokens.type && 's' !== tokens.type) { - throw new ValueError(`Unknown format code "${tokens.type}" for String object`); - } + if('' !== tokens.type && 's' !== tokens.type) { + throw new ValueError(`Unknown format code "${tokens.type}" for String object`); + } - if(tokens[',']) { - throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes - } + if(tokens[',']) { + throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes + } - if(tokens.sign) { - throw new ValueError('Sign not allowed in string format specifier'); - } + if(tokens.sign) { + throw new ValueError('Sign not allowed in string format specifier'); + } - if(tokens['#']) { - throw new ValueError('Alternate form (#) not allowed in string format specifier'); - } + if(tokens['#']) { + throw new ValueError('Alternate form (#) not allowed in string format specifier'); + } - if('=' === align) { - throw new ValueError('"=" alignment not allowed in string format specifier'); - } + if('=' === align) { + throw new ValueError('"=" alignment not allowed in string format specifier'); + } - return pad(renderSubstr(value, 0, precision), Number(tokens.width), fill, getPadAlign(align)); + return pad(renderSubstr(value, 0, precision), Number(tokens.width), fill, getPadAlign(align)); } const FormatNumRegExp = { - UpperType : /[A-Z]/, - ExponentRep : /e[+-](?=\d$)/, + UpperType : /[A-Z]/, + ExponentRep : /e[+-](?=\d$)/, }; function formatNumberHelper(n, precision, type) { - if(FormatNumRegExp.UpperType.test(type)) { - return formatNumberHelper(n, precision, type.toLowerCase()).toUpperCase(); - } + if(FormatNumRegExp.UpperType.test(type)) { + return formatNumberHelper(n, precision, type.toLowerCase()).toUpperCase(); + } - switch(type) { - case 'c' : return String.fromCharCode(n); - case 'd' : return n.toString(10); - case 'b' : return n.toString(2); - case 'o' : return n.toString(8); - case 'x' : return n.toString(16); - case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0'); - case 'f' : return n.toFixed(precision); - case 'g' : - // we don't want useless trailing zeros. parseFloat -> back to string fixes this for us - return parseFloat(n.toPrecision(precision || 1)).toString(); + switch(type) { + case 'c' : return String.fromCharCode(n); + case 'd' : return n.toString(10); + case 'b' : return n.toString(2); + case 'o' : return n.toString(8); + case 'x' : return n.toString(16); + case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0'); + case 'f' : return n.toFixed(precision); + case 'g' : + // we don't want useless trailing zeros. parseFloat -> back to string fixes this for us + return parseFloat(n.toPrecision(precision || 1)).toString(); - case '%' : return formatNumberHelper(n * 100, precision, 'f') + '%'; - case '' : return formatNumberHelper(n, precision, 'd'); + case '%' : return formatNumberHelper(n * 100, precision, 'f') + '%'; + case '' : return formatNumberHelper(n, precision, 'd'); - default : - throw new ValueError(`Unknown format code "${type}" for object of type 'float'`); - } + default : + throw new ValueError(`Unknown format code "${type}" for object of type 'float'`); + } } function formatNumber(value, tokens) { - const fill = tokens.fill || (tokens['0'] ? '0' : ' '); - const align = tokens.align || (tokens['0'] ? '=' : '>'); - const width = Number(tokens.width); - const type = tokens.type || (tokens.precision ? 'g' : ''); + const fill = tokens.fill || (tokens['0'] ? '0' : ' '); + const align = tokens.align || (tokens['0'] ? '=' : '>'); + const width = Number(tokens.width); + const type = tokens.type || (tokens.precision ? 'g' : ''); - if( [ 'c', 'd', 'b', 'o', 'x', 'X' ].indexOf(type) > -1) { - if(0 !== value % 1) { - throw new ValueError(`Cannot format non-integer with format specifier "${type}"`); - } + if( [ 'c', 'd', 'b', 'o', 'x', 'X' ].indexOf(type) > -1) { + if(0 !== value % 1) { + throw new ValueError(`Cannot format non-integer with format specifier "${type}"`); + } - if('' !== tokens.sign && 'c' !== type) { - throw new ValueError(`Sign not allowed with integer format specifier 'c'`); // eslint-disable-line quotes - } + if('' !== tokens.sign && 'c' !== type) { + throw new ValueError(`Sign not allowed with integer format specifier 'c'`); // eslint-disable-line quotes + } - if(tokens[','] && 'd' !== type) { - throw new ValueError(`Cannot specify ',' with '${type}'`); - } + if(tokens[','] && 'd' !== type) { + throw new ValueError(`Cannot specify ',' with '${type}'`); + } - if('' !== tokens.precision) { - throw new ValueError('Precision not allowed in integer format specifier'); - } - } else if( [ 'e', 'E', 'f', 'F', 'g', 'G', '%' ].indexOf(type) > - 1) { - if(tokens['#']) { - throw new ValueError('Alternate form (#) not allowed in float format specifier'); - } - } + if('' !== tokens.precision) { + throw new ValueError('Precision not allowed in integer format specifier'); + } + } else if( [ 'e', 'E', 'f', 'F', 'g', 'G', '%' ].indexOf(type) > - 1) { + if(tokens['#']) { + throw new ValueError('Alternate form (#) not allowed in float format specifier'); + } + } - const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type); - const sign = value < 0 || 1 / value < 0 ? - '-' : - '-' === tokens.sign ? '' : tokens.sign; + const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type); + const sign = value < 0 || 1 / value < 0 ? + '-' : + '-' === tokens.sign ? '' : tokens.sign; - const prefix = tokens['#'] && ( [ 'b', 'o', 'x', 'X' ].indexOf(type) > -1 ) ? '0' + type : ''; + const prefix = tokens['#'] && ( [ 'b', 'o', 'x', 'X' ].indexOf(type) > -1 ) ? '0' + type : ''; - if(tokens[',']) { - const match = /^(\d*)(.*)$/.exec(s); - const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; + if(tokens[',']) { + const match = /^(\d*)(.*)$/.exec(s); + const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; - if('=' !== align) { - return pad(sign + separated, width, fill, getPadAlign(align)); - } + if('=' !== align) { + return pad(sign + separated, width, fill, getPadAlign(align)); + } - if('0' === fill) { - const shortfall = Math.max(0, width - sign.length - separated.length); - const digits = /^\d*/.exec(separated)[0].length; - let padding = ''; - // :TODO: do this differntly... - for(let n = 0; n < shortfall; n++) { - padding = ((digits + n) % 4 === 3 ? ',' : '0') + padding; - } + if('0' === fill) { + const shortfall = Math.max(0, width - sign.length - separated.length); + const digits = /^\d*/.exec(separated)[0].length; + let padding = ''; + // :TODO: do this differntly... + for(let n = 0; n < shortfall; n++) { + padding = ((digits + n) % 4 === 3 ? ',' : '0') + padding; + } - return sign + (/^,/.test(padding) ? '0' : '') + padding + separated; - } + return sign + (/^,/.test(padding) ? '0' : '') + padding + separated; + } - return sign + pad(separated, width - sign.length, fill, getPadAlign('>')); - } + return sign + pad(separated, width - sign.length, fill, getPadAlign('>')); + } - if(0 === width) { - return sign + prefix + s; - } + if(0 === width) { + return sign + prefix + s; + } - if('=' === align) { - return sign + prefix + pad(s, width - sign.length - prefix.length, fill, getPadAlign('>')); - } + if('=' === align) { + return sign + prefix + pad(s, width - sign.length - prefix.length, fill, getPadAlign('>')); + } - return pad(sign + prefix + s, width, fill, getPadAlign(align)); + return pad(sign + prefix + s, width, fill, getPadAlign(align)); } const transformers = { - // String standard - toUpperCase : String.prototype.toUpperCase, - toLowerCase : String.prototype.toLowerCase, + // String standard + toUpperCase : String.prototype.toUpperCase, + toLowerCase : String.prototype.toLowerCase, - // some super l33b BBS styles!! - styleUpper : (s) => stylizeString(s, 'upper'), - styleLower : (s) => stylizeString(s, 'lower'), - styleTitle : (s) => stylizeString(s, 'title'), - styleFirstLower : (s) => stylizeString(s, 'first lower'), - styleSmallVowels : (s) => stylizeString(s, 'small vowels'), - styleBigVowels : (s) => stylizeString(s, 'big vowels'), - styleSmallI : (s) => stylizeString(s, 'small i'), - styleMixed : (s) => stylizeString(s, 'mixed'), - styleL33t : (s) => stylizeString(s, 'l33t'), + // some super l33b BBS styles!! + styleUpper : (s) => stylizeString(s, 'upper'), + styleLower : (s) => stylizeString(s, 'lower'), + styleTitle : (s) => stylizeString(s, 'title'), + styleFirstLower : (s) => stylizeString(s, 'first lower'), + styleSmallVowels : (s) => stylizeString(s, 'small vowels'), + styleBigVowels : (s) => stylizeString(s, 'big vowels'), + styleSmallI : (s) => stylizeString(s, 'small i'), + styleMixed : (s) => stylizeString(s, 'mixed'), + styleL33t : (s) => stylizeString(s, 'l33t'), - // :TODO: - // toMegs(), toKilobytes(), ... - // toList(), toCommaList(), + // :TODO: + // toMegs(), toKilobytes(), ... + // toList(), toCommaList(), - sizeWithAbbr : (n) => formatByteSize(n, true, 2), - sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), - sizeAbbr : (n) => formatByteSizeAbbr(n), - countWithAbbr : (n) => formatCount(n, true, 0), - countWithoutAbbr : (n) => formatCount(n, false, 0), - countAbbr : (n) => formatCountAbbr(n), + sizeWithAbbr : (n) => formatByteSize(n, true, 2), + sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), + sizeAbbr : (n) => formatByteSizeAbbr(n), + countWithAbbr : (n) => formatCount(n, true, 0), + countWithoutAbbr : (n) => formatCount(n, false, 0), + countAbbr : (n) => formatCountAbbr(n), }; function transformValue(transformerName, value) { - if(transformerName in transformers) { - const transformer = transformers[transformerName]; - value = transformer.apply(value, [ value ] ); - } + if(transformerName in transformers) { + const transformer = transformers[transformerName]; + value = transformer.apply(value, [ value ] ); + } - return value; + return value; } // :TODO: Use explicit set of chars for paths & function/transforms such that } is allowed as fill/etc. const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:!([^:}]+))?(?::([^}]+))?}/g; function getValue(obj, path) { - const value = _.get(obj, path); - if(!_.isUndefined(value)) { - return _.isFunction(value) ? value() : value; - } + const value = _.get(obj, path); + if(!_.isUndefined(value)) { + return _.isFunction(value) ? value() : value; + } - throw new KeyError(quote(path)); + throw new KeyError(quote(path)); } module.exports = function format(fmt, obj) { - const re = REGEXP_BASIC_FORMAT; - re.lastIndex = 0; // reset from prev + const re = REGEXP_BASIC_FORMAT; + re.lastIndex = 0; // reset from prev - let match; - let pos; - let out = ''; - let objPath ; - let transformer; - let formatSpec; - let value; - let tokens; + let match; + let pos; + let out = ''; + let objPath ; + let transformer; + let formatSpec; + let value; + let tokens; - do { - pos = re.lastIndex; - match = re.exec(fmt); + do { + pos = re.lastIndex; + match = re.exec(fmt); - if(match) { - if(match.index > pos) { - out += fmt.slice(pos, match.index); - } + if(match) { + if(match.index > pos) { + out += fmt.slice(pos, match.index); + } - objPath = match[1]; - transformer = match[2]; - formatSpec = match[3]; + objPath = match[1]; + transformer = match[2]; + formatSpec = match[3]; - value = getValue(obj, objPath); - if(transformer) { - value = transformValue(transformer, value); - } + value = getValue(obj, objPath); + if(transformer) { + value = transformValue(transformer, value); + } - tokens = tokenizeFormatSpec(formatSpec || ''); + tokens = tokenizeFormatSpec(formatSpec || ''); - if(_.isNumber(value)) { - out += formatNumber(value, tokens); - } else { - out += formatString(value, tokens); - } - } + if(_.isNumber(value)) { + out += formatNumber(value, tokens); + } else { + out += formatString(value, tokens); + } + } - } while(0 !== re.lastIndex); + } while(0 !== re.lastIndex); - // remainder - if(pos < fmt.length) { - out += fmt.slice(pos); - } + // remainder + if(pos < fmt.length) { + out += fmt.slice(pos); + } - return out; + return out; }; diff --git a/core/string_util.js b/core/string_util.js index 4539ea38..88f0e57e 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -34,187 +34,187 @@ const VOWELS = [ 'a', 'e', 'i', 'o', 'u' ]; VOWELS.concat(VOWELS.map(l => l.toUpperCase())); const SIMPLE_ELITE_MAP = { - 'a' : '4', - 'e' : '3', - 'i' : '1', - 'o' : '0', - 's' : '5', - 't' : '7' + 'a' : '4', + 'e' : '3', + 'i' : '1', + 'o' : '0', + 's' : '5', + 't' : '7' }; function stylizeString(s, style) { - var len = s.length; - var c; - var i; - var stylized = ''; + var len = s.length; + var c; + var i; + var stylized = ''; - switch(style) { - // None/normal - case 'normal' : - case 'N' : - return s; + switch(style) { + // None/normal + case 'normal' : + case 'N' : + return s; - // UPPERCASE - case 'upper' : - case 'U' : - return s.toUpperCase(); + // UPPERCASE + case 'upper' : + case 'U' : + return s.toUpperCase(); - // lowercase - case 'lower' : - case 'l' : - return s.toLowerCase(); + // lowercase + case 'lower' : + case 'l' : + return s.toLowerCase(); - // Title Case - case 'title' : - case 'T' : - return s.replace(/\w\S*/g, function onProperCaseChar(t) { - return t.charAt(0).toUpperCase() + t.substr(1).toLowerCase(); - }); + // Title Case + case 'title' : + case 'T' : + return s.replace(/\w\S*/g, function onProperCaseChar(t) { + return t.charAt(0).toUpperCase() + t.substr(1).toLowerCase(); + }); - // fIRST lOWER - case 'first lower' : - case 'f' : - return s.replace(/\w\S*/g, function onFirstLowerChar(t) { - return t.charAt(0).toLowerCase() + t.substr(1).toUpperCase(); - }); + // fIRST lOWER + case 'first lower' : + case 'f' : + return s.replace(/\w\S*/g, function onFirstLowerChar(t) { + return t.charAt(0).toLowerCase() + t.substr(1).toUpperCase(); + }); - // SMaLL VoWeLS - case 'small vowels' : - case 'v' : - for(i = 0; i < len; ++i) { - c = s[i]; - if(-1 !== VOWELS.indexOf(c)) { - stylized += c.toLowerCase(); - } else { - stylized += c.toUpperCase(); - } - } - return stylized; + // SMaLL VoWeLS + case 'small vowels' : + case 'v' : + for(i = 0; i < len; ++i) { + c = s[i]; + if(-1 !== VOWELS.indexOf(c)) { + stylized += c.toLowerCase(); + } else { + stylized += c.toUpperCase(); + } + } + return stylized; - // bIg vOwELS - case 'big vowels' : - case 'V' : - for(i = 0; i < len; ++i) { - c = s[i]; - if(-1 !== VOWELS.indexOf(c)) { - stylized += c.toUpperCase(); - } else { - stylized += c.toLowerCase(); - } - } - return stylized; + // bIg vOwELS + case 'big vowels' : + case 'V' : + for(i = 0; i < len; ++i) { + c = s[i]; + if(-1 !== VOWELS.indexOf(c)) { + stylized += c.toUpperCase(); + } else { + stylized += c.toLowerCase(); + } + } + return stylized; - // Small i's: DEMENTiA - case 'small i' : - case 'i' : - return s.toUpperCase().replace(/I/g, 'i'); + // Small i's: DEMENTiA + case 'small i' : + case 'i' : + return s.toUpperCase().replace(/I/g, 'i'); - // mIxeD CaSE (random upper/lower) - case 'mixed' : - case 'M' : - for(i = 0; i < len; i++) { - if(Math.random() < 0.5) { - stylized += s[i].toUpperCase(); - } else { - stylized += s[i].toLowerCase(); - } - } - return stylized; + // mIxeD CaSE (random upper/lower) + case 'mixed' : + case 'M' : + for(i = 0; i < len; i++) { + if(Math.random() < 0.5) { + stylized += s[i].toUpperCase(); + } else { + stylized += s[i].toLowerCase(); + } + } + return stylized; - // l337 5p34k - case 'l33t' : - case '3' : - for(i = 0; i < len; ++i) { - c = SIMPLE_ELITE_MAP[s[i].toLowerCase()]; - stylized += c || s[i]; - } - return stylized; - } + // l337 5p34k + case 'l33t' : + case '3' : + for(i = 0; i < len; ++i) { + c = SIMPLE_ELITE_MAP[s[i].toLowerCase()]; + stylized += c || s[i]; + } + return stylized; + } - return s; + return s; } function pad(s, len, padChar, justify, stringSGR, padSGR, useRenderLen) { - len = len || 0; - padChar = padChar || ' '; - justify = justify || 'left'; - stringSGR = stringSGR || ''; - padSGR = padSGR || ''; - useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; + len = len || 0; + padChar = padChar || ' '; + justify = justify || 'left'; + stringSGR = stringSGR || ''; + padSGR = padSGR || ''; + useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; - const renderLen = useRenderLen ? renderStringLength(s) : s.length; - const padlen = len >= renderLen ? len - renderLen : 0; + const renderLen = useRenderLen ? renderStringLength(s) : s.length; + const padlen = len >= renderLen ? len - renderLen : 0; - switch(justify) { - case 'L' : - case 'left' : - s = `${stringSGR}${s}${padSGR}${Array(padlen).join(padChar)}`; - break; + switch(justify) { + case 'L' : + case 'left' : + s = `${stringSGR}${s}${padSGR}${Array(padlen).join(padChar)}`; + break; - case 'C' : - case 'center' : - case 'both' : - { - const right = Math.ceil(padlen / 2); - const left = padlen - right; - s = `${padSGR}${Array(left + 1).join(padChar)}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; - } - break; + case 'C' : + case 'center' : + case 'both' : + { + const right = Math.ceil(padlen / 2); + const left = padlen - right; + s = `${padSGR}${Array(left + 1).join(padChar)}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; + } + break; - case 'R' : - case 'right' : - s = `${padSGR}${Array(padlen).join(padChar)}${stringSGR}${s}`; - break; + case 'R' : + case 'right' : + s = `${padSGR}${Array(padlen).join(padChar)}${stringSGR}${s}`; + break; - default : break; - } + default : break; + } - return stringSGR + s; + return stringSGR + s; } function insert(s, index, substr) { - return `${s.slice(0, index)}${substr}${s.slice(index)}`; + return `${s.slice(0, index)}${substr}${s.slice(index)}`; } 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]/; // eslint-disable-line no-control-regex function isPrintable(s) { - // - // See the following: - // https://mathiasbynens.be/notes/javascript-unicode - // http://stackoverflow.com/questions/11598786/how-to-replace-non-printable-unicode-characters-javascript - // http://stackoverflow.com/questions/12052825/regular-expression-for-all-printable-characters-in-javascript - // - // :TODO: Probably need somthing better here. - return !RE_NON_PRINTABLE.test(s); + // + // See the following: + // https://mathiasbynens.be/notes/javascript-unicode + // http://stackoverflow.com/questions/11598786/how-to-replace-non-printable-unicode-characters-javascript + // http://stackoverflow.com/questions/12052825/regular-expression-for-all-printable-characters-in-javascript + // + // :TODO: Probably need somthing better here. + return !RE_NON_PRINTABLE.test(s); } function stripAllLineFeeds(s) { - return s.replace(/\r?\n|[\r\u2028\u2029]/g, ''); + return s.replace(/\r?\n|[\r\u2028\u2029]/g, ''); } function debugEscapedString(s) { - return JSON.stringify(s).slice(1, -1); + return JSON.stringify(s).slice(1, -1); } function stringFromNullTermBuffer(buf, encoding) { - let nullPos = buf.indexOf( 0x00 ); - if(-1 === nullPos) { - nullPos = buf.length; - } + let nullPos = buf.indexOf( 0x00 ); + if(-1 === nullPos) { + nullPos = buf.length; + } - 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; + 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; @@ -226,45 +226,45 @@ const ANSI_OR_PIPE_REGEXP = new RegExp(PIPE_REGEXP.source + '|' + ANSI.getFullMa // Similar to substr() but works with ANSI/Pipe code strings // function renderSubstr(str, start, length) { - // shortcut for empty strings - if(0 === str.length) { - return str; - } + // shortcut for empty strings + if(0 === str.length) { + return str; + } - start = start || 0; - length = length || str.length - start; + start = start || 0; + length = length || str.length - start; - const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the obj; must reset! + const re = ANSI_OR_PIPE_REGEXP; + re.lastIndex = 0; // we recycle the obj; must reset! - let pos = 0; - let match; - let out = ''; - let renderLen = 0; - let s; - do { - pos = re.lastIndex; - match = re.exec(str); + let pos = 0; + let match; + let out = ''; + let renderLen = 0; + let s; + do { + pos = re.lastIndex; + match = re.exec(str); - if(match) { - if(match.index > pos) { - s = str.slice(pos + start, Math.min(match.index, pos + (length - renderLen))); - start = 0; // start offset applies only once - out += s; - renderLen += s.length; - } + if(match) { + if(match.index > pos) { + s = str.slice(pos + start, Math.min(match.index, pos + (length - renderLen))); + start = 0; // start offset applies only once + out += s; + renderLen += s.length; + } - out += match[0]; - } - } while(renderLen < length && 0 !== re.lastIndex); + out += match[0]; + } + } while(renderLen < length && 0 !== re.lastIndex); - // remainder - if(pos + start < str.length && renderLen < length) { - out += str.slice(pos + start, (pos + start + (length - renderLen))); - //out += str.slice(pos + start, Math.max(1, pos + (length - renderLen - 1))); - } + // remainder + if(pos + start < str.length && renderLen < length) { + out += str.slice(pos + start, (pos + start + (length - renderLen))); + //out += str.slice(pos + start, Math.max(1, pos + (length - renderLen - 1))); + } - return out; + return out; } // @@ -276,75 +276,75 @@ function renderSubstr(str, start, length) { // See also https://github.com/chalk/ansi-regex/blob/master/index.js // function renderStringLength(s) { - let m; - let pos; - let len = 0; + let m; + let pos; + let len = 0; - const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the rege; reset + const re = ANSI_OR_PIPE_REGEXP; + re.lastIndex = 0; // we recycle the rege; reset - // - // Loop counting only literal (non-control) sequences - // paying special attention to ESC[C which means forward - // - do { - pos = re.lastIndex; - m = re.exec(s); + // + // Loop counting only literal (non-control) sequences + // paying special attention to ESC[C which means forward + // + do { + pos = re.lastIndex; + m = re.exec(s); - if(m) { - if(m.index > pos) { - len += s.slice(pos, m.index).length; - } + if(m) { + if(m.index > pos) { + len += s.slice(pos, m.index).length; + } - if('C' === m[3]) { // ESC[C is foward/right - len += parseInt(m[2], 10) || 0; - } - } - } while(0 !== re.lastIndex); + if('C' === m[3]) { // ESC[C is foward/right + len += parseInt(m[2], 10) || 0; + } + } + } while(0 !== re.lastIndex); - if(pos < s.length) { - len += s.slice(pos).length; - } + if(pos < s.length) { + len += s.slice(pos).length; + } - return len; + return len; } const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) function formatByteSizeAbbr(byteSize) { - if(0 === byteSize) { - return BYTE_SIZE_ABBRS[0]; // B - } + if(0 === byteSize) { + return BYTE_SIZE_ABBRS[0]; // B + } - return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; + return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; } function formatByteSize(byteSize, withAbbr = false, decimals = 2) { - const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024)); - let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); - if(withAbbr) { - result += ` ${BYTE_SIZE_ABBRS[i]}`; - } - return result; + const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024)); + let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); + if(withAbbr) { + result += ` ${BYTE_SIZE_ABBRS[i]}`; + } + return result; } const COUNT_ABBRS = [ '', 'K', 'M', 'B', 'T', 'P', 'E', 'Z', 'Y' ]; function formatCountAbbr(count) { - if(count < 1000) { - return ''; - } + if(count < 1000) { + return ''; + } - return COUNT_ABBRS[Math.floor(Math.log(count) / Math.log(1000))]; + return COUNT_ABBRS[Math.floor(Math.log(count) / Math.log(1000))]; } function formatCount(count, withAbbr = false, decimals = 2) { - const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); - let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); - if(withAbbr) { - result += `${COUNT_ABBRS[i]}`; - } - return result; + const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); + let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); + if(withAbbr) { + result += `${COUNT_ABBRS[i]}`; + } + return result; } @@ -352,52 +352,52 @@ function formatCount(count, withAbbr = false, decimals = 2) { //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; // eslint-disable-line no-control-regex const ANSI_OPCODES_ALLOWED_CLEAN = [ - //'A', 'B', // up, down - //'C', 'D', // right, left - 'm', // color + //'A', 'B', // up, down + //'C', 'D', // right, left + 'm', // color ]; function cleanControlCodes(input, options) { - let m; - let pos; - let cleaned = ''; + let m; + let pos; + let cleaned = ''; - options = options || {}; + options = options || {}; - // - // Loop through |input| adding only allowed ESC - // sequences and literals to |cleaned| - // - do { - pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; - m = REGEXP_ANSI_CONTROL_CODES.exec(input); + // + // Loop through |input| adding only allowed ESC + // sequences and literals to |cleaned| + // + do { + pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; + m = REGEXP_ANSI_CONTROL_CODES.exec(input); - if(m) { - if(m.index > pos) { - cleaned += input.slice(pos, m.index); - } + if(m) { + if(m.index > pos) { + cleaned += input.slice(pos, m.index); + } - if(options.all) { - continue; - } + if(options.all) { + continue; + } - if(ANSI_OPCODES_ALLOWED_CLEAN.indexOf(m[2].charAt(0)) > -1) { - cleaned += m[0]; - } - } + if(ANSI_OPCODES_ALLOWED_CLEAN.indexOf(m[2].charAt(0)) > -1) { + cleaned += m[0]; + } + } - } while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex); + } while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex); - // remainder - if(pos < input.length) { - cleaned += input.slice(pos); - } + // remainder + if(pos < input.length) { + cleaned += input.slice(pos); + } - return cleaned; + return cleaned; } function isAnsiLine(line) { - return isAnsi(line);// || renderStringLength(line) < line.length; + return isAnsi(line);// || renderStringLength(line) < line.length; } // @@ -410,39 +410,39 @@ function isAnsiLine(line) { // * Contigous 3+ spaces before the end of the line // function isFormattedLine(line) { - if(renderStringLength(line) < line.length) { - return true; // ANSI or Pipe Codes - } + 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(line.match(/[\t\x00-\x1f\x80-\xff]/)) { // eslint-disable-line no-control-regex + return true; + } - if(_.trimEnd(line).match(/[ ]{3,}/)) { - return true; - } + if(_.trimEnd(line).match(/[ ]{3,}/)) { + return true; + } - return false; + return false; } // :TODO: rename to containsAnsi() function isAnsi(input) { - if(!input || 0 === input.length) { - return false; - } + if(!input || 0 === input.length) { + return false; + } - // - // * 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.) - // - // * - /* + // + // * 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'); @@ -450,12 +450,12 @@ function isAnsi(input) { }); */ - // :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 + // :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); + return s.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); } diff --git a/core/system_events.js b/core/system_events.js index a5e94cc1..e471c712 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -2,24 +2,24 @@ 'use strict'; module.exports = { - ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } - ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } - TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } + ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } + ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } + TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } - ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // { themeId } - ConfigChanged : 'codes.l33t.enigma.system.config_changed', - MenusChanged : 'codes.l33t.enigma.system.menus_changed', - PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', + ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // { themeId } + ConfigChanged : 'codes.l33t.enigma.system.config_changed', + MenusChanged : 'codes.l33t.enigma.system.menus_changed', + PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', - // User - includes { user, ...} - NewUser : 'codes.l33t.enigma.system.new_user', - UserLogin : 'codes.l33t.enigma.system.user_login', - UserLogoff : 'codes.l33t.enigma.system.user_logoff', - UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } - UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } + // User - includes { user, ...} + NewUser : 'codes.l33t.enigma.system.new_user', + UserLogin : 'codes.l33t.enigma.system.user_login', + UserLogoff : 'codes.l33t.enigma.system.user_logoff', + UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } + UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } - // NYI below here: - UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', - UserSendMail : 'codes.l33t.enigma.system.user_send_mail', - UserSendRunDoor : 'codes.l33t.enigma.system.user_run_door', + // NYI below here: + UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', + UserSendMail : 'codes.l33t.enigma.system.user_send_mail', + UserSendRunDoor : 'codes.l33t.enigma.system.user_run_door', }; diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 8a20af02..55da7430 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -23,151 +23,151 @@ exports.sendForgotPasswordEmail = sendForgotPasswordEmail; function login(callingMenu, formData, extraArgs, cb) { - userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { - if(err) { - // login failure - if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { - return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); - } else { - // Other error - return callingMenu.prevMenu(cb); - } - } + userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { + if(err) { + // login failure + if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { + return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); + } else { + // Other error + return callingMenu.prevMenu(cb); + } + } - // success! - return callingMenu.nextMenu(cb); - }); + // success! + return callingMenu.nextMenu(cb); + }); } function logoff(callingMenu, formData, extraArgs, cb) { - // - // Simple logoff. Note that recording of @ logoff properties/stats - // occurs elsewhere! - // - const client = callingMenu.client; + // + // Simple logoff. Note that recording of @ logoff properties/stats + // occurs elsewhere! + // + const client = callingMenu.client; - setTimeout( () => { - // - // For giggles... - // - client.term.write( - ansiNormal() + '\n' + + setTimeout( () => { + // + // For giggles... + // + client.term.write( + ansiNormal() + '\n' + iconv.decode(require('crypto').randomBytes(Math.floor(Math.random() * 65) + 20), client.term.outputEncoding) + 'NO CARRIER', null, () => { - // after data is written, disconnect & remove the client - removeClient(client); - return cb(null); - } - ); - }, 500); + // after data is written, disconnect & remove the client + removeClient(client); + return cb(null); + } + ); + }, 500); } function prevMenu(callingMenu, formData, extraArgs, cb) { - // :TODO: this is a pretty big hack -- need the whole key map concep there like other places - if(formData.key && 'return' === formData.key.name) { - callingMenu.submitFormData = formData; - } + // :TODO: this is a pretty big hack -- need the whole key map concep there like other places + if(formData.key && 'return' === formData.key.name) { + callingMenu.submitFormData = formData; + } - callingMenu.prevMenu( err => { - if(err) { - callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!'); - } - return cb(err); - }); + callingMenu.prevMenu( err => { + if(err) { + callingMenu.client.log.error( { error : err.message }, 'Error attempting to fallback!'); + } + return cb(err); + }); } function nextMenu(callingMenu, formData, extraArgs, cb) { - callingMenu.nextMenu( err => { - if(err) { - callingMenu.client.log.error( { error : err.message}, 'Error attempting to go to next menu!'); - } - return cb(err); - }); + callingMenu.nextMenu( err => { + if(err) { + callingMenu.client.log.error( { error : err.message}, 'Error attempting to go to next menu!'); + } + return cb(err); + }); } // :TODO: prev/nextConf, prev/nextArea should use a NYI MenuModule.redraw() or such -- avoid pop/goto() hack! function reloadMenu(menu, cb) { - const prevMenu = menu.client.menuStack.pop(); - prevMenu.instance.leave(); - menu.client.menuStack.goto(prevMenu.name, cb); + const prevMenu = menu.client.menuStack.pop(); + prevMenu.instance.leave(); + menu.client.menuStack.goto(prevMenu.name, cb); } function prevConf(callingMenu, formData, extraArgs, cb) { - const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); - const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag) || confs.length; + const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); + const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag) || confs.length; - messageArea.changeMessageConference(callingMenu.client, confs[currIndex - 1].confTag, err => { - if(err) { - return cb(err); // logged within changeMessageConference() - } + messageArea.changeMessageConference(callingMenu.client, confs[currIndex - 1].confTag, err => { + if(err) { + return cb(err); // logged within changeMessageConference() + } - return reloadMenu(callingMenu, cb); - }); + return reloadMenu(callingMenu, cb); + }); } function nextConf(callingMenu, formData, extraArgs, cb) { - const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); - let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag); + const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); + let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag); - if(currIndex === confs.length - 1) { - currIndex = -1; - } + if(currIndex === confs.length - 1) { + currIndex = -1; + } - messageArea.changeMessageConference(callingMenu.client, confs[currIndex + 1].confTag, err => { - if(err) { - return cb(err); // logged within changeMessageConference() - } + messageArea.changeMessageConference(callingMenu.client, confs[currIndex + 1].confTag, err => { + if(err) { + return cb(err); // logged within changeMessageConference() + } - return reloadMenu(callingMenu, cb); - }); + return reloadMenu(callingMenu, cb); + }); } function prevArea(callingMenu, formData, extraArgs, cb) { - const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); - const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag) || areas.length; + const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); + const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag) || areas.length; - messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => { - if(err) { - return cb(err); // logged within changeMessageArea() - } + messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => { + if(err) { + return cb(err); // logged within changeMessageArea() + } - return reloadMenu(callingMenu, cb); - }); + return reloadMenu(callingMenu, cb); + }); } function nextArea(callingMenu, formData, extraArgs, cb) { - const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); - let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag); + const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); + let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag); - if(currIndex === areas.length - 1) { - currIndex = -1; - } + if(currIndex === areas.length - 1) { + currIndex = -1; + } - messageArea.changeMessageArea(callingMenu.client, areas[currIndex + 1].areaTag, err => { - if(err) { - return cb(err); // logged within changeMessageArea() - } + messageArea.changeMessageArea(callingMenu.client, areas[currIndex + 1].areaTag, err => { + if(err) { + return cb(err); // logged within changeMessageArea() + } - return reloadMenu(callingMenu, cb); - }); + return reloadMenu(callingMenu, cb); + }); } function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { - const username = formData.value.username || callingMenu.client.user.username; + const username = formData.value.username || callingMenu.client.user.username; - const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; + const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; - WebPasswordReset.sendForgotPasswordEmail(username, err => { - if(err) { - callingMenu.client.log.warn( { err : err.message }, 'Failed sending forgot password email'); - } + WebPasswordReset.sendForgotPasswordEmail(username, err => { + if(err) { + callingMenu.client.log.warn( { err : err.message }, 'Failed sending forgot password email'); + } - if(extraArgs.next) { - return callingMenu.gotoMenu(extraArgs.next, cb); - } + if(extraArgs.next) { + return callingMenu.gotoMenu(extraArgs.next, cb); + } - return logoff(callingMenu, formData, extraArgs, cb); - }); + return logoff(callingMenu, formData, extraArgs, cb); + }); } diff --git a/core/system_view_validate.js b/core/system_view_validate.js index e2d01a89..397f08c0 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -22,135 +22,135 @@ exports.validateBirthdate = validateBirthdate; exports.validatePasswordSpec = validatePasswordSpec; function validateNonEmpty(data, cb) { - return cb(data && data.length > 0 ? null : new Error('Field cannot be empty')); + return cb(data && data.length > 0 ? null : new Error('Field cannot be empty')); } function validateMessageSubject(data, cb) { - return cb(data && data.length > 1 ? null : new Error('Subject too short')); + return cb(data && data.length > 1 ? null : new Error('Subject too short')); } function validateUserNameAvail(data, cb) { - const config = Config(); - if(!data || data.length < config.users.usernameMin) { - cb(new Error('Username too short')); - } else if(data.length > config.users.usernameMax) { - // generally should be unreached due to view restraints - return cb(new Error('Username too long')); - } else { - const usernameRegExp = new RegExp(config.users.usernamePattern); - const invalidNames = config.users.newUserNames + config.users.badUserNames; + const config = Config(); + if(!data || data.length < config.users.usernameMin) { + cb(new Error('Username too short')); + } else if(data.length > config.users.usernameMax) { + // generally should be unreached due to view restraints + return cb(new Error('Username too long')); + } else { + const usernameRegExp = new RegExp(config.users.usernamePattern); + const invalidNames = config.users.newUserNames + config.users.badUserNames; - if(!usernameRegExp.test(data)) { - return cb(new Error('Username contains invalid characters')); - } else if(invalidNames.indexOf(data.toLowerCase()) > -1) { - return cb(new Error('Username is blacklisted')); - } else if(/^[0-9]+$/.test(data)) { - return cb(new Error('Username cannot be a number')); - } else { - // a new user name cannot be an existing user name or an existing real name - User.getUserIdAndNameByLookup(data, function userIdAndName(err) { - if(!err) { // err is null if we succeeded -- meaning this user exists already - return cb(new Error('Username unavailable')); - } + if(!usernameRegExp.test(data)) { + return cb(new Error('Username contains invalid characters')); + } else if(invalidNames.indexOf(data.toLowerCase()) > -1) { + return cb(new Error('Username is blacklisted')); + } else if(/^[0-9]+$/.test(data)) { + return cb(new Error('Username cannot be a number')); + } else { + // a new user name cannot be an existing user name or an existing real name + User.getUserIdAndNameByLookup(data, function userIdAndName(err) { + if(!err) { // err is null if we succeeded -- meaning this user exists already + return cb(new Error('Username unavailable')); + } - return cb(null); - }); - } - } + return cb(null); + }); + } + } } const invalidUserNameError = () => new Error('Invalid username'); function validateUserNameExists(data, cb) { - if(0 === data.length) { - return cb(invalidUserNameError()); - } + if(0 === data.length) { + return cb(invalidUserNameError()); + } - User.getUserIdAndName(data, (err) => { - return cb(err ? invalidUserNameError() : null); - }); + User.getUserIdAndName(data, (err) => { + return cb(err ? invalidUserNameError() : null); + }); } function validateUserNameOrRealNameExists(data, cb) { - if(0 === data.length) { - return cb(invalidUserNameError()); - } + if(0 === data.length) { + return cb(invalidUserNameError()); + } - User.getUserIdAndNameByLookup(data, err => { - return cb(err ? invalidUserNameError() : null); - }); + User.getUserIdAndNameByLookup(data, err => { + return cb(err ? invalidUserNameError() : null); + }); } function validateGeneralMailAddressedTo(data, cb) { - // - // Allow any supported addressing: - // - Local username or real name - // - Supported remote flavors such as FTN, email, ... - // - // :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules. - const addressedToInfo = getAddressedToInfo(data); + // + // Allow any supported addressing: + // - Local username or real name + // - Supported remote flavors such as FTN, email, ... + // + // :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules. + const addressedToInfo = getAddressedToInfo(data); - if(Message.AddressFlavor.FTN === addressedToInfo.flavor) { - return cb(null); - } + if(Message.AddressFlavor.FTN === addressedToInfo.flavor) { + return cb(null); + } - return validateUserNameOrRealNameExists(data, cb); + return validateUserNameOrRealNameExists(data, cb); } function validateEmailAvail(data, cb) { - // - // This particular method allows empty data - e.g. no email entered - // - if(!data || 0 === data.length) { - return cb(null); - } + // + // This particular method allows empty data - e.g. no email entered + // + if(!data || 0 === data.length) { + return cb(null); + } - // - // Otherwise, it must be a valid email. We'll be pretty lose here, like - // the HTML5 spec. - // - // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation - // - const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; - if(!emailRegExp.test(data)) { - return cb(new Error('Invalid email address')); - } + // + // Otherwise, it must be a valid email. We'll be pretty lose here, like + // the HTML5 spec. + // + // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation + // + const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; + if(!emailRegExp.test(data)) { + return cb(new Error('Invalid email address')); + } - User.getUserIdsWithProperty('email_address', data, function userIdsWithEmail(err, uids) { - if(err) { - return cb(new Error('Internal system error')); - } else if(uids.length > 0) { - return cb(new Error('Email address not unique')); - } + User.getUserIdsWithProperty('email_address', data, function userIdsWithEmail(err, uids) { + if(err) { + return cb(new Error('Internal system error')); + } else if(uids.length > 0) { + return cb(new Error('Email address not unique')); + } - return cb(null); - }); + return cb(null); + }); } function validateBirthdate(data, cb) { - // :TODO: check for dates in the future, or > reasonable values - return cb(isNaN(Date.parse(data)) ? new Error('Invalid birthdate') : null); + // :TODO: check for dates in the future, or > reasonable values + return cb(isNaN(Date.parse(data)) ? new Error('Invalid birthdate') : null); } function validatePasswordSpec(data, cb) { - const config = Config(); - if(!data || data.length < config.users.passwordMin) { - return cb(new Error('Password too short')); - } + const config = Config(); + if(!data || data.length < config.users.passwordMin) { + return cb(new Error('Password too short')); + } - // check badpass, if avail - fs.readFile(config.users.badPassFile, 'utf8', (err, passwords) => { - if(err) { - Log.warn( { error : err.message }, 'Cannot read bad pass file'); - return cb(null); - } + // check badpass, if avail + fs.readFile(config.users.badPassFile, 'utf8', (err, passwords) => { + if(err) { + Log.warn( { error : err.message }, 'Cannot read bad pass file'); + return cb(null); + } - passwords = passwords.toString().split(/\r\n|\n/g); - if(passwords.includes(data)) { - return cb(new Error('Password is too common')); - } + passwords = passwords.toString().split(/\r\n|\n/g); + if(passwords.includes(data)) { + return cb(new Error('Password is too common')); + } - return cb(null); - }); + return cb(null); + }); } diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 36d12f5d..5276ebb0 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -28,191 +28,191 @@ const buffers = require('buffers'); // :TODO: ENH: Support nodeMax and tooManyArt exports.moduleInfo = { - name : 'Telnet Bridge', - desc : 'Connect to other Telnet Systems', - author : 'Andrew Pamment', + name : 'Telnet Bridge', + desc : 'Connect to other Telnet Systems', + author : 'Andrew Pamment', }; const IAC_DO_TERM_TYPE = Buffer.from( [ 255, 253, 24 ] ); class TelnetClientConnection extends EventEmitter { - constructor(client) { - super(); + constructor(client) { + super(); - this.client = client; - } + this.client = client; + } - restorePipe() { - if(!this.pipeRestored) { - this.pipeRestored = true; + restorePipe() { + if(!this.pipeRestored) { + this.pipeRestored = true; - // client may have bailed - if(null !== _.get(this, 'client.term.output', null)) { - if(this.bridgeConnection) { - this.client.term.output.unpipe(this.bridgeConnection); - } - this.client.term.output.resume(); - } - } - } + // client may have bailed + if(null !== _.get(this, 'client.term.output', null)) { + if(this.bridgeConnection) { + this.client.term.output.unpipe(this.bridgeConnection); + } + this.client.term.output.resume(); + } + } + } - connect(connectOpts) { - this.bridgeConnection = net.createConnection(connectOpts, () => { - this.emit('connected'); + connect(connectOpts) { + this.bridgeConnection = net.createConnection(connectOpts, () => { + this.emit('connected'); - this.pipeRestored = false; - this.client.term.output.pipe(this.bridgeConnection); - }); + this.pipeRestored = false; + this.client.term.output.pipe(this.bridgeConnection); + }); - this.bridgeConnection.on('data', data => { - this.client.term.rawWrite(data); + this.bridgeConnection.on('data', data => { + this.client.term.rawWrite(data); - // - // Wait for a terminal type request, and send it eactly once. - // This is enough (in additional to other negotiations handled in telnet.js) - // to get us in on most systems - // - if(!this.termSent && data.indexOf(IAC_DO_TERM_TYPE) > -1) { - this.termSent = true; - this.bridgeConnection.write(this.getTermTypeNegotiationBuffer()); - } - }); + // + // Wait for a terminal type request, and send it eactly once. + // This is enough (in additional to other negotiations handled in telnet.js) + // to get us in on most systems + // + if(!this.termSent && data.indexOf(IAC_DO_TERM_TYPE) > -1) { + this.termSent = true; + this.bridgeConnection.write(this.getTermTypeNegotiationBuffer()); + } + }); - this.bridgeConnection.once('end', () => { - this.restorePipe(); - this.emit('end'); - }); + this.bridgeConnection.once('end', () => { + this.restorePipe(); + this.emit('end'); + }); - this.bridgeConnection.once('error', err => { - this.restorePipe(); - this.emit('end', err); - }); - } + this.bridgeConnection.once('error', err => { + this.restorePipe(); + this.emit('end', err); + }); + } - disconnect() { - if(this.bridgeConnection) { - this.bridgeConnection.end(); - } - } + disconnect() { + if(this.bridgeConnection) { + this.bridgeConnection.end(); + } + } - destroy() { - if(this.bridgeConnection) { - this.bridgeConnection.destroy(); - this.bridgeConnection.removeAllListeners(); - this.restorePipe(); - this.emit('end'); - } - } + destroy() { + if(this.bridgeConnection) { + this.bridgeConnection.destroy(); + this.bridgeConnection.removeAllListeners(); + this.restorePipe(); + this.emit('end'); + } + } - getTermTypeNegotiationBuffer() { - // - // Create a TERMINAL-TYPE sub negotiation buffer using the - // actual/current terminal type. - // - let bufs = buffers(); + getTermTypeNegotiationBuffer() { + // + // Create a TERMINAL-TYPE sub negotiation buffer using the + // actual/current terminal type. + // + let bufs = buffers(); - bufs.push(Buffer.from( - [ - 255, // IAC - 250, // SB - 24, // TERMINAL-TYPE - 0, // IS - ] - )); + bufs.push(Buffer.from( + [ + 255, // IAC + 250, // SB + 24, // TERMINAL-TYPE + 0, // IS + ] + )); - bufs.push( - Buffer.from(this.client.term.termType), // e.g. "ansi" - Buffer.from( [ 255, 240 ] ) // IAC, SE - ); + bufs.push( + Buffer.from(this.client.term.termType), // e.g. "ansi" + Buffer.from( [ 255, 240 ] ) // IAC, SE + ); - return bufs.toBuffer(); - } + return bufs.toBuffer(); + } } exports.getModule = class TelnetBridgeModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.config.port = this.config.port || 23; - } + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + this.config.port = this.config.port || 23; + } - initSequence() { - let clientTerminated; - const self = this; + initSequence() { + let clientTerminated; + const self = this; - async.series( - [ - function validateConfig(callback) { - if(_.isString(self.config.host) && + async.series( + [ + function validateConfig(callback) { + if(_.isString(self.config.host) && _.isNumber(self.config.port)) - { - callback(null); - } else { - callback(new Error('Configuration is missing required option(s)')); - } - }, - function createTelnetBridge(callback) { - const connectOpts = { - port : self.config.port, - host : self.config.host, - }; + { + callback(null); + } else { + callback(new Error('Configuration is missing required option(s)')); + } + }, + function createTelnetBridge(callback) { + const connectOpts = { + port : self.config.port, + host : self.config.host, + }; - self.client.term.write(resetScreen()); - self.client.term.write( - ` Connecting to ${connectOpts.host}, please wait...\n (Press ESC to cancel)\n` - ); + self.client.term.write(resetScreen()); + self.client.term.write( + ` Connecting to ${connectOpts.host}, please wait...\n (Press ESC to cancel)\n` + ); - const telnetConnection = new TelnetClientConnection(self.client); + const telnetConnection = new TelnetClientConnection(self.client); - const connectionKeyPressHandler = (ch, key) => { - if('escape' === key.name) { - self.client.removeListener('key press', connectionKeyPressHandler); - telnetConnection.destroy(); - } - }; + const connectionKeyPressHandler = (ch, key) => { + if('escape' === key.name) { + self.client.removeListener('key press', connectionKeyPressHandler); + telnetConnection.destroy(); + } + }; - self.client.on('key press', connectionKeyPressHandler); + self.client.on('key press', connectionKeyPressHandler); - telnetConnection.on('connected', () => { - self.client.removeListener('key press', connectionKeyPressHandler); - self.client.log.info(connectOpts, 'Telnet bridge connection established'); + telnetConnection.on('connected', () => { + self.client.removeListener('key press', connectionKeyPressHandler); + self.client.log.info(connectOpts, 'Telnet bridge connection established'); - if(self.config.font) { - self.client.term.rawWrite(setSyncTermFontWithAlias(self.config.font)); - } + if(self.config.font) { + self.client.term.rawWrite(setSyncTermFontWithAlias(self.config.font)); + } - self.client.once('end', () => { - self.client.log.info('Connection ended. Terminating connection'); - clientTerminated = true; - telnetConnection.disconnect(); - }); - }); + self.client.once('end', () => { + self.client.log.info('Connection ended. Terminating connection'); + clientTerminated = true; + telnetConnection.disconnect(); + }); + }); - telnetConnection.on('end', err => { - self.client.removeListener('key press', connectionKeyPressHandler); + telnetConnection.on('end', err => { + self.client.removeListener('key press', connectionKeyPressHandler); - if(err) { - self.client.log.info(`Telnet bridge connection error: ${err.message}`); - } + if(err) { + self.client.log.info(`Telnet bridge connection error: ${err.message}`); + } - callback(clientTerminated ? new Error('Client connection terminated') : null); - }); + callback(clientTerminated ? new Error('Client connection terminated') : null); + }); - telnetConnection.connect(connectOpts); - } - ], - err => { - if(err) { - self.client.log.warn( { error : err.message }, 'Telnet connection error'); - } + telnetConnection.connect(connectOpts); + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'Telnet connection error'); + } - if(!clientTerminated) { - self.prevMenu(); - } - } - ); - } + if(!clientTerminated) { + self.prevMenu(); + } + } + ); + } }; diff --git a/core/text_view.js b/core/text_view.js index 9497a16b..69908e50 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -19,32 +19,32 @@ const _ = require('lodash'); exports.TextView = TextView; function TextView(options) { - if(options.dimens) { - options.dimens.height = 1; // force height of 1 for TextView's & sub classes - } + if(options.dimens) { + options.dimens.height = 1; // force height of 1 for TextView's & sub classes + } - View.call(this, options); + View.call(this, options); - if(options.maxLength) { - this.maxLength = options.maxLength; - } else { - this.maxLength = this.client.term.termWidth - this.position.col; - } + if(options.maxLength) { + this.maxLength = options.maxLength; + } else { + this.maxLength = this.client.term.termWidth - this.position.col; + } - this.fillChar = renderSubstr(miscUtil.valueWithDefault(options.fillChar, ' '), 0, 1); - this.justify = options.justify || 'left'; - this.resizable = miscUtil.valueWithDefault(options.resizable, true); - this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); + this.fillChar = renderSubstr(miscUtil.valueWithDefault(options.fillChar, ' '), 0, 1); + this.justify = options.justify || 'left'; + this.resizable = miscUtil.valueWithDefault(options.resizable, true); + this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); - if(_.isString(options.textOverflow)) { - this.textOverflow = options.textOverflow; - } + if(_.isString(options.textOverflow)) { + this.textOverflow = options.textOverflow; + } - if(_.isString(options.textMaskChar) && 1 === options.textMaskChar.length) { - this.textMaskChar = options.textMaskChar; - } + if(_.isString(options.textMaskChar) && 1 === options.textMaskChar.length) { + this.textMaskChar = options.textMaskChar; + } - /* + /* this.drawText = function(s) { // @@ -87,130 +87,130 @@ function TextView(options) { }; */ - this.drawText = function(s) { + this.drawText = function(s) { - // - // |<- this.maxLength - // ABCDEFGHIJK - // |ABCDEFG| ^_ this.text.length - // ^-- this.dimens.width - // - let renderLength = renderStringLength(s); // initial; may be adjusted below: + // + // |<- this.maxLength + // ABCDEFGHIJK + // |ABCDEFG| ^_ this.text.length + // ^-- this.dimens.width + // + let renderLength = renderStringLength(s); // initial; may be adjusted below: - let textToDraw = _.isString(this.textMaskChar) ? - new Array(renderLength + 1).join(this.textMaskChar) : - stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); + let textToDraw = _.isString(this.textMaskChar) ? + new Array(renderLength + 1).join(this.textMaskChar) : + stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - renderLength = renderStringLength(textToDraw); + renderLength = renderStringLength(textToDraw); - if(renderLength >= this.dimens.width) { - if(this.hasFocus) { - if(this.horizScroll) { - textToDraw = renderSubstr(textToDraw, renderLength - this.dimens.width, renderLength); - } - } else { - if(this.textOverflow && + if(renderLength >= this.dimens.width) { + if(this.hasFocus) { + if(this.horizScroll) { + textToDraw = renderSubstr(textToDraw, renderLength - this.dimens.width, renderLength); + } + } else { + if(this.textOverflow && this.dimens.width > this.textOverflow.length && renderLength - this.textOverflow.length >= this.textOverflow.length) - { - textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; - } else { - textToDraw = renderSubstr(textToDraw, 0, this.dimens.width); - } - } - } + { + textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; + } else { + textToDraw = renderSubstr(textToDraw, 0, this.dimens.width); + } + } + } - const renderedFillChar = pipeToAnsi(this.fillChar); + const renderedFillChar = pipeToAnsi(this.fillChar); - this.client.term.write( - padStr( - textToDraw, - this.dimens.width + 1, - renderedFillChar, //this.fillChar, - this.justify, - this.hasFocus ? this.getFocusSGR() : this.getSGR(), - this.getStyleSGR(1) || this.getSGR(), - true // use render len - ), - false // no converting CRLF needed - ); - }; + this.client.term.write( + padStr( + textToDraw, + this.dimens.width + 1, + renderedFillChar, //this.fillChar, + this.justify, + this.hasFocus ? this.getFocusSGR() : this.getSGR(), + this.getStyleSGR(1) || this.getSGR(), + true // use render len + ), + false // no converting CRLF needed + ); + }; - this.getEndOfTextColumn = function() { - var offset = Math.min(this.text.length, this.dimens.width); - return this.position.col + offset; - }; + this.getEndOfTextColumn = function() { + var offset = Math.min(this.text.length, this.dimens.width); + return this.position.col + offset; + }; - this.setText(options.text || '', false); // false=do not redraw now + this.setText(options.text || '', false); // false=do not redraw now } util.inherits(TextView, View); TextView.prototype.redraw = function() { - // - // A lot of views will get an initial redraw() with empty text (''). We can short - // circuit this by NOT doing any of the work if this is the initial drawText - // and there is no actual text (e.g. save SGR's and processing) - // - if(!this.hasDrawnOnce) { - if(_.isUndefined(this.text)) { - return; - } - } - this.hasDrawnOnce = true; + // + // A lot of views will get an initial redraw() with empty text (''). We can short + // circuit this by NOT doing any of the work if this is the initial drawText + // and there is no actual text (e.g. save SGR's and processing) + // + if(!this.hasDrawnOnce) { + if(_.isUndefined(this.text)) { + return; + } + } + this.hasDrawnOnce = true; - TextView.super_.prototype.redraw.call(this); + TextView.super_.prototype.redraw.call(this); - if(_.isString(this.text)) { - this.drawText(this.text); - } + if(_.isString(this.text)) { + this.drawText(this.text); + } }; TextView.prototype.setFocus = function(focused) { - TextView.super_.prototype.setFocus.call(this, focused); + TextView.super_.prototype.setFocus.call(this, focused); - this.redraw(); + this.redraw(); - this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); - this.client.term.write(this.getFocusSGR()); + this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); + this.client.term.write(this.getFocusSGR()); }; TextView.prototype.getData = function() { - return this.text; + return this.text; }; TextView.prototype.setText = function(text, redraw) { - redraw = _.isBoolean(redraw) ? redraw : true; + redraw = _.isBoolean(redraw) ? redraw : true; - if(!_.isString(text)) { - text = text.toString(); - } + if(!_.isString(text)) { + text = text.toString(); + } - text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. + text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. - var widthDelta = 0; - if(this.text && this.text !== text) { - widthDelta = Math.abs(renderStringLength(this.text) - renderStringLength(text)); - } + var widthDelta = 0; + if(this.text && this.text !== text) { + widthDelta = Math.abs(renderStringLength(this.text) - renderStringLength(text)); + } - this.text = text; + this.text = text; - if(this.maxLength > 0) { - this.text = renderSubstr(this.text, 0, this.maxLength); - //this.text = this.text.substr(0, this.maxLength); - } + if(this.maxLength > 0) { + this.text = renderSubstr(this.text, 0, this.maxLength); + //this.text = this.text.substr(0, this.maxLength); + } - // :TODO: it would be nice to be able to stylize strings with MCI and {special} MCI syntax, e.g. "|BN {UN!toUpper}" - this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); + // :TODO: it would be nice to be able to stylize strings with MCI and {special} MCI syntax, e.g. "|BN {UN!toUpper}" + this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); - if(this.autoScale.width) { - this.dimens.width = renderStringLength(this.text) + widthDelta; - } + if(this.autoScale.width) { + this.dimens.width = renderStringLength(this.text) + widthDelta; + } - if(redraw) { - this.redraw(); - } + if(redraw) { + this.redraw(); + } }; /* @@ -245,21 +245,21 @@ TextView.prototype.setText = function(text) { */ TextView.prototype.clearText = function() { - this.setText(''); + this.setText(''); }; TextView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'textMaskChar' : this.textMaskChar = value.substr(0, 1); break; - case 'textOverflow' : this.textOverflow = value; break; - case 'maxLength' : this.maxLength = parseInt(value, 10); break; - case 'password' : - if(true === value) { - this.textMaskChar = this.client.currentTheme.helpers.getPasswordChar(); - } - break; - } + switch(propName) { + case 'textMaskChar' : this.textMaskChar = value.substr(0, 1); break; + case 'textOverflow' : this.textOverflow = value; break; + case 'maxLength' : this.maxLength = parseInt(value, 10); break; + case 'password' : + if(true === value) { + this.textMaskChar = this.client.currentTheme.helpers.getPasswordChar(); + } + break; + } - TextView.super_.prototype.setPropertyValue.call(this, propName, value); + TextView.super_.prototype.setPropertyValue.call(this, propName, value); }; diff --git a/core/theme.js b/core/theme.js index 5545fad3..aca8ca2c 100644 --- a/core/theme.js +++ b/core/theme.js @@ -30,604 +30,604 @@ exports.displayThemedPrompt = displayThemedPrompt; exports.displayThemedAsset = displayThemedAsset; function refreshThemeHelpers(theme) { - // - // Create some handy helpers - // - theme.helpers = { - getPasswordChar : function() { - let pwChar = _.get( - theme, - 'customization.defaults.general.passwordChar', - Config().defaults.passwordChar - ); + // + // Create some handy helpers + // + theme.helpers = { + getPasswordChar : function() { + let pwChar = _.get( + theme, + 'customization.defaults.general.passwordChar', + Config().defaults.passwordChar + ); - if(_.isString(pwChar)) { - pwChar = pwChar.substr(0, 1); - } else if(_.isNumber(pwChar)) { - pwChar = String.fromCharCode(pwChar); - } + if(_.isString(pwChar)) { + pwChar = pwChar.substr(0, 1); + } else if(_.isNumber(pwChar)) { + pwChar = String.fromCharCode(pwChar); + } - return pwChar; - }, - getDateFormat : function(style = 'short') { - const format = Config().defaults.dateFormat[style] || 'MM/DD/YYYY'; - return _.get(theme, `customization.defaults.dateFormat.${style}`, format); - }, - getTimeFormat : function(style = 'short') { - const format = Config().defaults.timeFormat[style] || 'h:mm a'; - return _.get(theme, `customization.defaults.timeFormat.${style}`, format); - }, - getDateTimeFormat : function(style = 'short') { - const format = Config().defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; - return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); - } - }; + return pwChar; + }, + getDateFormat : function(style = 'short') { + const format = Config().defaults.dateFormat[style] || 'MM/DD/YYYY'; + return _.get(theme, `customization.defaults.dateFormat.${style}`, format); + }, + getTimeFormat : function(style = 'short') { + const format = Config().defaults.timeFormat[style] || 'h:mm a'; + return _.get(theme, `customization.defaults.timeFormat.${style}`, format); + }, + getDateTimeFormat : function(style = 'short') { + const format = Config().defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); + } + }; } function loadTheme(themeId, cb) { - const path = paths.join(Config().paths.themes, themeId, 'theme.hjson'); + const path = paths.join(Config().paths.themes, themeId, 'theme.hjson'); - const changed = ( { fileName, fileRoot } ) => { - const reCachedPath = paths.join(fileRoot, fileName); - if(reCachedPath === path) { - reloadTheme(themeId); - } - }; + const changed = ( { fileName, fileRoot } ) => { + const reCachedPath = paths.join(fileRoot, fileName); + if(reCachedPath === path) { + reloadTheme(themeId); + } + }; - const getOpts = { - filePath : path, - forceReCache : true, - callback : changed, - }; + const getOpts = { + filePath : path, + forceReCache : true, + callback : changed, + }; - ConfigCache.getConfigWithOptions(getOpts, (err, theme) => { - if(err) { - return cb(err); - } + ConfigCache.getConfigWithOptions(getOpts, (err, theme) => { + if(err) { + return cb(err); + } - if(!_.isObject(theme.info) || + if(!_.isObject(theme.info) || !_.isString(theme.info.name) || !_.isString(theme.info.author)) - { - return cb(Errors.Invalid('Invalid or missing "info" section')); - } + { + 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)); - } + if(false === _.get(theme, 'info.enabled')) { + return cb(Errors.General('Theme is not enalbed', ErrorReasons.ErrNotEnabled)); + } - refreshThemeHelpers(theme); + refreshThemeHelpers(theme); - return cb(null, theme, path); - }); + return cb(null, theme, path); + }); } const availableThemes = new Map(); const IMMUTABLE_MCI_PROPERTIES = [ - 'maxLength', 'argName', 'submit', 'validate' + 'maxLength', 'argName', 'submit', 'validate' ]; function getMergedTheme(menuConfig, promptConfig, theme) { - assert(_.isObject(menuConfig)); - assert(_.isObject(theme)); + assert(_.isObject(menuConfig)); + assert(_.isObject(theme)); - // :TODO: merge in defaults (customization.defaults{} ) - // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") + // :TODO: merge in defaults (customization.defaults{} ) + // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") - // - // Create a *clone* of menuConfig (menu.hjson) then bring in - // promptConfig (prompt.hjson) - // - const mergedTheme = _.cloneDeep(menuConfig); + // + // Create a *clone* of menuConfig (menu.hjson) then bring in + // promptConfig (prompt.hjson) + // + const mergedTheme = _.cloneDeep(menuConfig); - if(_.isObject(promptConfig.prompts)) { - mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); - } + if(_.isObject(promptConfig.prompts)) { + mergedTheme.prompts = _.cloneDeep(promptConfig.prompts); + } - // - // Add in data we won't be altering directly from the theme - // - mergedTheme.info = theme.info; - mergedTheme.helpers = theme.helpers; + // + // Add in data we won't be altering directly from the theme + // + mergedTheme.info = theme.info; + mergedTheme.helpers = theme.helpers; - // - // merge customizer to disallow immutable MCI properties - // - const mciCustomizer = function(objVal, srcVal, key) { - return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; - }; + // + // merge customizer to disallow immutable MCI properties + // + const mciCustomizer = function(objVal, srcVal, key) { + return IMMUTABLE_MCI_PROPERTIES.indexOf(key) > -1 ? objVal : srcVal; + }; - function getFormKeys(fromObj) { - return _.remove(_.keys(fromObj), function pred(k) { - return !isNaN(k); // remove all non-numbers - }); - } + function getFormKeys(fromObj) { + return _.remove(_.keys(fromObj), function pred(k) { + return !isNaN(k); // remove all non-numbers + }); + } - function mergeMciProperties(dest, src) { - Object.keys(src).forEach(function mciEntry(mci) { - _.mergeWith(dest[mci], src[mci], mciCustomizer); - }); - } + function mergeMciProperties(dest, src) { + Object.keys(src).forEach(function mciEntry(mci) { + _.mergeWith(dest[mci], src[mci], mciCustomizer); + }); + } - function applyThemeMciBlock(dest, src, formKey) { - if(_.isObject(src.mci)) { - mergeMciProperties(dest, src.mci); - } else { - if(_.has(src, [ formKey, 'mci' ])) { - mergeMciProperties(dest, src[formKey].mci); - } - } - } + function applyThemeMciBlock(dest, src, formKey) { + if(_.isObject(src.mci)) { + mergeMciProperties(dest, src.mci); + } else { + if(_.has(src, [ formKey, 'mci' ])) { + mergeMciProperties(dest, src[formKey].mci); + } + } + } - // - // menu.hjson can have a couple different structures: - // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block - // (this allows multiple layout types defined by one menu for example) - // - // 2) Non-explicit declaration: 'mci' directly under 'form:' - // - // theme.hjson has it's own mix: - // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) - // - // 2) Non-explicit: 'mci' directly under an entry - // - // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up - // with menu.hjson in #1. - // - // * When theming an explicit menu.hjson entry (1), we will use a matching explicit - // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" - // and fall back to generic if a match is not found. - // - // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming - // there is a generic 'mci' block. - // - function applyToForm(form, menuTheme, formKey) { - if(_.isObject(form.mci)) { - // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID - applyThemeMciBlock(form.mci, menuTheme, formKey); + // + // menu.hjson can have a couple different structures: + // 1) Explicit declaration of expected MCI code(s) under 'form:' before a 'mci' block + // (this allows multiple layout types defined by one menu for example) + // + // 2) Non-explicit declaration: 'mci' directly under 'form:' + // + // theme.hjson has it's own mix: + // 1) Explicit: Form ID before 'mci' (generally used where there are > 1 forms) + // + // 2) Non-explicit: 'mci' directly under an entry + // + // Additionally, #1 or #2 may be under an explicit key of MCI code(s) to match up + // with menu.hjson in #1. + // + // * When theming an explicit menu.hjson entry (1), we will use a matching explicit + // entry with a matching MCI code(s) key in theme.hjson (e.g. menu="ETVM"/theme="ETVM" + // and fall back to generic if a match is not found. + // + // * If theme.hjson provides form ID's, use them. Otherwise, we'll apply directly assuming + // there is a generic 'mci' block. + // + function applyToForm(form, menuTheme, formKey) { + if(_.isObject(form.mci)) { + // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID + applyThemeMciBlock(form.mci, menuTheme, formKey); - } else { - const menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { - return k === k.toUpperCase(); // remove anything not uppercase - }); + } else { + const menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { + return k === k.toUpperCase(); // remove anything not uppercase + }); - menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { - let applyFrom; - if(_.has(menuTheme, [ mciKey, 'mci' ])) { - applyFrom = menuTheme[mciKey]; - } else { - applyFrom = menuTheme; - } + menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { + let applyFrom; + if(_.has(menuTheme, [ mciKey, 'mci' ])) { + applyFrom = menuTheme[mciKey]; + } else { + applyFrom = menuTheme; + } - applyThemeMciBlock(form[mciKey].mci, applyFrom, formKey); - }); - } - } + applyThemeMciBlock(form[mciKey].mci, applyFrom, formKey); + }); + } + } - [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { - _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { - let createdFormSection = false; - const mergedThemeMenu = mergedTheme[sectionName][menuName]; + [ 'menus', 'prompts' ].forEach(function areaEntry(sectionName) { + _.keys(mergedTheme[sectionName]).forEach(function menuEntry(menuName) { + let createdFormSection = false; + const mergedThemeMenu = mergedTheme[sectionName][menuName]; - if(_.has(theme, [ 'customization', sectionName, menuName ])) { - const menuTheme = theme.customization[sectionName][menuName]; + if(_.has(theme, [ 'customization', sectionName, menuName ])) { + const menuTheme = theme.customization[sectionName][menuName]; - // config block is direct assign/overwrite - // :TODO: should probably be _.merge() - if(menuTheme.config) { - mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); - } + // config block is direct assign/overwrite + // :TODO: should probably be _.merge() + if(menuTheme.config) { + mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); + } - if('menus' === sectionName) { - if(_.isObject(mergedThemeMenu.form)) { - getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { - applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); - }); - } else { - if(_.isObject(menuTheme.mci)) { - // - // Not specified at menu level means we apply anything from the - // theme to form.0.mci{} - // - mergedThemeMenu.form = { 0 : { mci : { } } }; - mergeMciProperties(mergedThemeMenu.form[0], menuTheme); - createdFormSection = true; - } - } - } else if('prompts' === sectionName) { - // no 'form' or form keys for prompts -- direct to mci - applyToForm(mergedThemeMenu, menuTheme); - } - } + if('menus' === sectionName) { + if(_.isObject(mergedThemeMenu.form)) { + getFormKeys(mergedThemeMenu.form).forEach(function formKeyEntry(formKey) { + applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); + }); + } else { + if(_.isObject(menuTheme.mci)) { + // + // Not specified at menu level means we apply anything from the + // theme to form.0.mci{} + // + mergedThemeMenu.form = { 0 : { mci : { } } }; + mergeMciProperties(mergedThemeMenu.form[0], menuTheme); + createdFormSection = true; + } + } + } else if('prompts' === sectionName) { + // no 'form' or form keys for prompts -- direct to mci + applyToForm(mergedThemeMenu, menuTheme); + } + } - // - // Finished merging for this menu/prompt - // - // If the following conditions are true, set runtime.autoNext to true: - // * This is a menu - // * There is/was no explicit 'form' section - // * There is no 'prompt' specified - // - if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && + // + // Finished merging for this menu/prompt + // + // If the following conditions are true, set runtime.autoNext to true: + // * This is a menu + // * There is/was no explicit 'form' section + // * There is no 'prompt' specified + // + if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && (createdFormSection || !_.isObject(mergedThemeMenu.form))) - { - mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); - } - }); - }); + { + mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); + } + }); + }); - return mergedTheme; + return mergedTheme; } function reloadTheme(themeId) { - const config = Config(); - async.waterfall( - [ - function loadMenuConfig(callback) { - getFullConfig(config.general.menuFile, (err, menuConfig) => { - return callback(err, menuConfig); - }); - }, - function loadPromptConfig(menuConfig, callback) { - getFullConfig(config.general.promptFile, (err, promptConfig) => { - return callback(err, menuConfig, promptConfig); - }); - }, - function loadIt(menuConfig, promptConfig, callback) { - loadTheme(themeId, (err, theme) => { - if(err) { - if(ErrorReasons.NotEnabled !== err.reasonCode) { - Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); - return; - } - return callback(err); - } + const config = Config(); + async.waterfall( + [ + function loadMenuConfig(callback) { + getFullConfig(config.general.menuFile, (err, menuConfig) => { + return callback(err, menuConfig); + }); + }, + function loadPromptConfig(menuConfig, callback) { + getFullConfig(config.general.promptFile, (err, promptConfig) => { + return callback(err, menuConfig, promptConfig); + }); + }, + function loadIt(menuConfig, promptConfig, callback) { + loadTheme(themeId, (err, theme) => { + if(err) { + if(ErrorReasons.NotEnabled !== err.reasonCode) { + Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); + return; + } + return callback(err); + } - Object.assign(theme.info, { themeId } ); - availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); + Object.assign(theme.info, { themeId } ); + availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); - Events.emit( - Events.getSystemEvents().ThemeChanged, - { themeId } - ); + Events.emit( + Events.getSystemEvents().ThemeChanged, + { themeId } + ); - return callback(null, theme); - }); - } - ], - (err, theme) => { - if(err) { - Log.warn( { themeId, error : err.message }, 'Failed to reload theme'); - } else { - Log.debug( { info : theme.info }, 'Theme recached' ); - } - } - ); + return callback(null, theme); + }); + } + ], + (err, theme) => { + if(err) { + Log.warn( { themeId, error : err.message }, 'Failed to reload theme'); + } else { + Log.debug( { info : theme.info }, 'Theme recached' ); + } + } + ); } function reloadAllThemes() { - async.each([ ...availableThemes.keys() ], themeId => reloadTheme(themeId)); + async.each([ ...availableThemes.keys() ], themeId => reloadTheme(themeId)); } function initAvailableThemes(cb) { - const config = Config(); - async.waterfall( - [ - function loadMenuConfig(callback) { - getFullConfig(config.general.menuFile, (err, menuConfig) => { - return callback(err, menuConfig); - }); - }, - function loadPromptConfig(menuConfig, callback) { - getFullConfig(config.general.promptFile, (err, promptConfig) => { - return callback(err, menuConfig, promptConfig); - }); - }, - function getThemeDirectories(menuConfig, promptConfig, callback) { - fs.readdir(config.paths.themes, (err, files) => { - if(err) { - return callback(err); - } + const config = Config(); + async.waterfall( + [ + function loadMenuConfig(callback) { + getFullConfig(config.general.menuFile, (err, menuConfig) => { + return callback(err, menuConfig); + }); + }, + function loadPromptConfig(menuConfig, callback) { + getFullConfig(config.general.promptFile, (err, promptConfig) => { + return callback(err, menuConfig, promptConfig); + }); + }, + function getThemeDirectories(menuConfig, promptConfig, callback) { + fs.readdir(config.paths.themes, (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 populateAvailable(menuConfig, promptConfig, themeDirectories, callback) { - async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID - loadTheme(themeId, (err, theme) => { - if(err) { - if(ErrorReasons.NotEnabled !== err.reasonCode) { - Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); - } + 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 populateAvailable(menuConfig, promptConfig, themeDirectories, callback) { + async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID + loadTheme(themeId, (err, theme) => { + if(err) { + if(ErrorReasons.NotEnabled !== err.reasonCode) { + Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); + } - return nextThemeDir(null); // try next - } + return nextThemeDir(null); // try next + } - Object.assign(theme.info, { themeId } ); - availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); - return nextThemeDir(null); - }); - }, err => { - return callback(err); - }); - }, - function initEvents(callback) { - Events.on(Events.getSystemEvents().MenusChanged, () => { - return reloadAllThemes(); - }); - Events.on(Events.getSystemEvents().PromptsChanged, () => { - return reloadAllThemes(); - }); + Object.assign(theme.info, { themeId } ); + availableThemes.set(themeId, getMergedTheme(menuConfig, promptConfig, theme)); + return nextThemeDir(null); + }); + }, err => { + return callback(err); + }); + }, + function initEvents(callback) { + Events.on(Events.getSystemEvents().MenusChanged, () => { + return reloadAllThemes(); + }); + Events.on(Events.getSystemEvents().PromptsChanged, () => { + return reloadAllThemes(); + }); - return callback(null); - } - ], - err => { - return cb(err, availableThemes.size); - } - ); + return callback(null); + } + ], + err => { + return cb(err, availableThemes.size); + } + ); } function getAvailableThemes() { - return availableThemes; + return availableThemes; } function getRandomTheme() { - if(availableThemes.size > 0) { - const themeIds = [ ...availableThemes.keys() ]; - return themeIds[Math.floor(Math.random() * themeIds.length)]; - } + if(availableThemes.size > 0) { + const themeIds = [ ...availableThemes.keys() ]; + return themeIds[Math.floor(Math.random() * themeIds.length)]; + } } function setClientTheme(client, themeId) { - const availThemes = getAvailableThemes(); + const availThemes = getAvailableThemes(); - let msg; - let setThemeId; - const config = Config(); - if(availThemes.has(themeId)) { - msg = 'Set client theme'; - setThemeId = themeId; - } else if(availThemes.has(config.defaults.theme)) { - msg = 'Failed setting theme by supplied ID; Using default'; - setThemeId = config.defaults.theme; - } else { - msg = 'Failed setting theme by system default ID; Using the first one we can find'; - setThemeId = availThemes.keys().next().value; - } + let msg; + let setThemeId; + const config = Config(); + if(availThemes.has(themeId)) { + msg = 'Set client theme'; + setThemeId = themeId; + } else if(availThemes.has(config.defaults.theme)) { + msg = 'Failed setting theme by supplied ID; Using default'; + setThemeId = config.defaults.theme; + } else { + msg = 'Failed setting theme by system default ID; Using the first one we can find'; + setThemeId = availThemes.keys().next().value; + } - client.currentTheme = availThemes.get(setThemeId); - client.log.debug( { setThemeId, requestedThemeId : themeId, info : client.currentTheme.info }, msg); + client.currentTheme = availThemes.get(setThemeId); + client.log.debug( { setThemeId, requestedThemeId : themeId, info : client.currentTheme.info }, msg); } function getThemeArt(options, cb) { - // - // options - required: - // name - // - // options - optional - // client - needed for user's theme/etc. - // themeId - // asAnsi - // readSauce - // random - // - const config = Config(); - if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { - options.themeId = options.client.user.properties.theme_id; - } else { - options.themeId = config.defaults.theme; - } + // + // options - required: + // name + // + // options - optional + // client - needed for user's theme/etc. + // themeId + // asAnsi + // readSauce + // random + // + const config = Config(); + if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { + options.themeId = options.client.user.properties.theme_id; + } else { + options.themeId = config.defaults.theme; + } - // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... - // :TODO: Some of these options should only be set if not provided! - options.asAnsi = true; // always convert to ANSI - options.readSauce = true; // read SAUCE, if avail - options.random = _.get(options, 'random', true); // FILENAME.EXT support + // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... + // :TODO: Some of these options should only be set if not provided! + options.asAnsi = true; // always convert to ANSI + options.readSauce = true; // read SAUCE, if avail + options.random = _.get(options, 'random', true); // FILENAME.EXT support - // - // We look for themed art in the following order: - // 1) Direct/relative path - // 2) Via theme supplied by |themeId| - // 3) Via default theme - // 4) General art directory - // - async.waterfall( - [ - function fromPath(callback) { - // - // We allow relative (to enigma-bbs) or full paths - // - if('/' === options.name.charAt(0)) { - // just take the path as-is - options.basePath = paths.dirname(options.name); - } else if(options.name.indexOf('/') > -1) { - // make relative to base BBS dir - options.basePath = paths.join(__dirname, '../', paths.dirname(options.name)); - } else { - return callback(null, null); - } + // + // We look for themed art in the following order: + // 1) Direct/relative path + // 2) Via theme supplied by |themeId| + // 3) Via default theme + // 4) General art directory + // + async.waterfall( + [ + function fromPath(callback) { + // + // We allow relative (to enigma-bbs) or full paths + // + if('/' === options.name.charAt(0)) { + // just take the path as-is + options.basePath = paths.dirname(options.name); + } else if(options.name.indexOf('/') > -1) { + // make relative to base BBS dir + options.basePath = paths.join(__dirname, '../', paths.dirname(options.name)); + } else { + return callback(null, null); + } - art.getArt(options.name, options, (err, artInfo) => { - return callback(null, artInfo); - }); - }, - function fromSuppliedTheme(artInfo, callback) { - if(artInfo) { - return callback(null, artInfo); - } + art.getArt(options.name, options, (err, artInfo) => { + return callback(null, artInfo); + }); + }, + function fromSuppliedTheme(artInfo, callback) { + if(artInfo) { + return callback(null, artInfo); + } - options.basePath = paths.join(config.paths.themes, options.themeId); - art.getArt(options.name, options, (err, artInfo) => { - return callback(null, artInfo); - }); - }, - function fromDefaultTheme(artInfo, callback) { - if(artInfo || config.defaults.theme === options.themeId) { - return callback(null, artInfo); - } + options.basePath = paths.join(config.paths.themes, options.themeId); + art.getArt(options.name, options, (err, artInfo) => { + return callback(null, artInfo); + }); + }, + function fromDefaultTheme(artInfo, callback) { + if(artInfo || config.defaults.theme === options.themeId) { + return callback(null, artInfo); + } - options.basePath = paths.join(config.paths.themes, config.defaults.theme); - art.getArt(options.name, options, (err, artInfo) => { - return callback(null, artInfo); - }); - }, - function fromGeneralArtDir(artInfo, callback) { - if(artInfo) { - return callback(null, artInfo); - } + options.basePath = paths.join(config.paths.themes, config.defaults.theme); + art.getArt(options.name, options, (err, artInfo) => { + return callback(null, artInfo); + }); + }, + function fromGeneralArtDir(artInfo, callback) { + if(artInfo) { + return callback(null, artInfo); + } - options.basePath = config.paths.art; - art.getArt(options.name, options, (err, artInfo) => { - return callback(err, artInfo); - }); - } - ], - function complete(err, artInfo) { - if(err) { - const logger = _.get(options, 'client.log') || Log; - logger.debug( { reason : err.message }, 'Cannot find theme art'); - } - return cb(err, artInfo); - } - ); + options.basePath = config.paths.art; + art.getArt(options.name, options, (err, artInfo) => { + return callback(err, artInfo); + }); + } + ], + function complete(err, artInfo) { + if(err) { + const logger = _.get(options, 'client.log') || Log; + logger.debug( { reason : err.message }, 'Cannot find theme art'); + } + return cb(err, artInfo); + } + ); } function displayThemeArt(options, cb) { - assert(_.isObject(options)); - assert(_.isObject(options.client)); - assert(_.isString(options.name)); + assert(_.isObject(options)); + assert(_.isObject(options.client)); + assert(_.isString(options.name)); - getThemeArt(options, (err, artInfo) => { - if(err) { - return cb(err); - } - // :TODO: just use simple merge of options -> displayOptions - const displayOpts = { - sauce : artInfo.sauce, - font : options.font, - trailingLF : options.trailingLF, - }; + getThemeArt(options, (err, artInfo) => { + if(err) { + return cb(err); + } + // :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 } ); - }); - }); + art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { + return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } ); + }); + }); } function displayThemedPrompt(name, client, options, cb) { - const useTempViewController = _.isUndefined(options.viewController); + const useTempViewController = _.isUndefined(options.viewController); - async.waterfall( - [ - function display(callback) { - const promptConfig = client.currentTheme.prompts[name]; - if(!promptConfig) { - return callback(Errors.DoesNotExist(`Missing "${name}" prompt configuration!`)); - } + async.waterfall( + [ + function display(callback) { + const promptConfig = client.currentTheme.prompts[name]; + if(!promptConfig) { + return callback(Errors.DoesNotExist(`Missing "${name}" prompt configuration!`)); + } - if(options.clearScreen) { - client.term.rawWrite(ansi.resetScreen()); - } + if(options.clearScreen) { + client.term.rawWrite(ansi.resetScreen()); + } - // - // If we did *not* clear the screen, don't let the font change - // 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 ); - if(!options.clearScreen) { - dispOptions.font = 'not_really_a_font!'; // kludge :) - } + // + // If we did *not* clear the screen, don't let the font change + // 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 ); + if(!options.clearScreen) { + dispOptions.font = 'not_really_a_font!'; // kludge :) + } - displayThemedAsset( - promptConfig.art, - client, - dispOptions, - (err, artInfo) => { - if(err) { - return callback(err); - } + displayThemedAsset( + promptConfig.art, + client, + dispOptions, + (err, artInfo) => { + if(err) { + return callback(err); + } - return callback(null, promptConfig, artInfo); - } - ); - }, - function discoverCursorPosition(promptConfig, artInfo, callback) { - if(!options.clearPrompt) { - // no need to query cursor - we're not gonna use it - return callback(null, promptConfig, artInfo); - } + return callback(null, promptConfig, artInfo); + } + ); + }, + function discoverCursorPosition(promptConfig, artInfo, callback) { + if(!options.clearPrompt) { + // no need to query cursor - we're not gonna use it + return callback(null, promptConfig, artInfo); + } - client.once('cursor position report', pos => { - artInfo.startRow = pos[0] - artInfo.height; - return callback(null, promptConfig, artInfo); - }); + client.once('cursor position report', pos => { + artInfo.startRow = pos[0] - artInfo.height; + return callback(null, promptConfig, artInfo); + }); - client.term.rawWrite(ansi.queryPos()); - }, - function createMCIViews(promptConfig, artInfo, callback) { - const tempViewController = useTempViewController ? new ViewController( { client : client } ) : options.viewController; + client.term.rawWrite(ansi.queryPos()); + }, + function createMCIViews(promptConfig, artInfo, callback) { + const tempViewController = useTempViewController ? new ViewController( { client : client } ) : options.viewController; - const loadOpts = { - promptName : name, - mciMap : artInfo.mciMap, - config : promptConfig, - }; + const loadOpts = { + promptName : name, + mciMap : artInfo.mciMap, + config : promptConfig, + }; - tempViewController.loadFromPromptConfig(loadOpts, () => { - return callback(null, artInfo, tempViewController); - }); - }, - function pauseForUserInput(artInfo, tempViewController, callback) { - if(!options.pause) { - return callback(null, artInfo, tempViewController); - } + tempViewController.loadFromPromptConfig(loadOpts, () => { + return callback(null, artInfo, tempViewController); + }); + }, + function pauseForUserInput(artInfo, tempViewController, callback) { + if(!options.pause) { + return callback(null, artInfo, tempViewController); + } - client.waitForKeyPress( () => { - return callback(null, artInfo, tempViewController); - }); - }, - function clearPauseArt(artInfo, tempViewController, callback) { - if(options.clearPrompt) { - if(artInfo.startRow && artInfo.height) { - client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); + client.waitForKeyPress( () => { + return callback(null, artInfo, tempViewController); + }); + }, + function clearPauseArt(artInfo, tempViewController, callback) { + if(options.clearPrompt) { + if(artInfo.startRow && artInfo.height) { + client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); - // Note: Does not work properly in NetRunner < 2.0b17: - client.term.rawWrite(ansi.deleteLine(artInfo.height)); - } else { - client.term.rawWrite(ansi.eraseLine(1)); - } - } + // Note: Does not work properly in NetRunner < 2.0b17: + client.term.rawWrite(ansi.deleteLine(artInfo.height)); + } else { + client.term.rawWrite(ansi.eraseLine(1)); + } + } - return callback(null, tempViewController); - } - ], - (err, tempViewController) => { - if(err) { - client.log.warn( { error : err.message }, `Failed displaying "${name}" prompt` ); - } + return callback(null, tempViewController); + } + ], + (err, tempViewController) => { + if(err) { + client.log.warn( { error : err.message }, `Failed displaying "${name}" prompt` ); + } - if(tempViewController && useTempViewController) { - tempViewController.detachClientEvents(); - } + if(tempViewController && useTempViewController) { + tempViewController.detachClientEvents(); + } - return cb(null); - } - ); + return cb(null); + } + ); } // @@ -635,63 +635,63 @@ function displayThemedPrompt(name, client, options, cb) { // function displayThemedPause(client, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } - if(!_.isBoolean(options.clearPrompt)) { - options.clearPrompt = true; - } + if(!_.isBoolean(options.clearPrompt)) { + options.clearPrompt = true; + } - const promptOptions = Object.assign( {}, options, { pause : true } ); - return displayThemedPrompt('pause', client, promptOptions, cb); + const promptOptions = Object.assign( {}, options, { pause : true } ); + return displayThemedPrompt('pause', client, promptOptions, cb); } function displayThemedAsset(assetSpec, client, options, cb) { - assert(_.isObject(client)); + assert(_.isObject(client)); - // options are... optional - if(3 === arguments.length) { - cb = options; - options = {}; - } + // options are... optional + if(3 === arguments.length) { + cb = options; + options = {}; + } - if(Array.isArray(assetSpec) && _.isString(options.acsCondMember)) { - assetSpec = client.acs.getConditionalValue(assetSpec, options.acsCondMember); - } + if(Array.isArray(assetSpec) && _.isString(options.acsCondMember)) { + assetSpec = client.acs.getConditionalValue(assetSpec, options.acsCondMember); + } - const artAsset = asset.getArtAsset(assetSpec); - if(!artAsset) { - return cb(new Error('Asset not found: ' + assetSpec)); - } + const artAsset = asset.getArtAsset(assetSpec); + if(!artAsset) { + return cb(new Error('Asset not found: ' + assetSpec)); + } - // :TODO: just use simple merge of options -> displayOptions - var dispOpts = { - name : artAsset.asset, - client : client, - font : options.font, - trailingLF : options.trailingLF, - }; + // :TODO: just use simple merge of options -> displayOptions + var dispOpts = { + name : artAsset.asset, + client : client, + font : options.font, + trailingLF : options.trailingLF, + }; - switch(artAsset.type) { - case 'art' : - displayThemeArt(dispOpts, function displayed(err, artData) { - return cb(err, err ? null : { mciMap : artData.mciMap, height : artData.extraInfo.height } ); - }); - break; + switch(artAsset.type) { + case 'art' : + displayThemeArt(dispOpts, function displayed(err, artData) { + return cb(err, err ? null : { mciMap : artData.mciMap, height : artData.extraInfo.height } ); + }); + break; - case 'method' : - // :TODO: fetch & render via method - break; + case 'method' : + // :TODO: fetch & render via method + break; - case 'inline ' : - // :TODO: think about this more in relation to themes, etc. How can this come - // from a theme (with override from menu.json) ??? - // look @ client.currentTheme.inlineArt[name] -> menu/prompt[name] - break; + case 'inline ' : + // :TODO: think about this more in relation to themes, etc. How can this come + // from a theme (with override from menu.json) ??? + // look @ client.currentTheme.inlineArt[name] -> menu/prompt[name] + break; - default : - return cb(new Error('Unsupported art asset type: ' + artAsset.type)); - } + default : + return cb(new Error('Unsupported art asset type: ' + artAsset.type)); + } } \ No newline at end of file diff --git a/core/tic_file_info.js b/core/tic_file_info.js index 124bbca1..37aec134 100644 --- a/core/tic_file_info.js +++ b/core/tic_file_info.js @@ -22,264 +22,264 @@ const crypto = require('crypto'); // * FSC-0087.001 @ http://ftsc.org/docs/fsc-0087.001 // module.exports = class TicFileInfo { - constructor() { - this.entries = new Map(); - } + constructor() { + this.entries = new Map(); + } - static get requiredFields() { - return [ - 'Area', 'Origin', 'From', 'File', 'Crc', - // :TODO: validate this: - //'Path', 'Seenby' // these two are questionable; some systems don't send them? - ]; - } + static get requiredFields() { + return [ + 'Area', 'Origin', 'From', 'File', 'Crc', + // :TODO: validate this: + //'Path', 'Seenby' // these two are questionable; some systems don't send them? + ]; + } - get(key) { - return this.entries.get(key.toLowerCase()); - } + get(key) { + return this.entries.get(key.toLowerCase()); + } - getAsString(key, joinWith) { - const value = this.get(key); - if(value) { - // - // We call toString() on values to ensure numbers, addresses, etc. are converted - // - joinWith = joinWith || ''; - if(Array.isArray(value)) { - return value.map(v => v.toString() ).join(joinWith); - } + getAsString(key, joinWith) { + const value = this.get(key); + if(value) { + // + // We call toString() on values to ensure numbers, addresses, etc. are converted + // + joinWith = joinWith || ''; + if(Array.isArray(value)) { + return value.map(v => v.toString() ).join(joinWith); + } - return value.toString(); - } - } + return value.toString(); + } + } - get filePath() { - return paths.join(paths.dirname(this.path), this.getAsString('File')); - } + get filePath() { + return paths.join(paths.dirname(this.path), this.getAsString('File')); + } - get longFileName() { - return this.getAsString('Lfile') || this.getAsString('Fullname') || this.getAsString('File'); - } + get longFileName() { + return this.getAsString('Lfile') || this.getAsString('Fullname') || this.getAsString('File'); + } - hasRequiredFields() { - const req = TicFileInfo.requiredFields; - return req.every( f => this.get(f) ); - } + hasRequiredFields() { + const req = TicFileInfo.requiredFields; + return req.every( f => this.get(f) ); + } - validate(config, cb) { - // config.nodes - // config.defaultPassword (optional) - // config.localAreaTags - EnigAssert(config.nodes && config.localAreaTags); + validate(config, cb) { + // config.nodes + // config.defaultPassword (optional) + // config.localAreaTags + EnigAssert(config.nodes && config.localAreaTags); - const self = this; + const self = this; - async.waterfall( - [ - function initial(callback) { - if(!self.hasRequiredFields()) { - return callback(Errors.Invalid('One or more required fields missing from TIC')); - } + async.waterfall( + [ + function initial(callback) { + if(!self.hasRequiredFields()) { + return callback(Errors.Invalid('One or more required fields missing from TIC')); + } - const area = self.getAsString('Area').toUpperCase(); + const area = self.getAsString('Area').toUpperCase(); - const localInfo = { - areaTag : config.localAreaTags.find( areaTag => areaTag.toUpperCase() === area ), - }; + const localInfo = { + areaTag : config.localAreaTags.find( areaTag => areaTag.toUpperCase() === area ), + }; - if(!localInfo.areaTag) { - return callback(Errors.Invalid(`No local area for "Area" of ${area}`)); - } + if(!localInfo.areaTag) { + return callback(Errors.Invalid(`No local area for "Area" of ${area}`)); + } - const from = Address.fromString(self.getAsString('From')); - if(!from.isValid()) { - return callback(Errors.Invalid(`Invalid "From" address: ${self.getAsString('From')}`)); - } + const from = Address.fromString(self.getAsString('From')); + if(!from.isValid()) { + return callback(Errors.Invalid(`Invalid "From" address: ${self.getAsString('From')}`)); + } - // note that our config may have wildcards, such as "80:774/*" - localInfo.node = Object.keys(config.nodes).find( nodeAddrWildcard => from.isPatternMatch(nodeAddrWildcard) ); + // note that our config may have wildcards, such as "80:774/*" + localInfo.node = Object.keys(config.nodes).find( nodeAddrWildcard => from.isPatternMatch(nodeAddrWildcard) ); - if(!localInfo.node) { - return callback(Errors.Invalid('TIC is not from a known node')); - } + if(!localInfo.node) { + return callback(Errors.Invalid('TIC is not from a known node')); + } - // if we require a password, "PW" must match - const passActual = _.get(config.nodes, [ localInfo.node, 'tic', 'password' ] ) || config.defaultPassword; - if(!passActual) { - return callback(null, localInfo); // no pw validation - } + // if we require a password, "PW" must match + const passActual = _.get(config.nodes, [ localInfo.node, 'tic', 'password' ] ) || config.defaultPassword; + if(!passActual) { + return callback(null, localInfo); // no pw validation + } - const passTic = self.getAsString('Pw'); - if(passTic !== passActual) { - return callback(Errors.Invalid('Bad TIC password')); - } + const passTic = self.getAsString('Pw'); + if(passTic !== passActual) { + return callback(Errors.Invalid('Bad TIC password')); + } - return callback(null, localInfo); - }, - function checksumAndSize(localInfo, callback) { - const crcTic = self.get('Crc'); - const stream = fs.createReadStream(self.filePath); - const crc = new CRC32(); - let sizeActual = 0; + return callback(null, localInfo); + }, + function checksumAndSize(localInfo, callback) { + const crcTic = self.get('Crc'); + const stream = fs.createReadStream(self.filePath); + const crc = new CRC32(); + let sizeActual = 0; - let sha256Tic = self.getAsString('Sha256'); - let sha256; - if(sha256Tic) { - sha256Tic = sha256Tic.toLowerCase(); - sha256 = crypto.createHash('sha256'); - } + let sha256Tic = self.getAsString('Sha256'); + let sha256; + if(sha256Tic) { + sha256Tic = sha256Tic.toLowerCase(); + sha256 = crypto.createHash('sha256'); + } - stream.on('data', data => { - sizeActual += data.length; + stream.on('data', data => { + sizeActual += data.length; - // sha256 if possible, else crc32 - if(sha256) { - sha256.update(data); - } else { - crc.update(data); - } - }); + // sha256 if possible, else crc32 + if(sha256) { + sha256.update(data); + } else { + crc.update(data); + } + }); - stream.on('end', () => { - // again, use sha256 if possible - if(sha256) { - const sha256Actual = sha256.digest('hex'); - if(sha256Tic != sha256Actual) { - return callback(Errors.Invalid(`TIC "Sha256" of ${sha256Tic} does not match actual SHA-256 of ${sha256Actual}`)); - } + stream.on('end', () => { + // again, use sha256 if possible + if(sha256) { + const sha256Actual = sha256.digest('hex'); + if(sha256Tic != sha256Actual) { + return callback(Errors.Invalid(`TIC "Sha256" of ${sha256Tic} does not match actual SHA-256 of ${sha256Actual}`)); + } - localInfo.sha256 = sha256Actual; - } else { - const crcActual = crc.finalize(); - if(crcActual !== crcTic) { - return callback(Errors.Invalid(`TIC "Crc" of ${crcTic} does not match actual CRC-32 of ${crcActual}`)); - } - localInfo.crc32 = crcActual; - } + localInfo.sha256 = sha256Actual; + } else { + const crcActual = crc.finalize(); + if(crcActual !== crcTic) { + return callback(Errors.Invalid(`TIC "Crc" of ${crcTic} does not match actual CRC-32 of ${crcActual}`)); + } + localInfo.crc32 = crcActual; + } - const sizeTic = self.get('Size'); - if(_.isUndefined(sizeTic)) { - return callback(null, localInfo); - } + const sizeTic = self.get('Size'); + if(_.isUndefined(sizeTic)) { + return callback(null, localInfo); + } - if(sizeTic !== sizeActual) { - return callback(Errors.Invalid(`TIC "Size" of ${sizeTic} does not match actual size of ${sizeActual}`)); - } + if(sizeTic !== sizeActual) { + return callback(Errors.Invalid(`TIC "Size" of ${sizeTic} does not match actual size of ${sizeActual}`)); + } - return callback(null, localInfo); - }); + return callback(null, localInfo); + }); - stream.on('error', err => { - return callback(err); - }); - } - ], - (err, localInfo) => { - return cb(err, localInfo); - } - ); - } + stream.on('error', err => { + return callback(err); + }); + } + ], + (err, localInfo) => { + return cb(err, localInfo); + } + ); + } - isToAddress(address, allowNonExplicit) { - // - // FSP-1039.001: - // "This keyword specifies the FTN address of the system where to - // send the file to be distributed and the accompanying TIC file. - // Some File processors (Allfix) only insert a line with this - // keyword when the file and the associated TIC file are to be - // file routed through a third sysem instead of being processed - // by a file processor on that system. Others always insert it. - // Note that the To keyword may cause problems when the TIC file - // is proecessed by software that does not recognise it and - // passes the line "as is" to other systems. - // - // Example: To 292/854 - // - // This is an optional keyword." - // - const to = this.get('To'); + isToAddress(address, allowNonExplicit) { + // + // FSP-1039.001: + // "This keyword specifies the FTN address of the system where to + // send the file to be distributed and the accompanying TIC file. + // Some File processors (Allfix) only insert a line with this + // keyword when the file and the associated TIC file are to be + // file routed through a third sysem instead of being processed + // by a file processor on that system. Others always insert it. + // Note that the To keyword may cause problems when the TIC file + // is proecessed by software that does not recognise it and + // passes the line "as is" to other systems. + // + // Example: To 292/854 + // + // This is an optional keyword." + // + const to = this.get('To'); - if(!to) { - return allowNonExplicit; - } + if(!to) { + return allowNonExplicit; + } - return address.isEqual(to); - } + return address.isEqual(to); + } - static createFromFile(path, cb) { - fs.readFile(path, 'utf8', (err, ticData) => { - if(err) { - return cb(err); - } + static createFromFile(path, cb) { + fs.readFile(path, 'utf8', (err, ticData) => { + if(err) { + return cb(err); + } - const ticFileInfo = new TicFileInfo(); - ticFileInfo.path = path; + const ticFileInfo = new TicFileInfo(); + ticFileInfo.path = path; - // - // Lines in a TIC file should be separated by CRLF (DOS) - // may be separated by LF (UNIX) - // - const lines = ticData.split(/\r\n|\n/g); - let keyEnd; - let key; - let value; - let entry; + // + // Lines in a TIC file should be separated by CRLF (DOS) + // may be separated by LF (UNIX) + // + const lines = ticData.split(/\r\n|\n/g); + let keyEnd; + let key; + let value; + let entry; - lines.forEach(line => { - keyEnd = line.search(/\s/); + lines.forEach(line => { + keyEnd = line.search(/\s/); - if(keyEnd < 0) { - keyEnd = line.length; - } + if(keyEnd < 0) { + keyEnd = line.length; + } - key = line.substr(0, keyEnd).toLowerCase(); + key = line.substr(0, keyEnd).toLowerCase(); - if(0 === key.length) { - return; - } + if(0 === key.length) { + return; + } - value = line.substr(keyEnd + 1); + value = line.substr(keyEnd + 1); - // don't trim Ldesc; may mess with FILE_ID.DIZ type descriptions - if('ldesc' !== key) { - value = value.trim(); - } + // don't trim Ldesc; may mess with FILE_ID.DIZ type descriptions + if('ldesc' !== key) { + value = value.trim(); + } - // convert well known keys to a more reasonable format - switch(key) { - case 'origin' : - case 'from' : - case 'seenby' : - case 'to' : - value = Address.fromString(value); - break; + // convert well known keys to a more reasonable format + switch(key) { + case 'origin' : + case 'from' : + case 'seenby' : + case 'to' : + value = Address.fromString(value); + break; - case 'crc' : - value = parseInt(value, 16); - break; + case 'crc' : + value = parseInt(value, 16); + break; - case 'size' : - value = parseInt(value, 10); - break; + case 'size' : + value = parseInt(value, 10); + break; - default : - break; - } + default : + break; + } - entry = ticFileInfo.entries.get(key); + entry = ticFileInfo.entries.get(key); - if(entry) { - if(!Array.isArray(entry)) { - entry = [ entry ]; - ticFileInfo.entries.set(key, entry); - } - entry.push(value); - } else { - ticFileInfo.entries.set(key, value); - } - }); + if(entry) { + if(!Array.isArray(entry)) { + entry = [ entry ]; + ticFileInfo.entries.set(key, entry); + } + entry.push(value); + } else { + ticFileInfo.entries.set(key, value); + } + }); - return cb(null, ticFileInfo); - }); - } + return cb(null, ticFileInfo); + }); + } }; diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index 39d7ef95..28818189 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -10,112 +10,112 @@ const assert = require('assert'); exports.ToggleMenuView = ToggleMenuView; function ToggleMenuView (options) { - options.cursor = options.cursor || 'hide'; + options.cursor = options.cursor || 'hide'; - MenuView.call(this, options); + MenuView.call(this, options); - var self = this; + var self = this; - /* + /* this.cachePositions = function() { self.positionCacheExpired = false; }; */ - this.updateSelection = function() { - assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); - self.redraw(); - }; + this.updateSelection = function() { + assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); + self.redraw(); + }; } util.inherits(ToggleMenuView, MenuView); ToggleMenuView.prototype.redraw = function() { - ToggleMenuView.super_.prototype.redraw.call(this); + ToggleMenuView.super_.prototype.redraw.call(this); - //this.cachePositions(); + //this.cachePositions(); - this.client.term.write(this.hasFocus ? this.getFocusSGR() : this.getSGR()); + this.client.term.write(this.hasFocus ? this.getFocusSGR() : this.getSGR()); - assert(this.items.length === 2); - for(var i = 0; i < 2; i++) { - var item = this.items[i]; - var text = strUtil.stylizeString( - item.text, i === this.focusedItemIndex && this.hasFocus ? this.focusTextStyle : this.textStyle); + assert(this.items.length === 2); + for(var i = 0; i < 2; i++) { + var item = this.items[i]; + var text = strUtil.stylizeString( + item.text, i === this.focusedItemIndex && this.hasFocus ? this.focusTextStyle : this.textStyle); - if(1 === i) { - //console.log(this.styleColor1) - //var sepColor = this.getANSIColor(this.styleColor1 || this.getColor()); - //console.log(sepColor.substr(1)) - //var sepColor = '\u001b[0m\u001b[1;30m'; // :TODO: FIX ME!!! - // :TODO: sepChar needs to be configurable!!! - this.client.term.write(this.styleSGR1 + ' / '); - //this.client.term.write(sepColor + ' / '); - } + if(1 === i) { + //console.log(this.styleColor1) + //var sepColor = this.getANSIColor(this.styleColor1 || this.getColor()); + //console.log(sepColor.substr(1)) + //var sepColor = '\u001b[0m\u001b[1;30m'; // :TODO: FIX ME!!! + // :TODO: sepChar needs to be configurable!!! + this.client.term.write(this.styleSGR1 + ' / '); + //this.client.term.write(sepColor + ' / '); + } - this.client.term.write(i === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); - this.client.term.write(text); - } + this.client.term.write(i === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); + this.client.term.write(text); + } }; ToggleMenuView.prototype.setFocusItemIndex = function(index) { - ToggleMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + ToggleMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - this.updateSelection(); + this.updateSelection(); }; ToggleMenuView.prototype.setFocus = function(focused) { - ToggleMenuView.super_.prototype.setFocus.call(this, focused); + ToggleMenuView.super_.prototype.setFocus.call(this, focused); - this.redraw(); + this.redraw(); }; ToggleMenuView.prototype.focusNext = function() { - if(this.items.length - 1 === this.focusedItemIndex) { - this.focusedItemIndex = 0; - } else { - this.focusedItemIndex++; - } + if(this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + } else { + this.focusedItemIndex++; + } - this.updateSelection(); + this.updateSelection(); - ToggleMenuView.super_.prototype.focusNext.call(this); + ToggleMenuView.super_.prototype.focusNext.call(this); }; ToggleMenuView.prototype.focusPrevious = function() { - if(0 === this.focusedItemIndex) { - this.focusedItemIndex = this.items.length - 1; - } else { - this.focusedItemIndex--; - } + if(0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + } else { + this.focusedItemIndex--; + } - this.updateSelection(); + this.updateSelection(); - ToggleMenuView.super_.prototype.focusPrevious.call(this); + ToggleMenuView.super_.prototype.focusPrevious.call(this); }; ToggleMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) { - this.focusNext(); - } else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.nam4e)) { - this.focusPrevious(); - } - } + if(key) { + if(this.isKeyMapped('right', key.name) || this.isKeyMapped('down', key.name)) { + this.focusNext(); + } else if(this.isKeyMapped('left', key.name) || this.isKeyMapped('up', key.nam4e)) { + this.focusPrevious(); + } + } - ToggleMenuView.super_.prototype.onKeyPress.call(this, ch, key); + ToggleMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; ToggleMenuView.prototype.getData = function() { - return this.focusedItemIndex; + return this.focusedItemIndex; }; ToggleMenuView.prototype.setItems = function(items) { - items = items.slice(0, 2); // switch/toggle only works with two elements + items = items.slice(0, 2); // switch/toggle only works with two elements - ToggleMenuView.super_.prototype.setItems.call(this, items); + ToggleMenuView.super_.prototype.setItems.call(this, items); - this.dimens.width = items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) + this.dimens.width = items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) }; diff --git a/core/upload.js b/core/upload.js index 4ff6b3fb..2279e6b4 100644 --- a/core/upload.js +++ b/core/upload.js @@ -26,713 +26,713 @@ const paths = require('path'); const sanatizeFilename = require('sanitize-filename'); exports.moduleInfo = { - name : 'Upload', - desc : 'Module for classic file uploads', - author : 'NuSkooler', + name : 'Upload', + desc : 'Module for classic file uploads', + author : 'NuSkooler', }; const FormIds = { - options : 0, - processing : 1, - fileDetails : 2, - dupes : 3, + options : 0, + processing : 1, + fileDetails : 2, + dupes : 3, }; const MciViewIds = { - options : { - area : 1, // area selection - uploadType : 2, // blind vs specify filename - fileName : 3, // for non-blind; not editable for blind - navMenu : 4, // next/cancel/etc. - errMsg : 5, // errors (e.g. filename cannot be blank) - }, + options : { + area : 1, // area selection + uploadType : 2, // blind vs specify filename + fileName : 3, // for non-blind; not editable for blind + navMenu : 4, // next/cancel/etc. + errMsg : 5, // errors (e.g. filename cannot be blank) + }, - processing : { - calcHashIndicator : 1, - archiveListIndicator : 2, - descFileIndicator : 3, - logStep : 4, - customRangeStart : 10, // 10+ = customs - }, + processing : { + calcHashIndicator : 1, + archiveListIndicator : 2, + descFileIndicator : 3, + logStep : 4, + customRangeStart : 10, // 10+ = customs + }, - fileDetails : { - desc : 1, // defaults to 'desc' (e.g. from FILE_ID.DIZ) - tags : 2, // tag(s) for item - estYear : 3, - accept : 4, // accept fields & continue - customRangeStart : 10, // 10+ = customs - }, + fileDetails : { + desc : 1, // defaults to 'desc' (e.g. from FILE_ID.DIZ) + tags : 2, // tag(s) for item + estYear : 3, + accept : 4, // accept fields & continue + customRangeStart : 10, // 10+ = customs + }, - dupes : { - dupeList : 1, - } + dupes : { + dupeList : 1, + } }; exports.getModule = class UploadModule extends MenuModule { - constructor(options) { - super(options); - - if(_.has(options, 'lastMenuResult.recvFilePaths')) { - this.recvFilePaths = options.lastMenuResult.recvFilePaths; - } - - this.availAreas = getSortedAvailableFileAreas(this.client, { writeAcs : true } ); - - this.menuMethods = { - optionsNavContinue : (formData, extraArgs, cb) => { - return this.performUpload(cb); - }, - - fileDetailsContinue : (formData, extraArgs, cb) => { - // see displayFileDetailsPageForUploadEntry() for this hackery: - cb(null); - return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any - }, - - // validation - validateNonBlindFileName : (fileName, cb) => { - fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. - if(0 === fileName.length) { - return cb(new Error('Invalid filename')); - } - - if(0 === fileName.length) { - return cb(new Error('Filename cannot be empty')); - } - - // At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused - if(/^[0-9].*$/.test(fileName)) { - return cb(new Error('Invalid filename')); - } - - return cb(null); - }, - viewValidationListener : (err, cb) => { - const errView = this.viewControllers.options.getView(MciViewIds.options.errMsg); - if(errView) { - if(err) { - errView.setText(err.message); - } else { - errView.clearText(); - } - } - - return cb(null); - } - }; - } - - getSaveState() { - // if no areas, we're falling back due to lack of access/areas avail to upload to - if(this.availAreas.length > 0) { - return { - uploadType : this.uploadType, - tempRecvDirectory : this.tempRecvDirectory, - areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ], - }; - } - } - - restoreSavedState(savedState) { - if(savedState.areaInfo) { - this.uploadType = savedState.uploadType; - this.areaInfo = savedState.areaInfo; - this.tempRecvDirectory = savedState.tempRecvDirectory; - } - } - - isBlindUpload() { return 'blind' === this.uploadType; } - isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); } - - initSequence() { - const self = this; - - if(0 === this.availAreas.length) { - // - return this.gotoMenu(this.menuConfig.config.noUploadAreasAvailMenu || 'fileBaseNoUploadAreasAvail'); - } - - async.series( - [ - function before(callback) { - return self.beforeArt(callback); - }, - function display(callback) { - if(self.isFileTransferComplete()) { - return self.displayProcessingPage(callback); - } else { - return self.displayOptionsPage(callback); - } - } - ], - () => { - return self.finishedLoading(); - } - ); - } - - finishedLoading() { - if(this.isFileTransferComplete()) { - return this.processUploadedFiles(); - } - } - - performUpload(cb) { - temptmp.mkdir( { prefix : 'enigul-' }, (err, tempRecvDirectory) => { - if(err) { - return cb(err); - } - - // need a terminator for various external protocols - this.tempRecvDirectory = pathWithTerminatingSeparator(tempRecvDirectory); - - const modOpts = { - extraArgs : { - recvDirectory : this.tempRecvDirectory, // we'll move files from here to their area container once processed/confirmed - direction : 'recv', - } - }; - - if(!this.isBlindUpload()) { - // data has been sanatized at this point - modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData(); - } - - // - // Move along to protocol selection -> file transfer - // Upon completion, we'll re-enter the module with some file paths handed to us - // - return this.gotoMenu( - this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', - modOpts, - cb - ); - }); - } - - continueNonBlindUpload(cb) { - return cb(null); - } - - updateScanStepInfoViews(stepInfo) { - // :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC - - const fmtObj = Object.assign( {}, stepInfo); - let stepIndicatorFmt = ''; - let logStepFmt; - - const fmtConfig = this.menuConfig.config; - - const indicatorStates = fmtConfig.indicatorStates || [ '|', '/', '-', '\\' ]; - const indicatorFinished = fmtConfig.indicatorFinished || '√'; - - const indicator = { }; - const self = this; - - function updateIndicator(mci, isFinished) { - indicator.mci = mci; - - if(isFinished) { - indicator.text = indicatorFinished; - } else { - self.scanStatus.indicatorPos += 1; - if(self.scanStatus.indicatorPos >= indicatorStates.length) { - self.scanStatus.indicatorPos = 0; - } - indicator.text = indicatorStates[self.scanStatus.indicatorPos]; - } - } - - switch(stepInfo.step) { - case 'start' : - logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Scanning {fileName}'; - break; - - case 'hash_update' : - stepIndicatorFmt = fmtConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%'; - updateIndicator(MciViewIds.processing.calcHashIndicator); - break; - - case 'hash_finish' : - stepIndicatorFmt = fmtConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; - updateIndicator(MciViewIds.processing.calcHashIndicator, true); - break; - - case 'archive_list_start' : - stepIndicatorFmt = fmtConfig.extractArchiveListFormat || 'Extracting archive list'; - updateIndicator(MciViewIds.processing.archiveListIndicator); - break; - - case 'archive_list_finish' : - fmtObj.archivedFileCount = stepInfo.archiveEntries.length; - stepIndicatorFmt = fmtConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; - updateIndicator(MciViewIds.processing.archiveListIndicator, true); - break; - - case 'archive_list_failed' : - stepIndicatorFmt = fmtConfig.extractArchiveListFailedFormat || 'Archive list extraction failed'; - break; - - case 'desc_files_start' : - stepIndicatorFmt = fmtConfig.processingDescFilesFormat || 'Processing description files'; - updateIndicator(MciViewIds.processing.descFileIndicator); - break; - - case 'desc_files_finish' : - stepIndicatorFmt = fmtConfig.processingDescFilesFinishFormat || 'Finished processing description files'; - updateIndicator(MciViewIds.processing.descFileIndicator, true); - break; - - case 'finished' : - logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Finished'; - break; - } - - fmtObj.stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj); - - if(this.hasProcessingArt) { - this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj, { appendMultiLine : true } ); - - if(indicator.mci && indicator.text) { - this.setViewText('processing', indicator.mci, indicator.text); - } - - if(logStepFmt) { - this.setViewText('processing', MciViewIds.processing.logStep, stringFormat(logStepFmt, fmtObj), { appendMultiLine : true } ); - } - } else { - this.client.term.pipeWrite(fmtObj.stepIndicatorText); - } - } - - scanFiles(cb) { - const self = this; - - const results = { - newEntries : [], - dupes : [], - }; - - self.client.log.debug('Scanning upload(s)', { paths : this.recvFilePaths } ); - - let currentFileNum = 0; - - async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => { - // :TODO: virus scanning/etc. should occur around here - - currentFileNum += 1; - - self.scanStatus = { - indicatorPos : 0, - }; - - const scanOpts = { - areaTag : self.areaInfo.areaTag, - storageTag : self.areaInfo.storageTags[0], - }; - - function handleScanStep(stepInfo, nextScanStep) { - stepInfo.totalFileNum = self.recvFilePaths.length; - stepInfo.currentFileNum = currentFileNum; - - self.updateScanStepInfoViews(stepInfo); - return nextScanStep(null); - } - - self.client.log.debug('Scanning file', { filePath : filePath } ); - - scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => { - if(err) { - return nextFilePath(err); - } - - // new or dupe? - if(dupeEntries.length > 0) { - // 1:n dupes found - self.client.log.debug('Duplicate file(s) found', { dupeEntries : dupeEntries } ); - - results.dupes = results.dupes.concat(dupeEntries); - } else { - // new one - results.newEntries.push(fileEntry); - } - - return nextFilePath(null); - }); - }, err => { - return cb(err, results); - }); - } - - cleanupTempFiles() { - temptmp.cleanup( paths => { - Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); - }); - } - - moveAndPersistUploadsToDatabase(newEntries) { - - const areaStorageDir = getAreaDefaultStorageDirectory(this.areaInfo); - const self = this; - - async.eachSeries(newEntries, (newEntry, nextEntry) => { - const src = paths.join(self.tempRecvDirectory, newEntry.fileName); - const dst = paths.join(areaStorageDir, newEntry.fileName); - - moveFileWithCollisionHandling(src, dst, (err, finalPath) => { - if(err) { - self.client.log.error( - 'Failed moving physical upload file', { error : err.message, fileName : newEntry.fileName, source : src, dest : dst } - ); - - if(dst !== finalPath) { - // name changed; ajust before persist - newEntry.fileName = paths.basename(finalPath); - } - - return nextEntry(null); // still try next file - } - - self.client.log.debug('Moved upload to area', { path : finalPath } ); - - // persist to DB - newEntry.persist(err => { - if(err) { - self.client.log.error('Failed persisting upload to database', { path : finalPath, error : err.message } ); - } - - return nextEntry(null); // still try next file - }); - }); - }, () => { - // - // Finally, we can remove any temp files that we may have created - // - self.cleanupTempFiles(); - }); - } - - prepDetailsForUpload(scanResults, cb) { - async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { - newEntry.meta.upload_by_username = this.client.user.username; - newEntry.meta.upload_by_user_id = this.client.user.userId; - - this.displayFileDetailsPageForUploadEntry(newEntry, (err, newValues) => { - if(err) { - return nextEntry(err); - } - - if(!newEntry.descIsAnsi) { - newEntry.desc = _.trimEnd(newValues.shortDesc); - } - - if(newValues.estYear.length > 0) { - newEntry.meta.est_release_year = newValues.estYear; - } - - if(newValues.tags.length > 0) { - newEntry.setHashTags(newValues.tags); - } - - return nextEntry(err); - }); - }, err => { - delete this.fileDetailsCurrentEntrySubmitCallback; - return cb(err, scanResults); - }); - } - - displayDupesPage(dupes, cb) { - // - // If we have custom art to show, use it - else just dump basic info. - // Pause at the end in either case. - // - const self = this; - - async.waterfall( - [ - function prepArtAndViewController(callback) { - self.prepViewControllerWithArt( - 'dupes', - FormIds.dupes, - { clearScreen : true, trailingLF : false }, - err => { - if(err) { - self.client.term.pipeWrite('|00|07Duplicate upload(s) found:\n'); - return callback(null, null); - } - - const dupeListView = self.viewControllers.dupes.getView(MciViewIds.dupes.dupeList); - return callback(null, dupeListView); - } - ); - }, - function prepDupeObjects(dupeListView, callback) { - // update dupe objects with additional info that can be used for formatString() and the like - async.each(dupes, (dupe, nextDupe) => { - FileEntry.loadBasicEntry(dupe.fileId, dupe, err => { - if(err) { - return nextDupe(err); - } - - const areaInfo = getFileAreaByTag(dupe.areaTag); - if(areaInfo) { - dupe.areaName = areaInfo.name; - dupe.areaDesc = areaInfo.desc; - } - return nextDupe(null); - }); - }, err => { - return callback(err, dupeListView); - }); - }, - function populateDupeInfo(dupeListView, callback) { - const dupeInfoFormat = self.menuConfig.config.dupeInfoFormat || '{fileName} @ {areaName}'; - - if(dupeListView) { - dupeListView.setItems(dupes.map(dupe => stringFormat(dupeInfoFormat, dupe) ) ); - dupeListView.redraw(); - } else { - dupes.forEach(dupe => { - self.client.term.pipeWrite(`${stringFormat(dupeInfoFormat, dupe)}\n`); - }); - } - - return callback(null); - }, - function pause(callback) { - return self.pausePrompt( { row : self.client.term.termHeight }, callback); - } - ], - err => { - return cb(err); - } - ); - } - - processUploadedFiles() { - // - // For each file uploaded, we need to process & gather information - // - const self = this; - - async.waterfall( - [ - function prepNonBlind(callback) { - if(self.isBlindUpload()) { - return callback(null); - } - - // - // For non-blind uploads, batch is not supported, we expect a single file - // in |recvFilePaths|. If not, it's an error (we don't want to process the wrong thing) - // - if(self.recvFilePaths.length > 1) { - self.client.log.warn( { recvFilePaths : self.recvFilePaths }, 'Non-blind upload received 2:n files' ); - return callback(Errors.UnexpectedState(`Non-blind upload expected single file but got received ${self.recvFilePaths.length}`)); - } - - return callback(null); - }, - function scan(callback) { - return self.scanFiles(callback); - }, - function pause(scanResults, callback) { - if(self.hasProcessingArt) { - self.client.term.rawWrite(ansiGoto(self.client.term.termHeight, 1)); - } else { - self.client.term.write('\n'); - } - - self.pausePrompt( () => { - return callback(null, scanResults); - }); - }, - function displayDupes(scanResults, callback) { - if(0 === scanResults.dupes.length) { - return callback(null, scanResults); - } - - return self.displayDupesPage(scanResults.dupes, () => { - return callback(null, scanResults); - }); - }, - function prepDetails(scanResults, callback) { - return self.prepDetailsForUpload(scanResults, callback); - }, - function startMovingAndPersistingToDatabase(scanResults, callback) { - // - // *Start* the process of moving files from their current |tempRecvDirectory| - // locations -> their final area destinations. Don't make the user wait - // here as I/O can take quite a bit of time. Log any failures. - // - self.moveAndPersistUploadsToDatabase(scanResults.newEntries); - return callback(null, scanResults.newEntries); - }, - function sendEvent(uploadedEntries, callback) { - Events.emit( - Events.getSystemEvents().UserUpload, - { - user : self.client.user, - files : uploadedEntries, - } - ); - return callback(null); - } - ], - err => { - if(err) { - self.client.log.warn('File upload error encountered', { error : err.message } ); - self.cleanupTempFiles(); // normally called after moveAndPersistUploadsToDatabase() is completed. - } - - return self.prevMenu(); - } - ); - } - - displayOptionsPage(cb) { - const self = this; - - async.series( - [ - function prepArtAndViewController(callback) { - return self.prepViewControllerWithArt( - 'options', - FormIds.options, - { clearScreen : true, trailingLF : false }, - callback - ); - }, - function populateViews(callback) { - const areaSelectView = self.viewControllers.options.getView(MciViewIds.options.area); - areaSelectView.setItems( self.availAreas.map(areaInfo => areaInfo.name ) ); - - const uploadTypeView = self.viewControllers.options.getView(MciViewIds.options.uploadType); - const fileNameView = self.viewControllers.options.getView(MciViewIds.options.fileName); - - const blindFileNameText = self.menuConfig.config.blindFileNameText || '(blind - filename ignored)'; - - uploadTypeView.on('index update', idx => { - self.uploadType = (0 === idx) ? 'blind' : 'non-blind'; - - if(self.isBlindUpload()) { - fileNameView.setText(blindFileNameText); - fileNameView.acceptsFocus = false; - } else { - fileNameView.clearText(); - fileNameView.acceptsFocus = true; - } - }); - - // sanatize filename for display when leaving the view - self.viewControllers.options.on('leave', prevView => { - if(prevView.id === MciViewIds.options.fileName) { - fileNameView.setText(sanatizeFilename(fileNameView.getData())); - } - }); - - self.uploadType = 'blind'; - uploadTypeView.setFocusItemIndex(0); // default to blind - fileNameView.setText(blindFileNameText); - areaSelectView.redraw(); - - return callback(null); - } - ], - err => { - if(cb) { - return cb(err); - } - } - ); - } - - displayProcessingPage(cb) { - return this.prepViewControllerWithArt( - 'processing', - FormIds.processing, - { clearScreen : true, trailingLF : false }, - err => { - // note: this art is not required - this.hasProcessingArt = !err; - - return cb(null); - } - ); - } - - fileEntryHasDetectedDesc(fileEntry) { - return (fileEntry.desc && fileEntry.desc.length > 0); - } - - displayFileDetailsPageForUploadEntry(fileEntry, cb) { - const self = this; - - async.waterfall( - [ - function prepArtAndViewController(callback) { - return self.prepViewControllerWithArt( - 'fileDetails', - FormIds.fileDetails, - { clearScreen : true, trailingLF : false }, - err => { - return callback(err); - } - ); - }, - function populateViews(callback) { - const descView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.desc); - const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags); - const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear); - - self.updateCustomViewTextsWithFilter('fileDetails', MciViewIds.fileDetails.customRangeStart, fileEntry ); - - tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse - yearView.setText(fileEntry.meta.est_release_year || ''); - - if(isAnsi(fileEntry.desc)) { - fileEntry.descIsAnsi = true; - - return descView.setAnsi( - fileEntry.desc, - { - prepped : false, - forceLineTerm : true, - }, - () => { - return callback(null, descView, 'preview', MciViewIds.fileDetails.tags); - } - ); - } else { - const hasDesc = self.fileEntryHasDetectedDesc(fileEntry); - descView.setText( - hasDesc ? fileEntry.desc : getDescFromFileName(fileEntry.fileName), - { scrollMode : 'top' } // override scroll mode; we want to be @ top - ); - return callback(null, descView, 'edit', hasDesc ? MciViewIds.fileDetails.tags : MciViewIds.fileDetails.desc); - } - }, - function finalizeViews(descView, descViewMode, focusId, callback) { - descView.setPropertyValue('mode', descViewMode); - descView.acceptsFocus = 'preview' === descViewMode ? false : true; - self.viewControllers.fileDetails.switchFocus(focusId); - return callback(null); - } - ], - err => { - // - // we only call |cb| here if there is an error - // else, wait for the current from to be submit - then call - - // this way we'll move on to the next file entry when ready - // - if(err) { - return cb(err); - } - - self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue - } - ); - } + constructor(options) { + super(options); + + if(_.has(options, 'lastMenuResult.recvFilePaths')) { + this.recvFilePaths = options.lastMenuResult.recvFilePaths; + } + + this.availAreas = getSortedAvailableFileAreas(this.client, { writeAcs : true } ); + + this.menuMethods = { + optionsNavContinue : (formData, extraArgs, cb) => { + return this.performUpload(cb); + }, + + fileDetailsContinue : (formData, extraArgs, cb) => { + // see displayFileDetailsPageForUploadEntry() for this hackery: + cb(null); + return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any + }, + + // validation + validateNonBlindFileName : (fileName, cb) => { + fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. + if(0 === fileName.length) { + return cb(new Error('Invalid filename')); + } + + if(0 === fileName.length) { + return cb(new Error('Filename cannot be empty')); + } + + // At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused + if(/^[0-9].*$/.test(fileName)) { + return cb(new Error('Invalid filename')); + } + + return cb(null); + }, + viewValidationListener : (err, cb) => { + const errView = this.viewControllers.options.getView(MciViewIds.options.errMsg); + if(errView) { + if(err) { + errView.setText(err.message); + } else { + errView.clearText(); + } + } + + return cb(null); + } + }; + } + + getSaveState() { + // if no areas, we're falling back due to lack of access/areas avail to upload to + if(this.availAreas.length > 0) { + return { + uploadType : this.uploadType, + tempRecvDirectory : this.tempRecvDirectory, + areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ], + }; + } + } + + restoreSavedState(savedState) { + if(savedState.areaInfo) { + this.uploadType = savedState.uploadType; + this.areaInfo = savedState.areaInfo; + this.tempRecvDirectory = savedState.tempRecvDirectory; + } + } + + isBlindUpload() { return 'blind' === this.uploadType; } + isFileTransferComplete() { return !_.isUndefined(this.recvFilePaths); } + + initSequence() { + const self = this; + + if(0 === this.availAreas.length) { + // + return this.gotoMenu(this.menuConfig.config.noUploadAreasAvailMenu || 'fileBaseNoUploadAreasAvail'); + } + + async.series( + [ + function before(callback) { + return self.beforeArt(callback); + }, + function display(callback) { + if(self.isFileTransferComplete()) { + return self.displayProcessingPage(callback); + } else { + return self.displayOptionsPage(callback); + } + } + ], + () => { + return self.finishedLoading(); + } + ); + } + + finishedLoading() { + if(this.isFileTransferComplete()) { + return this.processUploadedFiles(); + } + } + + performUpload(cb) { + temptmp.mkdir( { prefix : 'enigul-' }, (err, tempRecvDirectory) => { + if(err) { + return cb(err); + } + + // need a terminator for various external protocols + this.tempRecvDirectory = pathWithTerminatingSeparator(tempRecvDirectory); + + const modOpts = { + extraArgs : { + recvDirectory : this.tempRecvDirectory, // we'll move files from here to their area container once processed/confirmed + direction : 'recv', + } + }; + + if(!this.isBlindUpload()) { + // data has been sanatized at this point + modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData(); + } + + // + // Move along to protocol selection -> file transfer + // Upon completion, we'll re-enter the module with some file paths handed to us + // + return this.gotoMenu( + this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', + modOpts, + cb + ); + }); + } + + continueNonBlindUpload(cb) { + return cb(null); + } + + updateScanStepInfoViews(stepInfo) { + // :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC + + const fmtObj = Object.assign( {}, stepInfo); + let stepIndicatorFmt = ''; + let logStepFmt; + + const fmtConfig = this.menuConfig.config; + + const indicatorStates = fmtConfig.indicatorStates || [ '|', '/', '-', '\\' ]; + const indicatorFinished = fmtConfig.indicatorFinished || '√'; + + const indicator = { }; + const self = this; + + function updateIndicator(mci, isFinished) { + indicator.mci = mci; + + if(isFinished) { + indicator.text = indicatorFinished; + } else { + self.scanStatus.indicatorPos += 1; + if(self.scanStatus.indicatorPos >= indicatorStates.length) { + self.scanStatus.indicatorPos = 0; + } + indicator.text = indicatorStates[self.scanStatus.indicatorPos]; + } + } + + switch(stepInfo.step) { + case 'start' : + logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Scanning {fileName}'; + break; + + case 'hash_update' : + stepIndicatorFmt = fmtConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%'; + updateIndicator(MciViewIds.processing.calcHashIndicator); + break; + + case 'hash_finish' : + stepIndicatorFmt = fmtConfig.calcHashCompleteFormat || 'Finished calculating hash/checksums'; + updateIndicator(MciViewIds.processing.calcHashIndicator, true); + break; + + case 'archive_list_start' : + stepIndicatorFmt = fmtConfig.extractArchiveListFormat || 'Extracting archive list'; + updateIndicator(MciViewIds.processing.archiveListIndicator); + break; + + case 'archive_list_finish' : + fmtObj.archivedFileCount = stepInfo.archiveEntries.length; + stepIndicatorFmt = fmtConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; + updateIndicator(MciViewIds.processing.archiveListIndicator, true); + break; + + case 'archive_list_failed' : + stepIndicatorFmt = fmtConfig.extractArchiveListFailedFormat || 'Archive list extraction failed'; + break; + + case 'desc_files_start' : + stepIndicatorFmt = fmtConfig.processingDescFilesFormat || 'Processing description files'; + updateIndicator(MciViewIds.processing.descFileIndicator); + break; + + case 'desc_files_finish' : + stepIndicatorFmt = fmtConfig.processingDescFilesFinishFormat || 'Finished processing description files'; + updateIndicator(MciViewIds.processing.descFileIndicator, true); + break; + + case 'finished' : + logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Finished'; + break; + } + + fmtObj.stepIndicatorText = stringFormat(stepIndicatorFmt, fmtObj); + + if(this.hasProcessingArt) { + this.updateCustomViewTextsWithFilter('processing', MciViewIds.processing.customRangeStart, fmtObj, { appendMultiLine : true } ); + + if(indicator.mci && indicator.text) { + this.setViewText('processing', indicator.mci, indicator.text); + } + + if(logStepFmt) { + this.setViewText('processing', MciViewIds.processing.logStep, stringFormat(logStepFmt, fmtObj), { appendMultiLine : true } ); + } + } else { + this.client.term.pipeWrite(fmtObj.stepIndicatorText); + } + } + + scanFiles(cb) { + const self = this; + + const results = { + newEntries : [], + dupes : [], + }; + + self.client.log.debug('Scanning upload(s)', { paths : this.recvFilePaths } ); + + let currentFileNum = 0; + + async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => { + // :TODO: virus scanning/etc. should occur around here + + currentFileNum += 1; + + self.scanStatus = { + indicatorPos : 0, + }; + + const scanOpts = { + areaTag : self.areaInfo.areaTag, + storageTag : self.areaInfo.storageTags[0], + }; + + function handleScanStep(stepInfo, nextScanStep) { + stepInfo.totalFileNum = self.recvFilePaths.length; + stepInfo.currentFileNum = currentFileNum; + + self.updateScanStepInfoViews(stepInfo); + return nextScanStep(null); + } + + self.client.log.debug('Scanning file', { filePath : filePath } ); + + scanFile(filePath, scanOpts, handleScanStep, (err, fileEntry, dupeEntries) => { + if(err) { + return nextFilePath(err); + } + + // new or dupe? + if(dupeEntries.length > 0) { + // 1:n dupes found + self.client.log.debug('Duplicate file(s) found', { dupeEntries : dupeEntries } ); + + results.dupes = results.dupes.concat(dupeEntries); + } else { + // new one + results.newEntries.push(fileEntry); + } + + return nextFilePath(null); + }); + }, err => { + return cb(err, results); + }); + } + + cleanupTempFiles() { + temptmp.cleanup( paths => { + Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); + }); + } + + moveAndPersistUploadsToDatabase(newEntries) { + + const areaStorageDir = getAreaDefaultStorageDirectory(this.areaInfo); + const self = this; + + async.eachSeries(newEntries, (newEntry, nextEntry) => { + const src = paths.join(self.tempRecvDirectory, newEntry.fileName); + const dst = paths.join(areaStorageDir, newEntry.fileName); + + moveFileWithCollisionHandling(src, dst, (err, finalPath) => { + if(err) { + self.client.log.error( + 'Failed moving physical upload file', { error : err.message, fileName : newEntry.fileName, source : src, dest : dst } + ); + + if(dst !== finalPath) { + // name changed; ajust before persist + newEntry.fileName = paths.basename(finalPath); + } + + return nextEntry(null); // still try next file + } + + self.client.log.debug('Moved upload to area', { path : finalPath } ); + + // persist to DB + newEntry.persist(err => { + if(err) { + self.client.log.error('Failed persisting upload to database', { path : finalPath, error : err.message } ); + } + + return nextEntry(null); // still try next file + }); + }); + }, () => { + // + // Finally, we can remove any temp files that we may have created + // + self.cleanupTempFiles(); + }); + } + + prepDetailsForUpload(scanResults, cb) { + async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { + newEntry.meta.upload_by_username = this.client.user.username; + newEntry.meta.upload_by_user_id = this.client.user.userId; + + this.displayFileDetailsPageForUploadEntry(newEntry, (err, newValues) => { + if(err) { + return nextEntry(err); + } + + if(!newEntry.descIsAnsi) { + newEntry.desc = _.trimEnd(newValues.shortDesc); + } + + if(newValues.estYear.length > 0) { + newEntry.meta.est_release_year = newValues.estYear; + } + + if(newValues.tags.length > 0) { + newEntry.setHashTags(newValues.tags); + } + + return nextEntry(err); + }); + }, err => { + delete this.fileDetailsCurrentEntrySubmitCallback; + return cb(err, scanResults); + }); + } + + displayDupesPage(dupes, cb) { + // + // If we have custom art to show, use it - else just dump basic info. + // Pause at the end in either case. + // + const self = this; + + async.waterfall( + [ + function prepArtAndViewController(callback) { + self.prepViewControllerWithArt( + 'dupes', + FormIds.dupes, + { clearScreen : true, trailingLF : false }, + err => { + if(err) { + self.client.term.pipeWrite('|00|07Duplicate upload(s) found:\n'); + return callback(null, null); + } + + const dupeListView = self.viewControllers.dupes.getView(MciViewIds.dupes.dupeList); + return callback(null, dupeListView); + } + ); + }, + function prepDupeObjects(dupeListView, callback) { + // update dupe objects with additional info that can be used for formatString() and the like + async.each(dupes, (dupe, nextDupe) => { + FileEntry.loadBasicEntry(dupe.fileId, dupe, err => { + if(err) { + return nextDupe(err); + } + + const areaInfo = getFileAreaByTag(dupe.areaTag); + if(areaInfo) { + dupe.areaName = areaInfo.name; + dupe.areaDesc = areaInfo.desc; + } + return nextDupe(null); + }); + }, err => { + return callback(err, dupeListView); + }); + }, + function populateDupeInfo(dupeListView, callback) { + const dupeInfoFormat = self.menuConfig.config.dupeInfoFormat || '{fileName} @ {areaName}'; + + if(dupeListView) { + dupeListView.setItems(dupes.map(dupe => stringFormat(dupeInfoFormat, dupe) ) ); + dupeListView.redraw(); + } else { + dupes.forEach(dupe => { + self.client.term.pipeWrite(`${stringFormat(dupeInfoFormat, dupe)}\n`); + }); + } + + return callback(null); + }, + function pause(callback) { + return self.pausePrompt( { row : self.client.term.termHeight }, callback); + } + ], + err => { + return cb(err); + } + ); + } + + processUploadedFiles() { + // + // For each file uploaded, we need to process & gather information + // + const self = this; + + async.waterfall( + [ + function prepNonBlind(callback) { + if(self.isBlindUpload()) { + return callback(null); + } + + // + // For non-blind uploads, batch is not supported, we expect a single file + // in |recvFilePaths|. If not, it's an error (we don't want to process the wrong thing) + // + if(self.recvFilePaths.length > 1) { + self.client.log.warn( { recvFilePaths : self.recvFilePaths }, 'Non-blind upload received 2:n files' ); + return callback(Errors.UnexpectedState(`Non-blind upload expected single file but got received ${self.recvFilePaths.length}`)); + } + + return callback(null); + }, + function scan(callback) { + return self.scanFiles(callback); + }, + function pause(scanResults, callback) { + if(self.hasProcessingArt) { + self.client.term.rawWrite(ansiGoto(self.client.term.termHeight, 1)); + } else { + self.client.term.write('\n'); + } + + self.pausePrompt( () => { + return callback(null, scanResults); + }); + }, + function displayDupes(scanResults, callback) { + if(0 === scanResults.dupes.length) { + return callback(null, scanResults); + } + + return self.displayDupesPage(scanResults.dupes, () => { + return callback(null, scanResults); + }); + }, + function prepDetails(scanResults, callback) { + return self.prepDetailsForUpload(scanResults, callback); + }, + function startMovingAndPersistingToDatabase(scanResults, callback) { + // + // *Start* the process of moving files from their current |tempRecvDirectory| + // locations -> their final area destinations. Don't make the user wait + // here as I/O can take quite a bit of time. Log any failures. + // + self.moveAndPersistUploadsToDatabase(scanResults.newEntries); + return callback(null, scanResults.newEntries); + }, + function sendEvent(uploadedEntries, callback) { + Events.emit( + Events.getSystemEvents().UserUpload, + { + user : self.client.user, + files : uploadedEntries, + } + ); + return callback(null); + } + ], + err => { + if(err) { + self.client.log.warn('File upload error encountered', { error : err.message } ); + self.cleanupTempFiles(); // normally called after moveAndPersistUploadsToDatabase() is completed. + } + + return self.prevMenu(); + } + ); + } + + displayOptionsPage(cb) { + const self = this; + + async.series( + [ + function prepArtAndViewController(callback) { + return self.prepViewControllerWithArt( + 'options', + FormIds.options, + { clearScreen : true, trailingLF : false }, + callback + ); + }, + function populateViews(callback) { + const areaSelectView = self.viewControllers.options.getView(MciViewIds.options.area); + areaSelectView.setItems( self.availAreas.map(areaInfo => areaInfo.name ) ); + + const uploadTypeView = self.viewControllers.options.getView(MciViewIds.options.uploadType); + const fileNameView = self.viewControllers.options.getView(MciViewIds.options.fileName); + + const blindFileNameText = self.menuConfig.config.blindFileNameText || '(blind - filename ignored)'; + + uploadTypeView.on('index update', idx => { + self.uploadType = (0 === idx) ? 'blind' : 'non-blind'; + + if(self.isBlindUpload()) { + fileNameView.setText(blindFileNameText); + fileNameView.acceptsFocus = false; + } else { + fileNameView.clearText(); + fileNameView.acceptsFocus = true; + } + }); + + // sanatize filename for display when leaving the view + self.viewControllers.options.on('leave', prevView => { + if(prevView.id === MciViewIds.options.fileName) { + fileNameView.setText(sanatizeFilename(fileNameView.getData())); + } + }); + + self.uploadType = 'blind'; + uploadTypeView.setFocusItemIndex(0); // default to blind + fileNameView.setText(blindFileNameText); + areaSelectView.redraw(); + + return callback(null); + } + ], + err => { + if(cb) { + return cb(err); + } + } + ); + } + + displayProcessingPage(cb) { + return this.prepViewControllerWithArt( + 'processing', + FormIds.processing, + { clearScreen : true, trailingLF : false }, + err => { + // note: this art is not required + this.hasProcessingArt = !err; + + return cb(null); + } + ); + } + + fileEntryHasDetectedDesc(fileEntry) { + return (fileEntry.desc && fileEntry.desc.length > 0); + } + + displayFileDetailsPageForUploadEntry(fileEntry, cb) { + const self = this; + + async.waterfall( + [ + function prepArtAndViewController(callback) { + return self.prepViewControllerWithArt( + 'fileDetails', + FormIds.fileDetails, + { clearScreen : true, trailingLF : false }, + err => { + return callback(err); + } + ); + }, + function populateViews(callback) { + const descView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.desc); + const tagsView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.tags); + const yearView = self.viewControllers.fileDetails.getView(MciViewIds.fileDetails.estYear); + + self.updateCustomViewTextsWithFilter('fileDetails', MciViewIds.fileDetails.customRangeStart, fileEntry ); + + tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse + yearView.setText(fileEntry.meta.est_release_year || ''); + + if(isAnsi(fileEntry.desc)) { + fileEntry.descIsAnsi = true; + + return descView.setAnsi( + fileEntry.desc, + { + prepped : false, + forceLineTerm : true, + }, + () => { + return callback(null, descView, 'preview', MciViewIds.fileDetails.tags); + } + ); + } else { + const hasDesc = self.fileEntryHasDetectedDesc(fileEntry); + descView.setText( + hasDesc ? fileEntry.desc : getDescFromFileName(fileEntry.fileName), + { scrollMode : 'top' } // override scroll mode; we want to be @ top + ); + return callback(null, descView, 'edit', hasDesc ? MciViewIds.fileDetails.tags : MciViewIds.fileDetails.desc); + } + }, + function finalizeViews(descView, descViewMode, focusId, callback) { + descView.setPropertyValue('mode', descViewMode); + descView.acceptsFocus = 'preview' === descViewMode ? false : true; + self.viewControllers.fileDetails.switchFocus(focusId); + return callback(null); + } + ], + err => { + // + // we only call |cb| here if there is an error + // else, wait for the current from to be submit - then call - + // this way we'll move on to the next file entry when ready + // + if(err) { + return cb(err); + } + + self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue + } + ); + } }; diff --git a/core/user.js b/core/user.js index 457604b3..72808f00 100644 --- a/core/user.js +++ b/core/user.js @@ -17,599 +17,599 @@ const moment = require('moment'); exports.isRootUserId = function(id) { return 1 === id; }; module.exports = class User { - constructor() { - this.userId = 0; - this.username = ''; - this.properties = {}; // name:value - this.groups = []; // group membership(s) - } + constructor() { + this.userId = 0; + this.username = ''; + this.properties = {}; // name:value + this.groups = []; // group membership(s) + } - // static property accessors - static get RootUserID() { - return 1; - } + // static property accessors + static get RootUserID() { + return 1; + } - static get PBKDF2() { - return { - iterations : 1000, - keyLen : 128, - saltLen : 32, - }; - } + static get PBKDF2() { + return { + iterations : 1000, + keyLen : 128, + saltLen : 32, + }; + } - static get StandardPropertyGroups() { - return { - password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], - }; - } + static get StandardPropertyGroups() { + return { + password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], + }; + } - static get AccountStatus() { - return { - disabled : 0, - inactive : 1, - active : 2, - }; - } + static get AccountStatus() { + return { + disabled : 0, + inactive : 1, + active : 2, + }; + } - isAuthenticated() { - return true === this.authenticated; - } + isAuthenticated() { + return true === this.authenticated; + } - isValid() { - if(this.userId <= 0 || this.username.length < Config().users.usernameMin) { - return false; - } + isValid() { + if(this.userId <= 0 || this.username.length < Config().users.usernameMin) { + return false; + } - return this.hasValidPassword(); - } + return this.hasValidPassword(); + } - hasValidPassword() { - if(!this.properties || !this.properties.pw_pbkdf2_salt || !this.properties.pw_pbkdf2_dk) { - return false; - } + hasValidPassword() { + if(!this.properties || !this.properties.pw_pbkdf2_salt || !this.properties.pw_pbkdf2_dk) { + return false; + } - return ((this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2) && + return ((this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2) && (this.properties.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2)); - } + } - isRoot() { - return User.isRootUserId(this.userId); - } + isRoot() { + return User.isRootUserId(this.userId); + } - isSysOp() { // alias to isRoot() - return this.isRoot(); - } + isSysOp() { // alias to isRoot() + return this.isRoot(); + } - isGroupMember(groupNames) { - if(_.isString(groupNames)) { - groupNames = [ groupNames ]; - } + isGroupMember(groupNames) { + if(_.isString(groupNames)) { + groupNames = [ groupNames ]; + } - const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); - return isMember; - } + const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); + return isMember; + } - getLegacySecurityLevel() { - if(this.isRoot() || this.isGroupMember('sysops')) { - return 100; - } + getLegacySecurityLevel() { + if(this.isRoot() || this.isGroupMember('sysops')) { + return 100; + } - if(this.isGroupMember('users')) { - return 30; - } + if(this.isGroupMember('users')) { + return 30; + } - return 10; // :TODO: Is this what we want? - } + return 10; // :TODO: Is this what we want? + } - authenticate(username, password, cb) { - const self = this; - const cachedInfo = {}; + authenticate(username, password, cb) { + const self = this; + const cachedInfo = {}; - async.waterfall( - [ - function fetchUserId(callback) { - // get user ID - User.getUserIdAndName(username, (err, uid, un) => { - cachedInfo.userId = uid; - cachedInfo.username = un; + async.waterfall( + [ + function fetchUserId(callback) { + // get user ID + User.getUserIdAndName(username, (err, uid, un) => { + cachedInfo.userId = uid; + cachedInfo.username = un; - return callback(err); - }); - }, - function getRequiredAuthProperties(callback) { - // fetch properties required for authentication - User.loadProperties(cachedInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { - return callback(err, props); - }); - }, - function getDkWithSalt(props, callback) { - // get DK from stored salt and password provided - User.generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, (err, dk) => { - return callback(err, dk, props.pw_pbkdf2_dk); - }); - }, - function validateAuth(passDk, propsDk, callback) { - // - // Use constant time comparison here for security feel-goods - // - const passDkBuf = Buffer.from(passDk, 'hex'); - const propsDkBuf = Buffer.from(propsDk, 'hex'); + return callback(err); + }); + }, + function getRequiredAuthProperties(callback) { + // fetch properties required for authentication + User.loadProperties(cachedInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { + return callback(err, props); + }); + }, + function getDkWithSalt(props, callback) { + // get DK from stored salt and password provided + User.generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, (err, dk) => { + return callback(err, dk, props.pw_pbkdf2_dk); + }); + }, + function validateAuth(passDk, propsDk, callback) { + // + // Use constant time comparison here for security feel-goods + // + const passDkBuf = Buffer.from(passDk, 'hex'); + const propsDkBuf = Buffer.from(propsDk, 'hex'); - if(passDkBuf.length !== propsDkBuf.length) { - return callback(Errors.AccessDenied('Invalid password')); - } + if(passDkBuf.length !== propsDkBuf.length) { + return callback(Errors.AccessDenied('Invalid password')); + } - let c = 0; - for(let i = 0; i < passDkBuf.length; i++) { - c |= passDkBuf[i] ^ propsDkBuf[i]; - } + let c = 0; + for(let i = 0; i < passDkBuf.length; i++) { + c |= passDkBuf[i] ^ propsDkBuf[i]; + } - return callback(0 === c ? null : Errors.AccessDenied('Invalid password')); - }, - function initProps(callback) { - User.loadProperties(cachedInfo.userId, (err, allProps) => { - if(!err) { - cachedInfo.properties = allProps; - } + return callback(0 === c ? null : Errors.AccessDenied('Invalid password')); + }, + function initProps(callback) { + User.loadProperties(cachedInfo.userId, (err, allProps) => { + if(!err) { + cachedInfo.properties = allProps; + } - return callback(err); - }); - }, - function initGroups(callback) { - userGroup.getGroupsForUser(cachedInfo.userId, (err, groups) => { - if(!err) { - cachedInfo.groups = groups; - } + return callback(err); + }); + }, + function initGroups(callback) { + userGroup.getGroupsForUser(cachedInfo.userId, (err, groups) => { + if(!err) { + cachedInfo.groups = groups; + } - return callback(err); - }); - } - ], - err => { - if(!err) { - self.userId = cachedInfo.userId; - self.username = cachedInfo.username; - self.properties = cachedInfo.properties; - self.groups = cachedInfo.groups; - self.authenticated = true; - } + return callback(err); + }); + } + ], + err => { + if(!err) { + self.userId = cachedInfo.userId; + self.username = cachedInfo.username; + self.properties = cachedInfo.properties; + self.groups = cachedInfo.groups; + self.authenticated = true; + } - return cb(err); - } - ); - } + return cb(err); + } + ); + } - create(password, cb) { - assert(0 === this.userId); - const config = Config(); + create(password, cb) { + assert(0 === this.userId); + const config = Config(); - if(this.username.length < config.users.usernameMin || this.username.length > config.users.usernameMax) { - return cb(Errors.Invalid('Invalid username length')); - } + if(this.username.length < config.users.usernameMin || this.username.length > config.users.usernameMax) { + return cb(Errors.Invalid('Invalid username length')); + } - const self = this; + const self = this; - // :TODO: set various defaults, e.g. default activation status, etc. - self.properties.account_status = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; + // :TODO: set various defaults, e.g. default activation status, etc. + self.properties.account_status = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; - async.waterfall( - [ - function beginTransaction(callback) { - return userDb.beginTransaction(callback); - }, - function createUserRec(trans, callback) { - trans.run( - `INSERT INTO user (user_name) + async.waterfall( + [ + function beginTransaction(callback) { + return userDb.beginTransaction(callback); + }, + function createUserRec(trans, callback) { + trans.run( + `INSERT INTO user (user_name) VALUES (?);`, - [ self.username ], - function inserted(err) { // use classic function for |this| - if(err) { - return callback(err); - } + [ self.username ], + function inserted(err) { // use classic function for |this| + if(err) { + return callback(err); + } - self.userId = this.lastID; + self.userId = this.lastID; - // Do not require activation for userId 1 (root/admin) - if(User.RootUserID === self.userId) { - self.properties.account_status = User.AccountStatus.active; - } + // Do not require activation for userId 1 (root/admin) + if(User.RootUserID === self.userId) { + self.properties.account_status = User.AccountStatus.active; + } - return callback(null, trans); - } - ); - }, - function genAuthCredentials(trans, callback) { - User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { - if(err) { - return callback(err); - } + return callback(null, trans); + } + ); + }, + function genAuthCredentials(trans, callback) { + User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { + if(err) { + return callback(err); + } - self.properties.pw_pbkdf2_salt = info.salt; - self.properties.pw_pbkdf2_dk = info.dk; - return callback(null, trans); - }); - }, - function setInitialGroupMembership(trans, callback) { - self.groups = config.users.defaultGroups; + self.properties.pw_pbkdf2_salt = info.salt; + self.properties.pw_pbkdf2_dk = info.dk; + return callback(null, trans); + }); + }, + function setInitialGroupMembership(trans, callback) { + self.groups = config.users.defaultGroups; - if(User.RootUserID === self.userId) { // root/SysOp? - self.groups.push('sysops'); - } + if(User.RootUserID === self.userId) { // root/SysOp? + self.groups.push('sysops'); + } - return callback(null, trans); - }, - function saveAll(trans, callback) { - self.persistWithTransaction(trans, err => { - return callback(err, trans); - }); - }, - function sendEvent(trans, callback) { - Events.emit(Events.getSystemEvents().NewUser, { user : self }); - return callback(null, trans); - } - ], - (err, trans) => { - if(trans) { - trans[err ? 'rollback' : 'commit'](transErr => { - return cb(err ? err : transErr); - }); - } else { - return cb(err); - } - } - ); - } + return callback(null, trans); + }, + function saveAll(trans, callback) { + self.persistWithTransaction(trans, err => { + return callback(err, trans); + }); + }, + function sendEvent(trans, callback) { + Events.emit(Events.getSystemEvents().NewUser, { user : self }); + return callback(null, trans); + } + ], + (err, trans) => { + if(trans) { + trans[err ? 'rollback' : 'commit'](transErr => { + return cb(err ? err : transErr); + }); + } else { + return cb(err); + } + } + ); + } - persistWithTransaction(trans, cb) { - assert(this.userId > 0); + persistWithTransaction(trans, cb) { + assert(this.userId > 0); - const self = this; + const self = this; - async.series( - [ - function saveProps(callback) { - self.persistProperties(self.properties, trans, err => { - return callback(err); - }); - }, - function saveGroups(callback) { - userGroup.addUserToGroups(self.userId, self.groups, trans, err => { - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); - } + async.series( + [ + function saveProps(callback) { + self.persistProperties(self.properties, trans, err => { + return callback(err); + }); + }, + function saveGroups(callback) { + userGroup.addUserToGroups(self.userId, self.groups, trans, err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } - persistProperty(propName, propValue, cb) { - // update live props - this.properties[propName] = propValue; + persistProperty(propName, propValue, cb) { + // update live props + this.properties[propName] = propValue; - userDb.run( - `REPLACE INTO user_property (user_id, prop_name, prop_value) + userDb.run( + `REPLACE INTO user_property (user_id, prop_name, prop_value) VALUES (?, ?, ?);`, - [ this.userId, propName, propValue ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + [ this.userId, propName, propValue ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - removeProperty(propName, cb) { - // update live - delete this.properties[propName]; + removeProperty(propName, cb) { + // update live + delete this.properties[propName]; - userDb.run( - `DELETE FROM user_property + userDb.run( + `DELETE FROM user_property WHERE user_id = ? AND prop_name = ?;`, - [ this.userId, propName ], - err => { - if(cb) { - return cb(err); - } - } - ); - } + [ this.userId, propName ], + err => { + if(cb) { + return cb(err); + } + } + ); + } - persistProperties(properties, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = userDb; - } + persistProperties(properties, transOrDb, cb) { + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = userDb; + } - const self = this; + const self = this; - // update live props - _.merge(this.properties, properties); + // update live props + _.merge(this.properties, properties); - const stmt = transOrDb.prepare( - `REPLACE INTO user_property (user_id, prop_name, prop_value) + const stmt = transOrDb.prepare( + `REPLACE INTO user_property (user_id, prop_name, prop_value) VALUES (?, ?, ?);` - ); + ); - async.each(Object.keys(properties), (propName, nextProp) => { - stmt.run(self.userId, propName, properties[propName], err => { - return nextProp(err); - }); - }, - err => { - if(err) { - return cb(err); - } + async.each(Object.keys(properties), (propName, nextProp) => { + stmt.run(self.userId, propName, properties[propName], err => { + return nextProp(err); + }); + }, + err => { + if(err) { + return cb(err); + } - stmt.finalize( () => { - return cb(null); - }); - }); - } + stmt.finalize( () => { + return cb(null); + }); + }); + } - setNewAuthCredentials(password, cb) { - User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { - if(err) { - return cb(err); - } + setNewAuthCredentials(password, cb) { + User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { + if(err) { + return cb(err); + } - const newProperties = { - pw_pbkdf2_salt : info.salt, - pw_pbkdf2_dk : info.dk, - }; + const newProperties = { + pw_pbkdf2_salt : info.salt, + pw_pbkdf2_dk : info.dk, + }; - this.persistProperties(newProperties, err => { - return cb(err); - }); - }); - } + this.persistProperties(newProperties, err => { + return cb(err); + }); + }); + } - getAge() { - if(_.has(this.properties, 'birthdate')) { - return moment().diff(this.properties.birthdate, 'years'); - } - } + getAge() { + if(_.has(this.properties, 'birthdate')) { + return moment().diff(this.properties.birthdate, 'years'); + } + } - static getUser(userId, cb) { - async.waterfall( - [ - function fetchUserId(callback) { - User.getUserName(userId, (err, userName) => { - return callback(null, userName); - }); - }, - function initProps(userName, callback) { - User.loadProperties(userId, (err, properties) => { - return callback(err, userName, properties); - }); - }, - function initGroups(userName, properties, callback) { - userGroup.getGroupsForUser(userId, (err, groups) => { - return callback(null, userName, properties, groups); - }); - } - ], - (err, userName, properties, groups) => { - const user = new User(); - user.userId = userId; - user.username = userName; - user.properties = properties; - user.groups = groups; - user.authenticated = false; // this is NOT an authenticated user! + static getUser(userId, cb) { + async.waterfall( + [ + function fetchUserId(callback) { + User.getUserName(userId, (err, userName) => { + return callback(null, userName); + }); + }, + function initProps(userName, callback) { + User.loadProperties(userId, (err, properties) => { + return callback(err, userName, properties); + }); + }, + function initGroups(userName, properties, callback) { + userGroup.getGroupsForUser(userId, (err, groups) => { + return callback(null, userName, properties, groups); + }); + } + ], + (err, userName, properties, groups) => { + const user = new User(); + user.userId = userId; + user.username = userName; + user.properties = properties; + user.groups = groups; + user.authenticated = false; // this is NOT an authenticated user! - return cb(err, user); - } - ); - } + return cb(err, user); + } + ); + } - static isRootUserId(userId) { - return (User.RootUserID === userId); - } + static isRootUserId(userId) { + return (User.RootUserID === userId); + } - static getUserIdAndName(username, cb) { - userDb.get( - `SELECT id, user_name + static getUserIdAndName(username, cb) { + userDb.get( + `SELECT id, user_name FROM user WHERE user_name LIKE ?;`, - [ username ], - (err, row) => { - if(err) { - return cb(err); - } + [ username ], + (err, row) => { + if(err) { + return cb(err); + } - if(row) { - return cb(null, row.id, row.user_name); - } + if(row) { + return cb(null, row.id, row.user_name); + } - return cb(Errors.DoesNotExist('No matching username')); - } - ); - } + return cb(Errors.DoesNotExist('No matching username')); + } + ); + } - static getUserIdAndNameByRealName(realName, cb) { - userDb.get( - `SELECT id, user_name + static getUserIdAndNameByRealName(realName, cb) { + userDb.get( + `SELECT id, user_name FROM user WHERE id = ( SELECT user_id FROM user_property WHERE prop_name='real_name' AND prop_value LIKE ? );`, - [ realName ], - (err, row) => { - if(err) { - return cb(err); - } + [ realName ], + (err, row) => { + if(err) { + return cb(err); + } - if(row) { - return cb(null, row.id, row.user_name); - } + if(row) { + return cb(null, row.id, row.user_name); + } - return cb(Errors.DoesNotExist('No matching real name')); - } - ); - } + return cb(Errors.DoesNotExist('No matching real name')); + } + ); + } - static getUserIdAndNameByLookup(lookup, cb) { - User.getUserIdAndName(lookup, (err, userId, userName) => { - if(err) { - User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => { - return cb(err, userId, userName); - }); - } else { - return cb(null, userId, userName); - } - }); - } + static getUserIdAndNameByLookup(lookup, cb) { + User.getUserIdAndName(lookup, (err, userId, userName) => { + if(err) { + User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => { + return cb(err, userId, userName); + }); + } else { + return cb(null, userId, userName); + } + }); + } - static getUserName(userId, cb) { - userDb.get( - `SELECT user_name + static getUserName(userId, cb) { + userDb.get( + `SELECT user_name FROM user WHERE id = ?;`, - [ userId ], - (err, row) => { - if(err) { - return cb(err); - } + [ userId ], + (err, row) => { + if(err) { + return cb(err); + } - if(row) { - return cb(null, row.user_name); - } + if(row) { + return cb(null, row.user_name); + } - return cb(Errors.DoesNotExist('No matching user ID')); - } - ); - } + return cb(Errors.DoesNotExist('No matching user ID')); + } + ); + } - static loadProperties(userId, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + static loadProperties(userId, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } - let sql = + let sql = `SELECT prop_name, prop_value FROM user_property WHERE user_id = ?`; - if(options.names) { - sql += ` AND prop_name IN("${options.names.join('","')}");`; - } else { - sql += ';'; - } + if(options.names) { + sql += ` AND prop_name IN("${options.names.join('","')}");`; + } else { + sql += ';'; + } - let properties = {}; - userDb.each(sql, [ userId ], (err, row) => { - if(err) { - return cb(err); - } - properties[row.prop_name] = row.prop_value; - }, (err) => { - return cb(err, err ? null : properties); - }); - } + let properties = {}; + userDb.each(sql, [ userId ], (err, row) => { + if(err) { + return cb(err); + } + properties[row.prop_name] = row.prop_value; + }, (err) => { + return cb(err, err ? null : properties); + }); + } - // :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. - static getUserIdsWithProperty(propName, propValue, cb) { - let userIds = []; + // :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. + static getUserIdsWithProperty(propName, propValue, cb) { + let userIds = []; - userDb.each( - `SELECT user_id + userDb.each( + `SELECT user_id FROM user_property WHERE prop_name = ? AND prop_value = ?;`, - [ propName, propValue ], - (err, row) => { - if(row) { - userIds.push(row.user_id); - } - }, - () => { - return cb(null, userIds); - } - ); - } + [ propName, propValue ], + (err, row) => { + if(row) { + userIds.push(row.user_id); + } + }, + () => { + return cb(null, userIds); + } + ); + } - static getUserList(options, cb) { - let userList = []; - let orderClause = 'ORDER BY ' + (options.order || 'user_name'); + static getUserList(options, cb) { + let userList = []; + let orderClause = 'ORDER BY ' + (options.order || 'user_name'); - userDb.each( - `SELECT id, user_name + userDb.each( + `SELECT id, user_name FROM user ${orderClause};`, - (err, row) => { - if(row) { - userList.push({ - userId : row.id, - userName : row.user_name, - }); - } - }, - () => { - options.properties = options.properties || []; - async.map(userList, (user, nextUser) => { - userDb.each( - `SELECT prop_name, prop_value + (err, row) => { + if(row) { + userList.push({ + userId : row.id, + userName : row.user_name, + }); + } + }, + () => { + options.properties = options.properties || []; + async.map(userList, (user, nextUser) => { + userDb.each( + `SELECT prop_name, prop_value FROM user_property WHERE user_id = ? AND prop_name IN ("${options.properties.join('","')}");`, - [ user.userId ], - (err, row) => { - if(row) { - user[row.prop_name] = row.prop_value; - } - }, - err => { - return nextUser(err, user); - } - ); - }, - (err, transformed) => { - return cb(err, transformed); - }); - } - ); - } + [ user.userId ], + (err, row) => { + if(row) { + user[row.prop_name] = row.prop_value; + } + }, + err => { + return nextUser(err, user); + } + ); + }, + (err, transformed) => { + return cb(err, transformed); + }); + } + ); + } - static generatePasswordDerivedKeyAndSalt(password, cb) { - async.waterfall( - [ - function getSalt(callback) { - User.generatePasswordDerivedKeySalt( (err, salt) => { - return callback(err, salt); - }); - }, - function getDk(salt, callback) { - User.generatePasswordDerivedKey(password, salt, (err, dk) => { - return callback(err, salt, dk); - }); - } - ], - (err, salt, dk) => { - return cb(err, { salt : salt, dk : dk } ); - } - ); - } + static generatePasswordDerivedKeyAndSalt(password, cb) { + async.waterfall( + [ + function getSalt(callback) { + User.generatePasswordDerivedKeySalt( (err, salt) => { + return callback(err, salt); + }); + }, + function getDk(salt, callback) { + User.generatePasswordDerivedKey(password, salt, (err, dk) => { + return callback(err, salt, dk); + }); + } + ], + (err, salt, dk) => { + return cb(err, { salt : salt, dk : dk } ); + } + ); + } - static generatePasswordDerivedKeySalt(cb) { - crypto.randomBytes(User.PBKDF2.saltLen, (err, salt) => { - if(err) { - return cb(err); - } - return cb(null, salt.toString('hex')); - }); - } + static generatePasswordDerivedKeySalt(cb) { + crypto.randomBytes(User.PBKDF2.saltLen, (err, salt) => { + if(err) { + return cb(err); + } + return cb(null, salt.toString('hex')); + }); + } - static generatePasswordDerivedKey(password, salt, cb) { - password = Buffer.from(password).toString('hex'); + static generatePasswordDerivedKey(password, salt, cb) { + password = Buffer.from(password).toString('hex'); - crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', (err, dk) => { - if(err) { - return cb(err); - } + crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', (err, dk) => { + if(err) { + return cb(err); + } - return cb(null, dk.toString('hex')); - }); - } + return cb(null, dk.toString('hex')); + }); + } }; diff --git a/core/user_config.js b/core/user_config.js index 6a51a36b..b5e124b6 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -12,211 +12,211 @@ const _ = require('lodash'); const moment = require('moment'); exports.moduleInfo = { - name : 'User Configuration', - desc : 'Module for user configuration', - author : 'NuSkooler', + name : 'User Configuration', + desc : 'Module for user configuration', + author : 'NuSkooler', }; const MciCodeIds = { - RealName : 1, - BirthDate : 2, - Sex : 3, - Loc : 4, - Affils : 5, - Email : 6, - Web : 7, - TermHeight : 8, - Theme : 9, - Password : 10, - PassConfirm : 11, - ThemeInfo : 20, - ErrorMsg : 21, + RealName : 1, + BirthDate : 2, + Sex : 3, + Loc : 4, + Affils : 5, + Email : 6, + Web : 7, + TermHeight : 8, + Theme : 9, + Password : 10, + PassConfirm : 11, + ThemeInfo : 20, + ErrorMsg : 21, - SaveCancel : 25, + SaveCancel : 25, }; exports.getModule = class UserConfigModule extends MenuModule { - constructor(options) { - super(options); + constructor(options) { + super(options); - const self = this; + const self = this; - this.menuMethods = { - // - // Validation support - // - validateEmailAvail : function(data, cb) { - // - // If nothing changed, we know it's OK - // - if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { - return cb(null); - } + this.menuMethods = { + // + // Validation support + // + validateEmailAvail : function(data, cb) { + // + // If nothing changed, we know it's OK + // + if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { + return cb(null); + } - // Otherwise we can use the standard system method - return sysValidate.validateEmailAvail(data, cb); - }, + // Otherwise we can use the standard system method + return sysValidate.validateEmailAvail(data, cb); + }, - validatePassword : function(data, cb) { - // - // Blank is OK - this means we won't be changing it - // - if(!data || 0 === data.length) { - return cb(null); - } + validatePassword : function(data, cb) { + // + // Blank is OK - this means we won't be changing it + // + if(!data || 0 === data.length) { + return cb(null); + } - // Otherwise we can use the standard system method - return sysValidate.validatePasswordSpec(data, cb); - }, + // Otherwise we can use the standard system method + return sysValidate.validatePasswordSpec(data, cb); + }, - validatePassConfirmMatch : function(data, cb) { - var passwordView = self.getView(MciCodeIds.Password); - cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); - }, + validatePassConfirmMatch : function(data, cb) { + var passwordView = self.getView(MciCodeIds.Password); + cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); + }, - viewValidationListener : function(err, cb) { - var errMsgView = self.getView(MciCodeIds.ErrorMsg); - var newFocusId; - if(errMsgView) { - if(err) { - errMsgView.setText(err.message); + viewValidationListener : function(err, cb) { + var errMsgView = self.getView(MciCodeIds.ErrorMsg); + var newFocusId; + if(errMsgView) { + if(err) { + errMsgView.setText(err.message); - if(err.view.getId() === MciCodeIds.PassConfirm) { - newFocusId = MciCodeIds.Password; - var passwordView = self.getView(MciCodeIds.Password); - passwordView.clearText(); - err.view.clearText(); - } - } else { - errMsgView.clearText(); - } - } - cb(newFocusId); - }, + if(err.view.getId() === MciCodeIds.PassConfirm) { + newFocusId = MciCodeIds.Password; + var passwordView = self.getView(MciCodeIds.Password); + passwordView.clearText(); + err.view.clearText(); + } + } else { + errMsgView.clearText(); + } + } + cb(newFocusId); + }, - // - // Handlers - // - saveChanges : function(formData, extraArgs, cb) { - assert(formData.value.password === formData.value.passwordConfirm); + // + // Handlers + // + saveChanges : function(formData, extraArgs, cb) { + assert(formData.value.password === formData.value.passwordConfirm); - const newProperties = { - real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), - sex : formData.value.sex, - location : formData.value.location, - affiliation : formData.value.affils, - email_address : formData.value.email, - web_address : formData.value.web, - term_height : formData.value.termHeight.toString(), - theme_id : self.availThemeInfo[formData.value.theme].themeId, - }; + const newProperties = { + real_name : formData.value.realName, + birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), + sex : formData.value.sex, + location : formData.value.location, + affiliation : formData.value.affils, + email_address : formData.value.email, + web_address : formData.value.web, + term_height : formData.value.termHeight.toString(), + theme_id : self.availThemeInfo[formData.value.theme].themeId, + }; - // runtime set theme - theme.setClientTheme(self.client, newProperties.theme_id); + // runtime set theme + theme.setClientTheme(self.client, newProperties.theme_id); - // persist all changes - self.client.user.persistProperties(newProperties, err => { - if(err) { - self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); - // :TODO: warn end user! - return self.prevMenu(cb); - } - // - // New password if it's not empty - // - self.client.log.info('User updated properties'); + // persist all changes + self.client.user.persistProperties(newProperties, err => { + if(err) { + self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); + // :TODO: warn end user! + return self.prevMenu(cb); + } + // + // New password if it's not empty + // + self.client.log.info('User updated properties'); - if(formData.value.password.length > 0) { - self.client.user.setNewAuthCredentials(formData.value.password, err => { - if(err) { - self.client.log.error( { err : err }, 'Failed storing new authentication credentials'); - } else { - self.client.log.info('User changed authentication credentials'); - } - return self.prevMenu(cb); - }); - } else { - return self.prevMenu(cb); - } - }); - }, - }; - } + if(formData.value.password.length > 0) { + self.client.user.setNewAuthCredentials(formData.value.password, err => { + if(err) { + self.client.log.error( { err : err }, 'Failed storing new authentication credentials'); + } else { + self.client.log.info('User changed authentication credentials'); + } + return self.prevMenu(cb); + }); + } else { + return self.prevMenu(cb); + } + }); + }, + }; + } - getView(viewId) { - return this.viewControllers.menu.getView(viewId); - } + getView(viewId) { + return this.viewControllers.menu.getView(viewId); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); - let currentThemeIdIndex = 0; + const self = this; + const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); + let currentThemeIdIndex = 0; - async.series( - [ - function loadFromConfig(callback) { - vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); - }, - function prepareAvailableThemes(callback) { - self.availThemeInfo = _.sortBy([...theme.getAvailableThemes()].map(entry => { - const theme = entry[1]; - return { - themeId : theme.info.themeId, - name : theme.info.name, - author : theme.info.author, - desc : _.isString(theme.info.desc) ? theme.info.desc : '', - group : _.isString(theme.info.group) ? theme.info.group : '', - }; - }), 'name'); + async.series( + [ + function loadFromConfig(callback) { + vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); + }, + function prepareAvailableThemes(callback) { + self.availThemeInfo = _.sortBy([...theme.getAvailableThemes()].map(entry => { + const theme = entry[1]; + return { + themeId : theme.info.themeId, + name : theme.info.name, + author : theme.info.author, + desc : _.isString(theme.info.desc) ? theme.info.desc : '', + group : _.isString(theme.info.group) ? theme.info.group : '', + }; + }), 'name'); - currentThemeIdIndex = Math.max(0, _.findIndex(self.availThemeInfo, function cmp(ti) { - return ti.themeId === self.client.user.properties.theme_id; - })); + currentThemeIdIndex = Math.max(0, _.findIndex(self.availThemeInfo, function cmp(ti) { + return ti.themeId === self.client.user.properties.theme_id; + })); - callback(null); - }, - function populateViews(callback) { - var user = self.client.user; + callback(null); + }, + function populateViews(callback) { + var user = self.client.user; - self.setViewText('menu', MciCodeIds.RealName, user.properties.real_name); - self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD')); - self.setViewText('menu', MciCodeIds.Sex, user.properties.sex); - self.setViewText('menu', MciCodeIds.Loc, user.properties.location); - self.setViewText('menu', MciCodeIds.Affils, user.properties.affiliation); - self.setViewText('menu', MciCodeIds.Email, user.properties.email_address); - self.setViewText('menu', MciCodeIds.Web, user.properties.web_address); - self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString()); + self.setViewText('menu', MciCodeIds.RealName, user.properties.real_name); + self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD')); + self.setViewText('menu', MciCodeIds.Sex, user.properties.sex); + self.setViewText('menu', MciCodeIds.Loc, user.properties.location); + self.setViewText('menu', MciCodeIds.Affils, user.properties.affiliation); + self.setViewText('menu', MciCodeIds.Email, user.properties.email_address); + self.setViewText('menu', MciCodeIds.Web, user.properties.web_address); + self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString()); - var themeView = self.getView(MciCodeIds.Theme); - if(themeView) { - themeView.setItems(_.map(self.availThemeInfo, 'name')); - themeView.setFocusItemIndex(currentThemeIdIndex); - } + var themeView = self.getView(MciCodeIds.Theme); + if(themeView) { + themeView.setItems(_.map(self.availThemeInfo, 'name')); + themeView.setFocusItemIndex(currentThemeIdIndex); + } - var realNameView = self.getView(MciCodeIds.RealName); - if(realNameView) { - realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! - } + var realNameView = self.getView(MciCodeIds.RealName); + if(realNameView) { + realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! + } - callback(null); - } - ], - function complete(err) { - if(err) { - self.client.log.warn( { error : err.toString() }, 'User configuration failed to init'); - self.prevMenu(); - } else { - cb(null); - } - } - ); - }); - } + callback(null); + } + ], + function complete(err) { + if(err) { + self.client.log.warn( { error : err.toString() }, 'User configuration failed to init'); + self.prevMenu(); + } else { + cb(null); + } + } + ); + }); + } }; diff --git a/core/user_group.js b/core/user_group.js index db444296..a350e5b2 100644 --- a/core/user_group.js +++ b/core/user_group.js @@ -12,57 +12,57 @@ exports.addUserToGroups = addUserToGroups; exports.removeUserFromGroup = removeUserFromGroup; function getGroupsForUser(userId, cb) { - const sql = + const sql = `SELECT group_name FROM user_group_member WHERE user_id=?;`; - const groups = []; + const groups = []; - userDb.each(sql, [ userId ], (err, row) => { - if(err) { - return cb(err); - } + userDb.each(sql, [ userId ], (err, row) => { + if(err) { + return cb(err); + } - groups.push(row.group_name); - }, - () => { - return cb(null, groups); - }); + groups.push(row.group_name); + }, + () => { + return cb(null, groups); + }); } function addUserToGroup(userId, groupName, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { - cb = transOrDb; - transOrDb = userDb; - } + if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + cb = transOrDb; + transOrDb = userDb; + } - transOrDb.run( - `REPLACE INTO user_group_member (group_name, user_id) + transOrDb.run( + `REPLACE INTO user_group_member (group_name, user_id) VALUES(?, ?);`, - [ groupName, userId ], - err => { - return cb(err); - } - ); + [ groupName, userId ], + err => { + return cb(err); + } + ); } function addUserToGroups(userId, groups, transOrDb, cb) { - async.each(groups, (groupName, nextGroupName) => { - return addUserToGroup(userId, groupName, transOrDb, nextGroupName); - }, err => { - return cb(err); - }); + async.each(groups, (groupName, nextGroupName) => { + return addUserToGroup(userId, groupName, transOrDb, nextGroupName); + }, err => { + return cb(err); + }); } function removeUserFromGroup(userId, groupName, cb) { - userDb.run( - `DELETE FROM user_group_member + userDb.run( + `DELETE FROM user_group_member WHERE group_name=? AND user_id=?;`, - [ groupName, userId ], - err => { - return cb(err); - } - ); + [ groupName, userId ], + err => { + return cb(err); + } + ); } diff --git a/core/user_list.js b/core/user_list.js index 30313a28..212eb9ea 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -23,90 +23,90 @@ const _ = require('lodash'); */ exports.moduleInfo = { - name : 'User List', - desc : 'Lists all system users', - author : 'NuSkooler', + name : 'User List', + desc : 'Lists all system users', + author : 'NuSkooler', }; const MciViewIds = { - UserList : 1, + UserList : 1, }; exports.getModule = class UserListModule extends MenuModule { - constructor(options) { - super(options); - } + constructor(options) { + super(options); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - let userList = []; + let userList = []; - const USER_LIST_OPTS = { - properties : [ 'location', 'affiliation', 'last_login_timestamp' ], - }; + const USER_LIST_OPTS = { + properties : [ 'location', 'affiliation', 'last_login_timestamp' ], + }; - async.series( - [ - function loadFromConfig(callback) { - var loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - }; + async.series( + [ + function loadFromConfig(callback) { + var loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + }; - vc.loadFromMenuConfig(loadOpts, callback); - }, - function fetchUserList(callback) { - // :TODO: Currently fetching all users - probably always OK, but this could be paged - User.getUserList(USER_LIST_OPTS, function got(err, ul) { - userList = ul; - callback(err); - }); - }, - function populateList(callback) { - var userListView = vc.getView(MciViewIds.UserList); + vc.loadFromMenuConfig(loadOpts, callback); + }, + function fetchUserList(callback) { + // :TODO: Currently fetching all users - probably always OK, but this could be paged + User.getUserList(USER_LIST_OPTS, function got(err, ul) { + userList = ul; + callback(err); + }); + }, + function populateList(callback) { + var userListView = vc.getView(MciViewIds.UserList); - var listFormat = self.menuConfig.config.listFormat || '{userName} - {affils}'; - var focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default changed color! - var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; + var listFormat = self.menuConfig.config.listFormat || '{userName} - {affils}'; + var focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default changed color! + var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; - function getUserFmtObj(ue) { - return { - userId : ue.userId, - userName : ue.userName, - affils : ue.affiliation, - location : ue.location, - // :TODO: the rest! - note : ue.note || '', - lastLoginTs : moment(ue.last_login_timestamp).format(dateTimeFormat), - }; - } + function getUserFmtObj(ue) { + return { + userId : ue.userId, + userName : ue.userName, + affils : ue.affiliation, + location : ue.location, + // :TODO: the rest! + note : ue.note || '', + lastLoginTs : moment(ue.last_login_timestamp).format(dateTimeFormat), + }; + } - userListView.setItems(_.map(userList, function formatUserEntry(ue) { - return stringFormat(listFormat, getUserFmtObj(ue)); - })); + userListView.setItems(_.map(userList, function formatUserEntry(ue) { + return stringFormat(listFormat, getUserFmtObj(ue)); + })); - userListView.setFocusItems(_.map(userList, function formatUserEntry(ue) { - return stringFormat(focusListFormat, getUserFmtObj(ue)); - })); + userListView.setFocusItems(_.map(userList, function formatUserEntry(ue) { + return stringFormat(focusListFormat, getUserFmtObj(ue)); + })); - userListView.redraw(); - callback(null); - } - ], - function complete(err) { - if(err) { - self.client.log.error( { error : err.toString() }, 'Error loading user list'); - } - cb(err); - } - ); - }); - } + userListView.redraw(); + callback(null); + } + ], + function complete(err) { + if(err) { + self.client.log.error( { error : err.toString() }, 'Error loading user list'); + } + cb(err); + } + ); + }); + } }; diff --git a/core/user_login.js b/core/user_login.js index 80d832e0..aa3cfe7b 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -14,85 +14,85 @@ const async = require('async'); exports.userLogin = userLogin; function userLogin(client, username, password, cb) { - client.user.authenticate(username, password, function authenticated(err) { - if(err) { - client.log.info( { username : username, error : err.message }, 'Failed login attempt'); + client.user.authenticate(username, password, function authenticated(err) { + if(err) { + client.log.info( { username : username, error : err.message }, 'Failed login attempt'); - // :TODO: if username exists, record failed login attempt to properties - // :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true + // :TODO: if username exists, record failed login attempt to properties + // :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true - return cb(err); - } - const user = client.user; + return cb(err); + } + const user = client.user; - // - // Ensure this user is not already logged in. - // Loop through active connections -- which includes the current -- - // and check for matching user ID. If the count is > 1, disallow. - // - let existingClientConnection; - clientConnections.forEach(function connEntry(cc) { - if(cc.user !== user && cc.user.userId === user.userId) { - existingClientConnection = cc; - } - }); + // + // Ensure this user is not already logged in. + // Loop through active connections -- which includes the current -- + // and check for matching user ID. If the count is > 1, disallow. + // + let existingClientConnection; + clientConnections.forEach(function connEntry(cc) { + if(cc.user !== user && cc.user.userId === user.userId) { + existingClientConnection = cc; + } + }); - if(existingClientConnection) { - client.log.info( - { - existingClientId : existingClientConnection.session.id, - username : user.username, - userId : user.userId - }, - 'Already logged in' - ); + if(existingClientConnection) { + client.log.info( + { + existingClientId : existingClientConnection.session.id, + username : user.username, + userId : user.userId + }, + 'Already logged in' + ); - const existingConnError = new Error('Already logged in as supplied user'); - existingConnError.existingConn = true; + const existingConnError = new Error('Already logged in as supplied user'); + 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 - return cb(existingConnError); - } + return cb(existingConnError); + } - // update client logger with addition of username - client.log = logger.log.child( - { - clientId : client.log.fields.clientId, - sessionId : client.log.fields.sessionId, - username : user.username, - } - ); - client.log.info('Successful login'); + // update client logger with addition of username + client.log = logger.log.child( + { + clientId : client.log.fields.clientId, + sessionId : client.log.fields.sessionId, + username : user.username, + } + ); + client.log.info('Successful login'); - // User's unique session identifier is the same as the connection itself - user.sessionId = client.session.uniqueId; // convienence + // User's unique session identifier is the same as the connection itself + user.sessionId = client.session.uniqueId; // convienence - Events.emit(Events.getSystemEvents().UserLogin, { user } ); + Events.emit(Events.getSystemEvents().UserLogin, { user } ); - async.parallel( - [ - function setTheme(callback) { - setClientTheme(client, user.properties.theme_id); - return callback(null); - }, - function updateSystemLoginCount(callback) { - return StatLog.incrementSystemStat('login_count', 1, callback); - }, - function recordLastLogin(callback) { - return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); - }, - function updateUserLoginCount(callback) { - return StatLog.incrementUserStat(user, 'login_count', 1, callback); - }, - function recordLoginHistory(callback) { - const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers - return StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); - } - ], - err => { - return cb(err); - } - ); - }); + async.parallel( + [ + function setTheme(callback) { + setClientTheme(client, user.properties.theme_id); + return callback(null); + }, + function updateSystemLoginCount(callback) { + return StatLog.incrementSystemStat('login_count', 1, callback); + }, + function recordLastLogin(callback) { + return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); + }, + function updateUserLoginCount(callback) { + return StatLog.incrementUserStat(user, 'login_count', 1, callback); + }, + function recordLoginHistory(callback) { + const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers + return StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); + } + ], + err => { + return cb(err); + } + ); + }); } \ No newline at end of file diff --git a/core/uuid_util.js b/core/uuid_util.js index 9af449dd..7cf582f5 100644 --- a/core/uuid_util.js +++ b/core/uuid_util.js @@ -6,33 +6,33 @@ const createHash = require('crypto').createHash; exports.createNamedUUID = createNamedUUID; function createNamedUUID(namespaceUuid, key) { - // - // v5 UUID generation code based on the work here: - // https://github.com/download13/uuidv5/blob/master/uuid.js - // - if(!Buffer.isBuffer(namespaceUuid)) { - namespaceUuid = Buffer.from(namespaceUuid); - } + // + // v5 UUID generation code based on the work here: + // https://github.com/download13/uuidv5/blob/master/uuid.js + // + if(!Buffer.isBuffer(namespaceUuid)) { + namespaceUuid = Buffer.from(namespaceUuid); + } - if(!Buffer.isBuffer(key)) { - key = Buffer.from(key); - } + if(!Buffer.isBuffer(key)) { + key = Buffer.from(key); + } - let digest = createHash('sha1').update( - Buffer.concat( [ namespaceUuid, key ] )).digest(); + let digest = createHash('sha1').update( + Buffer.concat( [ namespaceUuid, key ] )).digest(); - let u = Buffer.alloc(16); + let u = Buffer.alloc(16); - // bbbb - bb - bb - bb - bbbbbb - digest.copy(u, 0, 0, 4); // time_low - digest.copy(u, 4, 4, 6); // time_mid - digest.copy(u, 6, 6, 8); // time_hi_and_version + // bbbb - bb - bb - bb - bbbbbb + digest.copy(u, 0, 0, 4); // time_low + digest.copy(u, 4, 4, 6); // time_mid + digest.copy(u, 6, 6, 8); // time_hi_and_version - u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) - u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 - u[9] = digest[9]; + u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) + u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 + u[9] = digest[9]; - digest.copy(u, 10, 10, 16); + digest.copy(u, 10, 10, 16); - return u; + return u; } \ No newline at end of file diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 7e8fc808..e56207a2 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -15,339 +15,339 @@ const _ = require('lodash'); exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { - options.cursor = options.cursor || 'hide'; - options.justify = options.justify || 'left'; + options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; - MenuView.call(this, options); + MenuView.call(this, 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' ], - }); - } + // 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() { - if(this.autoScale.height) { - this.dimens.height = (self.items.length * (self.itemSpacing + 1)) - (self.itemSpacing); - this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row); - } + this.performAutoScale = function() { + if(this.autoScale.height) { + this.dimens.height = (self.items.length * (self.itemSpacing + 1)) - (self.itemSpacing); + this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row); + } - if(self.autoScale.width) { - let maxLen = 0; - self.items.forEach( item => { - if(item.text.length > maxLen) { - maxLen = Math.min(item.text.length, self.client.term.termWidth - self.position.col); - } - }); - self.dimens.width = maxLen + 1; - } - }; + if(self.autoScale.width) { + let maxLen = 0; + self.items.forEach( item => { + if(item.text.length > maxLen) { + maxLen = Math.min(item.text.length, self.client.term.termWidth - self.position.col); + } + }); + self.dimens.width = maxLen + 1; + } + }; - this.performAutoScale(); + this.performAutoScale(); - this.updateViewVisibleItems = function() { - self.maxVisibleItems = Math.ceil(self.dimens.height / (self.itemSpacing + 1)); + this.updateViewVisibleItems = function() { + self.maxVisibleItems = Math.ceil(self.dimens.height / (self.itemSpacing + 1)); - self.viewWindow = { - top : self.focusedItemIndex, - bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1, - }; - }; + self.viewWindow = { + top : self.focusedItemIndex, + bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1, + }; + }; - this.drawItem = function(index) { - const item = self.items[index]; - if(!item) { - return; - } + this.drawItem = function(index) { + const item = self.items[index]; + if(!item) { + return; + } - const cached = this.getRenderCacheItem(index, item.focused); - if(cached) { - return self.client.term.write(`${ansi.goto(item.row, self.position.col)}${cached}`); - } + const cached = this.getRenderCacheItem(index, item.focused); + if(cached) { + return self.client.term.write(`${ansi.goto(item.row, self.position.col)}${cached}`); + } - let text; - let sgr; - if(item.focused && self.hasFocusItems()) { - const focusItem = self.focusItems[index]; - text = focusItem ? focusItem.text : item.text; - sgr = ''; - } else if(this.complexItems) { - text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); - sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); - } else { - text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); - sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); - } + let text; + let sgr; + if(item.focused && self.hasFocusItems()) { + const focusItem = self.focusItems[index]; + text = focusItem ? focusItem.text : item.text; + sgr = ''; + } else if(this.complexItems) { + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } - text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; - self.client.term.write(`${ansi.goto(item.row, self.position.col)}${text}`); - this.setRenderCacheItem(index, text, item.focused); - }; + text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; + self.client.term.write(`${ansi.goto(item.row, self.position.col)}${text}`); + this.setRenderCacheItem(index, text, item.focused); + }; } util.inherits(VerticalMenuView, MenuView); VerticalMenuView.prototype.redraw = function() { - VerticalMenuView.super_.prototype.redraw.call(this); + VerticalMenuView.super_.prototype.redraw.call(this); - // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such - if(this.positionCacheExpired) { - this.performAutoScale(); - this.updateViewVisibleItems(); + // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such + if(this.positionCacheExpired) { + this.performAutoScale(); + this.updateViewVisibleItems(); - this.positionCacheExpired = false; - } + this.positionCacheExpired = false; + } - // erase old items - // :TODO: optimize this: only needed if a item is removed or new max width < old. - if(this.oldDimens) { - const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); - let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; - let row = this.position.row + 1; - const endRow = (row + this.oldDimens.height) - 2; + // erase old items + // :TODO: optimize this: only needed if a item is removed or new max width < old. + if(this.oldDimens) { + const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); + let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; + let row = this.position.row + 1; + const endRow = (row + this.oldDimens.height) - 2; - while(row <= endRow) { - seq += ansi.goto(row, this.position.col) + blank; - row += 1; - } - this.client.term.write(seq); - delete this.oldDimens; - } + while(row <= endRow) { + seq += ansi.goto(row, this.position.col) + blank; + row += 1; + } + this.client.term.write(seq); + delete this.oldDimens; + } - if(this.items.length) { - let row = this.position.row; - for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { - this.items[i].row = row; - row += this.itemSpacing + 1; - this.items[i].focused = this.focusedItemIndex === i; - this.drawItem(i); - } - } + if(this.items.length) { + let row = this.position.row; + for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { + this.items[i].row = row; + row += this.itemSpacing + 1; + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } + } }; VerticalMenuView.prototype.setHeight = function(height) { - VerticalMenuView.super_.prototype.setHeight.call(this, height); + VerticalMenuView.super_.prototype.setHeight.call(this, height); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; VerticalMenuView.prototype.setPosition = function(pos) { - VerticalMenuView.super_.prototype.setPosition.call(this, pos); + VerticalMenuView.super_.prototype.setPosition.call(this, pos); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; VerticalMenuView.prototype.setFocus = function(focused) { - VerticalMenuView.super_.prototype.setFocus.call(this, focused); + VerticalMenuView.super_.prototype.setFocus.call(this, focused); - this.redraw(); + this.redraw(); }; VerticalMenuView.prototype.setFocusItemIndex = function(index) { - VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - const remainAfterFocus = this.items.length - index; - if(remainAfterFocus >= this.maxVisibleItems) { - this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 - }; + const remainAfterFocus = this.items.length - index; + if(remainAfterFocus >= this.maxVisibleItems) { + this.viewWindow = { + top : this.focusedItemIndex, + bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + }; - this.positionCacheExpired = false; // skip standard behavior - this.performAutoScale(); - } + this.positionCacheExpired = false; // skip standard behavior + this.performAutoScale(); + } - this.redraw(); + this.redraw(); }; VerticalMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('up', key.name)) { - this.focusPrevious(); - } else if(this.isKeyMapped('down', key.name)) { - this.focusNext(); - } else if(this.isKeyMapped('page up', key.name)) { - this.focusPreviousPageItem(); - } else if(this.isKeyMapped('page down', key.name)) { - this.focusNextPageItem(); - } else if(this.isKeyMapped('home', key.name)) { - this.focusFirst(); - } else if(this.isKeyMapped('end', key.name)) { - this.focusLast(); - } - } + if(key) { + if(this.isKeyMapped('up', key.name)) { + this.focusPrevious(); + } else if(this.isKeyMapped('down', key.name)) { + this.focusNext(); + } else if(this.isKeyMapped('page up', key.name)) { + this.focusPreviousPageItem(); + } else if(this.isKeyMapped('page down', key.name)) { + this.focusNextPageItem(); + } else if(this.isKeyMapped('home', key.name)) { + this.focusFirst(); + } else if(this.isKeyMapped('end', key.name)) { + this.focusLast(); + } + } - VerticalMenuView.super_.prototype.onKeyPress.call(this, ch, key); + VerticalMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; VerticalMenuView.prototype.getData = function() { - const item = this.getItem(this.focusedItemIndex); - return _.isString(item.data) ? item.data : this.focusedItemIndex; + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; }; VerticalMenuView.prototype.setItems = function(items) { - // if we have items already, save off their drawing area so we don't leave fragments at redraw - if(this.items && this.items.length) { - this.oldDimens = Object.assign({}, this.dimens); - } + // if we have items already, save off their drawing area so we don't leave fragments at redraw + if(this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } - VerticalMenuView.super_.prototype.setItems.call(this, items); + VerticalMenuView.super_.prototype.setItems.call(this, items); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; VerticalMenuView.prototype.removeItem = function(index) { - if(this.items && this.items.length) { - this.oldDimens = Object.assign({}, this.dimens); - } + if(this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } - VerticalMenuView.super_.prototype.removeItem.call(this, index); + VerticalMenuView.super_.prototype.removeItem.call(this, index); }; // :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! VerticalMenuView.prototype.focusNext = function() { - if(this.items.length - 1 === this.focusedItemIndex) { - this.focusedItemIndex = 0; + if(this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; - this.viewWindow = { - top : 0, - bottom : Math.min(this.maxVisibleItems, this.items.length) - 1 - }; - } else { - this.focusedItemIndex++; + this.viewWindow = { + top : 0, + bottom : Math.min(this.maxVisibleItems, this.items.length) - 1 + }; + } else { + this.focusedItemIndex++; - if(this.focusedItemIndex > this.viewWindow.bottom) { - this.viewWindow.top++; - this.viewWindow.bottom++; - } - } + if(this.focusedItemIndex > this.viewWindow.bottom) { + this.viewWindow.top++; + this.viewWindow.bottom++; + } + } - this.redraw(); + this.redraw(); - VerticalMenuView.super_.prototype.focusNext.call(this); + VerticalMenuView.super_.prototype.focusNext.call(this); }; VerticalMenuView.prototype.focusPrevious = function() { - if(0 === this.focusedItemIndex) { - this.focusedItemIndex = this.items.length - 1; + if(0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; - this.viewWindow = { - //top : this.items.length - this.maxVisibleItems, - top : Math.max(this.items.length - this.maxVisibleItems, 0), - bottom : this.items.length - 1 - }; + this.viewWindow = { + //top : this.items.length - this.maxVisibleItems, + top : Math.max(this.items.length - this.maxVisibleItems, 0), + bottom : this.items.length - 1 + }; - } else { - this.focusedItemIndex--; + } else { + this.focusedItemIndex--; - if(this.focusedItemIndex < this.viewWindow.top) { - this.viewWindow.top--; - this.viewWindow.bottom--; + if(this.focusedItemIndex < this.viewWindow.top) { + this.viewWindow.top--; + this.viewWindow.bottom--; - // adjust for focus index being set & window needing expansion as we scroll up - const rem = (this.viewWindow.bottom - this.viewWindow.top) + 1; - if(rem < this.maxVisibleItems && (this.items.length - 1) > this.focusedItemIndex) { - this.viewWindow.bottom = this.items.length - 1; - } - } - } + // adjust for focus index being set & window needing expansion as we scroll up + const rem = (this.viewWindow.bottom - this.viewWindow.top) + 1; + if(rem < this.maxVisibleItems && (this.items.length - 1) > this.focusedItemIndex) { + this.viewWindow.bottom = this.items.length - 1; + } + } + } - this.redraw(); + this.redraw(); - 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 - } + // + // 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); + const index = Math.max(this.focusedItemIndex - this.dimens.height, 0); - if(index < this.viewWindow.top) { - this.oldDimens = Object.assign({}, this.dimens); - } + if(index < this.viewWindow.top) { + this.oldDimens = Object.assign({}, this.dimens); + } - this.setFocusItemIndex(index); + this.setFocusItemIndex(index); - return VerticalMenuView.super_.prototype.focusPreviousPageItem.call(this); + 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 - } + // + // 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); + const index = Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length - 1); - if(index > this.viewWindow.bottom) { - this.oldDimens = Object.assign({}, this.dimens); + if(index > this.viewWindow.bottom) { + this.oldDimens = Object.assign({}, this.dimens); - this.focusedItemIndex = index; + this.focusedItemIndex = index; - this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 - }; + this.viewWindow = { + top : this.focusedItemIndex, + bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + }; - this.redraw(); - } else { - this.setFocusItemIndex(index); - } + this.redraw(); + } else { + this.setFocusItemIndex(index); + } - return VerticalMenuView.super_.prototype.focusNextPageItem.call(this); + return VerticalMenuView.super_.prototype.focusNextPageItem.call(this); }; VerticalMenuView.prototype.focusFirst = function() { - if(0 < this.viewWindow.top) { - this.oldDimens = Object.assign({}, this.dimens); - } - this.setFocusItemIndex(0); - return VerticalMenuView.super_.prototype.focusFirst.call(this); + if(0 < this.viewWindow.top) { + this.oldDimens = Object.assign({}, this.dimens); + } + this.setFocusItemIndex(0); + return VerticalMenuView.super_.prototype.focusFirst.call(this); }; VerticalMenuView.prototype.focusLast = function() { - const index = this.items.length - 1; + const index = this.items.length - 1; - if(index > this.viewWindow.bottom) { - this.oldDimens = Object.assign({}, this.dimens); + if(index > this.viewWindow.bottom) { + this.oldDimens = Object.assign({}, this.dimens); - this.focusedItemIndex = index; + this.focusedItemIndex = index; - this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 - }; + this.viewWindow = { + top : this.focusedItemIndex, + bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + }; - this.redraw(); - } else { - this.setFocusItemIndex(index); - } + this.redraw(); + } else { + this.setFocusItemIndex(index); + } - return VerticalMenuView.super_.prototype.focusLast.call(this); + return VerticalMenuView.super_.prototype.focusLast.call(this); }; VerticalMenuView.prototype.setFocusItems = function(items) { - VerticalMenuView.super_.prototype.setFocusItems.call(this, items); + VerticalMenuView.super_.prototype.setFocusItems.call(this, items); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; VerticalMenuView.prototype.setItemSpacing = function(itemSpacing) { - VerticalMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); + VerticalMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; \ No newline at end of file diff --git a/core/view.js b/core/view.js index 1333ca24..fd46428f 100644 --- a/core/view.js +++ b/core/view.js @@ -15,271 +15,271 @@ const _ = require('lodash'); exports.View = View; const VIEW_SPECIAL_KEY_MAP_DEFAULT = { - accept : [ 'return' ], - exit : [ 'esc' ], - backspace : [ 'backspace', 'del' ], - del : [ 'del' ], - next : [ 'tab' ], - up : [ 'up arrow' ], - down : [ 'down arrow' ], - end : [ 'end' ], - home : [ 'home' ], - left : [ 'left arrow' ], - right : [ 'right arrow' ], - clearLine : [ 'ctrl + y' ], + accept : [ 'return' ], + exit : [ 'esc' ], + backspace : [ 'backspace', 'del' ], + del : [ 'del' ], + next : [ 'tab' ], + up : [ 'up arrow' ], + down : [ 'down arrow' ], + end : [ 'end' ], + home : [ 'home' ], + left : [ 'left arrow' ], + right : [ 'right arrow' ], + clearLine : [ 'ctrl + y' ], }; exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT; function View(options) { - events.EventEmitter.call(this); + events.EventEmitter.call(this); - enigAssert(_.isObject(options)); - enigAssert(_.isObject(options.client)); + enigAssert(_.isObject(options)); + enigAssert(_.isObject(options.client)); - var self = this; + var self = this; - this.client = options.client; + this.client = options.client; - this.cursor = options.cursor || 'show'; - this.cursorStyle = options.cursorStyle || 'default'; + this.cursor = options.cursor || 'show'; + this.cursorStyle = options.cursorStyle || 'default'; - this.acceptsFocus = options.acceptsFocus || false; - this.acceptsInput = options.acceptsInput || false; + this.acceptsFocus = options.acceptsFocus || false; + this.acceptsInput = options.acceptsInput || false; - this.position = { x : 0, y : 0 }; - this.dimens = { height : 1, width : 0 }; + this.position = { x : 0, y : 0 }; + this.dimens = { height : 1, width : 0 }; - this.textStyle = options.textStyle || 'normal'; - this.focusTextStyle = options.focusTextStyle || this.textStyle; + this.textStyle = options.textStyle || 'normal'; + this.focusTextStyle = options.focusTextStyle || this.textStyle; - if(options.id) { - this.setId(options.id); - } + if(options.id) { + this.setId(options.id); + } - if(options.position) { - this.setPosition(options.position); - } + if(options.position) { + this.setPosition(options.position); + } - if(_.isObject(options.autoScale)) { - this.autoScale = options.autoScale; - } else { - this.autoScale = { height : true, width : true }; - } + if(_.isObject(options.autoScale)) { + this.autoScale = options.autoScale; + } else { + this.autoScale = { height : true, width : true }; + } - if(options.dimens) { - this.setDimension(options.dimens); - this.autoScale = { height : false, width : false }; - } else { - this.dimens = { - width : options.width || 0, - height : 0 - }; - } + if(options.dimens) { + this.setDimension(options.dimens); + this.autoScale = { height : false, width : false }; + } else { + this.dimens = { + width : options.width || 0, + height : 0 + }; + } - // :TODO: Just use styleSGRx for these, e.g. styleSGR0, styleSGR1 = norm/focus - this.ansiSGR = options.ansiSGR || ansi.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); - this.ansiFocusSGR = options.ansiFocusSGR || this.ansiSGR; + // :TODO: Just use styleSGRx for these, e.g. styleSGR0, styleSGR1 = norm/focus + this.ansiSGR = options.ansiSGR || ansi.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); + this.ansiFocusSGR = options.ansiFocusSGR || this.ansiSGR; - this.styleSGR1 = options.styleSGR1 || this.ansiSGR; - this.styleSGR2 = options.styleSGR2 || this.ansiFocusSGR; + this.styleSGR1 = options.styleSGR1 || this.ansiSGR; + this.styleSGR2 = options.styleSGR2 || this.ansiFocusSGR; - if(this.acceptsInput) { - this.specialKeyMap = options.specialKeyMap || VIEW_SPECIAL_KEY_MAP_DEFAULT; + if(this.acceptsInput) { + this.specialKeyMap = options.specialKeyMap || VIEW_SPECIAL_KEY_MAP_DEFAULT; - if(_.isObject(options.specialKeyMapOverride)) { - this.setSpecialKeyMapOverride(options.specialKeyMapOverride); - } - } + if(_.isObject(options.specialKeyMapOverride)) { + this.setSpecialKeyMapOverride(options.specialKeyMapOverride); + } + } - this.isKeyMapped = function(keySet, keyName) { - return _.has(this.specialKeyMap, keySet) && this.specialKeyMap[keySet].indexOf(keyName) > -1; - }; + this.isKeyMapped = function(keySet, keyName) { + return _.has(this.specialKeyMap, keySet) && this.specialKeyMap[keySet].indexOf(keyName) > -1; + }; - this.getANSIColor = function(color) { - var sgr = [ color.flags, color.fg ]; - if(color.bg !== color.flags) { - sgr.push(color.bg); - } - return ansi.sgr(sgr); - }; + this.getANSIColor = function(color) { + var sgr = [ color.flags, color.fg ]; + if(color.bg !== color.flags) { + sgr.push(color.bg); + } + return ansi.sgr(sgr); + }; - this.hideCusor = function() { - self.client.term.rawWrite(ansi.hideCursor()); - }; + this.hideCusor = function() { + self.client.term.rawWrite(ansi.hideCursor()); + }; - this.restoreCursor = function() { - //this.client.term.write(ansi.setCursorStyle(this.cursorStyle)); - this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor()); - }; + this.restoreCursor = function() { + //this.client.term.write(ansi.setCursorStyle(this.cursorStyle)); + this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor()); + }; } util.inherits(View, events.EventEmitter); View.prototype.setId = function(id) { - this.id = id; + this.id = id; }; View.prototype.getId = function() { - return this.id; + return this.id; }; View.prototype.setPosition = function(pos) { - // - // Allow the following forms: [row, col], { row : r, col : c }, or (row, col) - // - if(util.isArray(pos)) { - this.position.row = pos[0]; - this.position.col = pos[1]; - } else if(_.isNumber(pos.row) && _.isNumber(pos.col)) { - this.position.row = pos.row; - this.position.col = pos.col; - } else if(2 === arguments.length) { - this.position.row = parseInt(arguments[0], 10); - this.position.col = parseInt(arguments[1], 10); - } + // + // Allow the following forms: [row, col], { row : r, col : c }, or (row, col) + // + if(util.isArray(pos)) { + this.position.row = pos[0]; + this.position.col = pos[1]; + } else if(_.isNumber(pos.row) && _.isNumber(pos.col)) { + this.position.row = pos.row; + this.position.col = pos.col; + } else if(2 === arguments.length) { + this.position.row = parseInt(arguments[0], 10); + this.position.col = parseInt(arguments[1], 10); + } - // sanatize - this.position.row = Math.max(this.position.row, 1); - this.position.col = Math.max(this.position.col, 1); - this.position.row = Math.min(this.position.row, this.client.term.termHeight); - this.position.col = Math.min(this.position.col, this.client.term.termWidth); + // sanatize + this.position.row = Math.max(this.position.row, 1); + this.position.col = Math.max(this.position.col, 1); + this.position.row = Math.min(this.position.row, this.client.term.termHeight); + this.position.col = Math.min(this.position.col, this.client.term.termWidth); }; View.prototype.setDimension = function(dimens) { - enigAssert(_.isObject(dimens) && _.isNumber(dimens.height) && _.isNumber(dimens.width)); + enigAssert(_.isObject(dimens) && _.isNumber(dimens.height) && _.isNumber(dimens.width)); - this.dimens = dimens; - this.autoScale = { height : false, width : false }; + this.dimens = dimens; + this.autoScale = { height : false, width : false }; }; View.prototype.setHeight = function(height) { - height = parseInt(height) || 1; - height = Math.min(height, this.client.term.termHeight); + height = parseInt(height) || 1; + height = Math.min(height, this.client.term.termHeight); - this.dimens.height = height; - this.autoScale.height = false; + this.dimens.height = height; + this.autoScale.height = false; }; View.prototype.setWidth = function(width) { - width = parseInt(width) || 1; - width = Math.min(width, this.client.term.termWidth); + width = parseInt(width) || 1; + width = Math.min(width, this.client.term.termWidth); - this.dimens.width = width; - this.autoScale.width = false; + this.dimens.width = width; + this.autoScale.width = false; }; View.prototype.getSGR = function() { - return this.ansiSGR; + return this.ansiSGR; }; View.prototype.getStyleSGR = function(n) { - n = parseInt(n) || 0; - return this['styleSGR' + n]; + n = parseInt(n) || 0; + return this['styleSGR' + n]; }; View.prototype.getFocusSGR = function() { - return this.ansiFocusSGR; + return this.ansiFocusSGR; }; View.prototype.setSpecialKeyMapOverride = function(specialKeyMapOverride) { - this.specialKeyMap = Object.assign(this.specialKeyMap, specialKeyMapOverride); + this.specialKeyMap = Object.assign(this.specialKeyMap, specialKeyMapOverride); }; View.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'height' : this.setHeight(value); break; - case 'width' : this.setWidth(value); break; - case 'focus' : this.setFocus(value); break; + switch(propName) { + case 'height' : this.setHeight(value); break; + case 'width' : this.setWidth(value); break; + case 'focus' : this.setFocus(value); break; - case 'text' : - if('setText' in this) { - this.setText(value); - } - break; + case 'text' : + if('setText' in this) { + this.setText(value); + } + break; - case 'textStyle' : this.textStyle = value; break; - case 'focusTextStyle' : this.focusTextStyle = value; break; + case 'textStyle' : this.textStyle = value; break; + case 'focusTextStyle' : this.focusTextStyle = value; break; - case 'justify' : this.justify = value; break; + case 'justify' : this.justify = value; break; - case 'fillChar' : - if('fillChar' in this) { - if(_.isNumber(value)) { - this.fillChar = String.fromCharCode(value); - } else if(_.isString(value)) { - this.fillChar = renderSubstr(value, 0, 1); - } - } - break; + case 'fillChar' : + if('fillChar' in this) { + if(_.isNumber(value)) { + this.fillChar = String.fromCharCode(value); + } else if(_.isString(value)) { + this.fillChar = renderSubstr(value, 0, 1); + } + } + break; - case 'submit' : - if(_.isBoolean(value)) { - this.submit = value; - }/* else { + case 'submit' : + if(_.isBoolean(value)) { + this.submit = value; + }/* else { this.submit = _.isArray(value) && value.length > 0; } */ - break; + break; - case 'resizable' : - if(_.isBoolean(value)) { - this.resizable = value; - } - break; + case 'resizable' : + if(_.isBoolean(value)) { + this.resizable = value; + } + break; - case 'argName' : this.submitArgName = value; break; + case 'argName' : this.submitArgName = value; break; - case 'validate' : - if(_.isFunction(value)) { - this.validate = value; - } - break; - } + case 'validate' : + if(_.isFunction(value)) { + this.validate = value; + } + break; + } - if(/styleSGR[0-9]{1,2}/.test(propName)) { - if(_.isObject(value)) { - this[propName] = ansi.getSGRFromGraphicRendition(value, true); - } else if(_.isString(value)) { - this[propName] = colorCodes.pipeToAnsi(value); - } - } + if(/styleSGR[0-9]{1,2}/.test(propName)) { + if(_.isObject(value)) { + this[propName] = ansi.getSGRFromGraphicRendition(value, true); + } else if(_.isString(value)) { + this[propName] = colorCodes.pipeToAnsi(value); + } + } }; View.prototype.redraw = function() { - this.client.term.write(ansi.goto(this.position.row, this.position.col)); + this.client.term.write(ansi.goto(this.position.row, this.position.col)); }; View.prototype.setFocus = function(focused) { - enigAssert(this.acceptsFocus, 'View does not accept focus'); + enigAssert(this.acceptsFocus, 'View does not accept focus'); - this.hasFocus = focused; - this.restoreCursor(); + this.hasFocus = focused; + this.restoreCursor(); }; View.prototype.onKeyPress = function(ch, key) { - enigAssert(this.hasFocus, 'View does not have focus'); - enigAssert(this.acceptsInput, 'View does not accept input'); + enigAssert(this.hasFocus, 'View does not have focus'); + enigAssert(this.acceptsInput, 'View does not accept input'); - if(!this.hasFocus || !this.acceptsInput) { - return; - } + if(!this.hasFocus || !this.acceptsInput) { + return; + } - if(key) { - enigAssert(this.specialKeyMap, 'No special key map defined'); + if(key) { + enigAssert(this.specialKeyMap, 'No special key map defined'); - if(this.isKeyMapped('accept', key.name)) { - this.emit('action', 'accept', key); - } else if(this.isKeyMapped('next', key.name)) { - this.emit('action', 'next', key); - } - } + if(this.isKeyMapped('accept', key.name)) { + this.emit('action', 'accept', key); + } else if(this.isKeyMapped('next', key.name)) { + this.emit('action', 'next', key); + } + } - if(ch) { - enigAssert(1 === ch.length); - } + if(ch) { + enigAssert(1 === ch.length); + } - this.emit('key press', ch, key); + this.emit('key press', ch, key); }; View.prototype.getData = function() { diff --git a/core/view_controller.js b/core/view_controller.js index 65a5a1b3..f6a2bc2b 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -20,672 +20,672 @@ exports.ViewController = ViewController; var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/; function ViewController(options) { - assert(_.isObject(options)); - assert(_.isObject(options.client)); + assert(_.isObject(options)); + assert(_.isObject(options.client)); - events.EventEmitter.call(this); + events.EventEmitter.call(this); - var self = this; + var self = this; - this.client = options.client; - this.views = {}; // map of ID -> view - this.formId = options.formId || 0; - this.mciViewFactory = new MCIViewFactory(this.client); // :TODO: can this not be a singleton? - this.noInput = _.isBoolean(options.noInput) ? options.noInput : false; + this.client = options.client; + this.views = {}; // map of ID -> view + this.formId = options.formId || 0; + this.mciViewFactory = new MCIViewFactory(this.client); // :TODO: can this not be a singleton? + this.noInput = _.isBoolean(options.noInput) ? options.noInput : false; - this.actionKeyMap = {}; + this.actionKeyMap = {}; - // - // Small wrapper/proxy around handleAction() to ensure we do not allow - // input/additional actions queued while performing an action - // - this.handleActionWrapper = function(formData, actionBlock) { - if(self.waitActionCompletion) { - return; // ignore until this is finished! - } + // + // Small wrapper/proxy around handleAction() to ensure we do not allow + // input/additional actions queued while performing an action + // + this.handleActionWrapper = function(formData, actionBlock) { + if(self.waitActionCompletion) { + return; // ignore until this is finished! + } - self.waitActionCompletion = true; - menuUtil.handleAction(self.client, formData, actionBlock, (err) => { - if(err) { - // :TODO: What can we really do here? - if('ALREADYTHERE' === err.reasonCode) { - self.client.log.trace( err.reason ); - } else { - self.client.log.warn( { err : err }, 'Error during handleAction()'); - } - } + self.waitActionCompletion = true; + menuUtil.handleAction(self.client, formData, actionBlock, (err) => { + if(err) { + // :TODO: What can we really do here? + if('ALREADYTHERE' === err.reasonCode) { + self.client.log.trace( err.reason ); + } else { + self.client.log.warn( { err : err }, 'Error during handleAction()'); + } + } - self.waitActionCompletion = false; - }); - }; + self.waitActionCompletion = false; + }); + }; - this.clientKeyPressHandler = function(ch, key) { - // - // Process key presses treating form submit mapped keys special. - // Everything else is forwarded on to the focused View, if any. - // - var actionForKey = key ? self.actionKeyMap[key.name] : self.actionKeyMap[ch]; - if(actionForKey) { - if(_.isNumber(actionForKey.viewId)) { - // - // Key works on behalf of a view -- switch focus & submit - // - self.switchFocus(actionForKey.viewId); - self.submitForm(key); - } else if(_.isString(actionForKey.action)) { - const formData = self.getFocusedView() ? self.getFormData() : { }; - self.handleActionWrapper( - Object.assign( { ch : ch, key : key }, formData ), // formData + key info - actionForKey); // actionBlock - } - } else { - if(self.focusedView && self.focusedView.acceptsInput) { - self.focusedView.onKeyPress(ch, key); - } - } - }; + this.clientKeyPressHandler = function(ch, key) { + // + // Process key presses treating form submit mapped keys special. + // Everything else is forwarded on to the focused View, if any. + // + var actionForKey = key ? self.actionKeyMap[key.name] : self.actionKeyMap[ch]; + if(actionForKey) { + if(_.isNumber(actionForKey.viewId)) { + // + // Key works on behalf of a view -- switch focus & submit + // + self.switchFocus(actionForKey.viewId); + self.submitForm(key); + } else if(_.isString(actionForKey.action)) { + const formData = self.getFocusedView() ? self.getFormData() : { }; + self.handleActionWrapper( + Object.assign( { ch : ch, key : key }, formData ), // formData + key info + actionForKey); // actionBlock + } + } else { + if(self.focusedView && self.focusedView.acceptsInput) { + self.focusedView.onKeyPress(ch, key); + } + } + }; - this.viewActionListener = function(action, key) { - switch(action) { - case 'next' : - self.emit('action', { view : this, action : action, key : key }); - self.nextFocus(); - break; + this.viewActionListener = function(action, key) { + switch(action) { + case 'next' : + self.emit('action', { view : this, action : action, key : key }); + self.nextFocus(); + break; - case 'accept' : - if(self.focusedView && self.focusedView.submit) { - // :TODO: need to do validation here!!! - var focusedView = self.focusedView; - self.validateView(focusedView, function validated(err, newFocusedViewId) { - if(err) { - var newFocusedView = self.getView(newFocusedViewId) || focusedView; - self.setViewFocusWithEvents(newFocusedView, true); - } else { - self.submitForm(key); - } - }); - //self.submitForm(key); - } else { - self.nextFocus(); - } - break; - } - }; + case 'accept' : + if(self.focusedView && self.focusedView.submit) { + // :TODO: need to do validation here!!! + var focusedView = self.focusedView; + self.validateView(focusedView, function validated(err, newFocusedViewId) { + if(err) { + var newFocusedView = self.getView(newFocusedViewId) || focusedView; + self.setViewFocusWithEvents(newFocusedView, true); + } else { + self.submitForm(key); + } + }); + //self.submitForm(key); + } else { + self.nextFocus(); + } + break; + } + }; - this.submitForm = function(key) { - self.emit('submit', this.getFormData(key)); - }; + this.submitForm = function(key) { + self.emit('submit', this.getFormData(key)); + }; - // :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them - this.getLogFriendlyFormData = function(formData) { - // :TODO: these fields should be part of menu.json sensitiveMembers[] - var safeFormData = _.cloneDeep(formData); - if(safeFormData.value.password) { - safeFormData.value.password = '*****'; - } - if(safeFormData.value.passwordConfirm) { - safeFormData.value.passwordConfirm = '*****'; - } - return safeFormData; - }; + // :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them + this.getLogFriendlyFormData = function(formData) { + // :TODO: these fields should be part of menu.json sensitiveMembers[] + var safeFormData = _.cloneDeep(formData); + if(safeFormData.value.password) { + safeFormData.value.password = '*****'; + } + if(safeFormData.value.passwordConfirm) { + safeFormData.value.passwordConfirm = '*****'; + } + return safeFormData; + }; - this.switchFocusEvent = function(event, view) { - if(self.emitSwitchFocus) { - return; - } + this.switchFocusEvent = function(event, view) { + if(self.emitSwitchFocus) { + return; + } - self.emitSwitchFocus = true; - self.emit(event, view); - self.emitSwitchFocus = false; - }; + self.emitSwitchFocus = true; + self.emit(event, view); + self.emitSwitchFocus = false; + }; - this.createViewsFromMCI = function(mciMap, cb) { - async.each(Object.keys(mciMap), (name, nextItem) => { - const mci = mciMap[name]; - const view = self.mciViewFactory.createFromMCI(mci); + this.createViewsFromMCI = function(mciMap, cb) { + async.each(Object.keys(mciMap), (name, nextItem) => { + const mci = mciMap[name]; + const view = self.mciViewFactory.createFromMCI(mci); - if(view) { - if(false === self.noInput) { - view.on('action', self.viewActionListener); - } + if(view) { + if(false === self.noInput) { + view.on('action', self.viewActionListener); + } - self.addView(view); - } + self.addView(view); + } - return nextItem(null); - }, - err => { - self.setViewOrder(); - return cb(err); - }); - }; + return nextItem(null); + }, + err => { + self.setViewOrder(); + return cb(err); + }); + }; - // :TODO: move this elsewhere - this.setViewPropertiesFromMCIConf = function(view, conf) { + // :TODO: move this elsewhere + this.setViewPropertiesFromMCIConf = function(view, conf) { - var propAsset; - var propValue; + var propAsset; + var propValue; - for(var propName in conf) { - propAsset = asset.getViewPropertyAsset(conf[propName]); - if(propAsset) { - switch(propAsset.type) { - case 'config' : - propValue = asset.resolveConfigAsset(conf[propName]); - break; + for(var propName in conf) { + propAsset = asset.getViewPropertyAsset(conf[propName]); + if(propAsset) { + switch(propAsset.type) { + case 'config' : + propValue = asset.resolveConfigAsset(conf[propName]); + break; - case 'sysStat' : - propValue = asset.resolveSystemStatAsset(conf[propName]); - break; + case 'sysStat' : + propValue = asset.resolveSystemStatAsset(conf[propName]); + break; - // :TODO: handle @art (e.g. text : @art ...) + // :TODO: handle @art (e.g. text : @art ...) - case 'method' : - case 'systemMethod' : - if('validate' === propName) { - // :TODO: handle propAsset.location for @method script specification - if('systemMethod' === propAsset.type) { - // :TODO: implementation validation @systemMethod handling! - var methodModule = require(paths.join(__dirname, 'system_view_validate.js')); - if(_.isFunction(methodModule[propAsset.asset])) { - propValue = methodModule[propAsset.asset]; - } - } else { - if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) { - propValue = self.client.currentMenuModule.menuMethods[propAsset.asset]; - } - } - } else { - if(_.isString(propAsset.location)) { - // :TODO: clean this code up! - } else { - if('systemMethod' === propAsset.type) { - // :TODO: - } else { - // local to current module - var currentModule = self.client.currentMenuModule; - if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { - // :TODO: Fix formData & extraArgs... this all needs general processing - propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); - } - } - } - } - break; + case 'method' : + case 'systemMethod' : + if('validate' === propName) { + // :TODO: handle propAsset.location for @method script specification + if('systemMethod' === propAsset.type) { + // :TODO: implementation validation @systemMethod handling! + var methodModule = require(paths.join(__dirname, 'system_view_validate.js')); + if(_.isFunction(methodModule[propAsset.asset])) { + propValue = methodModule[propAsset.asset]; + } + } else { + if(_.isFunction(self.client.currentMenuModule.menuMethods[propAsset.asset])) { + propValue = self.client.currentMenuModule.menuMethods[propAsset.asset]; + } + } + } else { + if(_.isString(propAsset.location)) { + // :TODO: clean this code up! + } else { + if('systemMethod' === propAsset.type) { + // :TODO: + } else { + // local to current module + var currentModule = self.client.currentMenuModule; + if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { + // :TODO: Fix formData & extraArgs... this all needs general processing + propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); + } + } + } + } + break; - default : - propValue = propValue = conf[propName]; - break; - } - } else { - propValue = conf[propName]; - } + default : + propValue = propValue = conf[propName]; + break; + } + } else { + propValue = conf[propName]; + } - if(!_.isUndefined(propValue)) { - view.setPropertyValue(propName, propValue); - } - } - }; + if(!_.isUndefined(propValue)) { + view.setPropertyValue(propName, propValue); + } + } + }; - this.applyViewConfig = function(config, cb) { - let highestId = 1; - let submitId; - let initialFocusId = 1; + this.applyViewConfig = function(config, cb) { + let highestId = 1; + let submitId; + let initialFocusId = 1; - async.each(Object.keys(config.mci || {}), function entry(mci, nextItem) { - const mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? - if(null === mciMatch) { - self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); - return; - } + async.each(Object.keys(config.mci || {}), function entry(mci, nextItem) { + const mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? + if(null === mciMatch) { + self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); + return; + } - const viewId = parseInt(mciMatch[2]); - assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used + const viewId = parseInt(mciMatch[2]); + assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used - if(viewId > highestId) { - highestId = viewId; - } + if(viewId > highestId) { + highestId = viewId; + } - const view = self.getView(viewId); + const view = self.getView(viewId); - if(!view) { - self.client.log.warn( { viewId : viewId }, 'Cannot find view'); - nextItem(null); - return; - } + if(!view) { + self.client.log.warn( { viewId : viewId }, 'Cannot find view'); + nextItem(null); + return; + } - const mciConf = config.mci[mci]; + const mciConf = config.mci[mci]; - self.setViewPropertiesFromMCIConf(view, mciConf); + self.setViewPropertiesFromMCIConf(view, mciConf); - if(mciConf.focus) { - initialFocusId = viewId; - } + if(mciConf.focus) { + initialFocusId = viewId; + } - if(true === view.submit) { - submitId = viewId; - } + if(true === view.submit) { + submitId = viewId; + } - nextItem(null); - }, - err => { - // default to highest ID if no 'submit' entry present - if(!submitId) { - var highestIdView = self.getView(highestId); - if(highestIdView) { - highestIdView.submit = true; - } else { - self.client.log.warn( { highestId : highestId }, 'View does not exist'); - } - } + nextItem(null); + }, + err => { + // default to highest ID if no 'submit' entry present + if(!submitId) { + var highestIdView = self.getView(highestId); + if(highestIdView) { + highestIdView.submit = true; + } else { + self.client.log.warn( { highestId : highestId }, 'View does not exist'); + } + } - return cb(err, { initialFocusId : initialFocusId } ); - }); - }; + return cb(err, { initialFocusId : initialFocusId } ); + }); + }; - // method for comparing submitted form data to configuration entries - this.actionBlockValueComparator = function(formValue, actionValue) { - // - // For a match to occur, one of the following must be true: - // - // * actionValue is a Object: - // a) All key/values must exactly match - // b) value is null; The key (view ID or "argName") must be present - // in formValue. This is a wildcard/any match. - // * actionValue is a Number: This represents a view ID that - // must be present in formValue. - // * actionValue is a string: This represents a view with - // "argName" set that must be present in formValue. - // - if(_.isUndefined(actionValue)) { - return false; - } + // method for comparing submitted form data to configuration entries + this.actionBlockValueComparator = function(formValue, actionValue) { + // + // For a match to occur, one of the following must be true: + // + // * actionValue is a Object: + // a) All key/values must exactly match + // b) value is null; The key (view ID or "argName") must be present + // in formValue. This is a wildcard/any match. + // * actionValue is a Number: This represents a view ID that + // must be present in formValue. + // * actionValue is a string: This represents a view with + // "argName" set that must be present in formValue. + // + if(_.isUndefined(actionValue)) { + return false; + } - if(_.isNumber(actionValue) || _.isString(actionValue)) { - if(_.isUndefined(formValue[actionValue])) { - return false; - } - } else { - /* + if(_.isNumber(actionValue) || _.isString(actionValue)) { + if(_.isUndefined(formValue[actionValue])) { + return false; + } + } else { + /* :TODO: support: value: { someArgName: [ "key1", "key2", ... ], someOtherArg: [ "key1, ... ] } */ - var actionValueKeys = Object.keys(actionValue); - for(var i = 0; i < actionValueKeys.length; ++i) { - var viewId = actionValueKeys[i]; - if(!_.has(formValue, viewId)) { - return false; - } + var actionValueKeys = Object.keys(actionValue); + for(var i = 0; i < actionValueKeys.length; ++i) { + var viewId = actionValueKeys[i]; + if(!_.has(formValue, viewId)) { + return false; + } - if(null !== actionValue[viewId] && actionValue[viewId] !== formValue[viewId]) { - return false; - } - } - } + if(null !== actionValue[viewId] && actionValue[viewId] !== formValue[viewId]) { + return false; + } + } + } - self.client.log.trace( - { - formValue : formValue, - actionValue : actionValue - }, - 'Action match' - ); + self.client.log.trace( + { + formValue : formValue, + actionValue : actionValue + }, + 'Action match' + ); - return true; - }; + return true; + }; - if(!options.detached) { - this.attachClientEvents(); - } + if(!options.detached) { + this.attachClientEvents(); + } - this.setViewFocusWithEvents = function(view, focused) { - if(!view || !view.acceptsFocus) { - return; - } + this.setViewFocusWithEvents = function(view, focused) { + if(!view || !view.acceptsFocus) { + return; + } - if(focused) { - self.switchFocusEvent('return', view); - self.focusedView = view; - } else { - self.switchFocusEvent('leave', view); - } + if(focused) { + self.switchFocusEvent('return', view); + self.focusedView = view; + } else { + self.switchFocusEvent('leave', view); + } - view.setFocus(focused); - }; + view.setFocus(focused); + }; - this.validateView = function(view, cb) { - if(view && _.isFunction(view.validate)) { - view.validate(view.getData(), function validateResult(err) { - var viewValidationListener = self.client.currentMenuModule.menuMethods.viewValidationListener; - if(_.isFunction(viewValidationListener)) { - if(err) { - err.view = view; // pass along the view that failed - } + this.validateView = function(view, cb) { + if(view && _.isFunction(view.validate)) { + view.validate(view.getData(), function validateResult(err) { + var viewValidationListener = self.client.currentMenuModule.menuMethods.viewValidationListener; + if(_.isFunction(viewValidationListener)) { + if(err) { + err.view = view; // pass along the view that failed + } - viewValidationListener(err, function validationComplete(newViewFocusId) { - cb(err, newViewFocusId); - }); - } else { - cb(err); - } - }); - } else { - cb(null); - } - }; + viewValidationListener(err, function validationComplete(newViewFocusId) { + cb(err, newViewFocusId); + }); + } else { + cb(err); + } + }); + } else { + cb(null); + } + }; } util.inherits(ViewController, events.EventEmitter); ViewController.prototype.attachClientEvents = function() { - if(this.attached) { - return; - } + if(this.attached) { + return; + } - var self = this; + var self = this; - this.client.on('key press', this.clientKeyPressHandler); + this.client.on('key press', this.clientKeyPressHandler); - Object.keys(this.views).forEach(function vid(i) { - // remove, then add to ensure we only have one listener - self.views[i].removeListener('action', self.viewActionListener); - self.views[i].on('action', self.viewActionListener); - }); + Object.keys(this.views).forEach(function vid(i) { + // remove, then add to ensure we only have one listener + self.views[i].removeListener('action', self.viewActionListener); + self.views[i].on('action', self.viewActionListener); + }); - this.attached = true; + this.attached = true; }; ViewController.prototype.detachClientEvents = function() { - if(!this.attached) { - return; - } + if(!this.attached) { + return; + } - this.client.removeListener('key press', this.clientKeyPressHandler); + this.client.removeListener('key press', this.clientKeyPressHandler); - for(var id in this.views) { - this.views[id].removeAllListeners(); - } + for(var id in this.views) { + this.views[id].removeAllListeners(); + } - this.attached = false; + this.attached = false; }; ViewController.prototype.viewExists = function(id) { - return id in this.views; + return id in this.views; }; ViewController.prototype.addView = function(view) { - assert(!this.viewExists(view.id), 'View with ID ' + view.id + ' already exists'); + assert(!this.viewExists(view.id), 'View with ID ' + view.id + ' already exists'); - this.views[view.id] = view; + this.views[view.id] = view; }; ViewController.prototype.getView = function(id) { - return this.views[id]; + return this.views[id]; }; ViewController.prototype.getViewsByMciCode = function(mciCode) { - if(!Array.isArray(mciCode)) { - mciCode = [ mciCode ]; - } + if(!Array.isArray(mciCode)) { + mciCode = [ mciCode ]; + } - const views = []; - _.each(this.views, v => { - if(mciCode.includes(v.mciCode)) { - views.push(v); - } - }); - return views; + const views = []; + _.each(this.views, v => { + if(mciCode.includes(v.mciCode)) { + views.push(v); + } + }); + return views; }; ViewController.prototype.getFocusedView = function() { - return this.focusedView; + return this.focusedView; }; ViewController.prototype.setFocus = function(focused) { - if(focused) { - this.attachClientEvents(); - } else { - this.detachClientEvents(); - } + if(focused) { + this.attachClientEvents(); + } else { + this.detachClientEvents(); + } - this.setViewFocusWithEvents(this.focusedView, focused); + this.setViewFocusWithEvents(this.focusedView, focused); }; ViewController.prototype.resetInitialFocus = function() { - if(this.formInitialFocusId) { - return this.switchFocus(this.formInitialFocusId); - } + if(this.formInitialFocusId) { + return this.switchFocus(this.formInitialFocusId); + } }; ViewController.prototype.switchFocus = function(id) { - // - // Perform focus switching validation now - // - var self = this; - var focusedView = self.focusedView; + // + // Perform focus switching validation now + // + var self = this; + var focusedView = self.focusedView; - self.validateView(focusedView, function validated(err, newFocusedViewId) { - if(err) { - var newFocusedView = self.getView(newFocusedViewId) || focusedView; - self.setViewFocusWithEvents(newFocusedView, true); - } else { - self.attachClientEvents(); + self.validateView(focusedView, function validated(err, newFocusedViewId) { + if(err) { + var newFocusedView = self.getView(newFocusedViewId) || focusedView; + self.setViewFocusWithEvents(newFocusedView, true); + } else { + self.attachClientEvents(); - // remove from old - self.setViewFocusWithEvents(focusedView, false); + // remove from old + self.setViewFocusWithEvents(focusedView, false); - // set to new - self.setViewFocusWithEvents(self.getView(id), true); - } - }); + // set to new + self.setViewFocusWithEvents(self.getView(id), true); + } + }); }; ViewController.prototype.nextFocus = function() { - let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId]; + let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId]; - // find the next view that accepts focus - while(nextFocusView && nextFocusView.nextId) { - nextFocusView = this.getView(nextFocusView.nextId); - if(!nextFocusView || nextFocusView.acceptsFocus) { - break; - } - } + // find the next view that accepts focus + while(nextFocusView && nextFocusView.nextId) { + nextFocusView = this.getView(nextFocusView.nextId); + if(!nextFocusView || nextFocusView.acceptsFocus) { + break; + } + } - if(nextFocusView && this.focusedView !== nextFocusView) { - this.switchFocus(nextFocusView.id); - } + if(nextFocusView && this.focusedView !== nextFocusView) { + this.switchFocus(nextFocusView.id); + } }; ViewController.prototype.setViewOrder = function(order) { - var viewIdOrder = order || []; + var viewIdOrder = order || []; - if(0 === viewIdOrder.length) { - for(var id in this.views) { - if(this.views[id].acceptsFocus) { - viewIdOrder.push(id); - } - } + if(0 === viewIdOrder.length) { + for(var id in this.views) { + if(this.views[id].acceptsFocus) { + viewIdOrder.push(id); + } + } - viewIdOrder.sort(function intSort(a, b) { - return a - b; - }); - } + viewIdOrder.sort(function intSort(a, b) { + return a - b; + }); + } - if(viewIdOrder.length > 0) { - var count = viewIdOrder.length - 1; - for(var i = 0; i < count; ++i) { - this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1]; - } + if(viewIdOrder.length > 0) { + var count = viewIdOrder.length - 1; + for(var i = 0; i < count; ++i) { + this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1]; + } - this.firstId = viewIdOrder[0]; - var lastId = viewIdOrder.length > 1 ? viewIdOrder[viewIdOrder.length - 1] : this.firstId; - this.views[lastId].nextId = this.firstId; - } + this.firstId = viewIdOrder[0]; + var lastId = viewIdOrder.length > 1 ? viewIdOrder[viewIdOrder.length - 1] : this.firstId; + this.views[lastId].nextId = this.firstId; + } }; ViewController.prototype.redrawAll = function(initialFocusId) { - this.client.term.rawWrite(ansi.hideCursor()); + this.client.term.rawWrite(ansi.hideCursor()); - for(var id in this.views) { - if(initialFocusId === id) { - continue; // will draw @ focus - } - this.views[id].redraw(); - } + for(var id in this.views) { + if(initialFocusId === id) { + continue; // will draw @ focus + } + this.views[id].redraw(); + } - this.client.term.rawWrite(ansi.showCursor()); + this.client.term.rawWrite(ansi.showCursor()); }; ViewController.prototype.loadFromPromptConfig = function(options, cb) { - assert(_.isObject(options)); - assert(_.isObject(options.mciMap)); + assert(_.isObject(options)); + assert(_.isObject(options.mciMap)); - var self = this; - var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig; - var initialFocusId = 1; // default to first + var self = this; + var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig; + var initialFocusId = 1; // default to first - async.waterfall( - [ - function createViewsFromMCI(callback) { - self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { - callback(err); - }); - }, - function applyViewConfiguration(callback) { - if(_.isObject(promptConfig.mci)) { - self.applyViewConfig(promptConfig, function configApplied(err, info) { - initialFocusId = info.initialFocusId; - callback(err); - }); - } else { - callback(null); - } - }, - function prepareFormSubmission(callback) { - if(false === self.noInput) { + async.waterfall( + [ + function createViewsFromMCI(callback) { + self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { + callback(err); + }); + }, + function applyViewConfiguration(callback) { + if(_.isObject(promptConfig.mci)) { + self.applyViewConfig(promptConfig, function configApplied(err, info) { + initialFocusId = info.initialFocusId; + callback(err); + }); + } else { + callback(null); + } + }, + function prepareFormSubmission(callback) { + if(false === self.noInput) { - self.on('submit', function promptSubmit(formData) { - self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Prompt submit'); + self.on('submit', function promptSubmit(formData) { + self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Prompt submit'); - if(_.isString(self.client.currentMenuModule.menuConfig.action)) { - self.handleActionWrapper(formData, self.client.currentMenuModule.menuConfig); - } else { - // - // Menus that reference prompts can have a sepcial "submit" block without the - // hassle of by-form-id configurations, etc. - // - // "submit" : [ - // { ... } - // ] - // - var menuSubmit = self.client.currentMenuModule.menuConfig.submit; - if(!_.isArray(menuSubmit)) { - self.client.log.debug('No configuration to handle submit'); - return; - } + if(_.isString(self.client.currentMenuModule.menuConfig.action)) { + self.handleActionWrapper(formData, self.client.currentMenuModule.menuConfig); + } else { + // + // Menus that reference prompts can have a sepcial "submit" block without the + // hassle of by-form-id configurations, etc. + // + // "submit" : [ + // { ... } + // ] + // + var menuSubmit = self.client.currentMenuModule.menuConfig.submit; + if(!_.isArray(menuSubmit)) { + self.client.log.debug('No configuration to handle submit'); + return; + } - // - // Locate matching action block - // - // :TODO: this is basically the same as for menus -- DRY it up! - for(var c = 0; c < menuSubmit.length; ++c) { - var actionBlock = menuSubmit[c]; + // + // Locate matching action block + // + // :TODO: this is basically the same as for menus -- DRY it up! + for(var c = 0; c < menuSubmit.length; ++c) { + var actionBlock = menuSubmit[c]; - if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { - self.handleActionWrapper(formData, actionBlock); - break; // there an only be one... - } - } - } - }); - } + if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { + self.handleActionWrapper(formData, actionBlock); + break; // there an only be one... + } + } + } + }); + } - callback(null); - }, - function loadActionKeys(callback) { - if(!_.isObject(promptConfig) || !_.isArray(promptConfig.actionKeys)) { - return callback(null); - } + callback(null); + }, + function loadActionKeys(callback) { + if(!_.isObject(promptConfig) || !_.isArray(promptConfig.actionKeys)) { + return callback(null); + } - promptConfig.actionKeys.forEach(ak => { - // - // * 'keys' must be present and be an array of key names - // * If 'viewId' is present, key(s) will focus & submit on behalf - // of the specified view. - // * If 'action' is present, that action will be procesed when - // triggered by key(s) - // - // Ultimately, create a map of key -> { action block } - // - if(!_.isArray(ak.keys)) { - return; - } + promptConfig.actionKeys.forEach(ak => { + // + // * 'keys' must be present and be an array of key names + // * If 'viewId' is present, key(s) will focus & submit on behalf + // of the specified view. + // * If 'action' is present, that action will be procesed when + // triggered by key(s) + // + // Ultimately, create a map of key -> { action block } + // + if(!_.isArray(ak.keys)) { + return; + } - ak.keys.forEach(kn => { - self.actionKeyMap[kn] = ak; - }); + ak.keys.forEach(kn => { + self.actionKeyMap[kn] = ak; + }); - }); + }); - return callback(null); - }, - function drawAllViews(callback) { - self.redrawAll(initialFocusId); - callback(null); - }, - function setInitialViewFocus(callback) { - if(initialFocusId) { - self.switchFocus(initialFocusId); - } - callback(null); - } - ], - function complete(err) { - cb(err); - } - ); + return callback(null); + }, + function drawAllViews(callback) { + self.redrawAll(initialFocusId); + callback(null); + }, + function setInitialViewFocus(callback) { + if(initialFocusId) { + self.switchFocus(initialFocusId); + } + callback(null); + } + ], + function complete(err) { + cb(err); + } + ); }; ViewController.prototype.loadFromMenuConfig = function(options, cb) { - assert(_.isObject(options)); + assert(_.isObject(options)); - if(!_.isObject(options.mciMap)) { - cb(new Error('Missing option: mciMap')); - return; - } + if(!_.isObject(options.mciMap)) { + cb(new Error('Missing option: mciMap')); + return; + } - var self = this; - var formIdKey = options.formId ? options.formId.toString() : '0'; - this.formInitialFocusId = 1; // default to first - var formConfig; + var self = this; + var formIdKey = options.formId ? options.formId.toString() : '0'; + this.formInitialFocusId = 1; // default to first + var formConfig; - // :TODO: honor options.withoutForm + // :TODO: honor options.withoutForm - async.waterfall( - [ - function findMatchingFormConfig(callback) { - menuUtil.getFormConfigByIDAndMap(self.client.currentMenuModule.menuConfig, formIdKey, options.mciMap, function matchingConfig(err, fc) { - formConfig = fc; + async.waterfall( + [ + function findMatchingFormConfig(callback) { + menuUtil.getFormConfigByIDAndMap(self.client.currentMenuModule.menuConfig, formIdKey, options.mciMap, function matchingConfig(err, fc) { + formConfig = fc; - if(err) { - // non-fatal - self.client.log.trace( - { reason : err.message, mci : Object.keys(options.mciMap), formId : formIdKey }, - 'Unable to find matching form configuration'); - } + if(err) { + // non-fatal + self.client.log.trace( + { reason : err.message, mci : Object.keys(options.mciMap), formId : formIdKey }, + 'Unable to find matching form configuration'); + } - callback(null); - }); - }, - function createViews(callback) { - self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { - callback(err); - }); - }, - /* + callback(null); + }); + }, + function createViews(callback) { + self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { + callback(err); + }); + }, + /* function applyThemeCustomization(callback) { formConfig = formConfig || {}; formConfig.mci = formConfig.mci || {}; @@ -709,119 +709,119 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { callback(null); }, */ - function applyViewConfiguration(callback) { - if(_.isObject(formConfig)) { - self.applyViewConfig(formConfig, function configApplied(err, info) { - self.formInitialFocusId = info.initialFocusId; - callback(err); - }); - } else { - callback(null); - } - }, - function prepareFormSubmission(callback) { - if(!_.isObject(formConfig) || !_.isObject(formConfig.submit)) { - callback(null); - return; - } + function applyViewConfiguration(callback) { + if(_.isObject(formConfig)) { + self.applyViewConfig(formConfig, function configApplied(err, info) { + self.formInitialFocusId = info.initialFocusId; + callback(err); + }); + } else { + callback(null); + } + }, + function prepareFormSubmission(callback) { + if(!_.isObject(formConfig) || !_.isObject(formConfig.submit)) { + callback(null); + return; + } - self.on('submit', function formSubmit(formData) { + self.on('submit', function formSubmit(formData) { - self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Form submit'); + self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Form submit'); - // - // Locate configuration for this form ID - // - var confForFormId; - if(_.isObject(formConfig.submit[formData.submitId])) { - confForFormId = formConfig.submit[formData.submitId]; - } else if(_.isObject(formConfig.submit['*'])) { - confForFormId = formConfig.submit['*']; - } else { - // no configuration for this submitId - self.client.log.debug( { formId : formData.submitId }, 'No configuration for form ID'); - return; - } + // + // Locate configuration for this form ID + // + var confForFormId; + if(_.isObject(formConfig.submit[formData.submitId])) { + confForFormId = formConfig.submit[formData.submitId]; + } else if(_.isObject(formConfig.submit['*'])) { + confForFormId = formConfig.submit['*']; + } else { + // no configuration for this submitId + self.client.log.debug( { formId : formData.submitId }, 'No configuration for form ID'); + return; + } - // - // Locate a matching action block based on the submitted data - // - for(var c = 0; c < confForFormId.length; ++c) { - var actionBlock = confForFormId[c]; + // + // Locate a matching action block based on the submitted data + // + for(var c = 0; c < confForFormId.length; ++c) { + var actionBlock = confForFormId[c]; - if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { - self.handleActionWrapper(formData, actionBlock); - break; // there an only be one... - } - } - }); + if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { + self.handleActionWrapper(formData, actionBlock); + break; // there an only be one... + } + } + }); - callback(null); - }, - function loadActionKeys(callback) { - if(!_.isObject(formConfig) || !_.isArray(formConfig.actionKeys)) { - callback(null); - return; - } + callback(null); + }, + function loadActionKeys(callback) { + if(!_.isObject(formConfig) || !_.isArray(formConfig.actionKeys)) { + callback(null); + return; + } - formConfig.actionKeys.forEach(function akEntry(ak) { - // - // * 'keys' must be present and be an array of key names - // * If 'viewId' is present, key(s) will focus & submit on behalf - // of the specified view. - // * If 'action' is present, that action will be procesed when - // triggered by key(s) - // - // Ultimately, create a map of key -> { action block } - // - if(!_.isArray(ak.keys)) { - return; - } + formConfig.actionKeys.forEach(function akEntry(ak) { + // + // * 'keys' must be present and be an array of key names + // * If 'viewId' is present, key(s) will focus & submit on behalf + // of the specified view. + // * If 'action' is present, that action will be procesed when + // triggered by key(s) + // + // Ultimately, create a map of key -> { action block } + // + if(!_.isArray(ak.keys)) { + return; + } - ak.keys.forEach(function actionKeyName(kn) { - self.actionKeyMap[kn] = ak; - }); + ak.keys.forEach(function actionKeyName(kn) { + self.actionKeyMap[kn] = ak; + }); - }); + }); - callback(null); - }, - function drawAllViews(callback) { - self.redrawAll(self.formInitialFocusId); - callback(null); - }, - function setInitialViewFocus(callback) { - if(self.formInitialFocusId) { - self.switchFocus(self.formInitialFocusId); - } - callback(null); - } - ], - function complete(err) { - if(_.isFunction(cb)) { - cb(err); - } - } - ); + callback(null); + }, + function drawAllViews(callback) { + self.redrawAll(self.formInitialFocusId); + callback(null); + }, + function setInitialViewFocus(callback) { + if(self.formInitialFocusId) { + self.switchFocus(self.formInitialFocusId); + } + callback(null); + } + ], + function complete(err) { + if(_.isFunction(cb)) { + cb(err); + } + } + ); }; ViewController.prototype.formatMCIString = function(format) { - var self = this; - var view; + var self = this; + var view; - return format.replace(/{(\d+)}/g, function replacer(match, number) { - view = self.getView(number); + return format.replace(/{(\d+)}/g, function replacer(match, number) { + view = self.getView(number); - if(!view) { - return match; - } + if(!view) { + return match; + } - return view.getData(); - }); + return view.getData(); + }); }; ViewController.prototype.getFormData = function(key) { - /* + /* Example form data: { id : 0, @@ -835,34 +835,34 @@ ViewController.prototype.getFormData = function(key) { } */ - const formData = { - id : this.formId, - submitId : this.focusedView.id, - value : {}, - }; + const formData = { + id : this.formId, + submitId : this.focusedView.id, + value : {}, + }; - if(key) { - formData.key = key; - } + if(key) { + formData.key = key; + } - let viewData; - _.each(this.views, view => { - try { - // don't fill forms with static, non user-editable data data - if(!view.acceptsInput) { - return; - } + let viewData; + _.each(this.views, view => { + try { + // don't fill forms with static, non user-editable data data + if(!view.acceptsInput) { + return; + } - viewData = view.getData(); - if(_.isUndefined(viewData)) { - return; - } + viewData = view.getData(); + if(_.isUndefined(viewData)) { + return; + } - formData.value[ view.submitArgName ? view.submitArgName : view.id ] = viewData; - } catch(e) { - this.client.log.error( { error : e.message }, 'Exception caught gathering form data' ); - } - }); + formData.value[ view.submitArgName ? view.submitArgName : view.id ] = viewData; + } catch(e) { + this.client.log.error( { error : e.message }, 'Exception caught gathering form data' ); + } + }); - return formData; + return formData; }; diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 2f98823c..7f30425f 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -27,293 +27,293 @@ a password reset has been requested for your account on %BOARDNAME%. `; function getWebServer() { - return getServer(webServerPackageName); + return getServer(webServerPackageName); } class WebPasswordReset { - static startup(cb) { - WebPasswordReset.registerRoutes( err => { - return cb(err); - }); - } + static startup(cb) { + WebPasswordReset.registerRoutes( err => { + return cb(err); + }); + } - static sendForgotPasswordEmail(username, cb) { - const webServer = getServer(webServerPackageName); - if(!webServer || !webServer.instance.isEnabled()) { - return cb(Errors.General('Web server is not enabled')); - } + static sendForgotPasswordEmail(username, cb) { + const webServer = getServer(webServerPackageName); + if(!webServer || !webServer.instance.isEnabled()) { + return cb(Errors.General('Web server is not enabled')); + } - async.waterfall( - [ - function getEmailAddress(callback) { - if(!username) { - return callback(Errors.MissingParam('Missing "username"')); - } + async.waterfall( + [ + function getEmailAddress(callback) { + if(!username) { + return callback(Errors.MissingParam('Missing "username"')); + } - User.getUserIdAndName(username, (err, userId) => { - if(err) { - return callback(err); - } + User.getUserIdAndName(username, (err, userId) => { + if(err) { + return callback(err); + } - User.getUser(userId, (err, user) => { - if(err || !user.properties.email_address) { - return callback(Errors.DoesNotExist('No email address associated with this user')); - } + User.getUser(userId, (err, user) => { + if(err || !user.properties.email_address) { + return callback(Errors.DoesNotExist('No email address associated with this user')); + } - return callback(null, user); - }); - }); - }, - function generateAndStoreResetToken(user, callback) { - // - // Reset "token" is simply HEX encoded cryptographically generated bytes - // - crypto.randomBytes(256, (err, token) => { - if(err) { - return callback(err); - } + return callback(null, user); + }); + }); + }, + function generateAndStoreResetToken(user, callback) { + // + // Reset "token" is simply HEX encoded cryptographically generated bytes + // + crypto.randomBytes(256, (err, token) => { + if(err) { + return callback(err); + } - token = token.toString('hex'); + token = token.toString('hex'); - const newProperties = { - email_password_reset_token : token, - email_password_reset_token_ts : getISOTimestampString(), - }; + const newProperties = { + email_password_reset_token : token, + email_password_reset_token_ts : getISOTimestampString(), + }; - // we simply place the reset token in the user's properties - user.persistProperties(newProperties, err => { - return callback(err, user); - }); - }); + // we simply place the reset token in the user's properties + user.persistProperties(newProperties, err => { + return callback(err, user); + }); + }); - }, - function getEmailTemplates(user, callback) { - const config = Config(); - fs.readFile(config.contentServers.web.resetPassword.resetPassEmailText, 'utf8', (err, textTemplate) => { - if(err) { - textTemplate = PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT; - } + }, + function getEmailTemplates(user, callback) { + const config = Config(); + fs.readFile(config.contentServers.web.resetPassword.resetPassEmailText, 'utf8', (err, textTemplate) => { + if(err) { + textTemplate = PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT; + } - fs.readFile(config.contentServers.web.resetPassword.resetPassEmailHtml, 'utf8', (err, htmlTemplate) => { - return callback(null, user, textTemplate, htmlTemplate); - }); - }); - }, - function buildAndSendEmail(user, textTemplate, htmlTemplate, callback) { - const sendMail = require('./email.js').sendMail; + fs.readFile(config.contentServers.web.resetPassword.resetPassEmailHtml, 'utf8', (err, htmlTemplate) => { + return callback(null, user, textTemplate, htmlTemplate); + }); + }); + }, + function buildAndSendEmail(user, textTemplate, htmlTemplate, callback) { + const sendMail = require('./email.js').sendMail; - const resetUrl = webServer.instance.buildUrl(`/reset_password?token=${user.properties.email_password_reset_token}`); + const resetUrl = webServer.instance.buildUrl(`/reset_password?token=${user.properties.email_password_reset_token}`); - function replaceTokens(s) { - return s - .replace(/%BOARDNAME%/g, Config().general.boardName) - .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, user.properties.email_password_reset_token) - .replace(/%RESET_URL%/g, resetUrl) - ; - } + function replaceTokens(s) { + return s + .replace(/%BOARDNAME%/g, Config().general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace(/%TOKEN%/g, user.properties.email_password_reset_token) + .replace(/%RESET_URL%/g, resetUrl) + ; + } - textTemplate = replaceTokens(textTemplate); - if(htmlTemplate) { - htmlTemplate = replaceTokens(htmlTemplate); - } + textTemplate = replaceTokens(textTemplate); + if(htmlTemplate) { + htmlTemplate = replaceTokens(htmlTemplate); + } - const message = { - to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, - // from will be filled in - subject : 'Forgot Password', - text : textTemplate, - html : htmlTemplate, - }; + const message = { + to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, + // from will be filled in + subject : 'Forgot Password', + text : textTemplate, + html : htmlTemplate, + }; - sendMail(message, (err, info) => { - if(err) { - Log.warn( { error : err.message }, 'Failed sending password reset email' ); - } else { - Log.debug( { info : info }, 'Successfully sent password reset email'); - } + sendMail(message, (err, info) => { + if(err) { + Log.warn( { error : err.message }, 'Failed sending password reset email' ); + } else { + Log.debug( { info : info }, 'Successfully sent password reset email'); + } - return callback(err); - }); - } - ], - err => { - return cb(err); - } - ); - } + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + } - static scheduleEvents(cb) { - // :TODO: schedule ~daily cleanup task - return cb(null); - } + static scheduleEvents(cb) { + // :TODO: schedule ~daily cleanup task + return cb(null); + } - static registerRoutes(cb) { - const webServer = getWebServer(); - if(!webServer) { - return cb(null); // no webserver enabled - } + static registerRoutes(cb) { + const webServer = getWebServer(); + if(!webServer) { + return cb(null); // no webserver enabled + } - if(!webServer.instance.isEnabled()) { - return cb(null); // no error, but we're not serving web stuff - } + if(!webServer.instance.isEnabled()) { + return cb(null); // no error, but we're not serving web stuff + } - [ - { - // this is the page displayed to user when they GET it - method : 'GET', - path : '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate - handler : WebPasswordReset.routeResetPasswordGet, - }, - // POST handler for performing the actual reset - { - method : 'POST', - path : '^\\/reset_password$', - handler : WebPasswordReset.routeResetPasswordPost, - } - ].forEach(r => { - webServer.instance.addRoute(r); - }); + [ + { + // this is the page displayed to user when they GET it + method : 'GET', + path : '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate + handler : WebPasswordReset.routeResetPasswordGet, + }, + // POST handler for performing the actual reset + { + method : 'POST', + path : '^\\/reset_password$', + handler : WebPasswordReset.routeResetPasswordPost, + } + ].forEach(r => { + webServer.instance.addRoute(r); + }); - return cb(null); - } + return cb(null); + } - static fileNotFound(webServer, resp) { - return webServer.instance.fileNotFound(resp); - } + static fileNotFound(webServer, resp) { + return webServer.instance.fileNotFound(resp); + } - static accessDenied(webServer, resp) { - return webServer.instance.accessDenied(resp); - } + static accessDenied(webServer, resp) { + return webServer.instance.accessDenied(resp); + } - static getUserByToken(token, cb) { - async.waterfall( - [ - function validateToken(callback) { - User.getUserIdsWithProperty('email_password_reset_token', token, (err, userIds) => { - if(userIds && userIds.length === 1) { - return callback(null, userIds[0]); - } + static getUserByToken(token, cb) { + async.waterfall( + [ + function validateToken(callback) { + User.getUserIdsWithProperty('email_password_reset_token', token, (err, userIds) => { + if(userIds && userIds.length === 1) { + return callback(null, userIds[0]); + } - return callback(Errors.Invalid('Invalid password reset token')); - }); - }, - function getUser(userId, callback) { - User.getUser(userId, (err, user) => { - return callback(null, user); - }); - }, - ], - (err, user) => { - return cb(err, user); - } - ); - } + return callback(Errors.Invalid('Invalid password reset token')); + }); + }, + function getUser(userId, callback) { + User.getUser(userId, (err, user) => { + return callback(null, user); + }); + }, + ], + (err, user) => { + return cb(err, user); + } + ); + } - static routeResetPasswordGet(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + static routeResetPasswordGet(req, resp) { + const webServer = getWebServer(); // must be valid, we just got a req! - const urlParts = url.parse(req.url, true); - const token = urlParts.query && urlParts.query.token; + const urlParts = url.parse(req.url, true); + const token = urlParts.query && urlParts.query.token; - if(!token) { - return WebPasswordReset.accessDenied(webServer, resp); - } + if(!token) { + return WebPasswordReset.accessDenied(webServer, resp); + } - WebPasswordReset.getUserByToken(token, (err, user) => { - if(err) { - // assume it's expired - return webServer.instance.respondWithError(resp, 410, 'Invalid or expired reset link.', 'Expired Link'); - } + WebPasswordReset.getUserByToken(token, (err, user) => { + if(err) { + // assume it's expired + return webServer.instance.respondWithError(resp, 410, 'Invalid or expired reset link.', 'Expired Link'); + } - const postResetUrl = webServer.instance.buildUrl('/reset_password'); + const postResetUrl = webServer.instance.buildUrl('/reset_password'); - const config = Config(); - return webServer.instance.routeTemplateFilePage( - config.contentServers.web.resetPassword.resetPageTemplate, - (templateData, preprocessFinished) => { + const config = Config(); + return webServer.instance.routeTemplateFilePage( + config.contentServers.web.resetPassword.resetPageTemplate, + (templateData, preprocessFinished) => { - const finalPage = templateData - .replace(/%BOARDNAME%/g, config.general.boardName) - .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, token) - .replace(/%RESET_URL%/g, postResetUrl) + const finalPage = templateData + .replace(/%BOARDNAME%/g, config.general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace(/%TOKEN%/g, token) + .replace(/%RESET_URL%/g, postResetUrl) ; - return preprocessFinished(null, finalPage); - }, - resp - ); - }); - } + return preprocessFinished(null, finalPage); + }, + resp + ); + }); + } - static routeResetPasswordPost(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + static routeResetPasswordPost(req, resp) { + const webServer = getWebServer(); // must be valid, we just got a req! - let bodyData = ''; - req.on('data', data => { - bodyData += data; - }); + let bodyData = ''; + req.on('data', data => { + bodyData += data; + }); - function badRequest() { - return webServer.instance.respondWithError(resp, 400, 'Bad Request.', 'Bad Request'); - } + function badRequest() { + return webServer.instance.respondWithError(resp, 400, 'Bad Request.', 'Bad Request'); + } - req.on('end', () => { - const formData = querystring.parse(bodyData); + req.on('end', () => { + const formData = querystring.parse(bodyData); - const config = Config(); - if(!formData.token || !formData.password || !formData.confirm_password || + const config = Config(); + if(!formData.token || !formData.password || !formData.confirm_password || formData.password !== formData.confirm_password || formData.password.length < config.users.passwordMin || formData.password.length > config.users.passwordMax) - { - return badRequest(); - } + { + return badRequest(); + } - WebPasswordReset.getUserByToken(formData.token, (err, user) => { - if(err) { - return badRequest(); - } + WebPasswordReset.getUserByToken(formData.token, (err, user) => { + if(err) { + return badRequest(); + } - user.setNewAuthCredentials(formData.password, err => { - if(err) { - return badRequest(); - } + user.setNewAuthCredentials(formData.password, err => { + if(err) { + return badRequest(); + } - // delete assoc properties - no need to wait for completion - user.removeProperty('email_password_reset_token'); - user.removeProperty('email_password_reset_token_ts'); + // delete assoc properties - no need to wait for completion + user.removeProperty('email_password_reset_token'); + user.removeProperty('email_password_reset_token_ts'); - resp.writeHead(200); - return resp.end('Password changed successfully'); - }); - }); - }); - } + resp.writeHead(200); + return resp.end('Password changed successfully'); + }); + }); + }); + } } function performMaintenanceTask(args, cb) { - const forgotPassExpireTime = args[0] || '24 hours'; + const forgotPassExpireTime = args[0] || '24 hours'; - // remove all reset token associated properties older than |forgotPassExpireTime| - userDb.run( - `DELETE FROM user_property + // remove all reset token associated properties older than |forgotPassExpireTime| + userDb.run( + `DELETE FROM user_property WHERE user_id IN ( SELECT user_id FROM user_property WHERE prop_name = "email_password_reset_token_ts" AND DATETIME("now") >= DATETIME(prop_value, "+${forgotPassExpireTime}") ) AND prop_name IN ("email_password_reset_token_ts", "email_password_reset_token");`, - err => { - if(err) { - Log.warn( { error : err.message }, 'Failed deleting old email reset tokens'); - } - return cb(err); - } - ); + err => { + if(err) { + Log.warn( { error : err.message }, 'Failed deleting old email reset tokens'); + } + return cb(err); + } + ); } exports.WebPasswordReset = WebPasswordReset; diff --git a/core/whos_online.js b/core/whos_online.js index f832bc10..edda6c20 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -12,73 +12,73 @@ const async = require('async'); const _ = require('lodash'); exports.moduleInfo = { - name : 'Who\'s Online', - desc : 'Who is currently online', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.whosonline' + name : 'Who\'s Online', + desc : 'Who is currently online', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.whosonline' }; const MciViewIds = { - OnlineList : 1, + OnlineList : 1, }; exports.getModule = class WhosOnlineModule extends MenuModule { - constructor(options) { - super(options); - } + constructor(options) { + super(options); + } - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if(err) { - return cb(err); - } + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - async.series( - [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, - }; + async.series( + [ + function loadFromConfig(callback) { + const loadOpts = { + callingMenu : self, + mciMap : mciData.menu, + noInput : true, + }; - return vc.loadFromMenuConfig(loadOpts, callback); - }, - function populateList(callback) { - const onlineListView = vc.getView(MciViewIds.OnlineList); - const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}'; - const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In'; - const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A'; - const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height); + return vc.loadFromMenuConfig(loadOpts, callback); + }, + function populateList(callback) { + const onlineListView = vc.getView(MciViewIds.OnlineList); + const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}'; + const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In'; + const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A'; + const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height); - onlineListView.setItems(_.map(onlineList, oe => { - if(oe.authenticated) { - oe.timeOn = _.upperFirst(oe.timeOn.humanize()); - } else { - [ 'realName', 'location', 'affils', 'timeOn' ].forEach(m => { - oe[m] = otherUnknown; - }); - oe.userName = nonAuthUser; - } - return stringFormat(listFormat, oe); - })); + onlineListView.setItems(_.map(onlineList, oe => { + if(oe.authenticated) { + oe.timeOn = _.upperFirst(oe.timeOn.humanize()); + } else { + [ 'realName', 'location', 'affils', 'timeOn' ].forEach(m => { + oe[m] = otherUnknown; + }); + oe.userName = nonAuthUser; + } + return stringFormat(listFormat, oe); + })); - onlineListView.focusItems = onlineListView.items; - onlineListView.redraw(); + onlineListView.focusItems = onlineListView.items; + onlineListView.redraw(); - return callback(null); - } - ], - function complete(err) { - if(err) { - self.client.log.error( { error : err.message }, 'Error loading who\'s online'); - } - return cb(err); - } - ); - }); - } + return callback(null); + } + ], + function complete(err) { + if(err) { + self.client.log.error( { error : err.message }, 'Error loading who\'s online'); + } + return cb(err); + } + ); + }); + } }; diff --git a/core/word_wrap.js b/core/word_wrap.js index ecb728a5..a42dd2ea 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -10,94 +10,94 @@ const _ = require('lodash'); exports.wordWrapText = wordWrapText; const SPACE_CHARS = [ - ' ', '\f', '\n', '\r', '\v', - '​\u00a0', '\u1680', '​\u180e', '\u2000​', '\u2001', '\u2002', '​\u2003', '\u2004', - '\u2005', '\u2006​', '\u2007', '\u2008​', '\u2009', '\u200a​', '\u2028', '\u2029​', - '\u202f', '\u205f​', '\u3000', + ' ', '\f', '\n', '\r', '\v', + '​\u00a0', '\u1680', '​\u180e', '\u2000​', '\u2001', '\u2002', '​\u2003', '\u2004', + '\u2005', '\u2006​', '\u2007', '\u2008​', '\u2009', '\u200a​', '\u2028', '\u2029​', + '\u202f', '\u205f​', '\u3000', ]; const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); function wordWrapText(text, options) { - assert(_.isObject(options)); - assert(_.isNumber(options.width)); + assert(_.isObject(options)); + assert(_.isNumber(options.width)); - options.tabHandling = options.tabHandling || 'expand'; - options.tabWidth = options.tabWidth || 4; - options.tabChar = options.tabChar || ' '; + options.tabHandling = options.tabHandling || 'expand'; + options.tabWidth = options.tabWidth || 4; + options.tabChar = options.tabChar || ' '; - //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'); + //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 word; - let c; - let renderLen; - let i = 0; - let wordStart = 0; - let result = { wrapped : [ '' ], renderLen : [ 0 ] }; + let m; + let word; + let c; + let renderLen; + let i = 0; + let wordStart = 0; + let result = { wrapped : [ '' ], renderLen : [ 0 ] }; - function expandTab(column) { - const remainWidth = options.tabWidth - (column % options.tabWidth); - return new Array(remainWidth).join(options.tabChar); - } + function expandTab(column) { + const remainWidth = options.tabWidth - (column % options.tabWidth); + return new Array(remainWidth).join(options.tabChar); + } - function appendWord() { - word.match(REGEXP_GOBBLE).forEach( w => { - renderLen = renderStringLength(w); + function appendWord() { + word.match(REGEXP_GOBBLE).forEach( w => { + renderLen = renderStringLength(w); - if(result.renderLen[i] + renderLen > options.width) { - if(0 === i) { - result.firstWrapRange = { start : wordStart, end : wordStart + w.length }; - } + if(result.renderLen[i] + renderLen > options.width) { + if(0 === i) { + result.firstWrapRange = { start : wordStart, end : wordStart + w.length }; + } - result.wrapped[++i] = w; - result.renderLen[i] = renderLen; - } else { - result.wrapped[i] += w; - result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; - } - }); - } + result.wrapped[++i] = w; + result.renderLen[i] = renderLen; + } else { + result.wrapped[i] += w; + result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; + } + }); + } - // - // Some of the way we word wrap is modeled after Sublime Test 3: - // - // * Sublime Text 3 for example considers spaces after a word - // part of said word. For example, "word " would be wraped - // in it's entirity. - // - // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. - // "\t" may resolve to " " and must fit within the space. - // - // * If a word is ultimately too long to fit, break it up until it does. - // - while(null !== (m = REGEXP_WORD_WRAP.exec(text))) { - word = text.substring(wordStart, REGEXP_WORD_WRAP.lastIndex - 1); + // + // Some of the way we word wrap is modeled after Sublime Test 3: + // + // * Sublime Text 3 for example considers spaces after a word + // part of said word. For example, "word " would be wraped + // in it's entirity. + // + // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. + // "\t" may resolve to " " and must fit within the space. + // + // * If a word is ultimately too long to fit, break it up until it does. + // + while(null !== (m = REGEXP_WORD_WRAP.exec(text))) { + word = text.substring(wordStart, REGEXP_WORD_WRAP.lastIndex - 1); - c = m[0].charAt(0); - if(SPACE_CHARS.indexOf(c) > -1) { - word += m[0]; - } else if('\t' === c) { - if('expand' === options.tabHandling) { - // Good info here: http://c-for-dummies.com/blog/?p=424 - word += expandTab(result.wrapped[i].length + word.length) + options.tabChar; - } else { - word += m[0]; - } - } + c = m[0].charAt(0); + if(SPACE_CHARS.indexOf(c) > -1) { + word += m[0]; + } else if('\t' === c) { + if('expand' === options.tabHandling) { + // Good info here: http://c-for-dummies.com/blog/?p=424 + word += expandTab(result.wrapped[i].length + word.length) + options.tabChar; + } else { + word += m[0]; + } + } - appendWord(); - wordStart = REGEXP_WORD_WRAP.lastIndex + m[0].length - 1; - } + appendWord(); + wordStart = REGEXP_WORD_WRAP.lastIndex + m[0].length - 1; + } - word = text.substring(wordStart); - appendWord(); + word = text.substring(wordStart); + appendWord(); - return result; + return result; } From a4e10f5ba5d746ad42eada126fd0782f109e0481 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 Jun 2018 23:35:52 -0600 Subject: [PATCH 152/569] Add .eslintignore, tidy up a bit --- .eslintignore | 2 ++ core/scanner_tossers/ftn_bso.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..b1342397 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +# ACS parser is generated +core/acs_parser.js diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index c5fa3f57..98fece9b 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -51,7 +51,7 @@ exports.moduleInfo = { exports.getModule = FTNMessageScanTossModule; -const SCHEDULE_REGEXP = /(?:^|or )?(@watch\:|@immediate)([^\0]+)?$/; +const SCHEDULE_REGEXP = /(?:^|or )?(@watch:|@immediate)([^\0]+)?$/; function FTNMessageScanTossModule() { MessageScanTossModule.call(this); @@ -1569,7 +1569,7 @@ function FTNMessageScanTossModule() { // // All extracted - import .pkt's // - self.importPacketFilesFromDirectory(self.importTempDir, '', err => { + self.importPacketFilesFromDirectory(self.importTempDir, '', () => { // :TODO: handle |err| callback(null, bundleFiles, rejects); }); From c3635bb26b50c40f57557fe5a6eb652d2361b34e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 22 Jun 2018 20:48:36 -0600 Subject: [PATCH 153/569] More tabs to spaces.. --- core/bbs.js | 62 ++++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 597a0b97..86134c38 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -5,30 +5,30 @@ //var SegfaultHandler = require('segfault-handler'); //SegfaultHandler.registerHandler('enigma-bbs-segfault.log'); -// ENiGMA½ -const conf = require('./config.js'); -const logger = require('./logger.js'); -const database = require('./database.js'); -const resolvePath = require('./misc_util.js').resolvePath; +// ENiGMA½ +const conf = require('./config.js'); +const logger = require('./logger.js'); +const database = require('./database.js'); +const resolvePath = require('./misc_util.js').resolvePath; -// deps -const async = require('async'); -const util = require('util'); -const _ = require('lodash'); -const mkdirs = require('fs-extra').mkdirs; -const fs = require('graceful-fs'); -const paths = require('path'); +// deps +const async = require('async'); +const util = require('util'); +const _ = require('lodash'); +const mkdirs = require('fs-extra').mkdirs; +const fs = require('graceful-fs'); +const paths = require('path'); -// our main entry point -exports.main = main; +// our main entry point +exports.main = main; -// object with various services we want to de-init/shutdown cleanly if possible +// object with various services we want to de-init/shutdown cleanly if possible const initServices = {}; -// only include bbs.js once @ startup; this should be fine +// only include bbs.js once @ startup; this should be fine const COPYRIGHT = fs.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8').split(/\r?\n/g)[0]; -const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`; +const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`; const HELP = `${FULL_COPYRIGHT} usage: main.js @@ -64,15 +64,15 @@ function main() { conf.init(resolvePath(configFile), function configInit(err) { // - // If the user supplied a path and we can't read/parse it - // then it's a fatal error + // If the user supplied a path and we can't read/parse it + // then it's a fatal error // if(err) { if('ENOENT' === err.code) { if(configPathSupplied) { console.error('Configuration file does not exist: ' + configFile); } else { - configPathSupplied = null; // make non-fatal; we'll go with defaults + configPathSupplied = null; // make non-fatal; we'll go with defaults } } else { console.error(err.toString()); @@ -91,7 +91,7 @@ function main() { } ], function complete(err) { - // note this is escaped: + // note this is escaped: fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { console.info(FULL_COPYRIGHT); if(!err) { @@ -129,13 +129,13 @@ function shutdownSystem() { }, function stopListeningServers(callback) { return require('./listening_server.js').shutdown( () => { - return callback(null); // ignore err + return callback(null); // ignore err }); }, function stopEventScheduler(callback) { if(initServices.eventScheduler) { return initServices.eventScheduler.shutdown( () => { - return callback(null); // ignore err + return callback(null); // ignore err }); } else { return callback(null); @@ -143,7 +143,7 @@ function shutdownSystem() { }, function stopFileAreaWeb(callback) { require('./file_area_web.js').startup( () => { - return callback(null); // ignore err + return callback(null); // ignore err }); }, function stopMsgNetwork(callback) { @@ -180,7 +180,7 @@ function initialize(cb) { process.on('SIGINT', shutdownSystem); - require('later').date.localTime(); // use local times for later.js/scheduling + require('later').date.localTime(); // use local times for later.js/scheduling return callback(null); }, @@ -197,7 +197,7 @@ function initialize(cb) { return require('./config_util.js').init(callback); }, function initThemes(callback) { - // Have to pull in here so it's after Config init + // Have to pull in here so it's after Config init require('./theme.js').initAvailableThemes( (err, themeCount) => { logger.log.info({ themeCount }, 'Themes initialized'); return callback(err); @@ -205,10 +205,10 @@ function initialize(cb) { }, function loadSysOpInformation(callback) { // - // Copy over some +op information from the user DB -> system propertys. - // * Makes this accessible for MCI codes, easy non-blocking access, etc. - // * We do this every time as the op is free to change this information just - // like any other user + // Copy over some +op information from the user DB -> system propertys. + // * Makes this accessible for MCI codes, easy non-blocking access, etc. + // * We do this every time as the op is free to change this information just + // like any other user // const User = require('./user.js'); @@ -219,7 +219,7 @@ function initialize(cb) { }, function getOpProps(opUserName, next) { const propLoadOpts = { - names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], + names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], }; User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { return next(err, opUserName, opProps); From 1d8be6b014cd7ee71336e98f6af2f7a79cb2b9e5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 22 Jun 2018 21:26:46 -0600 Subject: [PATCH 154/569] Pardon the noise. More tab to space conversion! --- core/abracadabra.js | 148 ++--- core/acs.js | 30 +- core/ansi_escape_parser.js | 304 ++++----- core/ansi_prep.js | 76 +-- core/ansi_term.js | 458 +++++++------- core/archive_util.js | 98 +-- core/art.js | 172 ++--- core/asset.js | 38 +- core/bbs_link.js | 110 ++-- core/bbs_list.js | 162 ++--- core/button_view.js | 26 +- core/client.js | 414 ++++++------ core/client_connections.js | 70 +-- core/client_term.js | 100 +-- core/color_codes.js | 244 +++---- core/combatnet.js | 34 +- core/conf_area_util.js | 14 +- core/config.js | 824 ++++++++++++------------ core/config_cache.js | 12 +- core/config_util.js | 20 +- core/connect.js | 68 +- core/crc.js | 8 +- core/database.js | 350 +++++------ core/descript_ion_file.js | 32 +- core/door.js | 72 +-- core/door_party.js | 56 +- core/download_queue.js | 20 +- core/dropfile.js | 246 ++++---- core/edit_text_view.js | 34 +- core/email.js | 18 +- core/enig_error.js | 40 +- core/enigma_assert.js | 10 +- core/erc_client.js | 66 +- core/event_scheduler.js | 92 +-- core/events.js | 20 +- core/exodus.js | 160 ++--- core/file_area_filter_edit.js | 108 ++-- core/file_area_list.js | 264 ++++---- core/file_area_web.js | 154 ++--- core/file_base_area.js | 330 +++++----- core/file_base_area_select.js | 32 +- core/file_base_download_manager.js | 78 +-- core/file_base_filter.js | 28 +- core/file_base_list_export.js | 198 +++--- core/file_base_search.js | 62 +- core/file_base_user_list_export.js | 146 ++--- core/file_base_web_download_manager.js | 88 +-- core/file_entry.js | 250 ++++---- core/file_transfer.js | 246 ++++---- core/file_transfer_protocol_select.js | 60 +- core/file_util.js | 44 +- core/fnv1a.js | 8 +- core/fse.js | 418 ++++++------ core/ftn_address.js | 66 +- core/ftn_mail_packet.js | 590 ++++++++--------- core/ftn_util.js | 392 ++++++------ core/horizontal_menu_view.js | 36 +- core/key_entry_view.js | 20 +- core/last_callers.js | 74 +-- core/listening_server.js | 24 +- core/logger.js | 30 +- core/login_server_module.js | 30 +- core/mail_packet.js | 18 +- core/mail_util.js | 34 +- core/mask_edit_text_view.js | 78 +-- core/mci_view_factory.js | 112 ++-- core/menu_module.js | 126 ++-- core/menu_stack.js | 62 +- core/menu_util.js | 78 +-- core/menu_view.js | 68 +- core/message.js | 440 ++++++------- core/message_area.js | 216 +++---- core/message_base_search.js | 68 +- core/mime_util.js | 24 +- core/misc_util.js | 24 +- core/mod_mixins.js | 10 +- core/module_util.js | 38 +- core/msg_area_list.js | 84 +-- core/msg_area_post_fse.js | 20 +- core/msg_area_reply_fse.js | 10 +- core/msg_area_view_fse.js | 76 +-- core/msg_conf_list.js | 60 +- core/msg_list.js | 124 ++-- core/msg_network.js | 20 +- core/msg_scan_toss_module.js | 6 +- core/multi_line_edit_text_view.js | 462 +++++++------- core/new_scan.js | 102 +-- core/nua.js | 72 +-- core/onelinerz.js | 144 ++--- core/plugin_module.js | 2 +- core/predefined_mci.js | 222 +++---- core/rumorz.js | 90 +-- core/sauce.js | 150 ++--- core/scanner_tossers/ftn_bso.js | 838 ++++++++++++------------- core/server_module.js | 4 +- core/servers/content/gopher.js | 144 ++--- core/servers/content/web.js | 100 +-- core/servers/login/ssh.js | 96 +-- core/servers/login/telnet.js | 468 +++++++------- core/servers/login/websocket.js | 88 +-- core/set_newscan_date.js | 122 ++-- core/show_art.js | 46 +- core/spinner_menu_view.js | 30 +- core/standard_menu.js | 10 +- core/stat_log.js | 108 ++-- core/string_format.js | 148 ++--- core/string_util.js | 248 ++++---- core/system_events.js | 34 +- core/system_menu_method.js | 78 +-- core/system_view_validate.js | 64 +- core/telnet_bridge.js | 82 +-- core/text_view.js | 184 +++--- core/theme.js | 164 ++--- core/tic_file_info.js | 106 ++-- core/toggle_menu_view.js | 28 +- core/upload.js | 230 +++---- core/user.js | 162 ++--- core/user_config.js | 100 +-- core/user_group.js | 24 +- core/user_list.js | 68 +- core/user_login.js | 52 +- core/uuid_util.js | 18 +- core/vertical_menu_view.js | 90 +-- core/view.js | 136 ++-- core/view_controller.js | 276 ++++---- core/web_password_reset.js | 128 ++-- core/whos_online.js | 46 +- core/word_wrap.js | 52 +- 128 files changed, 8017 insertions(+), 8017 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 77a5e4c3..3a1507b6 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -1,62 +1,62 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; -const DropFile = require('./dropfile.js').DropFile; -const door = require('./door.js'); -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const DropFile = require('./dropfile.js').DropFile; +const door = require('./door.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); -const async = require('async'); -const assert = require('assert'); -const paths = require('path'); -const _ = require('lodash'); -const mkdirs = require('fs-extra').mkdirs; +const async = require('async'); +const assert = require('assert'); +const paths = require('path'); +const _ = require('lodash'); +const mkdirs = require('fs-extra').mkdirs; -// :TODO: This should really be a system module... needs a little work to allow for such +// :TODO: This should really be a system module... needs a little work to allow for such const activeDoorNodeInstances = {}; exports.moduleInfo = { - name : 'Abracadabra', - desc : 'External BBS Door Module', - author : 'NuSkooler', + name : 'Abracadabra', + desc : 'External BBS Door Module', + author : 'NuSkooler', }; /* - Example configuration for LORD under DOSEMU: + Example configuration for LORD under DOSEMU: - { - config: { - name: PimpWars - dropFileType: DORINFO - cmd: qemu-system-i386 - args: [ - "-localtime", - "freedos.img", - "-chardev", - "socket,port={srvPort},nowait,host=localhost,id=s0", - "-device", - "isa-serial,chardev=s0" - ] - io: socket - } - } + { + config: { + name: PimpWars + dropFileType: DORINFO + cmd: qemu-system-i386 + args: [ + "-localtime", + "freedos.img", + "-chardev", + "socket,port={srvPort},nowait,host=localhost,id=s0", + "-device", + "isa-serial,chardev=s0" + ] + io: socket + } + } - listen: socket | stdio + listen: socket | stdio - { - "config" : { - "name" : "LORD", - "dropFileType" : "DOOR", - "cmd" : "/usr/bin/dosemu", - "args" : [ "-quiet", "-f", "/etc/dosemu/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ] ], - "nodeMax" : 32, - "tooManyArt" : "toomany-lord.ans" - } - } + { + "config" : { + "name" : "LORD", + "dropFileType" : "DOOR", + "cmd" : "/usr/bin/dosemu", + "args" : [ "-quiet", "-f", "/etc/dosemu/dosemu.conf", "X:\\PW\\START.BAT {dropfile} {node}" ] ], + "nodeMax" : 32, + "tooManyArt" : "toomany-lord.ans" + } + } - :TODO: See Mystic & others for other arg options that we may need to support + :TODO: See Mystic & others for other arg options that we may need to support */ exports.getModule = class AbracadabraModule extends MenuModule { @@ -64,21 +64,21 @@ exports.getModule = class AbracadabraModule extends MenuModule { super(options); this.config = options.menuConfig.config; - // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } - assert(_.isString(this.config.name, 'Config \'name\' is required')); - assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); - assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); + // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } + assert(_.isString(this.config.name, 'Config \'name\' is required')); + assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); + assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); - this.config.nodeMax = this.config.nodeMax || 0; - this.config.args = this.config.args || []; + this.config.nodeMax = this.config.nodeMax || 0; + this.config.args = this.config.args || []; } /* - :TODO: - * disconnecting wile door is open leaves dosemu - * http://bbslink.net/sysop.php support - * Font support ala all other menus... or does this just work? - */ + :TODO: + * disconnecting wile door is open leaves dosemu + * http://bbslink.net/sysop.php support + * Font support ala all other menus... or does this just work? + */ initSequence() { const self = this; @@ -87,12 +87,12 @@ exports.getModule = class AbracadabraModule extends MenuModule { [ function validateNodeCount(callback) { if(self.config.nodeMax > 0 && - _.isNumber(activeDoorNodeInstances[self.config.name]) && - activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) + _.isNumber(activeDoorNodeInstances[self.config.name]) && + activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) { self.client.log.info( { - name : self.config.name, + name : self.config.name, activeCount : activeDoorNodeInstances[self.config.name] }, 'Too many active instances'); @@ -106,13 +106,13 @@ exports.getModule = class AbracadabraModule extends MenuModule { } else { self.client.term.write('\nToo many active instances. Try again later.\n'); - // :TODO: Use MenuModule.pausePrompt() + // :TODO: Use MenuModule.pausePrompt() self.pausePrompt( () => { callback(new Error('Too many active instances')); }); } } else { - // :TODO: JS elegant way to do this? + // :TODO: JS elegant way to do this? if(activeDoorNodeInstances[self.config.name]) { activeDoorNodeInstances[self.config.name] += 1; } else { @@ -123,8 +123,8 @@ exports.getModule = class AbracadabraModule extends MenuModule { } }, function generateDropfile(callback) { - self.dropFile = new DropFile(self.client, self.config.dropFileType); - var fullPath = self.dropFile.fullPath; + self.dropFile = new DropFile(self.client, self.config.dropFileType); + var fullPath = self.dropFile.fullPath; mkdirs(paths.dirname(fullPath), function dirCreated(err) { if(err) { @@ -152,28 +152,28 @@ exports.getModule = class AbracadabraModule extends MenuModule { runDoor() { const exeInfo = { - cmd : this.config.cmd, - args : this.config.args, - io : this.config.io || 'stdio', - encoding : this.config.encoding || this.client.term.outputEncoding, - dropFile : this.dropFile.fileName, - node : this.client.node, - //inhSocket : this.client.output._handle.fd, + cmd : this.config.cmd, + args : this.config.args, + io : this.config.io || 'stdio', + encoding : this.config.encoding || this.client.term.outputEncoding, + dropFile : this.dropFile.fileName, + node : this.client.node, + //inhSocket : this.client.output._handle.fd, }; const doorInstance = new door.Door(this.client, exeInfo); doorInstance.once('finished', () => { // - // Try to clean up various settings such as scroll regions that may - // have been set within the door + // Try to clean up various settings such as scroll regions that may + // have been set within the door // this.client.term.rawWrite( ansi.normal() + - ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + - ansi.setScrollRegion() + - ansi.goto(this.client.term.termHeight, 0) + - '\r\n\r\n' + ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + + ansi.setScrollRegion() + + ansi.goto(this.client.term.termHeight, 0) + + '\r\n\r\n' ); this.prevMenu(); diff --git a/core/acs.js b/core/acs.js index b1461400..66f13a08 100644 --- a/core/acs.js +++ b/core/acs.js @@ -1,13 +1,13 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const checkAcs = require('./acs_parser.js').parse; -const Log = require('./logger.js').log; +// ENiGMA½ +const checkAcs = require('./acs_parser.js').parse; +const Log = require('./logger.js').log; -// deps -const assert = require('assert'); -const _ = require('lodash'); +// deps +const assert = require('assert'); +const _ = require('lodash'); class ACS { constructor(client) { @@ -26,7 +26,7 @@ class ACS { } // - // Message Conferences & Areas + // Message Conferences & Areas // hasMessageConfRead(conf) { return this.check(conf.acs, 'read', ACS.Defaults.MessageConfRead); @@ -37,7 +37,7 @@ class ACS { } // - // File Base / Areas + // File Base / Areas // hasFileAreaRead(area) { return this.check(area.acs, 'read', ACS.Defaults.FileAreaRead); @@ -53,7 +53,7 @@ class ACS { getConditionalValue(condArray, memberName) { if(!Array.isArray(condArray)) { - // no cond array, just use the value + // no cond array, just use the value return condArray; } @@ -68,7 +68,7 @@ class ACS { return false; } } else { - return true; // no acs check req. + return true; // no acs check req. } }); @@ -79,12 +79,12 @@ class ACS { } ACS.Defaults = { - MessageAreaRead : 'GM[users]', - MessageConfRead : 'GM[users]', + MessageAreaRead : 'GM[users]', + MessageConfRead : 'GM[users]', - FileAreaRead : 'GM[users]', - FileAreaWrite : 'GM[sysops]', - FileAreaDownload : 'GM[users]', + FileAreaRead : 'GM[users]', + FileAreaWrite : 'GM[sysops]', + FileAreaDownload : 'GM[users]', }; module.exports = ACS; diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 49001363..9786ea4c 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -const miscUtil = require('./misc_util.js'); -const ansi = require('./ansi_term.js'); -const Log = require('./logger.js').log; +const miscUtil = require('./misc_util.js'); +const ansi = require('./ansi_term.js'); +const Log = require('./logger.js').log; -// deps -const events = require('events'); -const util = require('util'); -const _ = require('lodash'); +// deps +const events = require('events'); +const util = require('util'); +const _ = require('lodash'); -exports.ANSIEscapeParser = ANSIEscapeParser; +exports.ANSIEscapeParser = ANSIEscapeParser; const CR = 0x0d; const LF = 0x0a; @@ -20,76 +20,76 @@ function ANSIEscapeParser(options) { events.EventEmitter.call(this); - this.column = 1; - this.row = 1; - this.scrollBack = 0; - this.graphicRendition = {}; + this.column = 1; + this.row = 1; + this.scrollBack = 0; + this.graphicRendition = {}; this.parseState = { - re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex }; options = miscUtil.valueWithDefault(options, { - mciReplaceChar : '', - termHeight : 25, - termWidth : 80, - trailingLF : 'default', // default|omit|no|yes, ... + mciReplaceChar : '', + termHeight : 25, + termWidth : 80, + trailingLF : 'default', // default|omit|no|yes, ... }); - this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); - this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); - this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); - this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); + this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); + this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); + this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); + this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); self.moveCursor = function(cols, rows) { - self.column += cols; - self.row += rows; + self.column += cols; + self.row += rows; - self.column = Math.max(self.column, 1); - self.column = Math.min(self.column, self.termWidth); // can't move past term width - self.row = Math.max(self.row, 1); + self.column = Math.max(self.column, 1); + self.column = Math.min(self.column, self.termWidth); // can't move past term width + self.row = Math.max(self.row, 1); self.positionUpdated(); }; self.saveCursorPosition = function() { self.savedPosition = { - row : self.row, - column : self.column + row : self.row, + column : self.column }; }; self.restoreCursorPosition = function() { - self.row = self.savedPosition.row; - self.column = self.savedPosition.column; + self.row = self.savedPosition.row; + self.column = self.savedPosition.column; delete self.savedPosition; self.positionUpdated(); - // self.rowUpdated(); + // self.rowUpdated(); }; self.clearScreen = function() { - // :TODO: should be doing something with row/column? + // :TODO: should be doing something with row/column? self.emit('clear screen'); }; /* - self.rowUpdated = function() { - self.emit('row update', self.row + self.scrollBack); - };*/ + self.rowUpdated = function() { + self.emit('row update', self.row + self.scrollBack); + };*/ self.positionUpdated = function() { self.emit('position update', self.row, self.column); }; function literal(text) { - const len = text.length; - let pos = 0; - let start = 0; + const len = text.length; + let pos = 0; + let start = 0; let charCode; while(pos < len) { - charCode = text.charCodeAt(pos) & 0xff; // 8bit clean + charCode = text.charCodeAt(pos) & 0xff; // 8bit clean switch(charCode) { case CR : @@ -116,7 +116,7 @@ function ANSIEscapeParser(options) { start = pos + 1; self.column = 1; - self.row += 1; + self.row += 1; self.positionUpdated(); } else { @@ -129,11 +129,11 @@ function ANSIEscapeParser(options) { } // - // Finalize this chunk + // Finalize this chunk // if(self.column > self.termWidth) { self.column = 1; - self.row += 1; + self.row += 1; self.positionUpdated(); } @@ -145,7 +145,7 @@ function ANSIEscapeParser(options) { } function parseMCI(buffer) { - // :TODO: move this to "constants" seciton @ top + // :TODO: move this to "constants" seciton @ top var mciRe = /%([A-Z]{2})([0-9]{1,2})?(?:\(([0-9A-Za-z,]+)\))*/g; var pos = 0; var match; @@ -154,16 +154,16 @@ function ANSIEscapeParser(options) { var id; do { - pos = mciRe.lastIndex; - match = mciRe.exec(buffer); + pos = mciRe.lastIndex; + match = mciRe.exec(buffer); if(null !== match) { if(match.index > pos) { literal(buffer.slice(pos, match.index)); } - mciCode = match[1]; - id = match[2] || null; + mciCode = match[1]; + id = match[2] || null; if(match[3]) { args = match[3].split(','); @@ -171,7 +171,7 @@ function ANSIEscapeParser(options) { args = []; } - // if MCI codes are changing, save off the current color + // if MCI codes are changing, save off the current color var fullMciCode = mciCode + (id || ''); if(self.lastMciCode !== fullMciCode) { @@ -182,10 +182,10 @@ function ANSIEscapeParser(options) { self.emit('mci', { - mci : mciCode, - id : id ? parseInt(id, 10) : null, - args : args, - SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + mci : mciCode, + id : id ? parseInt(id, 10) : null, + args : args, + SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) }); if(self.mciReplaceChar.length > 0) { @@ -208,10 +208,10 @@ function ANSIEscapeParser(options) { self.reset = function(input) { self.parseState = { - // ignore anything past EOF marker, if any - buffer : input.split(String.fromCharCode(0x1a), 1)[0], - re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex - stop : false, + // ignore anything past EOF marker, if any + buffer : input.split(String.fromCharCode(0x1a), 1)[0], + re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex + stop : false, }; }; @@ -224,13 +224,13 @@ function ANSIEscapeParser(options) { self.reset(input); } - // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. + // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. var pos; var match; var opCode; var args; - var re = self.parseState.re; - var buffer = self.parseState.buffer; + var re = self.parseState.re; + var buffer = self.parseState.buffer; self.parseState.stop = false; @@ -239,16 +239,16 @@ function ANSIEscapeParser(options) { return; } - pos = re.lastIndex; - match = re.exec(buffer); + pos = re.lastIndex; + match = re.exec(buffer); if(null !== match) { if(match.index > pos) { parseMCI(buffer.slice(pos, match.index)); } - opCode = match[2]; - args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints + opCode = match[2]; + args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints escape(opCode, args); @@ -260,13 +260,13 @@ function ANSIEscapeParser(options) { if(pos < buffer.length) { var lastBit = buffer.slice(pos); - // :TODO: check for various ending LF's, not just DOS \r\n + // :TODO: check for various ending LF's, not just DOS \r\n if('\r\n' === lastBit.slice(-2).toString()) { switch(self.trailingLF) { case 'default' : // - // Default is to *not* omit the trailing LF - // if we're going to end on termHeight + // Default is to *not* omit the trailing LF + // if we're going to end on termHeight // if(this.termHeight === self.row) { lastBit = lastBit.slice(0, -2); @@ -288,100 +288,100 @@ function ANSIEscapeParser(options) { }; /* - self.parse = function(buffer, savedRe) { - // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. - // :TODO: move this to "constants" section @ top - var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g; - var pos = 0; - var match; - var opCode; - var args; + self.parse = function(buffer, savedRe) { + // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. + // :TODO: move this to "constants" section @ top + var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g; + var pos = 0; + var match; + var opCode; + var args; - // ignore anything past EOF marker, if any - buffer = buffer.split(String.fromCharCode(0x1a), 1)[0]; + // ignore anything past EOF marker, if any + buffer = buffer.split(String.fromCharCode(0x1a), 1)[0]; - do { - pos = re.lastIndex; - match = re.exec(buffer); + do { + pos = re.lastIndex; + match = re.exec(buffer); - if(null !== match) { - if(match.index > pos) { - parseMCI(buffer.slice(pos, match.index)); - } + if(null !== match) { + if(match.index > pos) { + parseMCI(buffer.slice(pos, match.index)); + } - opCode = match[2]; - args = getArgArray(match[1].split(';')); + opCode = match[2]; + args = getArgArray(match[1].split(';')); - escape(opCode, args); + escape(opCode, args); - self.emit('chunk', match[0]); - } + self.emit('chunk', match[0]); + } - } while(0 !== re.lastIndex); + } while(0 !== re.lastIndex); - if(pos < buffer.length) { - parseMCI(buffer.slice(pos)); - } + if(pos < buffer.length) { + parseMCI(buffer.slice(pos)); + } - self.emit('complete'); - }; - */ + self.emit('complete'); + }; + */ function escape(opCode, args) { let arg; switch(opCode) { - // cursor up + // cursor up case 'A' : //arg = args[0] || 1; arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(0, -arg); break; - // cursor down + // cursor down case 'B' : //arg = args[0] || 1; arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(0, arg); break; - // cursor forward/right + // cursor forward/right case 'C' : //arg = args[0] || 1; arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(arg, 0); break; - // cursor back/left + // cursor back/left case 'D' : //arg = args[0] || 1; arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(-arg, 0); break; - case 'f' : // horiz & vertical - case 'H' : // cursor position - //self.row = args[0] || 1; - //self.column = args[1] || 1; - self.row = isNaN(args[0]) ? 1 : args[0]; - self.column = isNaN(args[1]) ? 1 : args[1]; + case 'f' : // horiz & vertical + case 'H' : // cursor position + //self.row = args[0] || 1; + //self.column = args[1] || 1; + self.row = isNaN(args[0]) ? 1 : args[0]; + self.column = isNaN(args[1]) ? 1 : args[1]; //self.rowUpdated(); self.positionUpdated(); break; - // save position + // save position case 's' : self.saveCursorPosition(); break; - // restore position + // restore position case 'u' : self.restoreCursorPosition(); break; - // set graphic rendition + // set graphic rendition case 'm' : self.graphicRendition.reset = false; @@ -395,7 +395,7 @@ function ANSIEscapeParser(options) { } else if(ANSIEscapeParser.styles[arg]) { switch(arg) { case 0 : - // clear out everything + // clear out everything delete self.graphicRendition.intensity; delete self.graphicRendition.underline; delete self.graphicRendition.blink; @@ -445,13 +445,13 @@ function ANSIEscapeParser(options) { } self.emit('sgr update', self.graphicRendition); - break; // m + break; // m - // :TODO: s, u, K + // :TODO: s, u, K - // erase display/screen + // erase display/screen case 'J' : - // :TODO: Handle other 'J' types! + // :TODO: Handle other 'J' types! if(2 === args[0]) { self.clearScreen(); } @@ -463,62 +463,62 @@ function ANSIEscapeParser(options) { util.inherits(ANSIEscapeParser, events.EventEmitter); ANSIEscapeParser.foregroundColors = { - 30 : 'black', - 31 : 'red', - 32 : 'green', - 33 : 'yellow', - 34 : 'blue', - 35 : 'magenta', - 36 : 'cyan', - 37 : 'white', - 39 : 'default', // same as white for most implementations + 30 : 'black', + 31 : 'red', + 32 : 'green', + 33 : 'yellow', + 34 : 'blue', + 35 : 'magenta', + 36 : 'cyan', + 37 : 'white', + 39 : 'default', // same as white for most implementations - 90 : 'grey' + 90 : 'grey' }; Object.freeze(ANSIEscapeParser.foregroundColors); ANSIEscapeParser.backgroundColors = { - 40 : 'black', - 41 : 'red', - 42 : 'green', - 43 : 'yellow', - 44 : 'blue', - 45 : 'magenta', - 46 : 'cyan', - 47 : 'white', - 49 : 'default', // same as black for most implementations + 40 : 'black', + 41 : 'red', + 42 : 'green', + 43 : 'yellow', + 44 : 'blue', + 45 : 'magenta', + 46 : 'cyan', + 47 : 'white', + 49 : 'default', // same as black for most implementations }; Object.freeze(ANSIEscapeParser.backgroundColors); -// :TODO: ensure these names all align with that of ansi_term.js +// :TODO: ensure these names all align with that of ansi_term.js // -// See the following specs: -// * http://www.ansi-bbs.org/ansi-bbs-core-server.html -// * http://www.vt100.net/docs/vt510-rm/SGR -// * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt +// See the following specs: +// * http://www.ansi-bbs.org/ansi-bbs-core-server.html +// * http://www.vt100.net/docs/vt510-rm/SGR +// * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // -// Note that these are intentionally not in order such that they -// can be grouped by concept here in code. +// Note that these are intentionally not in order such that they +// can be grouped by concept here in code. // ANSIEscapeParser.styles = { - 0 : 'default', // Everything disabled + 0 : 'default', // Everything disabled - 1 : 'intensityBright', // aka bold - 2 : 'intensityDim', - 22 : 'intensityNormal', + 1 : 'intensityBright', // aka bold + 2 : 'intensityDim', + 22 : 'intensityNormal', - 4 : 'underlineOn', // Not supported by most BBS-like terminals - 24 : 'underlineOff', // Not supported by most BBS-like terminals + 4 : 'underlineOn', // Not supported by most BBS-like terminals + 24 : 'underlineOff', // Not supported by most BBS-like terminals - 5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same - 6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same - 25 : 'blinkOff', + 5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same + 6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same + 25 : 'blinkOff', - 7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG" - 27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG" + 7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG" + 27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG" - 8 : 'invisibleOn', // FG set to BG - 28 : 'invisibleOff', // Not supported by most BBS-like terminals + 8 : 'invisibleOn', // FG set to BG + 28 : 'invisibleOff', // Not supported by most BBS-like terminals }; Object.freeze(ANSIEscapeParser.styles); diff --git a/core/ansi_prep.js b/core/ansi_prep.js index 3eb05b08..09c9bbf6 100644 --- a/core/ansi_prep.js +++ b/core/ansi_prep.js @@ -1,38 +1,38 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; -const ANSI = require('./ansi_term.js'); +// ENiGMA½ +const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; +const ANSI = require('./ansi_term.js'); const { splitTextAtTerms, renderStringLength -} = require('./string_util.js'); +} = require('./string_util.js'); -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); module.exports = function ansiPrep(input, options, cb) { if(!input) { return cb(null, ''); } - options.termWidth = options.termWidth || 80; - options.termHeight = options.termHeight || 25; - options.cols = options.cols || options.termWidth || 80; - options.rows = options.rows || options.termHeight || 'auto'; - options.startCol = options.startCol || 1; - options.exportMode = options.exportMode || false; - options.fillLines = _.get(options, 'fillLines', true); - options.indent = options.indent || 0; + options.termWidth = options.termWidth || 80; + options.termHeight = options.termHeight || 25; + options.cols = options.cols || options.termWidth || 80; + options.rows = options.rows || options.termHeight || 'auto'; + options.startCol = options.startCol || 1; + options.exportMode = options.exportMode || false; + options.fillLines = _.get(options, 'fillLines', true); + options.indent = options.indent || 0; - // in auto we start out at 25 rows, but can always expand for more + // in auto we start out at 25 rows, but can always expand for more const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } ); const state = { - row : 0, - col : 0, + row : 0, + col : 0, }; let lastRow = 0; @@ -46,19 +46,19 @@ module.exports = function ansiPrep(input, options, cb) { } parser.on('position update', (row, col) => { - state.row = row - 1; - state.col = col - 1; + state.row = row - 1; + state.col = col - 1; if(0 === state.col) { state.initialSgr = state.lastSgr; } - lastRow = Math.max(state.row, lastRow); + lastRow = Math.max(state.row, lastRow); }); parser.on('literal', literal => { // - // CR/LF are handled for 'position update'; we don't need the chars themselves + // CR/LF are handled for 'position update'; we don't need the chars themselves // literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); @@ -73,9 +73,9 @@ module.exports = function ansiPrep(input, options, cb) { canvas[state.row][state.col].char = c; if(state.sgr) { - canvas[state.row][state.col].sgr = _.clone(state.sgr); - state.lastSgr = canvas[state.row][state.col].sgr; - state.sgr = null; + canvas[state.row][state.col].sgr = _.clone(state.sgr); + state.lastSgr = canvas[state.row][state.col].sgr; + state.sgr = null; } } @@ -87,8 +87,8 @@ module.exports = function ansiPrep(input, options, cb) { ensureRow(state.row); if(state.col < options.cols) { - canvas[state.row][state.col].sgr = _.clone(sgr); - state.lastSgr = canvas[state.row][state.col].sgr; + canvas[state.row][state.col].sgr = _.clone(sgr); + state.lastSgr = canvas[state.row][state.col].sgr; } else { state.sgr = sgr; } @@ -147,16 +147,16 @@ module.exports = function ansiPrep(input, options, cb) { if(options.exportMode) { // - // If we're in export mode, we do some additional hackery: + // If we're in export mode, we do some additional hackery: // - // * Hard wrap ALL lines at <= 79 *characters* (not visible columns) - // if a line must wrap early, we'll place a ESC[A ESC[C where - // represents chars to get back to the position we were previously at + // * Hard wrap ALL lines at <= 79 *characters* (not visible columns) + // if a line must wrap early, we'll place a ESC[A ESC[C where + // represents chars to get back to the position we were previously at // - // * Replace contig spaces with ESC[C as well to save... space. + // * Replace contig spaces with ESC[C as well to save... space. // - // :TODO: this would be better to do as part of the processing above, but this will do for now - const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with + // :TODO: this would be better to do as part of the processing above, but this will do for now + const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with let exportOutput = ''; let m; @@ -176,16 +176,16 @@ module.exports = function ansiPrep(input, options, cb) { afterSeq = m.index + m[0].length; if(afterSeq < MAX_CHARS) { - // after current seq + // after current seq splitAt = afterSeq; } else { if(m.index < MAX_CHARS) { - // before last found seq + // before last found seq splitAt = m.index; - wantMore = false; // can't eat up any more + wantMore = false; // can't eat up any more } - break; // seq's beyond this point are >= MAX_CHARS + break; // seq's beyond this point are >= MAX_CHARS } } @@ -202,7 +202,7 @@ module.exports = function ansiPrep(input, options, cb) { renderStart += renderStringLength(part); exportOutput += `${part}\r\n`; - if(fullLine.length > 0) { // more to go for this line? + if(fullLine.length > 0) { // more to go for this line? exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; } else { exportOutput += ANSI.up(); diff --git a/core/ansi_term.js b/core/ansi_term.js index dc3399a6..44c82464 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -2,191 +2,191 @@ 'use strict'; // -// ANSI Terminal Support Resources +// ANSI Terminal Support Resources // -// ANSI-BBS -// * http://ansi-bbs.org/ +// ANSI-BBS +// * http://ansi-bbs.org/ // -// CTerm / SyncTERM -// * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt +// CTerm / SyncTERM +// * https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // -// BananaCom -// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt +// BananaCom +// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt // -// ANSI.SYS -// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt -// * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm +// ANSI.SYS +// * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt +// * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm // -// VTX -// * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt +// VTX +// * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt // -// General -// * http://en.wikipedia.org/wiki/ANSI_escape_code -// * http://www.inwap.com/pdp10/ansicode.txt +// General +// * http://en.wikipedia.org/wiki/ANSI_escape_code +// * http://www.inwap.com/pdp10/ansicode.txt // -// Other Implementations -// * https://github.com/chjj/term.js/blob/master/src/term.js +// Other Implementations +// * https://github.com/chjj/term.js/blob/master/src/term.js // // -// For a board, we need to support the semi-standard ANSI-BBS "spec" which -// is bastardized mix of DOS ANSI.SYS, cterm.txt, bansi.txt and a little other. -// This gives us NetRunner, SyncTERM, EtherTerm, most *nix terminals, compatibilitiy -// with legit oldschool DOS terminals, and so on. +// For a board, we need to support the semi-standard ANSI-BBS "spec" which +// is bastardized mix of DOS ANSI.SYS, cterm.txt, bansi.txt and a little other. +// This gives us NetRunner, SyncTERM, EtherTerm, most *nix terminals, compatibilitiy +// with legit oldschool DOS terminals, and so on. // -// ENiGMA½ -const miscUtil = require('./misc_util.js'); +// ENiGMA½ +const miscUtil = require('./misc_util.js'); -// deps -const assert = require('assert'); -const _ = require('lodash'); +// deps +const assert = require('assert'); +const _ = require('lodash'); -exports.getFullMatchRegExp = getFullMatchRegExp; -exports.getFGColorValue = getFGColorValue; -exports.getBGColorValue = getBGColorValue; -exports.sgr = sgr; -exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition; -exports.clearScreen = clearScreen; -exports.resetScreen = resetScreen; -exports.normal = normal; -exports.goHome = goHome; -exports.disableVT100LineWrapping = disableVT100LineWrapping; -exports.setSyncTERMFont = setSyncTERMFont; -exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; -exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; -exports.setCursorStyle = setCursorStyle; -exports.setEmulatedBaudRate = setEmulatedBaudRate; -exports.vtxHyperlink = vtxHyperlink; +exports.getFullMatchRegExp = getFullMatchRegExp; +exports.getFGColorValue = getFGColorValue; +exports.getBGColorValue = getBGColorValue; +exports.sgr = sgr; +exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition; +exports.clearScreen = clearScreen; +exports.resetScreen = resetScreen; +exports.normal = normal; +exports.goHome = goHome; +exports.disableVT100LineWrapping = disableVT100LineWrapping; +exports.setSyncTERMFont = setSyncTERMFont; +exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; +exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; +exports.setCursorStyle = setCursorStyle; +exports.setEmulatedBaudRate = setEmulatedBaudRate; +exports.vtxHyperlink = vtxHyperlink; // -// See also -// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js +// See also +// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js -const ESC_CSI = '\u001b['; +const ESC_CSI = '\u001b['; const CONTROL = { - up : 'A', - down : 'B', + up : 'A', + down : 'B', - forward : 'C', - right : 'C', + forward : 'C', + right : 'C', - back : 'D', - left : 'D', + back : 'D', + left : 'D', - nextLine : 'E', - prevLine : 'F', - horizAbsolute : 'G', + nextLine : 'E', + prevLine : 'F', + horizAbsolute : 'G', // - // CSI [ p1 ] J - // Erase in Page / Erase Data - // Defaults: p1 = 0 - // Erases from the current screen according to the value of p1 - // 0 - Erase from the current position to the end of the screen. - // 1 - Erase from the current position to the start of the screen. - // 2 - Erase entire screen. As a violation of ECMA-048, also moves - // the cursor to position 1/1 as a number of BBS programs assume - // this behaviour. - // Erased characters are set to the current attribute. + // CSI [ p1 ] J + // Erase in Page / Erase Data + // Defaults: p1 = 0 + // Erases from the current screen according to the value of p1 + // 0 - Erase from the current position to the end of the screen. + // 1 - Erase from the current position to the start of the screen. + // 2 - Erase entire screen. As a violation of ECMA-048, also moves + // the cursor to position 1/1 as a number of BBS programs assume + // this behaviour. + // Erased characters are set to the current attribute. // - // Support: - // * SyncTERM: Works as expected - // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 - // and screen remainder + // Support: + // * SyncTERM: Works as expected + // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 + // and screen remainder // - eraseData : 'J', + eraseData : 'J', - eraseLine : 'K', - insertLine : 'L', + eraseLine : 'K', + insertLine : 'L', // - // CSI [ p1 ] M - // Delete Line(s) / "ANSI" Music - // Defaults: p1 = 1 - // Deletes the current line and the p1 - 1 lines after it scrolling the - // first non-deleted line up to the current line and filling the newly - // empty lines at the end of the screen with the current attribute. - // If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music - // instead. - // See "ANSI" MUSIC section for more details. + // CSI [ p1 ] M + // Delete Line(s) / "ANSI" Music + // Defaults: p1 = 1 + // Deletes the current line and the p1 - 1 lines after it scrolling the + // first non-deleted line up to the current line and filling the newly + // empty lines at the end of the screen with the current attribute. + // If "ANSI" Music is fully enabled (CSI = 2 M), performs "ANSI" music + // instead. + // See "ANSI" MUSIC section for more details. // - // Support: - // * SyncTERM: Works as expected - // * NetRunner: + // Support: + // * SyncTERM: Works as expected + // * NetRunner: // - // General Notes: - // See also notes in bansi.txt and cterm.txt about the various - // incompatibilities & oddities around this sequence. ANSI-BBS - // states that it *should* work with any value of p1. + // General Notes: + // See also notes in bansi.txt and cterm.txt about the various + // incompatibilities & oddities around this sequence. ANSI-BBS + // states that it *should* work with any value of p1. // - deleteLine : 'M', - ansiMusic : 'M', + deleteLine : 'M', + ansiMusic : 'M', - scrollUp : 'S', - scrollDown : 'T', - setScrollRegion : 'r', - savePos : 's', - restorePos : 'u', - queryPos : '6n', - queryScreenSize : '255n', // See bansi.txt - goto : 'H', // row Pr, column Pc -- same as f - gotoAlt : 'f', // same as H + scrollUp : 'S', + scrollDown : 'T', + setScrollRegion : 'r', + savePos : 's', + restorePos : 'u', + queryPos : '6n', + queryScreenSize : '255n', // See bansi.txt + goto : 'H', // row Pr, column Pc -- same as f + gotoAlt : 'f', // same as H blinkToBrightIntensity : '?33h', - blinkNormal : '?33l', + blinkNormal : '?33l', - emulationSpeed : '*r', // Set output emulation speed. See cterm.txt + emulationSpeed : '*r', // Set output emulation speed. See cterm.txt - hideCursor : '?25l', // Nonstandard - cterm.txt - showCursor : '?25h', // Nonstandard - cterm.txt + hideCursor : '?25l', // Nonstandard - cterm.txt + showCursor : '?25h', // Nonstandard - cterm.txt - queryDeviceAttributes : 'c', // Nonstandard - cterm.txt + queryDeviceAttributes : 'c', // Nonstandard - cterm.txt - // :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes - // apparently some terms can report screen size and text area via 18t and 19t + // :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes + // apparently some terms can report screen size and text area via 18t and 19t }; // -// Select Graphics Rendition -// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt +// Select Graphics Rendition +// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // const SGRValues = { - reset : 0, - bold : 1, - dim : 2, - blink : 5, - fastBlink : 6, - negative : 7, - hidden : 8, + reset : 0, + bold : 1, + dim : 2, + blink : 5, + fastBlink : 6, + negative : 7, + hidden : 8, - normal : 22, // - steady : 25, - positive : 27, + normal : 22, // + steady : 25, + positive : 27, - black : 30, - red : 31, - green : 32, - yellow : 33, - blue : 34, - magenta : 35, - cyan : 36, - white : 37, + black : 30, + red : 31, + green : 32, + yellow : 33, + blue : 34, + magenta : 35, + cyan : 36, + white : 37, - blackBG : 40, - redBG : 41, - greenBG : 42, - yellowBG : 43, - blueBG : 44, - magentaBG : 45, - cyanBG : 46, - whiteBG : 47, + blackBG : 40, + redBG : 41, + greenBG : 42, + yellowBG : 43, + blueBG : 44, + magentaBG : 45, + cyanBG : 46, + whiteBG : 47, }; function getFullMatchRegExp(flags = 'g') { - // :TODO: expand this a bit - see strip-ansi/etc. - // :TODO: \u009b ? - return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex + // :TODO: expand this a bit - see strip-ansi/etc. + // :TODO: \u009b ? + return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex } function getFGColorValue(name) { @@ -198,20 +198,20 @@ function getBGColorValue(name) { } -// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt -// :TODO: document -// :TODO: Create mappings for aliases... maybe make this a map to values instead -// :TODO: Break this up in to two parts: -// 1) FONT_AND_CODE_PAGES (e.g. SyncTERM/cterm) -// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES. -// ...we can then have getFontFromSAUCEName(sauceFontName) -// Also, create a SAUCE_ENCODING_MAP: SAUCE font name -> encodings +// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt +// :TODO: document +// :TODO: Create mappings for aliases... maybe make this a map to values instead +// :TODO: Break this up in to two parts: +// 1) FONT_AND_CODE_PAGES (e.g. SyncTERM/cterm) +// 2) SAUCE_FONT_MAP: Sauce name(s) -> items in FONT_AND_CODE_PAGES. +// ...we can then have getFontFromSAUCEName(sauceFontName) +// Also, create a SAUCE_ENCODING_MAP: SAUCE font name -> encodings // -// An array of CTerm/SyncTERM font/encoding values. Each entry's index -// corresponds to it's escape sequence value (e.g. cp437 = 0) +// An array of CTerm/SyncTERM font/encoding values. Each entry's index +// corresponds to it's escape sequence value (e.g. cp437 = 0) // -// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt +// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // const SYNCTERM_FONT_AND_ENCODING_TABLE = [ 'cp437', @@ -260,54 +260,54 @@ const SYNCTERM_FONT_AND_ENCODING_TABLE = [ ]; // -// A map of various font name/aliases such as those used -// in SAUCE records to SyncTERM/CTerm names +// A map of various font name/aliases such as those used +// in SAUCE records to SyncTERM/CTerm names // -// This table contains lowercased entries with any spaces -// replaced with '_' for lookup purposes. +// This table contains lowercased entries with any spaces +// replaced with '_' for lookup purposes. // const FONT_ALIAS_TO_SYNCTERM_MAP = { - 'cp437' : 'cp437', - 'ibm_vga' : 'cp437', - 'ibmpc' : 'cp437', - 'ibm_pc' : 'cp437', - 'pc' : 'cp437', - 'cp437_art' : 'cp437', - 'ibmpcart' : 'cp437', - 'ibmpc_art' : 'cp437', - 'ibm_pc_art' : 'cp437', - 'msdos_art' : 'cp437', - 'msdosart' : 'cp437', - 'pc_art' : 'cp437', - 'pcart' : 'cp437', + 'cp437' : 'cp437', + 'ibm_vga' : 'cp437', + 'ibmpc' : 'cp437', + 'ibm_pc' : 'cp437', + 'pc' : 'cp437', + 'cp437_art' : 'cp437', + 'ibmpcart' : 'cp437', + 'ibmpc_art' : 'cp437', + 'ibm_pc_art' : 'cp437', + 'msdos_art' : 'cp437', + 'msdosart' : 'cp437', + 'pc_art' : 'cp437', + 'pcart' : 'cp437', - 'ibm_vga50' : 'cp437', - 'ibm_vga25g' : 'cp437', - 'ibm_ega' : 'cp437', - 'ibm_ega43' : 'cp437', + 'ibm_vga50' : 'cp437', + 'ibm_vga25g' : 'cp437', + 'ibm_ega' : 'cp437', + 'ibm_ega43' : 'cp437', - 'topaz' : 'topaz', - 'amiga_topaz_1' : 'topaz', - 'amiga_topaz_1+' : 'topaz_plus', - 'topazplus' : 'topaz_plus', - 'topaz_plus' : 'topaz_plus', - 'amiga_topaz_2' : 'topaz', - 'amiga_topaz_2+' : 'topaz_plus', - 'topaz2plus' : 'topaz_plus', + 'topaz' : 'topaz', + 'amiga_topaz_1' : 'topaz', + 'amiga_topaz_1+' : 'topaz_plus', + 'topazplus' : 'topaz_plus', + 'topaz_plus' : 'topaz_plus', + 'amiga_topaz_2' : 'topaz', + 'amiga_topaz_2+' : 'topaz_plus', + 'topaz2plus' : 'topaz_plus', - 'pot_noodle' : 'pot_noodle', - 'p0tnoodle' : 'pot_noodle', - 'amiga_p0t-noodle' : 'pot_noodle', + 'pot_noodle' : 'pot_noodle', + 'p0tnoodle' : 'pot_noodle', + 'amiga_p0t-noodle' : 'pot_noodle', - 'mo_soul' : 'mo_soul', - 'mosoul' : 'mo_soul', - 'mO\'sOul' : 'mo_soul', + 'mo_soul' : 'mo_soul', + 'mosoul' : 'mo_soul', + 'mO\'sOul' : 'mo_soul', - 'amiga_microknight' : 'microknight', - 'amiga_microknight+' : 'microknight_plus', + 'amiga_microknight' : 'microknight', + 'amiga_microknight+' : 'microknight_plus', - 'atari' : 'atari', - 'atarist' : 'atari', + 'atari' : 'atari', + 'atarist' : 'atari', }; @@ -334,13 +334,13 @@ function setSyncTermFontWithAlias(nameOrAlias) { } const DEC_CURSOR_STYLE = { - 'blinking block' : 0, - 'default' : 1, - 'steady block' : 2, - 'blinking underline' : 3, - 'steady underline' : 4, - 'blinking bar' : 5, - 'steady bar' : 6, + 'blinking block' : 0, + 'default' : 1, + 'steady block' : 2, + 'blinking underline' : 3, + 'steady underline' : 4, + 'blinking bar' : 5, + 'steady bar' : 6, }; function setCursorStyle(cursorStyle) { @@ -352,21 +352,21 @@ function setCursorStyle(cursorStyle) { } -// Create methods such as up(), nextLine(),... +// Create methods such as up(), nextLine(),... Object.keys(CONTROL).forEach(function onControlName(name) { const code = CONTROL[name]; exports[name] = function() { let c = code; if(arguments.length > 0) { - // arguments are array like -- we want an array + // arguments are array like -- we want an array c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; } return `${ESC_CSI}${c}`; }; }); -// Create various color methods such as white(), yellowBG(), reset(), ... +// Create various color methods such as white(), yellowBG(), reset(), ... Object.keys(SGRValues).forEach( name => { const code = SGRValues[name]; @@ -377,16 +377,16 @@ Object.keys(SGRValues).forEach( name => { function sgr() { // - // - Allow an single array or variable number of arguments - // - Each element can be either a integer or string found in SGRValues - // which in turn maps to a integer + // - Allow an single array or variable number of arguments + // - Each element can be either a integer or string found in SGRValues + // which in turn maps to a integer // if(arguments.length <= 0) { return ''; } - let result = []; - const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; + let result = []; + const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; for(let i = 0; i < args.length; ++i) { const arg = args[i]; @@ -401,12 +401,12 @@ function sgr() { } // -// Converts a Graphic Rendition object used elsewhere -// to a ANSI SGR sequence. +// Converts a Graphic Rendition object used elsewhere +// to a ANSI SGR sequence. // function getSGRFromGraphicRendition(graphicRendition, initialReset) { - let sgrSeq = []; - let styleCount = 0; + let sgrSeq = []; + let styleCount = 0; [ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => { if(graphicRendition[s]) { @@ -431,7 +431,7 @@ function getSGRFromGraphicRendition(graphicRendition, initialReset) { } /////////////////////////////////////////////////////////////////////////////// -// Shortcuts for common functions +// Shortcuts for common functions /////////////////////////////////////////////////////////////////////////////// function clearScreen() { @@ -447,20 +447,20 @@ function normal() { } function goHome() { - return exports.goto(); // no params = home = 1,1 + return exports.goto(); // no params = home = 1,1 } // -// Disable auto line wraping @ termWidth +// Disable auto line wraping @ termWidth // -// See: -// http://stjarnhimlen.se/snippets/vt100.txt -// https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt +// See: +// http://stjarnhimlen.se/snippets/vt100.txt +// https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // -// WARNING: -// * Not honored by all clients -// * If it is honored, ANSI's that rely on this (e.g. do not have \r\n endings -// and use term width -- generally 80 columns -- will display garbled! +// WARNING: +// * Not honored by all clients +// * If it is honored, ANSI's that rely on this (e.g. do not have \r\n endings +// and use term width -- generally 80 columns -- will display garbled! // function disableVT100LineWrapping() { return `${ESC_CSI}?7l`; @@ -468,20 +468,20 @@ function disableVT100LineWrapping() { function setEmulatedBaudRate(rate) { const speed = { - unlimited : 0, - off : 0, - 0 : 0, - 300 : 1, - 600 : 2, - 1200 : 3, - 2400 : 4, - 4800 : 5, - 9600 : 6, - 19200 : 7, - 38400 : 8, - 57600 : 9, - 76800 : 10, - 115200 : 11, + unlimited : 0, + off : 0, + 0 : 0, + 300 : 1, + 600 : 2, + 1200 : 3, + 2400 : 4, + 4800 : 5, + 9600 : 6, + 19200 : 7, + 38400 : 8, + 57600 : 9, + 76800 : 10, + 115200 : 11, }[rate] || 0; return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); } diff --git a/core/archive_util.js b/core/archive_util.js index d59a2609..c0076dba 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -1,26 +1,26 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const stringFormat = require('./string_format.js'); -const Errors = require('./enig_error.js').Errors; -const resolveMimeType = require('./mime_util.js').resolveMimeType; +// ENiGMA½ +const Config = require('./config.js').get; +const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; +const resolveMimeType = require('./mime_util.js').resolveMimeType; -// base/modules -const fs = require('graceful-fs'); -const _ = require('lodash'); -const pty = require('node-pty'); -const paths = require('path'); +// base/modules +const fs = require('graceful-fs'); +const _ = require('lodash'); +const pty = require('node-pty'); +const paths = require('path'); let archiveUtil; class Archiver { constructor(config) { - this.compress = config.compress; - this.decompress = config.decompress; - this.list = config.list; - this.extract = config.extract; + this.compress = config.compress; + this.decompress = config.decompress; + this.list = config.list; + this.extract = config.extract; } ok() { @@ -37,7 +37,7 @@ class Archiver { canCompress() { return this.can('compress'); } canDecompress() { return this.can('decompress'); } - canList() { return this.can('list'); } // :TODO: validate entryMatch + canList() { return this.can('list'); } // :TODO: validate entryMatch canExtract() { return this.can('extract'); } } @@ -48,7 +48,7 @@ module.exports = class ArchiveUtil { this.longestSignature = 0; } - // singleton access + // singleton access static getInstance() { if(!archiveUtil) { archiveUtil = new ArchiveUtil(); @@ -59,17 +59,17 @@ module.exports = class ArchiveUtil { init() { // - // Load configuration + // Load configuration // const config = Config(); if(_.has(config, 'archives.archivers')) { Object.keys(config.archives.archivers).forEach(archKey => { - const archConfig = config.archives.archivers[archKey]; - const archiver = new Archiver(archConfig); + const archConfig = config.archives.archivers[archKey]; + const archiver = new Archiver(archConfig); if(!archiver.ok()) { - // :TODO: Log warning - bad archiver/config + // :TODO: Log warning - bad archiver/config } this.archivers[archKey] = archiver; @@ -78,10 +78,10 @@ module.exports = class ArchiveUtil { if(_.isObject(config.fileTypes)) { const updateSig = (ft) => { - ft.sig = Buffer.from(ft.sig, 'hex'); - ft.offset = ft.offset || 0; + ft.sig = Buffer.from(ft.sig, 'hex'); + ft.offset = ft.offset || 0; - // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well + // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well const sigLen = ft.offset + ft.sig.length; if(sigLen > this.longestSignature) { this.longestSignature = sigLen; @@ -106,7 +106,7 @@ module.exports = class ArchiveUtil { getArchiver(mimeTypeOrExtension, justExtention) { const mimeType = resolveMimeType(mimeTypeOrExtension); - if(!mimeType) { // lookup returns false on failure + if(!mimeType) { // lookup returns false on failure return; } @@ -115,10 +115,10 @@ module.exports = class ArchiveUtil { if(Array.isArray(fileType)) { if(!justExtention) { - // need extention for lookup; ambiguous as-is :( + // need extention for lookup; ambiguous as-is :( return; } - // further refine by extention + // further refine by extention fileType = fileType.find(ft => justExtention === ft.ext); } @@ -135,11 +135,11 @@ module.exports = class ArchiveUtil { return this.getArchiver(archType) ? true : false; } - // :TODO: implement me: + // :TODO: implement me: /* - detectTypeWithBuf(buf, cb) { - } - */ + detectTypeWithBuf(buf, cb) { + } + */ detectType(path, cb) { fs.open(path, 'r', (err, fd) => { @@ -177,8 +177,8 @@ module.exports = class ArchiveUtil { } spawnHandler(proc, action, cb) { - // pty.js doesn't currently give us a error when things fail, - // so we have this horrible, horrible hack: + // pty.js doesn't currently give us a error when things fail, + // so we have this horrible, horrible hack: let err; proc.once('data', d => { if(_.isString(d) && d.startsWith('execvp(3) failed.')) { @@ -199,8 +199,8 @@ module.exports = class ArchiveUtil { } const fmtObj = { - archivePath : archivePath, - fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! + archivePath : archivePath, + fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! }; const args = archiver.compress.args.map( arg => stringFormat(arg, fmtObj) ); @@ -233,25 +233,25 @@ module.exports = class ArchiveUtil { } const fmtObj = { - archivePath : archivePath, - extractPath : extractPath, + archivePath : archivePath, + extractPath : extractPath, }; let action = haveFileList ? 'extract' : 'decompress'; if('extract' === action && !_.isObject(archiver[action])) { - // we're forced to do a full decompress + // we're forced to do a full decompress action = 'decompress'; haveFileList = false; } - // we need to treat {fileList} special in that it should be broken up to 0:n args + // we need to treat {fileList} special in that it should be broken up to 0:n args const args = archiver[action].args.map( arg => { return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); }); const fileListPos = args.indexOf('{fileList}'); if(fileListPos > -1) { - // replace {fileList} with 0:n sep file list arguments + // replace {fileList} with 0:n sep file list arguments args.splice.apply(args, [fileListPos, 1].concat(fileList)); } @@ -273,10 +273,10 @@ module.exports = class ArchiveUtil { } const fmtObj = { - archivePath : archivePath, + archivePath : archivePath, }; - const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); + const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); let proc; try { @@ -287,7 +287,7 @@ module.exports = class ArchiveUtil { let output = ''; proc.on('data', data => { - // :TODO: hack for: execvp(3) failed.: No such file or directory + // :TODO: hack for: execvp(3) failed.: No such file or directory output += data; }); @@ -304,8 +304,8 @@ module.exports = class ArchiveUtil { let m; while((m = entryMatchRe.exec(output))) { entries.push({ - byteSize : parseInt(m[entryGroupOrder.byteSize]), - fileName : m[entryGroupOrder.fileName].trim(), + byteSize : parseInt(m[entryGroupOrder.byteSize]), + fileName : m[entryGroupOrder.fileName].trim(), }); } @@ -315,15 +315,15 @@ module.exports = class ArchiveUtil { getPtyOpts(extractPath) { const opts = { - name : 'enigma-archiver', - cols : 80, - rows : 24, - env : process.env, + name : 'enigma-archiver', + cols : 80, + rows : 24, + env : process.env, }; if(extractPath) { opts.cwd = extractPath; } - // :TODO: set cwd to supplied temp path if not sepcific extract + // :TODO: set cwd to supplied temp path if not sepcific extract return opts; } }; diff --git a/core/art.js b/core/art.js index 3a1949b1..315950e1 100644 --- a/core/art.js +++ b/core/art.js @@ -1,43 +1,43 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const miscUtil = require('./misc_util.js'); -const ansi = require('./ansi_term.js'); -const aep = require('./ansi_escape_parser.js'); -const sauce = require('./sauce.js'); +// ENiGMA½ +const Config = require('./config.js').get; +const miscUtil = require('./misc_util.js'); +const ansi = require('./ansi_term.js'); +const aep = require('./ansi_escape_parser.js'); +const sauce = require('./sauce.js'); -// deps -const fs = require('graceful-fs'); -const paths = require('path'); -const assert = require('assert'); -const iconv = require('iconv-lite'); -const _ = require('lodash'); -const xxhash = require('xxhash'); +// deps +const fs = require('graceful-fs'); +const paths = require('path'); +const assert = require('assert'); +const iconv = require('iconv-lite'); +const _ = require('lodash'); +const xxhash = require('xxhash'); -exports.getArt = getArt; -exports.getArtFromPath = getArtFromPath; -exports.display = display; -exports.defaultEncodingFromExtension = defaultEncodingFromExtension; +exports.getArt = getArt; +exports.getArtFromPath = getArtFromPath; +exports.display = display; +exports.defaultEncodingFromExtension = defaultEncodingFromExtension; -// :TODO: Return MCI code information -// :TODO: process SAUCE comments -// :TODO: return font + font mapped information from SAUCE +// :TODO: Return MCI code information +// :TODO: process SAUCE comments +// :TODO: return font + font mapped information from SAUCE const SUPPORTED_ART_TYPES = { - // :TODO: the defualt encoding are really useless if they are all the same ... - // perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf - '.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a }, - '.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a }, - '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, - '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, + // :TODO: the defualt encoding are really useless if they are all the same ... + // perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf + '.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a }, + '.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a }, + '.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, + '.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, - '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, - '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, - // :TODO: extentions for wwiv, renegade, celerity, syncronet, ... - // :TODO: extension for atari - // :TODO: extension for topaz ansi/ascii. + '.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, + '.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, + // :TODO: extentions for wwiv, renegade, celerity, syncronet, ... + // :TODO: extension for atari + // :TODO: extension for topaz ansi/ascii. }; function getFontNameFromSAUCE(sauce) { @@ -47,8 +47,8 @@ function getFontNameFromSAUCE(sauce) { } function sliceAtEOF(data, eofMarker) { - let eof = data.length; - const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) + let eof = data.length; + const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) for(let i = eof - 1; i > stopPos; i--) { if(eofMarker === data[i]) { @@ -66,12 +66,12 @@ function getArtFromPath(path, options, cb) { } // - // Convert from encodedAs -> j + // Convert from encodedAs -> j // - const ext = paths.extname(path).toLowerCase(); - const encoding = options.encodedAs || defaultEncodingFromExtension(ext); + const ext = paths.extname(path).toLowerCase(); + const encoding = options.encodedAs || defaultEncodingFromExtension(ext); - // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? + // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? function sliceOfData() { if(options.fullFile === true) { @@ -84,8 +84,8 @@ function getArtFromPath(path, options, cb) { function getResult(sauce) { const result = { - data : sliceOfData(), - fromPath : path, + data : sliceOfData(), + fromPath : path, }; if(sauce) { @@ -102,18 +102,18 @@ function getArtFromPath(path, options, cb) { } // - // If a encoding was not provided & we have a mapping from - // the information provided by SAUCE, use that. + // If a encoding was not provided & we have a mapping from + // the information provided by SAUCE, use that. // if(!options.encodedAs) { /* - if(sauce.Character && sauce.Character.fontName) { - var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; - if(enc) { - encoding = enc; - } - } - */ + if(sauce.Character && sauce.Character.fontName) { + var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; + if(enc) { + encoding = enc; + } + } + */ } return cb(null, getResult(sauce)); }); @@ -126,10 +126,10 @@ function getArtFromPath(path, options, cb) { function getArt(name, options, cb) { const ext = paths.extname(name); - options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); - options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); + options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); + options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); - // :TODO: make use of asAnsi option and convert from supported -> ansi + // :TODO: make use of asAnsi option and convert from supported -> ansi if('' !== ext) { options.types = [ ext.toLowerCase() ]; @@ -141,7 +141,7 @@ function getArt(name, options, cb) { } } - // If an extension is provided, just read the file now + // If an extension is provided, just read the file now if('' !== ext) { const directPath = paths.join(options.basePath, name); return getArtFromPath(directPath, options, cb); @@ -225,10 +225,10 @@ function defaultEofFromExtension(ext) { } } -// :TODO: Implement the following -// * Pause (disabled | termHeight | keyPress ) -// * Cancel (disabled | ) -// * Resume from pause -> continous (disabled | ) +// :TODO: Implement the following +// * Pause (disabled | termHeight | keyPress ) +// * Cancel (disabled | ) +// * Resume from pause -> continous (disabled | ) function display(client, art, options, cb) { if(_.isFunction(options) && !cb) { cb = options; @@ -239,25 +239,25 @@ function display(client, art, options, cb) { return cb(new Error('Empty art')); } - options.mciReplaceChar = options.mciReplaceChar || ' '; - options.disableMciCache = options.disableMciCache || false; + options.mciReplaceChar = options.mciReplaceChar || ' '; + options.disableMciCache = options.disableMciCache || false; - // :TODO: this is going to be broken into two approaches controlled via options: - // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. - // 2) CPR driven + // :TODO: this is going to be broken into two approaches controlled via options: + // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. + // 2) CPR driven if(!_.isBoolean(options.iceColors)) { - // try to detect from SAUCE + // try to detect from SAUCE if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { options.iceColors = true; } } const ansiParser = new aep.ANSIEscapeParser({ - mciReplaceChar : options.mciReplaceChar, - termHeight : client.term.termHeight, - termWidth : client.term.termWidth, - trailingLF : options.trailingLF, + mciReplaceChar : options.mciReplaceChar, + termHeight : client.term.termHeight, + termWidth : client.term.termWidth, + trailingLF : options.trailingLF, }); let parseComplete = false; @@ -273,12 +273,12 @@ function display(client, art, options, cb) { } if(!options.disableMciCache && !mciMapFromCache) { - // cache our MCI findings... + // cache our MCI findings... client.mciCache[artHash] = mciMap; client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Added MCI map to cache'); } - ansiParser.removeAllListeners(); // :TODO: Necessary??? + ansiParser.removeAllListeners(); // :TODO: Necessary??? const extraInfo = { height : ansiParser.row - 1, @@ -288,11 +288,11 @@ function display(client, art, options, cb) { } if(!options.disableMciCache) { - artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE); + artHash = xxhash.hash(Buffer.from(art), 0xCAFEBABE); - // see if we have a mciMap cached for this art + // see if we have a mciMap cached for this art if(client.mciCache) { - mciMap = client.mciCache[artHash]; + mciMap = client.mciCache[artHash]; } } @@ -300,7 +300,7 @@ function display(client, art, options, cb) { mciMapFromCache = true; client.log.trace( { artHash : artHash.toString(16), mciMap : mciMap }, 'Loaded MCI map from cache'); } else { - // no cached MCI info + // no cached MCI info mciMap = {}; cprListener = function(pos) { @@ -318,20 +318,20 @@ function display(client, art, options, cb) { let generatedId = 100; ansiParser.on('mci', mciInfo => { - // :TODO: ensure generatedId's do not conflict with any existing |id| - const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId; - const mapKey = `${mciInfo.mci}${id}`; - const mapEntry = mciMap[mapKey]; + // :TODO: ensure generatedId's do not conflict with any existing |id| + const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId; + const mapKey = `${mciInfo.mci}${id}`; + const mapEntry = mciMap[mapKey]; if(mapEntry) { - mapEntry.focusSGR = mciInfo.SGR; - mapEntry.focusArgs = mciInfo.args; + mapEntry.focusSGR = mciInfo.SGR; + mapEntry.focusArgs = mciInfo.args; } else { mciMap[mapKey] = { - args : mciInfo.args, - SGR : mciInfo.SGR, - code : mciInfo.mci, - id : id, + args : mciInfo.args, + SGR : mciInfo.SGR, + code : mciInfo.mci, + id : id, }; if(!mciInfo.id) { @@ -366,10 +366,10 @@ function display(client, art, options, cb) { } // - // Set SyncTERM font if we're switching only. Most terminals - // that support this ESC sequence can only show *one* font - // at a time. This applies to detection only (e.g. SAUCE). - // If explicit, we'll set it no matter what (above) + // Set SyncTERM font if we're switching only. Most terminals + // that support this ESC sequence can only show *one* font + // at a time. This applies to detection only (e.g. SAUCE). + // If explicit, we'll set it no matter what (above) // if(fontName && client.term.currentSyncFont != fontName) { client.term.currentSyncFont = fontName; diff --git a/core/asset.js b/core/asset.js index ece05cfc..2204b1da 100644 --- a/core/asset.js +++ b/core/asset.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const StatLog = require('./stat_log.js'); +// ENiGMA½ +const Config = require('./config.js').get; +const StatLog = require('./stat_log.js'); -// deps -const _ = require('lodash'); -const assert = require('assert'); +// deps +const _ = require('lodash'); +const assert = require('assert'); -exports.parseAsset = parseAsset; -exports.getAssetWithShorthand = getAssetWithShorthand; -exports.getArtAsset = getArtAsset; -exports.getModuleAsset = getModuleAsset; -exports.resolveConfigAsset = resolveConfigAsset; -exports.resolveSystemStatAsset = resolveSystemStatAsset; -exports.getViewPropertyAsset = getViewPropertyAsset; +exports.parseAsset = parseAsset; +exports.getAssetWithShorthand = getAssetWithShorthand; +exports.getArtAsset = getArtAsset; +exports.getModuleAsset = getModuleAsset; +exports.resolveConfigAsset = resolveConfigAsset; +exports.resolveSystemStatAsset = resolveSystemStatAsset; +exports.getViewPropertyAsset = getViewPropertyAsset; const ALL_ASSETS = [ 'art', @@ -39,9 +39,9 @@ function parseAsset(s) { if(m[3]) { result.location = m[2]; - result.asset = m[3]; + result.asset = m[3]; } else { - result.asset = m[2]; + result.asset = m[2]; } return result; @@ -61,8 +61,8 @@ function getAssetWithShorthand(spec, defaultType) { } return { - type : defaultType, - asset : spec, + type : defaultType, + asset : spec, }; } @@ -94,8 +94,8 @@ function resolveConfigAsset(spec) { if(asset) { assert('config' === asset.type); - const path = asset.asset.split('.'); - let conf = Config(); + const path = asset.asset.split('.'); + let conf = Config(); for(let i = 0; i < path.length; ++i) { if(_.isUndefined(conf[path[i]])) { return spec; diff --git a/core/bbs_link.js b/core/bbs_link.js index 4034b383..cbb7c036 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -1,54 +1,54 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; -const resetScreen = require('./ansi_term.js').resetScreen; +const MenuModule = require('./menu_module.js').MenuModule; +const resetScreen = require('./ansi_term.js').resetScreen; -const async = require('async'); -const _ = require('lodash'); -const http = require('http'); -const net = require('net'); -const crypto = require('crypto'); +const async = require('async'); +const _ = require('lodash'); +const http = require('http'); +const net = require('net'); +const crypto = require('crypto'); -const packageJson = require('../package.json'); +const packageJson = require('../package.json'); /* - Expected configuration block: + Expected configuration block: - { - module: bbs_link - ... - config: { - sysCode: XXXXX - authCode: XXXXX - schemeCode: XXXX - door: lord + { + module: bbs_link + ... + config: { + sysCode: XXXXX + authCode: XXXXX + schemeCode: XXXX + door: lord - // default hoss: games.bbslink.net - host: games.bbslink.net + // default hoss: games.bbslink.net + host: games.bbslink.net - // defualt port: 23 - port: 23 - } - } + // defualt port: 23 + port: 23 + } + } */ -// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors -// :TODO: ENH: Support nodeMax and tooManyArt +// :TODO: BUG: When a client disconnects, it's not handled very well -- the log is spammed with tons of errors +// :TODO: ENH: Support nodeMax and tooManyArt exports.moduleInfo = { - name : 'BBSLink', - desc : 'BBSLink Access Module', - author : 'NuSkooler', + name : 'BBSLink', + desc : 'BBSLink Access Module', + author : 'NuSkooler', }; exports.getModule = class BBSLinkModule extends MenuModule { constructor(options) { super(options); - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'games.bbslink.net'; - this.config.port = this.config.port || 23; + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'games.bbslink.net'; + this.config.port = this.config.port || 23; } initSequence() { @@ -61,9 +61,9 @@ exports.getModule = class BBSLinkModule extends MenuModule { [ function validateConfig(callback) { if(_.isString(self.config.sysCode) && - _.isString(self.config.authCode) && - _.isString(self.config.schemeCode) && - _.isString(self.config.door)) + _.isString(self.config.authCode) && + _.isString(self.config.schemeCode) && + _.isString(self.config.door)) { callback(null); } else { @@ -72,7 +72,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { }, function acquireToken(callback) { // - // Acquire an authentication token + // Acquire an authentication token // crypto.randomBytes(16, function rand(ex, buf) { if(ex) { @@ -93,19 +93,19 @@ exports.getModule = class BBSLinkModule extends MenuModule { }, function authenticateToken(callback) { // - // Authenticate the token we acquired previously + // Authenticate the token we acquired previously // var headers = { - 'X-User' : self.client.user.userId.toString(), - 'X-System' : self.config.sysCode, - 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), - 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), - 'X-Rows' : self.client.term.termHeight.toString(), - 'X-Key' : randomKey, - 'X-Door' : self.config.door, - 'X-Token' : token, - 'X-Type' : 'enigma-bbs', - 'X-Version' : packageJson.version, + 'X-User' : self.client.user.userId.toString(), + 'X-System' : self.config.sysCode, + 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), + 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), + 'X-Rows' : self.client.term.termHeight.toString(), + 'X-Key' : randomKey, + 'X-Door' : self.config.door, + 'X-Token' : token, + 'X-Type' : 'enigma-bbs', + 'X-Version' : packageJson.version, }; self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { @@ -120,12 +120,12 @@ exports.getModule = class BBSLinkModule extends MenuModule { }, function createTelnetBridge(callback) { // - // Authentication with BBSLink successful. Now, we need to create a telnet - // bridge from us to them + // Authentication with BBSLink successful. Now, we need to create a telnet + // bridge from us to them // var connectOpts = { - port : self.config.port, - host : self.config.host, + port : self.config.port, + host : self.config.host, }; var clientTerminated; @@ -151,8 +151,8 @@ exports.getModule = class BBSLinkModule extends MenuModule { }; bridgeConnection.on('data', function incomingData(data) { - // pass along - // :TODO: just pipe this as well + // pass along + // :TODO: just pipe this as well self.client.term.rawWrite(data); }); @@ -182,9 +182,9 @@ exports.getModule = class BBSLinkModule extends MenuModule { simpleHttpRequest(path, headers, cb) { const getOpts = { - host : this.config.host, - path : path, - headers : headers, + host : this.config.host, + path : path, + headers : headers, }; const req = http.get(getOpts, function response(resp) { diff --git a/core/bbs_list.js b/core/bbs_list.js index b0da3dbb..2f9b6084 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -1,73 +1,73 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; const { getModDatabasePath, getTransactionDatabase -} = require('./database.js'); +} = require('./database.js'); -const ViewController = require('./view_controller.js').ViewController; -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const User = require('./user.js'); -const stringFormat = require('./string_format.js'); +const ViewController = require('./view_controller.js').ViewController; +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const User = require('./user.js'); +const stringFormat = require('./string_format.js'); -// deps -const async = require('async'); -const sqlite3 = require('sqlite3'); -const _ = require('lodash'); +// deps +const async = require('async'); +const sqlite3 = require('sqlite3'); +const _ = require('lodash'); -// :TODO: add notes field +// :TODO: add notes field const moduleInfo = exports.moduleInfo = { - name : 'BBS List', - desc : 'List of other BBSes', - author : 'Andrew Pamment', - packageName : 'com.magickabbs.enigma.bbslist' + name : 'BBS List', + desc : 'List of other BBSes', + author : 'Andrew Pamment', + packageName : 'com.magickabbs.enigma.bbslist' }; const MciViewIds = { view : { - BBSList : 1, - SelectedBBSName : 2, - SelectedBBSSysOp : 3, - SelectedBBSTelnet : 4, - SelectedBBSWww : 5, - SelectedBBSLoc : 6, - SelectedBBSSoftware : 7, - SelectedBBSNotes : 8, - SelectedBBSSubmitter : 9, + BBSList : 1, + SelectedBBSName : 2, + SelectedBBSSysOp : 3, + SelectedBBSTelnet : 4, + SelectedBBSWww : 5, + SelectedBBSLoc : 6, + SelectedBBSSoftware : 7, + SelectedBBSNotes : 8, + SelectedBBSSubmitter : 9, }, add : { - BBSName : 1, - Sysop : 2, - Telnet : 3, - Www : 4, - Location : 5, - Software : 6, - Notes : 7, - Error : 8, + BBSName : 1, + Sysop : 2, + Telnet : 3, + Www : 4, + Location : 5, + Software : 6, + Notes : 7, + Error : 8, } }; const FormIds = { - View : 0, - Add : 1, + View : 0, + Add : 1, }; const SELECTED_MCI_NAME_TO_ENTRY = { - SelectedBBSName : 'bbsName', - SelectedBBSSysOp : 'sysOp', - SelectedBBSTelnet : 'telnet', - SelectedBBSWww : 'www', - SelectedBBSLoc : 'location', - SelectedBBSSoftware : 'software', - SelectedBBSSubmitter : 'submitter', - SelectedBBSSubmitterId : 'submitterUserId', - SelectedBBSNotes : 'notes', + SelectedBBSName : 'bbsName', + SelectedBBSSysOp : 'sysOp', + SelectedBBSTelnet : 'telnet', + SelectedBBSWww : 'www', + SelectedBBSLoc : 'location', + SelectedBBSSoftware : 'software', + SelectedBBSSubmitter : 'submitter', + SelectedBBSSubmitterId : 'submitterUserId', + SelectedBBSNotes : 'notes', }; exports.getModule = class BBSListModule extends MenuModule { @@ -77,7 +77,7 @@ exports.getModule = class BBSListModule extends MenuModule { const self = this; this.menuMethods = { // - // Validators + // Validators // viewValidationListener : function(err, cb) { const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); @@ -93,7 +93,7 @@ exports.getModule = class BBSListModule extends MenuModule { }, // - // Key & submit handlers + // Key & submit handlers // addBBS : function(formData, extraArgs, cb) { self.displayAddScreen(cb); @@ -106,7 +106,7 @@ exports.getModule = class BBSListModule extends MenuModule { const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { - // must be owner or +op + // must be owner or +op return cb(null); } @@ -117,7 +117,7 @@ exports.getModule = class BBSListModule extends MenuModule { self.database.run( `DELETE FROM bbs_list - WHERE id=?;`, + WHERE id=?;`, [ entry.id ], err => { if (err) { @@ -147,13 +147,13 @@ exports.getModule = class BBSListModule extends MenuModule { } }); if(!ok) { - // validators should prevent this! + // validators should prevent this! return cb(null); } self.database.run( `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) - VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, + VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, [ formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes @@ -188,7 +188,7 @@ exports.getModule = class BBSListModule extends MenuModule { ], err => { if(err) { - // :TODO: Handle me -- initSequence() should really take a completion callback + // :TODO: Handle me -- initSequence() should really take a completion callback } self.finishedLoading(); } @@ -218,9 +218,9 @@ exports.getModule = class BBSListModule extends MenuModule { } setEntries(entriesView) { - const config = this.menuConfig.config; - const listFormat = config.listFormat || '{bbsName}'; - const focusListFormat = config.focusListFormat || '{bbsName}'; + const config = this.menuConfig.config; + const listFormat = config.listFormat || '{bbsName}'; + const focusListFormat = config.focusListFormat || '{bbsName}'; entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) ); entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) ); @@ -255,9 +255,9 @@ exports.getModule = class BBSListModule extends MenuModule { ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -273,19 +273,19 @@ exports.getModule = class BBSListModule extends MenuModule { self.database.each( `SELECT id, bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes - FROM bbs_list;`, + FROM bbs_list;`, (err, row) => { if (!err) { self.entries.push({ - id : row.id, - bbsName : row.bbs_name, - sysOp : row.sysop, - telnet : row.telnet, - www : row.www, - location : row.location, - software : row.software, - submitterUserId : row.submitter_user_id, - notes : row.notes, + id : row.id, + bbsName : row.bbs_name, + sysOp : row.sysop, + telnet : row.telnet, + www : row.www, + location : row.location, + software : row.software, + submitterUserId : row.submitter_user_id, + notes : row.notes, }); } }, @@ -371,9 +371,9 @@ exports.getModule = class BBSListModule extends MenuModule { ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -414,16 +414,16 @@ exports.getModule = class BBSListModule extends MenuModule { self.database.serialize( () => { self.database.run( `CREATE TABLE IF NOT EXISTS bbs_list ( - id INTEGER PRIMARY KEY, - bbs_name VARCHAR NOT NULL, - sysop VARCHAR NOT NULL, - telnet VARCHAR NOT NULL, - www VARCHAR, - location VARCHAR, - software VARCHAR, - submitter_user_id INTEGER NOT NULL, - notes VARCHAR - );` + id INTEGER PRIMARY KEY, + bbs_name VARCHAR NOT NULL, + sysop VARCHAR NOT NULL, + telnet VARCHAR NOT NULL, + www VARCHAR, + location VARCHAR, + software VARCHAR, + submitter_user_id INTEGER NOT NULL, + notes VARCHAR + );` ); }); callback(null); diff --git a/core/button_view.js b/core/button_view.js index 6c86b5c3..63de435a 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -1,17 +1,17 @@ /* jslint node: true */ 'use strict'; -const TextView = require('./text_view.js').TextView; -const miscUtil = require('./misc_util.js'); -const util = require('util'); +const TextView = require('./text_view.js').TextView; +const miscUtil = require('./misc_util.js'); +const util = require('util'); -exports.ButtonView = ButtonView; +exports.ButtonView = ButtonView; function ButtonView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.justify = miscUtil.valueWithDefault(options.justify, 'center'); - options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.justify = miscUtil.valueWithDefault(options.justify, 'center'); + options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); TextView.call(this, options); } @@ -29,12 +29,12 @@ ButtonView.prototype.onKeyPress = function(ch, key) { }; /* ButtonView.prototype.onKeyPress = function(ch, key) { - // allow space = submit - if(' ' === ch) { - this.emit('action', 'accept'); - } + // allow space = submit + if(' ' === ch) { + this.emit('action', 'accept'); + } - ButtonView.super_.prototype.onKeyPress.call(this, ch, key); + ButtonView.super_.prototype.onKeyPress.call(this, ch, key); }; */ diff --git a/core/client.js b/core/client.js index 4347c0b2..a2eb4bc3 100644 --- a/core/client.js +++ b/core/client.js @@ -2,69 +2,69 @@ 'use strict'; /* - Portions of this code for key handling heavily inspired from the following: - https://github.com/chjj/blessed/blob/master/lib/keys.js + Portions of this code for key handling heavily inspired from the following: + https://github.com/chjj/blessed/blob/master/lib/keys.js - chji's blessed is MIT licensed: + chji's blessed is MIT licensed: - ----/snip/---------------------- - The MIT License (MIT) + ----/snip/---------------------- + The MIT License (MIT) - Copyright (c) + Copyright (c) - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - ----/snip/---------------------- + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + ----/snip/---------------------- */ -// ENiGMA½ -const term = require('./client_term.js'); -const ansi = require('./ansi_term.js'); -const User = require('./user.js'); -const Config = require('./config.js').get; -const MenuStack = require('./menu_stack.js'); -const ACS = require('./acs.js'); -const Events = require('./events.js'); +// ENiGMA½ +const term = require('./client_term.js'); +const ansi = require('./ansi_term.js'); +const User = require('./user.js'); +const Config = require('./config.js').get; +const MenuStack = require('./menu_stack.js'); +const ACS = require('./acs.js'); +const Events = require('./events.js'); -// deps -const stream = require('stream'); -const assert = require('assert'); -const _ = require('lodash'); +// deps +const stream = require('stream'); +const assert = require('assert'); +const _ = require('lodash'); -exports.Client = Client; +exports.Client = Client; -// :TODO: Move all of the key stuff to it's own module +// :TODO: Move all of the key stuff to it's own module // -// Resources & Standards: -// * http://www.ansi-bbs.org/ansi-bbs-core-server.html +// Resources & Standards: +// * http://www.ansi-bbs.org/ansi-bbs-core-server.html // -const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; -const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; -const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; -const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); -const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ +const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; +const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; +const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; +const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); +const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ '(\\d+)(?:;(\\d+))?([~^$])', - '(?:M([@ #!a`])(.)(.))', // mouse stuff + '(?:M([@ #!a`])(.)(.))', // mouse stuff '(?:1;)?(\\d+)?([a-zA-Z@])' ].join('|') + ')'); -const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); -const RE_ESC_CODE_ANYWHERE = new RegExp( [ +const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); +const RE_ESC_CODE_ANYWHERE = new RegExp( [ RE_FUNCTION_KEYCODE_ANYWHERE.source, RE_META_KEYCODE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source, @@ -76,14 +76,14 @@ const RE_ESC_CODE_ANYWHERE = new RegExp( [ function Client(/*input, output*/) { stream.call(this); - const self = this; + const self = this; - this.user = new User(); - this.currentTheme = { info : { name : 'N/A', description : 'None' } }; - this.lastKeyPressMs = Date.now(); - this.menuStack = new MenuStack(this); - this.acs = new ACS(this); - this.mciCache = {}; + this.user = new User(); + this.currentTheme = { info : { name : 'N/A', description : 'None' } }; + this.lastKeyPressMs = Date.now(); + this.menuStack = new MenuStack(this); + this.acs = new ACS(this); + this.mciCache = {}; this.clearMciCache = function() { this.mciCache = {}; @@ -119,34 +119,34 @@ function Client(/*input, output*/) { // - // Peek at incoming |data| and emit events for any special - // handling that may include: - // * Keyboard input - // * ANSI CSR's and the like + // Peek at incoming |data| and emit events for any special + // handling that may include: + // * Keyboard input + // * ANSI CSR's and the like // - // References: - // * http://www.ansi-bbs.org/ansi-bbs-core-server.html - // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ + // References: + // * http://www.ansi-bbs.org/ansi-bbs-core-server.html + // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ // this.getTermClient = function(deviceAttr) { let termClient = { // - // See http://www.fbl.cz/arctel/download/techman.pdf + // See http://www.fbl.cz/arctel/download/techman.pdf // - // Known clients: - // * Irssi ConnectBot (Android) + // Known clients: + // * Irssi ConnectBot (Android) // - '63;1;2' : 'arctel', - '50;86;84;88' : 'vtx', + '63;1;2' : 'arctel', + '50;86;84;88' : 'vtx', }[deviceAttr]; if(!termClient) { if(_.startsWith(deviceAttr, '67;84;101;114;109')) { // - // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt + // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // - // Known clients: - // * SyncTERM + // Known clients: + // * SyncTERM // termClient = 'cterm'; } @@ -156,18 +156,18 @@ function Client(/*input, output*/) { }; this.isMouseInput = function(data) { - return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex - /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex - /\u001b\[(\d+;\d+;\d+)M/.test(data) || - /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || - /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || - /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || - /\u001b\[(O|I)/.test(data); + return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex + /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex + /\u001b\[(\d+;\d+;\d+)M/.test(data) || + /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || + /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || + /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || + /\u001b\[(O|I)/.test(data); }; this.getKeyComponentsFromCode = function(code) { return { - // xterm/gnome + // xterm/gnome 'OP' : { name : 'f1' }, 'OQ' : { name : 'f2' }, 'OR' : { name : 'f3' }, @@ -181,93 +181,93 @@ function Client(/*input, output*/) { 'OF' : { name : 'end' }, 'OH' : { name : 'home' }, - // xterm/rxvt - '[11~' : { name : 'f1' }, - '[12~' : { name : 'f2' }, - '[13~' : { name : 'f3' }, - '[14~' : { name : 'f4' }, + // xterm/rxvt + '[11~' : { name : 'f1' }, + '[12~' : { name : 'f2' }, + '[13~' : { name : 'f3' }, + '[14~' : { name : 'f4' }, - '[1~' : { name : 'home' }, - '[2~' : { name : 'insert' }, - '[3~' : { name : 'delete' }, - '[4~' : { name : 'end' }, - '[5~' : { name : 'page up' }, - '[6~' : { name : 'page down' }, + '[1~' : { name : 'home' }, + '[2~' : { name : 'insert' }, + '[3~' : { name : 'delete' }, + '[4~' : { name : 'end' }, + '[5~' : { name : 'page up' }, + '[6~' : { name : 'page down' }, - // Cygwin & libuv - '[[A' : { name : 'f1' }, - '[[B' : { name : 'f2' }, - '[[C' : { name : 'f3' }, - '[[D' : { name : 'f4' }, - '[[E' : { name : 'f5' }, + // Cygwin & libuv + '[[A' : { name : 'f1' }, + '[[B' : { name : 'f2' }, + '[[C' : { name : 'f3' }, + '[[D' : { name : 'f4' }, + '[[E' : { name : 'f5' }, - // Common impls - '[15~' : { name : 'f5' }, - '[17~' : { name : 'f6' }, - '[18~' : { name : 'f7' }, - '[19~' : { name : 'f8' }, - '[20~' : { name : 'f9' }, - '[21~' : { name : 'f10' }, - '[23~' : { name : 'f11' }, - '[24~' : { name : 'f12' }, + // Common impls + '[15~' : { name : 'f5' }, + '[17~' : { name : 'f6' }, + '[18~' : { name : 'f7' }, + '[19~' : { name : 'f8' }, + '[20~' : { name : 'f9' }, + '[21~' : { name : 'f10' }, + '[23~' : { name : 'f11' }, + '[24~' : { name : 'f12' }, - // xterm - '[A' : { name : 'up arrow' }, - '[B' : { name : 'down arrow' }, - '[C' : { name : 'right arrow' }, - '[D' : { name : 'left arrow' }, - '[E' : { name : 'clear' }, - '[F' : { name : 'end' }, - '[H' : { name : 'home' }, + // xterm + '[A' : { name : 'up arrow' }, + '[B' : { name : 'down arrow' }, + '[C' : { name : 'right arrow' }, + '[D' : { name : 'left arrow' }, + '[E' : { name : 'clear' }, + '[F' : { name : 'end' }, + '[H' : { name : 'home' }, - // PuTTY - '[[5~' : { name : 'page up' }, - '[[6~' : { name : 'page down' }, + // PuTTY + '[[5~' : { name : 'page up' }, + '[[6~' : { name : 'page down' }, - // rvxt - '[7~' : { name : 'home' }, - '[8~' : { name : 'end' }, + // rvxt + '[7~' : { name : 'home' }, + '[8~' : { name : 'end' }, - // rxvt with modifiers - '[a' : { name : 'up arrow', shift : true }, - '[b' : { name : 'down arrow', shift : true }, - '[c' : { name : 'right arrow', shift : true }, - '[d' : { name : 'left arrow', shift : true }, - '[e' : { name : 'clear', shift : true }, + // rxvt with modifiers + '[a' : { name : 'up arrow', shift : true }, + '[b' : { name : 'down arrow', shift : true }, + '[c' : { name : 'right arrow', shift : true }, + '[d' : { name : 'left arrow', shift : true }, + '[e' : { name : 'clear', shift : true }, - '[2$' : { name : 'insert', shift : true }, - '[3$' : { name : 'delete', shift : true }, - '[5$' : { name : 'page up', shift : true }, - '[6$' : { name : 'page down', shift : true }, - '[7$' : { name : 'home', shift : true }, - '[8$' : { name : 'end', shift : true }, + '[2$' : { name : 'insert', shift : true }, + '[3$' : { name : 'delete', shift : true }, + '[5$' : { name : 'page up', shift : true }, + '[6$' : { name : 'page down', shift : true }, + '[7$' : { name : 'home', shift : true }, + '[8$' : { name : 'end', shift : true }, - 'Oa' : { name : 'up arrow', ctrl : true }, - 'Ob' : { name : 'down arrow', ctrl : true }, - 'Oc' : { name : 'right arrow', ctrl : true }, - 'Od' : { name : 'left arrow', ctrl : true }, - 'Oe' : { name : 'clear', ctrl : true }, + 'Oa' : { name : 'up arrow', ctrl : true }, + 'Ob' : { name : 'down arrow', ctrl : true }, + 'Oc' : { name : 'right arrow', ctrl : true }, + 'Od' : { name : 'left arrow', ctrl : true }, + 'Oe' : { name : 'clear', ctrl : true }, - '[2^' : { name : 'insert', ctrl : true }, - '[3^' : { name : 'delete', ctrl : true }, - '[5^' : { name : 'page up', ctrl : true }, - '[6^' : { name : 'page down', ctrl : true }, - '[7^' : { name : 'home', ctrl : true }, - '[8^' : { name : 'end', ctrl : true }, + '[2^' : { name : 'insert', ctrl : true }, + '[3^' : { name : 'delete', ctrl : true }, + '[5^' : { name : 'page up', ctrl : true }, + '[6^' : { name : 'page down', ctrl : true }, + '[7^' : { name : 'home', ctrl : true }, + '[8^' : { name : 'end', ctrl : true }, - // SyncTERM / EtherTerm - '[K' : { name : 'end' }, - '[@' : { name : 'insert' }, - '[V' : { name : 'page up' }, - '[U' : { name : 'page down' }, + // SyncTERM / EtherTerm + '[K' : { name : 'end' }, + '[@' : { name : 'insert' }, + '[V' : { name : 'page up' }, + '[U' : { name : 'page down' }, - // other - '[Z' : { name : 'tab', shift : true }, + // other + '[Z' : { name : 'tab', shift : true }, }[code]; }; this.on('data', function clientData(data) { - // create a uniform format that can be parsed below + // create a uniform format that can be parsed below if(data[0] > 127 && undefined === data[1]) { data[0] -= 128; data = '\u001b' + data.toString('utf-8'); @@ -287,15 +287,15 @@ function Client(/*input, output*/) { data = data.slice(m.index + m[0].length); } - buf = buf.concat(data.split('')); // remainder + buf = buf.concat(data.split('')); // remainder buf.forEach(function bufPart(s) { var key = { - seq : s, - name : undefined, - ctrl : false, - meta : false, - shift : false, + seq : s, + name : undefined, + ctrl : false, + meta : false, + shift : false, }; var parts; @@ -325,55 +325,55 @@ function Client(/*input, output*/) { key.name = 'tab'; } else if('\x7f' === s) { // - // Backspace vs delete is a crazy thing, especially in *nix. - // - ANSI-BBS uses 0x7f for DEL - // - xterm et. al clients send 0x7f for backspace... ugg. + // Backspace vs delete is a crazy thing, especially in *nix. + // - ANSI-BBS uses 0x7f for DEL + // - xterm et. al clients send 0x7f for backspace... ugg. // - // See http://www.hypexr.org/linux_ruboff.php - // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html + // See http://www.hypexr.org/linux_ruboff.php + // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html // if(self.term.isNixTerm()) { - key.name = 'backspace'; + key.name = 'backspace'; } else { - key.name = 'delete'; + key.name = 'delete'; } } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { - // backspace, CTRL-H - key.name = 'backspace'; - key.meta = ('\x1b' === s.charAt(0)); + // backspace, CTRL-H + key.name = 'backspace'; + key.meta = ('\x1b' === s.charAt(0)); } else if('\x1b' === s || '\x1b\x1b' === s) { - key.name = 'escape'; - key.meta = (2 === s.length); + key.name = 'escape'; + key.meta = (2 === s.length); } else if (' ' === s || '\x1b ' === s) { - // rather annoying that space can come in other than just " " - key.name = 'space'; - key.meta = (2 === s.length); + // rather annoying that space can come in other than just " " + key.name = 'space'; + key.meta = (2 === s.length); } else if(1 === s.length && s <= '\x1a') { - // CTRL- - key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); - key.ctrl = true; + // CTRL- + key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); + key.ctrl = true; } else if(1 === s.length && s >= 'a' && s <= 'z') { - // normal, lowercased letter - key.name = s; + // normal, lowercased letter + key.name = s; } else if(1 === s.length && s >= 'A' && s <= 'Z') { - key.name = s.toLowerCase(); - key.shift = true; + key.name = s.toLowerCase(); + key.shift = true; } else if ((parts = RE_META_KEYCODE.exec(s))) { - // meta with character key - key.name = parts[1].toLowerCase(); - key.meta = true; - key.shift = /^[A-Z]$/.test(parts[1]); + // meta with character key + key.name = parts[1].toLowerCase(); + key.meta = true; + key.shift = /^[A-Z]$/.test(parts[1]); } else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { var code = - (parts[1] || '') + (parts[2] || '') + - (parts[4] || '') + (parts[9] || ''); + (parts[1] || '') + (parts[2] || '') + + (parts[4] || '') + (parts[9] || ''); var modifier = (parts[3] || parts[8] || 1) - 1; - key.ctrl = !!(modifier & 4); - key.meta = !!(modifier & 10); - key.shift = !!(modifier & 1); - key.code = code; + key.ctrl = !!(modifier & 4); + key.meta = !!(modifier & 10); + key.shift = !!(modifier & 1); + key.code = code; _.assign(key, self.getKeyComponentsFromCode(code)); } @@ -382,7 +382,7 @@ function Client(/*input, output*/) { if(1 === s.length) { ch = s; } else if('space' === key.name) { - // stupid hack to always get space as a regular char + // stupid hack to always get space as a regular char ch = ' '; } @@ -390,18 +390,18 @@ function Client(/*input, output*/) { key = undefined; } else { // - // Adjust name for CTRL/Shift/Meta modifiers + // Adjust name for CTRL/Shift/Meta modifiers // key.name = - (key.ctrl ? 'ctrl + ' : '') + - (key.meta ? 'meta + ' : '') + - (key.shift ? 'shift + ' : '') + - key.name; + (key.ctrl ? 'ctrl + ' : '') + + (key.meta ? 'meta + ' : '') + + (key.shift ? 'shift + ' : '') + + key.name; } if(key || ch) { if(Config().logging.traceUserKeyboardInput) { - self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line + self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line } self.lastKeyPressMs = Date.now(); @@ -417,15 +417,15 @@ function Client(/*input, output*/) { require('util').inherits(Client, stream); Client.prototype.setInputOutput = function(input, output) { - this.input = input; - this.output = output; + this.input = input; + this.output = output; - this.term = new term.ClientTerminal(this.output); + this.term = new term.ClientTerminal(this.output); }; Client.prototype.setTermType = function(termType) { - this.term.env.TERM = termType; - this.term.termType = termType; + this.term.env.TERM = termType; + this.term.termType = termType; this.log.debug( { termType : termType }, 'Set terminal type'); }; @@ -434,10 +434,10 @@ Client.prototype.startIdleMonitor = function() { this.lastKeyPressMs = Date.now(); // - // Every 1m, check for idle. + // Every 1m, check for idle. // this.idleCheck = setInterval( () => { - const nowMs = Date.now(); + const nowMs = Date.now(); const idleLogoutSeconds = this.user.isAuthenticated() ? Config().misc.idleLogoutSeconds : @@ -468,12 +468,12 @@ Client.prototype.end = function () { try { // - // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH + // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH // - // :TODO: is this OK? + // :TODO: is this OK? return this.output.end.apply(this.output, arguments); } catch(e) { - // TypeError + // TypeError } }; @@ -492,15 +492,15 @@ Client.prototype.waitForKeyPress = function(cb) { }; Client.prototype.isLocal = function() { - // :TODO: Handle ipv6 better + // :TODO: Handle ipv6 better return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); }; /////////////////////////////////////////////////////////////////////////////// -// Default error handlers +// Default error handlers /////////////////////////////////////////////////////////////////////////////// -// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something +// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something Client.prototype.defaultHandlerMissingMod = function() { var self = this; @@ -516,7 +516,7 @@ Client.prototype.defaultHandlerMissingMod = function() { //self.term.write(err); //if(miscUtil.isDevelopment() && err.stack) { - // self.term.write('\n' + err.stack + '\n'); + // self.term.write('\n' + err.stack + '\n'); //} self.end(); @@ -530,7 +530,7 @@ Client.prototype.terminalSupports = function(query) { switch(query) { case 'vtx_audio' : - // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt + // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt return 'vtx' === termClient; case 'vtx_hyperlink' : diff --git a/core/client_connections.js b/core/client_connections.js index bdeb4539..4c16d610 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -1,23 +1,23 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const logger = require('./logger.js'); +// ENiGMA½ +const logger = require('./logger.js'); const Events = require('./events.js'); -// deps -const _ = require('lodash'); -const moment = require('moment'); -const hashids = require('hashids'); +// deps +const _ = require('lodash'); +const moment = require('moment'); +const hashids = require('hashids'); -exports.getActiveConnections = getActiveConnections; -exports.getActiveNodeList = getActiveNodeList; -exports.addNewClient = addNewClient; -exports.removeClient = removeClient; -exports.getConnectionByUserId = getConnectionByUserId; +exports.getActiveConnections = getActiveConnections; +exports.getActiveNodeList = getActiveNodeList; +exports.addNewClient = addNewClient; +exports.removeClient = removeClient; +exports.getConnectionByUserId = getConnectionByUserId; const clientConnections = []; -exports.clientConnections = clientConnections; +exports.clientConnections = clientConnections; function getActiveConnections() { return clientConnections; } @@ -35,48 +35,48 @@ function getActiveNodeList(authUsersOnly) { return _.map(activeConnections, ac => { const entry = { - node : ac.node, - authenticated : ac.user.isAuthenticated(), - userId : ac.user.userId, - action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown', + node : ac.node, + authenticated : ac.user.isAuthenticated(), + userId : ac.user.userId, + action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown', }; // - // There may be a connection, but not a logged in user as of yet + // There may be a connection, but not a logged in user as of yet // if(ac.user.isAuthenticated()) { - entry.userName = ac.user.username; - entry.realName = ac.user.properties.real_name; - entry.location = ac.user.properties.location; - entry.affils = ac.user.properties.affiliation; + entry.userName = ac.user.username; + entry.realName = ac.user.properties.real_name; + entry.location = ac.user.properties.location; + entry.affils = ac.user.properties.affiliation; - const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); - entry.timeOn = moment.duration(diff, 'minutes'); + const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); + entry.timeOn = moment.duration(diff, 'minutes'); } return entry; }); } function addNewClient(client, clientSock) { - const id = client.session.id = clientConnections.push(client) - 1; - const remoteAddress = client.remoteAddress = clientSock.remoteAddress; + const id = client.session.id = clientConnections.push(client) - 1; + const remoteAddress = client.remoteAddress = clientSock.remoteAddress; - // create a uniqe identifier one-time ID for this session + // create a uniqe identifier one-time ID for this session client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); - // Create a client specific logger - // Note that this will be updated @ login with additional information + // Create a client specific logger + // Note that this will be updated @ login with additional information client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } ); const connInfo = { - remoteAddress : remoteAddress, - serverName : client.session.serverName, - isSecure : client.session.isSecure, + remoteAddress : remoteAddress, + serverName : client.session.serverName, + isSecure : client.session.isSecure, }; if(client.log.debug()) { - connInfo.port = clientSock.localPort; - connInfo.family = clientSock.localFamily; + connInfo.port = clientSock.localPort; + connInfo.family = clientSock.localFamily; } client.log.info(connInfo, 'Client connected'); @@ -98,8 +98,8 @@ function removeClient(client) { logger.log.info( { - connectionCount : clientConnections.length, - clientId : client.session.id + connectionCount : clientConnections.length, + clientId : client.session.id }, 'Client disconnected' ); diff --git a/core/client_term.js b/core/client_term.js index 77196586..eb1ee4f9 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -1,39 +1,39 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -var Log = require('./logger.js').log; -var enigmaToAnsi = require('./color_codes.js').enigmaToAnsi; -var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; +// ENiGMA½ +var Log = require('./logger.js').log; +var enigmaToAnsi = require('./color_codes.js').enigmaToAnsi; +var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; -var iconv = require('iconv-lite'); -var assert = require('assert'); -var _ = require('lodash'); +var iconv = require('iconv-lite'); +var assert = require('assert'); +var _ = require('lodash'); -exports.ClientTerminal = ClientTerminal; +exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { - this.output = output; + this.output = output; var outputEncoding = 'cp437'; assert(iconv.encodingExists(outputEncoding)); - // convert line feeds such as \n -> \r\n - this.convertLF = true; + // convert line feeds such as \n -> \r\n + this.convertLF = true; // - // Some terminal we handle specially - // They can also be found in this.env{} + // Some terminal we handle specially + // They can also be found in this.env{} // - var termType = 'unknown'; - var termHeight = 0; - var termWidth = 0; - var termClient = 'unknown'; + var termType = 'unknown'; + var termHeight = 0; + var termWidth = 0; + var termClient = 'unknown'; - this.currentSyncFont = 'not_set'; + this.currentSyncFont = 'not_set'; - // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. - this.env = {}; + // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. + this.env = {}; Object.defineProperty(this, 'outputEncoding', { get : function() { @@ -58,13 +58,13 @@ function ClientTerminal(output) { if(this.isANSI()) { this.outputEncoding = 'cp437'; } else { - // :TODO: See how x84 does this -- only set if local/remote are binary + // :TODO: See how x84 does this -- only set if local/remote are binary this.outputEncoding = 'utf8'; } - // :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification - // Windows telnet will send "VTNT". If so, set termClient='windows' - // there are some others on the page as well + // :TODO: according to this: http://mud-dev.wikidot.com/article:telnet-client-identification + // Windows telnet will send "VTNT". If so, set termClient='windows' + // there are some others on the page as well Log.debug( { encoding : this.outputEncoding }, 'Set output encoding due to terminal type change'); } @@ -110,7 +110,7 @@ ClientTerminal.prototype.disconnect = function() { ClientTerminal.prototype.isNixTerm = function() { // - // Standard *nix type terminals + // Standard *nix type terminals // if(this.termType.startsWith('xterm')) { return true; @@ -121,40 +121,40 @@ ClientTerminal.prototype.isNixTerm = function() { ClientTerminal.prototype.isANSI = function() { // - // ANSI terminals should be encoded to CP437 + // ANSI terminals should be encoded to CP437 // - // Some terminal types provided by Mercyful Fate / Enthral: - // ANSI-BBS - // PC-ANSI - // QANSI - // SCOANSI - // VT100 - // QNX + // Some terminal types provided by Mercyful Fate / Enthral: + // ANSI-BBS + // PC-ANSI + // QANSI + // SCOANSI + // VT100 + // QNX // - // Reports from various terminals + // Reports from various terminals // - // syncterm: - // * SyncTERM + // syncterm: + // * SyncTERM // - // xterm: - // * PuTTY + // xterm: + // * PuTTY // - // ansi-bbs: - // * fTelnet + // ansi-bbs: + // * fTelnet // - // pcansi: - // * ZOC + // pcansi: + // * ZOC // - // screen: - // * ConnectBot (Android) + // screen: + // * ConnectBot (Android) // - // linux: - // * JuiceSSH (note: TERM=linux also) + // linux: + // * JuiceSSH (note: TERM=linux also) // return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm' ].includes(this.termType); }; -// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) +// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) { this.rawWrite(this.encode(s, convertLineFeeds), cb); @@ -178,11 +178,11 @@ ClientTerminal.prototype.pipeWrite = function(s, spec, cb) { spec = spec || 'renegade'; var conv = { - enigma : enigmaToAnsi, - renegade : renegadeToAnsi, + enigma : enigmaToAnsi, + renegade : renegadeToAnsi, }[spec] || renegadeToAnsi; - this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| + this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| }; ClientTerminal.prototype.encode = function(s, convertLineFeeds) { diff --git a/core/color_codes.js b/core/color_codes.js index b04d2c0b..272b611a 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -1,39 +1,39 @@ /* jslint node: true */ 'use strict'; -var ansi = require('./ansi_term.js'); -var getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; +var ansi = require('./ansi_term.js'); +var getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; -var assert = require('assert'); -var _ = require('lodash'); +var assert = require('assert'); +var _ = require('lodash'); -exports.enigmaToAnsi = enigmaToAnsi; -exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes; -exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen; -exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; -exports.controlCodesToAnsi = controlCodesToAnsi; +exports.enigmaToAnsi = enigmaToAnsi; +exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes; +exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen; +exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; +exports.controlCodesToAnsi = controlCodesToAnsi; -// :TODO: Not really happy with the module name of "color_codes". Would like something better +// :TODO: Not really happy with the module name of "color_codes". Would like something better -// Also add: -// * fromCelerity(): | -// * fromPCBoard(): (@X) -// * fromWildcat(): (@@ (same as PCBoard without 'X' prefix and '@' suffix) -// * fromWWIV(): <0-7> -// * fromSyncronet(): -// See http://wiki.synchro.net/custom:colors +// Also add: +// * fromCelerity(): | +// * fromPCBoard(): (@X) +// * fromWildcat(): (@@ (same as PCBoard without 'X' prefix and '@' suffix) +// * fromWWIV(): <0-7> +// * fromSyncronet(): +// See http://wiki.synchro.net/custom:colors -// :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc... +// :TODO: rid of enigmaToAnsi() -- never really use. Instead, create bbsToAnsi() that supports renegade, PCB, WWIV, etc... function enigmaToAnsi(s, client) { if(-1 == s.indexOf('|')) { - return s; // no pipe codes present + return s; // no pipe codes present } - var result = ''; - var re = /\|([A-Z\d]{2}|\|)/g; + var result = ''; + var re = /\|([A-Z\d]{2}|\|)/g; var m; var lastIndex = 0; while((m = re.exec(s))) { @@ -44,14 +44,14 @@ function enigmaToAnsi(s, client) { continue; } - // convert to number + // convert to number val = parseInt(val, 10); if(isNaN(val)) { // - // ENiGMA MCI code? Only available if |client| - // is supplied. + // ENiGMA MCI code? Only available if |client| + // is supplied. // - val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal + val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal } if(_.isString(val)) { @@ -89,51 +89,51 @@ function enigmaStrLen(s) { function ansiSgrFromRenegadeColorCode(cc) { return ansi.sgr({ - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], - 8 : [ 'bold', 'black' ], - 9 : [ 'bold', 'blue' ], - 10 : [ 'bold', 'green' ], - 11 : [ 'bold', 'cyan' ], - 12 : [ 'bold', 'red' ], - 13 : [ 'bold', 'magenta' ], - 14 : [ 'bold', 'yellow' ], - 15 : [ 'bold', 'white' ], + 8 : [ 'bold', 'black' ], + 9 : [ 'bold', 'blue' ], + 10 : [ 'bold', 'green' ], + 11 : [ 'bold', 'cyan' ], + 12 : [ 'bold', 'red' ], + 13 : [ 'bold', 'magenta' ], + 14 : [ 'bold', 'yellow' ], + 15 : [ 'bold', 'white' ], - 16 : [ 'blackBG' ], - 17 : [ 'blueBG' ], - 18 : [ 'greenBG' ], - 19 : [ 'cyanBG' ], - 20 : [ 'redBG' ], - 21 : [ 'magentaBG' ], - 22 : [ 'yellowBG' ], - 23 : [ 'whiteBG' ], + 16 : [ 'blackBG' ], + 17 : [ 'blueBG' ], + 18 : [ 'greenBG' ], + 19 : [ 'cyanBG' ], + 20 : [ 'redBG' ], + 21 : [ 'magentaBG' ], + 22 : [ 'yellowBG' ], + 23 : [ 'whiteBG' ], - 24 : [ 'blink', 'blackBG' ], - 25 : [ 'blink', 'blueBG' ], - 26 : [ 'blink', 'greenBG' ], - 27 : [ 'blink', 'cyanBG' ], - 28 : [ 'blink', 'redBG' ], - 29 : [ 'blink', 'magentaBG' ], - 30 : [ 'blink', 'yellowBG' ], - 31 : [ 'blink', 'whiteBG' ], + 24 : [ 'blink', 'blackBG' ], + 25 : [ 'blink', 'blueBG' ], + 26 : [ 'blink', 'greenBG' ], + 27 : [ 'blink', 'cyanBG' ], + 28 : [ 'blink', 'redBG' ], + 29 : [ 'blink', 'magentaBG' ], + 30 : [ 'blink', 'yellowBG' ], + 31 : [ 'blink', 'whiteBG' ], }[cc] || 'normal'); } function renegadeToAnsi(s, client) { if(-1 == s.indexOf('|')) { - return s; // no pipe codes present + return s; // no pipe codes present } - var result = ''; - var re = /\|([A-Z\d]{2}|\|)/g; + var result = ''; + var re = /\|([A-Z\d]{2}|\|)/g; var m; var lastIndex = 0; while((m = re.exec(s))) { @@ -144,10 +144,10 @@ function renegadeToAnsi(s, client) { continue; } - // convert to number + // convert to number val = parseInt(val, 10); if(isNaN(val)) { - val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal + val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal } if(_.isString(val)) { @@ -164,27 +164,27 @@ function renegadeToAnsi(s, client) { } // -// Converts various control codes popular in BBS packages -// to ANSI escape sequences. Additionaly supports ENiGMA style -// MCI codes. +// Converts various control codes popular in BBS packages +// to ANSI escape sequences. Additionaly supports ENiGMA style +// MCI codes. // -// Supported control code formats: -// * Renegade : |## -// * PCBoard : @X## where the first number/char is FG color, and second is BG -// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix -// * WWIV : ^# +// Supported control code formats: +// * Renegade : |## +// * PCBoard : @X## where the first number/char is FG color, and second is BG +// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix +// * WWIV : ^# // -// TODO: Add Synchronet and Celerity format support +// TODO: Add Synchronet and Celerity format support // -// Resources: -// * http://wiki.synchro.net/custom:colors +// Resources: +// * http://wiki.synchro.net/custom:colors // function controlCodesToAnsi(s, client) { - const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex + const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex let m; - let result = ''; - let lastIndex = 0; + let result = ''; + let lastIndex = 0; let v; let fg; let bg; @@ -192,11 +192,11 @@ function controlCodesToAnsi(s, client) { while((m = RE.exec(s))) { switch(m[0].charAt(0)) { case '|' : - // Renegade or ENiGMA MCI + // Renegade or ENiGMA MCI v = parseInt(m[2], 10); if(isNaN(v)) { - v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal + v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal } if(_.isString(v)) { @@ -208,52 +208,52 @@ function controlCodesToAnsi(s, client) { break; case '@' : - // PCBoard @X## or Wildcat! @##@ + // PCBoard @X## or Wildcat! @##@ if('@' === m[0].substr(-1)) { - // Wildcat! + // Wildcat! v = m[6]; } else { v = m[4]; } fg = { - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], - 8 : [ 'blink', 'black' ], - 9 : [ 'blink', 'blue' ], - A : [ 'blink', 'green' ], - B : [ 'blink', 'cyan' ], - C : [ 'blink', 'red' ], - D : [ 'blink', 'magenta' ], - E : [ 'blink', 'yellow' ], - F : [ 'blink', 'white' ], + 8 : [ 'blink', 'black' ], + 9 : [ 'blink', 'blue' ], + A : [ 'blink', 'green' ], + B : [ 'blink', 'cyan' ], + C : [ 'blink', 'red' ], + D : [ 'blink', 'magenta' ], + E : [ 'blink', 'yellow' ], + F : [ 'blink', 'white' ], }[v.charAt(0)] || ['normal']; bg = { - 0 : [ 'blackBG' ], - 1 : [ 'blueBG' ], - 2 : [ 'greenBG' ], - 3 : [ 'cyanBG' ], - 4 : [ 'redBG' ], - 5 : [ 'magentaBG' ], - 6 : [ 'yellowBG' ], - 7 : [ 'whiteBG' ], + 0 : [ 'blackBG' ], + 1 : [ 'blueBG' ], + 2 : [ 'greenBG' ], + 3 : [ 'cyanBG' ], + 4 : [ 'redBG' ], + 5 : [ 'magentaBG' ], + 6 : [ 'yellowBG' ], + 7 : [ 'whiteBG' ], - 8 : [ 'bold', 'blackBG' ], - 9 : [ 'bold', 'blueBG' ], - A : [ 'bold', 'greenBG' ], - B : [ 'bold', 'cyanBG' ], - C : [ 'bold', 'redBG' ], - D : [ 'bold', 'magentaBG' ], - E : [ 'bold', 'yellowBG' ], - F : [ 'bold', 'whiteBG' ], + 8 : [ 'bold', 'blackBG' ], + 9 : [ 'bold', 'blueBG' ], + A : [ 'bold', 'greenBG' ], + B : [ 'bold', 'cyanBG' ], + C : [ 'bold', 'redBG' ], + D : [ 'bold', 'magentaBG' ], + E : [ 'bold', 'yellowBG' ], + F : [ 'bold', 'whiteBG' ], }[v.charAt(1)] || [ 'normal' ]; v = ansi.sgr(fg.concat(bg)); @@ -267,16 +267,16 @@ function controlCodesToAnsi(s, client) { v += m[0]; } else { v = ansi.sgr({ - 0 : [ 'reset', 'black' ], - 1 : [ 'bold', 'cyan' ], - 2 : [ 'bold', 'yellow' ], - 3 : [ 'reset', 'magenta' ], - 4 : [ 'bold', 'white', 'blueBG' ], - 5 : [ 'reset', 'green' ], - 6 : [ 'bold', 'blink', 'red' ], - 7 : [ 'bold', 'blue' ], - 8 : [ 'reset', 'blue' ], - 9 : [ 'reset', 'cyan' ], + 0 : [ 'reset', 'black' ], + 1 : [ 'bold', 'cyan' ], + 2 : [ 'bold', 'yellow' ], + 3 : [ 'reset', 'magenta' ], + 4 : [ 'bold', 'white', 'blueBG' ], + 5 : [ 'reset', 'green' ], + 6 : [ 'bold', 'blink', 'red' ], + 7 : [ 'bold', 'blue' ], + 8 : [ 'reset', 'blue' ], + 9 : [ 'reset', 'cyan' ], }[v] || 'normal'); } diff --git a/core/combatnet.js b/core/combatnet.js index d6616449..1b53a153 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -1,29 +1,29 @@ /* jslint node: true */ 'use strict'; -// enigma-bbs -const MenuModule = require('../core/menu_module.js').MenuModule; -const resetScreen = require('../core/ansi_term.js').resetScreen; +// enigma-bbs +const MenuModule = require('../core/menu_module.js').MenuModule; +const resetScreen = require('../core/ansi_term.js').resetScreen; -// deps -const async = require('async'); -const _ = require('lodash'); +// deps +const async = require('async'); +const _ = require('lodash'); const RLogin = require('rlogin'); exports.moduleInfo = { - name : 'CombatNet', - desc : 'CombatNet Access Module', - author : 'Dave Stephens', + name : 'CombatNet', + desc : 'CombatNet Access Module', + author : 'Dave Stephens', }; exports.getModule = class CombatNetModule extends MenuModule { constructor(options) { super(options); - // establish defaults - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'bbs.combatnet.us'; - this.config.rloginPort = this.config.rloginPort || 4513; + // establish defaults + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'bbs.combatnet.us'; + this.config.rloginPort = this.config.rloginPort || 4513; } initSequence() { @@ -51,7 +51,7 @@ exports.getModule = class CombatNetModule extends MenuModule { }; const rlogin = new RLogin( - { 'clientUsername' : self.config.password, + { 'clientUsername' : self.config.password, 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, 'host' : self.config.host, 'port' : self.config.rloginPort, @@ -79,7 +79,7 @@ exports.getModule = class CombatNetModule extends MenuModule { } rlogin.on('connect', - /* The 'connect' event handler will be supplied with one argument, + /* The 'connect' event handler will be supplied with one argument, a boolean indicating whether or not the connection was established. */ function(state) { @@ -101,7 +101,7 @@ exports.getModule = class CombatNetModule extends MenuModule { // connect... rlogin.connect(); - // note: no explicit callback() until we're finished! + // note: no explicit callback() until we're finished! } ], err => { @@ -109,7 +109,7 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.log.warn( { error : err.message }, 'CombatNet error'); } - // if the client is still here, go to previous + // if the client is still here, go to previous self.prevMenu(); } ); diff --git a/core/conf_area_util.js b/core/conf_area_util.js index 7c4bf5bb..1c0b65c4 100644 --- a/core/conf_area_util.js +++ b/core/conf_area_util.js @@ -1,15 +1,15 @@ /* jslint node: true */ 'use strict'; -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); -exports.sortAreasOrConfs = sortAreasOrConfs; +exports.sortAreasOrConfs = sortAreasOrConfs; // -// Method for sorting message, file, etc. areas and confs -// If the sort key is present and is a number, sort in numerical order; -// Otherwise, use a locale comparison on the sort key or name as a fallback +// Method for sorting message, file, etc. areas and confs +// If the sort key is present and is a number, sort in numerical order; +// Otherwise, use a locale comparison on the sort key or name as a fallback // function sortAreasOrConfs(areasOrConfs, type) { let entryA; @@ -24,7 +24,7 @@ function sortAreasOrConfs(areasOrConfs, type) { } else { const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; - return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare + return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare } }); } \ No newline at end of file diff --git a/core/config.js b/core/config.js index 3a1359d7..f85ffe2a 100644 --- a/core/config.js +++ b/core/config.js @@ -1,17 +1,17 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Errors = require('./enig_error.js').Errors; +// ENiGMA½ +const Errors = require('./enig_error.js').Errors; -// deps -const paths = require('path'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); +// deps +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); -exports.init = init; -exports.getDefaultPath = getDefaultPath; +exports.init = init; +exports.getDefaultPath = getDefaultPath; let currentConfiguration = {}; @@ -31,7 +31,7 @@ function hasMessageConferenceAndArea(config) { let result = false; _.forEach(nonInternalConfs, confTag => { if(_.has(config.messageConferences[confTag], 'areas') && - Object.keys(config.messageConferences[confTag].areas) > 0) + Object.keys(config.messageConferences[confTag].areas) > 0) { result = true; return false; // stop iteration @@ -48,9 +48,9 @@ function mergeValidateAndFinalize(config, cb) { const mergedConfig = _.mergeWith( getDefaultConfig(), config, (conf1, conf2) => { - // Arrays should always concat + // Arrays should always concat if(_.isArray(conf1)) { - // :TODO: look for collisions & override dupes + // :TODO: look for collisions & override dupes return conf1.concat(conf2); } } @@ -60,16 +60,16 @@ function mergeValidateAndFinalize(config, cb) { }, function validate(mergedConfig, callback) { // - // Various sections must now exist in config + // Various sections must now exist in config // - // :TODO: Logic is broken here: + // :TODO: Logic is broken here: if(hasMessageConferenceAndArea(mergedConfig)) { return callback(Errors.MissingConfig('Please create at least one message conference and area!')); } return callback(null, mergedConfig); }, function setIt(mergedConfig, callback) { - // :TODO: .config property is to be deprecated once conversions are done + // :TODO: .config property is to be deprecated once conversions are done exports.config = currentConfiguration = mergedConfig; exports.get = () => currentConfiguration; @@ -101,8 +101,8 @@ function init(configPath, options, cb) { const ConfigCache = require('./config_cache.js'); const getConfigOptions = { - filePath : configPath, - noWatch : options.noWatch, + filePath : configPath, + noWatch : options.noWatch, }; if(!options.noWatch) { getConfigOptions.callback = changed; @@ -117,138 +117,138 @@ function init(configPath, options, cb) { } function getDefaultPath() { - // e.g. /enigma-bbs-install-path/config/ + // e.g. /enigma-bbs-install-path/config/ return './config/'; } function getDefaultConfig() { return { general : { - boardName : 'Another Fine ENiGMA½ BBS', + boardName : 'Another Fine ENiGMA½ BBS', - closedSystem : false, // is the system closed to new users? + closedSystem : false, // is the system closed to new users? - loginAttempts : 3, + loginAttempts : 3, - menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config) - promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config) + menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config) + promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config) }, - // :TODO: see notes below about 'theme' section - move this! + // :TODO: see notes below about 'theme' section - move this! preLoginTheme : 'luciano_blocktronics', users : { - usernameMin : 2, - usernameMax : 16, // Note that FidoNet wants 36 max - usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ ]+$', + usernameMin : 2, + usernameMax : 16, // Note that FidoNet wants 36 max + usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ ]+$', - passwordMin : 6, - passwordMax : 128, - badPassFile : paths.join(__dirname, '../misc/10_million_password_list_top_10000.txt'), // https://github.com/danielmiessler/SecLists + passwordMin : 6, + passwordMax : 128, + badPassFile : paths.join(__dirname, '../misc/10_million_password_list_top_10000.txt'), // https://github.com/danielmiessler/SecLists - realNameMax : 32, - locationMax : 32, - affilsMax : 32, - emailMax : 255, - webMax : 255, + realNameMax : 32, + locationMax : 32, + affilsMax : 32, + emailMax : 255, + webMax : 255, - requireActivation : false, // require SysOp activation? false = auto-activate + requireActivation : false, // require SysOp activation? false = auto-activate - groups : [ 'users', 'sysops' ], // built in groups - defaultGroups : [ 'users' ], // default groups new users belong to + groups : [ 'users', 'sysops' ], // built in groups + defaultGroups : [ 'users' ], // default groups new users belong to - newUserNames : [ 'new', 'apply' ], // Names reserved for applying + newUserNames : [ 'new', 'apply' ], // Names reserved for applying - badUserNames : [ + badUserNames : [ 'sysop', 'admin', 'administrator', 'root', 'all', 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' ], }, - // :TODO: better name for "defaults"... which is redundant here! + // :TODO: better name for "defaults"... which is redundant here! /* - Concept - "theme" : { - "default" : "defaultThemeName", // or "*" - "preLogin" : "*", - "passwordChar" : "*", - ... - } - */ + Concept + "theme" : { + "default" : "defaultThemeName", // or "*" + "preLogin" : "*", + "passwordChar" : "*", + ... + } + */ defaults : { - theme : 'luciano_blocktronics', - passwordChar : '*', // TODO: move to user ? - dateFormat : { - short : 'MM/DD/YYYY', - long : 'ddd, MMMM Do, YYYY', + theme : 'luciano_blocktronics', + passwordChar : '*', // TODO: move to user ? + dateFormat : { + short : 'MM/DD/YYYY', + long : 'ddd, MMMM Do, YYYY', }, timeFormat : { - short : 'h:mm a', + short : 'h:mm a', }, dateTimeFormat : { - short : 'MM/DD/YYYY h:mm a', - long : 'ddd, MMMM Do, YYYY, h:mm a', + short : 'MM/DD/YYYY h:mm a', + long : 'ddd, MMMM Do, YYYY, h:mm a', } }, menus : { - cls : true, // Clear screen before each menu by default? + cls : true, // Clear screen before each menu by default? }, - paths : { - config : paths.join(__dirname, './../config/'), - mods : paths.join(__dirname, './../mods/'), - loginServers : paths.join(__dirname, './servers/login/'), - contentServers : paths.join(__dirname, './servers/content/'), + paths : { + config : paths.join(__dirname, './../config/'), + mods : paths.join(__dirname, './../mods/'), + loginServers : paths.join(__dirname, './servers/login/'), + contentServers : paths.join(__dirname, './servers/content/'), - scannerTossers : paths.join(__dirname, './scanner_tossers/'), - mailers : paths.join(__dirname, './mailers/') , + scannerTossers : paths.join(__dirname, './scanner_tossers/'), + mailers : paths.join(__dirname, './mailers/') , - art : paths.join(__dirname, './../art/general/'), - themes : paths.join(__dirname, './../art/themes/'), - logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such - db : paths.join(__dirname, './../db/'), - modsDb : paths.join(__dirname, './../db/mods/'), - dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/ - misc : paths.join(__dirname, './../misc/'), + art : paths.join(__dirname, './../art/general/'), + themes : paths.join(__dirname, './../art/themes/'), + logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such + db : paths.join(__dirname, './../db/'), + modsDb : paths.join(__dirname, './../db/mods/'), + dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/ + misc : paths.join(__dirname, './../misc/'), }, loginServers : { telnet : { - port : 8888, - enabled : true, - firstMenu : 'telnetConnected', + port : 8888, + enabled : true, + firstMenu : 'telnetConnected', }, ssh : { - port : 8889, - enabled : false, // default to false as PK/pass in config.hjson are required + port : 8889, + enabled : false, // default to false as PK/pass in config.hjson are required // - // Private key in PEM format + // Private key in PEM format // - // Generating your PK: - // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 + // Generating your PK: + // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 // - // Then, set servers.ssh.privateKeyPass to the password you use above - // in your config.hjson + // Then, set servers.ssh.privateKeyPass to the password you use above + // in your config.hjson // - privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), - firstMenu : 'sshConnected', - firstMenuNewUser : 'sshConnectedNewUser', + privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), + firstMenu : 'sshConnected', + firstMenuNewUser : 'sshConnectedNewUser', }, webSocket : { ws : { - // non-secure ws:// - enabled : false, - port : 8810, + // non-secure ws:// + enabled : false, + port : 8810, }, wss : { - // secure ws:// - // must provide valid certPem and keyPem - enabled : false, - port : 8811, - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + // secure ws:// + // must provide valid certPem and keyPem + enabled : false, + port : 8811, + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), }, }, }, @@ -261,200 +261,200 @@ function getDefaultConfig() { resetPassword : { // - // The following templates have these variables available to them: + // The following templates have these variables available to them: // - // * %BOARDNAME% : Name of BBS - // * %USERNAME% : Username of whom to reset password - // * %TOKEN% : Reset token - // * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page, - // URL to POST submit reset form. + // * %BOARDNAME% : Name of BBS + // * %USERNAME% : Username of whom to reset password + // * %TOKEN% : Reset token + // * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page, + // URL to POST submit reset form. - // templates for pw reset *email* - resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version - resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version + // templates for pw reset *email* + resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version + resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version - // tempalte for pw reset *landing page* + // tempalte for pw reset *landing page* // - resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'), + resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'), }, http : { enabled : false, - port : 8080, + port : 8080, }, https : { - enabled : false, - port : 8443, - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), + enabled : false, + port : 8443, + certPem : paths.join(__dirname, './../config/https_cert.pem'), + keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), } } }, infoExtractUtils : { Exiftool2Desc : { - cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x + cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x }, Exiftool : { - cmd : 'exiftool', - args : [ + cmd : 'exiftool', + args : [ '-charset', 'utf8', '{filePath}', - // exclude the following: + // exclude the following: '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', '--metadatadate', '--xmptoolkit' ] }, XDMS2Desc : { - // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'd', '{filePath}' ] + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'd', '{filePath}' ] }, XDMS2LongDesc : { - // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'f', '{filePath}' ] + // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html + cmd : 'xdms', + args : [ 'f', '{filePath}' ] } }, fileTypes : { // - // File types explicitly known to the system. Here we can configure - // information extraction, archive treatment, etc. + // File types explicitly known to the system. Here we can configure + // information extraction, archive treatment, etc. // - // MIME types can be found in mime-db: https://github.com/jshttp/mime-db + // MIME types can be found in mime-db: https://github.com/jshttp/mime-db // - // Resources for signature/magic bytes: - // * http://www.garykessler.net/library/file_sigs.html + // Resources for signature/magic bytes: + // * http://www.garykessler.net/library/file_sigs.html // // - // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads - // :TODO: textual : bool -- if text, we can view. - // :TODO: asText : { cmd, args[] } -> viewable text + // :TODO: text/x-ansi -> SAUCE extraction for .ans uploads + // :TODO: textual : bool -- if text, we can view. + // :TODO: asText : { cmd, args[] } -> viewable text // - // Audio + // Audio // 'audio/mpeg' : { - desc : 'MP3 Audio', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + desc : 'MP3 Audio', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, 'application/pdf' : { - desc : 'Adobe PDF', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + desc : 'Adobe PDF', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, // - // Video + // Video // 'video/mp4' : { - desc : 'MPEG Video', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + desc : 'MPEG Video', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, 'video/x-matroska ' : { - desc : 'Matroska Video', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + desc : 'Matroska Video', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, 'video/x-msvideo' : { - desc : 'Audio Video Interleave', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + desc : 'Audio Video Interleave', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, // - // Images + // Images // - 'image/jpeg' : { - desc : 'JPEG Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'image/jpeg' : { + desc : 'JPEG Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, - 'image/png' : { - desc : 'Portable Network Graphic Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'image/png' : { + desc : 'Portable Network Graphic Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, 'image/gif' : { - desc : 'Graphics Interchange Format Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + desc : 'Graphics Interchange Format Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, 'image/webp' : { - desc : 'WebP Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + desc : 'WebP Image', + shortDescUtil : 'Exiftool2Desc', + longDescUtil : 'Exiftool', }, // - // Archives + // Archives // 'application/zip' : { - desc : 'ZIP Archive', - sig : '504b0304', - offset : 0, - archiveHandler : '7Zip', + desc : 'ZIP Archive', + sig : '504b0304', + offset : 0, + archiveHandler : '7Zip', }, /* - 'application/x-cbr' : { - desc : 'Comic Book Archive', - sig : '504b0304', - }, - */ + 'application/x-cbr' : { + desc : 'Comic Book Archive', + sig : '504b0304', + }, + */ 'application/x-arj' : { - desc : 'ARJ Archive', - sig : '60ea', - offset : 0, - archiveHandler : 'Arj', + desc : 'ARJ Archive', + sig : '60ea', + offset : 0, + archiveHandler : 'Arj', }, 'application/x-rar-compressed' : { - desc : 'RAR Archive', - sig : '526172211a0700', - offset : 0, - archiveHandler : 'Rar', + desc : 'RAR Archive', + sig : '526172211a0700', + offset : 0, + archiveHandler : 'Rar', }, 'application/gzip' : { - desc : 'Gzip Archive', - sig : '1f8b', - offset : 0, - archiveHandler : 'TarGz', + desc : 'Gzip Archive', + sig : '1f8b', + offset : 0, + archiveHandler : 'TarGz', }, - // :TODO: application/x-bzip + // :TODO: application/x-bzip 'application/x-bzip2' : { - desc : 'BZip2 Archive', - sig : '425a68', - offset : 0, - archiveHandler : '7Zip', + desc : 'BZip2 Archive', + sig : '425a68', + offset : 0, + archiveHandler : '7Zip', }, 'application/x-lzh-compressed' : { - desc : 'LHArc Archive', - sig : '2d6c68', - offset : 2, - archiveHandler : 'Lha', + desc : 'LHArc Archive', + sig : '2d6c68', + offset : 2, + archiveHandler : 'Lha', }, 'application/x-lzx' : { - desc : 'LZX Archive', - sig : '4c5a5800', - offset : 0, - archiveHandler : 'Lzx', + desc : 'LZX Archive', + sig : '4c5a5800', + offset : 0, + archiveHandler : 'Lzx', }, 'application/x-7z-compressed' : { - desc : '7-Zip Archive', - sig : '377abcaf271c', - offset : 0, - archiveHandler : '7Zip', + desc : '7-Zip Archive', + sig : '377abcaf271c', + offset : 0, + archiveHandler : '7Zip', }, // - // Generics that need further mapping + // Generics that need further mapping // 'application/octet-stream' : [ { - desc : 'Amiga DISKMASHER', - sig : '444d5321', // DMS! - ext : '.dms', - shortDescUtil : 'XDMS2Desc', - longDescUtil : 'XDMS2LongDesc', + desc : 'Amiga DISKMASHER', + sig : '444d5321', // DMS! + ext : '.dms', + shortDescUtil : 'XDMS2Desc', + longDescUtil : 'XDMS2LongDesc', } ] }, @@ -462,119 +462,119 @@ function getDefaultConfig() { archives : { archivers : { '7Zip' : { - compress : { - cmd : '7za', - args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], + compress : { + cmd : '7za', + args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], }, - decompress : { - cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? + decompress : { + cmd : '7za', + args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? }, - list : { - cmd : '7za', - args : [ 'l', '{archivePath}' ], - entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', + list : { + cmd : '7za', + args : [ 'l', '{archivePath}' ], + entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', }, - extract : { - cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], + extract : { + cmd : '7za', + args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], }, }, Lha : { // - // 'lha' command can be obtained from: - // * apt-get: lhasa + // 'lha' command can be obtained from: + // * apt-get: lhasa // - // (compress not currently supported) + // (compress not currently supported) // - decompress : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}' ], + decompress : { + cmd : 'lha', + args : [ '-efw={extractPath}', '{archivePath}' ], }, - list : { - cmd : 'lha', - args : [ '-l', '{archivePath}' ], - entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', + list : { + cmd : 'lha', + args : [ '-l', '{archivePath}' ], + entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', }, - extract : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] + extract : { + cmd : 'lha', + args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] } }, Lzx : { // - // 'unlzx' command can be obtained from: - // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) - // * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html - // * Source: http://xavprods.free.fr/lzx/ + // 'unlzx' command can be obtained from: + // * Debian based: https://launchpad.net/~rzr/+archive/ubuntu/ppa/+build/2486127 (amd64/x86_64) + // * RedHat: https://fedora.pkgs.org/28/rpm-sphere/unlzx-1.1-4.1.x86_64.rpm.html + // * Source: http://xavprods.free.fr/lzx/ // - decompress : { - cmd : 'unlzx', - // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first - args : [ '-x', '{archivePath}' ], + decompress : { + cmd : 'unlzx', + // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first + args : [ '-x', '{archivePath}' ], }, - list : { - cmd : 'unlzx', - args : [ '-v', '{archivePath}' ], - entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', + list : { + cmd : 'unlzx', + args : [ '-v', '{archivePath}' ], + entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', } }, Arj : { // - // 'arj' command can be obtained from: - // * apt-get: arj + // 'arj' command can be obtained from: + // * apt-get: arj // - decompress : { - cmd : 'arj', - args : [ 'x', '{archivePath}', '{extractPath}' ], + decompress : { + cmd : 'arj', + args : [ 'x', '{archivePath}', '{extractPath}' ], }, - list : { - cmd : 'arj', - args : [ 'l', '{archivePath}' ], - entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', - entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } - fileName : 1, - byteSize : 2, + list : { + cmd : 'arj', + args : [ 'l', '{archivePath}' ], + entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', + entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } + fileName : 1, + byteSize : 2, } }, - extract : { - cmd : 'arj', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], + extract : { + cmd : 'arj', + args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], } }, Rar : { - decompress : { - cmd : 'unrar', - args : [ 'x', '{archivePath}', '{extractPath}' ], + decompress : { + cmd : 'unrar', + args : [ 'x', '{archivePath}', '{extractPath}' ], }, - list : { - cmd : 'unrar', - args : [ 'l', '{archivePath}' ], - entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', + list : { + cmd : 'unrar', + args : [ 'l', '{archivePath}' ], + entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', }, - extract : { - cmd : 'unrar', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], + extract : { + cmd : 'unrar', + args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], } }, TarGz : { - decompress : { - cmd : 'tar', - args : [ '-xf', '{archivePath}', '-C', '{extractPath}', '--strip-components=1' ], + 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]+)$', + 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}' ], + extract : { + cmd : 'tar', + args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ], } } }, @@ -582,89 +582,89 @@ function getDefaultConfig() { fileTransferProtocols : { // - // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ + // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ // zmodem8kSexyz : { - name : 'ZModem 8k (SEXYZ)', - type : 'external', - sort : 1, - external : { - // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems - sendCmd : 'sexyz', - sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], - recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], + name : 'ZModem 8k (SEXYZ)', + type : 'external', + sort : 1, + external : { + // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems + sendCmd : 'sexyz', + sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], + recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], } }, xmodemSexyz : { - name : 'XModem (SEXYZ)', - type : 'external', - sort : 3, - external : { - sendCmd : 'sexyz', - sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ] + name : 'XModem (SEXYZ)', + type : 'external', + sort : 3, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ] } }, ymodemSexyz : { - name : 'YModem (SEXYZ)', - type : 'external', - sort : 4, - external : { - sendCmd : 'sexyz', - sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', 'ry', '{uploadDir}' ], + name : 'YModem (SEXYZ)', + type : 'external', + sort : 4, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', 'ry', '{uploadDir}' ], } }, zmodem8kSz : { - name : 'ZModem 8k', - type : 'external', - sort : 2, - external : { - sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" - sendArgs : [ - // :TODO: try -q + name : 'ZModem 8k', + type : 'external', + sort : 2, + external : { + sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + sendArgs : [ + // :TODO: try -q '--zmodem', '--try-8k', '--binary', '--restricted', '{filePaths}' ], - recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" - recvArgs : [ - '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} + recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + recvArgs : [ + '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} ], - // :TODO: can we not just use --escape ? - escapeTelnet : true, // set to true to escape Telnet codes such as IAC + // :TODO: can we not just use --escape ? + escapeTelnet : true, // set to true to escape Telnet codes such as IAC } } }, messageAreaDefaults : { // - // The following can be override per-area as well + // The following can be override per-area as well // - maxMessages : 1024, // 0 = unlimited - maxAgeDays : 0, // 0 = unlimited + maxMessages : 1024, // 0 = unlimited + maxAgeDays : 0, // 0 = unlimited }, messageConferences : { system_internal : { - name : 'System Internal', - desc : 'Built in conference for private messages, bulletins, etc.', + name : 'System Internal', + desc : 'Built in conference for private messages, bulletins, etc.', areas : { private_mail : { - name : 'Private Mail', - desc : 'Private user to user mail/email', - maxExternalSentAgeDays : 30, // max external "outbox" item age + name : 'Private Mail', + desc : 'Private user to user mail/email', + maxExternalSentAgeDays : 30, // max external "outbox" item age }, local_bulletin : { - name : 'System Bulletins', - desc : 'Bulletin messages for all users', + name : 'System Bulletins', + desc : 'Bulletin messages for all users', } } } @@ -673,99 +673,99 @@ function getDefaultConfig() { scannerTossers : { ftn_bso : { paths : { - outbound : paths.join(__dirname, './../mail/ftn_out/'), - inbound : paths.join(__dirname, './../mail/ftn_in/'), - secInbound : paths.join(__dirname, './../mail/ftn_secin/'), - reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. - //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), - // set 'retain' to a valid path to keep good pkt files + outbound : paths.join(__dirname, './../mail/ftn_out/'), + inbound : paths.join(__dirname, './../mail/ftn_in/'), + secInbound : paths.join(__dirname, './../mail/ftn_secin/'), + reject : paths.join(__dirname, './../mail/reject/'), // bad pkt, bundles, TIC attachments that fail any check, etc. + //outboundNetMail : paths.join(__dirname, './../mail/ftn_netmail_out/'), + // set 'retain' to a valid path to keep good pkt files }, // - // Packet and (ArcMail) bundle target sizes are just that: targets. - // Actual sizes may be slightly larger when we must place a full - // PKT contents *somewhere* + // Packet and (ArcMail) bundle target sizes are just that: targets. + // Actual sizes may be slightly larger when we must place a full + // PKT contents *somewhere* // - packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt - 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 + packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt + 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 : { - secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) - uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) - allowReplace : false, // use "Replaces" TIC field - descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc + secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) + uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) + allowReplace : false, // use "Replaces" TIC field + descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc } } }, fileBase: { - // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: - areaStoragePrefix : paths.join(__dirname, './../file_base/'), + // areas with an explicit |storageDir| will be stored relative to |areaStoragePrefix|: + areaStoragePrefix : paths.join(__dirname, './../file_base/'), - maxDescFileByteSize : 471859, // ~1/4 MB - maxDescLongFileByteSize : 524288, // 1/2 MB + maxDescFileByteSize : 471859, // ~1/4 MB + maxDescLongFileByteSize : 524288, // 1/2 MB fileNamePatterns: { - // These are NOT case sensitive - // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ - // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. - desc : [ - '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' // eslint-disable-line no-useless-escape + // These are NOT case sensitive + // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ + // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. + desc : [ + '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' // eslint-disable-line no-useless-escape ], - // common README filename - https://en.wikipedia.org/wiki/README - descLong : [ - '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' // eslint-disable-line no-useless-escape + // common README filename - https://en.wikipedia.org/wiki/README + descLong : [ + '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' // eslint-disable-line no-useless-escape ], }, yearEstPatterns: [ // - // Patterns should produce the year in the first submatch. - // The extracted year may be YY or YYYY + // Patterns should produce the year in the first submatch. + // The extracted year may be YY or YYYY // - '\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ... - '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ... - '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ... - '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... - //'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc. - //"\\b('[1789][0-9])\\b", // eslint-disable-line quotes + '\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yyyy-mm-dd, yyyy/mm/dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1-2][0-9][0-9]{2}))\\b', // mm/dd/yyyy, mm.dd.yyyy, ... + '\\b((?:[1789][0-9]))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]\\b', // yy-mm-dd, yy-mm-dd, ... + '\\b[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[1789][0-9]))\\b', // mm-dd-yy, mm/dd/yy, ... + //'\\b((?:[1-2][0-9][0-9]{2}))[\\-\\/\\.][0-3]?[0-9][\\-\\/\\.][0-3]?[0-9]|[0-3]?[0-9][\\-\\/\\.][0-3]?[0-9][\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', // yyyy-mm-dd, m/d/yyyy, mm-dd-yyyy, etc. + //"\\b('[1789][0-9])\\b", // eslint-disable-line quotes '\\b[0-3]?[0-9][\\-\\/\\.](?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)[\\-\\/\\.]((?:[0-9]{2})?[0-9]{2})\\b', - '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 - '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority - '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries - '\\b\'([17-9][0-9])\\b', // '95, '17, ... - // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. + '\\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december),?\\s[0-9]+(?:st|nd|rd|th)?,?\\s((?:[0-9]{2})?[0-9]{2})\\b', // November 29th, 1997 + '\\(((?:19|20)[0-9]{2})\\)', // (19xx) or (20xx) -- with parens -- do this before 19xx 20xx such that this has priority + '\\b((?:19|20)[0-9]{2})\\b', // simple 19xx or 20xx with word boundaries + '\\b\'([17-9][0-9])\\b', // '95, '17, ... + // :TODO: DD/MMM/YY, DD/MMMM/YY, DD/MMM/YYYY, etc. ], web : { - path : '/f/', - routePath : '/f/[a-zA-Z0-9]+$', - expireMinutes : 1440, // 1 day + path : '/f/', + routePath : '/f/[a-zA-Z0-9]+$', + expireMinutes : 1440, // 1 day }, // - // File area storage location tag/value pairs. - // Non-absolute paths are relative to |areaStoragePrefix|. + // File area storage location tag/value pairs. + // Non-absolute paths are relative to |areaStoragePrefix|. // storageTags : { - sys_msg_attach : 'sys_msg_attach', - sys_temp_download : 'sys_temp_download', + sys_msg_attach : 'sys_msg_attach', + sys_temp_download : 'sys_temp_download', }, areas: { system_message_attachment : { - name : 'System Message Attachments', - desc : 'File attachments to messages', - storageTags : [ 'sys_msg_attach' ], + name : 'System Message Attachments', + desc : 'File attachments to messages', + storageTags : [ 'sys_msg_attach' ], }, system_temporary_download : { - name : 'System Temporary Downloads', - desc : 'Temporary downloadables', - storageTags : [ 'sys_temp_download' ], + name : 'System Temporary Downloads', + desc : 'Temporary downloadables', + storageTags : [ 'sys_temp_download' ], } } }, @@ -774,63 +774,63 @@ function getDefaultConfig() { events : { trimMessageAreas : { - // may optionally use [or ]@watch:/path/to/file - schedule : 'every 24 hours', + // may optionally use [or ]@watch:/path/to/file + schedule : 'every 24 hours', - // action: - // - @method:path/to/module.js:theMethodName - // (path is relative to engima base dir) + // action: + // - @method:path/to/module.js:theMethodName + // (path is relative to engima base dir) // - // - @execute:/path/to/something/executable.sh + // - @execute:/path/to/something/executable.sh // - action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', }, updateFileAreaStats : { - schedule : 'every 1 hours', - action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + schedule : 'every 1 hours', + action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', }, forgotPasswordMaintenance : { - schedule : 'every 24 hours', - action : '@method:core/web_password_reset.js:performMaintenanceTask', - args : [ '24 hours' ] // items older than this will be removed + schedule : 'every 24 hours', + action : '@method:core/web_password_reset.js:performMaintenanceTask', + args : [ '24 hours' ] // items older than this will be removed }, // - // Enable the following entry in your config.hjson to periodically create/update - // DESCRIPT.ION files for your file base + // Enable the following entry in your config.hjson to periodically create/update + // DESCRIPT.ION files for your file base // /* - updateDescriptIonFiles : { - schedule : 'on the last day of the week', - action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', - } - */ + updateDescriptIonFiles : { + schedule : 'on the last day of the week', + action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', + } + */ } }, misc : { - preAuthIdleLogoutSeconds : 60 * 3, // 3m - idleLogoutSeconds : 60 * 6, // 6m + preAuthIdleLogoutSeconds : 60 * 3, // 3m + idleLogoutSeconds : 60 * 6, // 6m }, logging : { - level : 'debug', + level : 'debug', - rotatingFile : { // set to 'disabled' or false to disable - type : 'rotating-file', - fileName : 'enigma-bbs.log', - period : '1d', - count : 3, - level : 'debug', + rotatingFile : { // set to 'disabled' or false to disable + type : 'rotating-file', + fileName : 'enigma-bbs.log', + period : '1d', + count : 3, + level : 'debug', } - // :TODO: syslog - https://github.com/mcavage/node-bunyan-syslog + // :TODO: syslog - https://github.com/mcavage/node-bunyan-syslog }, debug : { - assertsEnabled : false, + assertsEnabled : false, } }; } diff --git a/core/config_cache.js b/core/config_cache.js index 15143efc..e8e06f14 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -// deps -const paths = require('path'); -const fs = require('graceful-fs'); -const hjson = require('hjson'); -const sane = require('sane'); +// deps +const paths = require('path'); +const fs = require('graceful-fs'); +const hjson = require('hjson'); +const sane = require('sane'); module.exports = new class ConfigCache { constructor() { - this.cache = new Map(); // path->parsed config + this.cache = new Map(); // path->parsed config } getConfigWithOptions(options, cb) { diff --git a/core/config_util.js b/core/config_util.js index 1c61162c..bda0e1bd 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -1,19 +1,19 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').get; -const ConfigCache = require('./config_cache.js'); -const Events = require('./events.js'); +const Config = require('./config.js').get; +const ConfigCache = require('./config_cache.js'); +const Events = require('./events.js'); -// deps -const paths = require('path'); -const async = require('async'); +// deps +const paths = require('path'); +const async = require('async'); -exports.init = init; -exports.getFullConfig = getFullConfig; +exports.init = init; +exports.getFullConfig = getFullConfig; function getConfigPath(filePath) { - // |filePath| is assumed to be in the config path if it's only a file name + // |filePath| is assumed to be in the config path if it's only a file name if('.' === paths.dirname(filePath)) { filePath = paths.join(Config().paths.config, filePath); } @@ -21,7 +21,7 @@ function getConfigPath(filePath) { } function init(cb) { - // pre-cache menu.hjson and prompt.hjson + establish events + // pre-cache menu.hjson and prompt.hjson + establish events const changed = ( { fileName, fileRoot } ) => { const reCachedPath = paths.join(fileRoot, fileName); if(reCachedPath === getConfigPath(Config().general.menuFile)) { diff --git a/core/connect.js b/core/connect.js index 86cf3833..44277813 100644 --- a/core/connect.js +++ b/core/connect.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const ansi = require('./ansi_term.js'); +// ENiGMA½ +const ansi = require('./ansi_term.js'); const Events = require('./events.js'); -// deps -const async = require('async'); +// deps +const async = require('async'); -exports.connectEntry = connectEntry; +exports.connectEntry = connectEntry; function ansiDiscoverHomePosition(client, cb) { // - // We want to find the home position. ANSI-BBS and most terminals - // utilize 1,1 as home. However, some terminals such as ConnectBot - // think of home as 0,0. If this is the case, we need to offset - // our positioning to accomodate for such. + // We want to find the home position. ANSI-BBS and most terminals + // utilize 1,1 as home. However, some terminals such as ConnectBot + // think of home as 0,0. If this is the case, we need to offset + // our positioning to accomodate for such. // const done = function(err) { client.removeListener('cursor position report', cprListener); @@ -28,7 +28,7 @@ function ansiDiscoverHomePosition(client, cb) { const w = pos[1]; // - // We expect either 0,0, or 1,1. Anything else will be filed as bad data + // We expect either 0,0, or 1,1. Anything else will be filed as bad data // if(h > 1 || w > 1) { client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); @@ -37,7 +37,7 @@ function ansiDiscoverHomePosition(client, cb) { if(0 === h & 0 === w) { // - // Store a CPR offset in the client. All CPR's from this point on will offset by this amount + // Store a CPR offset in the client. All CPR's from this point on will offset by this amount // client.log.info('Setting CPR offset to 1'); client.cprOffset = 1; @@ -50,9 +50,9 @@ function ansiDiscoverHomePosition(client, cb) { const giveUpTimer = setTimeout( () => { return done(new Error('Giving up on home position CPR')); - }, 3000); // 3s + }, 3000); // 3s - client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos + client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos } function ansiQueryTermSizeIfNeeded(client, cb) { @@ -68,7 +68,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) { const cprListener = function(pos) { // - // If we've already found out, disregard + // If we've already found out, disregard // if(client.term.termHeight > 0 || client.term.termWidth > 0) { return done(null); @@ -78,8 +78,8 @@ function ansiQueryTermSizeIfNeeded(client, cb) { const w = pos[1]; // - // Netrunner for example gives us 1x1 here. Not really useful. Ignore - // values that seem obviously bad. + // Netrunner for example gives us 1x1 here. Not really useful. Ignore + // values that seem obviously bad. // if(h < 10 || w < 10) { client.log.warn( @@ -88,14 +88,14 @@ function ansiQueryTermSizeIfNeeded(client, cb) { return done(new Error('Term size <= 10 considered invalid')); } - client.term.termHeight = h; - client.term.termWidth = w; + client.term.termHeight = h; + client.term.termWidth = w; client.log.debug( { - termWidth : client.term.termWidth, - termHeight : client.term.termHeight, - source : 'ANSI CPR' + termWidth : client.term.termWidth, + termHeight : client.term.termHeight, + source : 'ANSI CPR' }, 'Window size updated' ); @@ -105,23 +105,23 @@ function ansiQueryTermSizeIfNeeded(client, cb) { client.once('cursor position report', cprListener); - // give up after 2s + // give up after 2s const giveUpTimer = setTimeout( () => { return done(new Error('No term size established by CPR within timeout')); }, 2000); - // Start the process: Query for CPR + // Start the process: Query for CPR client.term.rawWrite(ansi.queryScreenSize()); } function prepareTerminal(term) { term.rawWrite(ansi.normal()); //term.rawWrite(ansi.disableVT100LineWrapping()); - // :TODO: set xterm stuff -- see x84/others + // :TODO: set xterm stuff -- see x84/others } function displayBanner(term) { - // note: intentional formatting: + // note: intentional formatting: term.pipeWrite(` |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN |06Copyright (c) 2014-2018 Bryan Ashby |14- |12http://l33t.codes/ @@ -141,26 +141,26 @@ function connectEntry(client, nextMenu) { }, function discoverHomePosition(callback) { ansiDiscoverHomePosition(client, () => { - // :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required - return callback(null); // we try to continue anyway + // :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required + return callback(null); // we try to continue anyway }); }, function queryTermSizeByNonStandardAnsi(callback) { ansiQueryTermSizeIfNeeded(client, err => { if(err) { // - // Check again; We may have got via NAWS/similar before CPR completed. + // Check again; We may have got via NAWS/similar before CPR completed. // if(0 === term.termHeight || 0 === term.termWidth) { // - // We still don't have something good for term height/width. - // Default to DOS size 80x25. + // We still don't have something good for term height/width. + // Default to DOS size 80x25. // - // :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? + // :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!'); - term.termHeight = 25; - term.termWidth = 80; + term.termHeight = 25; + term.termWidth = 80; } } @@ -172,7 +172,7 @@ function connectEntry(client, nextMenu) { prepareTerminal(term); // - // Always show an ENiGMA½ banner + // Always show an ENiGMA½ banner // displayBanner(term); diff --git a/core/crc.js b/core/crc.js index d7974c66..f90ac961 100644 --- a/core/crc.js +++ b/core/crc.js @@ -52,8 +52,8 @@ exports.CRC32 = class CRC32 { } update_4(input) { - const len = input.length - 3; - let i = 0; + const len = input.length - 3; + let i = 0; for(i = 0; i < len;) { this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; @@ -67,8 +67,8 @@ exports.CRC32 = class CRC32 { } update_8(input) { - const len = input.length - 7; - let i = 0; + const len = input.length - 7; + let i = 0; for(i = 0; i < len;) { this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; diff --git a/core/database.js b/core/database.js index 3ce2e031..ba48b404 100644 --- a/core/database.js +++ b/core/database.js @@ -1,28 +1,28 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const conf = require('./config.js'); +// ENiGMA½ +const conf = require('./config.js'); -// deps -const sqlite3 = require('sqlite3'); -const sqlite3Trans = require('sqlite3-trans'); -const paths = require('path'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); -const moment = require('moment'); +// deps +const sqlite3 = require('sqlite3'); +const sqlite3Trans = require('sqlite3-trans'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); +const moment = require('moment'); -// database handles +// database handles const dbs = {}; -exports.getTransactionDatabase = getTransactionDatabase; -exports.getModDatabasePath = getModDatabasePath; -exports.getISOTimestampString = getISOTimestampString; -exports.sanatizeString = sanatizeString; -exports.initializeDatabases = initializeDatabases; +exports.getTransactionDatabase = getTransactionDatabase; +exports.getModDatabasePath = getModDatabasePath; +exports.getISOTimestampString = getISOTimestampString; +exports.sanatizeString = sanatizeString; +exports.initializeDatabases = initializeDatabases; -exports.dbs = dbs; +exports.dbs = dbs; function getTransactionDatabase(db) { return sqlite3Trans.wrap(db); @@ -34,9 +34,9 @@ function getDatabasePath(name) { function getModDatabasePath(moduleInfo, suffix) { // - // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) - // We expect that moduleInfo defines packageName which will be the base of the modules - // filename. An optional suffix may be supplied as well. + // Mods that use a database are stored in Config.paths.modsDb (e.g. enigma-bbs/db/mods) + // We expect that moduleInfo defines packageName which will be the base of the modules + // filename. An optional suffix may be supplied as well. // const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; @@ -61,14 +61,14 @@ function getISOTimestampString(ts) { } function sanatizeString(s) { - return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex + return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex switch (c) { - case '\0' : return '\\0'; - case '\x08' : return '\\b'; - case '\x09' : return '\\t'; - case '\x1a' : return '\\z'; - case '\n' : return '\\n'; - case '\r' : return '\\r'; + case '\0' : return '\\0'; + case '\x08' : return '\\b'; + case '\x09' : return '\\t'; + case '\x1a' : return '\\z'; + case '\n' : return '\\n'; + case '\r' : return '\\r'; case '"' : case '\'' : @@ -107,35 +107,35 @@ const DB_INIT_TABLE = { system : (cb) => { enableForeignKeys(dbs.system); - // Various stat/event logging - see stat_log.js + // Various stat/event logging - see stat_log.js dbs.system.run( `CREATE TABLE IF NOT EXISTS system_stat ( - stat_name VARCHAR PRIMARY KEY NOT NULL, - stat_value VARCHAR NOT NULL - );` + stat_name VARCHAR PRIMARY KEY NOT NULL, + stat_value VARCHAR NOT NULL + );` ); dbs.system.run( `CREATE TABLE IF NOT EXISTS system_event_log ( - id INTEGER PRIMARY KEY, - timestamp DATETIME NOT NULL, - log_name VARCHAR NOT NULL, - log_value VARCHAR NOT NULL, + id INTEGER PRIMARY KEY, + timestamp DATETIME NOT NULL, + log_name VARCHAR NOT NULL, + log_value VARCHAR NOT NULL, - UNIQUE(timestamp, log_name) - );` + UNIQUE(timestamp, log_name) + );` ); dbs.system.run( `CREATE TABLE IF NOT EXISTS user_event_log ( - id INTEGER PRIMARY KEY, - timestamp DATETIME NOT NULL, - user_id INTEGER NOT NULL, - log_name VARCHAR NOT NULL, - log_value VARCHAR NOT NULL, + id INTEGER PRIMARY KEY, + timestamp DATETIME NOT NULL, + user_id INTEGER NOT NULL, + log_name VARCHAR NOT NULL, + log_value VARCHAR NOT NULL, - UNIQUE(timestamp, user_id, log_name) - );` + UNIQUE(timestamp, user_id, log_name) + );` ); return cb(null); @@ -146,38 +146,38 @@ const DB_INIT_TABLE = { dbs.user.run( `CREATE TABLE IF NOT EXISTS user ( - id INTEGER PRIMARY KEY, - user_name VARCHAR NOT NULL, - UNIQUE(user_name) - );` + id INTEGER PRIMARY KEY, + user_name VARCHAR NOT NULL, + UNIQUE(user_name) + );` ); - // :TODO: create FK on delete/etc. + // :TODO: create FK on delete/etc. dbs.user.run( `CREATE TABLE IF NOT EXISTS user_property ( - user_id INTEGER NOT NULL, - prop_name VARCHAR NOT NULL, - prop_value VARCHAR, - UNIQUE(user_id, prop_name), - FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE - );` + user_id INTEGER NOT NULL, + prop_name VARCHAR NOT NULL, + prop_value VARCHAR, + UNIQUE(user_id, prop_name), + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE + );` ); dbs.user.run( `CREATE TABLE IF NOT EXISTS user_group_member ( - group_name VARCHAR NOT NULL, - user_id INTEGER NOT NULL, - UNIQUE(group_name, user_id) - );` + group_name VARCHAR NOT NULL, + user_id INTEGER NOT NULL, + UNIQUE(group_name, user_id) + );` ); dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_login_history ( - user_id INTEGER NOT NULL, - user_name VARCHAR NOT NULL, - timestamp DATETIME NOT NULL - );` + `CREATE TABLE IF NOT EXISTS user_login_history ( + user_id INTEGER NOT NULL, + user_name VARCHAR NOT NULL, + timestamp DATETIME NOT NULL + );` ); return cb(null); @@ -188,104 +188,104 @@ const DB_INIT_TABLE = { dbs.message.run( `CREATE TABLE IF NOT EXISTS message ( - message_id INTEGER PRIMARY KEY, - area_tag VARCHAR NOT NULL, - message_uuid VARCHAR(36) NOT NULL, - reply_to_message_id INTEGER, - to_user_name VARCHAR NOT NULL, - from_user_name VARCHAR NOT NULL, - subject, /* FTS @ message_fts */ - message, /* FTS @ message_fts */ - modified_timestamp DATETIME NOT NULL, - view_count INTEGER NOT NULL DEFAULT 0, - UNIQUE(message_uuid) - );` + message_id INTEGER PRIMARY KEY, + area_tag VARCHAR NOT NULL, + message_uuid VARCHAR(36) NOT NULL, + reply_to_message_id INTEGER, + to_user_name VARCHAR NOT NULL, + from_user_name VARCHAR NOT NULL, + subject, /* FTS @ message_fts */ + message, /* FTS @ message_fts */ + modified_timestamp DATETIME NOT NULL, + view_count INTEGER NOT NULL DEFAULT 0, + UNIQUE(message_uuid) + );` ); dbs.message.run( `CREATE INDEX IF NOT EXISTS message_by_area_tag_index - ON message (area_tag);` + ON message (area_tag);` ); dbs.message.run( `CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts4 ( - content="message", - subject, - message - );` + content="message", + subject, + message + );` ); dbs.message.run( `CREATE TRIGGER IF NOT EXISTS message_before_update BEFORE UPDATE ON message BEGIN - DELETE FROM message_fts WHERE docid=old.rowid; - END;` + DELETE FROM message_fts WHERE docid=old.rowid; + END;` ); dbs.message.run( `CREATE TRIGGER IF NOT EXISTS message_before_delete BEFORE DELETE ON message BEGIN - DELETE FROM message_fts WHERE docid=old.rowid; - END;` + DELETE FROM message_fts WHERE docid=old.rowid; + END;` ); dbs.message.run( `CREATE TRIGGER IF NOT EXISTS message_after_update AFTER UPDATE ON message BEGIN - INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); - END;` + INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); + END;` ); dbs.message.run( `CREATE TRIGGER IF NOT EXISTS message_after_insert AFTER INSERT ON message BEGIN - INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); - END;` + INSERT INTO message_fts(docid, subject, message) VALUES(new.rowid, new.subject, new.message); + END;` ); dbs.message.run( `CREATE TABLE IF NOT EXISTS message_meta ( - message_id INTEGER NOT NULL, - meta_category INTEGER NOT NULL, - meta_name VARCHAR NOT NULL, - meta_value VARCHAR NOT NULL, - UNIQUE(message_id, meta_category, meta_name, meta_value), - FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE - );` + message_id INTEGER NOT NULL, + meta_category INTEGER NOT NULL, + meta_name VARCHAR NOT NULL, + meta_value VARCHAR NOT NULL, + UNIQUE(message_id, meta_category, meta_name, meta_value), + FOREIGN KEY(message_id) REFERENCES message(message_id) ON DELETE CASCADE + );` ); - // :TODO: need SQL to ensure cleaned up if delete from message? + // :TODO: need SQL to ensure cleaned up if delete from message? /* - dbs.message.run( - `CREATE TABLE IF NOT EXISTS hash_tag ( - hash_tag_id INTEGER PRIMARY KEY, - hash_tag_name VARCHAR NOT NULL, - UNIQUE(hash_tag_name) - );` - ); + dbs.message.run( + `CREATE TABLE IF NOT EXISTS hash_tag ( + hash_tag_id INTEGER PRIMARY KEY, + hash_tag_name VARCHAR NOT NULL, + UNIQUE(hash_tag_name) + );` + ); - // :TODO: need SQL to ensure cleaned up if delete from message? - dbs.message.run( - `CREATE TABLE IF NOT EXISTS message_hash_tag ( - hash_tag_id INTEGER NOT NULL, - message_id INTEGER NOT NULL, - );` - ); - */ + // :TODO: need SQL to ensure cleaned up if delete from message? + dbs.message.run( + `CREATE TABLE IF NOT EXISTS message_hash_tag ( + hash_tag_id INTEGER NOT NULL, + message_id INTEGER NOT NULL, + );` + ); + */ dbs.message.run( `CREATE TABLE IF NOT EXISTS user_message_area_last_read ( - user_id INTEGER NOT NULL, - area_tag VARCHAR NOT NULL, - message_id INTEGER NOT NULL, - UNIQUE(user_id, area_tag) - );` + user_id INTEGER NOT NULL, + area_tag VARCHAR NOT NULL, + message_id INTEGER NOT NULL, + UNIQUE(user_id, area_tag) + );` ); dbs.message.run( `CREATE TABLE IF NOT EXISTS message_area_last_scan ( - scan_toss VARCHAR NOT NULL, - area_tag VARCHAR NOT NULL, - message_id INTEGER NOT NULL, - UNIQUE(scan_toss, area_tag) - );` + scan_toss VARCHAR NOT NULL, + area_tag VARCHAR NOT NULL, + message_id INTEGER NOT NULL, + UNIQUE(scan_toss, area_tag) + );` ); return cb(null); @@ -295,114 +295,114 @@ const DB_INIT_TABLE = { enableForeignKeys(dbs.file); dbs.file.run( - // :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system + // :TODO: should any of this be unique -- file_sha256 unless dupes are allowed on the system `CREATE TABLE IF NOT EXISTS file ( - file_id INTEGER PRIMARY KEY, - area_tag VARCHAR NOT NULL, - file_sha256 VARCHAR NOT NULL, - file_name, /* FTS @ file_fts */ - storage_tag VARCHAR NOT NULL, - desc, /* FTS @ file_fts */ - desc_long, /* FTS @ file_fts */ - upload_timestamp DATETIME NOT NULL - );` + file_id INTEGER PRIMARY KEY, + area_tag VARCHAR NOT NULL, + file_sha256 VARCHAR NOT NULL, + file_name, /* FTS @ file_fts */ + storage_tag VARCHAR NOT NULL, + desc, /* FTS @ file_fts */ + desc_long, /* FTS @ file_fts */ + upload_timestamp DATETIME NOT NULL + );` ); dbs.file.run( `CREATE INDEX IF NOT EXISTS file_by_area_tag_index - ON file (area_tag);` + ON file (area_tag);` ); dbs.file.run( `CREATE INDEX IF NOT EXISTS file_by_sha256_index - ON file (file_sha256);` + ON file (file_sha256);` ); dbs.file.run( `CREATE VIRTUAL TABLE IF NOT EXISTS file_fts USING fts4 ( - content="file", - file_name, - desc, - desc_long - );` + content="file", + file_name, + desc, + desc_long + );` ); dbs.file.run( `CREATE TRIGGER IF NOT EXISTS file_before_update BEFORE UPDATE ON file BEGIN - DELETE FROM file_fts WHERE docid=old.rowid; - END;` + DELETE FROM file_fts WHERE docid=old.rowid; + END;` ); dbs.file.run( `CREATE TRIGGER IF NOT EXISTS file_before_delete BEFORE DELETE ON file BEGIN - DELETE FROM file_fts WHERE docid=old.rowid; - END;` + DELETE FROM file_fts WHERE docid=old.rowid; + END;` ); dbs.file.run( `CREATE TRIGGER IF NOT EXISTS file_after_update AFTER UPDATE ON file BEGIN - INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); - END;` + INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); + END;` ); dbs.file.run( `CREATE TRIGGER IF NOT EXISTS file_after_insert AFTER INSERT ON file BEGIN - INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); - END;` + INSERT INTO file_fts(docid, file_name, desc, desc_long) VALUES(new.rowid, new.file_name, new.desc, new.desc_long); + END;` ); dbs.file.run( `CREATE TABLE IF NOT EXISTS file_meta ( - file_id INTEGER NOT NULL, - meta_name VARCHAR NOT NULL, - meta_value VARCHAR NOT NULL, - UNIQUE(file_id, meta_name, meta_value), - FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE - );` + file_id INTEGER NOT NULL, + meta_name VARCHAR NOT NULL, + meta_value VARCHAR NOT NULL, + UNIQUE(file_id, meta_name, meta_value), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE + );` ); dbs.file.run( `CREATE TABLE IF NOT EXISTS hash_tag ( - hash_tag_id INTEGER PRIMARY KEY, - hash_tag VARCHAR NOT NULL, - - UNIQUE(hash_tag) - );` + hash_tag_id INTEGER PRIMARY KEY, + hash_tag VARCHAR NOT NULL, + + UNIQUE(hash_tag) + );` ); dbs.file.run( `CREATE TABLE IF NOT EXISTS file_hash_tag ( - hash_tag_id INTEGER NOT NULL, - file_id INTEGER NOT NULL, - - UNIQUE(hash_tag_id, file_id) - );` + hash_tag_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + + UNIQUE(hash_tag_id, file_id) + );` ); dbs.file.run( `CREATE TABLE IF NOT EXISTS file_user_rating ( - file_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - rating INTEGER NOT NULL, + file_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + rating INTEGER NOT NULL, - UNIQUE(file_id, user_id) - );` + UNIQUE(file_id, user_id) + );` ); dbs.file.run( `CREATE TABLE IF NOT EXISTS file_web_serve ( - hash_id VARCHAR NOT NULL PRIMARY KEY, - expire_timestamp DATETIME NOT NULL - );` + hash_id VARCHAR NOT NULL PRIMARY KEY, + expire_timestamp DATETIME NOT NULL + );` ); dbs.file.run( `CREATE TABLE IF NOT EXISTS file_web_serve_batch ( - hash_id VARCHAR NOT NULL, - file_id INTEGER NOT NULL, + hash_id VARCHAR NOT NULL, + file_id INTEGER NOT NULL, - UNIQUE(hash_id, file_id) - );` + UNIQUE(hash_id, file_id) + );` ); return cb(null); diff --git a/core/descript_ion_file.js b/core/descript_ion_file.js index 1ead544f..a5d68e1d 100644 --- a/core/descript_ion_file.js +++ b/core/descript_ion_file.js @@ -1,10 +1,10 @@ /* jslint node: true */ 'use strict'; -// deps -const fs = require('graceful-fs'); -const iconv = require('iconv-lite'); -const async = require('async'); +// deps +const fs = require('graceful-fs'); +const iconv = require('iconv-lite'); +const async = require('async'); module.exports = class DescriptIonFile { constructor() { @@ -30,34 +30,34 @@ module.exports = class DescriptIonFile { const descIonFile = new DescriptIonFile(); - // DESCRIPT.ION entries are terminated with a CR and/or LF + // DESCRIPT.ION entries are terminated with a CR and/or LF const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); async.each(lines, (entryData, nextLine) => { // - // We allow quoted (long) filenames or non-quoted filenames. - // FILENAMEDESC<0x04> + // We allow quoted (long) filenames or non-quoted filenames. + // FILENAMEDESC<0x04> // - const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex + const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex if(!parts) { return nextLine(null); } - const fileName = parts[1] || parts[2]; + const fileName = parts[1] || parts[2]; // - // Un-escape CR/LF's - // - escapped \r and/or \n - // - BBBS style @n - See https://www.bbbs.net/sysop.html + // Un-escape CR/LF's + // - escapped \r and/or \n + // - BBBS style @n - See https://www.bbbs.net/sysop.html // - const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n'); + const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n'); descIonFile.entries.set( fileName, { - desc : desc, - programId : parts[4], - programData : parts[5], + desc : desc, + programId : parts[4], + programData : parts[5], } ); diff --git a/core/door.js b/core/door.js index 06a10f60..ffd57b48 100644 --- a/core/door.js +++ b/core/door.js @@ -2,36 +2,36 @@ 'use strict'; -const stringFormat = require('./string_format.js'); +const stringFormat = require('./string_format.js'); -const events = require('events'); -const _ = require('lodash'); -const pty = require('node-pty'); -const decode = require('iconv-lite').decode; -const createServer = require('net').createServer; +const events = require('events'); +const _ = require('lodash'); +const pty = require('node-pty'); +const decode = require('iconv-lite').decode; +const createServer = require('net').createServer; -exports.Door = Door; +exports.Door = Door; function Door(client, exeInfo) { events.EventEmitter.call(this); - const self = this; - this.client = client; - this.exeInfo = exeInfo; - this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); - let restored = false; + const self = this; + this.client = client; + this.exeInfo = exeInfo; + this.exeInfo.encoding = (this.exeInfo.encoding || 'cp437').toLowerCase(); + let restored = false; // - // Members of exeInfo: - // cmd - // args[] - // env{} - // cwd - // io - // encoding - // dropFile - // node - // inhSocket + // Members of exeInfo: + // cmd + // args[] + // env{} + // cwd + // io + // encoding + // dropFile + // node + // inhSocket // this.doorDataHandler = function(data) { @@ -52,7 +52,7 @@ function Door(client, exeInfo) { sockServer.getConnections( (err, count) => { - // We expect only one connection from our DOOR/emulator/etc. + // We expect only one connection from our DOOR/emulator/etc. if(!err && count <= 1) { self.client.term.output.pipe(conn); @@ -94,25 +94,25 @@ Door.prototype.run = function() { return self.doorExited(); } - // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS - // :TODO: Use .map() here - let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified + // Expand arg strings, e.g. {dropFile} -> DOOR32.SYS + // :TODO: Use .map() here + let args = _.clone(self.exeInfo.args); // we need a copy so the original is not modified for(let i = 0; i < args.length; ++i) { args[i] = stringFormat(self.exeInfo.args[i], { - dropFile : self.exeInfo.dropFile, - node : self.exeInfo.node.toString(), - srvPort : sockServer ? sockServer.address().port.toString() : '-1', - userId : self.client.user.userId.toString(), + dropFile : self.exeInfo.dropFile, + node : self.exeInfo.node.toString(), + srvPort : sockServer ? sockServer.address().port.toString() : '-1', + userId : self.client.user.userId.toString(), }); } const door = pty.spawn(self.exeInfo.cmd, args, { - cols : self.client.term.termWidth, - rows : self.client.term.termHeight, - // :TODO: cwd - env : self.exeInfo.env, - encoding : null, // we want to handle all encoding ourself + cols : self.client.term.termWidth, + rows : self.client.term.termHeight, + // :TODO: cwd + env : self.exeInfo.env, + encoding : null, // we want to handle all encoding ourself }); if('stdio' === self.exeInfo.io) { @@ -136,7 +136,7 @@ Door.prototype.run = function() { sockServer.close(); } - // we may not get a close + // we may not get a close if('stdio' === self.exeInfo.io) { self.restoreIo(door); } diff --git a/core/door_party.js b/core/door_party.js index 3c5b29a7..63247f69 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -1,30 +1,30 @@ /* jslint node: true */ 'use strict'; -// enigma-bbs -const MenuModule = require('../core/menu_module.js').MenuModule; -const resetScreen = require('../core/ansi_term.js').resetScreen; +// enigma-bbs +const MenuModule = require('../core/menu_module.js').MenuModule; +const resetScreen = require('../core/ansi_term.js').resetScreen; -// deps -const async = require('async'); -const _ = require('lodash'); -const SSHClient = require('ssh2').Client; +// deps +const async = require('async'); +const _ = require('lodash'); +const SSHClient = require('ssh2').Client; exports.moduleInfo = { - name : 'DoorParty', - desc : 'DoorParty Access Module', - author : 'NuSkooler', + name : 'DoorParty', + desc : 'DoorParty Access Module', + author : 'NuSkooler', }; exports.getModule = class DoorPartyModule extends MenuModule { constructor(options) { super(options); - // establish defaults - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'dp.throwbackbbs.com'; - this.config.sshPort = this.config.sshPort || 2022; - this.config.rloginPort = this.config.rloginPort || 513; + // establish defaults + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'dp.throwbackbbs.com'; + this.config.sshPort = this.config.sshPort || 2022; + this.config.rloginPort = this.config.rloginPort || 513; } initSequence() { @@ -61,32 +61,32 @@ exports.getModule = class DoorPartyModule extends MenuModule { }; sshClient.on('ready', () => { - // track client termination so we can clean up early + // track client termination so we can clean up early self.client.once('end', () => { self.client.log.info('Connection ended. Terminating DoorParty connection'); clientTerminated = true; sshClient.end(); }); - // establish tunnel for rlogin + // establish tunnel for rlogin sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { if(err) { return callback(new Error('Failed to establish tunnel')); } // - // Send rlogin - // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. - // [XA]nuskooler + // Send rlogin + // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. + // [XA]nuskooler // const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; stream.write(rlogin); - pipedStream = stream; // :TODO: this is hacky... + pipedStream = stream; // :TODO: this is hacky... self.client.term.output.pipe(stream); stream.on('data', d => { - // :TODO: we should just pipe this... + // :TODO: we should just pipe this... self.client.term.rawWrite(d); }); @@ -107,13 +107,13 @@ exports.getModule = class DoorPartyModule extends MenuModule { }); sshClient.connect( { - host : self.config.host, - port : self.config.sshPort, - username : self.config.username, - password : self.config.password, + host : self.config.host, + port : self.config.sshPort, + username : self.config.username, + password : self.config.password, }); - // note: no explicit callback() until we're finished! + // note: no explicit callback() until we're finished! } ], err => { @@ -121,7 +121,7 @@ exports.getModule = class DoorPartyModule extends MenuModule { self.client.log.warn( { error : err.message }, 'DoorParty error'); } - // if the client is stil here, go to previous + // if the client is stil here, go to previous if(!clientTerminated) { self.prevMenu(); } diff --git a/core/download_queue.js b/core/download_queue.js index d8617f75..0d1e3847 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -1,14 +1,14 @@ /* jslint node: true */ 'use strict'; -const FileEntry = require('./file_entry.js'); +const FileEntry = require('./file_entry.js'); -// deps -const { partition } = require('lodash'); +// deps +const { partition } = require('lodash'); module.exports = class DownloadQueue { constructor(client) { - this.client = client; + this.client = client; if(!Array.isArray(this.client.user.downloadQueue)) { if(this.client.user.properties.dl_queue) { @@ -37,12 +37,12 @@ module.exports = class DownloadQueue { add(fileEntry, systemFile=false) { this.client.user.downloadQueue.push({ - fileId : fileEntry.fileId, - areaTag : fileEntry.areaTag, - fileName : fileEntry.fileName, - path : fileEntry.filePath, - byteSize : fileEntry.meta.byte_size || 0, - systemFile : systemFile, + fileId : fileEntry.fileId, + areaTag : fileEntry.areaTag, + fileName : fileEntry.fileName, + path : fileEntry.filePath, + byteSize : fileEntry.meta.byte_size || 0, + systemFile : systemFile, }); } diff --git a/core/dropfile.js b/core/dropfile.js index 7a49cca4..76977eaf 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -1,31 +1,31 @@ /* jslint node: true */ 'use strict'; -var Config = require('./config.js').get; -const StatLog = require('./stat_log.js'); +var Config = require('./config.js').get; +const StatLog = require('./stat_log.js'); -var fs = require('graceful-fs'); -var paths = require('path'); -var _ = require('lodash'); -var moment = require('moment'); -var iconv = require('iconv-lite'); +var fs = require('graceful-fs'); +var paths = require('path'); +var _ = require('lodash'); +var moment = require('moment'); +var iconv = require('iconv-lite'); -exports.DropFile = DropFile; +exports.DropFile = DropFile; // -// Resources -// * http://goldfndr.home.mindspring.com/dropfile/ -// * https://en.wikipedia.org/wiki/Talk%3ADropfile -// * http://thoughtproject.com/libraries/bbs/Sysop/Doors/DropFiles/index.htm -// * http://thebbs.org/bbsfaq/ch06.02.htm +// Resources +// * http://goldfndr.home.mindspring.com/dropfile/ +// * https://en.wikipedia.org/wiki/Talk%3ADropfile +// * http://thoughtproject.com/libraries/bbs/Sysop/Doors/DropFiles/index.htm +// * http://thebbs.org/bbsfaq/ch06.02.htm -// http://lord.lordlegacy.com/dosemu/ +// http://lord.lordlegacy.com/dosemu/ function DropFile(client, fileType) { - var self = this; - this.client = client; - this.fileType = (fileType || 'DORINFO').toUpperCase(); + var self = this; + this.client = client; + this.fileType = (fileType || 'DORINFO').toUpperCase(); Object.defineProperty(this, 'fullPath', { get : function() { @@ -36,20 +36,20 @@ function DropFile(client, fileType) { Object.defineProperty(this, 'fileName', { get : function() { return { - DOOR : 'DOOR.SYS', // GAP BBS, many others - DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... - CALLINFO : 'CALLINFO.BBS', // Citadel? - DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... - CHAIN : 'CHAIN.TXT', // WWIV - CURRUSER : 'CURRUSER.BBS', // RyBBS - SFDOORS : 'SFDOORS.DAT', // Spitfire - PCBOARD : 'PCBOARD.SYS', // PCBoard - TRIBBS : 'TRIBBS.SYS', // TriBBS - USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ - JUMPER : 'JUMPER.DAT', // 2AM BBS - SXDOOR : // System/X, dESiRE - 'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'), - INFO : 'INFO.BBS', // Phoenix BBS + DOOR : 'DOOR.SYS', // GAP BBS, many others + DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... + CALLINFO : 'CALLINFO.BBS', // Citadel? + DORINFO : self.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... + CHAIN : 'CHAIN.TXT', // WWIV + CURRUSER : 'CURRUSER.BBS', // RyBBS + SFDOORS : 'SFDOORS.DAT', // Spitfire + PCBOARD : 'PCBOARD.SYS', // PCBoard + TRIBBS : 'TRIBBS.SYS', // TriBBS + USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ + JUMPER : 'JUMPER.DAT', // 2AM BBS + SXDOOR : // System/X, dESiRE + 'SXDOOR.' + _.pad(self.client.node.toString(), 3, '0'), + INFO : 'INFO.BBS', // Phoenix BBS }[self.fileType]; } }); @@ -57,9 +57,9 @@ function DropFile(client, fileType) { Object.defineProperty(this, 'dropFileContents', { get : function() { return { - DOOR : self.getDoorSysBuffer(), - DOOR32 : self.getDoor32Buffer(), - DORINFO : self.getDoorInfoDefBuffer(), + DOOR : self.getDoorSysBuffer(), + DOOR32 : self.getDoor32Buffer(), + DORINFO : self.getDoorInfoDefBuffer(), }[self.fileType]; } }); @@ -78,124 +78,124 @@ function DropFile(client, fileType) { }; this.getDoorSysBuffer = function() { - var up = self.client.user.properties; - var now = moment(); - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + var up = self.client.user.properties; + var now = moment(); + var secLevel = self.client.user.getLegacySecurityLevel().toString(); - // :TODO: fix time remaining - // :TODO: fix default protocol -- user prop: transfer_protocol + // :TODO: fix time remaining + // :TODO: fix default protocol -- user prop: transfer_protocol return iconv.encode( [ - 'COM1:', // "Comm Port - COM0: = LOCAL MODE" - '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) - '8', // "Parity - 7 or 8" - self.client.node.toString(), // "Node Number - 1 to 99" - '57600', // "DTE Rate. Actual BPS rate to use. (kg)" - 'Y', // "Screen Display - Y=On N=Off (Default to Y)" - 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" - 'Y', // "Page Bell - Y=On N=Off (Default to Y)" - 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" - up.real_name || self.client.user.username, // "User Full Name" - up.location || 'Anywhere', // "Calling From" - '123-456-7890', // "Home Phone" - '123-456-7890', // "Work/Data Phone" - 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) - secLevel, // "Security Level" - up.login_count.toString(), // "Total Times On" - now.format('MM/DD/YY'), // "Last Date Called" - '15360', // "Seconds Remaining THIS call (for those that particular)" - '256', // "Minutes Remaining THIS call" - 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" - self.client.term.termHeight.toString(), // "Page Length" - 'N', // "User Mode - Y = Expert, N = Novice" - '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" - '1', // "Conference Exited To DOOR From (G)" - '01/01/99', // "User Expiration Date (mm/dd/yy)" - self.client.user.userId.toString(), // "User File's Record Number" - 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." - // :TODO: fix up, down, etc. form user properties - '0', // "Total Uploads" - '0', // "Total Downloads" - '0', // "Daily Download "K" Total" - '999999', // "Daily Download Max. "K" Limit" - moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" - 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" - 'X:\\GEN\\', // "Path to the GEN directory" - StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" - self.client.user.username, // "Alias name" - '00:05', // "Event time (hh:mm)" (note: wat?) - 'Y', // "If its an error correcting connection (Y/N)" - 'Y', // "ANSI supported & caller using NG mode (Y/N)" - 'Y', // "Use Record Locking (Y/N)" - '7', // "BBS Default Color (Standard IBM color code, ie, 1-15)" - // :TODO: fix minutes here also: - '256', // "Time Credits In Minutes (positive/negative)" - '07/07/90', // "Last New Files Scan Date (mm/dd/yy)" - // :TODO: fix last vs now times: - now.format('hh:mm'), // "Time of This Call" - now.format('hh:mm'), // "Time of Last Call (hh:mm)" - '9999', // "Maximum daily files available" - // :TODO: fix these stats: - '0', // "Files d/led so far today" - '0', // "Total "K" Bytes Uploaded" - '0', // "Total "K" Bytes Downloaded" - up.user_comment || 'None', // "User Comment" - '0', // "Total Doors Opened" - '0', // "Total Messages Left" + 'COM1:', // "Comm Port - COM0: = LOCAL MODE" + '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) + '8', // "Parity - 7 or 8" + self.client.node.toString(), // "Node Number - 1 to 99" + '57600', // "DTE Rate. Actual BPS rate to use. (kg)" + 'Y', // "Screen Display - Y=On N=Off (Default to Y)" + 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" + 'Y', // "Page Bell - Y=On N=Off (Default to Y)" + 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" + up.real_name || self.client.user.username, // "User Full Name" + up.location || 'Anywhere', // "Calling From" + '123-456-7890', // "Home Phone" + '123-456-7890', // "Work/Data Phone" + 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) + secLevel, // "Security Level" + up.login_count.toString(), // "Total Times On" + now.format('MM/DD/YY'), // "Last Date Called" + '15360', // "Seconds Remaining THIS call (for those that particular)" + '256', // "Minutes Remaining THIS call" + 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" + self.client.term.termHeight.toString(), // "Page Length" + 'N', // "User Mode - Y = Expert, N = Novice" + '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" + '1', // "Conference Exited To DOOR From (G)" + '01/01/99', // "User Expiration Date (mm/dd/yy)" + self.client.user.userId.toString(), // "User File's Record Number" + 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." + // :TODO: fix up, down, etc. form user properties + '0', // "Total Uploads" + '0', // "Total Downloads" + '0', // "Daily Download "K" Total" + '999999', // "Daily Download Max. "K" Limit" + moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" + 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" + 'X:\\GEN\\', // "Path to the GEN directory" + StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" + self.client.user.username, // "Alias name" + '00:05', // "Event time (hh:mm)" (note: wat?) + 'Y', // "If its an error correcting connection (Y/N)" + 'Y', // "ANSI supported & caller using NG mode (Y/N)" + 'Y', // "Use Record Locking (Y/N)" + '7', // "BBS Default Color (Standard IBM color code, ie, 1-15)" + // :TODO: fix minutes here also: + '256', // "Time Credits In Minutes (positive/negative)" + '07/07/90', // "Last New Files Scan Date (mm/dd/yy)" + // :TODO: fix last vs now times: + now.format('hh:mm'), // "Time of This Call" + now.format('hh:mm'), // "Time of Last Call (hh:mm)" + '9999', // "Maximum daily files available" + // :TODO: fix these stats: + '0', // "Files d/led so far today" + '0', // "Total "K" Bytes Uploaded" + '0', // "Total "K" Bytes Downloaded" + up.user_comment || 'None', // "User Comment" + '0', // "Total Doors Opened" + '0', // "Total Messages Left" ].join('\r\n') + '\r\n', 'cp437'); }; this.getDoor32Buffer = function() { // - // Resources: - // * http://wiki.bbses.info/index.php/DOOR32.SYS + // Resources: + // * http://wiki.bbses.info/index.php/DOOR32.SYS // - // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! + // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! return iconv.encode([ - '2', // :TODO: This needs to be configurable! - // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely - '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! + '2', // :TODO: This needs to be configurable! + // :TODO: Completely broken right now -- This need to be configurable & come from temp socket server most likely + '-1', // self.client.output._handle.fd.toString(), // :TODO: ALWAYS -1 on Windows! '57600', Config().general.boardName, self.client.user.userId.toString(), self.client.user.properties.real_name || self.client.user.username, self.client.user.username, self.client.user.getLegacySecurityLevel().toString(), - '546', // :TODO: Minutes left! - '1', // ANSI + '546', // :TODO: Minutes left! + '1', // ANSI self.client.node.toString(), ].join('\r\n') + '\r\n', 'cp437'); }; this.getDoorInfoDefBuffer = function() { - // :TODO: fix time remaining + // :TODO: fix time remaining // - // Resources: - // * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm + // Resources: + // * http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm // - // Note that usernames are just used for first/last names here + // Note that usernames are just used for first/last names here // - var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; - var un = /[^\s]*/.exec(self.client.user.username)[0]; - var secLevel = self.client.user.getLegacySecurityLevel().toString(); + var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; + var un = /[^\s]*/.exec(self.client.user.username)[0]; + var secLevel = self.client.user.getLegacySecurityLevel().toString(); return iconv.encode( [ - Config().general.boardName, // "The name of the system." - opUn, // "The sysop's name up to the first space." - opUn, // "The sysop's name following the first space." - 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." - '57600', // "The current port (DTE) rate." - '0', // "The number "0"" - un, // "The current user's name, up to the first space." - un, // "The current user's name, following the first space." - self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." - '1', // "The number "0" if TTY, or "1" if ANSI." - secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." - '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." - '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." + Config().general.boardName, // "The name of the system." + opUn, // "The sysop's name up to the first space." + opUn, // "The sysop's name following the first space." + 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." + '57600', // "The current port (DTE) rate." + '0', // "The number "0"" + un, // "The current user's name, up to the first space." + un, // "The current user's name, following the first space." + self.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." + '1', // "The number "0" if TTY, or "1" if ANSI." + secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." + '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." + '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." ].join('\r\n') + '\r\n', 'cp437'); }; diff --git a/core/edit_text_view.js b/core/edit_text_view.js index b1b89726..05868e92 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const TextView = require('./text_view.js').TextView; -const miscUtil = require('./misc_util.js'); -const strUtil = require('./string_util.js'); +// ENiGMA½ +const TextView = require('./text_view.js').TextView; +const miscUtil = require('./misc_util.js'); +const strUtil = require('./string_util.js'); -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); -exports.EditTextView = EditTextView; +exports.EditTextView = EditTextView; function EditTextView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); - options.resizable = false; + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); + options.resizable = false; TextView.call(this, options); @@ -47,9 +47,9 @@ EditTextView.prototype.onKeyPress = function(ch, key) { return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); } else if(this.isKeyMapped('clearLine', key.name)) { - this.text = ''; - this.cursorPos.col = 0; - this.setFocus(true); // resetting focus will redraw & adjust cursor + this.text = ''; + this.cursorPos.col = 0; + this.setFocus(true); // resetting focus will redraw & adjust cursor return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); } @@ -62,7 +62,7 @@ EditTextView.prototype.onKeyPress = function(ch, key) { this.text += ch; if(this.text.length > this.dimens.width) { - // no shortcuts - redraw the view + // no shortcuts - redraw the view this.redraw(); } else { this.cursorPos.col += 1; @@ -82,9 +82,9 @@ EditTextView.prototype.onKeyPress = function(ch, key) { }; EditTextView.prototype.setText = function(text) { - // draw & set |text| + // draw & set |text| EditTextView.super_.prototype.setText.call(this, text); - // adjust local cursor tracking + // adjust local cursor tracking this.cursorPos = { row : 0, col : text.length }; }; diff --git a/core/email.js b/core/email.js index af195da5..1de3b034 100644 --- a/core/email.js +++ b/core/email.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const Log = require('./logger.js').log; +// ENiGMA½ +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const Log = require('./logger.js').log; -// deps -const _ = require('lodash'); -const nodeMailer = require('nodemailer'); +// deps +const _ = require('lodash'); +const nodeMailer = require('nodemailer'); -exports.sendMail = sendMail; +exports.sendMail = sendMail; function sendMail(message, cb) { const config = Config(); @@ -21,7 +21,7 @@ function sendMail(message, cb) { message.from = message.from || config.email.defaultFrom; const transportOptions = Object.assign( {}, config.email.transport, { - logger : Log, + logger : Log, }); const transport = nodeMailer.createTransport(transportOptions); diff --git a/core/enig_error.js b/core/enig_error.js index 879a18ab..63c90cb9 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -5,11 +5,11 @@ class EnigError extends Error { constructor(message, code, reason, reasonCode) { super(message); - this.name = this.constructor.name; - this.message = message; - this.code = code; - this.reason = reason; - this.reasonCode = reasonCode; + this.name = this.constructor.name; + this.message = message; + this.code = code; + this.reason = reason; + this.reasonCode = reasonCode; if(this.reason) { this.message += `: ${this.reason}`; @@ -23,24 +23,24 @@ class EnigError extends Error { } } -exports.EnigError = EnigError; +exports.EnigError = EnigError; exports.Errors = { - General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), - MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), - DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), - AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), - Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), - ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), - MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), - UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), - MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), + General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), + MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), + DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), + AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), + Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), + ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), + MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), + UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), + MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), }; exports.ErrorReasons = { - AlreadyThere : 'ALREADYTHERE', - InvalidNextMenu : 'BADNEXT', - NoPreviousMenu : 'NOPREV', - NoConditionMatch : 'NOCONDMATCH', - NotEnabled : 'NOTENABLED', + AlreadyThere : 'ALREADYTHERE', + InvalidNextMenu : 'BADNEXT', + NoPreviousMenu : 'NOPREV', + NoConditionMatch : 'NOCONDMATCH', + NotEnabled : 'NOTENABLED', }; \ No newline at end of file diff --git a/core/enigma_assert.js b/core/enigma_assert.js index 0d1d5176..34f9beed 100644 --- a/core/enigma_assert.js +++ b/core/enigma_assert.js @@ -1,12 +1,12 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const Log = require('./logger.js').log; +// ENiGMA½ +const Config = require('./config.js').get; +const Log = require('./logger.js').log; -// deps -const assert = require('assert'); +// deps +const assert = require('assert'); module.exports = function(condition, message) { if(Config().debug.assertsEnabled) { diff --git a/core/erc_client.js b/core/erc_client.js index 79a47240..018c9372 100644 --- a/core/erc_client.js +++ b/core/erc_client.js @@ -1,55 +1,55 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; -const stringFormat = require('./string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const stringFormat = require('./string_format.js'); -// deps -const async = require('async'); -const _ = require('lodash'); -const net = require('net'); +// deps +const async = require('async'); +const _ = require('lodash'); +const net = require('net'); /* - Expected configuration block example: + Expected configuration block example: - config: { - host: 192.168.1.171 - port: 5001 - bbsTag: SOME_TAG - } + config: { + host: 192.168.1.171 + port: 5001 + bbsTag: SOME_TAG + } */ -exports.getModule = ErcClientModule; +exports.getModule = ErcClientModule; exports.moduleInfo = { - name : 'ENiGMA Relay Chat Client', - desc : 'Chat with other ENiGMA BBSes', - author : 'Andrew Pamment', + name : 'ENiGMA Relay Chat Client', + desc : 'Chat with other ENiGMA BBSes', + author : 'Andrew Pamment', }; var MciViewIds = { ChatDisplay : 1, - InputArea : 3, + InputArea : 3, }; -// :TODO: needs converted to ES6 MenuModule subclass +// :TODO: needs converted to ES6 MenuModule subclass function ErcClientModule(options) { MenuModule.prototype.ctorShim.call(this, options); - const self = this; - this.config = options.menuConfig.config; + const self = this; + this.config = options.menuConfig.config; - this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; - this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; + this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; + this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; this.finishedLoading = function() { async.waterfall( [ function validateConfig(callback) { if(_.isString(self.config.host) && - _.isNumber(self.config.port) && - _.isString(self.config.bbsTag)) + _.isNumber(self.config.port) && + _.isString(self.config.bbsTag)) { return callback(null); } else { @@ -58,8 +58,8 @@ function ErcClientModule(options) { }, function connectToServer(callback) { const connectOpts = { - port : self.config.port, - host : self.config.host, + port : self.config.port, + host : self.config.host, }; const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); @@ -69,7 +69,7 @@ function ErcClientModule(options) { self.viewControllers.menu.switchFocus(MciViewIds.InputArea); - // :TODO: Track actual client->enig connection for optional prevMenu @ final CB + // :TODO: Track actual client->enig connection for optional prevMenu @ final CB self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); self.chatConnection.on('data', data => { @@ -87,10 +87,10 @@ function ErcClientModule(options) { let text; try { if(data.userName) { - // user message + // user message text = stringFormat(self.chatEntryFormat, data); } else { - // system message + // system message text = stringFormat(self.systemEntryFormat, data); } } catch(e) { @@ -99,7 +99,7 @@ function ErcClientModule(options) { chatMessageView.addText(text); - if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? + if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? chatMessageView.deleteLine(0); chatMessageView.scrollDown(); } @@ -130,8 +130,8 @@ function ErcClientModule(options) { }; this.scrollHandler = function(keyName) { - const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); - const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); + const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); + const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); if('up arrow' === keyName) { chatDisplayView.scrollUp(); @@ -147,7 +147,7 @@ function ErcClientModule(options) { this.menuMethods = { inputAreaSubmit : function(formData, extraArgs, cb) { const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); - const inputData = inputAreaView.getData(); + const inputData = inputAreaView.getData(); if('/quit' === inputData.toLowerCase()) { self.chatConnection.end(); diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 1465d42b..22c2d58d 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -1,37 +1,37 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const PluginModule = require('./plugin_module.js').PluginModule; -const Config = require('./config.js').get; -const Log = require('./logger.js').log; +// ENiGMA½ +const PluginModule = require('./plugin_module.js').PluginModule; +const Config = require('./config.js').get; +const Log = require('./logger.js').log; -const _ = require('lodash'); -const later = require('later'); -const path = require('path'); -const pty = require('node-pty'); -const sane = require('sane'); -const moment = require('moment'); -const paths = require('path'); -const fse = require('fs-extra'); +const _ = require('lodash'); +const later = require('later'); +const path = require('path'); +const pty = require('node-pty'); +const sane = require('sane'); +const moment = require('moment'); +const paths = require('path'); +const fse = require('fs-extra'); -exports.getModule = EventSchedulerModule; -exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart +exports.getModule = EventSchedulerModule; +exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart exports.moduleInfo = { - name : 'Event Scheduler', - desc : 'Support for scheduling arbritary events', - author : 'NuSkooler', + name : 'Event Scheduler', + desc : 'Support for scheduling arbritary events', + author : 'NuSkooler', }; -const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; -const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; +const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; +const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; class ScheduledEvent { constructor(events, name) { - this.name = name; - this.schedule = this.parseScheduleString(events[name].schedule); - this.action = this.parseActionSpec(events[name].action); + this.name = name; + this.schedule = this.parseScheduleString(events[name].schedule); + this.action = this.parseActionSpec(events[name].action); if(this.action) { this.action.args = events[name].args || []; } @@ -72,7 +72,7 @@ class ScheduledEvent { } } - // return undefined if we couldn't parse out anything useful + // return undefined if we couldn't parse out anything useful if(!_.isEmpty(schedule)) { return schedule; } @@ -86,21 +86,21 @@ class ScheduledEvent { if(m[2].indexOf(':') > -1) { const parts = m[2].split(':'); return { - type : m[1], - location : parts[0], - what : parts[1], + type : m[1], + location : parts[0], + what : parts[1], }; } else { return { - type : m[1], - what : m[2], + type : m[1], + what : m[2], }; } } } else { return { - type : 'execute', - what : actionSpec, + type : 'execute', + what : actionSpec, }; } } @@ -110,7 +110,7 @@ class ScheduledEvent { Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...'); if('method' === this.action.type) { - const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') + const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') try { const methodModule = require(modulePath); methodModule[this.action.what](this.action.args, err => { @@ -131,11 +131,11 @@ class ScheduledEvent { } } else if('execute' === this.action.type) { const opts = { - // :TODO: cwd - name : this.name, - cols : 80, - rows : 24, - env : process.env, + // :TODO: cwd + name : this.name, + cols : 80, + rows : 24, + env : process.env, }; const proc = pty.spawn(this.action.what, this.action.args, opts); @@ -165,7 +165,7 @@ function EventSchedulerModule(options) { this.performAction = function(schedEvent, reason) { if(self.runningActions.has(schedEvent.name)) { - return; // already running + return; // already running } self.runningActions.add(schedEvent.name); @@ -176,13 +176,13 @@ function EventSchedulerModule(options) { }; } -// convienence static method for direct load + start +// convienence static method for direct load + start EventSchedulerModule.loadAndStart = function(cb) { const loadModuleEx = require('./module_util.js').loadModuleEx; const loadOpts = { - name : path.basename(__filename, '.js'), - path : __dirname, + name : path.basename(__filename, '.js'), + path : __dirname, }; loadModuleEx(loadOpts, (err, mod) => { @@ -199,7 +199,7 @@ EventSchedulerModule.loadAndStart = function(cb) { EventSchedulerModule.prototype.startup = function(cb) { - this.eventTimers = []; + this.eventTimers = []; const self = this; if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { @@ -215,10 +215,10 @@ EventSchedulerModule.prototype.startup = function(cb) { Log.debug( { - eventName : schedEvent.name, - schedule : this.moduleConfig.events[schedEvent.name].schedule, - action : schedEvent.action, - next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', + eventName : schedEvent.name, + schedule : this.moduleConfig.events[schedEvent.name].schedule, + action : schedEvent.action, + next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', }, 'Scheduled event loaded' ); @@ -237,7 +237,7 @@ EventSchedulerModule.prototype.startup = function(cb) { } ); - // :TODO: should track watched files & stop watching @ shutdown? + // :TODO: should track watched files & stop watching @ shutdown? [ 'change', 'add', 'delete' ].forEach(event => { watcher.on(event, (fileName, fileRoot) => { diff --git a/core/events.js b/core/events.js index aa75345f..6284597e 100644 --- a/core/events.js +++ b/core/events.js @@ -1,20 +1,20 @@ /* jslint node: true */ 'use strict'; -const paths = require('path'); -const events = require('events'); -const Log = require('./logger.js').log; -const SystemEvents = require('./system_events.js'); +const paths = require('path'); +const events = require('events'); +const Log = require('./logger.js').log; +const SystemEvents = require('./system_events.js'); -// deps -const _ = require('lodash'); -const async = require('async'); -const glob = require('glob'); +// deps +const _ = require('lodash'); +const async = require('async'); +const glob = require('glob'); module.exports = new class Events extends events.EventEmitter { constructor() { super(); - this.setMaxListeners(32); // :TODO: play with this... + this.setMaxListeners(32); // :TODO: play with this... } getSystemEvents() { @@ -60,7 +60,7 @@ module.exports = new class Events extends events.EventEmitter { const mod = require(fullModulePath); if(_.isFunction(mod.registerEvents)) { - // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? + // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? mod.registerEvents(this); } } catch(e) { diff --git a/core/exodus.js b/core/exodus.js index b20f18a1..e25b4973 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -1,83 +1,83 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const resetScreen = require('./ansi_term.js').resetScreen; -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const Log = require('./logger.js').log; -const getEnigmaUserAgent = require('./misc_util.js').getEnigmaUserAgent; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const resetScreen = require('./ansi_term.js').resetScreen; +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const Log = require('./logger.js').log; +const getEnigmaUserAgent = require('./misc_util.js').getEnigmaUserAgent; -// deps -const async = require('async'); -const _ = require('lodash'); -const joinPath = require('path').join; -const crypto = require('crypto'); -const moment = require('moment'); -const https = require('https'); -const querystring = require('querystring'); -const fs = require('fs'); -const SSHClient = require('ssh2').Client; +// deps +const async = require('async'); +const _ = require('lodash'); +const joinPath = require('path').join; +const crypto = require('crypto'); +const moment = require('moment'); +const https = require('https'); +const querystring = require('querystring'); +const fs = require('fs'); +const SSHClient = require('ssh2').Client; /* - Configuration block: + Configuration block: - someDoor: { - module: exodus - config: { - // defaults - ticketHost: oddnetwork.org - ticketPort: 1984 - ticketPath: /exodus - rejectUnauthorized: false // set to true to allow untrusted CA's (dangerous!) - sshHost: oddnetwork.org - sshPort: 22 - sshUser: exodus - sshKeyPem: /path/to/enigma-bbs/misc/exodus.id_rsa + someDoor: { + module: exodus + config: { + // defaults + ticketHost: oddnetwork.org + ticketPort: 1984 + ticketPath: /exodus + rejectUnauthorized: false // set to true to allow untrusted CA's (dangerous!) + sshHost: oddnetwork.org + sshPort: 22 + sshUser: exodus + sshKeyPem: /path/to/enigma-bbs/misc/exodus.id_rsa - // optional - caPem: /path/to/cacerts.pem // see https://curl.haxx.se/docs/caextract.html + // optional + caPem: /path/to/cacerts.pem // see https://curl.haxx.se/docs/caextract.html - // required - board: XXXX - key: XXXX - door: some_door - } - } + // required + board: XXXX + key: XXXX + door: some_door + } + } */ exports.moduleInfo = { - name : 'Exodus', - desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/', - author : 'NuSkooler', + name : 'Exodus', + desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/', + author : 'NuSkooler', }; exports.getModule = class ExodusModule extends MenuModule { constructor(options) { super(options); - this.config = options.menuConfig.config || {}; - this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; - this.config.ticketPort = this.config.ticketPort || 1984, - this.config.ticketPath = this.config.ticketPath || '/exodus'; - this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); - this.config.sshHost = this.config.sshHost || this.config.ticketHost; - this.config.sshPort = this.config.sshPort || 22; - this.config.sshUser = this.config.sshUser || 'exodus_server'; - this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); + this.config = options.menuConfig.config || {}; + this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; + this.config.ticketPort = this.config.ticketPort || 1984, + this.config.ticketPath = this.config.ticketPath || '/exodus'; + this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); + this.config.sshHost = this.config.sshHost || this.config.ticketHost; + this.config.sshPort = this.config.sshPort || 22; + this.config.sshUser = this.config.sshUser || 'exodus_server'; + this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); } initSequence() { - const self = this; - let clientTerminated = false; + const self = this; + let clientTerminated = false; async.waterfall( [ function validateConfig(callback) { - // very basic validation on optionals + // very basic validation on optionals async.each( [ 'board', 'key', 'door' ], (key, next) => { return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`)); }, callback); @@ -92,27 +92,27 @@ exports.getModule = class ExodusModule extends MenuModule { }); }, function getTicket(certAuthorities, callback) { - const now = moment.utc().unix(); - const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); - const token = `${sha256}|${now}`; + const now = moment.utc().unix(); + const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); + const token = `${sha256}|${now}`; - const postData = querystring.stringify({ - token : token, - board : self.config.board, - user : self.client.user.username, - door : self.config.door, + const postData = querystring.stringify({ + token : token, + board : self.config.board, + user : self.client.user.username, + door : self.config.door, }); const reqOptions = { - hostname : self.config.ticketHost, - port : self.config.ticketPort, - path : self.config.ticketPath, - rejectUnauthorized : self.config.rejectUnauthorized, - method : 'POST', - headers : { - 'Content-Type' : 'application/x-www-form-urlencoded', - 'Content-Length' : postData.length, - 'User-Agent' : getEnigmaUserAgent(), + hostname : self.config.ticketHost, + port : self.config.ticketPort, + path : self.config.ticketPath, + rejectUnauthorized : self.config.rejectUnauthorized, + method : 'POST', + headers : { + 'Content-Type' : 'application/x-www-form-urlencoded', + 'Content-Length' : postData.length, + 'User-Agent' : getEnigmaUserAgent(), } }; @@ -165,11 +165,11 @@ exports.getModule = class ExodusModule extends MenuModule { const sshClient = new SSHClient(); const window = { - rows : self.client.term.termHeight, - cols : self.client.term.termWidth, - width : 0, - height : 0, - term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( + rows : self.client.term.termHeight, + cols : self.client.term.termWidth, + width : 0, + height : 0, + term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( }; const options = { @@ -186,7 +186,7 @@ exports.getModule = class ExodusModule extends MenuModule { }); sshClient.shell(window, options, (err, stream) => { - pipedStream = stream; // :TODO: ewwwwwwwww hack + pipedStream = stream; // :TODO: ewwwwwwwww hack self.client.term.output.pipe(stream); stream.on('data', d => { @@ -210,10 +210,10 @@ exports.getModule = class ExodusModule extends MenuModule { }); sshClient.connect({ - host : self.config.sshHost, - port : self.config.sshPort, - username : self.config.sshUser, - privateKey : privateKey, + host : self.config.sshHost, + port : self.config.sshPort, + username : self.config.sshUser, + privateKey : privateKey, }); } ], diff --git a/core/file_area_filter_edit.js b/core/file_area_filter_edit.js index 3cdeb68b..06c0fd6e 100644 --- a/core/file_area_filter_edit.js +++ b/core/file_area_filter_edit.js @@ -1,36 +1,36 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('./file_base_filter.js'); -const stringFormat = require('./string_format.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('./file_base_filter.js'); +const stringFormat = require('./string_format.js'); -// deps -const async = require('async'); +// deps +const async = require('async'); exports.moduleInfo = { - name : 'File Area Filter Editor', - desc : 'Module for adding, deleting, and modifying file base filters', - author : 'NuSkooler', + name : 'File Area Filter Editor', + desc : 'Module for adding, deleting, and modifying file base filters', + author : 'NuSkooler', }; const MciViewIds = { editor : { - searchTerms : 1, - tags : 2, - area : 3, - sort : 4, - order : 5, - filterName : 6, - navMenu : 7, + searchTerms : 1, + tags : 2, + area : 3, + sort : 4, + order : 5, + filterName : 6, + navMenu : 7, - // :TODO: use the customs new standard thing - filter obj can have active/selected, etc. - selectedFilterInfo : 10, // { ...filter object ... } - activeFilterInfo : 11, // { ...filter object ... } - error : 12, // validation errors + // :TODO: use the customs new standard thing - filter obj can have active/selected, etc. + selectedFilterInfo : 10, // { ...filter object ... } + activeFilterInfo : 11, // { ...filter object ... } + error : 12, // validation errors } }; @@ -38,11 +38,11 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { constructor(options) { super(options); - this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them - this.currentFilterIndex = 0; // into |filtersArray| + this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them + this.currentFilterIndex = 0; // into |filtersArray| // - // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| + // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| // const activeFilter = FileBaseFilters.getActiveFilter(this.client); this.filtersArray.sort( (filterA, filterB) => { @@ -87,41 +87,41 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { return cb(null); }, newFilter : (formData, extraArgs, cb) => { - this.currentFilterIndex = this.filtersArray.length; // next avail slot + this.currentFilterIndex = this.filtersArray.length; // next avail slot this.clearForm(MciViewIds.editor.searchTerms); return cb(null); }, deleteFilter : (formData, extraArgs, cb) => { - const selectedFilter = this.filtersArray[this.currentFilterIndex]; - const filterUuid = selectedFilter.uuid; + const selectedFilter = this.filtersArray[this.currentFilterIndex]; + const filterUuid = selectedFilter.uuid; - // cannot delete built-in/system filters + // cannot delete built-in/system filters if(true === selectedFilter.system) { this.showError('Cannot delete built in filters!'); return cb(null); } - this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry + this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry - // remove from stored properties + // remove from stored properties const filters = new FileBaseFilters(this.client); filters.remove(filterUuid); filters.persist( () => { // - // If the item was also the active filter, we need to make a new one active + // If the item was also the active filter, we need to make a new one active // if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) { const newActive = this.filtersArray[this.currentFilterIndex]; if(newActive) { filters.setActive(newActive.uuid); } else { - // nothing to set active to + // nothing to set active to this.client.user.removeProperty('file_base_filter_active_uuid'); } } - // update UI + // update UI this.updateActiveLabel(); if(this.filtersArray.length > 0) { @@ -140,7 +140,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { if(errorView) { if(err) { errorView.setText(err.message); - err.view.clearText(); // clear out the invalid data + err.view.clearText(); // clear out the invalid data } else { errorView.clearText(); } @@ -168,8 +168,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { return cb(err); } - const self = this; - const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) ); + const self = this; + const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) ); async.series( [ @@ -241,7 +241,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { getSelectedAreaTag(index) { if(0 === index) { - return ''; // -ALL- + return ''; // -ALL- } const area = this.availAreas[index]; if(!area) { @@ -258,7 +258,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { let index; const filter = this.getCurrentFilter(); if(filter) { - // special treatment: areaTag saved as blank ("") if -ALL- + // special treatment: areaTag saved as blank ("") if -ALL- index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; } else { index = 0; @@ -293,31 +293,31 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } setFilterValuesFromFormData(filter, formData) { - filter.name = formData.value.name; - filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); - filter.terms = formData.value.searchTerms; - filter.tags = formData.value.tags; - filter.order = this.getOrderBy(formData.value.orderByIndex); - filter.sort = this.getSortBy(formData.value.sortByIndex); + filter.name = formData.value.name; + filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); + filter.terms = formData.value.searchTerms; + filter.tags = formData.value.tags; + filter.order = this.getOrderBy(formData.value.orderByIndex); + filter.sort = this.getSortBy(formData.value.sortByIndex); } saveCurrentFilter(formData, cb) { - const filters = new FileBaseFilters(this.client); - const selectedFilter = this.filtersArray[this.currentFilterIndex]; + const filters = new FileBaseFilters(this.client); + const selectedFilter = this.filtersArray[this.currentFilterIndex]; if(selectedFilter) { - // *update* currently selected filter + // *update* currently selected filter this.setFilterValuesFromFormData(selectedFilter, formData); filters.replace(selectedFilter.uuid, selectedFilter); } else { - // add a new entry; note that UUID will be generated + // add a new entry; note that UUID will be generated const newFilter = {}; this.setFilterValuesFromFormData(newFilter, formData); - // set current to what we just saved + // set current to what we just saved newFilter.uuid = filters.add(newFilter); - // add to our array (at current index position) + // add to our array (at current index position) this.filtersArray[this.currentFilterIndex] = newFilter; } @@ -327,9 +327,9 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { loadDataForFilter(filterIndex) { const filter = this.filtersArray[filterIndex]; if(filter) { - this.setText(MciViewIds.editor.searchTerms, filter.terms); - this.setText(MciViewIds.editor.tags, filter.tags); - this.setText(MciViewIds.editor.filterName, filter.name); + this.setText(MciViewIds.editor.searchTerms, filter.terms); + this.setText(MciViewIds.editor.tags, filter.tags); + this.setText(MciViewIds.editor.filterName, filter.name); this.setAreaIndexFromCurrentFilter(); this.setSortByFromCurrentFilter(); diff --git a/core/file_area_list.js b/core/file_area_list.js index 8b992f94..f3172d4d 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -1,71 +1,71 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const FileEntry = require('./file_entry.js'); -const stringFormat = require('./string_format.js'); -const FileArea = require('./file_base_area.js'); -const Errors = require('./enig_error.js').Errors; -const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; -const ArchiveUtil = require('./archive_util.js'); -const Config = require('./config.js').get; -const DownloadQueue = require('./download_queue.js'); -const FileAreaWeb = require('./file_area_web.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const resolveMimeType = require('./mime_util.js').resolveMimeType; -const isAnsi = require('./string_util.js').isAnsi; -const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const FileEntry = require('./file_entry.js'); +const stringFormat = require('./string_format.js'); +const FileArea = require('./file_base_area.js'); +const Errors = require('./enig_error.js').Errors; +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const ArchiveUtil = require('./archive_util.js'); +const Config = require('./config.js').get; +const DownloadQueue = require('./download_queue.js'); +const FileAreaWeb = require('./file_area_web.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const resolveMimeType = require('./mime_util.js').resolveMimeType; +const isAnsi = require('./string_util.js').isAnsi; +const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi; -// deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); -const paths = require('path'); +// deps +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); +const paths = require('path'); exports.moduleInfo = { - name : 'File Area List', - desc : 'Lists contents of file an file area', - author : 'NuSkooler', + name : 'File Area List', + desc : 'Lists contents of file an file area', + author : 'NuSkooler', }; const FormIds = { - browse : 0, - details : 1, - detailsGeneral : 2, - detailsNfo : 3, - detailsFileList : 4, + browse : 0, + details : 1, + detailsGeneral : 2, + detailsNfo : 3, + detailsFileList : 4, }; const MciViewIds = { - browse : { - desc : 1, - navMenu : 2, + browse : { + desc : 1, + navMenu : 2, - customRangeStart : 10, // 10+ = customs + customRangeStart : 10, // 10+ = customs }, - details : { - navMenu : 1, - infoXyTop : 2, // %XY starting position for info area - infoXyBottom : 3, + details : { + navMenu : 1, + infoXyTop : 2, // %XY starting position for info area + infoXyBottom : 3, - customRangeStart : 10, // 10+ = customs + customRangeStart : 10, // 10+ = customs }, detailsGeneral : { - customRangeStart : 10, // 10+ = customs + customRangeStart : 10, // 10+ = customs }, detailsNfo : { - nfo : 1, + nfo : 1, - customRangeStart : 10, // 10+ = customs + customRangeStart : 10, // 10+ = customs }, detailsFileList : { - fileList : 1, + fileList : 1, - customRangeStart : 10, // 10+ = customs + customRangeStart : 10, // 10+ = customs }, }; @@ -74,12 +74,12 @@ exports.getModule = class FileAreaList extends MenuModule { constructor(options) { super(options); - this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); - this.fileList = _.get(options, 'extraArgs.fileList'); - this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); + this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); + this.fileList = _.get(options, 'extraArgs.fileList'); + this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); if(this.fileList) { - // we'll need to adjust position as well! + // we'll need to adjust position as well! this.fileListPosition = 0; } @@ -102,7 +102,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(this.fileListPosition + 1 < this.fileList.length) { this.fileListPosition += 1; - return this.displayBrowsePage(true, cb); // true=clerarScreen + return this.displayBrowsePage(true, cb); // true=clerarScreen } if(this.lastFileNextExit) { @@ -115,7 +115,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(this.fileListPosition > 0) { --this.fileListPosition; - return this.displayBrowsePage(true, cb); // true=clearScreen + return this.displayBrowsePage(true, cb); // true=clearScreen } return cb(null); @@ -132,7 +132,7 @@ exports.getModule = class FileAreaList extends MenuModule { } }); - return this.displayBrowsePage(true, cb); // true=clearScreen + return this.displayBrowsePage(true, cb); // true=clearScreen }, toggleQueue : (formData, extraArgs, cb) => { this.dlQueue.toggle(this.currentFileEntry); @@ -158,15 +158,15 @@ exports.getModule = class FileAreaList extends MenuModule { getSaveState() { return { - fileList : this.fileList, - fileListPosition : this.fileListPosition, + fileList : this.fileList, + fileListPosition : this.fileListPosition, }; } restoreSavedState(savedState) { if(savedState) { - this.fileList = savedState.fileList; - this.fileListPosition = savedState.fileListPosition; + this.fileList = savedState.fileList; + this.fileListPosition = savedState.fileListPosition; } } @@ -215,35 +215,35 @@ exports.getModule = class FileAreaList extends MenuModule { } populateCurrentEntryInfo(cb) { - const config = this.menuConfig.config; - const currEntry = this.currentFileEntry; + const config = this.menuConfig.config; + const currEntry = this.currentFileEntry; const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; - const area = FileArea.getFileAreaByTag(currEntry.areaTag); - const hashTagsSep = config.hashTagsSep || ', '; - const isQueuedIndicator = config.isQueuedIndicator || 'Y'; - const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; + const area = FileArea.getFileAreaByTag(currEntry.areaTag); + const hashTagsSep = config.hashTagsSep || ', '; + const isQueuedIndicator = config.isQueuedIndicator || 'Y'; + const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; const entryInfo = currEntry.entryInfo = { - fileId : currEntry.fileId, - areaTag : currEntry.areaTag, - areaName : _.get(area, 'name') || 'N/A', - areaDesc : _.get(area, 'desc') || 'N/A', - fileSha256 : currEntry.fileSha256, - fileName : currEntry.fileName, - desc : currEntry.desc || '', - descLong : currEntry.descLong || '', - userRating : currEntry.userRating, - uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), - hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), - isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, - webDlLink : '', // :TODO: fetch web any existing web d/l link - webDlExpire : '', // :TODO: fetch web d/l link expire time + fileId : currEntry.fileId, + areaTag : currEntry.areaTag, + areaName : _.get(area, 'name') || 'N/A', + areaDesc : _.get(area, 'desc') || 'N/A', + fileSha256 : currEntry.fileSha256, + fileName : currEntry.fileName, + desc : currEntry.desc || '', + descLong : currEntry.descLong || '', + userRating : currEntry.userRating, + uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), + hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), + isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, + webDlLink : '', // :TODO: fetch web any existing web d/l link + webDlExpire : '', // :TODO: fetch web d/l link expire time }; // - // We need the entry object to contain meta keys even if they are empty as - // consumers may very likely attempt to use them + // We need the entry object to contain meta keys even if they are empty as + // consumers may very likely attempt to use them // const metaValues = FileEntry.WellKnownMetaValues; metaValues.forEach(name => { @@ -258,7 +258,7 @@ exports.getModule = class FileAreaList extends MenuModule { let fileType = _.get(Config(), [ 'fileTypes', mimeType ] ); if(Array.isArray(fileType)) { - // further refine by extention + // further refine by extention fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext); } desc = fileType && fileType.desc; @@ -268,31 +268,31 @@ exports.getModule = class FileAreaList extends MenuModule { entryInfo.archiveTypeDesc = 'N/A'; } - entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported - entryInfo.hashTags = entryInfo.hashTags || '(none)'; + entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported + entryInfo.hashTags = entryInfo.hashTags || '(none)'; - // create a rating string, e.g. "**---" - const userRatingTicked = config.userRatingTicked || '*'; - const userRatingUnticked = config.userRatingUnticked || ''; - entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! - entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); + // create a rating string, e.g. "**---" + const userRatingTicked = config.userRatingTicked || '*'; + const userRatingUnticked = config.userRatingUnticked || ''; + entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! + entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); if(entryInfo.userRating < 5) { entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) ); } FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => { if(err) { - entryInfo.webDlExpire = ''; + entryInfo.webDlExpire = ''; if(ErrNotEnabled === err.reasonCode) { - entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; + entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; } else { - entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; + entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; } } else { const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; - entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; + entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); } return cb(null); @@ -304,8 +304,8 @@ exports.getModule = class FileAreaList extends MenuModule { } displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; async.waterfall( [ @@ -326,8 +326,8 @@ exports.getModule = class FileAreaList extends MenuModule { function prepeareViewController(artData, callback) { if(_.isUndefined(self.viewControllers[name])) { const vcOpts = { - client : self.client, - formId : FormIds[name], + client : self.client, + formId : FormIds[name], }; if(!_.isUndefined(options.noInput)) { @@ -339,8 +339,8 @@ exports.getModule = class FileAreaList extends MenuModule { if('details' === name) { try { self.detailsInfoArea = { - top : artData.mciMap.XY2.position, - bottom : artData.mciMap.XY3.position, + top : artData.mciMap.XY2.position, + bottom : artData.mciMap.XY3.position, }; } catch(e) { return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!')); @@ -348,9 +348,9 @@ exports.getModule = class FileAreaList extends MenuModule { } const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds[name], }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -368,7 +368,7 @@ exports.getModule = class FileAreaList extends MenuModule { } displayBrowsePage(clearScreen, cb) { - const self = this; + const self = this; async.series( [ @@ -376,7 +376,7 @@ exports.getModule = class FileAreaList extends MenuModule { if(self.fileList) { return callback(null); } - return self.loadFileIds(false, callback); // false=do not force + return self.loadFileIds(false, callback); // false=do not force }, function checkEmptyResults(callback) { if(0 === self.fileList.length) { @@ -403,21 +403,21 @@ exports.getModule = class FileAreaList extends MenuModule { const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); if(descView) { // - // For descriptions we want to support as many color code systems - // as we can for coverage of what is found in the while (e.g. Renegade - // pipes, PCB @X##, etc.) + // For descriptions we want to support as many color code systems + // as we can for coverage of what is found in the while (e.g. Renegade + // pipes, PCB @X##, etc.) // - // MLTEV doesn't support all of this, so convert. If we produced ANSI - // esc sequences, we'll proceed with specialization, else just treat - // it as text. + // MLTEV doesn't support all of this, so convert. If we produced ANSI + // esc sequences, we'll proceed with specialization, else just treat + // it as text. // const desc = controlCodesToAnsi(self.currentFileEntry.desc); if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { descView.setAnsi( desc, { - prepped : false, - forceLineTerm : true + prepped : false, + forceLineTerm : true }, () => { return callback(null); @@ -447,7 +447,7 @@ exports.getModule = class FileAreaList extends MenuModule { } displayDetailsPage(cb) { - const self = this; + const self = this; async.series( [ @@ -467,9 +467,9 @@ exports.getModule = class FileAreaList extends MenuModule { navMenu.on('index update', index => { const sectionName = { - 0 : 'general', - 1 : 'nfo', - 2 : 'fileList', + 0 : 'general', + 1 : 'nfo', + 2 : 'fileList', }[index]; if(sectionName) { @@ -524,8 +524,8 @@ exports.getModule = class FileAreaList extends MenuModule { const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; - self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); + self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; + self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); return callback(null); } @@ -547,8 +547,8 @@ exports.getModule = class FileAreaList extends MenuModule { } updateQueueIndicator() { - const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; - const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; + const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; + const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; this.currentFileEntry.entryInfo.isQueued = stringFormat( this.dlQueue.isQueued(this.currentFileEntry) ? @@ -565,7 +565,7 @@ exports.getModule = class FileAreaList extends MenuModule { } cacheArchiveEntries(cb) { - // check cache + // check cache if(this.currentFileEntry.archiveEntries) { return cb(null, 'cache'); } @@ -575,8 +575,8 @@ exports.getModule = class FileAreaList extends MenuModule { return cb(Errors.Invalid('Invalid area tag')); } - const filePath = this.currentFileEntry.filePath; - const archiveUtil = ArchiveUtil.getInstance(); + const filePath = this.currentFileEntry.filePath; + const archiveUtil = ArchiveUtil.getInstance(); archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => { if(err) { @@ -594,14 +594,14 @@ exports.getModule = class FileAreaList extends MenuModule { if(this.currentFileEntry.entryInfo.archiveType) { this.cacheArchiveEntries( (err, cacheStatus) => { if(err) { - // :TODO: Handle me!!! - fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck + // :TODO: Handle me!!! + fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck return; } if('re-cached' === cacheStatus) { - const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? - const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; + const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? + const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) ); fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) ); @@ -615,8 +615,8 @@ exports.getModule = class FileAreaList extends MenuModule { } displayDetailsSection(sectionName, clearArea, cb) { - const self = this; - const name = `details${_.upperFirst(sectionName)}`; + const self = this; + const name = `details${_.upperFirst(sectionName)}`; async.series( [ @@ -637,8 +637,8 @@ exports.getModule = class FileAreaList extends MenuModule { if(clearArea) { self.client.term.rawWrite(ansi.reset()); - let pos = self.detailsInfoArea.top[0]; - const bottom = self.detailsInfoArea.bottom[0]; + let pos = self.detailsInfoArea.top[0]; + const bottom = self.detailsInfoArea.bottom[0]; while(pos++ <= bottom) { self.client.term.rawWrite(ansi.eraseLine() + ansi.down()); @@ -664,8 +664,8 @@ exports.getModule = class FileAreaList extends MenuModule { nfoView.setAnsi( self.currentFileEntry.entryInfo.descLong, { - prepped : false, - forceLineTerm : true, + prepped : false, + forceLineTerm : true, }, () => { return callback(null); @@ -701,7 +701,7 @@ exports.getModule = class FileAreaList extends MenuModule { loadFileIds(force, cb) { if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) { - this.fileListPosition = 0; + this.fileListPosition = 0; const filterCriteria = Object.assign({}, this.filterCriteria); if(!filterCriteria.areaTag) { diff --git a/core/file_area_web.js b/core/file_area_web.js index 88c108f6..a1d12989 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -1,29 +1,29 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const FileDb = require('./database.js').dbs.file; -const getISOTimestampString = require('./database.js').getISOTimestampString; -const FileEntry = require('./file_entry.js'); -const getServer = require('./listening_server.js').getServer; -const Errors = require('./enig_error.js').Errors; -const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; -const StatLog = require('./stat_log.js'); -const User = require('./user.js'); -const Log = require('./logger.js').log; -const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; -const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const Events = require('./events.js'); +// ENiGMA½ +const Config = require('./config.js').get; +const FileDb = require('./database.js').dbs.file; +const getISOTimestampString = require('./database.js').getISOTimestampString; +const FileEntry = require('./file_entry.js'); +const getServer = require('./listening_server.js').getServer; +const Errors = require('./enig_error.js').Errors; +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const Log = require('./logger.js').log; +const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; +const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const Events = require('./events.js'); -// deps -const hashids = require('hashids'); -const moment = require('moment'); -const paths = require('path'); -const async = require('async'); -const fs = require('graceful-fs'); -const mimeTypes = require('mime-types'); -const yazl = require('yazl'); +// deps +const hashids = require('hashids'); +const moment = require('moment'); +const paths = require('path'); +const async = require('async'); +const fs = require('graceful-fs'); +const mimeTypes = require('mime-types'); +const yazl = require('yazl'); function notEnabledError() { return Errors.General('Web server is not enabled', ErrNotEnabled); @@ -31,8 +31,8 @@ function notEnabledError() { class FileAreaWebAccess { constructor() { - this.hashids = new hashids(Config().general.boardName); - this.expireTimers = {}; // hashId->timer + this.hashids = new hashids(Config().general.boardName); + this.expireTimers = {}; // hashId->timer } startup(cb) { @@ -51,13 +51,13 @@ class FileAreaWebAccess { if(self.isEnabled()) { const routeAdded = self.webServer.instance.addRoute({ - method : 'GET', - path : Config().fileBase.web.routePath, - handler : self.routeWebRequest.bind(self), + method : 'GET', + path : Config().fileBase.web.routePath, + handler : self.routeWebRequest.bind(self), }); return callback(routeAdded ? null : Errors.General('Failed adding route')); } else { - return callback(null); // not enabled, but no error + return callback(null); // not enabled, but no error } } ], @@ -77,18 +77,18 @@ class FileAreaWebAccess { static getHashIdTypes() { return { - SingleFile : 0, - BatchArchive : 1, + SingleFile : 0, + BatchArchive : 1, }; } load(cb) { // - // Load entries, register expiration timers + // Load entries, register expiration timers // FileDb.each( `SELECT hash_id, expire_timestamp - FROM file_web_serve;`, + FROM file_web_serve;`, (err, row) => { if(row) { this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); @@ -102,11 +102,11 @@ class FileAreaWebAccess { removeEntry(hashId) { // - // Delete record from DB, and our timer + // Delete record from DB, and our timer // FileDb.run( `DELETE FROM file_web_serve - WHERE hash_id = ?;`, + WHERE hash_id = ?;`, [ hashId ] ); @@ -115,7 +115,7 @@ class FileAreaWebAccess { scheduleExpire(hashId, expireTime) { - // remove any previous entry for this hashId + // remove any previous entry for this hashId const previous = this.expireTimers[hashId]; if(previous) { clearTimeout(previous); @@ -138,8 +138,8 @@ class FileAreaWebAccess { loadServedHashId(hashId, cb) { FileDb.get( `SELECT expire_timestamp FROM - file_web_serve - WHERE hash_id = ?`, + file_web_serve + WHERE hash_id = ?`, [ hashId ], (err, result) => { if(err || !result) { @@ -148,16 +148,16 @@ class FileAreaWebAccess { const decoded = this.hashids.decode(hashId); - // decode() should provide an array of [ userId, hashIdType, id, ... ] + // decode() should provide an array of [ userId, hashIdType, id, ... ] if(!Array.isArray(decoded) || decoded.length < 3) { return cb(Errors.Invalid('Invalid or unknown hash ID')); } const servedItem = { - hashId : hashId, - userId : decoded[0], - hashIdType : decoded[1], - expireTimestamp : moment(result.expire_timestamp), + hashId : hashId, + userId : decoded[0], + hashIdType : decoded[1], + expireTimestamp : moment(result.expire_timestamp), }; if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { @@ -209,10 +209,10 @@ class FileAreaWebAccess { } _addOrUpdateHashIdRecord(dbOrTrans, hashId, expireTime, cb) { - // add/update rec with hash id and (latest) timestamp + // add/update rec with hash id and (latest) timestamp dbOrTrans.run( `REPLACE INTO file_web_serve (hash_id, expire_timestamp) - VALUES (?, ?);`, + VALUES (?, ?);`, [ hashId, getISOTimestampString(expireTime) ], err => { if(err) { @@ -231,9 +231,9 @@ class FileAreaWebAccess { return cb(notEnabledError()); } - const hashId = this.getSingleFileHashId(client, fileEntry); - const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId); - options.expireTime = options.expireTime || moment().add(2, 'days'); + const hashId = this.getSingleFileHashId(client, fileEntry); + const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId); + options.expireTime = options.expireTime || moment().add(2, 'days'); this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => { return cb(err, url); @@ -245,10 +245,10 @@ class FileAreaWebAccess { return cb(notEnabledError()); } - const batchId = moment().utc().unix(); - const hashId = this.getBatchArchiveHashId(client, batchId); - const url = this.buildBatchArchiveTempDownloadLink(client, hashId); - options.expireTime = options.expireTime || moment().add(2, 'days'); + const batchId = moment().utc().unix(); + const hashId = this.getBatchArchiveHashId(client, batchId); + const url = this.buildBatchArchiveTempDownloadLink(client, hashId); + options.expireTime = options.expireTime || moment().add(2, 'days'); FileDb.beginTransaction( (err, trans) => { if(err) { @@ -265,7 +265,7 @@ class FileAreaWebAccess { async.eachSeries(fileEntries, (entry, nextEntry) => { trans.run( `INSERT INTO file_web_serve_batch (hash_id, file_id) - VALUES (?, ?);`, + VALUES (?, ?);`, [ hashId, entry.fileId ], err => { return nextEntry(err); @@ -332,19 +332,19 @@ class FileAreaWebAccess { } resp.on('close', () => { - // connection closed *before* the response was fully sent - // :TODO: Log and such + // connection closed *before* the response was fully sent + // :TODO: Log and such }); resp.on('finish', () => { - // transfer completed fully + // transfer completed fully this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); }); const headers = { - 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), - 'Content-Length' : stats.size, - 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, + 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), + 'Content-Length' : stats.size, + 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, }; const readStream = fs.createReadStream(filePath); @@ -358,10 +358,10 @@ class FileAreaWebAccess { Log.debug( { servedItem : servedItem }, 'Batch file web request'); // - // We are going to build an on-the-fly zip file stream of 1:n - // files in the batch. + // We are going to build an on-the-fly zip file stream of 1:n + // files in the batch. // - // First, collect all file IDs + // First, collect all file IDs // const self = this; @@ -370,8 +370,8 @@ class FileAreaWebAccess { function fetchFileIds(callback) { FileDb.all( `SELECT file_id - FROM file_web_serve_batch - WHERE hash_id = ?;`, + FROM file_web_serve_batch + WHERE hash_id = ?;`, [ servedItem.hashId ], (err, fileIdRows) => { if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { @@ -408,10 +408,10 @@ class FileAreaWebAccess { filePaths.forEach(fp => { zipFile.addFile( - fp, // path to physical file - paths.basename(fp), // filename/path *stored in archive* + fp, // path to physical file + paths.basename(fp), // filename/path *stored in archive* { - compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. + compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. } ); }); @@ -422,21 +422,21 @@ class FileAreaWebAccess { } resp.on('close', () => { - // connection closed *before* the response was fully sent - // :TODO: Log and such + // connection closed *before* the response was fully sent + // :TODO: Log and such }); resp.on('finish', () => { - // transfer completed fully + // transfer completed fully self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); }); const batchFileName = `batch_${servedItem.hashId}.zip`; const headers = { - 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), - 'Content-Length' : finalZipSize, - 'Content-Disposition' : `attachment; filename="${batchFileName}"`, + 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), + 'Content-Length' : finalZipSize, + 'Content-Disposition' : `attachment; filename="${batchFileName}"`, }; resp.writeHead(200, headers); @@ -446,11 +446,11 @@ class FileAreaWebAccess { ], err => { if(err) { - // :TODO: Log me! + // :TODO: Log me! return this.fileNotFound(resp); } - // ...otherwise, we would have called resp() already. + // ...otherwise, we would have called resp() already. } ); } @@ -464,7 +464,7 @@ class FileAreaWebAccess { return callback(null, clientForUserId.user); } - // not online now - look 'em up + // not online now - look 'em up User.getUser(userId, (err, assocUser) => { return callback(err, assocUser); }); @@ -481,8 +481,8 @@ class FileAreaWebAccess { Events.emit( Events.getSystemEvents().UserDownload, { - user : user, - files : fileEntries, + user : user, + files : fileEntries, } ); return callback(null); diff --git a/core/file_base_area.js b/core/file_base_area.js index 511e11af..7d7ce6fb 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -1,57 +1,57 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; -const FileEntry = require('./file_entry.js'); -const FileDb = require('./database.js').dbs.file; -const ArchiveUtil = require('./archive_util.js'); -const CRC32 = require('./crc.js').CRC32; -const Log = require('./logger.js').log; -const resolveMimeType = require('./mime_util.js').resolveMimeType; -const stringFormat = require('./string_format.js'); -const wordWrapText = require('./word_wrap.js').wordWrapText; -const StatLog = require('./stat_log.js'); +// ENiGMA½ +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; +const FileEntry = require('./file_entry.js'); +const FileDb = require('./database.js').dbs.file; +const ArchiveUtil = require('./archive_util.js'); +const CRC32 = require('./crc.js').CRC32; +const Log = require('./logger.js').log; +const resolveMimeType = require('./mime_util.js').resolveMimeType; +const stringFormat = require('./string_format.js'); +const wordWrapText = require('./word_wrap.js').wordWrapText; +const StatLog = require('./stat_log.js'); -// deps -const _ = require('lodash'); -const async = require('async'); -const fs = require('graceful-fs'); -const crypto = require('crypto'); -const paths = require('path'); -const temptmp = require('temptmp').createTrackedSession('file_area'); -const iconv = require('iconv-lite'); -const execFile = require('child_process').execFile; -const moment = require('moment'); +// deps +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const crypto = require('crypto'); +const paths = require('path'); +const temptmp = require('temptmp').createTrackedSession('file_area'); +const iconv = require('iconv-lite'); +const execFile = require('child_process').execFile; +const moment = require('moment'); -exports.startup = startup; -exports.isInternalArea = isInternalArea; -exports.getAvailableFileAreas = getAvailableFileAreas; -exports.getAvailableFileAreaTags = getAvailableFileAreaTags; -exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; -exports.isValidStorageTag = isValidStorageTag; -exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag; -exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; -exports.getAreaStorageLocations = getAreaStorageLocations; -exports.getDefaultFileAreaTag = getDefaultFileAreaTag; -exports.getFileAreaByTag = getFileAreaByTag; -exports.getFileEntryPath = getFileEntryPath; -exports.changeFileAreaWithOptions = changeFileAreaWithOptions; -exports.scanFile = scanFile; -exports.scanFileAreaForChanges = scanFileAreaForChanges; -exports.getDescFromFileName = getDescFromFileName; -exports.getAreaStats = getAreaStats; -exports.cleanUpTempSessionItems = cleanUpTempSessionItems; +exports.startup = startup; +exports.isInternalArea = isInternalArea; +exports.getAvailableFileAreas = getAvailableFileAreas; +exports.getAvailableFileAreaTags = getAvailableFileAreaTags; +exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; +exports.isValidStorageTag = isValidStorageTag; +exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag; +exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; +exports.getAreaStorageLocations = getAreaStorageLocations; +exports.getDefaultFileAreaTag = getDefaultFileAreaTag; +exports.getFileAreaByTag = getFileAreaByTag; +exports.getFileEntryPath = getFileEntryPath; +exports.changeFileAreaWithOptions = changeFileAreaWithOptions; +exports.scanFile = scanFile; +exports.scanFileAreaForChanges = scanFileAreaForChanges; +exports.getDescFromFileName = getDescFromFileName; +exports.getAreaStats = getAreaStats; +exports.cleanUpTempSessionItems = cleanUpTempSessionItems; -// for scheduler: -exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; +// for scheduler: +exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; -const WellKnownAreaTags = exports.WellKnownAreaTags = { - Invalid : '', - MessageAreaAttach : 'system_message_attachment', - TempDownloads : 'system_temporary_download', +const WellKnownAreaTags = exports.WellKnownAreaTags = { + Invalid : '', + MessageAreaAttach : 'system_message_attachment', + TempDownloads : 'system_temporary_download', }; function startup(cb) { @@ -65,7 +65,7 @@ function isInternalArea(areaTag) { function getAvailableFileAreas(client, options) { options = options || { }; - // perform ACS check per conf & omit internal if desired + // perform ACS check per conf & omit internal if desired const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => Object.assign(areaInfo, { areaTag : areaTag } )); return _.omitBy(allAreas, areaInfo => { @@ -74,11 +74,11 @@ function getAvailableFileAreas(client, options) { } if(options.skipAcsCheck) { - return false; // no ACS checks (below) + return false; // no ACS checks (below) } if(options.writeAcs && !client.acs.hasFileAreaWrite(areaInfo)) { - return true; // omit + return true; // omit } return !client.acs.hasFileAreaRead(areaInfo); @@ -116,8 +116,8 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { function getFileAreaByTag(areaTag) { const areaInfo = Config().fileBase.areas[areaTag]; if(areaInfo) { - areaInfo.areaTag = areaTag; // convienence! - areaInfo.storage = getAreaStorageLocations(areaInfo); + areaInfo.areaTag = areaTag; // convienence! + areaInfo.storage = getAreaStorageLocations(areaInfo); return areaInfo; } } @@ -183,8 +183,8 @@ function getAreaStorageLocations(areaInfo) { return _.compact(storageTags.map(storageTag => { if(avail[storageTag]) { return { - storageTag : storageTag, - dir : getAreaStorageDirectoryByTag(storageTag), + storageTag : storageTag, + dir : getAreaStorageDirectoryByTag(storageTag), }; } })); @@ -202,14 +202,14 @@ function getExistingFileEntriesBySha256(sha256, cb) { FileDb.each( `SELECT file_id, area_tag - FROM file - WHERE file_sha256=?;`, + FROM file + WHERE file_sha256=?;`, [ sha256 ], (err, fileRow) => { if(fileRow) { entries.push({ - fileId : fileRow.file_id, - areaTag : fileRow.area_tag, + fileId : fileRow.file_id, + areaTag : fileRow.area_tag, }); } }, @@ -219,10 +219,10 @@ function getExistingFileEntriesBySha256(sha256, cb) { ); } -// :TODO: This is bascially sliceAtEOF() from art.js .... DRY! +// :TODO: This is bascially sliceAtEOF() from art.js .... DRY! function sliceAtSauceMarker(data) { - let eof = data.length; - const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) + let eof = data.length; + const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) for(let i = eof - 1; i > stopPos; i--) { if(0x1a === data[i]) { @@ -234,8 +234,8 @@ function sliceAtSauceMarker(data) { } function attemptSetEstimatedReleaseDate(fileEntry) { - // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time - const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); + // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time + const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); function getMatch(input) { if(input) { @@ -250,10 +250,10 @@ function attemptSetEstimatedReleaseDate(fileEntry) { } // - // We attempt detection in short -> long order + // We attempt detection in short -> long order // - // Throw out anything that is current_year + 2 (we give some leway) - // with the assumption that must be wrong. + // Throw out anything that is current_year + 2 (we give some leway) + // with the assumption that must be wrong. // const maxYear = moment().add(2, 'year').year(); const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); @@ -279,7 +279,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { } } -// a simple log proxy for when we call from oputil.js +// a simple log proxy for when we call from oputil.js function logDebug(obj, msg) { if(Log) { Log.debug(obj, msg); @@ -290,8 +290,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { async.waterfall( [ function extractDescFiles(callback) { - // :TODO: would be nice if these RegExp's were cached - // :TODO: this is long winded... + // :TODO: would be nice if these RegExp's were cached + // :TODO: this is long winded... const config = Config(); const extractList = []; @@ -327,8 +327,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } const descFiles = { - desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, - descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null, + desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, + descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null, }; return callback(null, descFiles); @@ -348,7 +348,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { return next(null); } - // skip entries that are too large + // skip entries that are too large const maxFileSizeKey = `max${_.upperFirst(descType)}FileByteSize`; if(config.fileBase[maxFileSizeKey] && stats.size > config.fileBase[maxFileSizeKey]) { logDebug( { byteSize : stats.size, maxByteSize : config.fileBase[maxFileSizeKey] }, `Skipping "${descType}"; Too large` ); @@ -361,18 +361,18 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } // - // Assume FILE_ID.DIZ, NFO files, etc. are CP437. + // Assume FILE_ID.DIZ, NFO files, etc. are CP437. // - // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... - fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); - fileEntry[`${descType}Src`] = 'descFile'; + // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... + fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); + fileEntry[`${descType}Src`] = 'descFile'; return next(null); }); }); }, () => { - // cleanup but don't wait + // cleanup but don't wait temptmp.cleanup( paths => { - // note: don't use client logger here - may not be avail + // note: don't use client logger here - may not be avail logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); }); return callback(null); @@ -390,7 +390,7 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries async.waterfall( [ function extractToTemp(callback) { - // :TODO: we may want to skip this if the compressed file is too large... + // :TODO: we may want to skip this if the compressed file is too large... temptmp.mkdir( { prefix : 'enigextract-' }, (err, tempDir) => { if(err) { return callback(err); @@ -398,7 +398,7 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries const archiveUtil = ArchiveUtil.getInstance(); - // ensure we only extract one - there should only be one anyway -- we also just need the fileName + // ensure we only extract one - there should only be one anyway -- we also just need the fileName const extractList = archiveEntries.slice(0, 1).map(entry => entry.fileName); archiveUtil.extractTo(filePath, tempDir, fileEntry.meta.archive_type, extractList, err => { @@ -413,8 +413,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries function processSingleExtractedFile(extractedFile, callback) { populateFileEntryInfoFromFile(fileEntry, extractedFile, err => { if(!fileEntry.desc) { - fileEntry.desc = getDescFromFileName(filePath); - fileEntry.descSrc = 'fileName'; + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; } return callback(err); }); @@ -427,8 +427,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries } function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) { - const archiveUtil = ArchiveUtil.getInstance(); - const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() + const archiveUtil = ArchiveUtil.getInstance(); + const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() async.waterfall( [ @@ -449,7 +449,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c } iterator(iterErr => { - return callback( iterErr, entries || [] ); // ignore original |err| here + return callback( iterErr, entries || [] ); // ignore original |err| here }); }); }); @@ -462,12 +462,12 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c }, function extractDescFromArchive(entries, callback) { // - // If we have a -single- entry in the archive, extract that file - // and try retrieving info in the non-archive manor. This should - // work for things like zipped up .pdf files. + // If we have a -single- entry in the archive, extract that file + // and try retrieving info in the non-archive manor. This should + // work for things like zipped up .pdf files. // - // Otherwise, try to find particular desc files such as FILE_ID.DIZ - // and README.1ST + // Otherwise, try to find particular desc files such as FILE_ID.DIZ + // and README.1ST // const archDescHandler = (1 === entries.length) ? extractAndProcessSingleArchiveEntry : extractAndProcessDescFiles; archDescHandler(fileEntry, filePath, entries, err => { @@ -494,7 +494,7 @@ function getInfoExtractUtilForDesc(mimeType, filePath, descType) { let fileType = _.get(config, [ 'fileTypes', mimeType ] ); if(Array.isArray(fileType)) { - // further refine by extention + // further refine by extention fileType = fileType.find(ft => paths.extname(filePath) === ft.ext); } @@ -542,17 +542,17 @@ function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { const key = 'short' === descType ? 'desc' : 'descLong'; if('desc' === key) { // - // Word wrap short descriptions to FILE_ID.DIZ spec + // Word wrap short descriptions to FILE_ID.DIZ spec // - // "...no more than 45 characters long" + // "...no more than 45 characters long" // - // See http://www.textfiles.com/computers/fileid.txt + // See http://www.textfiles.com/computers/fileid.txt // stdout = (wordWrapText( stdout, { width : 45 } ).wrapped || []).join('\n'); } - fileEntry[key] = stdout; - fileEntry[`${key}Src`] = 'infoTool'; + fileEntry[key] = stdout; + fileEntry[`${key}Src`] = 'infoTool'; } } @@ -574,8 +574,8 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb function getDescriptions(callback) { populateFileEntryInfoFromFile(fileEntry, filePath, err => { if(!fileEntry.desc) { - fileEntry.desc = getDescFromFileName(filePath); - fileEntry.descSrc = 'fileName'; + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; } return callback(err); }); @@ -592,7 +592,7 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb } function addNewFileEntry(fileEntry, filePath, cb) { - // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data + // :TODO: Use detectTypeWithBuf() once avail - we *just* read some file data async.series( [ @@ -611,26 +611,26 @@ const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ]; function scanFile(filePath, options, iterator, cb) { if(3 === arguments.length && _.isFunction(iterator)) { - cb = iterator; - iterator = null; + cb = iterator; + iterator = null; } else if(2 === arguments.length && _.isFunction(options)) { - cb = options; - iterator = null; - options = {}; + cb = options; + iterator = null; + options = {}; } const fileEntry = new FileEntry({ - areaTag : options.areaTag, - meta : options.meta, - hashTags : options.hashTags, // Set() or Array - fileName : paths.basename(filePath), - storageTag : options.storageTag, - fileSha256 : options.sha256, // caller may know this already + areaTag : options.areaTag, + meta : options.meta, + hashTags : options.hashTags, // Set() or Array + fileName : paths.basename(filePath), + storageTag : options.storageTag, + fileSha256 : options.sha256, // caller may know this already }); const stepInfo = { - filePath : filePath, - fileName : paths.basename(filePath), + filePath : filePath, + fileName : paths.basename(filePath), }; const callIter = (next) => { @@ -638,8 +638,8 @@ function scanFile(filePath, options, iterator, cb) { }; const readErrorCallIter = (origError, next) => { - stepInfo.step = 'read_error'; - stepInfo.error = origError.message; + stepInfo.step = 'read_error'; + stepInfo.error = origError.message; callIter( () => { return next(origError); @@ -648,7 +648,7 @@ function scanFile(filePath, options, iterator, cb) { let lastCalcHashPercent; - // don't re-calc hashes for any we already have in |options| + // don't re-calc hashes for any we already have in |options| const hashesToCalc = HASH_NAMES.filter(hn => { if('sha256' === hn && fileEntry.fileSha256) { return false; @@ -669,8 +669,8 @@ function scanFile(filePath, options, iterator, cb) { return readErrorCallIter(err, callback); } - stepInfo.step = 'start'; - stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; + stepInfo.step = 'start'; + stepInfo.byteSize = fileEntry.meta.byte_size = stats.size; return callIter(callback); }); @@ -694,9 +694,9 @@ function scanFile(filePath, options, iterator, cb) { }; // - // Note that we are not using fs.createReadStream() here: - // While convenient, it is quite a bit slower -- which adds - // up to many seconds in time for larger files. + // Note that we are not using fs.createReadStream() here: + // While convenient, it is quite a bit slower -- which adds + // up to many seconds in time for larger files. // const chunkSize = 1024 * 64; const buffer = new Buffer(chunkSize); @@ -714,7 +714,7 @@ function scanFile(filePath, options, iterator, cb) { } if(0 === bytesRead) { - // done - finalize + // done - finalize fileEntry.meta.byte_size = stepInfo.bytesProcessed; for(let i = 0; i < hashesToCalc.length; ++i) { @@ -733,11 +733,11 @@ function scanFile(filePath, options, iterator, cb) { return callIter(callback); } - stepInfo.bytesProcessed += bytesRead; - stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); + stepInfo.bytesProcessed += bytesRead; + stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); // - // Only send 'hash_update' step update if we have a noticable percentage change in progress + // Only send 'hash_update' step update if we have a noticable percentage change in progress // const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { @@ -745,7 +745,7 @@ function scanFile(filePath, options, iterator, cb) { return nextChunk(); } else { lastCalcHashPercent = stepInfo.calcHashPercent; - stepInfo.step = 'hash_update'; + stepInfo.step = 'hash_update'; callIter(err => { if(err) { @@ -767,7 +767,7 @@ function scanFile(filePath, options, iterator, cb) { archiveUtil.detectType(filePath, (err, archiveType) => { if(archiveType) { - // save this off + // save this off fileEntry.meta.archive_type = archiveType; populateFileEntryWithArchive(fileEntry, filePath, stepInfo, callIter, err => { @@ -776,7 +776,7 @@ function scanFile(filePath, options, iterator, cb) { if(err) { logDebug( { error : err.message }, 'Non-archive file entry population failed'); } - return callback(null); // ignore err + return callback(null); // ignore err }); } else { return callback(null); @@ -787,7 +787,7 @@ function scanFile(filePath, options, iterator, cb) { if(err) { logDebug( { error : err.message }, 'Non-archive file entry population failed'); } - return callback(null); // ignore err + return callback(null); // ignore err }); } }); @@ -816,12 +816,12 @@ function scanFile(filePath, options, iterator, cb) { function scanFileAreaForChanges(areaInfo, options, iterator, cb) { if(3 === arguments.length && _.isFunction(iterator)) { - cb = iterator; - iterator = null; + cb = iterator; + iterator = null; } else if(2 === arguments.length && _.isFunction(options)) { - cb = options; - iterator = null; - options = {}; + cb = options; + iterator = null; + options = {}; } const storageLocations = getAreaStorageLocations(areaInfo); @@ -842,8 +842,8 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { fs.stat(fullPath, (err, stats) => { if(err) { - // :TODO: Log me! - return nextFile(null); // always try next file + // :TODO: Log me! + return nextFile(null); // always try next file } if(!stats.isFile()) { @@ -853,18 +853,18 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { scanFile( fullPath, { - areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag + areaTag : areaInfo.areaTag, + storageTag : storageLoc.storageTag }, iterator, (err, fileEntry, dupeEntries) => { if(err) { - // :TODO: Log me!!! - return nextFile(null); // try next anyway + // :TODO: Log me!!! + return nextFile(null); // try next anyway } if(dupeEntries.length > 0) { - // :TODO: Handle duplidates -- what to do here??? + // :TODO: Handle duplidates -- what to do here??? } else { if(Array.isArray(options.tags)) { options.tags.forEach(tag => { @@ -872,7 +872,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { }); } addNewFileEntry(fileEntry, fullPath, err => { - // pass along error; we failed to insert a record in our DB or something else bad + // pass along error; we failed to insert a record in our DB or something else bad return nextFile(err); }); } @@ -885,7 +885,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { }); }, function scanDbEntries(callback) { - // :TODO: Look @ db entries for area that were *not* processed above + // :TODO: Look @ db entries for area that were *not* processed above return callback(null); } ], @@ -900,7 +900,7 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { } function getDescFromFileName(fileName) { - // :TODO: this method could use some more logic to really be nice. + // :TODO: this method could use some more logic to really be nice. const ext = paths.extname(fileName); const name = paths.basename(fileName, ext); @@ -908,26 +908,26 @@ function getDescFromFileName(fileName) { } // -// Return an object of stats about an area(s) +// Return an object of stats about an area(s) // -// { +// { // -// totalFiles : , -// totalBytes : , -// areas : { -// : { -// files : , -// bytes : -// } -// } -// } +// totalFiles : , +// totalBytes : , +// areas : { +// : { +// files : , +// bytes : +// } +// } +// } // function getAreaStats(cb) { FileDb.all( `SELECT DISTINCT f.area_tag, COUNT(f.file_id) AS total_files, SUM(m.meta_value) AS total_byte_size - FROM file f, file_meta m - WHERE f.file_id = m.file_id AND m.meta_name='byte_size' - GROUP BY f.area_tag;`, + FROM file f, file_meta m + WHERE f.file_id = m.file_id AND m.meta_name='byte_size' + GROUP BY f.area_tag;`, (err, statRows) => { if(err) { return cb(err); @@ -946,8 +946,8 @@ function getAreaStats(cb) { stats.areas = stats.areas || {}; stats.areas[v.area_tag] = { - files : v.total_files, - bytes : v.total_byte_size, + files : v.total_files, + bytes : v.total_byte_size, }; return stats; }, {}) @@ -956,7 +956,7 @@ function getAreaStats(cb) { ); } -// method exposed for event scheduler +// method exposed for event scheduler function updateAreaStatsScheduledEvent(args, cb) { getAreaStats( (err, stats) => { if(!err) { @@ -968,13 +968,13 @@ function updateAreaStatsScheduledEvent(args, cb) { } function cleanUpTempSessionItems(cb) { - // find (old) temporary session items and nuke 'em + // find (old) temporary session items and nuke 'em const filter = { - areaTag : WellKnownAreaTags.TempDownloads, - metaPairs : [ + areaTag : WellKnownAreaTags.TempDownloads, + metaPairs : [ { - name : 'session_temp_dl', - value : 1 + name : 'session_temp_dl', + value : 1 } ] }; diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index bdca4e72..1b77eb21 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -1,22 +1,22 @@ /* jslint node: true */ 'use strict'; -// enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const { getSortedAvailableFileAreas } = require('./file_base_area.js'); -const StatLog = require('./stat_log.js'); +// enigma-bbs +const MenuModule = require('./menu_module.js').MenuModule; +const { getSortedAvailableFileAreas } = require('./file_base_area.js'); +const StatLog = require('./stat_log.js'); -// deps -const async = require('async'); +// deps +const async = require('async'); exports.moduleInfo = { - name : 'File Area Selector', - desc : 'Select from available file areas', - author : 'NuSkooler', + name : 'File Area Selector', + desc : 'Select from available file areas', + author : 'NuSkooler', }; const MciViewIds = { - areaList : 1, + areaList : 1, }; exports.getModule = class FileAreaSelectModule extends MenuModule { @@ -26,14 +26,14 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { this.menuMethods = { selectArea : (formData, extraArgs, cb) => { const filterCriteria = { - areaTag : formData.value.areaTag, + areaTag : formData.value.areaTag, }; const menuOpts = { - extraArgs : { - filterCriteria : filterCriteria, + extraArgs : { + filterCriteria : filterCriteria, }, - menuFlags : [ 'popParent', 'mergeFlags' ], + menuFlags : [ 'popParent', 'mergeFlags' ], }; return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); @@ -54,12 +54,12 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { function mergeAreaStats(callback) { const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; - // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override + // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override const availAreas = getSortedAvailableFileAreas(self.client); availAreas.forEach(area => { const stats = areaStats.areas[area.areaTag]; area.totalFiles = stats ? stats.files : 0; - area.totalBytes = stats ? stats.bytes : 0; + area.totalBytes = stats ? stats.bytes : 0; }); return callback(null, availAreas); diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index fc7672d0..f590ebd3 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -1,37 +1,37 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const DownloadQueue = require('./download_queue.js'); -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const Errors = require('./enig_error.js').Errors; -const stringFormat = require('./string_format.js'); -const FileAreaWeb = require('./file_area_web.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const DownloadQueue = require('./download_queue.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const Errors = require('./enig_error.js').Errors; +const stringFormat = require('./string_format.js'); +const FileAreaWeb = require('./file_area_web.js'); -// deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +// deps +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { - name : 'File Base Download Queue Manager', - desc : 'Module for interacting with download queue/batch', - author : 'NuSkooler', + name : 'File Base Download Queue Manager', + desc : 'Module for interacting with download queue/batch', + author : 'NuSkooler', }; const FormIds = { - queueManager : 0, + queueManager : 0, }; const MciViewIds = { queueManager : { - queue : 1, - navMenu : 2, + queue : 1, + navMenu : 2, - customRangeStart : 10, + customRangeStart : 10, }, }; @@ -52,8 +52,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { downloadAll : (formData, extraArgs, cb) => { const modOpts = { extraArgs : { - sendQueue : this.dlQueue.items, - direction : 'send', + sendQueue : this.dlQueue.items, + direction : 'send', } }; @@ -67,13 +67,13 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { this.dlQueue.removeItems(selectedItem.fileId); - // :TODO: broken: does not redraw menu properly - needs fixed! + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); - // :TODO: broken: does not redraw menu properly - needs fixed! + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView('all', cb); } }; @@ -82,11 +82,11 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { initSequence() { if(0 === this.dlQueue.items.length) { if(this.sendFileIds) { - // we've finished everything up - just fall back + // we've finished everything up - just fall back return this.prevMenu(); } - // Simply an empty D/L queue: Present a specialized "empty queue" page + // Simply an empty D/L queue: Present a specialized "empty queue" page return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); } @@ -129,11 +129,11 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { if(serveItem && serveItem.url) { const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; - fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; - fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; + fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); } else { - fileEntry.webDlLink = ''; - fileEntry.webDlExpire = ''; + fileEntry.webDlLink = ''; + fileEntry.webDlExpire = ''; } this.updateCustomViewTextsWithFilter( @@ -150,8 +150,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { return cb(Errors.DoesNotExist('Queue view does not exist')); } - const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; - const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; + const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; + const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); @@ -188,8 +188,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { } displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; async.waterfall( [ @@ -210,8 +210,8 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { function prepeareViewController(artData, callback) { if(_.isUndefined(self.viewControllers[name])) { const vcOpts = { - client : self.client, - formId : FormIds[name], + client : self.client, + formId : FormIds[name], }; if(!_.isUndefined(options.noInput)) { @@ -221,9 +221,9 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { const vc = self.addViewController(name, new ViewController(vcOpts)); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds[name], }; return vc.loadFromMenuConfig(loadOpts, callback); diff --git a/core/file_base_filter.js b/core/file_base_filter.js index 9a22051f..82a75986 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -1,13 +1,13 @@ /* jslint node: true */ 'use strict'; -// deps -const _ = require('lodash'); -const uuidV4 = require('uuid/v4'); +// deps +const _ = require('lodash'); +const uuidV4 = require('uuid/v4'); module.exports = class FileBaseFilters { constructor(client) { - this.client = client; + this.client = client; this.load(); } @@ -74,7 +74,7 @@ module.exports = class FileBaseFilters { try { this.filters = JSON.parse(filtersProperty); } catch(e) { - this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :( + this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :( defaulted = true; this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); } @@ -110,18 +110,18 @@ module.exports = class FileBaseFilters { } static getBuiltInSystemFilters() { - const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; + const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; const filters = { [ U_LATEST ] : { - name : 'By Date Added', - areaTag : '', // all - terms : '', // * - tags : '', // * - order : 'descending', - sort : 'upload_timestamp', - uuid : U_LATEST, - system : true, + name : 'By Date Added', + areaTag : '', // all + terms : '', // * + tags : '', // * + order : 'descending', + sort : 'upload_timestamp', + uuid : U_LATEST, + system : true, } }; diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index b66e04db..5c4991f3 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -1,46 +1,46 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const stringFormat = require('./string_format.js'); -const FileEntry = require('./file_entry.js'); -const FileArea = require('./file_base_area.js'); -const Config = require('./config.js').get; -const { Errors } = require('./enig_error.js'); +// ENiGMA½ +const stringFormat = require('./string_format.js'); +const FileEntry = require('./file_entry.js'); +const FileArea = require('./file_base_area.js'); +const Config = require('./config.js').get; +const { Errors } = require('./enig_error.js'); const { splitTextAtTerms, isAnsi, -} = require('./string_util.js'); -const AnsiPrep = require('./ansi_prep.js'); -const Log = require('./logger.js').log; +} = require('./string_util.js'); +const AnsiPrep = require('./ansi_prep.js'); +const Log = require('./logger.js').log; -// deps -const _ = require('lodash'); -const async = require('async'); -const fs = require('graceful-fs'); -const paths = require('path'); -const iconv = require('iconv-lite'); -const moment = require('moment'); +// deps +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const paths = require('path'); +const iconv = require('iconv-lite'); +const moment = require('moment'); -exports.exportFileList = exportFileList; -exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; +exports.exportFileList = exportFileList; +exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; function exportFileList(filterCriteria, options, cb) { - options.templateEncoding = options.templateEncoding || 'utf8'; - options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; - options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; - options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec - options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? + options.templateEncoding = options.templateEncoding || 'utf8'; + options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; + options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; + options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec + options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? if(true === options.escapeDesc) { options.escapeDesc = '\\n'; } const state = { - total : 0, - current : 0, - step : 'preparing', - status : 'Preparing', + total : 0, + current : 0, + step : 'preparing', + status : 'Preparing', }; const updateProgress = _.isFunction(options.progress) ? @@ -50,7 +50,7 @@ function exportFileList(filterCriteria, options, cb) { progCb => { return progCb(null); } - ; + ; async.waterfall( [ @@ -61,8 +61,8 @@ function exportFileList(filterCriteria, options, cb) { } const templateFiles = [ - { name : options.headerTemplate, req : false }, - { name : options.entryTemplate, req : true } + { name : options.headerTemplate, req : false }, + { name : options.entryTemplate, req : true } ]; const config = Config(); @@ -80,19 +80,19 @@ function exportFileList(filterCriteria, options, cb) { return callback(Errors.General(err.message)); } - // decode + ensure DOS style CRLF + // decode + ensure DOS style CRLF templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') ); - // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements + // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements let descIndent = 0; if(!options.escapeDesc) { splitTextAtTerms(templates[1]).some(line => { const pos = line.indexOf('{fileDesc}'); if(pos > -1) { descIndent = pos; - return true; // found it! + return true; // found it! } - return false; // keep looking + return false; // keep looking }); } @@ -101,8 +101,8 @@ function exportFileList(filterCriteria, options, cb) { }); }, function findFiles(headerTemplate, entryTemplate, descIndent, callback) { - state.step = 'gathering'; - state.status = 'Gathering files for supplied criteria'; + state.step = 'gathering'; + state.status = 'Gathering files for supplied criteria'; updateProgress(err => { if(err) { return callback(err); @@ -119,15 +119,15 @@ function exportFileList(filterCriteria, options, cb) { }, function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { const formatObj = { - totalFileCount : fileIds.length, + totalFileCount : fileIds.length, }; - let current = 0; - let listBody = ''; - const totals = { fileCount : fileIds.length, bytes : 0 }; - state.total = fileIds.length; + let current = 0; + let listBody = ''; + const totals = { fileCount : fileIds.length, bytes : 0 }; + state.total = fileIds.length; - state.step = 'file'; + state.step = 'file'; async.eachSeries(fileIds, (fileId, nextFileId) => { const fileInfo = new FileEntry(); @@ -135,7 +135,7 @@ function exportFileList(filterCriteria, options, cb) { fileInfo.load(fileId, err => { if(err) { - return nextFileId(null); // failed, but try the next + return nextFileId(null); // failed, but try the next } totals.bytes += fileInfo.meta.byte_size; @@ -151,9 +151,9 @@ function exportFileList(filterCriteria, options, cb) { listBody += stringFormat(entryTemplate, formatObj); - state.current = current; - state.status = `Processing ${fileInfo.fileName}`; - state.fileInfo = formatObj; + state.current = current; + state.status = `Processing ${fileInfo.fileName}`; + state.fileInfo = formatObj; updateProgress(err => { return nextFileId(err); @@ -162,33 +162,33 @@ function exportFileList(filterCriteria, options, cb) { const area = FileArea.getFileAreaByTag(fileInfo.areaTag); - formatObj.fileId = fileId; - formatObj.areaName = _.get(area, 'name') || 'N/A'; - formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; - formatObj.userRating = fileInfo.userRating || 0; - formatObj.fileName = fileInfo.fileName; - formatObj.fileSize = fileInfo.meta.byte_size; - formatObj.fileDesc = fileInfo.desc || ''; - formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); - formatObj.fileSha256 = fileInfo.fileSha256; - formatObj.fileCrc32 = fileInfo.meta.file_crc32; - formatObj.fileMd5 = fileInfo.meta.file_md5; - formatObj.fileSha1 = fileInfo.meta.file_sha1; - formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; - formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); - formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; - formatObj.currentFile = current; - formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); + formatObj.fileId = fileId; + formatObj.areaName = _.get(area, 'name') || 'N/A'; + formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; + formatObj.userRating = fileInfo.userRating || 0; + formatObj.fileName = fileInfo.fileName; + formatObj.fileSize = fileInfo.meta.byte_size; + formatObj.fileDesc = fileInfo.desc || ''; + formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); + formatObj.fileSha256 = fileInfo.fileSha256; + formatObj.fileCrc32 = fileInfo.meta.file_crc32; + formatObj.fileMd5 = fileInfo.meta.file_md5; + formatObj.fileSha1 = fileInfo.meta.file_sha1; + formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; + formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); + formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; + formatObj.currentFile = current; + formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); if(isAnsi(fileInfo.desc)) { AnsiPrep( fileInfo.desc, { - cols : Math.min(options.descWidth, 79 - descIndent), - forceLineTerm : true, // ensure each line is term'd - asciiMode : true, // export to ASCII - fillLines : false, // don't fill up to |cols| - indent : descIndent, + cols : Math.min(options.descWidth, 79 - descIndent), + forceLineTerm : true, // ensure each line is term'd + asciiMode : true, // export to ASCII + fillLines : false, // don't fill up to |cols| + indent : descIndent, }, (err, desc) => { if(desc) { @@ -208,29 +208,29 @@ function exportFileList(filterCriteria, options, cb) { }); }, function buildHeader(listBody, headerTemplate, totals, callback) { - // header is built last such that we can have totals/etc. + // header is built last such that we can have totals/etc. let filterAreaName; let filterAreaDesc; if(filterCriteria.areaTag) { - const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); - filterAreaName = _.get(area, 'name') || 'N/A'; - filterAreaDesc = _.get(area, 'desc') || 'N/A'; + const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); + filterAreaName = _.get(area, 'name') || 'N/A'; + filterAreaDesc = _.get(area, 'desc') || 'N/A'; } else { - filterAreaName = '-ALL-'; - filterAreaDesc = 'All areas'; + filterAreaName = '-ALL-'; + filterAreaDesc = 'All areas'; } const headerFormatObj = { - nowTs : moment().format(options.tsFormat), - boardName : Config().general.boardName, - totalFileCount : totals.fileCount, - totalFileSize : totals.bytes, - filterAreaTag : filterCriteria.areaTag || '-ALL-', - filterAreaName : filterAreaName, - filterAreaDesc : filterAreaDesc, - filterTerms : filterCriteria.terms || '(none)', - filterHashTags : filterCriteria.tags || '(none)', + nowTs : moment().format(options.tsFormat), + boardName : Config().general.boardName, + totalFileCount : totals.fileCount, + totalFileSize : totals.bytes, + filterAreaTag : filterCriteria.areaTag || '-ALL-', + filterAreaName : filterAreaName, + filterAreaDesc : filterAreaDesc, + filterTerms : filterCriteria.terms || '(none)', + filterHashTags : filterCriteria.tags || '(none)', }; listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; @@ -238,8 +238,8 @@ function exportFileList(filterCriteria, options, cb) { }, function done(listBody, callback) { delete state.fileInfo; - state.step = 'finished'; - state.status = 'Finished processing'; + state.step = 'finished'; + state.status = 'Finished processing'; updateProgress( () => { return callback(null, listBody); }); @@ -252,16 +252,16 @@ function exportFileList(filterCriteria, options, cb) { function updateFileBaseDescFilesScheduledEvent(args, cb) { // - // For each area, loop over storage locations and build - // DESCRIPT.ION file to store in the same directory. + // For each area, loop over storage locations and build + // DESCRIPT.ION file to store in the same directory. // - // Standard-ish 4DOS spec is as such: - // * Entry: [0x04]\r\n - // * Multi line descriptions are stored with *escaped* \r\n pairs - // * Default template uses 0x2c for as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec + // Standard-ish 4DOS spec is as such: + // * Entry: [0x04]\r\n + // * Multi line descriptions are stored with *escaped* \r\n pairs + // * Default template uses 0x2c for as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec // - const entryTemplate = args[0]; - const headerTemplate = args[1]; + const entryTemplate = args[0]; + const headerTemplate = args[1]; const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true }); async.each(areas, (area, nextArea) => { @@ -269,15 +269,15 @@ function updateFileBaseDescFilesScheduledEvent(args, cb) { async.each(storageLocations, (storageLoc, nextStorageLoc) => { const filterCriteria = { - areaTag : area.areaTag, - storageTag : storageLoc.storageTag, + areaTag : area.areaTag, + storageTag : storageLoc.storageTag, }; const exportOpts = { - headerTemplate : headerTemplate, - entryTemplate : entryTemplate, - escapeDesc : true, // escape CRLF's - maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes" + headerTemplate : headerTemplate, + entryTemplate : entryTemplate, + escapeDesc : true, // escape CRLF's + maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes" }; exportFileList(filterCriteria, exportOpts, (err, listBody) => { diff --git a/core/file_base_search.js b/core/file_base_search.js index 3e754b91..168ed39a 100644 --- a/core/file_base_search.js +++ b/core/file_base_search.js @@ -1,30 +1,30 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('./file_base_filter.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('./file_base_filter.js'); -// deps -const async = require('async'); +// deps +const async = require('async'); exports.moduleInfo = { - name : 'File Base Search', - desc : 'Module for quickly searching the file base', - author : 'NuSkooler', + name : 'File Base Search', + desc : 'Module for quickly searching the file base', + author : 'NuSkooler', }; const MciViewIds = { search : { - searchTerms : 1, - search : 2, - tags : 3, - area : 4, - orderBy : 5, - sort : 6, - advSearch : 7, + searchTerms : 1, + search : 2, + tags : 3, + area : 4, + orderBy : 5, + sort : 6, + advSearch : 7, } }; @@ -46,8 +46,8 @@ exports.getModule = class FileBaseSearch extends MenuModule { return cb(err); } - const self = this; - const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) ); + const self = this; + const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) ); async.series( [ @@ -74,7 +74,7 @@ exports.getModule = class FileBaseSearch extends MenuModule { getSelectedAreaTag(index) { if(0 === index) { - return ''; // -ALL- + return ''; // -ALL- } const area = this.availAreas[index]; if(!area) { @@ -92,16 +92,16 @@ exports.getModule = class FileBaseSearch extends MenuModule { } getFilterValuesFromFormData(formData, isAdvanced) { - const areaIndex = isAdvanced ? formData.value.areaIndex : 0; - const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; - const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; + const areaIndex = isAdvanced ? formData.value.areaIndex : 0; + const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; + const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; return { - areaTag : this.getSelectedAreaTag(areaIndex), - terms : formData.value.searchTerms, - tags : isAdvanced ? formData.value.tags : '', - order : this.getOrderBy(orderByIndex), - sort : this.getSortBy(sortByIndex), + areaTag : this.getSelectedAreaTag(areaIndex), + terms : formData.value.searchTerms, + tags : isAdvanced ? formData.value.tags : '', + order : this.getOrderBy(orderByIndex), + sort : this.getSortBy(sortByIndex), }; } @@ -109,10 +109,10 @@ exports.getModule = class FileBaseSearch extends MenuModule { const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced); const menuOpts = { - extraArgs : { - filterCriteria : filterCriteria, + extraArgs : { + filterCriteria : filterCriteria, }, - menuFlags : [ 'popParent' ], + menuFlags : [ 'popParent' ], }; return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); diff --git a/core/file_base_user_list_export.js b/core/file_base_user_list_export.js index 8f691273..3c00d167 100644 --- a/core/file_base_user_list_export.js +++ b/core/file_base_user_list_export.js @@ -1,66 +1,66 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const FileEntry = require('./file_entry.js'); -const FileArea = require('./file_base_area.js'); -const { renderSubstr } = require('./string_util.js'); -const { Errors } = require('./enig_error.js'); -const Events = require('./events.js'); -const Log = require('./logger.js').log; -const DownloadQueue = require('./download_queue.js'); -const { exportFileList } = require('./file_base_list_export.js'); +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const FileEntry = require('./file_entry.js'); +const FileArea = require('./file_base_area.js'); +const { renderSubstr } = require('./string_util.js'); +const { Errors } = require('./enig_error.js'); +const Events = require('./events.js'); +const Log = require('./logger.js').log; +const DownloadQueue = require('./download_queue.js'); +const { exportFileList } = require('./file_base_list_export.js'); -// deps -const _ = require('lodash'); -const async = require('async'); -const fs = require('graceful-fs'); -const fse = require('fs-extra'); -const paths = require('path'); -const moment = require('moment'); -const uuidv4 = require('uuid/v4'); -const yazl = require('yazl'); +// deps +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const fse = require('fs-extra'); +const paths = require('path'); +const moment = require('moment'); +const uuidv4 = require('uuid/v4'); +const yazl = require('yazl'); /* - Module config block can contain the following: - templateEncoding - encoding of template files (utf8) - tsFormat - timestamp format (theme 'short') - descWidth - max desc width (45) - progBarChar - progress bar character (▒) - compressThreshold - threshold to kick in comrpession for lists (1.44 MiB) - templates - object containing: - header - filename of header template (misc/file_list_header.asc) - entry - filename of entry template (misc/file_list_entry.asc) + Module config block can contain the following: + templateEncoding - encoding of template files (utf8) + tsFormat - timestamp format (theme 'short') + descWidth - max desc width (45) + progBarChar - progress bar character (▒) + compressThreshold - threshold to kick in comrpession for lists (1.44 MiB) + templates - object containing: + header - filename of header template (misc/file_list_header.asc) + entry - filename of entry template (misc/file_list_entry.asc) - Header template variables: - nowTs, boardName, totalFileCount, totalFileSize, - filterAreaTag, filterAreaName, filterAreaDesc, - filterTerms, filterHashTags + Header template variables: + nowTs, boardName, totalFileCount, totalFileSize, + filterAreaTag, filterAreaName, filterAreaDesc, + filterTerms, filterHashTags - Entry template variables: - fileId, areaName, areaDesc, userRating, fileName, - fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32, - fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags, - currentFile, progress, + Entry template variables: + fileId, areaName, areaDesc, userRating, fileName, + fileSize, fileDesc, fileDescShort, fileSha256, fileCrc32, + fileMd5, fileSha1, uploadBy, fileUploadTs, fileHashTags, + currentFile, progress, */ exports.moduleInfo = { - name : 'File Base List Export', - desc : 'Exports file base listings for download', - author : 'NuSkooler', + name : 'File Base List Export', + desc : 'Exports file base listings for download', + author : 'NuSkooler', }; const FormIds = { - main : 0, + main : 0, }; const MciViewIds = { main : { - status : 1, - progressBar : 2, + status : 1, + progressBar : 2, - customRangeStart : 10, + customRangeStart : 10, } }; @@ -70,11 +70,11 @@ exports.getModule = class FileBaseListExport extends MenuModule { super(options); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.config.templateEncoding = this.config.templateEncoding || 'utf8'; - this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); - this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ - this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); - this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) + this.config.templateEncoding = this.config.templateEncoding || 'utf8'; + this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); + this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ + this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); + this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) } mciReady(mciData, cb) { @@ -154,7 +154,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { async.waterfall( [ function buildList(callback) { - // this may take quite a while; temp disable of idle monitor + // this may take quite a while; temp disable of idle monitor self.client.stopIdleMonitor(); self.client.on('key press', keyPressHandler); @@ -165,12 +165,12 @@ exports.getModule = class FileBaseListExport extends MenuModule { } const opts = { - templateEncoding : self.config.templateEncoding, - headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), - entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), - tsFormat : self.config.tsFormat, - descWidth : self.config.descWidth, - progress : exportListProgress, + templateEncoding : self.config.templateEncoding, + headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), + entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), + tsFormat : self.config.tsFormat, + descWidth : self.config.descWidth, + progress : exportListProgress, }; exportFileList(filterCriteria, opts, (err, listBody) => { @@ -180,8 +180,8 @@ exports.getModule = class FileBaseListExport extends MenuModule { function persistList(listBody, callback) { updateStatus('Persisting list'); - const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); - const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); + const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); + const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); fse.mkdirs(sysTempDownloadDir, err => { if(err) { @@ -206,14 +206,14 @@ exports.getModule = class FileBaseListExport extends MenuModule { }, function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { const newEntry = new FileEntry({ - areaTag : sysTempDownloadArea.areaTag, - fileName : paths.basename(outputFileName), - storageTag : sysTempDownloadArea.storageTags[0], - meta : { - upload_by_username : self.client.user.username, - upload_by_user_id : self.client.user.userId, - byte_size : fileSize, - session_temp_dl : 1, // download is valid until session is over + areaTag : sysTempDownloadArea.areaTag, + fileName : paths.basename(outputFileName), + storageTag : sysTempDownloadArea.storageTags[0], + meta : { + upload_by_username : self.client.user.username, + upload_by_user_id : self.client.user.userId, + byte_size : fileSize, + session_temp_dl : 1, // download is valid until session is over } }); @@ -221,11 +221,11 @@ exports.getModule = class FileBaseListExport extends MenuModule { newEntry.persist(err => { if(!err) { - // queue it! + // queue it! const dlQueue = new DownloadQueue(self.client); - dlQueue.add(newEntry, true); // true=systemFile + dlQueue.add(newEntry, true); // true=systemFile - // clean up after ourselves when the session ends + // clean up after ourselves when the session ends const thisClientId = self.client.session.id; Events.once(Events.getSystemEvents().ClientDisconnected, evt => { if(thisClientId === _.get(evt, 'client.session.id')) { @@ -243,7 +243,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { }); }, function done(callback) { - // re-enable idle monitor + // re-enable idle monitor self.client.startIdleMonitor(); updateStatus('Exported list has been added to your download queue'); @@ -264,7 +264,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { } if(stats.size < this.config.compressThreshold) { - // small enough, keep orig + // small enough, keep orig return cb(null, filePath, stats.size); } @@ -276,13 +276,13 @@ exports.getModule = class FileBaseListExport extends MenuModule { const outZipFile = fs.createWriteStream(zipFilePath); zipFile.outputStream.pipe(outZipFile); zipFile.outputStream.on('finish', () => { - // delete the original + // delete the original fse.unlink(filePath, err => { if(err) { return cb(err); } - // finally stat the new output + // finally stat the new output fse.stat(zipFilePath, (err, stats) => { return cb(err, zipFilePath, stats ? stats.size : 0); }); diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index 69c87ec8..02bffa3e 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -1,39 +1,39 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const DownloadQueue = require('./download_queue.js'); -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const Errors = require('./enig_error.js').Errors; -const stringFormat = require('./string_format.js'); -const FileAreaWeb = require('./file_area_web.js'); -const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; -const Config = require('./config.js').get; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const DownloadQueue = require('./download_queue.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const Errors = require('./enig_error.js').Errors; +const stringFormat = require('./string_format.js'); +const FileAreaWeb = require('./file_area_web.js'); +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const Config = require('./config.js').get; -// deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +// deps +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { - name : 'File Base Download Web Queue Manager', - desc : 'Module for interacting with web backed download queue/batch', - author : 'NuSkooler', + name : 'File Base Download Web Queue Manager', + desc : 'Module for interacting with web backed download queue/batch', + author : 'NuSkooler', }; const FormIds = { - queueManager : 0 + queueManager : 0 }; const MciViewIds = { queueManager : { - queue : 1, - navMenu : 2, + queue : 1, + navMenu : 2, - customRangeStart : 10, + customRangeStart : 10, } }; @@ -53,13 +53,13 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { this.dlQueue.removeItems(selectedItem.fileId); - // :TODO: broken: does not redraw menu properly - needs fixed! + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); - // :TODO: broken: does not redraw menu properly - needs fixed! + // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView('all', cb); }, getBatchLink : (formData, extraArgs, cb) => { @@ -111,7 +111,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { this.updateCustomViewTextsWithFilter( 'queueManager', MciViewIds.queueManager.customRangeStart, fileEntry, - { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... + { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... ); } @@ -121,8 +121,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { return cb(Errors.DoesNotExist('Queue view does not exist')); } - const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; - const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; + const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; + const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); @@ -148,7 +148,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { expireTime : expireTime }, (err, webBatchDlLink) => { - // :TODO: handle not enabled -> display such + // :TODO: handle not enabled -> display such if(err) { return cb(err); } @@ -156,8 +156,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const formatObj = { - webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, - webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), + webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, + webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), }; this.updateCustomViewTextsWithFilter( @@ -188,7 +188,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { if(err) { if(ErrNotEnabled === err.reasonCode) { - return nextFileEntry(err); // we should have caught this prior + return nextFileEntry(err); // we should have caught this prior } const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); @@ -202,17 +202,17 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { return nextFileEntry(err); } - fileEntry.webDlLinkRaw = url; - fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; - fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); + fileEntry.webDlLinkRaw = url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; + fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); return nextFileEntry(null); } ); } else { - fileEntry.webDlLinkRaw = serveItem.url; - fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; - fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + fileEntry.webDlLinkRaw = serveItem.url; + fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; + fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); return nextFileEntry(null); } }); @@ -233,8 +233,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { } displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; async.waterfall( [ @@ -255,8 +255,8 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { function prepeareViewController(artData, callback) { if(_.isUndefined(self.viewControllers[name])) { const vcOpts = { - client : self.client, - formId : FormIds[name], + client : self.client, + formId : FormIds[name], }; if(!_.isUndefined(options.noInput)) { @@ -266,9 +266,9 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { const vc = self.addViewController(name, new ViewController(vcOpts)); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds[name], }; return vc.loadFromMenuConfig(loadOpts, callback); diff --git a/core/file_entry.js b/core/file_entry.js index 75fafb29..0f519e10 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -1,57 +1,57 @@ /* jslint node: true */ 'use strict'; -const fileDb = require('./database.js').dbs.file; -const Errors = require('./enig_error.js').Errors; +const fileDb = require('./database.js').dbs.file; +const Errors = require('./enig_error.js').Errors; const { getISOTimestampString, sanatizeString -} = require('./database.js'); -const Config = require('./config.js').get; +} = require('./database.js'); +const Config = require('./config.js').get; -// deps -const async = require('async'); -const _ = require('lodash'); -const paths = require('path'); -const fse = require('fs-extra'); -const { unlink, readFile } = require('graceful-fs'); -const crypto = require('crypto'); -const moment = require('moment'); +// deps +const async = require('async'); +const _ = require('lodash'); +const paths = require('path'); +const fse = require('fs-extra'); +const { unlink, readFile } = require('graceful-fs'); +const crypto = require('crypto'); +const moment = require('moment'); -const FILE_TABLE_MEMBERS = [ +const FILE_TABLE_MEMBERS = [ 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', 'desc', 'desc_long', 'upload_timestamp' ]; const FILE_WELL_KNOWN_META = { - // name -> *read* converter, if any - upload_by_username : null, - upload_by_user_id : (u) => parseInt(u) || 0, - file_md5 : null, - file_sha1 : null, - file_crc32 : null, - est_release_year : (y) => parseInt(y) || new Date().getFullYear(), - dl_count : (d) => parseInt(d) || 0, - byte_size : (b) => parseInt(b) || 0, - archive_type : null, - short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import - tic_origin : null, // TIC "Origin" - tic_desc : null, // TIC "Desc" - tic_ldesc : null, // TIC "Ldesc" joined by '\n' - session_temp_dl : (v) => parseInt(v) ? true : false, + // name -> *read* converter, if any + upload_by_username : null, + upload_by_user_id : (u) => parseInt(u) || 0, + file_md5 : null, + file_sha1 : null, + file_crc32 : null, + est_release_year : (y) => parseInt(y) || new Date().getFullYear(), + dl_count : (d) => parseInt(d) || 0, + byte_size : (b) => parseInt(b) || 0, + archive_type : null, + short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import + tic_origin : null, // TIC "Origin" + tic_desc : null, // TIC "Desc" + tic_ldesc : null, // TIC "Ldesc" joined by '\n' + session_temp_dl : (v) => parseInt(v) ? true : false, }; module.exports = class FileEntry { constructor(options) { - options = options || {}; + options = options || {}; - this.fileId = options.fileId || 0; - this.areaTag = options.areaTag || ''; - this.meta = Object.assign( { dl_count : 0 }, options.meta); - this.hashTags = options.hashTags || new Set(); - this.fileName = options.fileName; - this.storageTag = options.storageTag; - this.fileSha256 = options.fileSha256; + this.fileId = options.fileId || 0; + this.areaTag = options.areaTag || ''; + this.meta = Object.assign( { dl_count : 0 }, options.meta); + this.hashTags = options.hashTags || new Set(); + this.fileName = options.fileName; + this.storageTag = options.storageTag; + this.fileSha256 = options.fileSha256; } static loadBasicEntry(fileId, dest, cb) { @@ -59,9 +59,9 @@ module.exports = class FileEntry { fileDb.get( `SELECT ${FILE_TABLE_MEMBERS.join(', ')} - FROM file - WHERE file_id=? - LIMIT 1;`, + FROM file + WHERE file_id=? + LIMIT 1;`, [ fileId ], (err, file) => { if(err) { @@ -72,7 +72,7 @@ module.exports = class FileEntry { return cb(Errors.DoesNotExist('No file is available by that ID')); } - // assign props from |file| + // assign props from |file| FILE_TABLE_MEMBERS.forEach(prop => { dest[_.camelCase(prop)] = file[prop]; }); @@ -149,7 +149,7 @@ module.exports = class FileEntry { if(isUpdate) { trans.run( `REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) - VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, + VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, [ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], err => { return callback(err, trans); @@ -158,9 +158,9 @@ module.exports = class FileEntry { } else { trans.run( `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) - VALUES(?, ?, ?, ?, ?, ?, ?);`, + VALUES(?, ?, ?, ?, ?, ?, ?);`, [ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], - function inserted(err) { // use non-arrow func for 'this' scope / lastID + function inserted(err) { // use non-arrow func for 'this' scope / lastID if(!err) { self.fileId = this.lastID; } @@ -189,7 +189,7 @@ module.exports = class FileEntry { } ], (err, trans) => { - // :TODO: Log orig err + // :TODO: Log orig err if(trans) { trans[err ? 'rollback' : 'commit'](transErr => { return cb(transErr ? transErr : err); @@ -205,12 +205,12 @@ module.exports = class FileEntry { const config = Config(); const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); - // absolute paths as-is + // absolute paths as-is if(storageLocation && '/' === storageLocation.charAt(0)) { return storageLocation; } - // relative to |areaStoragePrefix| + // relative to |areaStoragePrefix| return paths.join(config.fileBase.areaStoragePrefix, storageLocation || ''); } @@ -222,9 +222,9 @@ module.exports = class FileEntry { static quickCheckExistsByPath(fullPath, cb) { fileDb.get( `SELECT COUNT() AS count - FROM file - WHERE file_name = ? - LIMIT 1;`, + FROM file + WHERE file_name = ? + LIMIT 1;`, [ paths.basename(fullPath) ], (err, rows) => { return err ? cb(err) : cb(null, rows.count > 0 ? true : false); @@ -235,7 +235,7 @@ module.exports = class FileEntry { static persistUserRating(fileId, userId, rating, cb) { return fileDb.run( `REPLACE INTO file_user_rating (file_id, user_id, rating) - VALUES (?, ?, ?);`, + VALUES (?, ?, ?);`, [ fileId, userId, rating ], cb ); @@ -249,7 +249,7 @@ module.exports = class FileEntry { return transOrDb.run( `REPLACE INTO file_meta (file_id, meta_name, meta_value) - VALUES (?, ?, ?);`, + VALUES (?, ?, ?);`, [ fileId, name, value ], cb ); @@ -259,8 +259,8 @@ module.exports = class FileEntry { incrementBy = incrementBy || 1; fileDb.run( `UPDATE file_meta - SET meta_value = meta_value + ? - WHERE file_id = ? AND meta_name = ?;`, + SET meta_value = meta_value + ? + WHERE file_id = ? AND meta_name = ?;`, [ incrementBy, fileId, name ], err => { if(cb) { @@ -273,8 +273,8 @@ module.exports = class FileEntry { loadMeta(cb) { fileDb.each( `SELECT meta_name, meta_value - FROM file_meta - WHERE file_id=?;`, + FROM file_meta + WHERE file_id=?;`, [ this.fileId ], (err, meta) => { if(meta) { @@ -297,18 +297,18 @@ module.exports = class FileEntry { transOrDb.serialize( () => { transOrDb.run( `INSERT OR IGNORE INTO hash_tag (hash_tag) - VALUES (?);`, + VALUES (?);`, [ hashTag ] ); transOrDb.run( `REPLACE INTO file_hash_tag (hash_tag_id, file_id) - VALUES ( - (SELECT hash_tag_id - FROM hash_tag - WHERE hash_tag = ?), - ? - );`, + VALUES ( + (SELECT hash_tag_id + FROM hash_tag + WHERE hash_tag = ?), + ? + );`, [ hashTag, fileId ], err => { return cb(err); @@ -320,12 +320,12 @@ module.exports = class FileEntry { loadHashTags(cb) { fileDb.each( `SELECT ht.hash_tag_id, ht.hash_tag - FROM hash_tag ht - WHERE ht.hash_tag_id IN ( - SELECT hash_tag_id - FROM file_hash_tag - WHERE file_id=? - );`, + FROM hash_tag ht + WHERE ht.hash_tag_id IN ( + SELECT hash_tag_id + FROM file_hash_tag + WHERE file_id=? + );`, [ this.fileId ], (err, hashTag) => { if(hashTag) { @@ -341,10 +341,10 @@ module.exports = class FileEntry { loadRating(cb) { fileDb.get( `SELECT AVG(fur.rating) AS avg_rating - FROM file_user_rating fur - INNER JOIN file f - ON f.file_id = fur.file_id - AND f.file_id = ?`, + FROM file_user_rating fur + INNER JOIN file f + ON f.file_id = fur.file_id + AND f.file_id = ?`, [ this.fileId ], (err, result) => { if(result) { @@ -370,12 +370,12 @@ module.exports = class FileEntry { } static findFileBySha(sha, cb) { - // full or partial SHA-256 + // full or partial SHA-256 fileDb.all( `SELECT file_id - FROM file - WHERE file_sha256 LIKE "${sha}%" - LIMIT 2;`, // limit 2 such that we can find if there are dupes + FROM file + WHERE file_sha256 LIKE "${sha}%" + LIMIT 2;`, // limit 2 such that we can find if there are dupes (err, fileIdRows) => { if(err) { return cb(err); @@ -398,14 +398,14 @@ module.exports = class FileEntry { } static findByFileNameWildcard(wc, cb) { - // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html + // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html wc = wc.replace(/\*/g, '%').replace(/\?/g, '_'); fileDb.all( `SELECT file_id - FROM file - WHERE file_name LIKE "${wc}" - `, + FROM file + WHERE file_name LIKE "${wc}" + `, (err, fileIdRows) => { if(err) { return cb(err); @@ -462,38 +462,38 @@ module.exports = class FileEntry { } if(filter.sort && filter.sort.length > 0) { - if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? + if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? sql = - `SELECT DISTINCT f.file_id - FROM file f, file_meta m`; + `SELECT DISTINCT f.file_id + FROM file f, file_meta m`; appendWhereClause(`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`); sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`; } else { - // additional special treatment for user ratings: we need to average them + // additional special treatment for user ratings: we need to average them if('user_rating' === filter.sort) { sql = - `SELECT DISTINCT f.file_id, - (SELECT IFNULL(AVG(rating), 0) rating - FROM file_user_rating - WHERE file_id = f.file_id) - AS avg_rating - FROM file f`; + `SELECT DISTINCT f.file_id, + (SELECT IFNULL(AVG(rating), 0) rating + FROM file_user_rating + WHERE file_id = f.file_id) + AS avg_rating + FROM file f`; sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; } else { sql = - `SELECT DISTINCT f.file_id - FROM file f`; + `SELECT DISTINCT f.file_id + FROM file f`; sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; } } } else { sql = - `SELECT DISTINCT f.file_id - FROM file f`; + `SELECT DISTINCT f.file_id + FROM file f`; sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; } @@ -511,22 +511,22 @@ module.exports = class FileEntry { filter.metaPairs.forEach(mp => { if(mp.wildcards) { - // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html + // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); appendWhereClause( `f.file_id IN ( - SELECT file_id - FROM file_meta - WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}" - )` + SELECT file_id + FROM file_meta + WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}" + )` ); } else { appendWhereClause( `f.file_id IN ( - SELECT file_id - FROM file_meta - WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}" - )` + SELECT file_id + FROM file_meta + WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}" + )` ); } }); @@ -537,30 +537,30 @@ module.exports = class FileEntry { } if(filter.terms && filter.terms.length > 0) { - // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex appendWhereClause( `f.file_id IN ( - SELECT rowid - FROM file_fts - WHERE file_fts MATCH ":${sanatizeString(filter.terms)}" - )` + SELECT rowid + FROM file_fts + WHERE file_fts MATCH ":${sanatizeString(filter.terms)}" + )` ); } if(filter.tags && filter.tags.length > 0) { - // build list of quoted tags; filter.tags comes in as a space and/or comma separated values + // build list of quoted tags; filter.tags comes in as a space and/or comma separated values const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanatizeString(tag)}"` ).join(','); appendWhereClause( `f.file_id IN ( - SELECT file_id - FROM file_hash_tag - WHERE hash_tag_id IN ( - SELECT hash_tag_id - FROM hash_tag - WHERE hash_tag IN (${tags}) - ) - )` + SELECT file_id + FROM file_hash_tag + WHERE hash_tag_id IN ( + SELECT hash_tag_id + FROM hash_tag + WHERE hash_tag IN (${tags}) + ) + )` ); } @@ -585,7 +585,7 @@ module.exports = class FileEntry { return cb(err); } if(!rows || 0 === rows.length) { - return cb(null, []); // no matches + return cb(null, []); // no matches } return cb(null, rows.map(r => r.file_id)); }); @@ -602,7 +602,7 @@ module.exports = class FileEntry { function removeFromDatabase(callback) { fileDb.run( `DELETE FROM file - WHERE file_id = ?;`, + WHERE file_id = ?;`, [ srcFileEntry.fileId ], err => { return callback(err); @@ -631,20 +631,20 @@ module.exports = class FileEntry { destFileName = srcFileEntry.fileName; } - const srcPath = srcFileEntry.filePath; - const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); + const srcPath = srcFileEntry.filePath; + const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); if(!dstDir) { return cb(Errors.Invalid('Invalid storage tag')); } - const dstPath = paths.join(dstDir, destFileName); + const dstPath = paths.join(dstDir, destFileName); async.series( [ function movePhysFile(callback) { if(srcPath === dstPath) { - return callback(null); // don't need to move file, but may change areas + return callback(null); // don't need to move file, but may change areas } fse.move(srcPath, dstPath, err => { @@ -654,8 +654,8 @@ module.exports = class FileEntry { function updateDatabase(callback) { fileDb.run( `UPDATE file - SET area_tag = ?, file_name = ?, storage_tag = ? - WHERE file_id = ?;`, + SET area_tag = ?, file_name = ?, storage_tag = ? + WHERE file_id = ?;`, [ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ], err => { return callback(err); diff --git a/core/file_transfer.js b/core/file_transfer.js index e66a98f7..dbd19a8f 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -1,50 +1,50 @@ /* jslint node: true */ 'use strict'; -// enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const Config = require('./config.js').get; -const stringFormat = require('./string_format.js'); -const Errors = require('./enig_error.js').Errors; -const DownloadQueue = require('./download_queue.js'); -const StatLog = require('./stat_log.js'); -const FileEntry = require('./file_entry.js'); -const Log = require('./logger.js').log; -const Events = require('./events.js'); +// enigma-bbs +const MenuModule = require('./menu_module.js').MenuModule; +const Config = require('./config.js').get; +const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; +const DownloadQueue = require('./download_queue.js'); +const StatLog = require('./stat_log.js'); +const FileEntry = require('./file_entry.js'); +const Log = require('./logger.js').log; +const Events = require('./events.js'); -// deps -const async = require('async'); -const _ = require('lodash'); -const pty = require('node-pty'); -const temptmp = require('temptmp').createTrackedSession('transfer_file'); -const paths = require('path'); -const fs = require('graceful-fs'); -const fse = require('fs-extra'); +// deps +const async = require('async'); +const _ = require('lodash'); +const pty = require('node-pty'); +const temptmp = require('temptmp').createTrackedSession('transfer_file'); +const paths = require('path'); +const fs = require('graceful-fs'); +const fse = require('fs-extra'); -// some consts -const SYSTEM_EOL = require('os').EOL; -const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. +// some consts +const SYSTEM_EOL = require('os').EOL; +const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. /* - Notes - ----------------------------------------------------------------------------- + Notes + ----------------------------------------------------------------------------- - See core/config.js for external protocol configuration + See core/config.js for external protocol configuration - Resources - ----------------------------------------------------------------------------- + Resources + ----------------------------------------------------------------------------- - ZModem - * http://gallium.inria.fr/~doligez/zmodem/zmodem.txt - * https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c + ZModem + * http://gallium.inria.fr/~doligez/zmodem/zmodem.txt + * https://github.com/protomouse/synchronet/blob/master/src/sbbs3/zmodem.c */ exports.moduleInfo = { - name : 'Transfer file', - desc : 'Sends or receives a file(s)', - author : 'NuSkooler', + name : 'Transfer file', + desc : 'Sends or receives a file(s)', + author : 'NuSkooler', }; exports.getModule = class TransferFileModule extends MenuModule { @@ -54,7 +54,7 @@ exports.getModule = class TransferFileModule extends MenuModule { this.config = this.menuConfig.config || {}; // - // Most options can be set via extraArgs or config block + // Most options can be set via extraArgs or config block // const config = Config(); if(options.extraArgs) { @@ -99,11 +99,11 @@ exports.getModule = class TransferFileModule extends MenuModule { } } - this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* - this.direction = this.direction || 'send'; - this.sendQueue = this.sendQueue || []; + this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* + this.direction = this.direction || 'send'; + this.sendQueue = this.sendQueue || []; - // Ensure sendQueue is an array of objects that contain at least a 'path' member + // Ensure sendQueue is an array of objects that contain at least a 'path' member this.sendQueue = this.sendQueue.map(item => { if(_.isString(item)) { return { path : item }; @@ -128,8 +128,8 @@ exports.getModule = class TransferFileModule extends MenuModule { } sendFiles(cb) { - // assume *sending* can always batch - // :TODO: Look into this further + // assume *sending* can always batch + // :TODO: Look into this further const allFiles = this.sendQueue.map(f => f.path); this.executeExternalProtocolHandlerForSend(allFiles, err => { if(err) { @@ -149,64 +149,64 @@ exports.getModule = class TransferFileModule extends MenuModule { } /* - sendFiles(cb) { - // :TODO: built in/native protocol support + sendFiles(cb) { + // :TODO: built in/native protocol support - if(this.protocolConfig.external.supportsBatch) { - const allFiles = this.sendQueue.map(f => f.path); - this.executeExternalProtocolHandlerForSend(allFiles, err => { - if(err) { - this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); - } else { - const sentFiles = []; - this.sendQueue.forEach(f => { - f.sent = true; - sentFiles.push(f.path); + if(this.protocolConfig.external.supportsBatch) { + const allFiles = this.sendQueue.map(f => f.path); + this.executeExternalProtocolHandlerForSend(allFiles, err => { + if(err) { + this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); + } else { + const sentFiles = []; + this.sendQueue.forEach(f => { + f.sent = true; + sentFiles.push(f.path); - }); + }); - this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); - } - return cb(err); - }); - } else { - // :TODO: we need to prompt between entries such that users can prepare their clients - async.eachSeries(this.sendQueue, (queueItem, next) => { - this.executeExternalProtocolHandlerForSend(queueItem.path, err => { - if(err) { - this.client.log.warn( { file : queueItem.path, error : err.message }, 'Error sending file' ); - } else { - queueItem.sent = true; + this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); + } + return cb(err); + }); + } else { + // :TODO: we need to prompt between entries such that users can prepare their clients + async.eachSeries(this.sendQueue, (queueItem, next) => { + this.executeExternalProtocolHandlerForSend(queueItem.path, err => { + if(err) { + this.client.log.warn( { file : queueItem.path, error : err.message }, 'Error sending file' ); + } else { + queueItem.sent = true; - this.client.log.info( { sentFile : queueItem.path }, 'Successfully sent file' ); - } - return next(err); - }); - }, err => { - return cb(err); - }); - } - } - */ + this.client.log.info( { sentFile : queueItem.path }, 'Successfully sent file' ); + } + return next(err); + }); + }, err => { + return cb(err); + }); + } + } + */ moveFileWithCollisionHandling(src, dst, cb) { // - // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. - // in the case of collisions. + // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. + // in the case of collisions. // - const dstPath = paths.dirname(dst); - const dstFileExt = paths.extname(dst); - const dstFileSuffix = paths.basename(dst, dstFileExt); + const dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); + const dstFileSuffix = paths.basename(dst, dstFileExt); - let renameIndex = 0; - let movedOk = false; + let renameIndex = 0; + let movedOk = false; let tryDstPath; async.until( - () => movedOk, // until moved OK + () => movedOk, // until moved OK (cb) => { if(0 === renameIndex) { - // try originally supplied path first + // try originally supplied path first tryDstPath = dst; } else { tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); @@ -216,7 +216,7 @@ exports.getModule = class TransferFileModule extends MenuModule { if(err) { if('EEXIST' === err.code) { renameIndex += 1; - return cb(null); // keep trying + return cb(null); // keep trying } return cb(err); @@ -242,8 +242,8 @@ exports.getModule = class TransferFileModule extends MenuModule { if(this.recvFileName) { // - // file name specified - we expect a single file in |this.recvDirectory| - // by the name of |this.recvFileName| + // file name specified - we expect a single file in |this.recvDirectory| + // by the name of |this.recvFileName| // const recvFullPath = paths.join(this.recvDirectory, this.recvFileName); fs.stat(recvFullPath, (err, stats) => { @@ -260,21 +260,21 @@ exports.getModule = class TransferFileModule extends MenuModule { }); } else { // - // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already + // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already // fs.readdir(this.recvDirectory, (err, files) => { if(err) { return cb(err); } - // stat each to grab files only + // stat each to grab files only async.each(files, (fileName, nextFile) => { const recvFullPath = paths.join(this.recvDirectory, fileName); fs.stat(recvFullPath, (err, stats) => { if(err) { this.client.log.warn('Failed to stat file', { path : recvFullPath } ); - return nextFile(null); // just try the next one + return nextFile(null); // just try the next one } if(stats.isFile()) { @@ -299,7 +299,7 @@ exports.getModule = class TransferFileModule extends MenuModule { } prepAndBuildSendArgs(filePaths, cb) { - const externalArgs = this.protocolConfig.external['sendArgs']; + const externalArgs = this.protocolConfig.external['sendArgs']; async.waterfall( [ @@ -311,7 +311,7 @@ exports.getModule = class TransferFileModule extends MenuModule { temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { if(err) { - return callback(err); // failed to create it + return callback(err); // failed to create it } fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); @@ -321,16 +321,16 @@ exports.getModule = class TransferFileModule extends MenuModule { }); }, function createArgs(tempFileListPath, callback) { - // initial args: ignore {filePaths} as we must break that into it's own sep array items + // initial args: ignore {filePaths} as we must break that into it's own sep array items const args = externalArgs.map(arg => { return '{filePaths}' === arg ? arg : stringFormat(arg, { - fileListPath : tempFileListPath || '', + fileListPath : tempFileListPath || '', }); }); const filePathsPos = args.indexOf('{filePaths}'); if(filePathsPos > -1) { - // replace {filePaths} with 0:n individual entries in |args| + // replace {filePaths} with 0:n individual entries in |args| args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); } @@ -344,19 +344,19 @@ exports.getModule = class TransferFileModule extends MenuModule { } prepAndBuildRecvArgs(cb) { - const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; - const externalArgs = this.protocolConfig.external[argsKey]; - const args = externalArgs.map(arg => stringFormat(arg, { - uploadDir : this.recvDirectory, - fileName : this.recvFileName || '', + const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; + const externalArgs = this.protocolConfig.external[argsKey]; + const args = externalArgs.map(arg => stringFormat(arg, { + uploadDir : this.recvDirectory, + fileName : this.recvFileName || '', })); return cb(null, args); } executeExternalProtocolHandler(args, cb) { - const external = this.protocolConfig.external; - const cmd = external[`${this.direction}Cmd`]; + const external = this.protocolConfig.external; + const cmd = external[`${this.direction}Cmd`]; this.client.log.debug( { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, @@ -364,18 +364,18 @@ exports.getModule = class TransferFileModule extends MenuModule { ); const spawnOpts = { - cols : this.client.term.termWidth, - rows : this.client.term.termHeight, - cwd : this.recvDirectory, - encoding : null, // don't bork our data! + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, + cwd : this.recvDirectory, + encoding : null, // don't bork our data! }; const externalProc = pty.spawn(cmd, args, spawnOpts); this.client.setTemporaryDirectDataHandler(data => { - // needed for things like sz/rz + // needed for things like sz/rz if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape + const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape externalProc.write(Buffer.from(tmp, 'binary')); } else { externalProc.write(data); @@ -383,9 +383,9 @@ exports.getModule = class TransferFileModule extends MenuModule { }); externalProc.on('data', data => { - // needed for things like sz/rz + // needed for things like sz/rz if(external.escapeTelnet) { - const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape + const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape this.client.term.rawWrite(Buffer.from(tmp, 'binary')); } else { this.client.term.rawWrite(data); @@ -443,9 +443,9 @@ exports.getModule = class TransferFileModule extends MenuModule { } updateSendStats(cb) { - let downloadBytes = 0; - let downloadCount = 0; - let fileIds = []; + let downloadBytes = 0; + let downloadCount = 0; + let fileIds = []; async.each(this.sendQueue, (queueItem, next) => { if(!queueItem.sent) { @@ -462,7 +462,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return next(null); } - // we just have a path - figure it out + // we just have a path - figure it out fs.stat(queueItem.path, (err, stats) => { if(err) { this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); @@ -474,7 +474,7 @@ exports.getModule = class TransferFileModule extends MenuModule { return next(null); }); }, () => { - // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks + // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount); StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes); StatLog.incrementSystemStat('dl_total_count', downloadCount); @@ -489,16 +489,16 @@ exports.getModule = class TransferFileModule extends MenuModule { } updateRecvStats(cb) { - let uploadBytes = 0; - let uploadCount = 0; + let uploadBytes = 0; + let uploadCount = 0; async.each(this.recvFilePaths, (filePath, next) => { - // we just have a path - figure it out + // we just have a path - figure it out fs.stat(filePath, (err, stats) => { if(err) { this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); } else { - uploadCount += 1; + uploadCount += 1; uploadBytes += stats.size; } @@ -517,7 +517,7 @@ exports.getModule = class TransferFileModule extends MenuModule { initSequence() { const self = this; - // :TODO: break this up to send|recv + // :TODO: break this up to send|recv async.series( [ @@ -545,16 +545,16 @@ exports.getModule = class TransferFileModule extends MenuModule { }); if(sentFileIds.length > 0) { - // remove items we sent from the D/L queue + // remove items we sent from the D/L queue const dlQueue = new DownloadQueue(self.client); const dlFileEntries = dlQueue.removeItems(sentFileIds); - // fire event for downloaded entries + // fire event for downloaded entries Events.emit( Events.getSystemEvents().UserDownload, { - user : self.client.user, - files : dlFileEntries + user : self.client.user, + files : dlFileEntries } ); diff --git a/core/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js index 1fe1944b..d8500dc5 100644 --- a/core/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -1,24 +1,24 @@ /* jslint node: true */ 'use strict'; -// enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const Config = require('./config.js').get; -const stringFormat = require('./string_format.js'); -const ViewController = require('./view_controller.js').ViewController; +// enigma-bbs +const MenuModule = require('./menu_module.js').MenuModule; +const Config = require('./config.js').get; +const stringFormat = require('./string_format.js'); +const ViewController = require('./view_controller.js').ViewController; -// deps -const async = require('async'); -const _ = require('lodash'); +// deps +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'File transfer protocol selection', - desc : 'Select protocol / method for file transfer', - author : 'NuSkooler', + name : 'File transfer protocol selection', + desc : 'Select protocol / method for file transfer', + author : 'NuSkooler', }; const MciViewIds = { - protList : 1, + protList : 1, }; exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { @@ -36,7 +36,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { this.config.direction = this.config.direction || 'send'; - this.extraArgs = options.extraArgs; + this.extraArgs = options.extraArgs; if(_.has(options, 'lastMenuResult.sentFileIds')) { this.sentFileIds = options.lastMenuResult.sentFileIds; @@ -46,13 +46,13 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { this.recvFilePaths = options.lastMenuResult.recvFilePaths; } - this.fallbackOnly = options.lastMenuResult ? true : false; + this.fallbackOnly = options.lastMenuResult ? true : false; this.loadAvailProtocols(); this.menuMethods = { selectProtocol : (formData, extraArgs, cb) => { - const protocol = this.protocols[formData.value.protocol]; + const protocol = this.protocols[formData.value.protocol]; const finalExtraArgs = this.extraArgs || {}; Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs ); @@ -81,7 +81,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { initSequence() { if(this.sentFileIds || this.recvFilePaths) { - // nothing to do here; move along (we're just falling through) + // nothing to do here; move along (we're just falling through) this.prevMenu(); } else { super.initSequence(); @@ -94,15 +94,15 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); async.series( [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu + callingMenu : self, + mciMap : mciData.menu }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -110,8 +110,8 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { function populateList(callback) { const protListView = vc.getView(MciViewIds.protList); - const protListFormat = self.config.protListFormat || '{name}'; - const protListFocusFormat = self.config.protListFocusFormat || protListFormat; + const protListFormat = self.config.protListFormat || '{name}'; + const protListFocusFormat = self.config.protListFocusFormat || protListFormat; protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) ); protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) ); @@ -131,22 +131,22 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { loadAvailProtocols() { this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { return { - protocol : protocol, - name : protInfo.name, - hasBatch : _.has(protInfo, 'external.recvArgs'), - hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), - sort : protInfo.sort, + protocol : protocol, + name : protInfo.name, + hasBatch : _.has(protInfo, 'external.recvArgs'), + hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), + sort : protInfo.sort, }; }); - // Filter out batch vs non-batch only protocols - if(this.extraArgs.recvFileName) { // non-batch aka non-blind + // Filter out batch vs non-batch only protocols + if(this.extraArgs.recvFileName) { // non-batch aka non-blind this.protocols = this.protocols.filter( prot => prot.hasNonBatch ); } else { this.protocols = this.protocols.filter( prot => prot.hasBatch ); } - // natural sort taking explicit orders into consideration + // natural sort taking explicit orders into consideration this.protocols.sort( (a, b) => { if(_.isNumber(a.sort) && _.isNumber(b.sort)) { return a.sort - b.sort; diff --git a/core/file_util.js b/core/file_util.js index 428622da..64167771 100644 --- a/core/file_util.js +++ b/core/file_util.js @@ -1,28 +1,28 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const EnigAssert = require('./enigma_assert.js'); +// ENiGMA½ +const EnigAssert = require('./enigma_assert.js'); -// deps -const fse = require('fs-extra'); -const paths = require('path'); -const async = require('async'); +// deps +const fse = require('fs-extra'); +const paths = require('path'); +const async = require('async'); -exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling; -exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling; -exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; +exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling; +exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling; +exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { - operation = operation || 'copy'; - const dstPath = paths.dirname(dst); - const dstFileExt = paths.extname(dst); - const dstFileSuffix = paths.basename(dst, dstFileExt); + operation = operation || 'copy'; + const dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); + const dstFileSuffix = paths.basename(dst, dstFileExt); EnigAssert('move' === operation || 'copy' === operation); - let renameIndex = 0; - let opOk = false; + let renameIndex = 0; + let opOk = false; let tryDstPath; function tryOperation(src, dst, callback) { @@ -38,10 +38,10 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { } async.until( - () => opOk, // until moved OK + () => opOk, // until moved OK (cb) => { if(0 === renameIndex) { - // try originally supplied path first + // try originally supplied path first tryDstPath = dst; } else { tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); @@ -49,11 +49,11 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { tryOperation(src, tryDstPath, err => { if(err) { - // for some reason fs-extra copy doesn't pass err.code - // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST + // for some reason fs-extra copy doesn't pass err.code + // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST if('EEXIST' === err.code || 'copy' === operation) { renameIndex += 1; - return cb(null); // keep trying + return cb(null); // keep trying } return cb(err); @@ -70,8 +70,8 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { } // -// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. -// in the case of collisions. +// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. +// in the case of collisions. // function moveFileWithCollisionHandling(src, dst, cb) { return moveOrCopyFileWithCollisionHandling(src, dst, 'move', cb); diff --git a/core/fnv1a.js b/core/fnv1a.js index 53400a66..9acc8f27 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -1,9 +1,9 @@ /* jslint node: true */ 'use strict'; -let _ = require('lodash'); +let _ = require('lodash'); -// FNV-1a based on work here: https://github.com/wiedi/node-fnv +// FNV-1a based on work here: https://github.com/wiedi/node-fnv module.exports = class FNV1a { constructor(data) { this.hash = 0x811c9dc5; @@ -29,8 +29,8 @@ module.exports = class FNV1a { for(let b of data) { this.hash = this.hash ^ b; this.hash += - (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + - (this.hash << 4) + (this.hash << 1); + (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + + (this.hash << 4) + (this.hash << 1); } return this; diff --git a/core/fse.js b/core/fse.js index 6dea6a1b..7d3565ad 100644 --- a/core/fse.js +++ b/core/fse.js @@ -1,118 +1,118 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const Message = require('./message.js'); -const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; -const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; -const User = require('./user.js'); -const StatLog = require('./stat_log.js'); -const stringFormat = require('./string_format.js'); -const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; -const { isAnsi, cleanControlCodes, insert } = require('./string_util.js'); -const Config = require('./config.js').get; -const { getAddressedToInfo } = require('./mail_util.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const Message = require('./message.js'); +const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; +const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; +const User = require('./user.js'); +const StatLog = require('./stat_log.js'); +const stringFormat = require('./string_format.js'); +const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; +const { isAnsi, cleanControlCodes, insert } = require('./string_util.js'); +const Config = require('./config.js').get; +const { getAddressedToInfo } = require('./mail_util.js'); -// deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const moment = require('moment'); +// deps +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { - name : 'Full Screen Editor (FSE)', - desc : 'A full screen editor/viewer', - author : 'NuSkooler', + name : 'Full Screen Editor (FSE)', + desc : 'A full screen editor/viewer', + author : 'NuSkooler', }; const MciViewIds = { header : { - from : 1, - to : 2, - subject : 3, - errorMsg : 4, - modTimestamp : 5, - msgNum : 6, - msgTotal : 7, + from : 1, + to : 2, + subject : 3, + errorMsg : 4, + modTimestamp : 5, + msgNum : 6, + msgTotal : 7, - customRangeStart : 10, // 10+ = customs + customRangeStart : 10, // 10+ = customs }, body : { - message : 1, + message : 1, }, - // :TODO: quote builder MCIs - remove all magic #'s + // :TODO: quote builder MCIs - remove all magic #'s - // :TODO: consolidate all footer MCI's - remove all magic #'s + // :TODO: consolidate all footer MCI's - remove all magic #'s ViewModeFooter : { - MsgNum : 6, - MsgTotal : 7, - // :TODO: Just use custom ranges + MsgNum : 6, + MsgTotal : 7, + // :TODO: Just use custom ranges }, quoteBuilder : { - quotedMsg : 1, - // 2 NYI - quoteLines : 3, + quotedMsg : 1, + // 2 NYI + quoteLines : 3, } }; /* - Custom formatting: - header - fromUserName - toUserName + Custom formatting: + header + fromUserName + toUserName - fromRealName (may be fromUserName) NYI - toRealName (may be toUserName) NYI + fromRealName (may be fromUserName) NYI + toRealName (may be toUserName) NYI - fromRemoteUser (may be "N/A") - toRemoteUser (may be "N/A") - subject - modTimestamp - msgNum - msgTotal (in area) - messageId + fromRemoteUser (may be "N/A") + toRemoteUser (may be "N/A") + subject + modTimestamp + msgNum + msgTotal (in area) + messageId */ -// :TODO: convert code in this class to newer styles, conventions, etc. There is a lot of experimental stuff here that has better (DRY) alternatives +// :TODO: convert code in this class to newer styles, conventions, etc. There is a lot of experimental stuff here that has better (DRY) alternatives exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModule extends MessageAreaConfTempSwitcher(MenuModule) { constructor(options) { super(options); - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; // - // menuConfig.config: - // editorType : email | area - // editorMode : view | edit | quote + // menuConfig.config: + // editorType : email | area + // editorMode : view | edit | quote // - // menuConfig.config or extraArgs - // messageAreaTag - // messageIndex / messageTotal - // toUserId + // menuConfig.config or extraArgs + // messageAreaTag + // messageIndex / messageTotal + // toUserId // - this.editorType = config.editorType; - this.editorMode = config.editorMode; + this.editorType = config.editorType; + this.editorMode = config.editorMode; if(config.messageAreaTag) { - // :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs - this.messageAreaTag = config.messageAreaTag; + // :TODO: swtich to this.config.messageAreaTag so we can follow Object.assign pattern for config/extraArgs + this.messageAreaTag = config.messageAreaTag; } - this.messageIndex = config.messageIndex || 0; - this.messageTotal = config.messageTotal || 0; - this.toUserId = config.toUserId || 0; + this.messageIndex = config.messageIndex || 0; + this.messageTotal = config.messageTotal || 0; + this.toUserId = config.toUserId || 0; - // extraArgs can override some config + // extraArgs can override some config if(_.isObject(options.extraArgs)) { if(options.extraArgs.messageAreaTag) { this.messageAreaTag = options.extraArgs.messageAreaTag; @@ -140,7 +140,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.menuMethods = { // - // Validation stuff + // Validation stuff // viewValidationListener : function(err, cb) { var errMsgView = self.viewControllers.header.getView(MciViewIds.header.errorMsg); @@ -150,7 +150,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul errMsgView.setText(err.message); if(MciViewIds.header.subject === err.view.getId()) { - // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) + // :TODO: for "area" mode, should probably just bail if this is emtpy (e.g. cancel) } } else { errMsgView.clearText(); @@ -201,19 +201,19 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(self.newQuoteBlock) { self.newQuoteBlock = false; - // :TODO: If replying to ANSI, add a blank sepration line here + // :TODO: If replying to ANSI, add a blank sepration line here quoteMsgView.addText(self.getQuoteByHeader()); } const quoteListView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); - const quoteText = quoteListView.getItem(formData.value.quote); + const quoteText = quoteListView.getItem(formData.value.quote); quoteMsgView.addText(quoteText); // - // If this is *not* the last item, advance. Otherwise, do nothing as we - // don't want to jump back to the top and repeat already quoted lines + // If this is *not* the last item, advance. Otherwise, do nothing as we + // don't want to jump back to the top and repeat already quoted lines // if(quoteListView.getData() !== quoteListView.getCount() - 1) { @@ -229,18 +229,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return cb(null); }, /* - replyDiscard : function(formData, extraArgs) { - // :TODO: need to prompt yes/no - // :TODO: @method for fallback would be better - self.prevMenu(); - }, - */ + replyDiscard : function(formData, extraArgs) { + // :TODO: need to prompt yes/no + // :TODO: @method for fallback would be better + self.prevMenu(); + }, + */ editModeMenuHelp : function(formData, extraArgs, cb) { self.viewControllers.footerEditorMenu.setFocus(false); return self.displayHelp(cb); }, /////////////////////////////////////////////////////////////////////// - // View Mode + // View Mode /////////////////////////////////////////////////////////////////////// viewModeMenuHelp : function(formData, extraArgs, cb) { self.viewControllers.footerView.setFocus(false); @@ -266,43 +266,43 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } getFooterName() { - return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... + return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... } getFormId(name) { return { - header : 0, - body : 1, - footerEditor : 2, - footerEditorMenu : 3, - footerView : 4, - quoteBuilder : 5, + header : 0, + body : 1, + footerEditor : 2, + footerEditorMenu : 3, + footerView : 4, + quoteBuilder : 5, - help : 50, + help : 50, }[name]; } getHeaderFormatObj() { - const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A'; - const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A'; - const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat(); + const remoteUserNotAvail = this.menuConfig.config.remoteUserNotAvail || 'N/A'; + const localUserIdNotAvail = this.menuConfig.config.localUserIdNotAvail || 'N/A'; + const modTimestampFormat = this.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat(); return { - // :TODO: ensure we show real names for form/to if they are enforced in the area - fromUserName : this.message.fromUserName, - toUserName : this.message.toUserName, - // :TODO: + // :TODO: ensure we show real names for form/to if they are enforced in the area + fromUserName : this.message.fromUserName, + toUserName : this.message.toUserName, + // :TODO: //fromRealName //toRealName - fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail), - toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail), - fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail), - toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail), - subject : this.message.subject, - modTimestamp : this.message.modTimestamp.format(modTimestampFormat), - msgNum : this.messageIndex + 1, - msgTotal : this.messageTotal, - messageId : this.message.messageId, + fromUserId : _.get(this.message, 'meta.System.local_from_user_id', localUserIdNotAvail), + toUserId : _.get(this.message, 'meta.System.local_to_user_id', localUserIdNotAvail), + fromRemoteUser : _.get(this.message, 'meta.System.remote_from_user', remoteUserNotAvail), + toRemoteUser : _.get(this.messgae, 'meta.System.remote_to_user', remoteUserNotAvail), + subject : this.message.subject, + modTimestamp : this.message.modTimestamp.format(modTimestampFormat), + msgNum : this.messageIndex + 1, + msgTotal : this.messageTotal, + messageId : this.message.messageId, }; } @@ -317,26 +317,26 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const headerValues = this.viewControllers.header.getFormData().value; const msgOpts = { - areaTag : this.messageAreaTag, - toUserName : headerValues.to, - fromUserName : this.client.user.username, - subject : headerValues.subject, - // :TODO: don't hard code 1 here: - message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), + areaTag : this.messageAreaTag, + toUserName : headerValues.to, + fromUserName : this.client.user.username, + subject : headerValues.subject, + // :TODO: don't hard code 1 here: + message : this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ), }; if(this.isReply()) { - msgOpts.replyToMsgId = this.replyToMessage.messageId; + msgOpts.replyToMsgId = this.replyToMessage.messageId; if(this.replyIsAnsi) { // - // Ensure first characters indicate ANSI for detection down - // the line (other boards/etc.). We also set explicit_encoding - // to packetAnsiMsgEncoding (generally cp437) as various boards - // really don't like ANSI messages in UTF-8 encoding (they should!) + // Ensure first characters indicate ANSI for detection down + // the line (other boards/etc.). We also set explicit_encoding + // to packetAnsiMsgEncoding (generally cp437) as various boards + // really don't like ANSI messages in UTF-8 encoding (they should!) // - msgOpts.meta = { System : { 'explicit_encoding' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; - msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; + msgOpts.meta = { System : { 'explicit_encoding' : _.get(Config(), 'scannerTossers.ftn_bso.packetAnsiMsgEncoding', 'cp437') } }; + msgOpts.message = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${msgOpts.message}`; } } @@ -363,18 +363,18 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.initHeaderViewMode(); this.initFooterViewMode(); - const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); - let msg = this.message.message; + const bodyMessageView = this.viewControllers.body.getView(MciViewIds.body.message); + let msg = this.message.message; if(bodyMessageView && _.has(this, 'message.message')) { // - // We handle ANSI messages differently than standard messages -- this is required as - // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted - // how the author wanted it + // We handle ANSI messages differently than standard messages -- this is required as + // we don't want to do things like word wrap ANSI, but instead, trust that it's formatted + // how the author wanted it // if(isAnsi(msg)) { // - // Find tearline - we want to color it differently. + // Find tearline - we want to color it differently. // const tearLinePos = this.message.getTearLinePosition(msg); @@ -383,10 +383,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } bodyMessageView.setAnsi( - msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF + msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF { - prepped : false, - forceLineTerm : true, + prepped : false, + forceLineTerm : true, } ); } else { @@ -404,7 +404,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul [ function buildIfNecessary(callback) { if(self.isEditMode()) { - return self.buildMessage(callback); // creates initial self.message + return self.buildMessage(callback); // creates initial self.message } return callback(null); @@ -422,9 +422,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } // - // If the message we're replying to is from a remote user - // don't try to look up the local user ID. Instead, mark the mail - // for export with the remote to address. + // If the message we're replying to is from a remote user + // don't try to look up the local user ID. Instead, mark the mail + // for export with the remote to address. // if(self.replyToMessage && self.replyToMessage.isFromRemoteUser()) { self.message.setRemoteToUser(self.replyToMessage.meta.System[Message.SystemMetaNames.RemoteFromUser]); @@ -433,9 +433,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } // - // Detect if the user is attempting to send to a remote mail type that we support + // Detect if the user is attempting to send to a remote mail type that we support // - // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such + // :TODO: how to plug in support without tying to various types here? isSupportedExteranlType() or such const addressedToInfo = getAddressedToInfo(self.message.toUserName); if(addressedToInfo.name && Message.AddressFlavor.FTN === addressedToInfo.flavor) { self.message.setRemoteToUser(addressedToInfo.remote); @@ -444,7 +444,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul return callback(null); } - // we need to look it up + // we need to look it up User.getUserIdAndNameByLookup(self.message.toUserName, (err, toUserId) => { if(err) { return callback(err); @@ -466,7 +466,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(cb) { cb(null); } - return; // don't inc stats for private messages + return; // don't inc stats for private messages } return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); @@ -479,9 +479,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul [ function moveToFooterPosition(callback) { // - // Calculate footer starting position + // Calculate footer starting position // - // row = (header height + body height) + // row = (header height + body height) // var footerRow = self.header.height + self.body.height; self.client.term.rawWrite(ansi.goto(footerRow, 1)); @@ -489,10 +489,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }, function clearFooterArea(callback) { if(options.clear) { - // footer up to 3 rows in height + // footer up to 3 rows in height - // :TODO: We'd like to delete up to N rows, but this does not work - // in NetRunner: + // :TODO: We'd like to delete up to N rows, but this does not work + // in NetRunner: self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)); @@ -519,9 +519,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } redrawScreen(cb) { - var comps = [ 'header', 'body' ]; - const self = this; - var art = self.menuConfig.config.art; + var comps = [ 'header', 'body' ]; + const self = this; + var art = self.menuConfig.config.art; self.client.term.rawWrite(ansi.resetScreen()); @@ -543,7 +543,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }); }, function displayFooter(callback) { - // we have to treat the footer special + // we have to treat the footer special self.redrawFooter( { clear : false, footerName : self.getFooterName() }, function footerDisplayed(err) { callback(err); }); @@ -577,9 +577,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(_.isUndefined(this.viewControllers[footerName])) { var menuLoadOpts = { - callingMenu : this, - formId : formId, - mciMap : artData.mciMap + callingMenu : this, + formId : formId, + mciMap : artData.mciMap }; this.addViewController( @@ -597,8 +597,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul initSequence() { var mciData = { }; - const self = this; - var art = self.menuConfig.config.art; + const self = this; + var art = self.menuConfig.config.art; assert(_.isObject(art)); @@ -659,7 +659,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul [ function header(callback) { menuLoadOpts.formId = self.getFormId('header'); - menuLoadOpts.mciMap = mciData.header.mciMap; + menuLoadOpts.mciMap = mciData.header.mciMap; self.addViewController( 'header', @@ -669,8 +669,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }); }, function body(callback) { - menuLoadOpts.formId = self.getFormId('body'); - menuLoadOpts.mciMap = mciData.body.mciMap; + menuLoadOpts.formId = self.getFormId('body'); + menuLoadOpts.mciMap = mciData.body.mciMap; self.addViewController( 'body', @@ -698,12 +698,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul from.acceptsFocus = false; //from.setText(self.client.user.username); - // :TODO: make this a method + // :TODO: make this a method var body = self.viewControllers.body.getView(MciViewIds.body.message); self.updateTextEditMode(body.getTextEditMode()); self.updateEditModePosition(body.getEditPosition()); - // :TODO: If view mode, set body to read only... which needs an impl... + // :TODO: If view mode, set body to read only... which needs an impl... callback(null); }, @@ -767,22 +767,22 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul mciReadyHandler(mciData, cb) { this.createInitialViews(mciData, err => { - // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in - // place - if this is for existing usernames else validate spec + // :TODO: Can probably be replaced with @systemMethod:validateUserNameExists when the framework is in + // place - if this is for existing usernames else validate spec /* - self.viewControllers.header.on('leave', function headerViewLeave(view) { + self.viewControllers.header.on('leave', function headerViewLeave(view) { - if(2 === view.id) { // "to" field - self.validateToUserName(view.getData(), function result(err) { - if(err) { - // :TODO: display a error in a %TL area or such - view.clearText(); - self.viewControllers.headers.switchFocus(2); - } - }); - } - });*/ + if(2 === view.id) { // "to" field + self.validateToUserName(view.getData(), function result(err) { + if(err) { + // :TODO: display a error in a %TL area or such + view.clearText(); + self.viewControllers.headers.switchFocus(2); + } + }); + } + });*/ cb(err); }); @@ -793,7 +793,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul var posView = this.viewControllers.footerEditor.getView(1); if(posView) { this.client.term.rawWrite(ansi.savePos()); - // :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat + // :TODO: Use new formatting techniques here, e.g. state.cursorPositionRow, cursorPositionCol and cursorPositionFormat posView.setText(_.padStart(String(pos.row + 1), 2, '0') + ',' + _.padEnd(String(pos.col + 1), 2, '0')); this.client.term.rawWrite(ansi.restorePos()); } @@ -816,16 +816,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } initHeaderViewMode() { - this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); - this.setHeaderText(MciViewIds.header.to, this.message.toUserName); - this.setHeaderText(MciViewIds.header.subject, this.message.subject); - this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); - this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); - this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); + this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + this.setHeaderText(MciViewIds.header.to, this.message.toUserName); + this.setHeaderText(MciViewIds.header.subject, this.message.subject); + this.setHeaderText(MciViewIds.header.modTimestamp, moment(this.message.modTimestamp).format(this.client.currentTheme.helpers.getDateTimeFormat())); + this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); + this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); - // if we changed conf/area we need to update any related standard MCI view + // if we changed conf/area we need to update any related standard MCI view this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] ); } @@ -835,15 +835,15 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.setHeaderText(MciViewIds.header.to, this.replyToMessage.fromUserName); // - // We want to prefix the subject with "RE: " only if it's not already - // that way -- avoid RE: RE: RE: RE: ... + // We want to prefix the subject with "RE: " only if it's not already + // that way -- avoid RE: RE: RE: RE: ... // let newSubj = this.replyToMessage.subject; if(false === /^RE:\s+/i.test(newSubj)) { newSubj = `RE: ${newSubj}`; } - this.setHeaderText(MciViewIds.header.subject, newSubj); + this.setHeaderText(MciViewIds.header.subject, newSubj); } initFooterViewMode() { @@ -869,7 +869,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul displayQuoteBuilder() { // - // Clear body area + // Clear body area // this.newQuoteBlock = true; const self = this; @@ -877,10 +877,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul async.waterfall( [ function clearAndDisplayArt(callback) { - // :TODO: NetRunner does NOT support delete line, so this does not work: + // :TODO: NetRunner does NOT support delete line, so this does not work: self.client.term.rawWrite( ansi.goto(self.header.height + 1, 1) + - ansi.deleteLine((self.client.term.termHeight - self.header.height) - 1)); + ansi.deleteLine((self.client.term.termHeight - self.header.height) - 1)); theme.displayThemeArt( { name : self.menuConfig.config.art.quote, client : self.client }, function displayed(err, artData) { callback(err, artData); @@ -891,9 +891,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul if(_.isUndefined(self.viewControllers.quoteBuilder)) { var menuLoadOpts = { - callingMenu : self, - formId : formId, - mciMap : artData.mciMap, + callingMenu : self, + formId : formId, + mciMap : artData.mciMap, }; self.addViewController( @@ -909,16 +909,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }, function loadQuoteLines(callback) { const quoteView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); - const bodyView = self.viewControllers.body.getView(MciViewIds.body.message); + const bodyView = self.viewControllers.body.getView(MciViewIds.body.message); self.replyToMessage.getQuoteLines( { - termWidth : self.client.term.termWidth, - termHeight : self.client.term.termHeight, - cols : quoteView.dimens.width, - startCol : quoteView.position.col, - ansiResetSgr : bodyView.styleSGR1, - ansiFocusPrefixSgr : quoteView.styleSGR2, + termWidth : self.client.term.termWidth, + termHeight : self.client.term.termHeight, + cols : quoteView.dimens.width, + startCol : quoteView.position.col, + ansiResetSgr : bodyView.styleSGR1, + ansiFocusPrefixSgr : quoteView.styleSGR2, }, (err, quoteLines, focusQuoteLines, replyIsAnsi) => { if(err) { @@ -959,16 +959,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } /* - this.observeViewPosition = function() { - self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) { - console.log(pos.percent + ' / ' + pos.below) - }); - }; - */ + this.observeViewPosition = function() { + self.viewControllers.body.getView(MciViewIds.body.message).on('edit position', function positionUpdate(pos) { + console.log(pos.percent + ' / ' + pos.below) + }); + }; + */ switchToHeader() { this.viewControllers.body.setFocus(false); - this.viewControllers.header.switchFocus(2); // to + this.viewControllers.header.switchFocus(2); // to } switchToBody() { @@ -982,7 +982,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul this.viewControllers.header.setFocus(false); this.viewControllers.body.setFocus(false); - this.viewControllers[this.getFooterName()].switchFocus(1); // HM1 + this.viewControllers[this.getFooterName()].switchFocus(1); // HM1 } switchFromQuoteBuilderToBody() { @@ -991,7 +991,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul body.redraw(); this.viewControllers.body.switchFocus(1); - // :TODO: create method (DRY) + // :TODO: create method (DRY) this.updateTextEditMode(body.getTextEditMode()); this.updateEditModePosition(body.getEditPosition()); @@ -1000,11 +1000,11 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } quoteBuilderFinalize() { - // :TODO: fix magic #'s - const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); - const msgView = this.viewControllers.body.getView(MciViewIds.body.message); + // :TODO: fix magic #'s + const quoteMsgView = this.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quotedMsg); + const msgView = this.viewControllers.body.getView(MciViewIds.body.message); - let quoteLines = quoteMsgView.getData().trim(); + let quoteLines = quoteMsgView.getData().trim(); if(quoteLines.length > 0) { if(this.replyIsAnsi) { @@ -1034,8 +1034,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const dtFormat = this.menuConfig.config.quoteDateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); return stringFormat(quoteFormat, { - dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), - userName : this.replyToMessage.fromUserName, + dateTime : moment(this.replyToMessage.modTimestamp).format(dtFormat), + userName : this.replyToMessage.fromUserName, }); } diff --git a/core/ftn_address.js b/core/ftn_address.js index 92c37557..6751adb8 100644 --- a/core/ftn_address.js +++ b/core/ftn_address.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -const _ = require('lodash'); +const _ = require('lodash'); const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i; const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; @@ -25,7 +25,7 @@ module.exports = class Address { } isValid() { - // FTN address is valid if we have at least a net/node + // FTN address is valid if we have at least a net/node return _.isNumber(this.net) && _.isNumber(this.node); } @@ -36,10 +36,10 @@ module.exports = class Address { return ( this.net === other.net && - this.node === other.node && - this.zone === other.zone && - this.point === other.point && - this.domain === other.domain + this.node === other.node && + this.zone === other.zone && + this.point === other.point && + this.domain === other.domain ); } @@ -95,36 +95,36 @@ module.exports = class Address { } /* - getMatchScore(pattern) { - let score = 0; - const addr = this.getMatchAddr(pattern); - if(addr) { - const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ]; - for(let i = 0; i < PARTS.length; ++i) { - const member = PARTS[i]; - if(this[member] === addr[member]) { - score += 2; - } else if('*' === addr[member]) { - score += 1; - } else { - break; - } - } - } + getMatchScore(pattern) { + let score = 0; + const addr = this.getMatchAddr(pattern); + if(addr) { + const PARTS = [ 'net', 'node', 'zone', 'point', 'domain' ]; + for(let i = 0; i < PARTS.length; ++i) { + const member = PARTS[i]; + if(this[member] === addr[member]) { + score += 2; + } else if('*' === addr[member]) { + score += 1; + } else { + break; + } + } + } - return score; - } - */ + return score; + } + */ isPatternMatch(pattern) { const addr = this.getMatchAddr(pattern); if(addr) { return ( ('*' === addr.net || this.net === addr.net) && - ('*' === addr.node || this.node === addr.node) && - ('*' === addr.zone || this.zone === addr.zone) && - ('*' === addr.point || this.point === addr.point) && - ('*' === addr.domain || this.domain === addr.domain) + ('*' === addr.node || this.node === addr.node) && + ('*' === addr.zone || this.zone === addr.zone) && + ('*' === addr.point || this.point === addr.point) && + ('*' === addr.domain || this.domain === addr.domain) ); } @@ -137,8 +137,8 @@ module.exports = class Address { if(m) { // start with a 2D let addr = { - net : parseInt(m[2]), - node : parseInt(m[3].substr(1)), + net : parseInt(m[2]), + node : parseInt(m[3].substr(1)), }; // 3D: Addition of zone if present @@ -165,14 +165,14 @@ module.exports = class Address { let addrStr = `${this.zone}:${this.net}`; - // allow for e.g. '4D' or 5 + // allow for e.g. '4D' or 5 const dim = parseInt(dimensions.toString()[0]); if(dim >= 3) { addrStr += `/${this.node}`; } - // missing & .0 are equiv for point + // missing & .0 are equiv for point if(dim >= 4 && this.point) { addrStr += `.${this.point}`; } diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 6c49c1c6..cc0dde3e 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -1,75 +1,75 @@ /* jslint node: true */ 'use strict'; -const ftn = require('./ftn_util.js'); -const Message = require('./message.js'); -const sauce = require('./sauce.js'); -const Address = require('./ftn_address.js'); -const strUtil = require('./string_util.js'); -const Log = require('./logger.js').log; -const ansiPrep = require('./ansi_prep.js'); -const Errors = require('./enig_error.js').Errors; +const ftn = require('./ftn_util.js'); +const Message = require('./message.js'); +const sauce = require('./sauce.js'); +const Address = require('./ftn_address.js'); +const strUtil = require('./string_util.js'); +const Log = require('./logger.js').log; +const ansiPrep = require('./ansi_prep.js'); +const Errors = require('./enig_error.js').Errors; -const _ = require('lodash'); -const assert = require('assert'); -const { Parser } = require('binary-parser'); -const fs = require('graceful-fs'); -const async = require('async'); -const iconv = require('iconv-lite'); -const moment = require('moment'); +const _ = require('lodash'); +const assert = require('assert'); +const { Parser } = require('binary-parser'); +const fs = require('graceful-fs'); +const async = require('async'); +const iconv = require('iconv-lite'); +const moment = require('moment'); -exports.Packet = Packet; +exports.Packet = Packet; -const FTN_PACKET_HEADER_SIZE = 58; // fixed header size -const FTN_PACKET_HEADER_TYPE = 2; -const FTN_PACKET_MESSAGE_TYPE = 2; -const FTN_PACKET_BAUD_TYPE_2_2 = 2; +const FTN_PACKET_HEADER_SIZE = 58; // fixed header size +const FTN_PACKET_HEADER_TYPE = 2; +const FTN_PACKET_MESSAGE_TYPE = 2; +const FTN_PACKET_BAUD_TYPE_2_2 = 2; -// SAUCE magic header + version ("00") +// SAUCE magic header + version ("00") const FTN_MESSAGE_SAUCE_HEADER = Buffer.from('SAUCE00'); -const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; +const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; class PacketHeader { constructor(origAddr, destAddr, version, createdMoment) { const EMPTY_ADDRESS = { - node : 0, - net : 0, - zone : 0, - point : 0, + node : 0, + net : 0, + zone : 0, + point : 0, }; - this.version = version || '2+'; - this.origAddress = origAddr || EMPTY_ADDRESS; - this.destAddress = destAddr || EMPTY_ADDRESS; - this.created = createdMoment || moment(); + this.version = version || '2+'; + this.origAddress = origAddr || EMPTY_ADDRESS; + this.destAddress = destAddr || EMPTY_ADDRESS; + this.created = createdMoment || moment(); - // uncommon to set the following explicitly - this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 - this.prodRevLo = 0; - this.baud = 0; - this.packetType = FTN_PACKET_HEADER_TYPE; - this.password = ''; - this.prodData = 0x47694e45; // "ENiG" + // uncommon to set the following explicitly + this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 + this.prodRevLo = 0; + this.baud = 0; + this.packetType = FTN_PACKET_HEADER_TYPE; + this.password = ''; + this.prodData = 0x47694e45; // "ENiG" - this.capWord = 0x0001; - this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap + this.capWord = 0x0001; + this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap - this.prodCodeHi = 0xfe; // see above - this.prodRevHi = 0; + this.prodCodeHi = 0xfe; // see above + this.prodRevHi = 0; } get origAddress() { let addr = new Address({ - node : this.origNode, - zone : this.origZone, + node : this.origNode, + zone : this.origZone, }); if(this.origPoint) { - addr.point = this.origPoint; - addr.net = this.auxNet; + addr.point = this.origPoint; + addr.net = this.auxNet; } else { - addr.net = this.origNet; + addr.net = this.origNet; } return addr; @@ -82,29 +82,29 @@ class PacketHeader { this.origNode = address.node; - // See FSC-48 - // :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2 + // See FSC-48 + // :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2 /*if(address.point) { - this.auxNet = address.origNet; - this.origNet = -1; - } else { - this.origNet = address.net; - this.auxNet = 0; - } - */ - this.origNet = address.net; - this.auxNet = 0; + this.auxNet = address.origNet; + this.origNet = -1; + } else { + this.origNet = address.net; + this.auxNet = 0; + } + */ + this.origNet = address.net; + this.auxNet = 0; - this.origZone = address.zone; - this.origZone2 = address.zone; - this.origPoint = address.point || 0; + this.origZone = address.zone; + this.origZone2 = address.zone; + this.origPoint = address.point || 0; } get destAddress() { let addr = new Address({ - node : this.destNode, - net : this.destNet, - zone : this.destZone, + node : this.destNode, + net : this.destNet, + zone : this.destZone, }); if(this.destPoint) { @@ -119,21 +119,21 @@ class PacketHeader { address = Address.fromString(address); } - this.destNode = address.node; - this.destNet = address.net; - this.destZone = address.zone; - this.destZone2 = address.zone; - this.destPoint = address.point || 0; + this.destNode = address.node; + this.destNet = address.net; + this.destZone = address.zone; + this.destZone2 = address.zone; + this.destPoint = address.point || 0; } get created() { return moment({ - year : this.year, - month : this.month - 1, // moment uses 0 indexed months - date : this.day, - hour : this.hour, - minute : this.minute, - second : this.second + year : this.year, + month : this.month - 1, // moment uses 0 indexed months + date : this.day, + hour : this.hour, + minute : this.minute, + second : this.second }); } @@ -142,28 +142,28 @@ class PacketHeader { momentCreated = moment(momentCreated); } - this.year = momentCreated.year(); - this.month = momentCreated.month() + 1; // moment uses 0 indexed months - this.day = momentCreated.date(); // day of month - this.hour = momentCreated.hour(); - this.minute = momentCreated.minute(); - this.second = momentCreated.second(); + this.year = momentCreated.year(); + this.month = momentCreated.month() + 1; // moment uses 0 indexed months + this.day = momentCreated.date(); // day of month + this.hour = momentCreated.hour(); + this.minute = momentCreated.minute(); + this.second = momentCreated.second(); } } exports.PacketHeader = PacketHeader; // -// Read/Write FTN packets with support for the following formats: +// Read/Write FTN packets with support for the following formats: // -// * Type 2 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete) -// * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001 -// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 -// and http://ftsc.org/docs/fsc-0048.002 +// * Type 2 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete) +// * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001 +// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 +// and http://ftsc.org/docs/fsc-0048.002 // -// Additional resources: -// * Writeup on differences between type 2, 2.2, and 2+: -// http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt +// Additional resources: +// * Writeup on differences between type 2, 2.2, and 2+: +// http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt // function Packet(options) { var self = this; @@ -189,13 +189,13 @@ function Packet(options) { .uint16le('origNet') .uint16le('destNet') .int8('prodCodeLo') - .int8('prodRevLo') // aka serialNo - .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 + .int8('prodRevLo') // aka serialNo + .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 .uint16le('origZone') .uint16le('destZone') // - // The following is "filler" in FTS-0001, specifics in - // FSC-0045 and FSC-0048 + // The following is "filler" in FTS-0001, specifics in + // FSC-0045 and FSC-0048 // .uint16le('auxNet') .uint16le('capWordValidate') @@ -212,7 +212,7 @@ function Packet(options) { return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`); } - // Convert password from NULL padded array to string + // Convert password from NULL padded array to string packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { @@ -220,50 +220,50 @@ function Packet(options) { } // - // What kind of packet do we really have here? + // What kind of packet do we really have here? // - // :TODO: adjust values based on version discovered + // :TODO: adjust values based on version discovered if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { packetHeader.version = '2.2'; - // See FSC-0045 - packetHeader.origPoint = packetHeader.year; - packetHeader.destPoint = packetHeader.month; + // See FSC-0045 + packetHeader.origPoint = packetHeader.year; + packetHeader.destPoint = packetHeader.month; packetHeader.destDomain = packetHeader.origZone2; - packetHeader.origDomain = packetHeader.auxNet; + packetHeader.origDomain = packetHeader.auxNet; } else { // - // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" + // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" // const capWordValidateSwapped = - ((packetHeader.capWordValidate & 0xff) << 8) | - ((packetHeader.capWordValidate >> 8) & 0xff); + ((packetHeader.capWordValidate & 0xff) << 8) | + ((packetHeader.capWordValidate >> 8) & 0xff); if(capWordValidateSwapped === packetHeader.capWord && - 0 != packetHeader.capWord && - packetHeader.capWord & 0x0001) + 0 != packetHeader.capWord && + packetHeader.capWord & 0x0001) { packetHeader.version = '2+'; - // See FSC-0048 + // See FSC-0048 if(-1 === packetHeader.origNet) { packetHeader.origNet = packetHeader.auxNet; } } else { packetHeader.version = '2'; - // :TODO: should fill bytes be 0? + // :TODO: should fill bytes be 0? } } packetHeader.created = moment({ - year : packetHeader.year, - month : packetHeader.month - 1, // moment uses 0 indexed months - date : packetHeader.day, - hour : packetHeader.hour, - minute : packetHeader.minute, - second : packetHeader.second + year : packetHeader.year, + month : packetHeader.month - 1, // moment uses 0 indexed months + date : packetHeader.day, + hour : packetHeader.hour, + minute : packetHeader.minute, + second : packetHeader.second }); const ph = new PacketHeader(); @@ -352,36 +352,36 @@ function Packet(options) { this.processMessageBody = function(messageBodyBuffer, cb) { // - // From FTS-0001.16: - // "Message text is unbounded and null terminated (note exception below). + // From FTS-0001.16: + // "Message text is unbounded and null terminated (note exception below). // - // A 'hard' carriage return, 0DH, marks the end of a paragraph, and must - // be preserved. + // A 'hard' carriage return, 0DH, marks the end of a paragraph, and must + // be preserved. // - // So called 'soft' carriage returns, 8DH, may mark a previous - // processor's automatic line wrap, and should be ignored. Beware that - // they may be followed by linefeeds, or may not. + // So called 'soft' carriage returns, 8DH, may mark a previous + // processor's automatic line wrap, and should be ignored. Beware that + // they may be followed by linefeeds, or may not. // - // All linefeeds, 0AH, should be ignored. Systems which display message - // text should wrap long lines to suit their application." + // All linefeeds, 0AH, should be ignored. Systems which display message + // text should wrap long lines to suit their application." // - // This can be a bit tricky: - // * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that - // * Many kludge lines specify an encoding. If we find one of such lines, we'll - // likely need to re-decode as the specified encoding - // * SAUCE is binary-ish data, so we need to inspect for it before any - // decoding occurs + // This can be a bit tricky: + // * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that + // * Many kludge lines specify an encoding. If we find one of such lines, we'll + // likely need to re-decode as the specified encoding + // * SAUCE is binary-ish data, so we need to inspect for it before any + // decoding occurs // let messageBodyData = { - message : [], - kludgeLines : {}, // KLUDGE:[value1, value2, ...] map - seenBy : [], + message : [], + kludgeLines : {}, // KLUDGE:[value1, value2, ...] map + seenBy : [], }; function addKludgeLine(line) { // - // We have to special case INTL/TOPT/FMPT as they don't contain - // a ':' name/value separator like the rest of the kludge lines... because stupdity. + // We have to special case INTL/TOPT/FMPT as they don't contain + // a ':' name/value separator like the rest of the kludge lines... because stupdity. // let key = line.substr(0, 4).trim(); let value; @@ -389,13 +389,13 @@ function Packet(options) { value = line.substr(key.length).trim(); } else { const sepIndex = line.indexOf(':'); - key = line.substr(0, sepIndex).toUpperCase(); - value = line.substr(sepIndex + 1).trim(); + key = line.substr(0, sepIndex).toUpperCase(); + value = line.substr(sepIndex + 1).trim(); } // - // Allow mapped value to be either a key:value if there is only - // one entry, or key:[value1, value2,...] if there are more + // Allow mapped value to be either a key:value if there is only + // one entry, or key:[value1, value2,...] if there are more // if(messageBodyData.kludgeLines[key]) { if(!_.isArray(messageBodyData.kludgeLines[key])) { @@ -412,21 +412,21 @@ function Packet(options) { async.series( [ function extractSauce(callback) { - // :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's - // present, we need to extract it but keep the rest of hte message intact as it likely - // has SEEN-BY, PATH, and other kludge information *appended* + // :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's + // present, we need to extract it but keep the rest of hte message intact as it likely + // has SEEN-BY, PATH, and other kludge information *appended* const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER); if(sauceHeaderPosition > -1) { sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition, sauceHeaderPosition + sauce.SAUCE_SIZE), (err, theSauce) => { if(!err) { - // we read some SAUCE - don't re-process that portion into the body - messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); - // messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); - messageBodyData.sauce = theSauce; + // we read some SAUCE - don't re-process that portion into the body + messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); + // messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); + messageBodyData.sauce = theSauce; } else { Log.warn( { error : err.message }, 'Found what looks like to be a SAUCE record, but failed to read'); } - return callback(null); // failure to read SAUCE is OK + return callback(null); // failure to read SAUCE is OK }); } else { callback(null); @@ -434,21 +434,21 @@ function Packet(options) { }, function extractChrsAndDetermineEncoding(callback) { // - // From FTS-5003.001: - // "The CHRS control line is formatted as follows: + // From FTS-5003.001: + // "The CHRS control line is formatted as follows: // - // ^ACHRS: + // ^ACHRS: // - // Where is a character string of no more than eight (8) - // ASCII characters identifying the character set or character encoding - // scheme used, and level is a positive integer value describing what - // level of CHRS the message is written in." + // Where is a character string of no more than eight (8) + // ASCII characters identifying the character set or character encoding + // scheme used, and level is a positive integer value describing what + // level of CHRS the message is written in." // - // Also according to the spec, the deprecated "CHARSET" value may be used - // :TODO: Look into CHARSET more - should we bother supporting it? - // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam - const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" - const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] ); + // Also according to the spec, the deprecated "CHARSET" value may be used + // :TODO: Look into CHARSET more - should we bother supporting it? + // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam + const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" + const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] ); let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); if(chrsPrefixIndex < 0) { @@ -476,9 +476,9 @@ function Packet(options) { }, function extractMessageData(callback) { // - // Decode |messageBodyBuffer| using |encoding| defaulted or detected above + // Decode |messageBodyBuffer| using |encoding| defaulted or detected above // - // :TODO: Look into \xec thing more - document + // :TODO: Look into \xec thing more - document let decoded; try { decoded = iconv.decode(messageBodyBuffer, encoding); @@ -487,8 +487,8 @@ function Packet(options) { decoded = iconv.decode(messageBodyBuffer, 'ascii'); } - const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, '')); - let endOfMessage = false; + const messageLines = strUtil.splitTextAtTerms(decoded.replace(/\xec/g, '')); + let endOfMessage = false; messageLines.forEach(line => { if(0 === line.length) { @@ -499,21 +499,21 @@ function Packet(options) { if(line.startsWith('AREA:')) { messageBodyData.area = line.substring(line.indexOf(':') + 1).trim(); } else if(line.startsWith('--- ')) { - // Tear Lines are tracked allowing for specialized display/etc. + // Tear Lines are tracked allowing for specialized display/etc. messageBodyData.tearLine = line; - } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." + } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." messageBodyData.originLine = line; - endOfMessage = true; // Anything past origin is not part of the message body + endOfMessage = true; // Anything past origin is not part of the message body } else if(line.startsWith('SEEN-BY:')) { - endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body + endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim()); } else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) { if('PATH:' === line.slice(1, 6)) { - endOfMessage = true; // Anything pats the first PATH is not part of the message body + endOfMessage = true; // Anything pats the first PATH is not part of the message body } addKludgeLine(line.slice(1)); } else if(!endOfMessage) { - // regular ol' message line + // regular ol' message line messageBodyData.message.push(line); } }); @@ -530,16 +530,16 @@ function Packet(options) { this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { // - // Check for end-of-messages marker up front before parse so we can easily - // tell the difference between end and bad header + // Check for end-of-messages marker up front before parse so we can easily + // tell the difference between end and bad header // if(packetBuffer.length < 3) { const peek = packetBuffer.slice(0, 2); if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) { - // end marker - no more messages + // end marker - no more messages return cb(null); } - // else fall through & hit exception below to log error + // else fall through & hit exception below to log error } let msgData; @@ -552,26 +552,26 @@ function Packet(options) { .uint16le('ftn_msg_dest_net') .uint16le('ftn_attr_flags') .uint16le('ftn_cost') - // :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved + // :TODO: use string() for these if https://github.com/keichi/binary-parser/issues/33 is resolved .array('modDateTime', { - type : 'uint8', - readUntil : b => 0x00 === b, + type : 'uint8', + readUntil : b => 0x00 === b, }) .array('toUserName', { - type : 'uint8', - readUntil : b => 0x00 === b, + type : 'uint8', + readUntil : b => 0x00 === b, }) .array('fromUserName', { - type : 'uint8', - readUntil : b => 0x00 === b, + type : 'uint8', + readUntil : b => 0x00 === b, }) .array('subject', { - type : 'uint8', - readUntil : b => 0x00 === b, + type : 'uint8', + readUntil : b => 0x00 === b, }) .array('message', { - type : 'uint8', - readUntil : b => 0x00 === b, + type : 'uint8', + readUntil : b => 0x00 === b, }) .parse(packetBuffer); } catch(e) { @@ -583,49 +583,49 @@ function Packet(options) { } // - // Convert null terminated arrays to strings + // Convert null terminated arrays to strings // [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437'); }); - // Technically the following fields have length limits as per fts-0001.016: - // * modDateTime : 20 bytes - // * toUserName : 36 bytes - // * fromUserName : 36 bytes - // * subject : 72 bytes + // Technically the following fields have length limits as per fts-0001.016: + // * modDateTime : 20 bytes + // * toUserName : 36 bytes + // * fromUserName : 36 bytes + // * subject : 72 bytes // - // The message body itself is a special beast as it may - // contain an origin line, kludges, SAUCE in the case - // of ANSI files, etc. + // The message body itself is a special beast as it may + // contain an origin line, kludges, SAUCE in the case + // of ANSI files, etc. // const msg = new Message( { - toUserName : msgData.toUserName, - fromUserName : msgData.fromUserName, - subject : msgData.subject, - modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), + toUserName : msgData.toUserName, + fromUserName : msgData.fromUserName, + subject : msgData.subject, + modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), }); - // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) + // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) msg.meta.FtnProperty = { - ftn_orig_node : header.origNode, - ftn_dest_node : header.destNode, - ftn_orig_network : header.origNet, - ftn_dest_network : header.destNet, + ftn_orig_node : header.origNode, + ftn_dest_node : header.destNode, + ftn_orig_network : header.origNet, + ftn_dest_network : header.destNet, - ftn_attr_flags : msgData.ftn_attr_flags, - ftn_cost : msgData.ftn_cost, + ftn_attr_flags : msgData.ftn_attr_flags, + ftn_cost : msgData.ftn_cost, - ftn_msg_orig_node : msgData.ftn_msg_orig_node, - ftn_msg_dest_node : msgData.ftn_msg_dest_node, - ftn_msg_orig_net : msgData.ftn_msg_orig_net, - ftn_msg_dest_net : msgData.ftn_msg_dest_net, + ftn_msg_orig_node : msgData.ftn_msg_orig_node, + ftn_msg_dest_node : msgData.ftn_msg_dest_node, + ftn_msg_orig_net : msgData.ftn_msg_orig_net, + ftn_msg_dest_net : msgData.ftn_msg_dest_net, }; self.processMessageBody(msgData.message, messageBodyData => { - msg.message = messageBodyData.message; - msg.meta.FtnKludge = messageBodyData.kludgeLines; + msg.message = messageBodyData.message; + msg.meta.FtnKludge = messageBodyData.kludgeLines; if(messageBodyData.tearLine) { msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; @@ -652,21 +652,21 @@ function Packet(options) { } // - // If we have a UTC offset kludge (e.g. TZUTC) then update - // modDateTime with it + // If we have a UTC offset kludge (e.g. TZUTC) then update + // modDateTime with it // if(_.isString(msg.meta.FtnKludge.TZUTC) && msg.meta.FtnKludge.TZUTC.length > 0) { msg.modDateTime = msg.modTimestamp.utcOffset(msg.meta.FtnKludge.TZUTC); } - // :TODO: Parser should give is this info: + // :TODO: Parser should give is this info: const bytesRead = - 14 + // fixed header size - msgData.modDateTime.length + 1 + // +1 = NULL - msgData.toUserName.length + 1 + // +1 = NULL - msgData.fromUserName.length + 1 + // +1 = NULL - msgData.subject.length + 1 + // +1 = NULL - msgData.message.length; // includes NULL + 14 + // fixed header size + msgData.modDateTime.length + 1 + // +1 = NULL + msgData.toUserName.length + 1 + // +1 = NULL + msgData.fromUserName.length + 1 + // +1 = NULL + msgData.subject.length + 1 + // +1 = NULL + msgData.message.length; // includes NULL const nextBuf = packetBuffer.slice(bytesRead); if(nextBuf.length > 0) { @@ -710,11 +710,11 @@ function Packet(options) { }; this.writeMessageHeader = function(message, buf) { - // ensure address FtnProperties are numbers + // ensure address FtnProperties are numbers self.sanatizeFtnProperties(message); - const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node; - const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network; + const destNode = message.meta.FtnProperty.ftn_msg_dest_node || message.meta.FtnProperty.ftn_dest_node; + const destNet = message.meta.FtnProperty.ftn_msg_dest_net || message.meta.FtnProperty.ftn_dest_network; buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0); buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2); @@ -751,43 +751,43 @@ function Packet(options) { self.writeMessageHeader(message, basicHeader); // - // To, from, and subject must be NULL term'd and have max lengths as per spec. + // To, from, and subject must be NULL term'd and have max lengths as per spec. // - const toUserNameBuf = strUtil.stringToNullTermBuffer(message.toUserName, { encoding : 'cp437', maxBufLen : 36 } ); - const fromUserNameBuf = strUtil.stringToNullTermBuffer(message.fromUserName, { encoding : 'cp437', maxBufLen : 36 } ); - const subjectBuf = strUtil.stringToNullTermBuffer(message.subject, { encoding : 'cp437', maxBufLen : 72 } ); + const toUserNameBuf = strUtil.stringToNullTermBuffer(message.toUserName, { encoding : 'cp437', maxBufLen : 36 } ); + const fromUserNameBuf = strUtil.stringToNullTermBuffer(message.fromUserName, { encoding : 'cp437', maxBufLen : 36 } ); + const subjectBuf = strUtil.stringToNullTermBuffer(message.subject, { encoding : 'cp437', maxBufLen : 72 } ); // - // message: unbound length, NULL term'd + // message: unbound length, NULL term'd // - // We need to build in various special lines - kludges, area, - // seen-by, etc. + // We need to build in various special lines - kludges, area, + // seen-by, etc. // let msgBody = ''; // - // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 - // AREA:CONFERENCE - // Should be first line in a message + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // AREA:CONFERENCE + // Should be first line in a message // if(message.meta.FtnProperty.ftn_area) { - msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) + msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) } - // :TODO: DRY with similar function in this file! + // :TODO: DRY with similar function in this file! Object.keys(message.meta.FtnKludge).forEach(k => { switch(k) { case 'PATH' : - break; // skip & save for last + break; // skip & save for last case 'Via' : case 'FMPT' : case 'TOPT' : case 'INTL' : - msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar + msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); // no sepChar break; - default : + default : msgBody += getAppendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; } @@ -803,10 +803,10 @@ function Packet(options) { ansiPrep( message.message, { - cols : 80, - rows : 'auto', - forceLineTerm : true, - exportMode : true, + cols : 80, + rows : 'auto', + forceLineTerm : true, + exportMode : true, }, (err, preppedMsg) => { return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, preppedMsg || message.message); @@ -817,25 +817,25 @@ function Packet(options) { msgBody += preppedMsg + '\r'; // - // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 - // Tear line should be near the bottom of a message + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // Tear line should be near the bottom of a message // if(message.meta.FtnProperty.ftn_tear_line) { msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`; } // - // Origin line should be near the bottom of a message + // Origin line should be near the bottom of a message // if(message.meta.FtnProperty.ftn_origin) { msgBody += `${message.meta.FtnProperty.ftn_origin}\r`; } // - // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 - // SEEN-BY and PATH should be the last lines of a message + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // SEEN-BY and PATH should be the last lines of a message // - msgBody += getAppendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) + msgBody += getAppendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) msgBody += getAppendMeta('\x01PATH', message.meta.FtnKludge['PATH']); let msgBodyEncoded; @@ -869,28 +869,28 @@ function Packet(options) { ws.write(basicHeader); - // toUserName & fromUserName: up to 36 bytes in length, NULL term'd - // :TODO: DRY... + // toUserName & fromUserName: up to 36 bytes in length, NULL term'd + // :TODO: DRY... let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); - // subject: up to 72 bytes in length, NULL term'd + // subject: up to 72 bytes in length, NULL term'd encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); // - // message: unbound length, NULL term'd + // message: unbound length, NULL term'd // - // We need to build in various special lines - kludges, area, - // seen-by, etc. + // We need to build in various special lines - kludges, area, + // seen-by, etc. // - // :TODO: Put this in it's own method + // :TODO: Put this in it's own method let msgBody = ''; function appendMeta(k, m, sepChar=':') { @@ -906,54 +906,54 @@ function Packet(options) { } // - // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 - // AREA:CONFERENCE - // Should be first line in a message + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // AREA:CONFERENCE + // Should be first line in a message // if(message.meta.FtnProperty.ftn_area) { - msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) + msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\r`; // note: no ^A (0x01) } Object.keys(message.meta.FtnKludge).forEach(k => { switch(k) { - case 'PATH' : break; // skip & save for last + case 'PATH' : break; // skip & save for last case 'Via' : case 'FMPT' : case 'TOPT' : - case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar + case 'INTL' : appendMeta(`\x01${k}`, message.meta.FtnKludge[k], ''); break; // no sepChar - default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; + default : appendMeta(`\x01${k}`, message.meta.FtnKludge[k]); break; } }); msgBody += message.message + '\r'; // - // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 - // Tear line should be near the bottom of a message + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // Tear line should be near the bottom of a message // if(message.meta.FtnProperty.ftn_tear_line) { msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`; } // - // Origin line should be near the bottom of a message + // Origin line should be near the bottom of a message // if(message.meta.FtnProperty.ftn_origin) { msgBody += `${message.meta.FtnProperty.ftn_origin}\r`; } // - // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 - // SEEN-BY and PATH should be the last lines of a message + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // SEEN-BY and PATH should be the last lines of a message // - appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) + appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) appendMeta('\x01PATH', message.meta.FtnKludge['PATH']); // - // :TODO: We should encode based on config and add the proper kludge here! + // :TODO: We should encode based on config and add the proper kludge here! ws.write(iconv.encode(msgBody + '\0', options.encoding)); }; @@ -981,35 +981,35 @@ function Packet(options) { callback); } ], - cb // complete + cb // complete ); }; } // -// Message attributes defined in FTS-0001.016 -// http://ftsc.org/docs/fts-0001.016 +// Message attributes defined in FTS-0001.016 +// http://ftsc.org/docs/fts-0001.016 // -// See also: -// * http://www.skepticfiles.org/aj/basics03.htm +// See also: +// * http://www.skepticfiles.org/aj/basics03.htm // Packet.Attribute = { - Private : 0x0001, // Private message / NetMail - Crash : 0x0002, - Received : 0x0004, - Sent : 0x0008, - FileAttached : 0x0010, - InTransit : 0x0020, - Orphan : 0x0040, - KillSent : 0x0080, - Local : 0x0100, // Message is from *this* system - Hold : 0x0200, - Reserved0 : 0x0400, - FileRequest : 0x0800, - ReturnReceiptRequest : 0x1000, - ReturnReceipt : 0x2000, - AuditRequest : 0x4000, - FileUpdateRequest : 0x8000, + Private : 0x0001, // Private message / NetMail + Crash : 0x0002, + Received : 0x0004, + Sent : 0x0008, + FileAttached : 0x0010, + InTransit : 0x0020, + Orphan : 0x0040, + KillSent : 0x0080, + Local : 0x0100, // Message is from *this* system + Hold : 0x0200, + Reserved0 : 0x0400, + FileRequest : 0x0800, + ReturnReceiptRequest : 0x1000, + ReturnReceipt : 0x2000, + AuditRequest : 0x4000, + FileUpdateRequest : 0x8000, }; Object.freeze(Packet.Attribute); @@ -1051,10 +1051,10 @@ Packet.prototype.writeMessageEntry = function(ws, msgEntry) { Packet.prototype.writeTerminator = function(ws) { // - // From FTS-0001.016: - // "A pseudo-message beginning with the word 0000H signifies the end of the packet." + // From FTS-0001.016: + // "A pseudo-message beginning with the word 0000H signifies the end of the packet." // - ws.write(Buffer.from( [ 0x00, 0x00 ] )); // final extra null term + ws.write(Buffer.from( [ 0x00, 0x00 ] )); // final extra null term return 2; }; @@ -1074,7 +1074,7 @@ Packet.prototype.writeStream = function(ws, messages, options) { }); if(true === options.terminatePacket) { - ws.write(Buffer.from( [ 0 ] )); // final extra null term + ws.write(Buffer.from( [ 0 ] )); // final extra null term } }; @@ -1083,10 +1083,10 @@ Packet.prototype.write = function(path, packetHeader, messages, options) { messages = [ messages ]; } - options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4' + options = options || { encoding : 'utf8' }; // utf-8 = 'CHRS UTF-8 4' this.writeStream( - fs.createWriteStream(path), // :TODO: specify mode/etc. + fs.createWriteStream(path), // :TODO: specify mode/etc. messages, Object.assign( { packetHeader : packetHeader, terminatePacket : true }, options) ); diff --git a/core/ftn_util.js b/core/ftn_util.js index 0f65e127..e4637554 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -1,52 +1,52 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').get; -const Address = require('./ftn_address.js'); -const FNV1a = require('./fnv1a.js'); -const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion; +const Config = require('./config.js').get; +const Address = require('./ftn_address.js'); +const FNV1a = require('./fnv1a.js'); +const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion; -const _ = require('lodash'); -const iconv = require('iconv-lite'); -const moment = require('moment'); -const os = require('os'); +const _ = require('lodash'); +const iconv = require('iconv-lite'); +const moment = require('moment'); +const os = require('os'); -const packageJson = require('../package.json'); +const packageJson = require('../package.json'); -// :TODO: Remove "Ftn" from most of these -- it's implied in the module -exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; -exports.getMessageSerialNumber = getMessageSerialNumber; -exports.getDateFromFtnDateTime = getDateFromFtnDateTime; -exports.getDateTimeString = getDateTimeString; +// :TODO: Remove "Ftn" from most of these -- it's implied in the module +exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; +exports.getMessageSerialNumber = getMessageSerialNumber; +exports.getDateFromFtnDateTime = getDateFromFtnDateTime; +exports.getDateTimeString = getDateTimeString; -exports.getMessageIdentifier = getMessageIdentifier; -exports.getProductIdentifier = getProductIdentifier; -exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; -exports.getOrigin = getOrigin; -exports.getTearLine = getTearLine; -exports.getVia = getVia; -exports.getIntl = getIntl; -exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; -exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList; -exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; -exports.getUpdatedPathEntries = getUpdatedPathEntries; +exports.getMessageIdentifier = getMessageIdentifier; +exports.getProductIdentifier = getProductIdentifier; +exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; +exports.getOrigin = getOrigin; +exports.getTearLine = getTearLine; +exports.getVia = getVia; +exports.getIntl = getIntl; +exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; +exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList; +exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; +exports.getUpdatedPathEntries = getUpdatedPathEntries; -exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding; -exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier; +exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding; +exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier; -exports.getQuotePrefix = getQuotePrefix; +exports.getQuotePrefix = getQuotePrefix; // -// Namespace for RFC-4122 name based UUIDs generated from -// FTN kludges MSGID + AREA +// Namespace for RFC-4122 name based UUIDs generated from +// FTN kludges MSGID + AREA // -//const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654'); +//const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654'); -// See list here: https://github.com/Mithgol/node-fidonet-jam +// See list here: https://github.com/Mithgol/node-fidonet-jam function stringToNullPaddedBuffer(s, bufLen) { - let buffer = Buffer.alloc(bufLen); - let enc = iconv.encode(s, 'CP437').slice(0, bufLen); + let buffer = Buffer.alloc(bufLen); + let enc = iconv.encode(s, 'CP437').slice(0, bufLen); for(let i = 0; i < enc.length; ++i) { buffer[i] = enc[i]; } @@ -54,37 +54,37 @@ function stringToNullPaddedBuffer(s, bufLen) { } // -// Convert a FTN style DateTime string to a Date object +// Convert a FTN style DateTime string to a Date object // -// :TODO: Name the next couple methods better - for FTN *packets* +// :TODO: Name the next couple methods better - for FTN *packets* function getDateFromFtnDateTime(dateTime) { // - // Examples seen in the wild (Working): - // "12 Sep 88 18:17:59" - // "Tue 01 Jan 80 00:00" - // "27 Feb 15 00:00:03" + // Examples seen in the wild (Working): + // "12 Sep 88 18:17:59" + // "Tue 01 Jan 80 00:00" + // "27 Feb 15 00:00:03" // - // :TODO: Use moment.js here - return moment(Date.parse(dateTime)); // Date.parse() allows funky formats -// return (new Date(Date.parse(dateTime))).toISOString(); + // :TODO: Use moment.js here + return moment(Date.parse(dateTime)); // Date.parse() allows funky formats +// return (new Date(Date.parse(dateTime))).toISOString(); } function getDateTimeString(m) { // - // From http://ftsc.org/docs/fts-0001.016: - // DateTime = (* a character string 20 characters long *) - // (* 01 Jan 86 02:34:56 *) - // DayOfMonth " " Month " " Year " " - // " " HH ":" MM ":" SS - // Null + // From http://ftsc.org/docs/fts-0001.016: + // DateTime = (* a character string 20 characters long *) + // (* 01 Jan 86 02:34:56 *) + // DayOfMonth " " Month " " Year " " + // " " HH ":" MM ":" SS + // Null // - // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) - // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | - // "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" - // Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00" - // HH = "00" | .. | "23" - // MM = "00" | .. | "59" - // SS = "00" | .. | "59" + // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) + // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | + // "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" + // Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00" + // HH = "00" | .. | "23" + // MM = "00" | .. | "59" + // SS = "00" | .. | "59" // if(!moment.isMoment(m)) { m = moment(m); @@ -95,52 +95,52 @@ function getDateTimeString(m) { function getMessageSerialNumber(messageId) { const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); - const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); + const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); return `00000000${hash}`.substr(-8); } // -// Return a FTS-0009.001 compliant MSGID value given a message -// See http://ftsc.org/docs/fts-0009.001 +// Return a FTS-0009.001 compliant MSGID value given a message +// See http://ftsc.org/docs/fts-0009.001 // -// "A MSGID line consists of the string "^AMSGID:" (where ^A is a -// control-A (hex 01) and the double-quotes are not part of the -// string), followed by a space, the address of the originating -// system, and a serial number unique to that message on the -// originating system, i.e.: +// "A MSGID line consists of the string "^AMSGID:" (where ^A is a +// control-A (hex 01) and the double-quotes are not part of the +// string), followed by a space, the address of the originating +// system, and a serial number unique to that message on the +// originating system, i.e.: // -// ^AMSGID: origaddr serialno +// ^AMSGID: origaddr serialno // -// The originating address should be specified in a form that -// constitutes a valid return address for the originating network. -// If the originating address is enclosed in double-quotes, the -// entire string between the beginning and ending double-quotes is -// considered to be the orginating address. A double-quote character -// within a quoted address is represented by by two consecutive -// double-quote characters. The serial number may be any eight -// character hexadecimal number, as long as it is unique - no two -// messages from a given system may have the same serial number -// within a three years. The manner in which this serial number is -// generated is left to the implementor." +// The originating address should be specified in a form that +// constitutes a valid return address for the originating network. +// If the originating address is enclosed in double-quotes, the +// entire string between the beginning and ending double-quotes is +// considered to be the orginating address. A double-quote character +// within a quoted address is represented by by two consecutive +// double-quote characters. The serial number may be any eight +// character hexadecimal number, as long as it is unique - no two +// messages from a given system may have the same serial number +// within a three years. The manner in which this serial number is +// generated is left to the implementor." // // -// Examples & Implementations +// Examples & Implementations // -// Synchronet: .@ -// 2606.agora-agn_tst@46:1/142 19609217 +// Synchronet: .@ +// 2606.agora-agn_tst@46:1/142 19609217 // -// Mystic: -// 46:3/102 46686263 +// Mystic: +// 46:3/102 46686263 // -// ENiGMA½: .@<5dFtnAddress> +// ENiGMA½: .@<5dFtnAddress> // -// 0.0.8-alpha: -// Made compliant with FTN spec *when exporting NetMail* due to -// Mystic rejecting messages with the true-unique version. -// Strangely, Synchronet uses the unique format and Mystic does -// OK with it. Will need to research further. Note also that -// g00r00 was kind enough to fix Mystic to allow for the Sync/Enig -// format, but that will only help when using newer Mystic versions. +// 0.0.8-alpha: +// Made compliant with FTN spec *when exporting NetMail* due to +// Mystic rejecting messages with the true-unique version. +// Strangely, Synchronet uses the unique format and Mystic does +// OK with it. Will need to research further. Note also that +// g00r00 was kind enough to fix Mystic to allow for the Sync/Enig +// format, but that will only help when using newer Mystic versions. // function getMessageIdentifier(message, address, isNetMail = false) { const addrStr = new Address(address).toString('5D'); @@ -151,42 +151,42 @@ function getMessageIdentifier(message, address, isNetMail = false) { } // -// Return a FSC-0046.005 Product Identifier or "PID" -// http://ftsc.org/docs/fsc-0046.005 +// Return a FSC-0046.005 Product Identifier or "PID" +// http://ftsc.org/docs/fsc-0046.005 // -// Note that we use a variant on the spec for -// in which (; ; ) is used instead +// Note that we use a variant on the spec for +// in which (; ; ) is used instead // function getProductIdentifier() { const version = getCleanEnigmaVersion(); - const nodeVer = process.version.substr(1); // remove 'v' prefix + const nodeVer = process.version.substr(1); // remove 'v' prefix return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } // -// Return a FRL-1004 style time zone offset for a -// 'TZUTC' kludge line +// Return a FRL-1004 style time zone offset for a +// 'TZUTC' kludge line // -// http://ftsc.org/docs/frl-1004.002 +// http://ftsc.org/docs/frl-1004.002 // function getUTCTimeZoneOffset() { return moment().format('ZZ').replace(/\+/, ''); } // -// Get a FSC-0032 style quote prefix -// http://ftsc.org/docs/fsc-0032.001 +// Get a FSC-0032 style quote prefix +// http://ftsc.org/docs/fsc-0032.001 // function getQuotePrefix(name) { let initials; const parts = name.split(' '); if(parts.length > 1) { - // First & Last initials - (Bryan Ashby -> BA) + // First & Last initials - (Bryan Ashby -> BA) initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase(); } else { - // Just use the first two - (NuSkooler -> Nu) + // Just use the first two - (NuSkooler -> Nu) initials = _.capitalize(name.slice(0, 2)); } @@ -194,8 +194,8 @@ function getQuotePrefix(name) { } // -// Return a FTS-0004 Origin line -// http://ftsc.org/docs/fts-0004.001 +// Return a FTS-0004 Origin line +// http://ftsc.org/docs/fts-0004.001 // function getOrigin(address) { const config = Config(); @@ -208,38 +208,38 @@ function getOrigin(address) { } function getTearLine() { - const nodeVer = process.version.substr(1); // remove 'v' prefix + const nodeVer = process.version.substr(1); // remove 'v' prefix return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } // -// Return a FRL-1005.001 "Via" line -// http://ftsc.org/docs/frl-1005.001 +// Return a FRL-1005.001 "Via" line +// http://ftsc.org/docs/frl-1005.001 // function getVia(address) { /* - FRL-1005.001 states teh following format: + FRL-1005.001 states teh following format: - ^AVia: @YYYYMMDD.HHMMSS[.Precise][.Time Zone] - [Serial Number] - */ - const addrStr = new Address(address).toString('5D'); - const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); - const version = getCleanEnigmaVersion(); + ^AVia: @YYYYMMDD.HHMMSS[.Precise][.Time Zone] + [Serial Number] + */ + const addrStr = new Address(address).toString('5D'); + const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); + const version = getCleanEnigmaVersion(); return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; } // -// Creates a INTL kludge value as per FTS-4001 -// http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac +// Creates a INTL kludge value as per FTS-4001 +// http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac // function getIntl(toAddress, fromAddress) { // - // INTL differs from 'standard' kludges in that there is no ':' after "INTL" + // INTL differs from 'standard' kludges in that there is no ':' after "INTL" // - // ""INTL "" "" - // "...These addresses shall be given on the form :/" + // ""INTL "" "" + // "...These addresses shall be given on the form :/" // return `${toAddress.toString('3D')} ${fromAddress.toString('3D')}`; } @@ -258,11 +258,11 @@ function getAbbreviatedNetNodeList(netNodes) { abbrList += `${netNode.node} `; }); - return abbrList.trim(); // remove trailing space + return abbrList.trim(); // remove trailing space } // -// Parse an abbreviated net/node list commonly used for SEEN-BY and PATH +// Parse an abbreviated net/node list commonly used for SEEN-BY and PATH // function parseAbbreviatedNetNodeList(netNodes) { const re = /([0-9]+)\/([0-9]+)\s?|([0-9]+)\s?/g; @@ -282,39 +282,39 @@ function parseAbbreviatedNetNodeList(netNodes) { } // -// Return a FTS-0004.001 SEEN-BY entry(s) that include -// all pre-existing SEEN-BY entries with the addition -// of |additions|. +// Return a FTS-0004.001 SEEN-BY entry(s) that include +// all pre-existing SEEN-BY entries with the addition +// of |additions|. // -// See http://ftsc.org/docs/fts-0004.001 -// and notes at http://ftsc.org/docs/fsc-0043.002. +// See http://ftsc.org/docs/fts-0004.001 +// and notes at http://ftsc.org/docs/fsc-0043.002. // -// For a great write up, see http://www.skepticfiles.org/aj/basics03.htm +// For a great write up, see http://www.skepticfiles.org/aj/basics03.htm // -// This method returns an sorted array of values, but -// not the "SEEN-BY" prefix itself +// This method returns an sorted array of values, but +// not the "SEEN-BY" prefix itself // function getUpdatedSeenByEntries(existingEntries, additions) { /* - From FTS-0004: + From FTS-0004: - "There can be many seen-by lines at the end of Conference - Mail messages, and they are the real "meat" of the control - information. They are used to determine the systems to - receive the exported messages. The format of the line is: + "There can be many seen-by lines at the end of Conference + Mail messages, and they are the real "meat" of the control + information. They are used to determine the systems to + receive the exported messages. The format of the line is: - SEEN-BY: 132/101 113 136/601 1014/1 + SEEN-BY: 132/101 113 136/601 1014/1 - The net/node numbers correspond to the net/node numbers of - the systems having already received the message. In this way - a message is never sent to a system twice. In a conference - with many participants the number of seen-by lines can be - very large. This line is added if it is not already a part - of the message, or added to if it already exists, each time - a message is exported to other systems. This is a REQUIRED - field, and Conference Mail will not function correctly if - this field is not put in place by other Echomail compatible - programs." + The net/node numbers correspond to the net/node numbers of + the systems having already received the message. In this way + a message is never sent to a system twice. In a conference + with many participants the number of seen-by lines can be + very large. This line is added if it is not already a part + of the message, or added to if it already exists, each time + a message is exported to other systems. This is a REQUIRED + field, and Conference Mail will not function correctly if + this field is not put in place by other Echomail compatible + programs." */ existingEntries = existingEntries || []; if(!_.isArray(existingEntries)) { @@ -328,15 +328,15 @@ function getUpdatedSeenByEntries(existingEntries, additions) { additions = additions.sort(Address.getComparator()); // - // For now, we'll just append a new SEEN-BY entry + // For now, we'll just append a new SEEN-BY entry // - // :TODO: we should at least try and update what is already there in a smart way + // :TODO: we should at least try and update what is already there in a smart way existingEntries.push(getAbbreviatedNetNodeList(additions)); return existingEntries; } function getUpdatedPathEntries(existingEntries, localAddress) { - // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line + // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line existingEntries = existingEntries || []; if(!_.isArray(existingEntries)) { @@ -350,23 +350,23 @@ function getUpdatedPathEntries(existingEntries, localAddress) { } // -// Return FTS-5000.001 "CHRS" value -// http://ftsc.org/docs/fts-5003.001 +// Return FTS-5000.001 "CHRS" value +// http://ftsc.org/docs/fts-5003.001 // const ENCODING_TO_FTS_5003_001_CHARS = { - // level 1 - generally should not be used - ascii : [ 'ASCII', 1 ], - 'us-ascii' : [ 'ASCII', 1 ], + // level 1 - generally should not be used + ascii : [ 'ASCII', 1 ], + 'us-ascii' : [ 'ASCII', 1 ], - // level 2 - 8 bit, ASCII based - cp437 : [ 'CP437', 2 ], - cp850 : [ 'CP850', 2 ], + // level 2 - 8 bit, ASCII based + cp437 : [ 'CP437', 2 ], + cp850 : [ 'CP850', 2 ], - // level 3 - reserved + // level 3 - reserved - // level 4 - utf8 : [ 'UTF-8', 4 ], - 'utf-8' : [ 'UTF-8', 4 ], + // level 4 + utf8 : [ 'UTF-8', 4 ], + 'utf-8' : [ 'UTF-8', 4 ], }; @@ -378,47 +378,47 @@ function getCharacterSetIdentifierByEncoding(encodingName) { function getEncodingFromCharacterSetIdentifier(chrs) { const ident = chrs.split(' ')[0].toUpperCase(); - // :TODO: fill in the rest!!! + // :TODO: fill in the rest!!! return { - // level 1 - 'ASCII' : 'iso-646-1', - 'DUTCH' : 'iso-646', - 'FINNISH' : 'iso-646-10', - 'FRENCH' : 'iso-646', - 'CANADIAN' : 'iso-646', - 'GERMAN' : 'iso-646', - 'ITALIAN' : 'iso-646', - 'NORWEIG' : 'iso-646', - 'PORTU' : 'iso-646', - 'SPANISH' : 'iso-656', - 'SWEDISH' : 'iso-646-10', - 'SWISS' : 'iso-646', - 'UK' : 'iso-646', - 'ISO-10' : 'iso-646-10', + // level 1 + 'ASCII' : 'iso-646-1', + 'DUTCH' : 'iso-646', + 'FINNISH' : 'iso-646-10', + 'FRENCH' : 'iso-646', + 'CANADIAN' : 'iso-646', + 'GERMAN' : 'iso-646', + 'ITALIAN' : 'iso-646', + 'NORWEIG' : 'iso-646', + 'PORTU' : 'iso-646', + 'SPANISH' : 'iso-656', + 'SWEDISH' : 'iso-646-10', + 'SWISS' : 'iso-646', + 'UK' : 'iso-646', + 'ISO-10' : 'iso-646-10', - // level 2 - 'CP437' : 'cp437', - 'CP850' : 'cp850', - 'CP852' : 'cp852', - 'CP866' : 'cp866', - 'CP848' : 'cp848', - 'CP1250' : 'cp1250', - 'CP1251' : 'cp1251', - 'CP1252' : 'cp1252', - 'CP10000' : 'macroman', - 'LATIN-1' : 'iso-8859-1', - 'LATIN-2' : 'iso-8859-2', - 'LATIN-5' : 'iso-8859-9', - 'LATIN-9' : 'iso-8859-15', + // level 2 + 'CP437' : 'cp437', + 'CP850' : 'cp850', + 'CP852' : 'cp852', + 'CP866' : 'cp866', + 'CP848' : 'cp848', + 'CP1250' : 'cp1250', + 'CP1251' : 'cp1251', + 'CP1252' : 'cp1252', + 'CP10000' : 'macroman', + 'LATIN-1' : 'iso-8859-1', + 'LATIN-2' : 'iso-8859-2', + 'LATIN-5' : 'iso-8859-9', + 'LATIN-9' : 'iso-8859-15', - // level 4 - 'UTF-8' : 'utf8', + // level 4 + 'UTF-8' : 'utf8', - // deprecated stuff - 'IBMPC' : 'cp1250', // :TODO: validate - '+7_FIDO' : 'cp866', - '+7' : 'cp866', - 'MAC' : 'macroman', // :TODO: validate + // deprecated stuff + 'IBMPC' : 'cp1250', // :TODO: validate + '+7_FIDO' : 'cp866', + '+7' : 'cp866', + 'MAC' : 'macroman', // :TODO: validate }[ident]; } \ No newline at end of file diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index eb45d993..b7d0aba3 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -const MenuView = require('./menu_view.js').MenuView; -const strUtil = require('./string_util.js'); -const formatString = require('./string_format'); -const { pipeToAnsi } = require('./color_codes.js'); -const { goto } = require('./ansi_term.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const { pipeToAnsi } = require('./color_codes.js'); +const { goto } = require('./ansi_term.js'); -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); -exports.HorizontalMenuView = HorizontalMenuView; +exports.HorizontalMenuView = HorizontalMenuView; -// :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView) +// :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView) function HorizontalMenuView(options) { - options.cursor = options.cursor || 'hide'; + options.cursor = options.cursor || 'hide'; if(!_.isNumber(options.itemSpacing)) { options.itemSpacing = 1; @@ -23,7 +23,7 @@ function HorizontalMenuView(options) { MenuView.call(this, options); - this.dimens.height = 1; // always the case + this.dimens.height = 1; // always the case var self = this; @@ -33,8 +33,8 @@ function HorizontalMenuView(options) { this.performAutoScale = function() { if(self.autoScale.width) { - var spacer = self.getSpacer(); - var width = self.items.join(spacer).length + (spacer.length * 2); + var spacer = self.getSpacer(); + var width = self.items.join(spacer).length + (spacer.length * 2); assert(width <= self.client.term.termWidth - self.position.col); self.dimens.width = width; } @@ -44,8 +44,8 @@ function HorizontalMenuView(options) { this.cachePositions = function() { if(this.positionCacheExpired) { - var col = self.position.col; - var spacer = self.getSpacer(); + var col = self.position.col; + var spacer = self.getSpacer(); for(var i = 0; i < self.items.length; ++i) { self.items[i].col = col; @@ -90,7 +90,7 @@ require('util').inherits(HorizontalMenuView, MenuView); HorizontalMenuView.prototype.setHeight = function(height) { height = parseInt(height, 10); - assert(1 === height); // nothing else allowed here + assert(1 === height); // nothing else allowed here HorizontalMenuView.super_.prototype.setHeight(this, height); }; @@ -130,7 +130,7 @@ HorizontalMenuView.prototype.focusNext = function() { this.focusedItemIndex++; } - // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes + // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes this.redraw(); HorizontalMenuView.super_.prototype.focusNext.call(this); @@ -144,7 +144,7 @@ HorizontalMenuView.prototype.focusPrevious = function() { this.focusedItemIndex--; } - // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes + // :TODO: Optimize this in cases where we only need to redraw two items. Always the case now, somtimes this.redraw(); HorizontalMenuView.super_.prototype.focusPrevious.call(this); diff --git a/core/key_entry_view.js b/core/key_entry_view.js index 1d7ca905..0bab0ad5 100644 --- a/core/key_entry_view.js +++ b/core/key_entry_view.js @@ -1,12 +1,12 @@ /* jslint node: true */ 'use strict'; -const View = require('./view.js').View; -const valueWithDefault = require('./misc_util.js').valueWithDefault; -const isPrintable = require('./string_util.js').isPrintable; -const stylizeString = require('./string_util.js').stylizeString; +const View = require('./view.js').View; +const valueWithDefault = require('./misc_util.js').valueWithDefault; +const isPrintable = require('./string_util.js').isPrintable; +const stylizeString = require('./string_util.js').stylizeString; -const _ = require('lodash'); +const _ = require('lodash'); module.exports = class KeyEntryView extends View { constructor(options) { @@ -15,8 +15,8 @@ module.exports = class KeyEntryView extends View { super(options); - this.eatTabKey = options.eatTabKey || true; - this.caseInsensitive = options.caseInsensitive || true; + this.eatTabKey = options.eatTabKey || true; + this.caseInsensitive = options.caseInsensitive || true; if(Array.isArray(options.keys)) { if(this.caseInsensitive) { @@ -35,7 +35,7 @@ module.exports = class KeyEntryView extends View { } if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { - this.redraw(); // sets position + this.redraw(); // sets position this.client.term.write(stylizeString(ch, this.textStyle)); } @@ -46,7 +46,7 @@ module.exports = class KeyEntryView extends View { } this.emit('action', 'accept'); - // NOTE: we don't call super here. KeyEntryView is a special snowflake. + // NOTE: we don't call super here. KeyEntryView is a special snowflake. } setPropertyValue(propName, propValue) { @@ -73,5 +73,5 @@ module.exports = class KeyEntryView extends View { super.setPropertyValue(propName, propValue); } - getData() { return this.keyEntered; } + getData() { return this.keyEntered; } }; \ No newline at end of file diff --git a/core/last_callers.js b/core/last_callers.js index 88b0b716..52ec08f9 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -1,37 +1,37 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const StatLog = require('./stat_log.js'); -const User = require('./user.js'); -const stringFormat = require('./string_format.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const stringFormat = require('./string_format.js'); -// deps -const moment = require('moment'); -const async = require('async'); -const _ = require('lodash'); +// deps +const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); /* - Available listFormat object members: - userId - userName - location - affiliation - ts + Available listFormat object members: + userId + userName + location + affiliation + ts */ exports.moduleInfo = { - name : 'Last Callers', - desc : 'Last callers to the system', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.lastcallers' + name : 'Last Callers', + desc : 'Last callers to the system', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.lastcallers' }; const MciCodeIds = { - CallerList : 1, + CallerList : 1, }; exports.getModule = class LastCallersModule extends MenuModule { @@ -45,8 +45,8 @@ exports.getModule = class LastCallersModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); let loginHistory; let callersView; @@ -55,9 +55,9 @@ exports.getModule = class LastCallersModule extends MenuModule { [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, + callingMenu : self, + mciMap : mciData.menu, + noInput : true, }; vc.loadFromMenuConfig(loadOpts, callback); @@ -65,18 +65,18 @@ exports.getModule = class LastCallersModule extends MenuModule { function fetchHistory(callback) { callersView = vc.getView(MciCodeIds.CallerList); - // fetch up + // fetch up StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { loginHistory = lh; if(self.menuConfig.config.hideSysOpLogin) { const noOpLoginHistory = loginHistory.filter(lh => { - return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId + return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId }); // - // If we have enough items to display, or hideSysOpLogin is set to 'always', - // then set loginHistory to our filtered list. Else, we'll leave it be. + // If we have enough items to display, or hideSysOpLogin is set to 'always', + // then set loginHistory to our filtered list. Else, we'll leave it be. // if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) { loginHistory = noOpLoginHistory; @@ -84,7 +84,7 @@ exports.getModule = class LastCallersModule extends MenuModule { } // - // Finally, we need to trim up the list to the needed size + // Finally, we need to trim up the list to the needed size // loginHistory = loginHistory.slice(0, callersView.dimens.height); @@ -93,7 +93,7 @@ exports.getModule = class LastCallersModule extends MenuModule { }, function getUserNamesAndProperties(callback) { const getPropOpts = { - names : [ 'location', 'affiliation' ] + names : [ 'location', 'affiliation' ] }; const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; @@ -102,7 +102,7 @@ exports.getModule = class LastCallersModule extends MenuModule { loginHistory, (item, next) => { item.userId = parseInt(item.log_value); - item.ts = moment(item.timestamp).format(dateTimeFormat); + item.ts = moment(item.timestamp).format(dateTimeFormat); User.getUserName(item.userId, (err, userName) => { if(err) { @@ -113,11 +113,11 @@ exports.getModule = class LastCallersModule extends MenuModule { User.loadProperties(item.userId, getPropOpts, (err, props) => { if(!err && props) { - item.location = props.location || 'N/A'; - item.affiliation = item.affils = (props.affiliation || 'N/A'); + item.location = props.location || 'N/A'; + item.affiliation = item.affils = (props.affiliation || 'N/A'); } else { - item.location = 'N/A'; - item.affiliation = item.affils = 'N/A'; + item.location = 'N/A'; + item.affiliation = item.affils = 'N/A'; } return next(null); }); diff --git a/core/listening_server.js b/core/listening_server.js index 3678ff3f..00cf0a86 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -1,17 +1,17 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const logger = require('./logger.js'); +// ENiGMA½ +const logger = require('./logger.js'); -// deps -const async = require('async'); +// deps +const async = require('async'); -const listeningServers = {}; // packageName -> info +const listeningServers = {}; // packageName -> info -exports.startup = startup; -exports.shutdown = shutdown; -exports.getServer = getServer; +exports.startup = startup; +exports.shutdown = shutdown; +exports.getServer = getServer; function startup(cb) { return startListening(cb); @@ -26,11 +26,11 @@ function getServer(packageName) { } function startListening(cb) { - const moduleUtil = require('./module_util.js'); // late load so we get Config + const moduleUtil = require('./module_util.js'); // late load so we get Config async.each( [ 'login', 'content' ], (category, next) => { moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { - // :TODO: use enig error here! + // :TODO: use enig error here! if(err) { if('EENIGMODDISABLED' === err.code) { logger.log.debug(err.message); @@ -48,8 +48,8 @@ function startListening(cb) { } listeningServers[module.moduleInfo.packageName] = { - instance : moduleInst, - info : module.moduleInfo, + instance : moduleInst, + info : module.moduleInfo, }; } catch(e) { diff --git a/core/logger.js b/core/logger.js index 8b95c821..9a4e8711 100644 --- a/core/logger.js +++ b/core/logger.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -// deps -const bunyan = require('bunyan'); -const paths = require('path'); -const fs = require('graceful-fs'); -const _ = require('lodash'); +// deps +const bunyan = require('bunyan'); +const paths = require('path'); +const fs = require('graceful-fs'); +const _ = require('lodash'); module.exports = class Log { static init() { - const Config = require('./config.js').get(); - const logPath = Config.paths.logs; + const Config = require('./config.js').get(); + const logPath = Config.paths.logs; const err = this.checkLogPath(logPath); if(err) { - console.error(err.message); // eslint-disable-line no-console + console.error(err.message); // eslint-disable-line no-console return process.exit(); } @@ -26,18 +26,18 @@ module.exports = class Log { } const serializers = { - err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. + err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. }; - // try to remove sensitive info by default, e.g. 'password' fields + // try to remove sensitive info by default, e.g. 'password' fields [ 'formData', 'formValue' ].forEach(keyName => { serializers[keyName] = (fd) => Log.hideSensitive(fd); }); this.log = bunyan.createLogger({ - name : 'ENiGMA½ BBS', - streams : logStreams, - serializers : serializers, + name : 'ENiGMA½ BBS', + streams : logStreams, + serializers : serializers, }); } @@ -59,7 +59,7 @@ module.exports = class Log { static hideSensitive(obj) { try { // - // Use a regexp -- we don't know how nested fields we want to seek and destroy may be + // Use a regexp -- we don't know how nested fields we want to seek and destroy may be // return JSON.parse( JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { @@ -67,7 +67,7 @@ module.exports = class Log { }) ); } catch(e) { - // be safe and return empty obj! + // be safe and return empty obj! return {}; } } diff --git a/core/login_server_module.js b/core/login_server_module.js index ef4e712e..02cd4e41 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -1,27 +1,27 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const conf = require('./config.js'); -const logger = require('./logger.js'); -const ServerModule = require('./server_module.js').ServerModule; -const clientConns = require('./client_connections.js'); +// ENiGMA½ +const conf = require('./config.js'); +const logger = require('./logger.js'); +const ServerModule = require('./server_module.js').ServerModule; +const clientConns = require('./client_connections.js'); -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); module.exports = class LoginServerModule extends ServerModule { constructor() { super(); } - // :TODO: we need to max connections -- e.g. from config 'maxConnections' + // :TODO: we need to max connections -- e.g. from config 'maxConnections' prepareClient(client, cb) { const theme = require('./theme.js'); // - // Choose initial theme before we have user context + // Choose initial theme before we have user context // if('*' === conf.config.preLoginTheme) { client.user.properties.theme_id = theme.getRandomTheme() || ''; @@ -35,15 +35,15 @@ module.exports = class LoginServerModule extends ServerModule { handleNewClient(client, clientSock, modInfo) { // - // Start tracking the client. We'll assign it an ID which is - // just the index in our connections array. + // Start tracking the client. We'll assign it an ID which is + // just the index in our connections array. // if(_.isUndefined(client.session)) { client.session = {}; } - client.session.serverName = modInfo.name; - client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); + client.session.serverName = modInfo.name; + client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); clientConns.addNewClient(client, clientSock); @@ -51,7 +51,7 @@ module.exports = class LoginServerModule extends ServerModule { client.startIdleMonitor(); - // Go to module -- use default error handler + // Go to module -- use default error handler this.prepareClient(client, () => { require('./connect.js').connectEntry(client, readyOptions.firstMenu); }); @@ -77,7 +77,7 @@ module.exports = class LoginServerModule extends ServerModule { client.menuStack.goto('idleLogoff', err => { if(err) { - // likely just doesn't exist + // likely just doesn't exist client.term.write('\nIdle timeout expired. Goodbye!\n'); client.end(); } diff --git a/core/mail_packet.js b/core/mail_packet.js index 32b85a06..ce5b160a 100644 --- a/core/mail_packet.js +++ b/core/mail_packet.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -var events = require('events'); -var assert = require('assert'); -var _ = require('lodash'); +var events = require('events'); +var assert = require('assert'); +var _ = require('lodash'); module.exports = MailPacket; function MailPacket(options) { events.EventEmitter.call(this); - // map of network name -> address obj ( { zone, net, node, point, domain } ) + // map of network name -> address obj ( { zone, net, node, point, domain } ) this.nodeAddresses = options.nodeAddresses || {}; } @@ -18,19 +18,19 @@ require('util').inherits(MailPacket, events.EventEmitter); MailPacket.prototype.read = function(options) { // - // options.packetPath | opts.packetBuffer: supplies a path-to-file - // or a buffer containing packet data + // options.packetPath | opts.packetBuffer: supplies a path-to-file + // or a buffer containing packet data // - // emits 'message' event per message read + // emits 'message' event per message read // assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer)); }; MailPacket.prototype.write = function(options) { // - // options.messages[]: array of message(s) to create packets from + // options.messages[]: array of message(s) to create packets from // - // emits 'packet' event per packet constructed + // emits 'packet' event per packet constructed // assert(_.isArray(options.messages)); }; \ No newline at end of file diff --git a/core/mail_util.js b/core/mail_util.js index 6822e78e..6bd433d3 100644 --- a/core/mail_util.js +++ b/core/mail_util.js @@ -1,25 +1,25 @@ /* jslint node: true */ 'use strict'; -const Address = require('./ftn_address.js'); -const Message = require('./message.js'); +const Address = require('./ftn_address.js'); +const Message = require('./message.js'); -exports.getAddressedToInfo = getAddressedToInfo; +exports.getAddressedToInfo = getAddressedToInfo; const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /* - Input Output - ---------------------------------------------------------------------------------------------------- - User { name : 'User', flavor : 'local' } - Some User { name : 'Some User', flavor : 'local' } - JoeUser @ 1:103/75 { name : 'JoeUser', flavor : 'ftn', remote : '1:103/75' } - Bob@1:103/705@fidonet.org { name : 'Bob', flavor : 'ftn', remote : '1:103/705@fidonet.org' } - 1:103/705@fidonet.org { flavor : 'ftn', remote : '1:103/705@fidonet.org' } - Jane <23:4/100> { name : 'Jane', flavor : 'ftn', remote : '23:4/100' } - 43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' } - foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' } - Bar { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' } + Input Output + ---------------------------------------------------------------------------------------------------- + User { name : 'User', flavor : 'local' } + Some User { name : 'Some User', flavor : 'local' } + JoeUser @ 1:103/75 { name : 'JoeUser', flavor : 'ftn', remote : '1:103/75' } + Bob@1:103/705@fidonet.org { name : 'Bob', flavor : 'ftn', remote : '1:103/705@fidonet.org' } + 1:103/705@fidonet.org { flavor : 'ftn', remote : '1:103/705@fidonet.org' } + Jane <23:4/100> { name : 'Jane', flavor : 'ftn', remote : '23:4/100' } + 43:20/100.2 { flavor : 'ftn', remote : '43:20/100.2' } + foo@host.com { name : 'foo', flavor : 'email', remote : 'foo@host.com' } + Bar { name : 'Bar', flavor : 'email', remote : 'baz@foobar.com' } */ function getAddressedToInfo(input) { input = input.trim(); @@ -50,8 +50,8 @@ function getAddressedToInfo(input) { return { name : input, flavor : Message.AddressFlavor.Local }; } - const lessThanPos = input.indexOf('<'); - const greaterThanPos = input.indexOf('>'); + const lessThanPos = input.indexOf('<'); + const greaterThanPos = input.indexOf('>'); if(lessThanPos > 0 && greaterThanPos > lessThanPos) { const addr = input.slice(lessThanPos + 1, greaterThanPos); const m = addr.match(EMAIL_REGEX); @@ -67,7 +67,7 @@ function getAddressedToInfo(input) { return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input }; } - let addr = Address.fromString(input); // 5D? + let addr = Address.fromString(input); // 5D? if(Address.isValidAddress(addr)) { return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ; } diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index 417b7928..6cdf40b0 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -1,42 +1,42 @@ /* jslint node: true */ 'use strict'; -var TextView = require('./text_view.js').TextView; -var miscUtil = require('./misc_util.js'); -var strUtil = require('./string_util.js'); -var ansi = require('./ansi_term.js'); +var TextView = require('./text_view.js').TextView; +var miscUtil = require('./misc_util.js'); +var strUtil = require('./string_util.js'); +var ansi = require('./ansi_term.js'); -//var util = require('util'); -var assert = require('assert'); -var _ = require('lodash'); +//var util = require('util'); +var assert = require('assert'); +var _ = require('lodash'); -exports.MaskEditTextView = MaskEditTextView; +exports.MaskEditTextView = MaskEditTextView; -// ##/##/#### <--styleSGR2 if fillChar -// ^- styleSGR1 -// buildPattern -> [ RE, RE, '/', RE, RE, '/', RE, RE, RE, RE ] -// patternIndex -----^ +// ##/##/#### <--styleSGR2 if fillChar +// ^- styleSGR1 +// buildPattern -> [ RE, RE, '/', RE, RE, '/', RE, RE, RE, RE ] +// patternIndex -----^ -// styleSGR1: Literal's (non-focus) -// styleSGR2: Literals (focused) -// styleSGR3: fillChar +// styleSGR1: Literal's (non-focus) +// styleSGR2: Literals (focused) +// styleSGR3: fillChar // -// :TODO: -// * Hint, e.g. YYYY/MM/DD -// * Return values with literals in place +// :TODO: +// * Hint, e.g. YYYY/MM/DD +// * Return values with literals in place // function MaskEditTextView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); - options.resizable = false; + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); + options.resizable = false; TextView.call(this, options); - this.cursorPos = { x : 0 }; - this.patternArrayPos = 0; + this.cursorPos = { x : 0 }; + this.patternArrayPos = 0; var self = this; @@ -52,7 +52,7 @@ function MaskEditTextView(options) { assert(textToDraw.length <= self.patternArray.length); - // draw out the text we have so far + // draw out the text we have so far var i = 0; var t = 0; while(i < self.patternArray.length) { @@ -72,11 +72,11 @@ function MaskEditTextView(options) { }; this.buildPattern = function() { - self.patternArray = []; - self.maxLength = 0; + self.patternArray = []; + self.maxLength = 0; for(var i = 0; i < self.maskPattern.length; i++) { - // :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark! + // :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark! if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) { self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]); ++self.maxLength; @@ -97,16 +97,16 @@ function MaskEditTextView(options) { require('util').inherits(MaskEditTextView, TextView); MaskEditTextView.maskPatternCharacterRegEx = { - '#' : /[0-9]/, // Numeric - 'A' : /[a-zA-Z]/, // Alpha - '@' : /[0-9a-zA-Z]/, // Alphanumeric - '&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 + '#' : /[0-9]/, // Numeric + 'A' : /[a-zA-Z]/, // Alpha + '@' : /[0-9a-zA-Z]/, // Alphanumeric + '&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 }; MaskEditTextView.prototype.setText = function(text) { MaskEditTextView.super_.prototype.setText.call(this, text); - if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() + if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() this.patternArrayPos = this.patternArray.length; } }; @@ -143,9 +143,9 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { return; } else if(this.isKeyMapped('clearLine', key.name)) { - this.text = ''; - this.patternArrayPos = 0; - this.setFocus(true); // redraw + adjust cursor + this.text = ''; + this.patternArrayPos = 0; + this.setFocus(true); // redraw + adjust cursor return; } @@ -163,7 +163,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { this.patternArrayPos++; while(this.patternArrayPos < this.patternArray.length && - !_.isRegExp(this.patternArray[this.patternArrayPos])) + !_.isRegExp(this.patternArray[this.patternArrayPos])) { this.patternArrayPos++; } @@ -178,7 +178,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { MaskEditTextView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'maskPattern' : this.setMaskPattern(value); break; + case 'maskPattern' : this.setMaskPattern(value); break; } MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); @@ -191,7 +191,7 @@ MaskEditTextView.prototype.getData = function() { return rawData; } - var data = ''; + var data = ''; assert(rawData.length <= this.patternArray.length); diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 3bc333ae..86246d45 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -1,25 +1,25 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const TextView = require('./text_view.js').TextView; -const EditTextView = require('./edit_text_view.js').EditTextView; -const ButtonView = require('./button_view.js').ButtonView; -const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; -const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; -const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; -const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; -const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; -const KeyEntryView = require('./key_entry_view.js'); -const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; -const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; -const ansi = require('./ansi_term.js'); +// ENiGMA½ +const TextView = require('./text_view.js').TextView; +const EditTextView = require('./edit_text_view.js').EditTextView; +const ButtonView = require('./button_view.js').ButtonView; +const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; +const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; +const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; +const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; +const KeyEntryView = require('./key_entry_view.js'); +const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; +const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; +const ansi = require('./ansi_term.js'); -// deps -const assert = require('assert'); -const _ = require('lodash'); +// deps +const assert = require('assert'); +const _ = require('lodash'); -exports.MCIViewFactory = MCIViewFactory; +exports.MCIViewFactory = MCIViewFactory; function MCIViewFactory(client) { this.client = client; @@ -29,9 +29,9 @@ MCIViewFactory.UserViewCodes = [ 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', // - // XY is a special MCI code that allows finding positions - // and counts for key lookup, but does not explicitly - // represent a visible View on it's own + // XY is a special MCI code that allows finding positions + // and counts for key lookup, but does not explicitly + // represent a visible View on it's own // 'XY', ]; @@ -43,14 +43,14 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { var view; var options = { - client : this.client, - id : mci.id, - ansiSGR : mci.SGR, - ansiFocusSGR : mci.focusSGR, - position : { row : mci.position[0], col : mci.position[1] }, + client : this.client, + id : mci.id, + ansiSGR : mci.SGR, + ansiFocusSGR : mci.focusSGR, + position : { row : mci.position[0], col : mci.position[1] }, }; - // :TODO: These should use setPropertyValue()! + // :TODO: These should use setPropertyValue()! function setOption(pos, name) { if(mci.args.length > pos && mci.args[pos].length > 0) { options[name] = mci.args[pos]; @@ -73,44 +73,44 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { } // - // Note: Keep this in sync with UserViewCodes above! + // Note: Keep this in sync with UserViewCodes above! // switch(mci.code) { - // Text Label (Text View) + // Text Label (Text View) case 'TL' : - setOption(0, 'textStyle'); - setOption(1, 'justify'); + setOption(0, 'textStyle'); + setOption(1, 'justify'); setWidth(2); view = new TextView(options); break; - // Edit Text + // Edit Text case 'ET' : setWidth(0); - setOption(1, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setOption(1, 'textStyle'); + setFocusOption(0, 'focusTextStyle'); view = new EditTextView(options); break; - // Masked Edit Text + // Masked Edit Text case 'ME' : - setOption(0, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setOption(0, 'textStyle'); + setFocusOption(0, 'focusTextStyle'); view = new MaskEditTextView(options); break; - // Multi Line Edit Text + // Multi Line Edit Text case 'MT' : - // :TODO: apply params + // :TODO: apply params view = new MultiLineEditTextView(options); break; - // Pre-defined Label (Text View) - // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove + // Pre-defined Label (Text View) + // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove case 'PL' : if(mci.args.length > 0) { options.text = getPredefinedMCIValue(this.client, mci.args[0]); @@ -124,7 +124,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { } break; - // Button + // Button case 'BT' : if(mci.args.length > 0) { options.dimens = { width : parseInt(mci.args[0], 10) }; @@ -138,32 +138,32 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new ButtonView(options); break; - // Vertial Menu + // Vertial Menu case 'VM' : - setOption(0, 'itemSpacing'); - setOption(1, 'justify'); - setOption(2, 'textStyle'); + setOption(0, 'itemSpacing'); + setOption(1, 'justify'); + setOption(2, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); view = new VerticalMenuView(options); break; - // Horizontal Menu + // Horizontal Menu case 'HM' : - setOption(0, 'itemSpacing'); - setOption(1, 'textStyle'); + setOption(0, 'itemSpacing'); + setOption(1, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); view = new HorizontalMenuView(options); break; case 'SM' : - setOption(0, 'textStyle'); - setOption(1, 'justify'); + setOption(0, 'textStyle'); + setOption(1, 'justify'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); view = new SpinnerMenuView(options); break; @@ -177,7 +177,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true); } - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); view = new ToggleMenuView(options); break; @@ -191,8 +191,8 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { if(_.isString(options.text)) { setWidth(0); - setOption(1, 'textStyle'); - setOption(2, 'justify'); + setOption(1, 'textStyle'); + setOption(2, 'justify'); view = new TextView(options); } diff --git a/core/menu_module.js b/core/menu_module.js index 520d30e9..2a3bf23b 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -1,37 +1,37 @@ /* jslint node: true */ 'use strict'; -const PluginModule = require('./plugin_module.js').PluginModule; -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const ViewController = require('./view_controller.js').ViewController; -const menuUtil = require('./menu_util.js'); -const Config = require('./config.js').get; -const stringFormat = require('../core/string_format.js'); -const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; -const Errors = require('../core/enig_error.js').Errors; -const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); +const PluginModule = require('./plugin_module.js').PluginModule; +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const ViewController = require('./view_controller.js').ViewController; +const menuUtil = require('./menu_util.js'); +const Config = require('./config.js').get; +const stringFormat = require('../core/string_format.js'); +const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; +const Errors = require('../core/enig_error.js').Errors; +const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); -// deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); +// deps +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); exports.MenuModule = class MenuModule extends PluginModule { constructor(options) { super(options); - this.menuName = options.menuName; - this.menuConfig = options.menuConfig; - this.client = options.client; - this.menuConfig.options = options.menuConfig.options || {}; - this.menuMethods = {}; // methods called from @method's - this.menuConfig.config = this.menuConfig.config || {}; + this.menuName = options.menuName; + this.menuConfig = options.menuConfig; + this.client = options.client; + this.menuConfig.options = options.menuConfig.options || {}; + this.menuMethods = {}; // methods called from @method's + this.menuConfig.config = this.menuConfig.config || {}; this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; - this.viewControllers = {}; + this.viewControllers = {}; } enter() { @@ -43,8 +43,8 @@ exports.MenuModule = class MenuModule extends PluginModule { } initSequence() { - const self = this; - const mciData = {}; + const self = this; + const mciData = {}; let pausePosition; async.series( @@ -67,13 +67,13 @@ exports.MenuModule = class MenuModule extends PluginModule { mciData.menu = artData.mciMap; } - return callback(null); // any errors are non-fatal + return callback(null); // any errors are non-fatal } ); }, function moveToPromptLocation(callback) { if(self.menuConfig.prompt) { - // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements + // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements } return callback(null); @@ -94,13 +94,13 @@ exports.MenuModule = class MenuModule extends PluginModule { if(artData) { mciData.prompt = artData.mciMap; } - return callback(err); // pass err here; prompts *must* have art + return callback(err); // pass err here; prompts *must* have art } ); }, function recordCursorPosition(callback) { if(!self.shouldPause()) { - return callback(null); // cursor position not needed + return callback(null); // cursor position not needed } self.client.once('cursor position report', pos => { @@ -138,7 +138,7 @@ exports.MenuModule = class MenuModule extends PluginModule { beforeArt(cb) { if(_.isNumber(this.menuConfig.options.baudRate)) { - // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here + // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); } @@ -150,30 +150,30 @@ exports.MenuModule = class MenuModule extends PluginModule { } mciReady(mciData, cb) { - // available for sub-classes + // available for sub-classes return cb(null); } finishedLoading() { - // nothing in base + // nothing in base } getSaveState() { - // nothing in base + // nothing in base } restoreSavedState(/*savedState*/) { - // nothing in base + // nothing in base } getMenuResult() { - // default to the formData that was provided @ a submit, if any + // default to the formData that was provided @ a submit, if any return this.submitFormData; } nextMenu(cb) { if(!this.haveNext()) { - return this.prevMenu(cb); // no next, go to prev + return this.prevMenu(cb); // no next, go to prev } return this.client.menuStack.next(cb); @@ -236,10 +236,10 @@ exports.MenuModule = class MenuModule extends PluginModule { standardMCIReadyHandler(mciData, cb) { // - // A quick rundown: - // * We may have mciData.menu, mciData.prompt, or both. - // * Prompt form is favored over menu form if both are present. - // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) + // A quick rundown: + // * We may have mciData.menu, mciData.prompt, or both. + // * Prompt form is favored over menu form if both are present. + // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) // const self = this; @@ -259,9 +259,9 @@ exports.MenuModule = class MenuModule extends PluginModule { } const menuLoadOpts = { - mciMap : mciData.menu, - callingMenu : self, - withoutForm : _.isObject(mciData.prompt), + mciMap : mciData.menu, + callingMenu : self, + withoutForm : _.isObject(mciData.prompt), }; self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { @@ -274,8 +274,8 @@ exports.MenuModule = class MenuModule extends PluginModule { } const promptLoadOpts = { - callingMenu : self, - mciMap : mciData.prompt, + callingMenu : self, + mciMap : mciData.prompt, }; self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { @@ -314,16 +314,16 @@ exports.MenuModule = class MenuModule extends PluginModule { prepViewController(name, formId, mciMap, cb) { if(_.isUndefined(this.viewControllers[name])) { const vcOpts = { - client : this.client, - formId : formId, + client : this.client, + formId : formId, }; const vc = this.addViewController(name, new ViewController(vcOpts)); const loadOpts = { - callingMenu : this, - mciMap : mciMap, - formId : formId, + callingMenu : this, + mciMap : mciMap, + formId : formId, }; return vc.loadFromMenuConfig(loadOpts, err => { @@ -371,20 +371,20 @@ exports.MenuModule = class MenuModule extends PluginModule { } /* - :TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... ) - promptForInput(formName, name, options, cb) { - if(!cb && _.isFunction(options)) { - cb = options; - options = {}; - } + :TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... ) + promptForInput(formName, name, options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } - options.viewController = this.viewControllers[formName]; + options.viewController = this.viewControllers[formName]; - this.optionalMoveToPosition(options.position); + this.optionalMoveToPosition(options.position); - return theme.displayThemedPrompt(name, this.client, options, cb); - } - */ + return theme.displayThemedPrompt(name, this.client, options, cb); + } + */ setViewText(formName, mciId, text, appendMultiLine) { const view = this.viewControllers[formName].getView(mciId); @@ -404,12 +404,12 @@ exports.MenuModule = class MenuModule extends PluginModule { let textView; let customMciId = startId; - const config = this.menuConfig.config; - const endId = options.endId || 99; // we'll fail to get a view before 99 + const config = this.menuConfig.config; + const endId = options.endId || 99; // we'll fail to get a view before 99 while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { - const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" - const format = config[key]; + const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" + const format = config[key]; if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { const text = stringFormat(format, fmtObj); diff --git a/core/menu_stack.js b/core/menu_stack.js index 90269bcb..5ab5a091 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -1,20 +1,20 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const loadMenu = require('./menu_util.js').loadMenu; -const Errors = require('./enig_error.js').Errors; +// ENiGMA½ +const loadMenu = require('./menu_util.js').loadMenu; +const Errors = require('./enig_error.js').Errors; -// deps -const _ = require('lodash'); -const assert = require('assert'); +// deps +const _ = require('lodash'); +const assert = require('assert'); -// :TODO: Stack is backwards.... top should be most recent! :) +// :TODO: Stack is backwards.... top should be most recent! :) module.exports = class MenuStack { constructor(client) { - this.client = client; - this.stack = []; + this.client = client; + this.stack = []; } push(moduleInfo) { @@ -52,8 +52,8 @@ module.exports = class MenuStack { const currentModuleInfo = this.top(); assert(currentModuleInfo, 'Empty menu stack!'); - const menuConfig = currentModuleInfo.instance.menuConfig; - const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); + const menuConfig = currentModuleInfo.instance.menuConfig; + const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); if(!nextMenu) { return cb(Array.isArray(menuConfig.next) ? Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') : @@ -71,16 +71,16 @@ module.exports = class MenuStack { prev(cb) { const menuResult = this.top().instance.getMenuResult(); - // :TODO: leave() should really take a cb... - this.pop().instance.leave(); // leave & remove current + // :TODO: leave() should really take a cb... + this.pop().instance.leave(); // leave & remove current - const previousModuleInfo = this.pop(); // get previous + const previousModuleInfo = this.pop(); // get previous if(previousModuleInfo) { const opts = { - extraArgs : previousModuleInfo.extraArgs, - savedState : previousModuleInfo.savedState, - lastMenuResult : menuResult, + extraArgs : previousModuleInfo.extraArgs, + savedState : previousModuleInfo.savedState, + lastMenuResult : menuResult, }; return this.goto(previousModuleInfo.name, opts, cb); @@ -108,8 +108,8 @@ module.exports = class MenuStack { } const loadOpts = { - name : name, - client : self.client, + name : name, + client : self.client, }; if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { @@ -117,19 +117,19 @@ module.exports = class MenuStack { } else { loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); } - loadOpts.lastMenuResult = options.lastMenuResult; + loadOpts.lastMenuResult = options.lastMenuResult; loadMenu(loadOpts, (err, modInst) => { if(err) { - // :TODO: probably should just require a cb... + // :TODO: probably should just require a cb... const errCb = cb || self.client.defaultHandlerMissingMod(); errCb(err); } else { self.client.log.debug( { menuName : name }, 'Goto menu module'); // - // If menuFlags were supplied in menu.hjson, they should win over - // anything supplied in code. + // If menuFlags were supplied in menu.hjson, they should win over + // anything supplied in code. // let menuFlags; if(0 === modInst.menuConfig.options.menuFlags.length) { @@ -137,14 +137,14 @@ module.exports = class MenuStack { } else { menuFlags = modInst.menuConfig.options.menuFlags; - // in code we can ask to merge in + // in code we can ask to merge in if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); } } if(currentModuleInfo) { - // save stack state + // save stack state currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); currentModuleInfo.instance.leave(); @@ -154,18 +154,18 @@ module.exports = class MenuStack { } if(menuFlags.includes('popParent')) { - this.pop().instance.leave(); // leave & remove current + this.pop().instance.leave(); // leave & remove current } } self.push({ - name : name, - instance : modInst, - extraArgs : loadOpts.extraArgs, - menuFlags : menuFlags, + name : name, + instance : modInst, + extraArgs : loadOpts.extraArgs, + menuFlags : menuFlags, }); - // restore previous state if requested + // restore previous state if requested if(options.savedState) { modInst.restoreSavedState(options.savedState); } diff --git a/core/menu_util.js b/core/menu_util.js index 76205d3f..f362041c 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -1,22 +1,22 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -var moduleUtil = require('./module_util.js'); -var Log = require('./logger.js').log; -var Config = require('./config.js').get; -var asset = require('./asset.js'); -var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; +// ENiGMA½ +var moduleUtil = require('./module_util.js'); +var Log = require('./logger.js').log; +var Config = require('./config.js').get; +var asset = require('./asset.js'); +var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; -var paths = require('path'); -var async = require('async'); -var assert = require('assert'); -var _ = require('lodash'); +var paths = require('path'); +var async = require('async'); +var assert = require('assert'); +var _ = require('lodash'); -exports.loadMenu = loadMenu; -exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; -exports.handleAction = handleAction; -exports.handleNext = handleNext; +exports.loadMenu = loadMenu; +exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; +exports.handleAction = handleAction; +exports.handleNext = handleNext; function getMenuConfig(client, name, cb) { var menuConfig; @@ -70,20 +70,20 @@ function loadMenu(options, cb) { menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ]; } - const modAsset = asset.getModuleAsset(menuConfig.module); - const modSupplied = null !== modAsset; + const modAsset = asset.getModuleAsset(menuConfig.module); + const modSupplied = null !== modAsset; const modLoadOpts = { - name : modSupplied ? modAsset.asset : 'standard_menu', - path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, - category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', + name : modSupplied ? modAsset.asset : 'standard_menu', + path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, + category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', }; moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => { const modData = { - name : modLoadOpts.name, - config : menuConfig, - mod : mod, + name : modLoadOpts.name, + config : menuConfig, + mod : mod, }; return callback(err, modData); @@ -97,11 +97,11 @@ function loadMenu(options, cb) { let moduleInstance; try { moduleInstance = new modData.mod.getModule({ - menuName : options.name, - menuConfig : modData.config, - extraArgs : options.extraArgs, - client : options.client, - lastMenuResult : options.lastMenuResult, + menuName : options.name, + menuConfig : modData.config, + extraArgs : options.extraArgs, + client : options.client, + lastMenuResult : options.lastMenuResult, }); } catch(e) { return callback(e); @@ -137,7 +137,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key'); // - // Exact, explicit match? + // Exact, explicit match? // if(_.isObject(formForId[mciReqKey])) { Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); @@ -146,7 +146,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { } // - // Generic match + // Generic match // if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { Log.trace('Using generic configuration'); @@ -156,7 +156,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\'')); } -// :TODO: Most of this should be moved elsewhere .... DRY... +// :TODO: Most of this should be moved elsewhere .... DRY... function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) { if('' === paths.extname(path)) { path += '.js'; @@ -194,8 +194,8 @@ function handleAction(client, formData, conf, cb) { conf.extraArgs, cb); } else if('systemMethod' === actionAsset.type) { - // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () - // :TODO: Probably better as system_method.js + // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () + // :TODO: Probably better as system_method.js return callModuleMenuMethod( client, actionAsset, @@ -204,7 +204,7 @@ function handleAction(client, formData, conf, cb) { conf.extraArgs, cb); } else { - // local to current module + // local to current module const currentModule = client.currentMenuModule; if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); @@ -221,28 +221,28 @@ function handleAction(client, formData, conf, cb) { } function handleNext(client, nextSpec, conf, cb) { - nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals + nextSpec = client.acs.getConditionalValue(nextSpec, 'next'); // handle any conditionals const nextAsset = asset.getAssetWithShorthand(nextSpec, 'menu'); - // :TODO: getAssetWithShorthand() can return undefined - handle it! + // :TODO: getAssetWithShorthand() can return undefined - handle it! conf = conf || {}; const extraArgs = conf.extraArgs || {}; - // :TODO: DRY this with handleAction() + // :TODO: DRY this with handleAction() switch(nextAsset.type) { case 'method' : case 'systemMethod' : if(_.isString(nextAsset.location)) { return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb); } else if('systemMethod' === nextAsset.type) { - // :TODO: see other notes about system_menu_method.js here + // :TODO: see other notes about system_menu_method.js here return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb); } else { - // local to current module + // local to current module const currentModule = client.currentMenuModule; if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) { - const formData = {}; // we don't have any + const formData = {}; // we don't have any return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb ); } diff --git a/core/menu_view.js b/core/menu_view.js index 78bf2c87..73e56a4a 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -1,17 +1,17 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +// ENiGMA½ +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; -// deps -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +// deps +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuView = MenuView; +exports.MenuView = MenuView; function MenuView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); @@ -38,14 +38,14 @@ function MenuView(options) { this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; + // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + this.justify = options.justify || 'none'; this.hasFocusItems = function() { return !_.isUndefined(self.focusItems); @@ -74,15 +74,15 @@ MenuView.prototype.setItems = function(items) { this.renderCache = {}; // - // Items can be an array of strings or an array of objects. + // Items can be an array of strings or an array of objects. // - // In the case of objects, items are considered complex and - // may have one or more members that can later be formatted - // against. The default member is 'text'. The member 'data' - // may be overridden to provide a form value other than the - // item's index. + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. // - // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // Items can be formatted with 'itemFormat' and 'focusItemFormat' // let text; let stringItem; @@ -96,7 +96,7 @@ MenuView.prototype.setItems = function(items) { } text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others }); if(this.complexItems) { @@ -122,7 +122,7 @@ MenuView.prototype.setSort = function(sort) { const key = true === sort ? 'text' : sort; if('text' !== sort && !this.complexItems) { - return; // need a valid sort key + return; // need a valid sort key } this.items.sort( (a, b) => { @@ -237,26 +237,26 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { itemSpacing = parseInt(itemSpacing); assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'itemSpacing' : this.setItemSpacing(value); break; - case 'items' : this.setItems(value); break; - case 'focusItems' : this.setFocusItems(value); break; - case 'hotKeys' : this.setHotKeys(value); break; - case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; - case 'focusItemIndex' : this.focusedItemIndex = value; break; + case 'itemSpacing' : this.setItemSpacing(value); break; + case 'items' : this.setItems(value); break; + case 'focusItems' : this.setFocusItems(value); break; + case 'hotKeys' : this.setHotKeys(value); break; + case 'hotKeySubmit' : this.hotKeySubmit = value; break; + case 'justify' : this.justify = value; break; + case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'itemFormat' : case 'focusItemFormat' : this[propName] = value; break; - case 'sort' : this.setSort(value); break; + case 'sort' : this.setSort(value); break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); diff --git a/core/message.js b/core/message.js index 277f5781..53b82e73 100644 --- a/core/message.js +++ b/core/message.js @@ -1,96 +1,96 @@ /* jslint node: true */ 'use strict'; -const msgDb = require('./database.js').dbs.message; -const wordWrapText = require('./word_wrap.js').wordWrapText; -const ftnUtil = require('./ftn_util.js'); -const createNamedUUID = require('./uuid_util.js').createNamedUUID; -const Errors = require('./enig_error.js').Errors; -const ANSI = require('./ansi_term.js'); +const msgDb = require('./database.js').dbs.message; +const wordWrapText = require('./word_wrap.js').wordWrapText; +const ftnUtil = require('./ftn_util.js'); +const createNamedUUID = require('./uuid_util.js').createNamedUUID; +const Errors = require('./enig_error.js').Errors; +const ANSI = require('./ansi_term.js'); const { sanatizeString, - getISOTimestampString } = require('./database.js'); + getISOTimestampString } = require('./database.js'); const { isAnsi, isFormattedLine, splitTextAtTerms, renderSubstr -} = require('./string_util.js'); +} = require('./string_util.js'); -const ansiPrep = require('./ansi_prep.js'); +const ansiPrep = require('./ansi_prep.js'); -// deps -const uuidParse = require('uuid-parse'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); -const moment = require('moment'); -const iconvEncode = require('iconv-lite').encode; +// deps +const uuidParse = require('uuid-parse'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); +const moment = require('moment'); +const iconvEncode = require('iconv-lite').encode; -const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); +const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); const WELL_KNOWN_AREA_TAGS = { - Invalid : '', - Private : 'private_mail', - Bulletin : 'local_bulletin', + Invalid : '', + Private : 'private_mail', + Bulletin : 'local_bulletin', }; const SYSTEM_META_NAMES = { - LocalToUserID : 'local_to_user_id', - LocalFromUserID : 'local_from_user_id', - StateFlags0 : 'state_flags0', // See Message.StateFlags0 - ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. - ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor - RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address - RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address + LocalToUserID : 'local_to_user_id', + LocalFromUserID : 'local_from_user_id', + StateFlags0 : 'state_flags0', // See Message.StateFlags0 + ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. + ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor + RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address + RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address }; -// Types for Message.SystemMetaNames.ExternalFlavor meta +// Types for Message.SystemMetaNames.ExternalFlavor meta const ADDRESS_FLAVOR = { - Local : 'local', // local / non-remote addressing - FTN : 'ftn', // FTN style - Email : 'email', + Local : 'local', // local / non-remote addressing + FTN : 'ftn', // FTN style + Email : 'email', }; const STATE_FLAGS0 = { - None : 0x00000000, - Imported : 0x00000001, // imported from foreign system - Exported : 0x00000002, // exported to foreign system + None : 0x00000000, + Imported : 0x00000001, // imported from foreign system + Exported : 0x00000002, // exported to foreign system }; -// :TODO: these should really live elsewhere... +// :TODO: these should really live elsewhere... const FTN_PROPERTY_NAMES = { - // packet header oriented - FtnOrigNode : 'ftn_orig_node', - FtnDestNode : 'ftn_dest_node', - // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping - FtnOrigNetwork : 'ftn_orig_network', - FtnDestNetwork : 'ftn_dest_network', - FtnAttrFlags : 'ftn_attr_flags', - FtnCost : 'ftn_cost', - FtnOrigZone : 'ftn_orig_zone', - FtnDestZone : 'ftn_dest_zone', - FtnOrigPoint : 'ftn_orig_point', - FtnDestPoint : 'ftn_dest_point', + // packet header oriented + FtnOrigNode : 'ftn_orig_node', + FtnDestNode : 'ftn_dest_node', + // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping + FtnOrigNetwork : 'ftn_orig_network', + FtnDestNetwork : 'ftn_dest_network', + FtnAttrFlags : 'ftn_attr_flags', + FtnCost : 'ftn_cost', + FtnOrigZone : 'ftn_orig_zone', + FtnDestZone : 'ftn_dest_zone', + FtnOrigPoint : 'ftn_orig_point', + FtnDestPoint : 'ftn_dest_point', - // message header oriented - FtnMsgOrigNode : 'ftn_msg_orig_node', - FtnMsgDestNode : 'ftn_msg_dest_node', - FtnMsgOrigNet : 'ftn_msg_orig_net', - FtnMsgDestNet : 'ftn_msg_dest_net', + // message header oriented + FtnMsgOrigNode : 'ftn_msg_orig_node', + FtnMsgDestNode : 'ftn_msg_dest_node', + FtnMsgOrigNet : 'ftn_msg_orig_net', + FtnMsgDestNet : 'ftn_msg_dest_net', - FtnAttribute : 'ftn_attribute', + FtnAttribute : 'ftn_attribute', - FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 - FtnOrigin : 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 - FtnArea : 'ftn_area', // http://ftsc.org/docs/fts-0004.001 - FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 + FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 + FtnOrigin : 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 + FtnArea : 'ftn_area', // http://ftsc.org/docs/fts-0004.001 + FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 }; -// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! +// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! const MESSAGE_ROW_MAP = { - reply_to_message_id : 'replyToMsgId', - modified_timestamp : 'modTimestamp' + reply_to_message_id : 'replyToMsgId', + modified_timestamp : 'modTimestamp' }; module.exports = class Message { @@ -102,28 +102,28 @@ module.exports = class Message { } = { } ) { - this.messageId = messageId; - this.areaTag = areaTag; - this.uuid = uuid; - this.replyToMsgId = replyToMsgId; - this.toUserName = toUserName; - this.fromUserName = fromUserName; - this.subject = subject; - this.message = message; + this.messageId = messageId; + this.areaTag = areaTag; + this.uuid = uuid; + this.replyToMsgId = replyToMsgId; + this.toUserName = toUserName; + this.fromUserName = fromUserName; + this.subject = subject; + this.message = message; if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { modTimestamp = moment(modTimestamp); } - this.modTimestamp = modTimestamp; + this.modTimestamp = modTimestamp; this.meta = {}; _.defaultsDeep(this.meta, { System : {} }, meta); - this.hashTags = hashTags; + this.hashTags = hashTags; } - isValid() { return true; } // :TODO: obviously useless; look into this or remove it + isValid() { return true; } // :TODO: obviously useless; look into this or remove it static isPrivateAreaTag(areaTag) { return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; @@ -187,10 +187,10 @@ module.exports = class Message { modTimestamp = moment(modTimestamp); } - areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); - modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); - subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); - body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); + areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); + modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); + subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); + body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); } @@ -198,7 +198,7 @@ module.exports = class Message { static getMessageFromRow(row) { const msg = {}; _.each(row, (v, k) => { - // :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()! + // :TODO: see notes around MESSAGE_ROW_MAP -- clean this up so we can just _camelCase()! k = MESSAGE_ROW_MAP[k] || _.camelCase(k); msg[k] = v; }); @@ -206,38 +206,38 @@ module.exports = class Message { } /* - Find message IDs or UUIDs by filter. Available filters/options: + Find message IDs or UUIDs by filter. Available filters/options: - filter.uuids - use with resultType='id' - filter.ids - use with resultType='uuid' - filter.toUserName - filter.fromUserName - filter.replyToMesageId - filter.newerThanTimestamp - filter.newerThanMessageId - filter.areaTag - note if you want by conf, send in all areas for a conf - *filter.metaTuples - {category, name, value} + filter.uuids - use with resultType='id' + filter.ids - use with resultType='uuid' + filter.toUserName + filter.fromUserName + filter.replyToMesageId + filter.newerThanTimestamp + filter.newerThanMessageId + filter.areaTag - note if you want by conf, send in all areas for a conf + *filter.metaTuples - {category, name, value} - filter.terms - FTS search + filter.terms - FTS search - filter.sort = modTimestamp | messageId - filter.order = ascending | (descending) + filter.sort = modTimestamp | messageId + filter.order = ascending | (descending) - filter.limit - filter.resultType = (id) | uuid | count - filter.extraFields = [] + filter.limit + filter.resultType = (id) | uuid | count + filter.extraFields = [] - filter.privateTagUserId = - if set, only private messages belonging to are processed - - any other areaTag or confTag filters will be ignored - - if NOT present, private areas are skipped + filter.privateTagUserId = - if set, only private messages belonging to are processed + - any other areaTag or confTag filters will be ignored + - if NOT present, private areas are skipped - *=NYI - */ + *=NYI + */ static findMessages(filter, cb) { filter = filter || {}; - filter.resultType = filter.resultType || 'id'; - filter.extraFields = filter.extraFields || []; + filter.resultType = filter.resultType || 'id'; + filter.extraFields = filter.extraFields || []; if('messageList' === filter.resultType) { filter.extraFields = _.uniq(filter.extraFields.concat( @@ -254,13 +254,13 @@ module.exports = class Message { let sql; if('count' === filter.resultType) { sql = - `SELECT COUNT() AS count - FROM message m`; + `SELECT COUNT() AS count + FROM message m`; } else { sql = - `SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} - FROM message m`; + `SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} + FROM message m`; } const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; @@ -276,7 +276,7 @@ module.exports = class Message { sqlWhere += clause; } - // currently only avail sort + // currently only avail sort if('modTimestamp' === filter.sort) { sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`; } else { @@ -297,10 +297,10 @@ module.exports = class Message { appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); appendWhereClause( `m.message_id IN ( - SELECT message_id - FROM message_meta - WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} - )`); + SELECT message_id + FROM message_meta + WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} + )`); } else { if(filter.areaTag && filter.areaTag.length > 0) { if(Array.isArray(filter.areaTag)) { @@ -315,7 +315,7 @@ module.exports = class Message { } } - // explicit exclude of Private + // explicit exclude of Private appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`); } @@ -338,13 +338,13 @@ module.exports = class Message { } if(filter.terms && filter.terms.length > 0) { - // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex appendWhereClause( `m.message_id IN ( - SELECT rowid - FROM message_fts - WHERE message_fts MATCH ":${sanatizeString(filter.terms)}" - )` + SELECT rowid + FROM message_fts + WHERE message_fts MATCH ":${sanatizeString(filter.terms)}" + )` ); } @@ -376,13 +376,13 @@ module.exports = class Message { } } - // :TODO: use findMessages, by uuid, limit=1 + // :TODO: use findMessages, by uuid, limit=1 static getMessageIdByUuid(uuid, cb) { msgDb.get( `SELECT message_id - FROM message - WHERE message_uuid = ? - LIMIT 1;`, + FROM message + WHERE message_uuid = ? + LIMIT 1;`, [ uuid ], (err, row) => { if(err) { @@ -398,27 +398,27 @@ module.exports = class Message { ); } - // :TODO: use findMessages + // :TODO: use findMessages static getMessageIdsByMetaValue(category, name, value, cb) { msgDb.all( `SELECT message_id - FROM message_meta - WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, + FROM message_meta + WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, [ category, name, value ], (err, rows) => { if(err) { return cb(err); } - return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) + return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) } ); } static getMetaValuesByMessageId(messageId, category, name, cb) { const sql = - `SELECT meta_value - FROM message_meta - WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; + `SELECT meta_value + FROM message_meta + WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; msgDb.all(sql, [ messageId, category, name ], (err, rows) => { if(err) { @@ -429,12 +429,12 @@ module.exports = class Message { return cb(Errors.DoesNotExist('No value for category/name')); } - // single values are returned without an array + // single values are returned without an array if(1 === rows.length) { return cb(null, rows[0].meta_value); } - return cb(null, rows.map(r => r.meta_value)); // map to array of values only + return cb(null, rows.map(r => r.meta_value)); // map to array of values only }); } @@ -460,23 +460,23 @@ module.exports = class Message { loadMeta(cb) { /* - Example of loaded this.meta: + Example of loaded this.meta: - meta: { - System: { - local_to_user_id: 1234, - }, - FtnProperty: { - ftn_seen_by: [ "1/102 103", "2/42 52 65" ] - } - } - */ + meta: { + System: { + local_to_user_id: 1234, + }, + FtnProperty: { + ftn_seen_by: [ "1/102 103", "2/42 52 65" ] + } + } + */ const sql = - `SELECT meta_category, meta_name, meta_value - FROM message_meta - WHERE message_id = ?;`; + `SELECT meta_category, meta_name, meta_value + FROM message_meta + WHERE message_id = ?;`; - const self = this; // :TODO: not required - arrow functions below: + const self = this; // :TODO: not required - arrow functions below: msgDb.each(sql, [ this.messageId ], (err, row) => { if(!(row.meta_category in self.meta)) { self.meta[row.meta_category] = { }; @@ -497,7 +497,7 @@ module.exports = class Message { }); } - // :TODO: this should only take a UUID... + // :TODO: this should only take a UUID... load(options, cb) { assert(_.isString(options.uuid)); @@ -508,10 +508,10 @@ module.exports = class Message { function loadMessage(callback) { msgDb.get( `SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, - message, modified_timestamp, view_count - FROM message - WHERE message_uuid=? - LIMIT 1;`, + message, modified_timestamp, view_count + FROM message + WHERE message_uuid=? + LIMIT 1;`, [ options.uuid ], (err, msgRow) => { if(err) { @@ -522,15 +522,15 @@ module.exports = class Message { return callback(Errors.DoesNotExist('Message (no longer) available')); } - self.messageId = msgRow.message_id; - self.areaTag = msgRow.area_tag; - self.messageUuid = msgRow.message_uuid; - self.replyToMsgId = msgRow.reply_to_message_id; - self.toUserName = msgRow.to_user_name; - self.fromUserName = msgRow.from_user_name; - self.subject = msgRow.subject; - self.message = msgRow.message; - self.modTimestamp = moment(msgRow.modified_timestamp); + self.messageId = msgRow.message_id; + self.areaTag = msgRow.area_tag; + self.messageUuid = msgRow.message_uuid; + self.replyToMsgId = msgRow.reply_to_message_id; + self.toUserName = msgRow.to_user_name; + self.fromUserName = msgRow.from_user_name; + self.subject = msgRow.subject; + self.message = msgRow.message; + self.modTimestamp = moment(msgRow.modified_timestamp); return callback(err); } @@ -542,7 +542,7 @@ module.exports = class Message { }); }, function loadHashTags(callback) { - // :TODO: + // :TODO: return callback(null); } ], @@ -560,7 +560,7 @@ module.exports = class Message { const metaStmt = transOrDb.prepare( `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) - VALUES (?, ?, ?, ?);`); + VALUES (?, ?, ?, ?);`); if(!_.isArray(value)) { value = [ value ]; @@ -590,7 +590,7 @@ module.exports = class Message { return msgDb.beginTransaction(callback); }, function storeMessage(trans, callback) { - // generate a UUID for this message if required (general case) + // generate a UUID for this message if required (general case) const msgTimestamp = moment(); if(!self.uuid) { self.uuid = Message.createMessageUUID( @@ -603,9 +603,9 @@ module.exports = class Message { trans.run( `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) - VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, [ self.areaTag, self.uuid, self.replyToMsgId, self.toUserName, self.fromUserName, self.subject, self.message, getISOTimestampString(msgTimestamp) ], - function inserted(err) { // use non-arrow function for 'this' scope + function inserted(err) { // use non-arrow function for 'this' scope if(!err) { self.messageId = this.lastID; } @@ -619,17 +619,17 @@ module.exports = class Message { return callback(null, trans); } /* - Example of self.meta: + Example of self.meta: - meta: { - System: { - local_to_user_id: 1234, - }, - FtnProperty: { - ftn_seen_by: [ "1/102 103", "2/42 52 65" ] - } - } - */ + meta: { + System: { + local_to_user_id: 1234, + }, + FtnProperty: { + ftn_seen_by: [ "1/102 103", "2/42 52 65" ] + } + } + */ async.each(Object.keys(self.meta), (category, nextCat) => { async.each(Object.keys(self.meta[category]), (name, nextName) => { self.persistMetaValue(category, name, self.meta[category][name], trans, err => { @@ -644,7 +644,7 @@ module.exports = class Message { }); }, function storeHashTags(trans, callback) { - // :TODO: hash tag support + // :TODO: hash tag support return callback(null, trans); } ], @@ -660,7 +660,7 @@ module.exports = class Message { ); } - // :TODO: FTN stuff doesn't have any business here + // :TODO: FTN stuff doesn't have any business here getFTNQuotePrefix(source) { source = source || 'fromUserName'; @@ -677,32 +677,32 @@ module.exports = class Message { return cb(Errors.MissingParam()); } - options.startCol = options.startCol || 1; - options.includePrefix = _.get(options, 'includePrefix', true); - options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); - options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); - options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting + options.startCol = options.startCol || 1; + options.includePrefix = _.get(options, 'includePrefix', true); + options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); + options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); + options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting /* - Some long text that needs to be wrapped and quoted should look right after - doing so, don't ya think? yeah I think so + Some long text that needs to be wrapped and quoted should look right after + doing so, don't ya think? yeah I think so - Nu> Some long text that needs to be wrapped and quoted should look right - Nu> after doing so, don't ya think? yeah I think so + Nu> Some long text that needs to be wrapped and quoted should look right + Nu> after doing so, don't ya think? yeah I think so - Ot> Nu> Some long text that needs to be wrapped and quoted should look - Ot> Nu> right after doing so, don't ya think? yeah I think so + Ot> Nu> Some long text that needs to be wrapped and quoted should look + Ot> Nu> right after doing so, don't ya think? yeah I think so - */ + */ const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; function getWrapped(text, extraPrefix) { extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; const wrapOpts = { - width : options.cols - (quotePrefix.length + extraPrefix.length), - tabHandling : 'expand', - tabWidth : 4, + width : options.cols - (quotePrefix.length + extraPrefix.length), + tabHandling : 'expand', + tabWidth : 4, }; return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { @@ -711,7 +711,7 @@ module.exports = class Message { } function getFormattedLine(line) { - // for pre-formatted text, we just append a line truncated to fit + // for pre-formatted text, we just append a line truncated to fit let newLen; const total = line.length + quotePrefix.length; @@ -726,14 +726,14 @@ module.exports = class Message { if(options.isAnsi) { ansiPrep( - this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF + this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF { - termWidth : options.termWidth, - termHeight : options.termHeight, - cols : options.cols, - rows : 'auto', - startCol : options.startCol, - forceLineTerm : true, + termWidth : options.termWidth, + termHeight : options.termHeight, + cols : options.cols, + rows : 'auto', + startCol : options.startCol, + forceLineTerm : true, }, (err, prepped) => { prepped = prepped || this.message; @@ -741,20 +741,20 @@ module.exports = class Message { let lastSgr = ''; const split = splitTextAtTerms(prepped); - const quoteLines = []; - const focusQuoteLines = []; + const quoteLines = []; + const focusQuoteLines = []; // - // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) - // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to - // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do - // the trick and allow them to leave them alone! + // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) + // as while this works in ENiGMA, other boards such as Mystic, WWIV, etc. will try to + // strip colors, colorize the lines, etc. If we exclude the prefixes, this seems to do + // the trick and allow them to leave them alone! // split.forEach(l => { quoteLines.push(`${lastSgr}${l}`); focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`); - lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex + lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex }); quoteLines[quoteLines.length - 1] += options.ansiResetSgr; @@ -763,23 +763,23 @@ module.exports = class Message { } ); } else { - const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; - const quoted = []; - const input = _.trimEnd(this.message).replace(/\b/g, ''); + const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; + const quoted = []; + const input = _.trimEnd(this.message).replace(/\b/g, ''); - // find *last* tearline + // find *last* tearline let tearLinePos = this.getTearLinePosition(input); - tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string + tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { // - // For each paragraph, a state machine: - // - New line - line - // - New (pre)quoted line - quote_line - // - Continuation of new/quoted line + // For each paragraph, a state machine: + // - New line - line + // - New (pre)quoted line - quote_line + // - Continuation of new/quoted line // - // Also: - // - Detect pre-formatted lines & try to keep them as-is + // Also: + // - Detect pre-formatted lines & try to keep them as-is // let state; let buf = ''; @@ -787,18 +787,18 @@ module.exports = class Message { if(quoted.length > 0) { // - // Preserve paragraph seperation. + // Preserve paragraph seperation. // - // FSC-0032 states something about leaving blank lines fully blank - // (without a prefix) but it seems nicer (and more consistent with other systems) - // to put 'em in. + // FSC-0032 states something about leaving blank lines fully blank + // (without a prefix) but it seems nicer (and more consistent with other systems) + // to put 'em in. // quoted.push(quotePrefix); } paragraph.split(/\r?\n/).forEach(line => { if(0 === line.trim().length) { - // see blank line notes above + // see blank line notes above return quoted.push(quotePrefix); } @@ -839,8 +839,8 @@ module.exports = class Message { if(isFormattedLine(line)) { quoted.push(getFormattedLine(line)); } else { - state = quoteMatch ? 'quote_line' : 'line'; - buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any + state = quoteMatch ? 'quote_line' : 'line'; + buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any } break; } diff --git a/core/message_area.js b/core/message_area.js index 03cc914c..e5227ef0 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -1,45 +1,45 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const msgDb = require('./database.js').dbs.message; -const Config = require('./config.js').get; -const Message = require('./message.js'); -const Log = require('./logger.js').log; -const msgNetRecord = require('./msg_network.js').recordMessage; -const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; +// ENiGMA½ +const msgDb = require('./database.js').dbs.message; +const Config = require('./config.js').get; +const Message = require('./message.js'); +const Log = require('./logger.js').log; +const msgNetRecord = require('./msg_network.js').recordMessage; +const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; -// deps -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); +// deps +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); exports.getAvailableMessageConferences = getAvailableMessageConferences; -exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; +exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag; exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag; exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag; -exports.getMessageConferenceByTag = getMessageConferenceByTag; -exports.getMessageAreaByTag = getMessageAreaByTag; -exports.changeMessageConference = changeMessageConference; -exports.changeMessageArea = changeMessageArea; -exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; -exports.getMessageListForArea = getMessageListForArea; -exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; -exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; -exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; -exports.getMessageAreaLastReadId = getMessageAreaLastReadId; -exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; -exports.persistMessage = persistMessage; -exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; +exports.getMessageConferenceByTag = getMessageConferenceByTag; +exports.getMessageAreaByTag = getMessageAreaByTag; +exports.changeMessageConference = changeMessageConference; +exports.changeMessageArea = changeMessageArea; +exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; +exports.getMessageListForArea = getMessageListForArea; +exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; +exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; +exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; +exports.getMessageAreaLastReadId = getMessageAreaLastReadId; +exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; +exports.persistMessage = persistMessage; +exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; function getAvailableMessageConferences(client, options) { options = options || { includeSystemInternal : false }; assert(client || true === options.noClient); - // perform ACS check per conf & omit system_internal if desired + // perform ACS check per conf & omit system_internal if desired return _.omitBy(Config().messageConferences, (conf, confTag) => { if(!options.includeSystemInternal && 'system_internal' === confTag) { return true; @@ -53,7 +53,7 @@ function getSortedAvailMessageConferences(client, options) { const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => { return { confTag : k, - conf : v, + conf : v, }; }); @@ -73,10 +73,10 @@ function getAvailableMessageAreasByConfTag(confTag, options) { const areas = config.messageConferences[confTag].areas; if(!options.client || true === options.noAcsCheck) { - // everything - no ACS checks + // everything - no ACS checks return areas; } else { - // perform ACS check per area + // perform ACS check per area return _.omitBy(areas, area => { return !options.client.acs.hasMessageAreaRead(area); }); @@ -87,8 +87,8 @@ function getAvailableMessageAreasByConfTag(confTag, options) { function getSortedAvailMessageAreasByConfTag(confTag, options) { const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => { return { - areaTag : k, - area : v, + areaTag : k, + area : v, }; }); @@ -99,16 +99,16 @@ function getSortedAvailMessageAreasByConfTag(confTag, options) { function getDefaultMessageConferenceTag(client, disableAcsCheck) { // - // Find the first conference marked 'default'. If found, - // inspect |client| against *read* ACS using defaults if not - // specified. + // Find the first conference marked 'default'. If found, + // inspect |client| against *read* ACS using defaults if not + // specified. // - // If the above fails, just go down the list until we get one - // that passes. + // If the above fails, just go down the list until we get one + // that passes. // - // It's possible that we end up with nothing here! + // It's possible that we end up with nothing here! // - // Note that built in 'system_internal' is always ommited here + // Note that built in 'system_internal' is always ommited here // const config = Config(); let defaultConf = _.findKey(config.messageConferences, o => o.default); @@ -170,7 +170,7 @@ function getMessageConfTagByAreaTag(areaTag) { function getMessageAreaByTag(areaTag, optionalConfTag) { const confs = Config().messageConferences; - // :TODO: this could be cached + // :TODO: this could be cached if(_.isString(optionalConfTag)) { if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { return confs[optionalConfTag].areas[areaTag]; @@ -204,8 +204,8 @@ function changeMessageConference(client, confTag, cb) { } }, function getDefaultAreaInConf(conf, callback) { - const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); - const area = getMessageAreaByTag(areaTag, confTag); + const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); + const area = getMessageAreaByTag(areaTag, confTag); if(area) { callback(null, conf, { areaTag : areaTag, area : area } ); @@ -222,8 +222,8 @@ function changeMessageConference(client, confTag, cb) { }, function changeConferenceAndArea(conf, areaInfo, callback) { const newProps = { - message_conf_tag : confTag, - message_area_tag : areaInfo.areaTag, + message_conf_tag : confTag, + message_area_tag : areaInfo.areaTag, }; client.user.persistProperties(newProps, err => { callback(err, conf, areaInfo); @@ -242,7 +242,7 @@ function changeMessageConference(client, confTag, cb) { } function changeMessageAreaWithOptions(client, areaTag, options, cb) { - options = options || {}; // :TODO: this is currently pointless... cb is required... + options = options || {}; // :TODO: this is currently pointless... cb is required... async.waterfall( [ @@ -284,14 +284,14 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { } // -// Temporairly -- e.g. non-persisted -- change to an area and it's -// associated underlying conference. ACS is checked for both. +// Temporairly -- e.g. non-persisted -- change to an area and it's +// associated underlying conference. ACS is checked for both. // -// This is useful for example when doing a new scan +// This is useful for example when doing a new scan // function tempChangeMessageConfAndArea(client, areaTag) { - const area = getMessageAreaByTag(areaTag); - const confTag = getMessageConfTagByAreaTag(areaTag); + const area = getMessageAreaByTag(areaTag); + const confTag = getMessageConfTagByAreaTag(areaTag); if(!area || !confTag) { return false; @@ -303,7 +303,7 @@ function tempChangeMessageConfAndArea(client, areaTag) { return false; } - client.user.properties.message_conf_tag = confTag; + client.user.properties.message_conf_tag = confTag; client.user.properties.message_area_tag = areaTag; return true; @@ -319,8 +319,8 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) { const filter = { areaTag, - newerThanMessageId : lastMessageId, - resultType : 'count', + newerThanMessageId : lastMessageId, + resultType : 'count', }; if(Message.isPrivateAreaTag(areaTag)) { @@ -339,10 +339,10 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { const filter = { areaTag, - resultType : 'messageList', - newerThanMessageId : lastMessageId, - sort : 'messageId', - order : 'ascending', + resultType : 'messageList', + newerThanMessageId : lastMessageId, + sort : 'messageId', + order : 'ascending', }; if(Message.isPrivateAreaTag(areaTag)) { @@ -356,9 +356,9 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { function getMessageListForArea(client, areaTag, cb) { const filter = { areaTag, - resultType : 'messageList', - sort : 'messageId', - order : 'ascending', + resultType : 'messageList', + sort : 'messageId', + order : 'ascending', }; if(Message.isPrivateAreaTag(areaTag)) { @@ -373,9 +373,9 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { { areaTag, newerThanTimestamp, - sort : 'modTimestamp', - order : 'ascending', - limit : 1, + sort : 'modTimestamp', + order : 'ascending', + limit : 1, }, (err, id) => { if(err) { @@ -388,9 +388,9 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { function getMessageAreaLastReadId(userId, areaTag, cb) { msgDb.get( - 'SELECT message_id ' + - 'FROM user_message_area_last_read ' + - 'WHERE user_id = ? AND area_tag = ?;', + 'SELECT message_id ' + + 'FROM user_message_area_last_read ' + + 'WHERE user_id = ? AND area_tag = ?;', [ userId, areaTag.toLowerCase() ], function complete(err, row) { cb(err, row ? row.message_id : 0); @@ -404,20 +404,20 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) allowOlder = false; } - // :TODO: likely a better way to do this... + // :TODO: likely a better way to do this... async.waterfall( [ function getCurrent(callback) { getMessageAreaLastReadId(userId, areaTag, function result(err, lastId) { lastId = lastId || 0; - callback(null, lastId); // ignore errors as we default to 0 + callback(null, lastId); // ignore errors as we default to 0 }); }, function update(lastId, callback) { if(allowOlder || messageId > lastId) { msgDb.run( - 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + - 'VALUES (?, ?, ?);', + 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + + 'VALUES (?, ?, ?);', [ userId, areaTag, messageId ], function written(err) { callback(err, true); // true=didUpdate @@ -459,7 +459,7 @@ function persistMessage(message, cb) { ); } -// method exposed for event scheduler +// method exposed for event scheduler function trimMessageAreasScheduledEvent(args, cb) { function trimMessageAreaByMaxMessages(areaInfo, cb) { @@ -469,15 +469,15 @@ function trimMessageAreasScheduledEvent(args, cb) { msgDb.run( `DELETE FROM message - WHERE message_id IN( - SELECT message_id - FROM message - WHERE area_tag = ? - ORDER BY message_id DESC - LIMIT -1 OFFSET ${areaInfo.maxMessages} - );`, + WHERE message_id IN( + SELECT message_id + FROM message + WHERE area_tag = ? + ORDER BY message_id DESC + LIMIT -1 OFFSET ${areaInfo.maxMessages} + );`, [ areaInfo.areaTag.toLowerCase() ], - function result(err) { // no arrow func; need this + function result(err) { // no arrow func; need this if(err) { Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area'); } else { @@ -495,9 +495,9 @@ function trimMessageAreasScheduledEvent(args, cb) { msgDb.run( `DELETE FROM message - WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`, + WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`, [ areaInfo.areaTag ], - function result(err) { // no arrow func; need this + function result(err) { // no arrow func; need this if(err) { Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area'); } else { @@ -514,17 +514,17 @@ function trimMessageAreasScheduledEvent(args, cb) { const areaTags = []; // - // We use SQL here vs API such that no-longer-used tags are picked up + // We use SQL here vs API such that no-longer-used tags are picked up // msgDb.each( `SELECT DISTINCT area_tag - FROM message;`, + FROM message;`, (err, row) => { if(err) { return callback(err); } - // We treat private mail special + // We treat private mail special if(!Message.isPrivateAreaTag(row.area_tag)) { areaTags.push(row.area_tag); } @@ -537,23 +537,23 @@ function trimMessageAreasScheduledEvent(args, cb) { function prepareAreaInfo(areaTags, callback) { let areaInfos = []; - // determine maxMessages & maxAgeDays per area + // determine maxMessages & maxAgeDays per area const config = Config(); areaTags.forEach(areaTag => { let maxMessages = config.messageAreaDefaults.maxMessages; - let maxAgeDays = config.messageAreaDefaults.maxAgeDays; + let maxAgeDays = config.messageAreaDefaults.maxAgeDays; - const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here + const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here if(area) { maxMessages = area.maxMessages || maxMessages; - maxAgeDays = area.maxAgeDays || maxAgeDays; + maxAgeDays = area.maxAgeDays || maxAgeDays; } areaInfos.push( { - areaTag : areaTag, - maxMessages : maxMessages, - maxAgeDays : maxAgeDays, + areaTag : areaTag, + maxMessages : maxMessages, + maxAgeDays : maxAgeDays, } ); }); @@ -578,13 +578,13 @@ function trimMessageAreasScheduledEvent(args, cb) { }, function trimExternalPrivateSentMail(callback) { // - // *External* (FTN, email, ...) outgoing is cleaned up *after export* - // if it is older than the configured |maxExternalSentAgeDays| days + // *External* (FTN, email, ...) outgoing is cleaned up *after export* + // if it is older than the configured |maxExternalSentAgeDays| days // - // Outgoing externally exported private mail is: - // - In the 'private_mail' area - // - Marked exported (state_flags0 exported bit set) - // - Marked with any external flavor (we don't mark local) + // Outgoing externally exported private mail is: + // - In the 'private_mail' area + // - Marked exported (state_flags0 exported bit set) + // - Marked with any external flavor (we don't mark local) // const maxExternalSentAgeDays = _.get( Config, @@ -594,18 +594,18 @@ function trimMessageAreasScheduledEvent(args, cb) { msgDb.run( `DELETE FROM message - WHERE message_id IN ( - SELECT m.message_id - FROM message m - JOIN message_meta mms - ON m.message_id = mms.message_id AND - (mms.meta_category='System' AND mms.meta_name='${Message.SystemMetaNames.StateFlags0}' AND (mms.meta_value & ${Message.StateFlags0.Exported} = ${Message.StateFlags0.Exported})) - JOIN message_meta mmf - ON m.message_id = mmf.message_id AND - (mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}') - WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days') - );`, - function results(err) { // no arrow func; need this + WHERE message_id IN ( + SELECT m.message_id + FROM message m + JOIN message_meta mms + ON m.message_id = mms.message_id AND + (mms.meta_category='System' AND mms.meta_name='${Message.SystemMetaNames.StateFlags0}' AND (mms.meta_value & ${Message.StateFlags0.Exported} = ${Message.StateFlags0.Exported})) + JOIN message_meta mmf + ON m.message_id = mmf.message_id AND + (mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}') + WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days') + );`, + function results(err) { // no arrow func; need this if(err) { Log.warn( { error : err.message }, 'Error trimming private externally sent messages'); } else { diff --git a/core/message_base_search.js b/core/message_base_search.js index 98f78552..9684b8f0 100644 --- a/core/message_base_search.js +++ b/core/message_base_search.js @@ -1,34 +1,34 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; const { getSortedAvailMessageConferences, getAvailableMessageAreasByConfTag, getSortedAvailMessageAreasByConfTag, -} = require('./message_area.js'); -const Errors = require('./enig_error.js').Errors; -const Message = require('./message.js'); +} = require('./message_area.js'); +const Errors = require('./enig_error.js').Errors; +const Message = require('./message.js'); -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Base Search', - desc : 'Module for quickly searching the message base', - author : 'NuSkooler', + name : 'Message Base Search', + desc : 'Module for quickly searching the message base', + author : 'NuSkooler', }; const MciViewIds = { search : { - searchTerms : 1, - search : 2, - conf : 3, - area : 4, - to : 5, - from : 6, - advSearch : 7, + searchTerms : 1, + search : 2, + conf : 3, + area : 4, + to : 5, + from : 6, + advSearch : 7, } }; @@ -54,8 +54,8 @@ exports.getModule = class MessageBaseSearch extends MenuModule { return cb(err); } - const confView = vc.getView(MciViewIds.search.conf); - const areaView = vc.getView(MciViewIds.search.area); + const confView = vc.getView(MciViewIds.search.conf); + const areaView = vc.getView(MciViewIds.search.area); if(!confView || !areaView) { return cb(Errors.DoesNotExist('Missing one or more required views')); @@ -65,7 +65,7 @@ exports.getModule = class MessageBaseSearch extends MenuModule { getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || [] ); - let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL + let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL confView.setItems(availConfs); areaView.setItems(availAreas); @@ -90,30 +90,30 @@ exports.getModule = class MessageBaseSearch extends MenuModule { } searchNow(formData, cb) { - const isAdvanced = formData.submitId === MciViewIds.search.advSearch; - const value = formData.value; + const isAdvanced = formData.submitId === MciViewIds.search.advSearch; + const value = formData.value; const filter = { - resultType : 'messageList', - sort : 'modTimestamp', - terms : value.searchTerms, - //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], - limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned + resultType : 'messageList', + sort : 'modTimestamp', + terms : value.searchTerms, + //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], + limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned }; if(isAdvanced) { - filter.toUserName = value.toUserName; - filter.fromUserName = value.fromUserName; + filter.toUserName = value.toUserName; + filter.fromUserName = value.fromUserName; if(value.confTag && !value.areaTag) { - // areaTag may be a string or array of strings - // getAvailableMessageAreasByConfTag() returns a obj - we only need tags + // areaTag may be a string or array of strings + // getAvailableMessageAreasByConfTag() returns a obj - we only need tags filter.areaTag = _.map( getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ), (area, areaTag) => areaTag ); } else if(value.areaTag) { - filter.areaTag = value.areaTag; // specific conf + area + filter.areaTag = value.areaTag; // specific conf + area } } @@ -133,9 +133,9 @@ exports.getModule = class MessageBaseSearch extends MenuModule { const menuOpts = { extraArgs : { messageList, - noUpdateLastReadId : true + noUpdateLastReadId : true }, - menuFlags : [ 'popParent' ], + menuFlags : [ 'popParent' ], }; return this.gotoMenu( diff --git a/core/mime_util.js b/core/mime_util.js index d6631077..857b967c 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -1,26 +1,26 @@ /* jslint node: true */ 'use strict'; -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); -const mimeTypes = require('mime-types'); +const mimeTypes = require('mime-types'); -exports.startup = startup; -exports.resolveMimeType = resolveMimeType; +exports.startup = startup; +exports.resolveMimeType = resolveMimeType; function startup(cb) { // - // Add in types (not yet) supported by mime-db -- and therefor, mime-types + // Add in types (not yet) supported by mime-db -- and therefor, mime-types // const ADDITIONAL_EXT_MIMETYPES = { - ans : 'text/x-ansi', - gz : 'application/gzip', // not in mime-types 2.1.15 :( - lzx : 'application/x-lzx', // :TODO: submit to mime-types + ans : 'text/x-ansi', + gz : 'application/gzip', // not in mime-types 2.1.15 :( + lzx : 'application/x-lzx', // :TODO: submit to mime-types }; _.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => { - // don't override any entries + // don't override any entries if(!_.isString(mimeTypes.types[ext])) { mimeTypes[ext] = mimeType; } @@ -35,8 +35,8 @@ function startup(cb) { function resolveMimeType(query) { if(mimeTypes.extensions[query]) { - return query; // alreaed a mime-type + return query; // alreaed a mime-type } - return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined + return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined } \ No newline at end of file diff --git a/core/misc_util.js b/core/misc_util.js index 3a75d065..633cc967 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -1,17 +1,17 @@ /* jslint node: true */ 'use strict'; -const paths = require('path'); +const paths = require('path'); -const os = require('os'); -const packageJson = require('../package.json'); +const os = require('os'); +const packageJson = require('../package.json'); -exports.isProduction = isProduction; -exports.isDevelopment = isDevelopment; -exports.valueWithDefault = valueWithDefault; -exports.resolvePath = resolvePath; -exports.getCleanEnigmaVersion = getCleanEnigmaVersion; -exports.getEnigmaUserAgent = getEnigmaUserAgent; +exports.isProduction = isProduction; +exports.isDevelopment = isDevelopment; +exports.valueWithDefault = valueWithDefault; +exports.resolvePath = resolvePath; +exports.getCleanEnigmaVersion = getCleanEnigmaVersion; +exports.getEnigmaUserAgent = getEnigmaUserAgent; function isProduction() { var env = process.env.NODE_ENV || 'dev'; @@ -42,11 +42,11 @@ function getCleanEnigmaVersion() { ; } -// See also ftn_util.js getTearLine() & getProductIdentifier() +// See also ftn_util.js getTearLine() & getProductIdentifier() function getEnigmaUserAgent() { - // can't have 1/2 or ½ in User-Agent according to RFC 1945 :( + // can't have 1/2 or ½ in User-Agent according to RFC 1945 :( const version = getCleanEnigmaVersion(); - const nodeVer = process.version.substr(1); // remove 'v' prefix + const nodeVer = process.version.substr(1); // remove 'v' prefix return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } \ No newline at end of file diff --git a/core/mod_mixins.js b/core/mod_mixins.js index fd9db771..f3d9d5ad 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const messageArea = require('../core/message_area.js'); -const { get } = require('lodash'); +const messageArea = require('../core/message_area.js'); +const { get } = require('lodash'); exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { @@ -10,13 +10,13 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); if(!messageAreaTag) { - return; // nothing to do! + return; // nothing to do! } if(recordPrevious) { this.prevMessageConfAndArea = { - confTag : this.client.user.properties.message_conf_tag, - areaTag : this.client.user.properties.message_area_tag, + confTag : this.client.user.properties.message_conf_tag, + areaTag : this.client.user.properties.message_area_tag, }; } diff --git a/core/module_util.js b/core/module_util.js index 26a1ec53..5a575f3e 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; +// ENiGMA½ +const Config = require('./config.js').get; -// deps -const fs = require('graceful-fs'); -const paths = require('path'); -const _ = require('lodash'); -const assert = require('assert'); -const async = require('async'); +// deps +const fs = require('graceful-fs'); +const paths = require('path'); +const _ = require('lodash'); +const assert = require('assert'); +const async = require('async'); -// exports -exports.loadModuleEx = loadModuleEx; -exports.loadModule = loadModule; -exports.loadModulesForCategory = loadModulesForCategory; -exports.getModulePaths = getModulePaths; +// exports +exports.loadModuleEx = loadModuleEx; +exports.loadModule = loadModule; +exports.loadModulesForCategory = loadModulesForCategory; +exports.getModulePaths = getModulePaths; function loadModuleEx(options, cb) { assert(_.isObject(options)); @@ -25,18 +25,18 @@ function loadModuleEx(options, cb) { const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; if(_.isObject(modConfig) && false === modConfig.enabled) { - const err = new Error(`Module "${options.name}" is disabled`); - err.code = 'EENIGMODDISABLED'; + const err = new Error(`Module "${options.name}" is disabled`); + err.code = 'EENIGMODDISABLED'; return cb(err); } // - // Modules are allowed to live in /path/to//.js or - // simply in /path/to/.js. This allows for more advanced modules - // to have their own containing folder, package.json & dependencies, etc. + // Modules are allowed to live in /path/to//.js or + // simply in /path/to/.js. This allows for more advanced modules + // to have their own containing folder, package.json & dependencies, etc. // let mod; - let modPath = paths.join(options.path, `${options.name}.js`); // general case first + let modPath = paths.join(options.path, `${options.name}.js`); // general case first try { mod = require(modPath); } catch(e) { diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 2d61044d..27cbcbb1 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -1,30 +1,30 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const messageArea = require('./message_area.js'); -const displayThemeArt = require('./theme.js').displayThemeArt; -const resetScreen = require('./ansi_term.js').resetScreen; -const stringFormat = require('./string_format.js'); -const Errors = require('./enig_error.js').Errors; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const messageArea = require('./message_area.js'); +const displayThemeArt = require('./theme.js').displayThemeArt; +const resetScreen = require('./ansi_term.js').resetScreen; +const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; -// deps -const async = require('async'); -const _ = require('lodash'); +// deps +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Area List', - desc : 'Module for listing / choosing message areas', - author : 'NuSkooler', + name : 'Message Area List', + desc : 'Module for listing / choosing message areas', + author : 'NuSkooler', }; /* - :TODO: + :TODO: - Obv/2 has the following: - CHANGE .ANS - Message base changing ansi + Obv/2 has the following: + CHANGE .ANS - Message base changing ansi |SN Current base name |SS Current base sponsor |NM Number of messages in current base @@ -35,9 +35,9 @@ exports.moduleInfo = { */ const MciViewIds = { - AreaList : 1, - SelAreaInfo1 : 2, - SelAreaInfo2 : 3, + AreaList : 1, + SelAreaInfo1 : 2, + SelAreaInfo2 : 3, }; exports.getModule = class MessageAreaListModule extends MenuModule { @@ -53,9 +53,9 @@ exports.getModule = class MessageAreaListModule extends MenuModule { this.menuMethods = { changeArea : function(formData, extraArgs, cb) { if(1 === formData.submitId) { - let area = self.messageAreas[formData.value.area]; - const areaTag = area.areaTag; - area = area.area; // what we want is actually embedded + let area = self.messageAreas[formData.value.area]; + const areaTag = area.areaTag; + area = area.area; // what we want is actually embedded messageArea.changeMessageArea(self.client, areaTag, err => { if(err) { @@ -65,14 +65,14 @@ exports.getModule = class MessageAreaListModule extends MenuModule { } else { if(_.isString(area.art)) { const dispOptions = { - client : self.client, - name : area.art, + client : self.client, + name : area.art, }; self.client.term.rawWrite(resetScreen()); displayThemeArt(dispOptions, () => { - // pause by default, unless explicitly told not to + // pause by default, unless explicitly told not to if(_.has(area, 'options.pause') && false === area.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { @@ -99,18 +99,18 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }, timeout); } - // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! + // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! updateGeneralAreaInfoViews(areaIndex) { /* - const areaInfo = self.messageAreas[areaIndex]; + const areaInfo = self.messageAreas[areaIndex]; - [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { - const v = self.viewControllers.areaList.getView(mciId); - if(v) { - v.setFormatObject(areaInfo.area); - } - }); - */ + [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { + const v = self.viewControllers.areaList.getView(mciId); + if(v) { + v.setFormatObject(areaInfo.area); + } + }); + */ } mciReady(mciData, cb) { @@ -119,16 +119,16 @@ exports.getModule = class MessageAreaListModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); async.series( [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - formId : 0, + callingMenu : self, + mciMap : mciData.menu, + formId : 0, }; vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) { @@ -136,8 +136,8 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }); }, function populateAreaListView(callback) { - const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; - const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; + const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; + const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const areaListView = vc.getView(MciViewIds.AreaList); if(!areaListView) { diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 8c2136c7..8e4d9f3f 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; -const persistMessage = require('./message_area.js').persistMessage; +const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +const persistMessage = require('./message_area.js').persistMessage; -const _ = require('lodash'); -const async = require('async'); +const _ = require('lodash'); +const async = require('async'); exports.moduleInfo = { - name : 'Message Area Post', - desc : 'Module for posting a new message to an area', - author : 'NuSkooler', + name : 'Message Area Post', + desc : 'Module for posting a new message to an area', + author : 'NuSkooler', }; exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { @@ -19,7 +19,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { const self = this; - // we're posting, so always start with 'edit' mode + // we're posting, so always start with 'edit' mode this.editorMode = 'edit'; this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) { @@ -42,9 +42,9 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { ], function complete(err) { if(err) { - // :TODO:... sooooo now what? + // :TODO:... sooooo now what? } else { - // note: not logging 'from' here as it's part of client.log.xxxx() + // note: not logging 'from' here as it's part of client.log.xxxx() self.client.log.info( { to : msg.toUserName, subject : msg.subject, uuid : msg.uuid }, 'Message persisted' diff --git a/core/msg_area_reply_fse.js b/core/msg_area_reply_fse.js index 83cb99c7..11742865 100644 --- a/core/msg_area_reply_fse.js +++ b/core/msg_area_reply_fse.js @@ -1,14 +1,14 @@ /* jslint node: true */ 'use strict'; -var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; -exports.getModule = AreaReplyFSEModule; +exports.getModule = AreaReplyFSEModule; exports.moduleInfo = { - name : 'Message Area Reply', - desc : 'Module for replying to an area message', - author : 'NuSkooler', + name : 'Message Area Reply', + desc : 'Module for replying to an area message', + author : 'NuSkooler', }; function AreaReplyFSEModule(options) { diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index af0cbb78..1ca5617c 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -1,35 +1,35 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; -const Message = require('./message.js'); +// ENiGMA½ +const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +const Message = require('./message.js'); -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Area View', - desc : 'Module for viewing an area message', - author : 'NuSkooler', + name : 'Message Area View', + desc : 'Module for viewing an area message', + author : 'NuSkooler', }; exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { constructor(options) { super(options); - this.editorType = 'area'; - this.editorMode = 'view'; + this.editorType = 'area'; + this.editorMode = 'view'; if(_.isObject(options.extraArgs)) { - this.messageList = options.extraArgs.messageList; - this.messageIndex = options.extraArgs.messageIndex; - this.lastMessageNextExit = options.extraArgs.lastMessageNextExit; + this.messageList = options.extraArgs.messageList; + this.messageIndex = options.extraArgs.messageIndex; + this.lastMessageNextExit = options.extraArgs.lastMessageNextExit; } - this.messageList = this.messageList || []; - this.messageIndex = this.messageIndex || 0; - this.messageTotal = this.messageList.length; + this.messageList = this.messageList || []; + this.messageIndex = this.messageIndex || 0; + this.messageTotal = this.messageList.length; if(this.messageList.length > 0) { this.messageAreaTag = this.messageList[this.messageIndex].areaTag; @@ -37,19 +37,19 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { const self = this; - // assign *additional* menuMethods + // assign *additional* menuMethods Object.assign(this.menuMethods, { nextMessage : (formData, extraArgs, cb) => { if(self.messageIndex + 1 < self.messageList.length) { self.messageIndex++; this.messageAreaTag = this.messageList[this.messageIndex].areaTag; - this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); } - // auto-exit if no more to go? + // auto-exit if no more to go? if(self.lastMessageNextExit) { self.lastMessageReached = true; return self.prevMenu(cb); @@ -63,7 +63,7 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { self.messageIndex--; this.messageAreaTag = this.messageList[this.messageIndex].areaTag; - this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); } @@ -72,18 +72,18 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { }, movementKeyPressed : (formData, extraArgs, cb) => { - const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic # + const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic # - // :TODO: Create methods for up/down vs using keyPressXXXXX + // :TODO: Create methods for up/down vs using keyPressXXXXX switch(formData.key.name) { - case 'down arrow' : bodyView.scrollDocumentUp(); break; - case 'up arrow' : bodyView.scrollDocumentDown(); break; - case 'page up' : bodyView.keyPressPageUp(); break; - case 'page down' : bodyView.keyPressPageDown(); break; + case 'down arrow' : bodyView.scrollDocumentUp(); break; + case 'up arrow' : bodyView.scrollDocumentDown(); break; + case 'page up' : bodyView.keyPressPageUp(); break; + case 'page down' : bodyView.keyPressPageDown(); break; } - // :TODO: need to stop down/page down if doing so would push the last - // visible page off the screen at all .... this should be handled by MLTEV though... + // :TODO: need to stop down/page down if doing so would push the last + // visible page off the screen at all .... this should be handled by MLTEV though... return cb(null); }, @@ -92,8 +92,8 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { if(_.isString(extraArgs.menu)) { const modOpts = { extraArgs : { - messageAreaTag : self.messageAreaTag, - replyToMessage : self.message, + messageAreaTag : self.messageAreaTag, + replyToMessage : self.message, } }; @@ -124,22 +124,22 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { getSaveState() { return { - messageList : this.messageList, - messageIndex : this.messageIndex, - messageTotal : this.messageList.length, + messageList : this.messageList, + messageIndex : this.messageIndex, + messageTotal : this.messageList.length, }; } restoreSavedState(savedState) { - this.messageList = savedState.messageList; - this.messageIndex = savedState.messageIndex; - this.messageTotal = savedState.messageTotal; + this.messageList = savedState.messageList; + this.messageIndex = savedState.messageIndex; + this.messageTotal = savedState.messageTotal; } getMenuResult() { return { - messageIndex : this.messageIndex, - lastMessageReached : this.lastMessageReached, + messageIndex : this.messageIndex, + lastMessageReached : this.lastMessageReached, }; } }; diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index 0876f89a..c20d06ca 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -1,29 +1,29 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const messageArea = require('./message_area.js'); -const displayThemeArt = require('./theme.js').displayThemeArt; -const resetScreen = require('./ansi_term.js').resetScreen; -const stringFormat = require('./string_format.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const messageArea = require('./message_area.js'); +const displayThemeArt = require('./theme.js').displayThemeArt; +const resetScreen = require('./ansi_term.js').resetScreen; +const stringFormat = require('./string_format.js'); -// deps -const async = require('async'); -const _ = require('lodash'); +// deps +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Conference List', - desc : 'Module for listing / choosing message conferences', - author : 'NuSkooler', + name : 'Message Conference List', + desc : 'Module for listing / choosing message conferences', + author : 'NuSkooler', }; const MciViewIds = { - ConfList : 1, + ConfList : 1, - // :TODO: - // # areas in conf .... see Obv/2, iNiQ, ... + // :TODO: + // # areas in conf .... see Obv/2, iNiQ, ... // }; @@ -37,9 +37,9 @@ exports.getModule = class MessageConfListModule extends MenuModule { this.menuMethods = { changeConference : function(formData, extraArgs, cb) { if(1 === formData.submitId) { - let conf = self.messageConfs[formData.value.conf]; - const confTag = conf.confTag; - conf = conf.conf; // what we want is embedded + let conf = self.messageConfs[formData.value.conf]; + const confTag = conf.confTag; + conf = conf.conf; // what we want is embedded messageArea.changeMessageConference(self.client, confTag, err => { if(err) { @@ -51,14 +51,14 @@ exports.getModule = class MessageConfListModule extends MenuModule { } else { if(_.isString(conf.art)) { const dispOptions = { - client : self.client, - name : conf.art, + client : self.client, + name : conf.art, }; self.client.term.rawWrite(resetScreen()); displayThemeArt(dispOptions, () => { - // pause by default, unless explicitly told not to + // pause by default, unless explicitly told not to if(_.has(conf, 'options.pause') && false === conf.options.pause) { return self.prevMenuOnTimeout(1000, cb); } else { @@ -91,23 +91,23 @@ exports.getModule = class MessageConfListModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); async.series( [ function loadFromConfig(callback) { let loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - formId : 0, + callingMenu : self, + mciMap : mciData.menu, + formId : 0, }; vc.loadFromMenuConfig(loadOpts, callback); }, function populateConfListView(callback) { - const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; - const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; + const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; + const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; const confListView = vc.getView(MciViewIds.ConfList); let i = 1; @@ -135,7 +135,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { callback(null); }, function populateTextViews(callback) { - // :TODO: populate other avail MCI, e.g. current conf name + // :TODO: populate other avail MCI, e.g. current conf name callback(null); } ], diff --git a/core/msg_list.js b/core/msg_list.js index 5c8624ab..a4eb18aa 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -1,51 +1,51 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const messageArea = require('./message_area.js'); -const stringFormat = require('./string_format.js'); -const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const messageArea = require('./message_area.js'); +const stringFormat = require('./string_format.js'); +const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; -// deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +// deps +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); /* - Available listFormat/focusListFormat members (VM1): + Available listFormat/focusListFormat members (VM1): - msgNum : Message number - to : To username/handle - from : From username/handle - subj : Subject - ts : Message mod timestamp (format with config.dateTimeFormat) - newIndicator : New mark/indicator (config.newIndicator) + msgNum : Message number + to : To username/handle + from : From username/handle + subj : Subject + ts : Message mod timestamp (format with config.dateTimeFormat) + newIndicator : New mark/indicator (config.newIndicator) - MCI codes: + MCI codes: - VM1 : Message list - TL2 : Message info 1: { msgNumSelected, msgNumTotal } + VM1 : Message list + TL2 : Message info 1: { msgNumSelected, msgNumTotal } */ exports.moduleInfo = { - name : 'Message List', - desc : 'Module for listing/browsing available messages', - author : 'NuSkooler', + name : 'Message List', + desc : 'Module for listing/browsing available messages', + author : 'NuSkooler', }; const MciViewIds = { - msgList : 1, // VM1 - msgInfo1 : 2, // TL2 + msgList : 1, // VM1 + msgInfo1 : 2, // TL2 }; exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) { constructor(options) { super(options); - // :TODO: consider this pattern in base MenuModule - clean up code all over - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + // :TODO: consider this pattern in base MenuModule - clean up code all over + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); @@ -55,11 +55,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( this.initialFocusIndex = formData.value.message; const modOpts = { - extraArgs : { - messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, - messageList : this.config.messageList, - messageIndex : formData.value.message, - lastMessageNextExit : true, + extraArgs : { + messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, + messageList : this.config.messageList, + messageIndex : formData.value.message, + lastMessageNextExit : true, } }; @@ -68,8 +68,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } // - // Provide a serializer so we don't dump *huge* bits of information to the log - // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 + // Provide a serializer so we don't dump *huge* bits of information to the log + // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 // const self = this; modOpts.extraArgs.toJSON = function() { @@ -78,11 +78,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); return { - // note |this| is scope of toJSON()! - messageAreaTag : this.messageAreaTag, - apprevMessageList : logMsgList, - messageCount : this.messageList.length, - messageIndex : this.messageIndex, + // note |this| is scope of toJSON()! + messageAreaTag : this.messageAreaTag, + apprevMessageList : logMsgList, + messageCount : this.messageList.length, + messageIndex : this.messageIndex, }; }; @@ -111,10 +111,10 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( super.enter(); // - // Config can specify |messageAreaTag| else it comes from - // the user's current area. If |messageList| is supplied, - // each item is expected to contain |areaTag|, so we use that - // instead in those cases. + // Config can specify |messageAreaTag| else it comes from + // the user's current area. If |messageList| is supplied, + // each item is expected to contain |areaTag|, so we use that + // instead in those cases. // if(!Array.isArray(this.config.messageList)) { if(this.config.messageAreaTag) { @@ -136,23 +136,23 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); let configProvidedMessageList = false; async.series( [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu + callingMenu : self, + mciMap : mciData.menu }; return vc.loadFromMenuConfig(loadOpts, callback); }, function fetchMessagesInArea(callback) { // - // Config can supply messages else we'll need to populate the list now + // Config can supply messages else we'll need to populate the list now // if(_.isArray(self.config.messageList)) { configProvidedMessageList = true; @@ -169,7 +169,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( }); }, function getLastReadMesageId(callback) { - // messageList entries can contain |isNew| if they want to be considered new + // messageList entries can contain |isNew| if they want to be considered new if(configProvidedMessageList) { self.lastReadId = 0; return callback(null); @@ -177,33 +177,33 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { self.lastReadId = lastReadId || 0; - return callback(null); // ignore any errors, e.g. missing value + return callback(null); // ignore any errors, e.g. missing value }); }, function updateMessageListObjects(callback) { - const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); - const newIndicator = self.menuConfig.config.newIndicator || '*'; - const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues + const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); + const newIndicator = self.menuConfig.config.newIndicator || '*'; + const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues let msgNum = 1; self.config.messageList.forEach( (listItem, index) => { - listItem.msgNum = msgNum++; - listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); - const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; - listItem.newIndicator = isNew ? newIndicator : regIndicator; + listItem.msgNum = msgNum++; + listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); + const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; + listItem.newIndicator = isNew ? newIndicator : regIndicator; if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { self.initialFocusIndex = index; } - listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text + listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text }); return callback(null); }, function populateList(callback) { - const msgListView = vc.getView(MciViewIds.msgList); - // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... - const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; + const msgListView = vc.getView(MciViewIds.msgList); + // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... + const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; msgListView.setItems(self.config.messageList); @@ -215,7 +215,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( }); if(self.initialFocusIndex > 0) { - // note: causes redraw() + // note: causes redraw() msgListView.setFocusItemIndex(self.initialFocusIndex); } else { msgListView.redraw(); diff --git a/core/msg_network.js b/core/msg_network.js index 721ebba4..b26d5f1b 100644 --- a/core/msg_network.js +++ b/core/msg_network.js @@ -1,15 +1,15 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -let loadModulesForCategory = require('./module_util.js').loadModulesForCategory; +// ENiGMA½ +let loadModulesForCategory = require('./module_util.js').loadModulesForCategory; -// standard/deps -let async = require('async'); +// standard/deps +let async = require('async'); -exports.startup = startup; -exports.shutdown = shutdown; -exports.recordMessage = recordMessage; +exports.startup = startup; +exports.shutdown = shutdown; +exports.recordMessage = recordMessage; let msgNetworkModules = []; @@ -53,9 +53,9 @@ function shutdown(cb) { function recordMessage(message, cb) { // - // Give all message network modules (scanner/tossers) - // a chance to do something with |message|. Any or all can - // choose to ignore it. + // Give all message network modules (scanner/tossers) + // a chance to do something with |message|. Any or all can + // choose to ignore it. // async.each(msgNetworkModules, (modInst, next) => { modInst.record(message); diff --git a/core/msg_scan_toss_module.js b/core/msg_scan_toss_module.js index 002c2cc3..59c94be0 100644 --- a/core/msg_scan_toss_module.js +++ b/core/msg_scan_toss_module.js @@ -1,10 +1,10 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -var PluginModule = require('./plugin_module.js').PluginModule; +// ENiGMA½ +var PluginModule = require('./plugin_module.js').PluginModule; -exports.MessageScanTossModule = MessageScanTossModule; +exports.MessageScanTossModule = MessageScanTossModule; function MessageScanTossModule() { PluginModule.call(this); diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 3a7f29d9..1d16e201 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -1,22 +1,22 @@ /* jslint node: true */ 'use strict'; -const View = require('./view.js').View; -const strUtil = require('./string_util.js'); -const ansi = require('./ansi_term.js'); -const wordWrapText = require('./word_wrap.js').wordWrapText; -const ansiPrep = require('./ansi_prep.js'); +const View = require('./view.js').View; +const strUtil = require('./string_util.js'); +const ansi = require('./ansi_term.js'); +const wordWrapText = require('./word_wrap.js').wordWrapText; +const ansiPrep = require('./ansi_prep.js'); -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); -// :TODO: Determine CTRL-* keys for various things -// See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt -// http://wiki.synchro.net/howto:editor:slyedit#edit_mode -// http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html +// :TODO: Determine CTRL-* keys for various things +// See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt +// http://wiki.synchro.net/howto:editor:slyedit#edit_mode +// http://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_win.html /* Mystic - [^B] Reformat Paragraph [^O] Show this help file + [^B] Reformat Paragraph [^O] Show this help file [^I] Insert tab space [^Q] Enter quote mode [^K] Cut current line of text [^V] Toggle insert/overwrite [^U] Paste previously cut text [^Y] Delete current line @@ -29,58 +29,58 @@ const _ = require('lodash'); */ // -// Some other interesting implementations, resources, etc. +// Some other interesting implementations, resources, etc. // -// Editors - BBS -// * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp +// Editors - BBS +// * https://github.com/M-griffin/Enthral/blob/master/src/msg_fse.cpp // // -// Editors - Other -// * http://joe-editor.sourceforge.net/ -// * http://www.jbox.dk/downloads/edit.c -// * https://github.com/dominictarr/hipster +// Editors - Other +// * http://joe-editor.sourceforge.net/ +// * http://www.jbox.dk/downloads/edit.c +// * https://github.com/dominictarr/hipster // -// Implementations - Word Wrap -// * https://github.com/protomouse/synchronet/blob/93b01c55b3102ebc3c4f4793c3a45b8c13d0dc2a/src/sbbs3/wordwrap.c +// Implementations - Word Wrap +// * https://github.com/protomouse/synchronet/blob/93b01c55b3102ebc3c4f4793c3a45b8c13d0dc2a/src/sbbs3/wordwrap.c // -// Misc notes -// * https://github.com/dominictarr/hipster/issues/15 (Deleting lines/etc.) +// Misc notes +// * https://github.com/dominictarr/hipster/issues/15 (Deleting lines/etc.) // -// Blessed -// insertLine: CSR(top, bottom) + CUP(y, 0) + IL(1) + CSR(0, height) -// deleteLine: CSR(top, bottom) + CUP(y, 0) + DL(1) + CSR(0, height) -// Quick Ansi -- update only what was changed: -// https://github.com/dominictarr/quickansi +// Blessed +// insertLine: CSR(top, bottom) + CUP(y, 0) + IL(1) + CSR(0, height) +// deleteLine: CSR(top, bottom) + CUP(y, 0) + DL(1) + CSR(0, height) +// Quick Ansi -- update only what was changed: +// https://github.com/dominictarr/quickansi // -// To-Do +// To-Do // -// * Index pos % for emit scroll events -// * Some of this should be async'd where there is lots of processing (e.g. word wrap) -// * Fix backspace when col=0 (e.g. bs to prev line) -// * Add word delete (CTRL+????) -// * +// * Index pos % for emit scroll events +// * Some of this should be async'd where there is lots of processing (e.g. word wrap) +// * Fix backspace when col=0 (e.g. bs to prev line) +// * Add word delete (CTRL+????) +// * const SPECIAL_KEY_MAP_DEFAULT = { - 'line feed' : [ 'return' ], - exit : [ 'esc' ], - backspace : [ 'backspace' ], - delete : [ 'delete' ], - tab : [ 'tab' ], - up : [ 'up arrow' ], - down : [ 'down arrow' ], - end : [ 'end' ], - home : [ 'home' ], - left : [ 'left arrow' ], - right : [ 'right arrow' ], - 'delete line' : [ 'ctrl + y' ], - 'page up' : [ 'page up' ], - 'page down' : [ 'page down' ], - insert : [ 'insert', 'ctrl + v' ], + 'line feed' : [ 'return' ], + exit : [ 'esc' ], + backspace : [ 'backspace' ], + delete : [ 'delete' ], + tab : [ 'tab' ], + up : [ 'up arrow' ], + down : [ 'down arrow' ], + end : [ 'end' ], + home : [ 'home' ], + left : [ 'left arrow' ], + right : [ 'right arrow' ], + 'delete line' : [ 'ctrl + y' ], + 'page up' : [ 'page up' ], + 'page down' : [ 'page down' ], + insert : [ 'insert', 'ctrl + v' ], }; -exports.MultiLineEditTextView = MultiLineEditTextView; +exports.MultiLineEditTextView = MultiLineEditTextView; function MultiLineEditTextView(options) { if(!_.isBoolean(options.acceptsFocus)) { @@ -100,18 +100,18 @@ function MultiLineEditTextView(options) { var self = this; // - // ANSI seems to want tabs to default to 8 characters. See the following: - // * http://www.ansi-bbs.org/ansi-bbs2/control_chars/ - // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt + // ANSI seems to want tabs to default to 8 characters. See the following: + // * http://www.ansi-bbs.org/ansi-bbs2/control_chars/ + // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt // - // This seems overkill though, so let's default to 4 :) - // :TODO: what shoudl this really be? Maybe 8 is OK + // This seems overkill though, so let's default to 4 :) + // :TODO: what shoudl this really be? Maybe 8 is OK // - this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4; + this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4; - this.textLines = [ ]; - this.topVisibleIndex = 0; - this.mode = options.mode || 'edit'; // edit | preview | read-only + this.textLines = [ ]; + this.topVisibleIndex = 0; + this.mode = options.mode || 'edit'; // edit | preview | read-only if ('preview' === this.mode) { this.autoScroll = options.autoScroll || true; @@ -121,10 +121,10 @@ function MultiLineEditTextView(options) { this.tabSwitchesView = options.tabSwitchesView || false; } // - // cursorPos represents zero-based row, col positions - // within the editor itself + // cursorPos represents zero-based row, col positions + // within the editor itself // - this.cursorPos = { col : 0, row : 0 }; + this.cursorPos = { col : 0, row : 0 }; this.getSGRFor = function(sgrFor) { return { @@ -140,7 +140,7 @@ function MultiLineEditTextView(options) { return 'preview' === self.mode; }; - // :TODO: Most of the calls to this could be avoided via incrementRow(), decrementRow() that keeps track or such + // :TODO: Most of the calls to this could be avoided via incrementRow(), decrementRow() that keeps track or such this.getTextLinesIndex = function(row) { if(!_.isNumber(row)) { row = self.cursorPos.row; @@ -172,34 +172,34 @@ function MultiLineEditTextView(options) { this.redrawRows = function(startRow, endRow) { self.toggleTextCursor('hide'); - const startIndex = self.getTextLinesIndex(startRow); - const endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length); - const absPos = self.getAbsolutePosition(startRow, 0); + const startIndex = self.getTextLinesIndex(startRow); + const endIndex = Math.min(self.getTextLinesIndex(endRow), self.textLines.length); + const absPos = self.getAbsolutePosition(startRow, 0); for(let i = startIndex; i < endIndex; ++i) { //${self.getSGRFor('text')} self.client.term.write( `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, - false // convertLineFeeds + false // convertLineFeeds ); } self.toggleTextCursor('show'); - return absPos.row - self.position.row; // row we ended on + return absPos.row - self.position.row; // row we ended on }; this.eraseRows = function(startRow, endRow) { self.toggleTextCursor('hide'); - const absPos = self.getAbsolutePosition(startRow, 0); - const absPosEnd = self.getAbsolutePosition(endRow, 0); - const eraseFiller = ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' '); + const absPos = self.getAbsolutePosition(startRow, 0); + const absPosEnd = self.getAbsolutePosition(endRow, 0); + const eraseFiller = ' '.repeat(self.dimens.width);//new Array(self.dimens.width).join(' '); while(absPos.row < absPosEnd.row) { self.client.term.write( `${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`, - false // convertLineFeeds + false // convertLineFeeds ); } @@ -213,16 +213,16 @@ function MultiLineEditTextView(options) { self.eraseRows(lastRow, self.dimens.height); /* - // :TOOD: create eraseRows(startRow, endRow) - if(lastRow < self.dimens.height) { - var absPos = self.getAbsolutePosition(lastRow, 0); - var empty = new Array(self.dimens.width).join(' '); - while(lastRow++ < self.dimens.height) { - self.client.term.write(ansi.goto(absPos.row++, absPos.col)); - self.client.term.write(empty); - } - } - */ + // :TOOD: create eraseRows(startRow, endRow) + if(lastRow < self.dimens.height) { + var absPos = self.getAbsolutePosition(lastRow, 0); + var empty = new Array(self.dimens.width).join(' '); + while(lastRow++ < self.dimens.height) { + self.client.term.write(ansi.goto(absPos.row++, absPos.col)); + self.client.term.write(empty); + } + } + */ }; this.getVisibleText = function(index) { @@ -262,12 +262,12 @@ function MultiLineEditTextView(options) { }; this.getRenderText = function(index) { - let text = self.getVisibleText(index); - const remain = self.dimens.width - text.length; + let text = self.getVisibleText(index); + const remain = self.dimens.width - text.length; if(remain > 0) { text += ' '.repeat(remain + 1); - // text += new Array(remain + 1).join(' '); + // text += new Array(remain + 1).join(' '); } return text; @@ -278,15 +278,15 @@ function MultiLineEditTextView(options) { if(startIndex === endIndex) { lines = [ self.textLines[startIndex] ]; } else { - lines = self.textLines.slice(startIndex, endIndex + 1); // "slice extracts up to but not including end." + lines = self.textLines.slice(startIndex, endIndex + 1); // "slice extracts up to but not including end." } return lines; }; this.getOutputText = function(startIndex, endIndex, eolMarker, options) { - const lines = self.getTextLines(startIndex, endIndex); - let text = ''; - const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); + const lines = self.getTextLines(startIndex, endIndex); + let text = ''; + const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); lines.forEach(line => { text += line.text.replace(re, '\t'); @@ -317,28 +317,28 @@ function MultiLineEditTextView(options) { }; /* - this.editTextAtPosition = function(editAction, text, index, col) { - switch(editAction) { - case 'insert' : - self.insertCharactersInText(text, index, col); - break; + this.editTextAtPosition = function(editAction, text, index, col) { + switch(editAction) { + case 'insert' : + self.insertCharactersInText(text, index, col); + break; - case 'deleteForward' : - break; + case 'deleteForward' : + break; - case 'deleteBack' : - break; + case 'deleteBack' : + break; - case 'replace' : - break; - } - }; - */ + case 'replace' : + break; + } + }; + */ this.updateTextWordWrap = function(index) { - const nextEolIndex = self.getNextEndOfLineIndex(index); - const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); - const newLines = wrapped.wrapped.map(l => { return { text : l }; } ); + const nextEolIndex = self.getNextEndOfLineIndex(index); + const wrapped = self.wordWrapSingleLine(self.getContiguousText(index, nextEolIndex), 'tabsIntact'); + const newLines = wrapped.wrapped.map(l => { return { text : l }; } ); newLines[newLines.length - 1].eol = true; @@ -352,17 +352,17 @@ function MultiLineEditTextView(options) { this.removeCharactersFromText = function(index, col, operation, count) { if('delete' === operation) { self.textLines[index].text = - self.textLines[index].text.slice(0, col) + - self.textLines[index].text.slice(col + count); + self.textLines[index].text.slice(0, col) + + self.textLines[index].text.slice(col + count); self.updateTextWordWrap(index); self.redrawRows(self.cursorPos.row, self.dimens.height); self.moveClientCursorToCursorPos(); } else if ('backspace' === operation) { - // :TODO: method for splicing text + // :TODO: method for splicing text self.textLines[index].text = - self.textLines[index].text.slice(0, col - (count - 1)) + - self.textLines[index].text.slice(col + 1); + self.textLines[index].text.slice(0, col - (count - 1)) + + self.textLines[index].text.slice(col + 1); self.cursorPos.col -= (count - 1); @@ -372,14 +372,14 @@ function MultiLineEditTextView(options) { self.moveClientCursorToCursorPos(); } else if('delete line' === operation) { // - // Delete a visible line. Note that this is *not* the "physical" line, or - // 1:n entries up to eol! This is to keep consistency with home/end, and - // some other text editors such as nano. Sublime for example want to - // treat all of these things using the physical approach, but this seems - // a bit odd in this context. + // Delete a visible line. Note that this is *not* the "physical" line, or + // 1:n entries up to eol! This is to keep consistency with home/end, and + // some other text editors such as nano. Sublime for example want to + // treat all of these things using the physical approach, but this seems + // a bit odd in this context. // - var isLastLine = (index === self.textLines.length - 1); - var hadEol = self.textLines[index].eol; + var isLastLine = (index === self.textLines.length - 1); + var hadEol = self.textLines[index].eol; self.textLines.splice(index, 1); if(hadEol && self.textLines.length > index && !self.textLines[index].eol) { @@ -387,11 +387,11 @@ function MultiLineEditTextView(options) { } // - // Create a empty edit buffer if necessary - // :TODO: Make this a method + // Create a empty edit buffer if necessary + // :TODO: Make this a method if(self.textLines.length < 1) { self.textLines = [ { text : '', eol : true } ]; - isLastLine = false; // resetting + isLastLine = false; // resetting } self.cursorPos.col = 0; @@ -400,7 +400,7 @@ function MultiLineEditTextView(options) { self.eraseRows(lastRow, self.dimens.height); // - // If we just deleted the last line in the buffer, move up + // If we just deleted the last line in the buffer, move up // if(isLastLine) { self.cursorEndOfPreviousLine(); @@ -411,8 +411,8 @@ function MultiLineEditTextView(options) { }; this.insertCharactersInText = function(c, index, col) { - const prevTextLength = self.getTextLength(index); - let editingEol = self.cursorPos.col === prevTextLength; + const prevTextLength = self.getTextLength(index); + let editingEol = self.cursorPos.col === prevTextLength; self.textLines[index].text = [ self.textLines[index].text.slice(0, col), @@ -424,43 +424,43 @@ function MultiLineEditTextView(options) { if(self.getTextLength(index) > self.dimens.width) { // - // Update word wrapping and |cursorOffset| if the cursor - // was within the bounds of the wrapped text + // Update word wrapping and |cursorOffset| if the cursor + // was within the bounds of the wrapped text // let cursorOffset; - const lastCol = self.cursorPos.col - c.length; - const firstWrapRange = self.updateTextWordWrap(index); + const lastCol = self.cursorPos.col - c.length; + const firstWrapRange = self.updateTextWordWrap(index); if(lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { cursorOffset = self.cursorPos.col - firstWrapRange.start; - editingEol = true; //override + editingEol = true; //override } else { cursorOffset = firstWrapRange.end; } - // redraw from current row to end of visible area + // redraw from current row to end of visible area self.redrawRows(self.cursorPos.row, self.dimens.height); - // If we're editing mid, we're done here. Else, we need to - // move the cursor to the new editing position after a wrap + // If we're editing mid, we're done here. Else, we need to + // move the cursor to the new editing position after a wrap if(editingEol) { self.cursorBeginOfNextLine(); self.cursorPos.col += cursorOffset; self.client.term.rawWrite(ansi.right(cursorOffset)); } else { - // adjust cursor after drawing new rows + // adjust cursor after drawing new rows const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); self.client.term.rawWrite(ansi.goto(absPos.row, absPos.col)); } } else { // - // We must only redraw from col -> end of current visible line + // We must only redraw from col -> end of current visible line // const absPos = self.getAbsolutePosition(self.cursorPos.row, self.cursorPos.col); const renderText = self.getRenderText(index).slice(self.cursorPos.col - c.length); self.client.term.write( `${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto(absPos.row, absPos.col)}${ansi.showCursor()}`, - false // convertLineFeeds + false // convertLineFeeds ); } }; @@ -502,24 +502,24 @@ function MultiLineEditTextView(options) { return wordWrapText( line, { - width : self.dimens.width, - tabHandling : tabHandling, - tabWidth : self.tabWidth, - tabChar : '\t', + width : self.dimens.width, + tabHandling : tabHandling, + tabWidth : self.tabWidth, + tabChar : '\t', } ); }; this.setTextLines = function(lines, index, termWithEol) { if(0 === index && (0 === self.textLines.length || (self.textLines.length === 1 && '' === self.textLines[0].text) )) { - // quick path: just set the things + // quick path: just set the things self.textLines = lines.slice(0, -1).map(l => { return { text : l }; }).concat( { text : lines[lines.length - 1], eol : termWithEol } ); } else { - // insert somewhere in textLines... + // insert somewhere in textLines... if(index > self.textLines.length) { - // fill with empty + // fill with empty self.textLines.splice( self.textLines.length, 0, @@ -547,7 +547,7 @@ function MultiLineEditTextView(options) { let index = 0; text.forEach(line => { - self.setTextLines( [ line ], index, true); // true=termWithEol + self.setTextLines( [ line ], index, true); // true=termWithEol index += 1; }); @@ -565,12 +565,12 @@ function MultiLineEditTextView(options) { ansiPrep( ansi, { - termWidth : this.client.term.termWidth, - termHeight : this.client.term.termHeight, - cols : this.dimens.width, - rows : 'auto', - startCol : this.position.col, - forceLineTerm : options.forceLineTerm, + termWidth : this.client.term.termWidth, + termHeight : this.client.term.termHeight, + cols : this.dimens.width, + rows : 'auto', + startCol : this.position.col, + forceLineTerm : options.forceLineTerm, }, (err, preppedAnsi) => { return setLines(err ? ansi : preppedAnsi); @@ -580,31 +580,31 @@ function MultiLineEditTextView(options) { this.insertRawText = function(text, index, col) { // - // Perform the following on |text|: - // * Normalize various line feed formats -> \n - // * Remove some control characters (e.g. \b) - // * Word wrap lines such that they fit in the visible workspace. - // Each actual line will then take 1:n elements in textLines[]. - // * Each tab will be appropriately expanded and take 1:n \t - // characters. This allows us to know when we're in tab space - // when doing cursor movement/etc. + // Perform the following on |text|: + // * Normalize various line feed formats -> \n + // * Remove some control characters (e.g. \b) + // * Word wrap lines such that they fit in the visible workspace. + // Each actual line will then take 1:n elements in textLines[]. + // * Each tab will be appropriately expanded and take 1:n \t + // characters. This allows us to know when we're in tab space + // when doing cursor movement/etc. // // - // Try to handle any possible newline that can be fed to us. - // See http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line + // Try to handle any possible newline that can be fed to us. + // See http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line // - // :TODO: support index/col insertion point + // :TODO: support index/col insertion point if(_.isNumber(index)) { if(_.isNumber(col)) { // - // Modify text to have information from index - // before and and after column + // Modify text to have information from index + // before and and after column // - // :TODO: Need to clean this string (e.g. collapse tabs) + // :TODO: Need to clean this string (e.g. collapse tabs) text = self.textLines; - // :TODO: Remove original line @ index + // :TODO: Remove original line @ index } } else { index = self.textLines.length; @@ -616,7 +616,7 @@ function MultiLineEditTextView(options) { text.forEach(line => { wrapped = self.wordWrapSingleLine(line, 'expand').wrapped; - self.setTextLines(wrapped, index, true); // true=termWithEol + self.setTextLines(wrapped, index, true); // true=termWithEol index += wrapped.length; }); }; @@ -638,16 +638,16 @@ function MultiLineEditTextView(options) { var index = self.getTextLinesIndex(); // - // :TODO: stuff that needs to happen - // * Break up into smaller methods - // * Even in overtype mode, word wrapping must apply if past bounds - // * A lot of this can be used for backspacing also - // * See how Sublime treats tabs in *non* overtype mode... just overwrite them? + // :TODO: stuff that needs to happen + // * Break up into smaller methods + // * Even in overtype mode, word wrapping must apply if past bounds + // * A lot of this can be used for backspacing also + // * See how Sublime treats tabs in *non* overtype mode... just overwrite them? // // if(self.overtypeMode) { - // :TODO: special handing for insert over eol mark? + // :TODO: special handing for insert over eol mark? self.replaceCharacterInText(c, index, self.cursorPos.col); self.cursorPos.col++; self.client.term.write(c); @@ -754,7 +754,7 @@ function MultiLineEditTextView(options) { self.adjustCursorIfPastEndOfLine(true); } else { self.cursorPos.row = 0; - self.moveClientCursorToCursorPos(); // :TODO: ajust if eol, etc. + self.moveClientCursorToCursorPos(); // :TODO: ajust if eol, etc. } self.emitEditPosition(); @@ -773,13 +773,13 @@ function MultiLineEditTextView(options) { this.keyPressLineFeed = function() { // - // Break up text from cursor position, redraw, and update cursor - // position to start of next line + // Break up text from cursor position, redraw, and update cursor + // position to start of next line // - var index = self.getTextLinesIndex(); - var nextEolIndex = self.getNextEndOfLineIndex(index); - var text = self.getContiguousText(index, nextEolIndex); - const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; + var index = self.getTextLinesIndex(); + var nextEolIndex = self.getNextEndOfLineIndex(index); + var text = self.getContiguousText(index, nextEolIndex); + const newLines = self.wordWrapSingleLine(text.slice(self.cursorPos.col), 'tabsIntact').wrapped; newLines.unshift( { text : text.slice(0, self.cursorPos.col), eol : true } ); for(var i = 1; i < newLines.length; ++i) { @@ -791,7 +791,7 @@ function MultiLineEditTextView(options) { self.textLines, [ index, (nextEolIndex - index) + 1 ].concat(newLines)); - // redraw from current row to end of visible area + // redraw from current row to end of visible area self.redrawRows(self.cursorPos.row, self.dimens.height); self.cursorBeginOfNextLine(); @@ -812,8 +812,8 @@ function MultiLineEditTextView(options) { this.keyPressBackspace = function() { if(self.cursorPos.col >= 1) { // - // Don't want to delete character at cursor, but rather the character - // to the left of the cursor! + // Don't want to delete character at cursor, but rather the character + // to the left of the cursor! // self.cursorPos.col -= 1; @@ -842,12 +842,12 @@ function MultiLineEditTextView(options) { count); } else { // - // Delete character at end of line previous. - // * This may be a eol marker - // * Word wrapping will need re-applied + // Delete character at end of line previous. + // * This may be a eol marker + // * Word wrapping will need re-applied // - // :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev - self.keyPressLeft(); // same as hitting left - jump to previous line + // :TODO: apply word wrapping such that text can be re-adjusted if it can now fit on prev + self.keyPressLeft(); // same as hitting left - jump to previous line //self.keyPressBackspace(); } @@ -859,7 +859,7 @@ function MultiLineEditTextView(options) { if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) { // - // Start of line and nothing left. Just delete the line + // Start of line and nothing left. Just delete the line // self.removeCharactersFromText( lineIndex, @@ -906,7 +906,7 @@ function MultiLineEditTextView(options) { var move; switch(direction) { // - // Next tabstop to the right + // Next tabstop to the right // case 'right' : move = self.getNextTabStop(self.cursorPos.col) - self.cursorPos.col; @@ -915,7 +915,7 @@ function MultiLineEditTextView(options) { break; // - // Next tabstop to the left + // Next tabstop to the left // case 'left' : move = self.cursorPos.col - self.getPrevTabStop(self.cursorPos.col); @@ -926,7 +926,7 @@ function MultiLineEditTextView(options) { case 'up' : case 'down' : // - // Jump to the tabstop nearest the cursor + // Jump to the tabstop nearest the cursor // var newCol = self.tabStops.reduce(function r(prev, curr) { return (Math.abs(curr - self.cursorPos.col) < Math.abs(prev - self.cursorPos.col) ? curr : prev); @@ -946,43 +946,43 @@ function MultiLineEditTextView(options) { return true; } - return false; // did not fall on a tab + return false; // did not fall on a tab }; this.cursorStartOfDocument = function() { - self.topVisibleIndex = 0; - self.cursorPos = { row : 0, col : 0 }; + self.topVisibleIndex = 0; + self.cursorPos = { row : 0, col : 0 }; self.redraw(); self.moveClientCursorToCursorPos(); }; this.cursorEndOfDocument = function() { - self.topVisibleIndex = Math.max(self.textLines.length - self.dimens.height, 0); - self.cursorPos.row = (self.textLines.length - self.topVisibleIndex) - 1; - self.cursorPos.col = self.getTextEndOfLineColumn(); + self.topVisibleIndex = Math.max(self.textLines.length - self.dimens.height, 0); + self.cursorPos.row = (self.textLines.length - self.topVisibleIndex) - 1; + self.cursorPos.col = self.getTextEndOfLineColumn(); self.redraw(); self.moveClientCursorToCursorPos(); }; this.cursorBeginOfNextLine = function() { - // e.g. when scrolling right past eol + // e.g. when scrolling right past eol var linesBelow = self.getRemainingLinesBelowRow(); if(linesBelow > 0) { - var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; + var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; if(self.cursorPos.row < lastVisibleRow) { self.cursorPos.row++; } else { self.scrollDocumentUp(); } - self.keyPressHome(); // same as pressing 'home' + self.keyPressHome(); // same as pressing 'home' } }; this.cursorEndOfPreviousLine = function() { - // e.g. when scrolling left past start of line + // e.g. when scrolling left past start of line var moveToEnd; if(self.cursorPos.row > 0) { self.cursorPos.row--; @@ -993,30 +993,30 @@ function MultiLineEditTextView(options) { } if(moveToEnd) { - self.keyPressEnd(); // same as pressing 'end' + self.keyPressEnd(); // same as pressing 'end' } }; /* - this.cusorEndOfNextLine = function() { - var linesBelow = self.getRemainingLinesBelowRow(); + this.cusorEndOfNextLine = function() { + var linesBelow = self.getRemainingLinesBelowRow(); - if(linesBelow > 0) { - var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; - if(self.cursorPos.row < lastVisibleRow) { - self.cursorPos.row++; - } else { - self.scrollDocumentUp(); - } - self.keyPressEnd(); // same as pressing 'end' - } - }; - */ + if(linesBelow > 0) { + var lastVisibleRow = Math.min(self.dimens.height, self.textLines.length) - 1; + if(self.cursorPos.row < lastVisibleRow) { + self.cursorPos.row++; + } else { + self.scrollDocumentUp(); + } + self.keyPressEnd(); // same as pressing 'end' + } + }; + */ this.scrollDocumentUp = function() { // - // Note: We scroll *up* when the cursor goes *down* beyond - // the visible area! + // Note: We scroll *up* when the cursor goes *down* beyond + // the visible area! // var linesBelow = self.getRemainingLinesBelowRow(); if(linesBelow > 0) { @@ -1027,8 +1027,8 @@ function MultiLineEditTextView(options) { this.scrollDocumentDown = function() { // - // Note: We scroll *down* when the cursor goes *up* beyond - // the visible area! + // Note: We scroll *down* when the cursor goes *up* beyond + // the visible area! // if(self.topVisibleIndex > 0) { self.topVisibleIndex--; @@ -1037,7 +1037,7 @@ function MultiLineEditTextView(options) { }; this.emitEditPosition = function() { - self.emit('edit position', self.getEditPosition()); + self.emit('edit position', self.getEditPosition()); }; this.toggleTextEditMode = function() { @@ -1045,7 +1045,7 @@ function MultiLineEditTextView(options) { self.emit('text edit mode', self.getTextEditMode()); }; - this.insertRawText(''); // init to blank/empty + this.insertRawText(''); // init to blank/empty } require('util').inherits(MultiLineEditTextView, View); @@ -1074,11 +1074,11 @@ MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode this.addText(text, options); /*this.insertRawText(text); - if(this.isEditMode()) { - this.cursorEndOfDocument(); - } else if(this.isPreviewMode()) { - this.cursorStartOfDocument(); - }*/ + if(this.isEditMode()) { + this.cursorEndOfDocument(); + } else if(this.isPreviewMode()) { + this.cursorStartOfDocument(); + }*/ }; MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : false }, cb) { @@ -1116,14 +1116,14 @@ MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms : MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'mode' : + case 'mode' : this.mode = value; if('preview' === value && !this.specialKeyMap.next) { this.specialKeyMap.next = [ 'tab' ]; } break; - case 'autoScroll' : + case 'autoScroll' : this.autoScroll = value; break; @@ -1205,9 +1205,9 @@ MultiLineEditTextView.prototype.getEditPosition = function() { var currentIndex = this.getTextLinesIndex() + 1; return { - row : this.getTextLinesIndex(this.cursorPos.row), - col : this.cursorPos.col, - percent : Math.floor(((currentIndex / this.textLines.length) * 100)), - below : this.getRemainingLinesBelowRow(), + row : this.getTextLinesIndex(this.cursorPos.row), + col : this.cursorPos.col, + percent : Math.floor(((currentIndex / this.textLines.length) * 100)), + below : this.getRemainingLinesBelowRow(), }; }; diff --git a/core/new_scan.js b/core/new_scan.js index b84eab52..974519af 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -1,24 +1,24 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const msgArea = require('./message_area.js'); -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const stringFormat = require('./string_format.js'); -const FileEntry = require('./file_entry.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const Errors = require('./enig_error.js').Errors; -const { getAvailableFileAreaTags } = require('./file_base_area.js'); +// ENiGMA½ +const msgArea = require('./message_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const stringFormat = require('./string_format.js'); +const FileEntry = require('./file_entry.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const Errors = require('./enig_error.js').Errors; +const { getAvailableFileAreaTags } = require('./file_base_area.js'); -// deps -const _ = require('lodash'); -const async = require('async'); +// deps +const _ = require('lodash'); +const async = require('async'); exports.moduleInfo = { - name : 'New Scan', - desc : 'Performs a new scan against various areas of the system', - author : 'NuSkooler', + name : 'New Scan', + desc : 'Performs a new scan against various areas of the system', + author : 'NuSkooler', }; /* @@ -30,15 +30,15 @@ exports.moduleInfo = { */ const MciCodeIds = { - ScanStatusLabel : 1, // TL1 - ScanStatusList : 2, // VM2 (appends) + ScanStatusLabel : 1, // TL1 + ScanStatusList : 2, // VM2 (appends) }; const Steps = { - MessageConfs : 'messageConferences', - FileBase : 'fileBase', + MessageConfs : 'messageConferences', + FileBase : 'fileBase', - Finished : 'finished', + Finished : 'finished', }; exports.getModule = class NewScanModule extends MenuModule { @@ -46,17 +46,17 @@ exports.getModule = class NewScanModule extends MenuModule { super(options); - this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false); + this.newScanFullExit = _.get(options, 'lastMenuResult.fullExit', false); - this.currentStep = Steps.MessageConfs; - this.currentScanAux = {}; + this.currentStep = Steps.MessageConfs; + this.currentScanAux = {}; // :TODO: Make this conf/area specific: - const config = this.menuConfig.config; - this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; - this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; - this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; - this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; + const config = this.menuConfig.config; + this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; + this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; + this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; + this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; } updateScanStatus(statusText) { @@ -76,9 +76,9 @@ exports.getModule = class NewScanModule extends MenuModule { }); // - // Sort conferences by name, other than 'system_internal' which should - // always come first such that we display private mails/etc. before - // other conferences & areas + // Sort conferences by name, other than 'system_internal' which should + // always come first such that we display private mails/etc. before + // other conferences & areas // this.sortedMessageConfs.sort((a, b) => { if('system_internal' === a.confTag) { @@ -110,23 +110,23 @@ exports.getModule = class NewScanModule extends MenuModule { newScanMessageArea(conf, cb) { // :TODO: it would be nice to cache this - must be done by conf! const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); - const currentArea = sortedAreas[this.currentScanAux.area]; + const currentArea = sortedAreas[this.currentScanAux.area]; // - // Scan and update index until we find something. If results are found, - // we'll goto the list module & show them. + // Scan and update index until we find something. If results are found, + // we'll goto the list module & show them. // const self = this; async.waterfall( [ function checkAndUpdateIndex(callback) { - // Advance to next area if possible + // Advance to next area if possible if(sortedAreas.length >= self.currentScanAux.area + 1) { self.currentScanAux.area += 1; return callback(null); } else { self.updateScanStatus(self.scanCompleteMsg); - return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan + return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan } }, function updateStatusScanStarted(callback) { @@ -147,7 +147,7 @@ exports.getModule = class NewScanModule extends MenuModule { }, function displayMessageList(newMessageCount) { if(newMessageCount <= 0) { - return self.newScanMessageArea(conf, cb); // next area, if any + return self.newScanMessageArea(conf, cb); // next area, if any } const nextModuleOpts = { @@ -166,11 +166,11 @@ exports.getModule = class NewScanModule extends MenuModule { } newScanFileBase(cb) { - // :TODO: add in steps + // :TODO: add in steps const filterCriteria = { newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), - areaTag : getAvailableFileAreaTags(this.client), - order : 'ascending', // oldest first + areaTag : getAvailableFileAreaTags(this.client), + order : 'ascending', // oldest first }; FileEntry.findFiles( @@ -195,14 +195,14 @@ exports.getModule = class NewScanModule extends MenuModule { getSaveState() { return { - currentStep : this.currentStep, - currentScanAux : this.currentScanAux, + currentStep : this.currentStep, + currentScanAux : this.currentScanAux, }; } restoreSavedState(savedState) { - this.currentStep = savedState.currentStep; - this.currentScanAux = savedState.currentScanAux; + this.currentStep = savedState.currentStep; + this.currentScanAux = savedState.currentScanAux; } performScanCurrentStep(cb) { @@ -227,7 +227,7 @@ exports.getModule = class NewScanModule extends MenuModule { mciReady(mciData, cb) { if(this.newScanFullExit) { - // user has canceled the entire scan @ message list view + // user has canceled the entire scan @ message list view return cb(null); } @@ -236,18 +236,18 @@ exports.getModule = class NewScanModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - // :TODO: display scan step/etc. + // :TODO: display scan step/etc. async.series( [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, + callingMenu : self, + mciMap : mciData.menu, + noInput : true, }; vc.loadFromMenuConfig(loadOpts, callback); diff --git a/core/nua.js b/core/nua.js index 011ad943..7eafe16d 100644 --- a/core/nua.js +++ b/core/nua.js @@ -1,24 +1,24 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const User = require('./user.js'); -const theme = require('./theme.js'); -const login = require('./system_menu_method.js').login; -const Config = require('./config.js').get; -const messageArea = require('./message_area.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const User = require('./user.js'); +const theme = require('./theme.js'); +const login = require('./system_menu_method.js').login; +const Config = require('./config.js').get; +const messageArea = require('./message_area.js'); exports.moduleInfo = { - name : 'NUA', - desc : 'New User Application', + name : 'NUA', + desc : 'New User Application', }; const MciViewIds = { - userName : 1, - password : 9, - confirm : 10, - errMsg : 11, + userName : 1, + password : 9, + confirm : 10, + errMsg : 11, }; exports.getModule = class NewUserAppModule extends MenuModule { @@ -30,7 +30,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { this.menuMethods = { // - // Validation stuff + // Validation stuff // validatePassConfirmMatch : function(data, cb) { const passwordView = self.viewControllers.menu.getView(MciViewIds.password); @@ -58,7 +58,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { // - // Submit handlers + // Submit handlers // submitApplication : function(formData, extraArgs, cb) { const newUser = new User(); @@ -67,33 +67,33 @@ exports.getModule = class NewUserAppModule extends MenuModule { newUser.username = formData.value.username; // - // We have to disable ACS checks for initial default areas as the user is not yet ready + // We have to disable ACS checks for initial default areas as the user is not yet ready // - let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck - let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck + let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck + let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck // can't store undefined! confTag = confTag || ''; areaTag = areaTag || ''; newUser.properties = { - real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format - sex : formData.value.sex, - location : formData.value.location, - affiliation : formData.value.affils, - email_address : formData.value.email, - web_address : formData.value.web, - account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format + real_name : formData.value.realName, + birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format + sex : formData.value.sex, + location : formData.value.location, + affiliation : formData.value.affils, + email_address : formData.value.email, + web_address : formData.value.web, + account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format message_conf_tag : confTag, message_area_tag : areaTag, - term_height : self.client.term.termHeight, - term_width : self.client.term.termWidth, + term_height : self.client.term.termHeight, + term_width : self.client.term.termWidth, - // :TODO: Other defaults - // :TODO: should probably have a place to create defaults/etc. + // :TODO: Other defaults + // :TODO: should probably have a place to create defaults/etc. }; if('*' === config.defaults.theme) { @@ -102,7 +102,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { newUser.properties.theme_id = config.defaults.theme; } - // :TODO: User.create() should validate email uniqueness! + // :TODO: User.create() should validate email uniqueness! newUser.create(formData.value.password, err => { if(err) { self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); @@ -116,12 +116,12 @@ exports.getModule = class NewUserAppModule extends MenuModule { } else { self.client.log.info( { username : formData.value.username, userId : newUser.userId }, 'New user created'); - // Cache SysOp information now - // :TODO: Similar to bbs.js. DRY + // Cache SysOp information now + // :TODO: Similar to bbs.js. DRY if(newUser.isSysOp()) { config.general.sysOp = { - username : formData.value.username, - properties : newUser.properties, + username : formData.value.username, + properties : newUser.properties, }; } @@ -129,7 +129,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { return self.gotoMenu(extraArgs.inactive, cb); } else { // - // If active now, we need to call login() to authenticate + // If active now, we need to call login() to authenticate // return login(self, formData, extraArgs, cb); } diff --git a/core/onelinerz.js b/core/onelinerz.js index d54d7f4f..2a917ad2 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -1,56 +1,56 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; const { getModDatabasePath, getTransactionDatabase -} = require('./database.js'); +} = require('./database.js'); -const ViewController = require('./view_controller.js').ViewController; -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const stringFormat = require('./string_format.js'); +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const stringFormat = require('./string_format.js'); -// deps -const sqlite3 = require('sqlite3'); -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +// deps +const sqlite3 = require('sqlite3'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); /* - Module :TODO: - * Add pipe code support - - override max length & monitor *display* len as user types in order to allow for actual display len with color - * Add preview control: Shows preview with pipe codes resolved - * Add ability to at least alternate formatStrings -- every other + Module :TODO: + * Add pipe code support + - override max length & monitor *display* len as user types in order to allow for actual display len with color + * Add preview control: Shows preview with pipe codes resolved + * Add ability to at least alternate formatStrings -- every other */ exports.moduleInfo = { - name : 'Onelinerz', - desc : 'Standard local onelinerz', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.onelinerz', + name : 'Onelinerz', + desc : 'Standard local onelinerz', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.onelinerz', }; const MciViewIds = { - ViewForm : { - Entries : 1, - AddPrompt : 2, + ViewForm : { + Entries : 1, + AddPrompt : 2, }, AddForm : { - NewEntry : 1, - EntryPreview : 2, - AddPrompt : 3, + NewEntry : 1, + EntryPreview : 2, + AddPrompt : 3, } }; const FormIds = { - View : 0, - Add : 1, + View : 0, + Add : 1, }; exports.getModule = class OnelinerzModule extends MenuModule { @@ -66,7 +66,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { addEntry : function(formData, extraArgs, cb) { if(_.isString(formData.value.oneliner) && formData.value.oneliner.length > 0) { - const oneliner = formData.value.oneliner.trim(); // remove any trailing ws + const oneliner = formData.value.oneliner.trim(); // remove any trailing ws self.storeNewOneliner(oneliner, err => { if(err) { @@ -74,18 +74,18 @@ exports.getModule = class OnelinerzModule extends MenuModule { } self.clearAddForm(); - return self.displayViewScreen(true, cb); // true=cls + return self.displayViewScreen(true, cb); // true=cls }); } else { - // empty message - treat as if cancel was hit - return self.displayViewScreen(true, cb); // true=cls + // empty message - treat as if cancel was hit + return self.displayViewScreen(true, cb); // true=cls } }, cancelAdd : function(formData, extraArgs, cb) { self.clearAddForm(); - return self.displayViewScreen(true, cb); // true=cls + return self.displayViewScreen(true, cb); // true=cls } }; } @@ -103,7 +103,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { ], err => { if(err) { - // :TODO: Handle me -- initSequence() should really take a completion callback + // :TODO: Handle me -- initSequence() should really take a completion callback } self.finishedLoading(); } @@ -141,9 +141,9 @@ exports.getModule = class OnelinerzModule extends MenuModule { ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -160,16 +160,16 @@ exports.getModule = class OnelinerzModule extends MenuModule { self.db.each( `SELECT * - FROM ( - SELECT * - FROM onelinerz - ORDER BY timestamp DESC - LIMIT ${limit} - ) - ORDER BY timestamp ASC;`, + FROM ( + SELECT * + FROM onelinerz + ORDER BY timestamp DESC + LIMIT ${limit} + ) + ORDER BY timestamp ASC;`, (err, row) => { if(!err) { - row.timestamp = moment(row.timestamp); // convert -> moment + row.timestamp = moment(row.timestamp); // convert -> moment entries.push(row); } }, @@ -179,15 +179,15 @@ exports.getModule = class OnelinerzModule extends MenuModule { ); }, function populateEntries(entriesView, entries, callback) { - const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent - const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma'; + const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent + const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma'; entriesView.setItems(entries.map( e => { return stringFormat(listFormat, { - userId : e.user_id, - username : e.user_name, - oneliner : e.oneliner, - ts : e.timestamp.format(tsFormat), + userId : e.user_id, + username : e.user_name, + oneliner : e.oneliner, + ts : e.timestamp.format(tsFormat), } ); })); @@ -197,7 +197,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { }, function finalPrep(callback) { const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt); - promptView.setFocusItemIndex(1); // default to NO + promptView.setFocusItemIndex(1); // default to NO return callback(null); } ], @@ -235,9 +235,9 @@ exports.getModule = class OnelinerzModule extends MenuModule { ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -278,12 +278,12 @@ exports.getModule = class OnelinerzModule extends MenuModule { function createTables(callback) { self.db.run( `CREATE TABLE IF NOT EXISTS onelinerz ( - id INTEGER PRIMARY KEY, - user_id INTEGER_NOT NULL, - user_name VARCHAR NOT NULL, - oneliner VARCHAR NOT NULL, - timestamp DATETIME NOT NULL - );` + id INTEGER PRIMARY KEY, + user_id INTEGER_NOT NULL, + user_name VARCHAR NOT NULL, + oneliner VARCHAR NOT NULL, + timestamp DATETIME NOT NULL + );` , err => { return callback(err); @@ -297,29 +297,29 @@ exports.getModule = class OnelinerzModule extends MenuModule { } storeNewOneliner(oneliner, cb) { - const self = this; - const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + const self = this; + const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); async.series( [ function addRec(callback) { self.db.run( `INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp) - VALUES (?, ?, ?, ?);`, + VALUES (?, ?, ?, ?);`, [ self.client.user.userId, self.client.user.username, oneliner, ts ], callback ); }, function removeOld(callback) { - // keep 25 max most recent items - remove the older ones + // keep 25 max most recent items - remove the older ones self.db.run( `DELETE FROM onelinerz - WHERE id IN ( - SELECT id - FROM onelinerz - ORDER BY id DESC - LIMIT -1 OFFSET 25 - );`, + WHERE id IN ( + SELECT id + FROM onelinerz + ORDER BY id DESC + LIMIT -1 OFFSET 25 + );`, callback ); } diff --git a/core/plugin_module.js b/core/plugin_module.js index da9410b0..60b878aa 100644 --- a/core/plugin_module.js +++ b/core/plugin_module.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -exports.PluginModule = PluginModule; +exports.PluginModule = PluginModule; function PluginModule(/*options*/) { } diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 584feffa..fe8d4b43 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -1,24 +1,24 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const Log = require('./logger.js').log; -const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; -const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; -const clientConnections = require('./client_connections.js'); -const StatLog = require('./stat_log.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const formatByteSize = require('./string_util.js').formatByteSize; +// ENiGMA½ +const Config = require('./config.js').get; +const Log = require('./logger.js').log; +const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; +const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; +const clientConnections = require('./client_connections.js'); +const StatLog = require('./stat_log.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const formatByteSize = require('./string_util.js').formatByteSize; -// deps -const packageJson = require('../package.json'); -const os = require('os'); -const _ = require('lodash'); -const moment = require('moment'); +// deps +const packageJson = require('../package.json'); +const os = require('os'); +const _ = require('lodash'); +const moment = require('moment'); -exports.getPredefinedMCIValue = getPredefinedMCIValue; -exports.init = init; +exports.getPredefinedMCIValue = getPredefinedMCIValue; +exports.init = init; function init(cb) { setNextRandomRumor(cb); @@ -39,8 +39,8 @@ function setNextRandomRumor(cb) { function getUserRatio(client, propA, propB) { const a = StatLog.getUserStatNum(client.user, propA); - const b = StatLog.getUserStatNum(client.user, propB); - const ratio = ~~((a / b) * 100); + const b = StatLog.getUserStatNum(client.user, propB); + const ratio = ~~((a / b) * 100); return `${ratio}%`; } @@ -54,72 +54,72 @@ function sysStatAsString(statName, defaultValue) { const PREDEFINED_MCI_GENERATORS = { // - // Board + // Board // - BN : function boardName() { return Config().general.boardName; }, + BN : function boardName() { return Config().general.boardName; }, - // ENiGMA - VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, - VN : function version() { return packageJson.version; }, + // ENiGMA + VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, + VN : function version() { return packageJson.version; }, - // +op info - SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, - SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, - SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, - SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, - SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, - SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, - // :TODO: op age, web, ????? + // +op info + SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, + SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, + SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, + SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, + SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, + SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, + // :TODO: op age, web, ????? // - // Current user / session + // Current user / session // - UN : function userName(client) { return client.user.username; }, - UI : function userId(client) { return client.user.userId.toString(); }, - UG : function groups(client) { return _.values(client.user.groups).join(', '); }, - UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, - LO : function location(client) { return userStatAsString(client, 'location', ''); }, - UA : function age(client) { return client.user.getAge().toString(); }, - BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY - US : function sex(client) { return userStatAsString(client, 'sex', ''); }, - UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, - UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, - UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, - UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, - UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, - ND : function connectedNode(client) { return client.node.toString(); }, - IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version - ST : function serverName(client) { return client.session.serverName; }, - FN : function activeFileBaseFilterName(client) { + UN : function userName(client) { return client.user.username; }, + UI : function userId(client) { return client.user.userId.toString(); }, + UG : function groups(client) { return _.values(client.user.groups).join(', '); }, + UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, + LO : function location(client) { return userStatAsString(client, 'location', ''); }, + UA : function age(client) { return client.user.getAge().toString(); }, + BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY + US : function sex(client) { return userStatAsString(client, 'sex', ''); }, + UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, + UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, + UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, + UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, + UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, + ND : function connectedNode(client) { return client.node.toString(); }, + IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version + ST : function serverName(client) { return client.session.serverName; }, + FN : function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); return activeFilter ? activeFilter.name : ''; }, - DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 - DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes + DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 + DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 - UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes + UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 + UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - NR : function userUpDownRatio(client) { // Obv/2 + NR : function userUpDownRatio(client) { // Obv/2 return getUserRatio(client, 'ul_total_count', 'dl_total_count'); }, - KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio + KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes'); }, - MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, - PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, - PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, + MS : function accountCreatedclient(client) { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, + PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, + PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, - MD : function currentMenuDescription(client) { + MD : function currentMenuDescription(client) { return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; }, - MA : function messageAreaName(client) { + MA : function messageAreaName(client) { const area = getMessageAreaByTag(client.user.properties.message_area_tag); return area ? area.name : ''; }, @@ -131,102 +131,102 @@ const PREDEFINED_MCI_GENERATORS = { const area = getMessageAreaByTag(client.user.properties.message_area_tag); return area ? area.desc : ''; }, - CM : function messageConfDescription(client) { + CM : function messageConfDescription(client) { const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); return conf ? conf.desc : ''; }, - SH : function termHeight(client) { return client.term.termHeight.toString(); }, - SW : function termWidth(client) { return client.term.termWidth.toString(); }, + SH : function termHeight(client) { return client.term.termHeight.toString(); }, + SW : function termWidth(client) { return client.term.termWidth.toString(); }, // - // Date/Time + // Date/Time // - // :TODO: change to CD for 'Current Date' - DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, - CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, + // :TODO: change to CD for 'Current Date' + DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, + CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, // - // OS/System Info + // OS/System Info // - OS : function operatingSystem() { + OS : function operatingSystem() { return { - linux : 'Linux', - darwin : 'Mac OS X', - win32 : 'Windows', - sunos : 'SunOS', - freebsd : 'FreeBSD', + linux : 'Linux', + darwin : 'Mac OS X', + win32 : 'Windows', + sunos : 'SunOS', + freebsd : 'FreeBSD', }[os.platform()] || os.type(); }, - OA : function systemArchitecture() { return os.arch(); }, + OA : function systemArchitecture() { return os.arch(); }, - SC : function systemCpuModel() { + SC : function systemCpuModel() { // - // Clean up CPU strings a bit for better display + // Clean up CPU strings a bit for better display // return os.cpus()[0].model .replace(/\(R\)|\(TM\)|processor|CPU/g, '') .replace(/\s+(?= )/g, ''); }, - // :TODO: MCI for core count, e.g. os.cpus().length + // :TODO: MCI for core count, e.g. os.cpus().length - // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage - NV : function nodeVersion() { return process.version; }, + // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage + NV : function nodeVersion() { return process.version; }, - AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, + AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, - TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); }, + TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); }, - RR : function randomRumor() { - // start the process of picking another random one + RR : function randomRumor() { + // start the process of picking another random one setNextRandomRumor(); return StatLog.getSystemStat('random_rumor'); }, // - // System File Base, Up/Download Info + // System File Base, Up/Download Info // - // :TODO: DD - Today's # of downloads (iNiQUiTY) + // :TODO: DD - Today's # of downloads (iNiQUiTY) // - SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, - SO : function systemByteDownload() { + SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, + SO : function systemByteDownload() { const byteSize = StatLog.getSystemStatNum('dl_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); }, - SP : function systemByteUpload() { + SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); }, + SP : function systemByteUpload() { const byteSize = StatLog.getSystemStatNum('ul_total_bytes'); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - TF : function totalFilesOnSystem() { + TF : function totalFilesOnSystem() { const areaStats = StatLog.getSystemStat('file_base_area_stats'); return _.get(areaStats, 'totalFiles', 0).toLocaleString(); }, - TB : function totalBytesOnSystem() { - const areaStats = StatLog.getSystemStat('file_base_area_stats'); - const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); - return formatByteSize(totalBytes, true); // true=withAbbr + TB : function totalBytesOnSystem() { + const areaStats = StatLog.getSystemStat('file_base_area_stats'); + const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); + return formatByteSize(totalBytes, true); // true=withAbbr }, - // :TODO: PT - Messages posted *today* (Obv/2) - // -> Include FTN/etc. - // :TODO: NT - New users today (Obv/2) - // :TODO: CT - Calls *today* (Obv/2) - // :TODO: FT - Files uploaded/added *today* (Obv/2) - // :TODO: DD - Files downloaded *today* (iNiQUiTY) - // :TODO: TP - total message/posts on the system (Obv/2) - // -> Include FTN/etc. - // :TODO: LC - name of last caller to system (Obv/2) - // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) + // :TODO: PT - Messages posted *today* (Obv/2) + // -> Include FTN/etc. + // :TODO: NT - New users today (Obv/2) + // :TODO: CT - Calls *today* (Obv/2) + // :TODO: FT - Files uploaded/added *today* (Obv/2) + // :TODO: DD - Files downloaded *today* (iNiQUiTY) + // :TODO: TP - total message/posts on the system (Obv/2) + // -> Include FTN/etc. + // :TODO: LC - name of last caller to system (Obv/2) + // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) // - // Special handling for XY + // Special handling for XY // - XY : function xyHack() { return; /* nothing */ }, + XY : function xyHack() { return; /* nothing */ }, }; function getPredefinedMCIValue(client, code) { diff --git a/core/rumorz.js b/core/rumorz.js index d51e33e8..153a74ee 100644 --- a/core/rumorz.js +++ b/core/rumorz.js @@ -1,42 +1,42 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const theme = require('./theme.js'); -const resetScreen = require('./ansi_term.js').resetScreen; -const StatLog = require('./stat_log.js'); -const renderStringLength = require('./string_util.js').renderStringLength; -const stringFormat = require('./string_format.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const resetScreen = require('./ansi_term.js').resetScreen; +const StatLog = require('./stat_log.js'); +const renderStringLength = require('./string_util.js').renderStringLength; +const stringFormat = require('./string_format.js'); -// deps -const async = require('async'); -const _ = require('lodash'); +// deps +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Rumorz', - desc : 'Standard local rumorz', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.rumorz', + name : 'Rumorz', + desc : 'Standard local rumorz', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.rumorz', }; -const STATLOG_KEY_RUMORZ = 'system_rumorz'; +const STATLOG_KEY_RUMORZ = 'system_rumorz'; const FormIds = { - View : 0, - Add : 1, + View : 0, + Add : 1, }; const MciCodeIds = { - ViewForm : { - Entries : 1, - AddPrompt : 2, + ViewForm : { + Entries : 1, + AddPrompt : 2, }, AddForm : { - NewEntry : 1, - EntryPreview : 2, - AddPrompt : 3, + NewEntry : 1, + EntryPreview : 2, + AddPrompt : 3, } }; @@ -51,21 +51,21 @@ exports.getModule = class RumorzModule extends MenuModule { addEntry : (formData, extraArgs, cb) => { if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { - const rumor = formData.value.rumor.trim(); // remove any trailing ws + const rumor = formData.value.rumor.trim(); // remove any trailing ws StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { this.clearAddForm(); - return this.displayViewScreen(true, cb); // true=cls + return this.displayViewScreen(true, cb); // true=cls }); } else { - // empty message - treat as if cancel was hit - return this.displayViewScreen(true, cb); // true=cls + // empty message - treat as if cancel was hit + return this.displayViewScreen(true, cb); // true=cls } }, cancelAdd : (formData, extraArgs, cb) => { this.clearAddForm(); - return this.displayViewScreen(true, cb); // true=cls + return this.displayViewScreen(true, cb); // true=cls } }; } @@ -73,12 +73,12 @@ exports.getModule = class RumorzModule extends MenuModule { get config() { return this.menuConfig.config; } clearAddForm() { - const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); - const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); + const newEntryView = this.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); + const previewView = this.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); newEntryView.setText(''); - // preview is optional + // preview is optional if(previewView) { previewView.setText(''); } @@ -98,7 +98,7 @@ exports.getModule = class RumorzModule extends MenuModule { ], err => { if(err) { - // :TODO: Handle me -- initSequence() should really take a completion callback + // :TODO: Handle me -- initSequence() should really take a completion callback } self.finishedLoading(); } @@ -135,9 +135,9 @@ exports.getModule = class RumorzModule extends MenuModule { ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.View, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -155,9 +155,9 @@ exports.getModule = class RumorzModule extends MenuModule { }); }, function populateEntries(entriesView, entries, callback) { - const config = self.config; - const listFormat = config.listFormat || '{rumor}'; - const focusListFormat = config.focusListFormat || listFormat; + const config = self.config; + const listFormat = config.listFormat || '{rumor}'; + const focusListFormat = config.focusListFormat || listFormat; entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) ); entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) ); @@ -167,7 +167,7 @@ exports.getModule = class RumorzModule extends MenuModule { }, function finalPrep(callback) { const promptView = self.viewControllers.view.getView(MciCodeIds.ViewForm.AddPrompt); - promptView.setFocusItemIndex(1); // default to NO + promptView.setFocusItemIndex(1); // default to NO return callback(null); } ], @@ -205,9 +205,9 @@ exports.getModule = class RumorzModule extends MenuModule { ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, + callingMenu : self, + mciMap : artData.mciMap, + formId : FormIds.Add, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -219,8 +219,8 @@ exports.getModule = class RumorzModule extends MenuModule { } }, function initPreviewUpdates(callback) { - const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); - const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); + const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); + const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); if(previewView) { let timerId; entryView.on('key press', () => { diff --git a/core/sauce.js b/core/sauce.js index 29176d8d..6291c16e 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -1,28 +1,28 @@ /* jslint node: true */ 'use strict'; -const Errors = require('./enig_error.js').Errors; +const Errors = require('./enig_error.js').Errors; -// deps -const iconv = require('iconv-lite'); -const { Parser } = require('binary-parser'); +// deps +const iconv = require('iconv-lite'); +const { Parser } = require('binary-parser'); -exports.readSAUCE = readSAUCE; +exports.readSAUCE = readSAUCE; -const SAUCE_SIZE = 128; -const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' +const SAUCE_SIZE = 128; +const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' -// :TODO read comments -//const COMNT_ID = Buffer.from([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' +// :TODO read comments +//const COMNT_ID = Buffer.from([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' -exports.SAUCE_SIZE = SAUCE_SIZE; -// :TODO: SAUCE should be a class -// - with getFontName() -// - ...other methods +exports.SAUCE_SIZE = SAUCE_SIZE; +// :TODO: SAUCE should be a class +// - with getFontName() +// - ...other methods // -// See -// http://www.acid.org/info/sauce/sauce.htm +// See +// http://www.acid.org/info/sauce/sauce.htm // const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; @@ -49,8 +49,8 @@ function readSAUCE(data, cb) { .uint16le('tinfo4') .int8('numComments') .int8('flags') - // :TODO: does this need to be optional? - .buffer('tinfos', { length: 22 } ) // SAUCE 00.5 + // :TODO: does this need to be optional? + .buffer('tinfos', { length: 22 } ) // SAUCE 00.5 .parse(data.slice(data.length - SAUCE_SIZE)); } catch(e) { return cb(Errors.Invalid('Invalid SAUCE record')); @@ -72,22 +72,22 @@ function readSAUCE(data, cb) { } const sauce = { - id : iconv.decode(sauceRec.id, 'cp437'), - version : iconv.decode(sauceRec.version, 'cp437').trim(), - title : iconv.decode(sauceRec.title, 'cp437').trim(), - author : iconv.decode(sauceRec.author, 'cp437').trim(), - group : iconv.decode(sauceRec.group, 'cp437').trim(), - date : iconv.decode(sauceRec.date, 'cp437').trim(), - fileSize : sauceRec.fileSize, - dataType : sauceRec.dataType, - fileType : sauceRec.fileType, - tinfo1 : sauceRec.tinfo1, - tinfo2 : sauceRec.tinfo2, - tinfo3 : sauceRec.tinfo3, - tinfo4 : sauceRec.tinfo4, - numComments : sauceRec.numComments, - flags : sauceRec.flags, - tinfos : sauceRec.tinfos, + id : iconv.decode(sauceRec.id, 'cp437'), + version : iconv.decode(sauceRec.version, 'cp437').trim(), + title : iconv.decode(sauceRec.title, 'cp437').trim(), + author : iconv.decode(sauceRec.author, 'cp437').trim(), + group : iconv.decode(sauceRec.group, 'cp437').trim(), + date : iconv.decode(sauceRec.date, 'cp437').trim(), + fileSize : sauceRec.fileSize, + dataType : sauceRec.dataType, + fileType : sauceRec.fileType, + tinfo1 : sauceRec.tinfo1, + tinfo2 : sauceRec.tinfo2, + tinfo3 : sauceRec.tinfo3, + tinfo4 : sauceRec.tinfo4, + numComments : sauceRec.numComments, + flags : sauceRec.flags, + tinfos : sauceRec.tinfos, }; const dt = SAUCE_DATA_TYPES[sauce.dataType]; @@ -98,51 +98,51 @@ function readSAUCE(data, cb) { return cb(null, sauce); } -// :TODO: These need completed: +// :TODO: These need completed: const SAUCE_DATA_TYPES = { - 0 : { name : 'None' }, - 1 : { name : 'Character', parser : parseCharacterSAUCE }, - 2 : 'Bitmap', - 3 : 'Vector', - 4 : 'Audio', - 5 : 'BinaryText', - 6 : 'XBin', - 7 : 'Archive', - 8 : 'Executable', + 0 : { name : 'None' }, + 1 : { name : 'Character', parser : parseCharacterSAUCE }, + 2 : 'Bitmap', + 3 : 'Vector', + 4 : 'Audio', + 5 : 'BinaryText', + 6 : 'XBin', + 7 : 'Archive', + 8 : 'Executable', }; const SAUCE_CHARACTER_FILE_TYPES = { - 0 : 'ASCII', - 1 : 'ANSi', - 2 : 'ANSiMation', - 3 : 'RIP script', - 4 : 'PCBoard', - 5 : 'Avatar', - 6 : 'HTML', - 7 : 'Source', - 8 : 'TundraDraw', + 0 : 'ASCII', + 1 : 'ANSi', + 2 : 'ANSiMation', + 3 : 'RIP script', + 4 : 'PCBoard', + 5 : 'Avatar', + 6 : 'HTML', + 7 : 'Source', + 8 : 'TundraDraw', }; // -// Map of SAUCE font -> encoding hint +// Map of SAUCE font -> encoding hint // -// Note that this is the same mapping that x84 uses. Be compatible! +// Note that this is the same mapping that x84 uses. Be compatible! // const SAUCE_FONT_TO_ENCODING_HINT = { - 'Amiga MicroKnight' : 'amiga', - 'Amiga MicroKnight+' : 'amiga', - 'Amiga mOsOul' : 'amiga', - 'Amiga P0T-NOoDLE' : 'amiga', - 'Amiga Topaz 1' : 'amiga', - 'Amiga Topaz 1+' : 'amiga', - 'Amiga Topaz 2' : 'amiga', - 'Amiga Topaz 2+' : 'amiga', - 'Atari ATASCII' : 'atari', - 'IBM EGA43' : 'cp437', - 'IBM EGA' : 'cp437', - 'IBM VGA25G' : 'cp437', - 'IBM VGA50' : 'cp437', - 'IBM VGA' : 'cp437', + 'Amiga MicroKnight' : 'amiga', + 'Amiga MicroKnight+' : 'amiga', + 'Amiga mOsOul' : 'amiga', + 'Amiga P0T-NOoDLE' : 'amiga', + 'Amiga Topaz 1' : 'amiga', + 'Amiga Topaz 1+' : 'amiga', + 'Amiga Topaz 2' : 'amiga', + 'Amiga Topaz 2+' : 'amiga', + 'Atari ATASCII' : 'atari', + 'IBM EGA43' : 'cp437', + 'IBM EGA' : 'cp437', + 'IBM VGA25G' : 'cp437', + 'IBM VGA50' : 'cp437', + 'IBM VGA' : 'cp437', }; [ @@ -150,20 +150,20 @@ const SAUCE_FONT_TO_ENCODING_HINT = { '860', '861', '862', '863', '864', '865', '866', '869', '872' ].forEach( page => { const codec = 'cp' + page; - SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; }); function parseCharacterSAUCE(sauce) { const result = {}; - result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; + result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { - // convience: create ansiFlags + // convience: create ansiFlags sauce.ansiFlags = sauce.flags; let i = 0; diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 98fece9b..2e08b6e2 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1,57 +1,57 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MessageScanTossModule = require('../msg_scan_toss_module.js').MessageScanTossModule; -const Config = require('../config.js').get; -const ftnMailPacket = require('../ftn_mail_packet.js'); -const ftnUtil = require('../ftn_util.js'); -const Address = require('../ftn_address.js'); -const Log = require('../logger.js').log; -const ArchiveUtil = require('../archive_util.js'); -const msgDb = require('../database.js').dbs.message; -const Message = require('../message.js'); -const TicFileInfo = require('../tic_file_info.js'); -const Errors = require('../enig_error.js').Errors; -const FileEntry = require('../file_entry.js'); -const scanFile = require('../file_base_area.js').scanFile; -const getFileAreaByTag = require('../file_base_area.js').getFileAreaByTag; -const getDescFromFileName = require('../file_base_area.js').getDescFromFileName; -const copyFileWithCollisionHandling = require('../file_util.js').copyFileWithCollisionHandling; -const getAreaStorageDirectoryByTag = require('../file_base_area.js').getAreaStorageDirectoryByTag; -const isValidStorageTag = require('../file_base_area.js').isValidStorageTag; -const User = require('../user.js'); +// ENiGMA½ +const MessageScanTossModule = require('../msg_scan_toss_module.js').MessageScanTossModule; +const Config = require('../config.js').get; +const ftnMailPacket = require('../ftn_mail_packet.js'); +const ftnUtil = require('../ftn_util.js'); +const Address = require('../ftn_address.js'); +const Log = require('../logger.js').log; +const ArchiveUtil = require('../archive_util.js'); +const msgDb = require('../database.js').dbs.message; +const Message = require('../message.js'); +const TicFileInfo = require('../tic_file_info.js'); +const Errors = require('../enig_error.js').Errors; +const FileEntry = require('../file_entry.js'); +const scanFile = require('../file_base_area.js').scanFile; +const getFileAreaByTag = require('../file_base_area.js').getFileAreaByTag; +const getDescFromFileName = require('../file_base_area.js').getDescFromFileName; +const copyFileWithCollisionHandling = require('../file_util.js').copyFileWithCollisionHandling; +const getAreaStorageDirectoryByTag = require('../file_base_area.js').getAreaStorageDirectoryByTag; +const isValidStorageTag = require('../file_base_area.js').isValidStorageTag; +const User = require('../user.js'); -// deps -const moment = require('moment'); -const _ = require('lodash'); -const paths = require('path'); -const async = require('async'); -const fs = require('graceful-fs'); -const later = require('later'); -const temptmp = require('temptmp').createTrackedSession('ftn_bso'); -const assert = require('assert'); -const sane = require('sane'); -const fse = require('fs-extra'); -const iconv = require('iconv-lite'); -const uuidV4 = require('uuid/v4'); +// deps +const moment = require('moment'); +const _ = require('lodash'); +const paths = require('path'); +const async = require('async'); +const fs = require('graceful-fs'); +const later = require('later'); +const temptmp = require('temptmp').createTrackedSession('ftn_bso'); +const assert = require('assert'); +const sane = require('sane'); +const fse = require('fs-extra'); +const iconv = require('iconv-lite'); +const uuidV4 = require('uuid/v4'); exports.moduleInfo = { - name : 'FTN BSO', - desc : 'BSO style message scanner/tosser for FTN networks', - author : 'NuSkooler', + name : 'FTN BSO', + desc : 'BSO style message scanner/tosser for FTN networks', + author : 'NuSkooler', }; /* - :TODO: - * Support (approx) max bundle size - * Validate packet passwords!!!! - => secure vs insecure landing areas + :TODO: + * Support (approx) max bundle size + * Validate packet passwords!!!! + => secure vs insecure landing areas */ exports.getModule = FTNMessageScanTossModule; -const SCHEDULE_REGEXP = /(?:^|or )?(@watch:|@immediate)([^\0]+)?$/; +const SCHEDULE_REGEXP = /(?:^|or )?(@watch:|@immediate)([^\0]+)?$/; function FTNMessageScanTossModule() { MessageScanTossModule.call(this); @@ -82,7 +82,7 @@ function FTNMessageScanTossModule() { return config.messageNetworks.ftn.networks[networkName].defaultZone; } - // non-explicit: default to local address zone + // non-explicit: default to local address zone const networkLocalAddress = config.messageNetworks.ftn.networks[networkName].localAddress; if(networkLocalAddress) { const addr = Address.fromString(networkLocalAddress); @@ -91,11 +91,11 @@ function FTNMessageScanTossModule() { }; /* - this.isDefaultDomainZone = function(networkName, address) { - const defaultNetworkName = this.getDefaultNetworkName(); - return(networkName === defaultNetworkName && address.zone === this.moduleConfig.defaultZone); - }; - */ + this.isDefaultDomainZone = function(networkName, address) { + const defaultNetworkName = this.getDefaultNetworkName(); + return(networkName === defaultNetworkName && address.zone === this.moduleConfig.defaultZone); + }; + */ this.getNetworkNameByAddress = function(remoteAddress) { return _.findKey(Config().messageNetworks.ftn.networks, network => { @@ -112,7 +112,7 @@ function FTNMessageScanTossModule() { }; this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) { - ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper + ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper return _.findKey(Config().messageNetworks.ftn.areas, areaConf => { return areaConf.tag.toUpperCase() === ftnAreaTag; }); @@ -123,41 +123,41 @@ function FTNMessageScanTossModule() { }; /* - this.getSeenByAddresses = function(messageSeenBy) { - if(!_.isArray(messageSeenBy)) { - messageSeenBy = [ messageSeenBy ]; - } + this.getSeenByAddresses = function(messageSeenBy) { + if(!_.isArray(messageSeenBy)) { + messageSeenBy = [ messageSeenBy ]; + } - let seenByAddrs = []; - messageSeenBy.forEach(sb => { - seenByAddrs = seenByAddrs.concat(ftnUtil.parseAbbreviatedNetNodeList(sb)); - }); - return seenByAddrs; - }; - */ + let seenByAddrs = []; + messageSeenBy.forEach(sb => { + seenByAddrs = seenByAddrs.concat(ftnUtil.parseAbbreviatedNetNodeList(sb)); + }); + return seenByAddrs; + }; + */ this.messageHasValidMSGID = function(msg) { return _.isString(msg.meta.FtnKludge.MSGID) && msg.meta.FtnKludge.MSGID.length > 0; }; /* - this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { - let dir = this.moduleConfig.paths.outbound; - if(!this.isDefaultDomainZone(networkName, destAddress)) { - const hexZone = `000${destAddress.zone.toString(16)}`.substr(-3); - dir = paths.join(dir, `${networkName.toLowerCase()}.${hexZone}`); - } - return dir; - }; - */ + this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { + let dir = this.moduleConfig.paths.outbound; + if(!this.isDefaultDomainZone(networkName, destAddress)) { + const hexZone = `000${destAddress.zone.toString(16)}`.substr(-3); + dir = paths.join(dir, `${networkName.toLowerCase()}.${hexZone}`); + } + return dir; + }; + */ this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { networkName = networkName.toLowerCase(); let dir = this.moduleConfig.paths.outbound; - const defaultNetworkName = this.getDefaultNetworkName(); - const defaultZone = this.getDefaultZone(networkName); + const defaultNetworkName = this.getDefaultNetworkName(); + const defaultZone = this.getDefaultZone(networkName); let zoneExt; if(defaultZone !== destAddress.zone) { @@ -177,24 +177,24 @@ function FTNMessageScanTossModule() { this.getOutgoingPacketFileName = function(basePath, messageId, isTemp, fileCase) { // - // Generating an outgoing packet file name comes with a few issues: - // * We must use DOS 8.3 filenames due to legacy systems that receive - // the packet not understanding LFNs - // * We need uniqueness; This is especially important with packets that - // end up in bundles and on the receiving/remote system where conflicts - // with other systems could also occur + // Generating an outgoing packet file name comes with a few issues: + // * We must use DOS 8.3 filenames due to legacy systems that receive + // the packet not understanding LFNs + // * We need uniqueness; This is especially important with packets that + // end up in bundles and on the receiving/remote system where conflicts + // with other systems could also occur // - // There are a lot of systems in use here for the name: - // * HEX CRC16/32 of data - // * HEX UNIX timestamp - // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second) - // See https://groups.google.com/forum/#!searchin/alt.bbs.mystic/netmail$20filename/alt.bbs.mystic/m1xLnY8i1pU/YnG2excdl6MJ - // * SBBSEcho uses DDHHMMSS - see https://github.com/ftnapps/pkg-sbbs/blob/master/docs/fidonet.txt - // * We already have a system for 8-character serial number gernation that is - // used for e.g. in FTS-0009.001 MSGIDs... let's use that! + // There are a lot of systems in use here for the name: + // * HEX CRC16/32 of data + // * HEX UNIX timestamp + // * Mystic at least at one point, used Hex8(day of month + seconds past midnight + hundredths of second) + // See https://groups.google.com/forum/#!searchin/alt.bbs.mystic/netmail$20filename/alt.bbs.mystic/m1xLnY8i1pU/YnG2excdl6MJ + // * SBBSEcho uses DDHHMMSS - see https://github.com/ftnapps/pkg-sbbs/blob/master/docs/fidonet.txt + // * We already have a system for 8-character serial number gernation that is + // used for e.g. in FTS-0009.001 MSGIDs... let's use that! // - const name = ftnUtil.getMessageSerialNumber(messageId); - const ext = (true === isTemp) ? 'pk_' : 'pkt'; + const name = ftnUtil.getMessageSerialNumber(messageId); + const ext = (true === isTemp) ? 'pk_' : 'pkt'; let fileName = `${name}.${ext}`; if('upper' === fileCase) { @@ -208,11 +208,11 @@ function FTNMessageScanTossModule() { let ext; switch(flowType) { - case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break; - case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break; - case 'busy' : ext = 'bsy'; break; - case 'request' : ext = 'req'; break; - case 'requests' : ext = 'hrq'; break; + case 'mail' : ext = `${exportType.toLowerCase()[0]}ut`; break; + case 'ref' : ext = `${exportType.toLowerCase()[0]}lo`; break; + case 'busy' : ext = 'bsy'; break; + case 'request' : ext = 'req'; break; + case 'requests' : ext = 'hrq'; break; } if('upper' === fileCase) { @@ -224,9 +224,9 @@ function FTNMessageScanTossModule() { this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) { // - // Refs - // * http://ftsc.org/docs/fts-5005.003 - // * http://wiki.synchro.net/ref:fidonet_files#flow_files + // Refs + // * http://ftsc.org/docs/fts-5005.003 + // * http://wiki.synchro.net/ref:fidonet_files#flow_files // let controlFileBaseName; let pointDir; @@ -238,30 +238,30 @@ function FTNMessageScanTossModule() { fileCase ); - const netComponent = `0000${destAddress.net.toString(16)}`.substr(-4); - const nodeComponent = `0000${destAddress.node.toString(16)}`.substr(-4); + const netComponent = `0000${destAddress.net.toString(16)}`.substr(-4); + const nodeComponent = `0000${destAddress.node.toString(16)}`.substr(-4); if(destAddress.point) { - // point's go in an extra subdir, e.g. outbound/NNNNnnnn.pnt/00000001.pnt (for a point of 1) - pointDir = `${netComponent}${nodeComponent}.pnt`; + // point's go in an extra subdir, e.g. outbound/NNNNnnnn.pnt/00000001.pnt (for a point of 1) + pointDir = `${netComponent}${nodeComponent}.pnt`; controlFileBaseName = `00000000${destAddress.point.toString(16)}`.substr(-8); } else { pointDir = ''; // - // Use |destAddress| nnnnNNNN.??? where nnnn is dest net and NNNN is dest - // node. This seems to match what Mystic does + // Use |destAddress| nnnnNNNN.??? where nnnn is dest net and NNNN is dest + // node. This seems to match what Mystic does // controlFileBaseName = `${netComponent}${nodeComponent}`; } // - // From FTS-5005.003: "Lower case filenames are prefered if supported by the file system." - // ...but we let the user override. + // From FTS-5005.003: "Lower case filenames are prefered if supported by the file system." + // ...but we let the user override. // if('upper' === fileCase) { - controlFileBaseName = controlFileBaseName.toUpperCase(); - pointDir = pointDir.toUpperCase(); + controlFileBaseName = controlFileBaseName.toUpperCase(); + pointDir = pointDir.toUpperCase(); } return paths.join(basePath, pointDir, `${controlFileBaseName}.${ext}`); @@ -269,12 +269,12 @@ function FTNMessageScanTossModule() { this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) { // - // We have to ensure the *directory* of |filePath| exists here esp. - // for cases such as point destinations where a subdir may be - // present in the path that doesn't yet exist. + // We have to ensure the *directory* of |filePath| exists here esp. + // for cases such as point destinations where a subdir may be + // present in the path that doesn't yet exist. // const flowFileDir = paths.dirname(filePath); - fse.mkdirs(flowFileDir, () => { // note not checking err; let's try appendFile + fse.mkdirs(flowFileDir, () => { // note not checking err; let's try appendFile const appendLines = fileRefs.reduce( (content, ref) => { return content + `${directive}${ref}\n`; }, ''); @@ -287,14 +287,14 @@ function FTNMessageScanTossModule() { this.getOutgoingBundleFileName = function(basePath, sourceAddress, destAddress, cb) { // - // Base filename is constructed as such: - // * If this |destAddress| is *not* a point address, we use NNNNnnnn where - // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded - // hex of dest node - source node. - // * If |destAddress| is a point, NNNN becomes 0000 and nnnn becomes 'p' + - // 3 digit 0 padded hex point + // Base filename is constructed as such: + // * If this |destAddress| is *not* a point address, we use NNNNnnnn where + // NNNN is 0 padded hex of dest net - source net and and nnnn is 0 padded + // hex of dest node - source node. + // * If |destAddress| is a point, NNNN becomes 0000 and nnnn becomes 'p' + + // 3 digit 0 padded hex point // - // Extension is dd? where dd is Su...Mo and ? is 0...Z as collisions arise + // Extension is dd? where dd is Su...Mo and ? is 0...Z as collisions arise // let basename; if(destAddress.point) { @@ -302,13 +302,13 @@ function FTNMessageScanTossModule() { basename = `0000p${pointHex}`; } else { basename = - `0000${Math.abs(sourceAddress.net - destAddress.net).toString(16)}`.substr(-4) + - `0000${Math.abs(sourceAddress.node - destAddress.node).toString(16)}`.substr(-4); + `0000${Math.abs(sourceAddress.net - destAddress.net).toString(16)}`.substr(-4) + + `0000${Math.abs(sourceAddress.node - destAddress.node).toString(16)}`.substr(-4); } // - // We need to now find the first entry that does not exist starting - // with dd0 to ddz + // We need to now find the first entry that does not exist starting + // with dd0 to ddz // const EXT_SUFFIXES = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); let fileName = `${basename}.${moment().format('dd').toLowerCase()}`; @@ -328,63 +328,63 @@ function FTNMessageScanTossModule() { this.prepareMessage = function(message, options) { // - // Set various FTN kludges/etc. + // Set various FTN kludges/etc. // - const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version + const localAddress = new Address(options.network.localAddress); // ensure we have an Address obj not a string version - // :TODO: create Address.toMeta() / similar + // :TODO: create Address.toMeta() / similar message.meta.FtnProperty = message.meta.FtnProperty || {}; message.meta.FtnKludge = message.meta.FtnKludge || {}; - message.meta.FtnProperty.ftn_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_orig_network = localAddress.net; - message.meta.FtnProperty.ftn_cost = 0; - message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; + message.meta.FtnProperty.ftn_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_orig_network = localAddress.net; + message.meta.FtnProperty.ftn_cost = 0; + message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; const destAddress = options.routeAddress || options.destAddress; - message.meta.FtnProperty.ftn_dest_node = destAddress.node; - message.meta.FtnProperty.ftn_dest_network = destAddress.net; + message.meta.FtnProperty.ftn_dest_node = destAddress.node; + message.meta.FtnProperty.ftn_dest_network = destAddress.net; if(destAddress.zone) { - message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; + message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; } if(destAddress.point) { - message.meta.FtnProperty.ftn_dest_point = destAddress.point; + message.meta.FtnProperty.ftn_dest_point = destAddress.point; } - // tear line and origin can both go in EchoMail & NetMail - message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); - message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(localAddress); + // tear line and origin can both go in EchoMail & NetMail + message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); + message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(localAddress); - let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system + let ftnAttribute = ftnMailPacket.Packet.Attribute.Local; // message from our system const config = Config(); if(self.isNetMailMessage(message)) { // - // Set route and message destination properties -- they may differ + // Set route and message destination properties -- they may differ // - message.meta.FtnProperty.ftn_msg_dest_node = options.destAddress.node; - message.meta.FtnProperty.ftn_msg_dest_net = options.destAddress.net; + message.meta.FtnProperty.ftn_msg_dest_node = options.destAddress.node; + message.meta.FtnProperty.ftn_msg_dest_net = options.destAddress.net; ftnAttribute |= ftnMailPacket.Packet.Attribute.Private; // - // NetMail messages need a FRL-1005.001 "Via" line - // http://ftsc.org/docs/frl-1005.001 + // NetMail messages need a FRL-1005.001 "Via" line + // http://ftsc.org/docs/frl-1005.001 // - // :TODO: We need to do this when FORWARDING NetMail + // :TODO: We need to do this when FORWARDING NetMail /* - if(_.isString(message.meta.FtnKludge.Via)) { - message.meta.FtnKludge.Via = [ message.meta.FtnKludge.Via ]; - } - message.meta.FtnKludge.Via = message.meta.FtnKludge.Via || []; - message.meta.FtnKludge.Via.push(ftnUtil.getVia(options.network.localAddress)); - */ + if(_.isString(message.meta.FtnKludge.Via)) { + message.meta.FtnKludge.Via = [ message.meta.FtnKludge.Via ]; + } + message.meta.FtnKludge.Via = message.meta.FtnKludge.Via || []; + message.meta.FtnKludge.Via.push(ftnUtil.getVia(options.network.localAddress)); + */ // - // We need to set INTL, and possibly FMPT and/or TOPT - // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac + // We need to set INTL, and possibly FMPT and/or TOPT + // See http://retro.fidoweb.ru/docs/index=ftsc&doc=FTS-4001&enc=mac // message.meta.FtnKludge.INTL = ftnUtil.getIntl(options.destAddress, localAddress); @@ -397,30 +397,30 @@ function FTNMessageScanTossModule() { } } else { // - // Set appropriate attribute flag for export type + // Set appropriate attribute flag for export type // switch(this.getExportType(options.nodeConfig)) { - case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break; - case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break; - // :TODO: Others? + case 'crash' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Crash; break; + case 'hold' : ftnAttribute |= ftnMailPacket.Packet.Attribute.Hold; break; + // :TODO: Others? } // - // EchoMail requires some additional properties & kludges + // EchoMail requires some additional properties & kludges // message.meta.FtnProperty.ftn_area = config.messageNetworks.ftn.areas[message.areaTag].tag; // - // When exporting messages, we should create/update SEEN-BY - // with remote address(s) we are exporting to. + // When exporting messages, we should create/update SEEN-BY + // with remote address(s) we are exporting to. // const seenByAdditions = - [ `${localAddress.net}/${localAddress.node}` ].concat(config.messageNetworks.ftn.areas[message.areaTag].uplinks); + [ `${localAddress.net}/${localAddress.node}` ].concat(config.messageNetworks.ftn.areas[message.areaTag].uplinks); message.meta.FtnProperty.ftn_seen_by = - ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions); + ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions); // - // And create/update PATH for ourself + // And create/update PATH for ourself // message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, localAddress); } @@ -428,33 +428,33 @@ function FTNMessageScanTossModule() { message.meta.FtnProperty.ftn_attr_flags = ftnAttribute; // - // Additional kludges + // Additional kludges // - // Check for existence of MSGID as we may already have stored it from a previous - // export that failed to finish + // Check for existence of MSGID as we may already have stored it from a previous + // export that failed to finish // if(!message.meta.FtnKludge.MSGID) { message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier( message, localAddress, - message.isPrivate() // true = isNetMail + message.isPrivate() // true = isNetMail ); } message.meta.FtnKludge.TZUTC = ftnUtil.getUTCTimeZoneOffset(); // - // According to FSC-0046: + // According to FSC-0046: // - // "When a Conference Mail processor adds a TID to a message, it may not - // add a PID. An existing TID should, however, be replaced. TIDs follow - // the same format used for PIDs, as explained above." + // "When a Conference Mail processor adds a TID to a message, it may not + // add a PID. An existing TID should, however, be replaced. TIDs follow + // the same format used for PIDs, as explained above." // message.meta.FtnKludge.TID = ftnUtil.getProductIdentifier(); // - // Determine CHRS and actual internal encoding name. If the message has an - // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set. + // Determine CHRS and actual internal encoding name. If the message has an + // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set. // let encoding = options.nodeConfig.encoding || config.scannerTossers.ftn_bso.packetMsgEncoding || 'utf8'; const explicitEncoding = _.get(message.meta, 'System.explicit_encoding'); @@ -468,40 +468,40 @@ function FTNMessageScanTossModule() { } // - // Ensure we ended up with something useable. If not, back to utf8! + // Ensure we ended up with something useable. If not, back to utf8! // if(!iconv.encodingExists(encoding)) { Log.debug( { encoding : encoding }, 'Unknown encoding. Falling back to utf8'); encoding = 'utf8'; } - options.encoding = encoding; // save for later + options.encoding = encoding; // save for later message.meta.FtnKludge.CHRS = ftnUtil.getCharacterSetIdentifierByEncoding(encoding); }; this.setReplyKludgeFromReplyToMsgId = function(message, cb) { // - // Look up MSGID kludge for |message.replyToMsgId|, if any. - // If found, we can create a REPLY kludge with the previously - // discovered MSGID. + // Look up MSGID kludge for |message.replyToMsgId|, if any. + // If found, we can create a REPLY kludge with the previously + // discovered MSGID. // if(0 === message.replyToMsgId) { - return cb(null); // nothing to do + return cb(null); // nothing to do } Message.getMetaValuesByMessageId(message.replyToMsgId, 'FtnKludge', 'MSGID', (err, msgIdVal) => { if(!err) { assert(_.isString(msgIdVal), 'Expected string but got ' + (typeof msgIdVal) + ' (' + msgIdVal + ')'); - // got a MSGID - create a REPLY + // got a MSGID - create a REPLY message.meta.FtnKludge.REPLY = msgIdVal; } - cb(null); // this method always passes + cb(null); // this method always passes }); }; - // check paths, Addresses, etc. + // check paths, Addresses, etc. this.isAreaConfigValid = function(areaConfig) { if(!areaConfig || !_.isString(areaConfig.tag) || !_.isString(areaConfig.network)) { return false; @@ -520,14 +520,14 @@ function FTNMessageScanTossModule() { return false; } - // :TODO: need to check more! + // :TODO: need to check more! return true; }; this.parseScheduleString = function(schedStr) { if(!schedStr) { - return; // nothing to parse! + return; // nothing to parse! } let schedule = {}; @@ -550,7 +550,7 @@ function FTNMessageScanTossModule() { } } - // return undefined if we couldn't parse out anything useful + // return undefined if we couldn't parse out anything useful if(!_.isEmpty(schedule)) { return schedule; } @@ -558,10 +558,10 @@ function FTNMessageScanTossModule() { this.getAreaLastScanId = function(areaTag, cb) { const sql = - `SELECT area_tag, message_id - FROM message_area_last_scan - WHERE scan_toss = "ftn_bso" AND area_tag = ? - LIMIT 1;`; + `SELECT area_tag, message_id + FROM message_area_last_scan + WHERE scan_toss = "ftn_bso" AND area_tag = ? + LIMIT 1;`; msgDb.get(sql, [ areaTag ], (err, row) => { return cb(err, row ? row.message_id : 0); @@ -570,8 +570,8 @@ function FTNMessageScanTossModule() { this.setAreaLastScanId = function(areaTag, lastScanId, cb) { const sql = - `REPLACE INTO message_area_last_scan (scan_toss, area_tag, message_id) - VALUES ("ftn_bso", ?, ?);`; + `REPLACE INTO message_area_last_scan (scan_toss, area_tag, message_id) + VALUES ("ftn_bso", ?, ?);`; msgDb.run(sql, [ areaTag, lastScanId ], err => { return cb(err); @@ -581,7 +581,7 @@ function FTNMessageScanTossModule() { this.getNodeConfigByAddress = function(addr) { addr = _.isString(addr) ? Address.fromString(addr) : addr; - // :TODO: sort wildcard nodes{} entries by most->least explicit according to FTN hierarchy + // :TODO: sort wildcard nodes{} entries by most->least explicit according to FTN hierarchy return _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { return addr.isPatternMatch(nodeAddrWildcard); }); @@ -589,7 +589,7 @@ function FTNMessageScanTossModule() { this.exportNetMailMessagePacket = function(message, exportOpts, cb) { // - // For NetMail, we always create a *single* packet per message. + // For NetMail, we always create a *single* packet per message. // async.series( [ @@ -609,11 +609,11 @@ function FTNMessageScanTossModule() { packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - // use current message ID for filename seed + // use current message ID for filename seed exportOpts.pktFileName = self.getOutgoingPacketFileName( self.exportTempDir, message.messageId, - false, // createTempPacket=false + false, // createTempPacket=false exportOpts.fileCase ); @@ -645,13 +645,13 @@ function FTNMessageScanTossModule() { this.exportMessagesByUuid = function(messageUuids, exportOpts, cb) { // - // This method has a lot of madness going on: - // - Try to stuff messages into packets until we've hit the target size - // - We need to wait for write streams to finish before proceeding in many cases - // or data will be cut off when closing and creating a new stream + // This method has a lot of madness going on: + // - Try to stuff messages into packets until we've hit the target size + // - We need to wait for write streams to finish before proceeding in many cases + // or data will be cut off when closing and creating a new stream // - let exportedFiles = []; - let currPacketSize = self.moduleConfig.packetTargetByteSize; + let exportedFiles = []; + let currPacketSize = self.moduleConfig.packetTargetByteSize; let packet; let ws; let remainMessageBuf; @@ -684,7 +684,7 @@ function FTNMessageScanTossModule() { return callback(err); } - // General preperation + // General preperation self.prepareMessage(message, exportOpts); self.setReplyKludgeFromReplyToMsgId(message, err => { @@ -703,7 +703,7 @@ function FTNMessageScanTossModule() { packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - // use current message ID for filename seed + // use current message ID for filename seed const pktFileName = self.getOutgoingPacketFileName( self.exportTempDir, message.messageId, @@ -731,11 +731,11 @@ function FTNMessageScanTossModule() { return callback(err); } - currPacketSize += msgBuf.length; + currPacketSize += msgBuf.length; if(currPacketSize >= self.moduleConfig.packetTargetByteSize) { - remainMessageBuf = msgBuf; // save for next packet - remainMessageId = message.messageId; + remainMessageBuf = msgBuf; // save for next packet + remainMessageId = message.messageId; } else { ws.write(msgBuf); } @@ -750,8 +750,8 @@ function FTNMessageScanTossModule() { }, function storeMsgIdMeta(callback) { // - // We want to store some meta as if we had imported - // this message for later reference + // We want to store some meta as if we had imported + // this message for later reference // if(message.meta.FtnKludge.MSGID) { message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, err => { @@ -781,7 +781,7 @@ function FTNMessageScanTossModule() { }, function writeRemainPacket(callback) { if(remainMessageBuf) { - // :TODO: DRY this with the code above -- they are basically identical + // :TODO: DRY this with the code above -- they are basically identical packet = new ftnMailPacket.Packet(); const packetHeader = new ftnMailPacket.PacketHeader( @@ -791,7 +791,7 @@ function FTNMessageScanTossModule() { packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; - // use current message ID for filename seed + // use current message ID for filename seed const pktFileName = self.getOutgoingPacketFileName( self.exportTempDir, remainMessageId, @@ -821,7 +821,7 @@ function FTNMessageScanTossModule() { this.getNetMailRoute = function(dstAddr) { // - // Route full|wildcard -> full adddress/network lookup + // Route full|wildcard -> full adddress/network lookup // const routes = _.get(Config(), 'scannerTossers.ftn_bso.netMail.routes'); if(!routes) { @@ -835,15 +835,15 @@ function FTNMessageScanTossModule() { this.getNetMailRouteInfoFromAddress = function(destAddress, cb) { // - // Attempt to find route information for |destAddress|: + // Attempt to find route information for |destAddress|: // - // 1) Routes: scannerTossers.ftn_bso.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config - // - Where we send may not be where destAddress is (it's routed!) - // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config - // - Where we send is direct to destAddress + // 1) Routes: scannerTossers.ftn_bso.netMail.routes{} -> scannerTossers.ftn_bso.nodes{} -> config + // - Where we send may not be where destAddress is (it's routed!) + // 2) Direct to nodes: scannerTossers.ftn_bso.nodes{} -> config + // - Where we send is direct to destAddress // - // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address - // falling back to Config.scannerTossers.ftn_bso.defaultNetwork + // In both cases, attempt to look up Zone:Net/* to discover local "from" network/address + // falling back to Config.scannerTossers.ftn_bso.defaultNetwork // const route = this.getNetMailRoute(destAddress); @@ -851,21 +851,21 @@ function FTNMessageScanTossModule() { let networkName; let isRouted; if(route) { - routeAddress = Address.fromString(route.address); - networkName = route.network; - isRouted = true; + routeAddress = Address.fromString(route.address); + networkName = route.network; + isRouted = true; } else { - routeAddress = destAddress; - isRouted = false; + routeAddress = destAddress; + isRouted = false; } networkName = networkName || this.getNetworkNameByAddress(routeAddress); const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { return routeAddress.isPatternMatch(nodeAddrWildcard); - }) || { packetType : '2+', encoding : Config().scannerTossers.ftn_bso.packetMsgEncoding }; + }) || { packetType : '2+', encoding : Config().scannerTossers.ftn_bso.packetMsgEncoding }; - // we should never be failing here; we may just be using defaults. + // we should never be failing here; we may just be using defaults. return cb( networkName ? null : Errors.DoesNotExist(`No NetMail route for ${destAddress.toString()}`), { destAddress, routeAddress, networkName, config, isRouted } @@ -873,7 +873,7 @@ function FTNMessageScanTossModule() { }; this.exportNetMailMessagesToUplinks = function(messagesOrMessageUuids, cb) { - // for each message/UUID, find where to send the thing + // for each message/UUID, find where to send the thing async.each(messagesOrMessageUuids, (msgOrUuid, nextMessageOrUuid) => { const exportOpts = {}; @@ -898,14 +898,14 @@ function FTNMessageScanTossModule() { return callback(err); } - exportOpts.nodeConfig = routeInfo.config; - exportOpts.destAddress = dstAddr; - exportOpts.routeAddress = routeInfo.routeAddress; - exportOpts.fileCase = routeInfo.config.fileCase || 'lower'; - exportOpts.network = Config().messageNetworks.ftn.networks[routeInfo.networkName]; - exportOpts.networkName = routeInfo.networkName; - exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); - exportOpts.exportType = self.getExportType(routeInfo.config); + exportOpts.nodeConfig = routeInfo.config; + exportOpts.destAddress = dstAddr; + exportOpts.routeAddress = routeInfo.routeAddress; + exportOpts.fileCase = routeInfo.config.fileCase || 'lower'; + exportOpts.network = Config().messageNetworks.ftn.networks[routeInfo.networkName]; + exportOpts.networkName = routeInfo.networkName; + exportOpts.outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); + exportOpts.exportType = self.getExportType(routeInfo.config); if(!exportOpts.network) { return callback(Errors.DoesNotExist(`No configuration found for network ${routeInfo.networkName}`)); @@ -915,7 +915,7 @@ function FTNMessageScanTossModule() { }); }, function createOutgoingDir(callback) { - // ensure outgoing NetMail directory exists + // ensure outgoing NetMail directory exists return fse.mkdirs(exportOpts.outgoingDir, callback); }, function exportPacket(callback) { @@ -945,7 +945,7 @@ function FTNMessageScanTossModule() { return message.persistMetaValue('System', 'state_flags0', Message.StateFlags0.Exported.toString(), callback); }, function storeMsgIdMeta(callback) { - // Store meta as if we had imported this message -- for later reference + // Store meta as if we had imported this message -- for later reference if(message.meta.FtnKludge.MSGID) { return message.persistMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.MSGID, callback); } @@ -978,18 +978,18 @@ function FTNMessageScanTossModule() { const exportOpts = { nodeConfig, - network : config.messageNetworks.ftn.networks[areaConfig.network], - destAddress : Address.fromString(uplink), - networkName : areaConfig.network, - fileCase : nodeConfig.fileCase || 'lower', + network : config.messageNetworks.ftn.networks[areaConfig.network], + destAddress : Address.fromString(uplink), + networkName : areaConfig.network, + fileCase : nodeConfig.fileCase || 'lower', }; if(_.isString(exportOpts.network.localAddress)) { exportOpts.network.localAddress = Address.fromString(exportOpts.network.localAddress); } - const outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); - const exportType = self.getExportType(exportOpts.nodeConfig); + const outgoingDir = self.getOutgoingEchoMailPacketDir(exportOpts.networkName, exportOpts.destAddress); + const exportType = self.getExportType(exportOpts.nodeConfig); async.waterfall( [ @@ -1003,19 +1003,19 @@ function FTNMessageScanTossModule() { }, function createArcMailBundle(exportedFileNames, callback) { if(self.archUtil.haveArchiver(exportOpts.nodeConfig.archiveType)) { - // :TODO: support bundleTargetByteSize: + // :TODO: support bundleTargetByteSize: // - // Compress to a temp location then we'll move it in the next step + // Compress to a temp location then we'll move it in the next step // - // Note that we must use the *final* output dir for getOutgoingBundleFileName() - // as it checks for collisions in bundle names! + // Note that we must use the *final* output dir for getOutgoingBundleFileName() + // as it checks for collisions in bundle names! // self.getOutgoingBundleFileName(outgoingDir, exportOpts.network.localAddress, exportOpts.destAddress, (err, bundlePath) => { if(err) { return callback(err); } - // adjust back to temp path + // adjust back to temp path const tempBundlePath = paths.join(self.exportTempDir, paths.basename(bundlePath)); self.archUtil.compressTo( @@ -1035,8 +1035,8 @@ function FTNMessageScanTossModule() { const ext = paths.extname(oldPath).toLowerCase(); if('.pk_' === ext.toLowerCase()) { // - // For a given temporary .pk_ file, we need to move it to the outoing - // directory with the appropriate BSO style filename. + // For a given temporary .pk_ file, we need to move it to the outoing + // directory with the appropriate BSO style filename. // const newExt = self.getOutgoingFlowFileExtension( exportOpts.destAddress, @@ -1062,7 +1062,7 @@ function FTNMessageScanTossModule() { } // - // For bundles, we need to append to the appropriate flow file + // For bundles, we need to append to the appropriate flow file // const flowFilePath = self.getOutgoingFlowFileName( outgoingDir, @@ -1072,7 +1072,7 @@ function FTNMessageScanTossModule() { exportOpts.fileCase ); - // directive of '^' = delete file after transfer + // directive of '^' = delete file after transfer self.flowFileAppendRefs(flowFilePath, [ newPath ], '^', err => { if(err) { Log.warn( { path : flowFilePath }, 'Failed appending flow reference record!'); @@ -1085,31 +1085,31 @@ function FTNMessageScanTossModule() { } ], err => { - // :TODO: do something with |err| ? + // :TODO: do something with |err| ? if(err) { Log.warn(err.message); } nextUplink(); } ); - }, cb); // complete + }, cb); // complete }; this.setReplyToMsgIdFtnReplyKludge = function(message, cb) { // - // Given a FTN REPLY kludge, set |message.replyToMsgId|, if possible, - // by looking up an associated MSGID kludge meta. + // Given a FTN REPLY kludge, set |message.replyToMsgId|, if possible, + // by looking up an associated MSGID kludge meta. // - // See also: http://ftsc.org/docs/fts-0009.001 + // See also: http://ftsc.org/docs/fts-0009.001 // if(!_.isString(message.meta.FtnKludge.REPLY)) { - // nothing to do + // nothing to do return cb(); } Message.getMessageIdsByMetaValue('FtnKludge', 'MSGID', message.meta.FtnKludge.REPLY, (err, msgIds) => { if(msgIds && msgIds.length > 0) { - // expect a single match, but dupe checking is not perfect - warn otherwise + // expect a single match, but dupe checking is not perfect - warn otherwise if(1 === msgIds.length) { message.replyToMsgId = msgIds[0]; } else { @@ -1125,7 +1125,7 @@ function FTNMessageScanTossModule() { const aliases = _.get(Config(), 'messageNetworks.ftn.netMail.aliases'); if(!aliases) { - return lookup; // keep orig + return lookup; // keep orig } const alias = _.find(aliases, (localName, alias) => { @@ -1148,7 +1148,7 @@ function FTNMessageScanTossModule() { } const fromPoint = _.get(message, 'meta.FtnKludge.FMPT'); - const toPoint = _.get(message, 'meta.FtnKludge.TOPT'); + const toPoint = _.get(message, 'meta.FtnKludge.TOPT'); if(fromPoint) { from += `.${fromPoint}`; @@ -1172,7 +1172,7 @@ function FTNMessageScanTossModule() { }, function checkForDupeMSGID(callback) { // - // If we have a MSGID, don't allow a dupe + // If we have a MSGID, don't allow a dupe // if(!_.has(message.meta, 'FtnKludge.MSGID')) { return callback(null); @@ -1191,16 +1191,16 @@ function FTNMessageScanTossModule() { function basicSetup(callback) { message.areaTag = config.localAreaTag; - // indicate this was imported from FTN + // indicate this was imported from FTN message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.FTN; // - // If we *allow* dupes (disabled by default), then just generate - // a random UUID. Otherwise, don't assign the UUID just yet. It will be - // generated at persist() time and should be consistent across import/exports + // If we *allow* dupes (disabled by default), then just generate + // a random UUID. Otherwise, don't assign the UUID just yet. It will be + // generated at persist() time and should be consistent across import/exports // if(true === _.get(Config(), [ 'messageNetworks', 'ftn', 'areas', config.localAreaTag, 'allowDupes' ], false)) { - // just generate a UUID & therefor always allow for dupes + // just generate a UUID & therefor always allow for dupes message.uuid = uuidV4(); } @@ -1213,15 +1213,15 @@ function FTNMessageScanTossModule() { }, function setupPrivateMessage(callback) { // - // If this is a private message (e.g. NetMail) we set the local user ID + // If this is a private message (e.g. NetMail) we set the local user ID // if(Message.WellKnownAreaTags.Private !== config.localAreaTag) { return callback(null); } // - // Create a meta value for the *remote* from user. In the case here with FTN, - // their fully qualified FTN from address + // Create a meta value for the *remote* from user. In the case here with FTN, + // their fully qualified FTN from address // const { from } = self.getAddressesFromNetMailMessage(message); @@ -1236,8 +1236,8 @@ function FTNMessageScanTossModule() { User.getUserIdAndNameByLookup(lookupName, (err, localToUserId, localUserName) => { if(err) { // - // Couldn't find a local username. If the toUserName itself is a FTN address - // we can only assume the message is to the +op, else we'll have to fail. + // Couldn't find a local username. If the toUserName itself is a FTN address + // we can only assume the message is to the +op, else we'll have to fail. // const toUserNameAsAddress = Address.fromString(message.toUserName); if(toUserNameAsAddress.isValid()) { @@ -1261,21 +1261,21 @@ function FTNMessageScanTossModule() { } } - // we do this after such that error cases can be preseved above + // we do this after such that error cases can be preseved above if(lookupName !== message.toUserName) { message.toUserName = localUserName; } - // set the meta information - used elsehwere for retrieval + // set the meta information - used elsehwere for retrieval message.meta.System[Message.SystemMetaNames.LocalToUserID] = localToUserId; return callback(null); }); }, function persistImport(callback) { - // mark as imported + // mark as imported message.meta.System.state_flags0 = Message.StateFlags0.Imported.toString(); - // save to disc + // save to disc message.persist(err => { return callback(err); }); @@ -1298,19 +1298,19 @@ function FTNMessageScanTossModule() { }; // - // Ref. implementations on import: - // * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c - // https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/handle.c + // Ref. implementations on import: + // * https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/pkt.c + // https://github.com/larsks/crashmail/blob/26e5374710c7868dab3d834be14bf4041041aae5/crashmail/handle.c // this.importMessagesFromPacketFile = function(packetPath, password, cb) { let packetHeader; - const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later + const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later let importStats = { - areaSuccess : {}, // areaTag->count - areaFail : {}, // areaTag->count - otherFail : 0, + areaSuccess : {}, // areaTag->count + areaFail : {}, // areaTag->count + otherFail : 0, }; new ftnMailPacket.Packet(packetOpts).read(packetPath, (entryType, entryData, next) => { @@ -1323,7 +1323,7 @@ function FTNMessageScanTossModule() { return next(new Error(`No local configuration for packet addressed to ${addrString}`)); } else { - // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?! + // :TODO: password needs validated - need to determine if it will use the same node config (which can have wildcards) or something else?! return next(null); } @@ -1337,19 +1337,19 @@ function FTNMessageScanTossModule() { if(!localAreaTag) { // - // No local area configured for this import + // No local area configured for this import // - // :TODO: Handle the "catch all" area bucket case if configured + // :TODO: Handle the "catch all" area bucket case if configured Log.warn( { areaTag : areaTag }, 'No local area configured for this packet file!'); - // bump generic failure + // bump generic failure importStats.otherFail += 1; return next(null); } } else { // - // No area tag: If marked private in attributes, this is a NetMail + // No area tag: If marked private in attributes, this is a NetMail // if(message.meta.FtnProperty.ftn_attr_flags & ftnMailPacket.Packet.Attribute.Private) { localAreaTag = Message.WellKnownAreaTags.Private; @@ -1369,12 +1369,12 @@ function FTNMessageScanTossModule() { self.appendTearAndOrigin(message); const importConfig = { - localAreaTag : localAreaTag, + localAreaTag : localAreaTag, }; self.importMailToArea(importConfig, packetHeader, message, err => { if(err) { - // bump area fail stats + // bump area fail stats importStats.areaFail[localAreaTag] = (importStats.areaFail[localAreaTag] || 0) + 1; if('SQLITE_CONSTRAINT' === err.code || 'DUPE_MSGID' === err.code) { @@ -1386,7 +1386,7 @@ function FTNMessageScanTossModule() { return next(null); } } else { - // bump area success + // bump area success importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1; } @@ -1395,7 +1395,7 @@ function FTNMessageScanTossModule() { } }, err => { // - // try to produce something helpful in the log + // try to produce something helpful in the log // const finalStats = Object.assign(importStats, { packetPath : packetPath } ); if(err || Object.keys(finalStats.areaFail).length > 0) { @@ -1414,11 +1414,11 @@ function FTNMessageScanTossModule() { this.maybeArchiveImportFile = function(origPath, type, status, cb) { // - // type : pkt|tic|bundle - // status : good|reject + // type : pkt|tic|bundle + // status : good|reject // - // Status of "good" is only applied to pkt files & placed - // in |retain| if set. This is generally used for debugging only. + // Status of "good" is only applied to pkt files & placed + // in |retain| if set. This is generally used for debugging only. // let archivePath; const ts = moment().format('YYYY-MM-DDTHH.mm.ss.SSS'); @@ -1433,7 +1433,7 @@ function FTNMessageScanTossModule() { } else if('good' !== status) { archivePath = paths.join(self.moduleConfig.paths.reject, `${status}-${type}--${ts}-${fn}`); } else { - return cb(null); // don't archive non-good/pkt files + return cb(null); // don't archive non-good/pkt files } Log.debug( { origPath : origPath, archivePath : archivePath, type : type, status : status }, 'Archiving import file'); @@ -1443,7 +1443,7 @@ function FTNMessageScanTossModule() { Log.warn( { error : err.message, origPath : origPath, archivePath : archivePath, type : type, status : status }, 'Failed to archive packet file'); } - return cb(null); // never fatal + return cb(null); // never fatal }); }; @@ -1472,13 +1472,13 @@ function FTNMessageScanTossModule() { nextFile(); }); }, err => { - // :TODO: Handle err! we should try to keep going though... + // :TODO: Handle err! we should try to keep going though... callback(err, packetFiles, rejects); }); }, function handleProcessedFiles(packetFiles, rejects, callback) { async.each(packetFiles, (packetFile, nextFile) => { - // possibly archive, then remove original + // possibly archive, then remove original const fullPath = paths.join(importDir, packetFile); self.maybeArchiveImportFile( fullPath, @@ -1504,7 +1504,7 @@ function FTNMessageScanTossModule() { this.importFromDirectory = function(inboundType, importDir, cb) { async.waterfall( [ - // start with .pkt files + // start with .pkt files function importPacketFiles(callback) { self.importPacketFilesFromDirectory(importDir, '', err => { callback(err); @@ -1512,7 +1512,7 @@ function FTNMessageScanTossModule() { }, function discoverBundles(callback) { fs.readdir(importDir, (err, files) => { - // :TODO: if we do much more of this, probably just use the glob module + // :TODO: if we do much more of this, probably just use the glob module const bundleRegExp = /\.(su|mo|tu|we|th|fr|sa)[0-9a-z]/i; files = files.filter(f => { const fext = paths.extname(f); @@ -1540,7 +1540,7 @@ function FTNMessageScanTossModule() { rejects.push(bundleFile.path); - return nextFile(); // unknown archive type + return nextFile(); // unknown archive type } Log.debug( { bundleFile : bundleFile }, 'Processing bundle' ); @@ -1567,10 +1567,10 @@ function FTNMessageScanTossModule() { } // - // All extracted - import .pkt's + // All extracted - import .pkt's // self.importPacketFilesFromDirectory(self.importTempDir, '', () => { - // :TODO: handle |err| + // :TODO: handle |err| callback(null, bundleFiles, rejects); }); }); @@ -1622,7 +1622,7 @@ function FTNMessageScanTossModule() { }); }; - // Starts an export block - returns true if we can proceed + // Starts an export block - returns true if we can proceed this.exportingStart = function() { if(!this.exportRunning) { this.exportRunning = true; @@ -1632,7 +1632,7 @@ function FTNMessageScanTossModule() { return false; }; - // ends an export block + // ends an export block this.exportingEnd = function(cb) { this.exportRunning = false; @@ -1666,9 +1666,9 @@ function FTNMessageScanTossModule() { function generalValidation(callback) { const sysConfig = Config(); const config = { - nodes : sysConfig.scannerTossers.ftn_bso.nodes, - defaultPassword : sysConfig.scannerTossers.ftn_bso.tic.password, - localAreaTags : self.getLocalAreaTagsForTic(), + nodes : sysConfig.scannerTossers.ftn_bso.nodes, + defaultPassword : sysConfig.scannerTossers.ftn_bso.tic.password, + localAreaTags : self.getLocalAreaTagsForTic(), }; return ticFileInfo.validate(config, (err, localInfo) => { @@ -1677,14 +1677,14 @@ function FTNMessageScanTossModule() { return callback(err); } - // We may need to map |localAreaTag| back to real areaTag if it's a mapping/alias + // We may need to map |localAreaTag| back to real areaTag if it's a mapping/alias const mappedLocalAreaTag = _.get(Config().scannerTossers.ftn_bso, [ 'ticAreas', localInfo.areaTag ]); if(mappedLocalAreaTag) { if(_.isString(mappedLocalAreaTag.areaTag)) { - localInfo.areaTag = mappedLocalAreaTag.areaTag; - localInfo.hashTags = mappedLocalAreaTag.hashTags; // override default for node - localInfo.storageTag = mappedLocalAreaTag.storageTag; // override default + localInfo.areaTag = mappedLocalAreaTag.areaTag; + localInfo.hashTags = mappedLocalAreaTag.hashTags; // override default for node + localInfo.storageTag = mappedLocalAreaTag.storageTag; // override default } else if(_.isString(mappedLocalAreaTag)) { localInfo.areaTag = mappedLocalAreaTag; } @@ -1695,18 +1695,18 @@ function FTNMessageScanTossModule() { }, function findExistingItem(localInfo, callback) { // - // We will need to look for an existing item to replace/update if: - // a) The TIC file has a "Replaces" field - // b) The general or node specific |allowReplace| is true + // We will need to look for an existing item to replace/update if: + // a) The TIC file has a "Replaces" field + // b) The general or node specific |allowReplace| is true // - // Replace specifies a DOS 8.3 *pattern* which is allowed to have - // ? and * characters. For example, RETRONET.* + // Replace specifies a DOS 8.3 *pattern* which is allowed to have + // ? and * characters. For example, RETRONET.* // - // Lastly, we will only replace if the item is in the same/specified area - // and that come from the same origin as a previous entry. + // Lastly, we will only replace if the item is in the same/specified area + // and that come from the same origin as a previous entry. // - const allowReplace = _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config().scannerTossers.ftn_bso.tic.allowReplace); - const replaces = ticFileInfo.getAsString('Replaces'); + const allowReplace = _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config().scannerTossers.ftn_bso.tic.allowReplace); + const replaces = ticFileInfo.getAsString('Replaces'); if(!allowReplace || !replaces) { return callback(null, localInfo); @@ -1714,13 +1714,13 @@ function FTNMessageScanTossModule() { const metaPairs = [ { - name : 'short_file_name', - value : replaces.toUpperCase(), // we store upper as well - wildcards : true, // value may contain wildcards + name : 'short_file_name', + value : replaces.toUpperCase(), // we store upper as well + wildcards : true, // value may contain wildcards }, { - name : 'tic_origin', - value : ticFileInfo.getAsString('Origin'), + name : 'tic_origin', + value : ticFileInfo.getAsString('Origin'), } ]; @@ -1729,11 +1729,11 @@ function FTNMessageScanTossModule() { return callback(err); } - // 0:1 allowed + // 0:1 allowed if(1 === fileIds.length) { localInfo.existingFileId = fileIds[0]; - // fetch old filename - we may need to remove it if replacing with a new name + // fetch old filename - we may need to remove it if replacing with a new name FileEntry.loadBasicEntry(localInfo.existingFileId, {}, (err, info) => { if(info) { Log.trace( @@ -1741,10 +1741,10 @@ function FTNMessageScanTossModule() { 'Existing TIC file target to be replaced' ); - localInfo.oldFileName = info.fileName; - localInfo.oldStorageTag = info.storageTag; + localInfo.oldFileName = info.fileName; + localInfo.oldStorageTag = info.storageTag; } - return callback(null, localInfo); // continue even if we couldn't find an old match + return callback(null, localInfo); // continue even if we couldn't find an old match }); } else if(fileIds.legnth > 1) { return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`)); @@ -1755,13 +1755,13 @@ function FTNMessageScanTossModule() { }, function scan(localInfo, callback) { const scanOpts = { - sha256 : localInfo.sha256, // *may* have already been calculated - meta : { - // some TIC-related metadata we always want - short_file_name : ticFileInfo.getAsString('File').toUpperCase(), // upper to ensure no case issues later; this should be a DOS 8.3 name - tic_origin : ticFileInfo.getAsString('Origin'), - tic_desc : ticFileInfo.getAsString('Desc'), - upload_by_username : _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config().scannerTossers.ftn_bso.tic.uploadBy), + sha256 : localInfo.sha256, // *may* have already been calculated + meta : { + // some TIC-related metadata we always want + short_file_name : ticFileInfo.getAsString('File').toUpperCase(), // upper to ensure no case issues later; this should be a DOS 8.3 name + tic_origin : ticFileInfo.getAsString('Origin'), + tic_desc : ticFileInfo.getAsString('Desc'), + upload_by_username : _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'uploadBy' ], Config().scannerTossers.ftn_bso.tic.uploadBy), } }; @@ -1771,18 +1771,18 @@ function FTNMessageScanTossModule() { } // - // We may have TIC auto-tagging for this node and/or specific (remote) area + // We may have TIC auto-tagging for this node and/or specific (remote) area // const hashTags = - localInfo.hashTags || - _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/ + localInfo.hashTags || + _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/ if(hashTags) { scanOpts.hashTags = new Set(hashTags.split(/[\s,]+/)); } if(localInfo.crc32) { - scanOpts.meta.file_crc32 = localInfo.crc32.toString(16); // again, *may* have already been calculated + scanOpts.meta.file_crc32 = localInfo.crc32.toString(16); // again, *may* have already been calculated } scanFile( @@ -1800,7 +1800,7 @@ function FTNMessageScanTossModule() { }, function store(localInfo, callback) { // - // Move file to final area storage and persist to DB + // Move file to final area storage and persist to DB // const areaInfo = getFileAreaByTag(localInfo.areaTag); if(!areaInfo) { @@ -1812,15 +1812,15 @@ function FTNMessageScanTossModule() { return callback(Errors.Invalid(`Invalid storage tag: ${storageTag}`)); } - localInfo.fileEntry.storageTag = storageTag; - localInfo.fileEntry.areaTag = localInfo.areaTag; - localInfo.fileEntry.fileName = ticFileInfo.longFileName; + localInfo.fileEntry.storageTag = storageTag; + localInfo.fileEntry.areaTag = localInfo.areaTag; + localInfo.fileEntry.fileName = ticFileInfo.longFileName; // - // We may now have two descriptions: from .DIZ/etc. or the TIC itself. - // Determine which one to use using |descPriority| and availability. + // We may now have two descriptions: from .DIZ/etc. or the TIC itself. + // Determine which one to use using |descPriority| and availability. // - // We will still fallback as needed from -> -> + // We will still fallback as needed from -> -> // const descPriority = _.get( Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ], @@ -1831,7 +1831,7 @@ function FTNMessageScanTossModule() { const origDesc = localInfo.fileEntry.desc; localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || origDesc || getDescFromFileName(ticFileInfo.filePath); } else { - // see if we got desc from .DIZ/etc. + // see if we got desc from .DIZ/etc. const fromDescFile = 'descFile' === localInfo.fileEntry.descSrc; localInfo.fileEntry.desc = fromDescFile ? localInfo.fileEntry.desc : ticFileInfo.getAsString('Ldesc'); localInfo.fileEntry.desc = localInfo.fileEntry.desc || getDescFromFileName(ticFileInfo.filePath); @@ -1845,7 +1845,7 @@ function FTNMessageScanTossModule() { const isUpdate = localInfo.existingFileId ? true : false; if(isUpdate) { - // we need to *update* an existing record/file + // we need to *update* an existing record/file localInfo.fileEntry.fileId = localInfo.existingFileId; } @@ -1866,14 +1866,14 @@ function FTNMessageScanTossModule() { }); }); }, - // :TODO: from here, we need to re-toss files if needed, before they are removed + // :TODO: from here, we need to re-toss files if needed, before they are removed function cleanupOldFile(localInfo, callback) { if(!localInfo.existingFileId) { return callback(null, localInfo); } const oldStorageDir = getAreaStorageDirectoryByTag(localInfo.oldStorageTag); - const oldPath = paths.join(oldStorageDir, localInfo.oldFileName); + const oldPath = paths.join(oldStorageDir, localInfo.oldFileName); fs.unlink(oldPath, err => { if(err) { @@ -1881,7 +1881,7 @@ function FTNMessageScanTossModule() { } else { Log.debug( { oldPath : oldPath }, 'Removed old physical file during TIC replacement'); } - return callback(null, localInfo); // continue even if err + return callback(null, localInfo); // continue even if err }); }, ], @@ -1902,7 +1902,7 @@ function FTNMessageScanTossModule() { this.removeAssocTicFiles = function(ticFileInfo, cb) { async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { fs.unlink(path, err => { - if(err && 'ENOENT' !== err.code) { // don't log when the file doesn't exist + if(err && 'ENOENT' !== err.code) { // don't log when the file doesn't exist Log.warn( { error : err.message, path : path }, 'Failed unlinking TIC file'); } return nextPath(null); @@ -1915,23 +1915,23 @@ function FTNMessageScanTossModule() { this.performEchoMailExport = function(cb) { // - // Select all messages with a |message_id| > |lastScanId|. - // Additionally exclude messages with the System state_flags0 which will be present for - // imported or already exported messages + // Select all messages with a |message_id| > |lastScanId|. + // Additionally exclude messages with the System state_flags0 which will be present for + // imported or already exported messages // - // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! + // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! // const getNewUuidsSql = - `SELECT message_id, message_uuid - FROM message m - WHERE area_tag = ? AND message_id > ? AND - (SELECT COUNT(message_id) - FROM message_meta - WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0 - ORDER BY message_id;` - ; + `SELECT message_id, message_uuid + FROM message m + WHERE area_tag = ? AND message_id > ? AND + (SELECT COUNT(message_id) + FROM message_meta + WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0 + ORDER BY message_id;` + ; - // we shouldn't, but be sure we don't try to pick up private mail here + // we shouldn't, but be sure we don't try to pick up private mail here const config = Config(); const areaTags = Object.keys(config.messageNetworks.ftn.areas) .filter(areaTag => Message.WellKnownAreaTags.Private !== areaTag); @@ -1943,8 +1943,8 @@ function FTNMessageScanTossModule() { } // - // For each message that is newer than that of the last scan - // we need to export to each configured associated uplink(s) + // For each message that is newer than that of the last scan + // we need to export to each configured associated uplink(s) // async.waterfall( [ @@ -1967,7 +1967,7 @@ function FTNMessageScanTossModule() { }); }, function exportToConfiguredUplinks(msgRows, callback) { - const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only + const uuidsOnly = msgRows.map(r => r.message_uuid); // convert to array of UUIDs only self.exportEchoMailMessagesToUplinks(uuidsOnly, areaConfig, err => { const newLastScanId = msgRows[msgRows.length - 1].message_id; @@ -1994,35 +1994,35 @@ function FTNMessageScanTossModule() { this.performNetMailExport = function(cb) { // - // Select all messages with a |message_id| > |lastScanId| in the private area - // that are schedule for export to FTN-style networks. + // Select all messages with a |message_id| > |lastScanId| in the private area + // that are schedule for export to FTN-style networks. // - // Just like EchoMail, we additionally exclude messages with the System state_flags0 - // which will be present for imported or already exported messages + // Just like EchoMail, we additionally exclude messages with the System state_flags0 + // which will be present for imported or already exported messages // // - // :TODO: fill out the rest of the consts here - // :TODO: this statement is crazy ugly -- use JOIN / NOT EXISTS for state_flags & 0x02 + // :TODO: fill out the rest of the consts here + // :TODO: this statement is crazy ugly -- use JOIN / NOT EXISTS for state_flags & 0x02 const getNewUuidsSql = - `SELECT message_id, message_uuid - FROM message m - WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND - (SELECT COUNT(message_id) - FROM message_meta - WHERE message_id = m.message_id - AND meta_category = 'System' - AND (meta_name = 'state_flags0' OR meta_name = 'local_to_user_id') - ) = 0 - AND - (SELECT COUNT(message_id) - FROM message_meta - WHERE message_id = m.message_id - AND meta_category = 'System' - AND meta_name = '${Message.SystemMetaNames.ExternalFlavor}' - AND meta_value = '${Message.AddressFlavor.FTN}' - ) = 1 - ORDER BY message_id; - `; + `SELECT message_id, message_uuid + FROM message m + WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND + (SELECT COUNT(message_id) + FROM message_meta + WHERE message_id = m.message_id + AND meta_category = 'System' + AND (meta_name = 'state_flags0' OR meta_name = 'local_to_user_id') + ) = 0 + AND + (SELECT COUNT(message_id) + FROM message_meta + WHERE message_id = m.message_id + AND meta_category = 'System' + AND meta_name = '${Message.SystemMetaNames.ExternalFlavor}' + AND meta_value = '${Message.AddressFlavor.FTN}' + ) = 1 + ORDER BY message_id; + `; async.waterfall( [ @@ -2036,7 +2036,7 @@ function FTNMessageScanTossModule() { } if(0 === rows.length) { - return cb(null); // note |cb| -- early bail out! + return cb(null); // note |cb| -- early bail out! } return callback(null, rows); @@ -2055,17 +2055,17 @@ function FTNMessageScanTossModule() { this.isNetMailMessage = function(message) { return message.isPrivate() && - null === _.get(message, 'meta.System.LocalToUserID', null) && - Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null); + null === _.get(message, 'meta.System.LocalToUserID', null) && + Message.AddressFlavor.FTN === _.get(message, 'meta.System.external_flavor', null); }; } require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule); -// :TODO: *scheduled* portion of this stuff should probably use event_scheduler - @immediate would still use record(). +// :TODO: *scheduled* portion of this stuff should probably use event_scheduler - @immediate would still use record(). FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importDir, cb) { - // :TODO: pass in 'inbound' vs 'secInbound' -- pass along to processSingleTicFile() where password will be checked + // :TODO: pass in 'inbound' vs 'secInbound' -- pass along to processSingleTicFile() where password will be checked const self = this; async.waterfall( @@ -2103,9 +2103,9 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD async.eachSeries(ticFilesInfo, (ticFileInfo, nextTicInfo) => { self.processSingleTicFile(ticFileInfo, err => { if(err) { - // archive rejected TIC stuff (.TIC + attach) + // archive rejected TIC stuff (.TIC + attach) async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { - if(!path) { // possibly rejected due to "File" not existing/etc. + if(!path) { // possibly rejected due to "File" not existing/etc. return nextPath(null); } @@ -2170,10 +2170,10 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { if(exportSchedule) { Log.debug( { - schedule : this.moduleConfig.schedule.export, - schedOK : -1 === exportSchedule.sched.error, - next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), - immediate : exportSchedule.immediate ? true : false, + schedule : this.moduleConfig.schedule.export, + schedOK : -1 === exportSchedule.sched.error, + next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), + immediate : exportSchedule.immediate ? true : false, }, 'Export schedule loaded' ); @@ -2199,10 +2199,10 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { if(importSchedule) { Log.debug( { - schedule : this.moduleConfig.schedule.import, - schedOK : -1 === importSchedule.sched.error, - next : moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), - watchFile : _.isString(importSchedule.watchFile) ? importSchedule.watchFile : 'None', + schedule : this.moduleConfig.schedule.import, + schedOK : -1 === importSchedule.sched.error, + next : moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), + watchFile : _.isString(importSchedule.watchFile) ? importSchedule.watchFile : 'None', }, 'Import schedule loaded' ); @@ -2231,8 +2231,8 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { }); // - // If the watch file already exists, kick off now - // https://github.com/NuSkooler/enigma-bbs/issues/122 + // If the watch file already exists, kick off now + // https://github.com/NuSkooler/enigma-bbs/issues/122 // fse.exists(importSchedule.watchFile, exists => { if(exists) { @@ -2259,14 +2259,14 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) { } // - // Clean up temp dir/files we created + // Clean up temp dir/files we created // temptmp.cleanup( paths => { const fullStats = { - exportDir : this.exportTempDir, - importTemp : this.importTempDir, - paths : paths, - sessionId : temptmp.sessionId, + exportDir : this.exportTempDir, + importTemp : this.importTempDir, + paths : paths, + sessionId : temptmp.sessionId, }; Log.trace(fullStats, 'Temporary directories cleaned up'); @@ -2293,8 +2293,8 @@ FTNMessageScanTossModule.prototype.performImport = function(cb) { FTNMessageScanTossModule.prototype.performExport = function(cb) { // - // We're only concerned with areas related to FTN. For each area, loop though - // and let's find out what messages need exported. + // We're only concerned with areas related to FTN. For each area, loop though + // and let's find out what messages need exported. // if(!this.hasValidConfiguration()) { return cb(new Error('Missing or invalid configuration')); @@ -2307,7 +2307,7 @@ FTNMessageScanTossModule.prototype.performExport = function(cb) { if(err) { Log.warn( { error : err.message, type : type }, 'Error(s) during export' ); } - return nextType(null); // try next, always + return nextType(null); // try next, always }); }, () => { return cb(null); @@ -2316,7 +2316,7 @@ FTNMessageScanTossModule.prototype.performExport = function(cb) { FTNMessageScanTossModule.prototype.record = function(message) { // - // This module works off schedules, but we do support @immediate for export + // This module works off schedules, but we do support @immediate for export // if(true !== this.exportImmediate || !this.hasValidConfiguration()) { return; diff --git a/core/server_module.js b/core/server_module.js index 26000c1b..c9ba1cab 100644 --- a/core/server_module.js +++ b/core/server_module.js @@ -1,9 +1,9 @@ /* jslint node: true */ 'use strict'; -var PluginModule = require('./plugin_module.js').PluginModule; +var PluginModule = require('./plugin_module.js').PluginModule; -exports.ServerModule = ServerModule; +exports.ServerModule = ServerModule; function ServerModule() { PluginModule.call(this); diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index bc221b84..019dbffa 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -1,62 +1,62 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Log = require('../../logger.js').log; -const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').get; +// ENiGMA½ +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; const { splitTextAtTerms, isAnsi, cleanControlCodes -} = require('../../string_util.js'); +} = require('../../string_util.js'); const { getMessageConferenceByTag, getMessageAreaByTag, getMessageListForArea, -} = require('../../message_area.js'); -const { sortAreasOrConfs } = require('../../conf_area_util.js'); -const AnsiPrep = require('../../ansi_prep.js'); +} = require('../../message_area.js'); +const { sortAreasOrConfs } = require('../../conf_area_util.js'); +const AnsiPrep = require('../../ansi_prep.js'); -// deps -const net = require('net'); -const _ = require('lodash'); -const fs = require('graceful-fs'); -const paths = require('path'); -const moment = require('moment'); +// deps +const net = require('net'); +const _ = require('lodash'); +const fs = require('graceful-fs'); +const paths = require('path'); +const moment = require('moment'); const ModuleInfo = exports.moduleInfo = { - name : 'Gopher', - desc : 'Gopher Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.gopher.server', + name : 'Gopher', + desc : 'Gopher Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.gopher.server', }; -const Message = require('../../message.js'); +const Message = require('../../message.js'); const ItemTypes = { - Invalid : '', // not really a type, of course! + Invalid : '', // not really a type, of course! - // Canonical, RFC-1436 - TextFile : '0', - SubMenu : '1', - CCSONameserver : '2', - Error : '3', - BinHexFile : '4', - DOSFile : '5', - UuEncodedFile : '6', - FullTextSearch : '7', - Telnet : '8', - BinaryFile : '9', - AltServer : '+', - GIFFile : 'g', - ImageFile : 'I', - Telnet3270 : 'T', + // Canonical, RFC-1436 + TextFile : '0', + SubMenu : '1', + CCSONameserver : '2', + Error : '3', + BinHexFile : '4', + DOSFile : '5', + UuEncodedFile : '6', + FullTextSearch : '7', + Telnet : '8', + BinaryFile : '9', + AltServer : '+', + GIFFile : 'g', + ImageFile : 'I', + Telnet3270 : 'T', - // Non-canonical - HtmlFile : 'h', - InfoMessage : 'i', - SoundFile : 's', + // Non-canonical + HtmlFile : 'h', + InfoMessage : 'i', + SoundFile : 's', }; exports.getModule = class GopherModule extends ServerModule { @@ -64,7 +64,7 @@ exports.getModule = class GopherModule extends ServerModule { constructor() { super(); - this.routes = new Map(); // selector->generator => gopher item + this.routes = new Map(); // selector->generator => gopher item this.log = Log.child( { server : 'Gopher' } ); } @@ -75,7 +75,7 @@ exports.getModule = class GopherModule extends ServerModule { const config = Config(); this.publicHostname = config.contentServers.gopher.publicHostname; - this.publicPort = config.contentServers.gopher.publicPort; + this.publicPort = config.contentServers.gopher.publicPort; this.addRoute(/^\/?\r\n$/, this.defaultGenerator); this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); @@ -88,7 +88,7 @@ exports.getModule = class GopherModule extends ServerModule { }); socket.on('error', err => { - if('ECONNRESET' !== err.code) { // normal + if('ECONNRESET' !== err.code) { // normal this.log.trace( { error : err.message }, 'Socket error'); } }); @@ -97,7 +97,7 @@ exports.getModule = class GopherModule extends ServerModule { listen() { if(!this.enabled) { - return true; // nothing to do, but not an error + return true; // nothing to do, but not an error } const config = Config(); @@ -115,10 +115,10 @@ exports.getModule = class GopherModule extends ServerModule { } isConfigured() { - // public hostname & port must be set; responses contain them! + // public hostname & port must be set; responses contain them! const config = Config(); return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) && - _.isNumber(_.get(config, 'contentServers.gopher.publicPort')); + _.isNumber(_.get(config, 'contentServers.gopher.publicPort')); } addRoute(selectorRegExp, generatorHandler) { @@ -149,7 +149,7 @@ exports.getModule = class GopherModule extends ServerModule { } makeItem(itemType, text, selector, hostname, port) { - selector = selector || ''; // e.g. for info + selector = selector || ''; // e.g. for info hostname = hostname || this.publicHostname; port = port || this.publicPort; return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; @@ -186,10 +186,10 @@ exports.getModule = class GopherModule extends ServerModule { AnsiPrep( body, { - cols : 79, // Gopher std. wants 70, but we'll have to deal with it. - forceLineTerm : true, // ensure each line is term'd - asciiMode : true, // export to ASCII - fillLines : false, // don't fill up to |cols| + cols : 79, // Gopher std. wants 70, but we'll have to deal with it. + forceLineTerm : true, // ensure each line is term'd + asciiMode : true, // export to ASCII + fillLines : false, // don't fill up to |cols| }, (err, prepped) => { return cb(prepped || body); @@ -207,21 +207,21 @@ exports.getModule = class GopherModule extends ServerModule { messageAreaGenerator(selectorMatch, cb) { this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content'); // - // Selector should be: - // /msgarea - list confs - // /msgarea/conftag - list areas in conf - // /msgarea/conftag/areatag - list messages in area - // /msgarea/conftag/areatag/ - message as text - // /msgarea/conftag/areatag/_raw - full message as text + headers + // Selector should be: + // /msgarea - list confs + // /msgarea/conftag - list areas in conf + // /msgarea/conftag/areatag - list messages in area + // /msgarea/conftag/areatag/ - message as text + // /msgarea/conftag/areatag/_raw - full message as text + headers // if(selectorMatch[3] || selectorMatch[4]) { - // message + // message //const raw = selectorMatch[4] ? true : false; - // :TODO: support 'raw' - const msgUuid = selectorMatch[3].replace(/\r\n|\//g, ''); - const confTag = selectorMatch[1].substr(1).split('/')[0]; - const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); - const message = new Message(); + // :TODO: support 'raw' + const msgUuid = selectorMatch[3].replace(/\r\n|\//g, ''); + const confTag = selectorMatch[1].substr(1).split('/')[0]; + const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); + const message = new Message(); return message.load( { uuid : msgUuid }, err => { if(err) { @@ -248,15 +248,15 @@ Subject: ${message.subject} ID : ${message.messageUuid} (${message.messageId}) ${'-'.repeat(70)} ${msgBody} - `; + `; return cb(response); }); }); } else if(selectorMatch[2]) { - // list messages in area - const confTag = selectorMatch[1].substr(1).split('/')[0]; - const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); - const area = getMessageAreaByTag(areaTag); + // list messages in area + const confTag = selectorMatch[1].substr(1).split('/')[0]; + const areaTag = selectorMatch[2].replace(/\r\n|\//g, ''); + const area = getMessageAreaByTag(areaTag); if(Message.isPrivateAreaTag(areaTag)) { this.log.warn( { areaTag }, 'Attempted access to private area!'); @@ -283,10 +283,10 @@ ${msgBody} return cb(response); }); } else if(selectorMatch[1]) { - // list areas in conf + // list areas in conf const sysConfig = Config(); - const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); - const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); + const confTag = selectorMatch[1].replace(/\r\n|\//g, ''); + const conf = _.get(sysConfig, [ 'contentServers', 'gopher', 'messageConferences', confTag ]) && getMessageConferenceByTag(confTag); if(!conf) { return this.notFoundGenerator(selectorMatch, cb); } @@ -310,10 +310,10 @@ ${msgBody} return cb(response); } else { - // message area base (list confs) + // message area base (list confs) const confs = Object.keys(_.get(Config(), 'contentServers.gopher.messageConferences', {})) .map(confTag => Object.assign( { confTag }, getMessageConferenceByTag(confTag))) - .filter(conf => conf); // remove any baddies + .filter(conf => conf); // remove any baddies if(0 === confs.length) { return cb(this.makeItem(ItemTypes.InfoMessage, 'No message conferences available')); diff --git a/core/servers/content/web.js b/core/servers/content/web.js index c31a3720..7088b8f9 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -1,24 +1,24 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Log = require('../../logger.js').log; -const ServerModule = require('../../server_module.js').ServerModule; -const Config = require('../../config.js').get; +// ENiGMA½ +const Log = require('../../logger.js').log; +const ServerModule = require('../../server_module.js').ServerModule; +const Config = require('../../config.js').get; -// deps -const http = require('http'); -const https = require('https'); -const _ = require('lodash'); -const fs = require('graceful-fs'); -const paths = require('path'); -const mimeTypes = require('mime-types'); +// deps +const http = require('http'); +const https = require('https'); +const _ = require('lodash'); +const fs = require('graceful-fs'); +const paths = require('path'); +const mimeTypes = require('mime-types'); const ModuleInfo = exports.moduleInfo = { - name : 'Web', - desc : 'Web Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.web.server', + name : 'Web', + desc : 'Web Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.web.server', }; class Route { @@ -39,8 +39,8 @@ class Route { isValid() { return ( this.pathRegExp instanceof RegExp && - ( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) || - !_.isFunction(this.handler) + ( -1 !== [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', ].indexOf(this.method) ) || + !_.isFunction(this.handler) ); } @@ -55,28 +55,28 @@ exports.getModule = class WebServerModule extends ServerModule { constructor() { super(); - const config = Config(); - this.enableHttp = config.contentServers.web.http.enabled || false; - this.enableHttps = config.contentServers.web.https.enabled || false; + const config = Config(); + this.enableHttp = config.contentServers.web.http.enabled || false; + this.enableHttps = config.contentServers.web.https.enabled || false; this.routes = {}; if(this.isEnabled() && config.contentServers.web.staticRoot) { this.addRoute({ - method : 'GET', - path : '/static/.*$', - handler : this.routeStaticFile.bind(this), + method : 'GET', + path : '/static/.*$', + handler : this.routeStaticFile.bind(this), }); } } buildUrl(pathAndQuery) { // - // Create a URL such as - // https://l33t.codes:44512/ + |pathAndQuery| + // Create a URL such as + // https://l33t.codes:44512/ + |pathAndQuery| // - // Prefer HTTPS over HTTP. Be explicit about the port - // only if non-standard. Allow users to override full prefix in config. + // Prefer HTTPS over HTTP. Be explicit about the port + // only if non-standard. Allow users to override full prefix in config. // const config = Config(); if(_.isString(config.contentServers.web.overrideUrlPrefix)) { @@ -86,13 +86,13 @@ exports.getModule = class WebServerModule extends ServerModule { let schema; let port; if(config.contentServers.web.https.enabled) { - schema = 'https://'; - port = (443 === config.contentServers.web.https.port) ? + schema = 'https://'; + port = (443 === config.contentServers.web.https.port) ? '' : `:${config.contentServers.web.https.port}`; } else { - schema = 'http://'; - port = (80 === config.contentServers.web.http.port) ? + schema = 'http://'; + port = (80 === config.contentServers.web.http.port) ? '' : `:${config.contentServers.web.http.port}`; } @@ -112,11 +112,11 @@ exports.getModule = class WebServerModule extends ServerModule { const config = Config(); if(this.enableHttps) { const options = { - cert : fs.readFileSync(config.contentServers.web.https.certPem), - key : fs.readFileSync(config.contentServers.web.https.keyPem), + cert : fs.readFileSync(config.contentServers.web.https.certPem), + key : fs.readFileSync(config.contentServers.web.https.keyPem), }; - // additional options + // additional options Object.assign(options, config.contentServers.web.https.options || {} ); this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) ); @@ -178,18 +178,18 @@ exports.getModule = class WebServerModule extends ServerModule { if(err) { return resp.end(` - - - - ${title} - - - -
-

${bodyText}

-
- - ` + + + + ${title} + + + +
+

${bodyText}

+
+ + ` ); } @@ -227,8 +227,8 @@ exports.getModule = class WebServerModule extends ServerModule { } const headers = { - 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'), - 'Content-Length' : stats.size, + 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'), + 'Content-Length' : stats.size, }; const readStream = fs.createReadStream(filePath); @@ -251,8 +251,8 @@ exports.getModule = class WebServerModule extends ServerModule { } const headers = { - 'Content-Type' : contentType || mimeTypes.contentType('.html'), - 'Content-Length' : finalPage.length, + 'Content-Type' : contentType || mimeTypes.contentType('.html'), + 'Content-Length' : finalPage.length, }; resp.writeHead(200, headers); diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 982ab0a1..016c215e 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -1,37 +1,37 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('../../config.js').get; -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const LoginServerModule = require('../../login_server_module.js'); -const userLogin = require('../../user_login.js').userLogin; -const enigVersion = require('../../../package.json').version; -const theme = require('../../theme.js'); -const stringFormat = require('../../string_format.js'); +// ENiGMA½ +const Config = require('../../config.js').get; +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); +const userLogin = require('../../user_login.js').userLogin; +const enigVersion = require('../../../package.json').version; +const theme = require('../../theme.js'); +const stringFormat = require('../../string_format.js'); -// deps -const ssh2 = require('ssh2'); -const fs = require('graceful-fs'); -const util = require('util'); -const _ = require('lodash'); -const assert = require('assert'); +// deps +const ssh2 = require('ssh2'); +const fs = require('graceful-fs'); +const util = require('util'); +const _ = require('lodash'); +const assert = require('assert'); const ModuleInfo = exports.moduleInfo = { - name : 'SSH', - desc : 'SSH Server', - author : 'NuSkooler', - isSecure : true, - packageName : 'codes.l33t.enigma.ssh.server', + name : 'SSH', + desc : 'SSH Server', + author : 'NuSkooler', + isSecure : true, + packageName : 'codes.l33t.enigma.ssh.server', }; function SSHClient(clientConn) { baseClient.Client.apply(this, arguments); // - // WARNING: Until we have emit 'ready', self.input, and self.output and - // not yet defined! + // WARNING: Until we have emit 'ready', self.input, and self.output and + // not yet defined! // const self = this; @@ -39,11 +39,11 @@ function SSHClient(clientConn) { let loginAttempts = 0; clientConn.on('authentication', function authAttempt(ctx) { - const username = ctx.username || ''; - const password = ctx.password || ''; + const username = ctx.username || ''; + const password = ctx.password || ''; - const config = Config(); - self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; + const config = Config(); + self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); @@ -58,8 +58,8 @@ function SSHClient(clientConn) { } // - // If the system is open and |isNewUser| is true, the login - // sequence is hijacked in order to start the applicaiton process. + // If the system is open and |isNewUser| is true, the login + // sequence is hijacked in order to start the applicaiton process. // if(false === config.general.closedSystem && self.isNewUser) { return ctx.accept(); @@ -85,7 +85,7 @@ function SSHClient(clientConn) { } if(0 === username.length) { - // :TODO: can we display something here? + // :TODO: can we display something here? return ctx.reject(); } @@ -105,9 +105,9 @@ function SSHClient(clientConn) { } const artOpts = { - client : self, - name : 'SSHPMPT.ASC', - readSauce : false, + client : self, + name : 'SSHPMPT.ASC', + readSauce : false, }; theme.getThemeArt(artOpts, (err, artInfo) => { @@ -136,31 +136,31 @@ function SSHClient(clientConn) { this.updateTermInfo = function(info) { // - // From ssh2 docs: - // "rows and cols override width and height when rows and cols are non-zero." + // From ssh2 docs: + // "rows and cols override width and height when rows and cols are non-zero." // let termHeight; let termWidth; if(info.rows > 0 && info.cols > 0) { - termHeight = info.rows; - termWidth = info.cols; + termHeight = info.rows; + termWidth = info.cols; } else if(info.width > 0 && info.height > 0) { - termHeight = info.height; - termWidth = info.width; + termHeight = info.height; + termWidth = info.width; } assert(_.isObject(self.term)); // - // Note that if we fail here, connect.js attempts some non-standard - // queries/etc., and ultimately will default to 80x24 if all else fails + // Note that if we fail here, connect.js attempts some non-standard + // queries/etc., and ultimately will default to 80x24 if all else fails // if(termHeight > 0 && termWidth > 0) { self.term.termHeight = termHeight; - self.term.termWidth = termWidth; + self.term.termWidth = termWidth; - self.clearMciCache(); // term size changes = invalidate cache + self.clearMciCache(); // term size changes = invalidate cache } if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { @@ -182,7 +182,7 @@ function SSHClient(clientConn) { accept(); } - if(self.input) { // do we have I/O? + if(self.input) { // do we have I/O? self.updateTermInfo(info); } else { self.cachedTermInfo = info; @@ -203,7 +203,7 @@ function SSHClient(clientConn) { delete self.cachedTermInfo; } - // we're ready! + // we're ready! const firstMenu = self.isNewUser ? Config().loginServers.ssh.firstMenuNewUser : Config().loginServers.ssh.firstMenu; self.emit('ready', { firstMenu : firstMenu } ); }); @@ -222,7 +222,7 @@ function SSHClient(clientConn) { }); clientConn.on('end', () => { - self.emit('end'); // remove client connection/tracking + self.emit('end'); // remove client connection/tracking }); clientConn.on('error', err => { @@ -244,13 +244,13 @@ exports.getModule = class SSHServerModule extends LoginServerModule { const serverConf = { hostKeys : [ { - key : fs.readFileSync(config.loginServers.ssh.privateKeyPem), - passphrase : config.loginServers.ssh.privateKeyPass, + key : fs.readFileSync(config.loginServers.ssh.privateKeyPem), + passphrase : config.loginServers.ssh.privateKeyPass, } ], ident : 'enigma-bbs-' + enigVersion + '-srv', - // Note that sending 'banner' breaks at least EtherTerm! + // Note that sending 'banner' breaks at least EtherTerm! debug : (sshDebugLine) => { if(true === config.loginServers.ssh.traceConnections) { Log.trace(`SSH: ${sshDebugLine}`); diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index ce854013..1f8afa6a 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -1,166 +1,166 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; -const LoginServerModule = require('../../login_server_module.js'); -const Config = require('../../config.js').get; -const EnigAssert = require('../../enigma_assert.js'); -const { stringFromNullTermBuffer } = require('../../string_util.js'); +// ENiGMA½ +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); +const Config = require('../../config.js').get; +const EnigAssert = require('../../enigma_assert.js'); +const { stringFromNullTermBuffer } = require('../../string_util.js'); -// deps -const net = require('net'); -const buffers = require('buffers'); -const { Parser } = require('binary-parser'); -const util = require('util'); +// deps +const net = require('net'); +const buffers = require('buffers'); +const { Parser } = require('binary-parser'); +const util = require('util'); -//var debug = require('debug')('telnet'); +//var debug = require('debug')('telnet'); const ModuleInfo = exports.moduleInfo = { - name : 'Telnet', - desc : 'Telnet Server', - author : 'NuSkooler', - isSecure : false, - packageName : 'codes.l33t.enigma.telnet.server', + name : 'Telnet', + desc : 'Telnet Server', + author : 'NuSkooler', + isSecure : false, + packageName : 'codes.l33t.enigma.telnet.server', }; -exports.TelnetClient = TelnetClient; +exports.TelnetClient = TelnetClient; // -// Telnet Protocol Resources -// * http://pcmicro.com/netfoss/telnet.html -// * http://mud-dev.wikidot.com/telnet:negotiation +// Telnet Protocol Resources +// * http://pcmicro.com/netfoss/telnet.html +// * http://mud-dev.wikidot.com/telnet:negotiation // /* - TODO: - * Document COMMANDS -- add any missing - * Document OPTIONS -- add any missing - * Internally handle OPTIONS: - * Some should be emitted generically - * Some should be handled internally -- denied, handled, etc. - * + TODO: + * Document COMMANDS -- add any missing + * Document OPTIONS -- add any missing + * Internally handle OPTIONS: + * Some should be emitted generically + * Some should be handled internally -- denied, handled, etc. + * - * Allow term (ttype) to be set by environ sub negotiation + * Allow term (ttype) to be set by environ sub negotiation - * Process terms in loop.... research needed + * Process terms in loop.... research needed - * Handle will/won't - * Handle do's, .. - * Some won't should close connection + * Handle will/won't + * Handle do's, .. + * Some won't should close connection - * Options/Commands we don't understand shouldn't crash the server!! + * Options/Commands we don't understand shouldn't crash the server!! */ const COMMANDS = { - SE : 240, // End of Sub-Negotation Parameters - NOP : 241, // No Operation - DM : 242, // Data Mark - BRK : 243, // Break - IP : 244, // Interrupt Process - AO : 245, // Abort Output - AYT : 246, // Are You There? - EC : 247, // Erase Character - EL : 248, // Erase Line - GA : 249, // Go Ahead - SB : 250, // Start Sub-Negotiation Parameters - WILL : 251, // - WONT : 252, - DO : 253, - DONT : 254, - IAC : 255, // (Data Byte) + SE : 240, // End of Sub-Negotation Parameters + NOP : 241, // No Operation + DM : 242, // Data Mark + BRK : 243, // Break + IP : 244, // Interrupt Process + AO : 245, // Abort Output + AYT : 246, // Are You There? + EC : 247, // Erase Character + EL : 248, // Erase Line + GA : 249, // Go Ahead + SB : 250, // Start Sub-Negotiation Parameters + WILL : 251, // + WONT : 252, + DO : 253, + DONT : 254, + IAC : 255, // (Data Byte) }; // -// Resources: -// * http://www.faqs.org/rfcs/rfc1572.html +// Resources: +// * http://www.faqs.org/rfcs/rfc1572.html // const SB_COMMANDS = { - IS : 0, - SEND : 1, - INFO : 2, + IS : 0, + SEND : 1, + INFO : 2, }; // -// Telnet Options +// Telnet Options // -// Resources -// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html -// * http://www.networksorcery.com/enp/protocol/telnet.htm +// Resources +// * http://mars.netanya.ac.il/~unesco/cdrom/booklet/HTML/NETWORKING/node300.html +// * http://www.networksorcery.com/enp/protocol/telnet.htm // const OPTIONS = { - TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 - ECHO : 1, // http://tools.ietf.org/html/rfc857 - // RECONNECTION : 2 - SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 - //APPROX_MESSAGE_SIZE : 4 - STATUS : 5, // http://tools.ietf.org/html/rfc859 - TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 - //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt - //OUPUT_LINE_WIDTH : 8, - //OUTPUT_PAGE_SIZE : 9, // - //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 - //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 - //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 - //OUTPUT_FORMFEED_DISP : 13, // RFC 655 - //OUTPUT_VERT_TABSTOPS : 14, // RFC 656 - //OUTPUT_VERT_TAB_DISP : 15, // RFC 657 - //OUTPUT_LF_DISP : 16, // RFC 658 - //EXTENDED_ASCII : 17, // RFC 659 - //LOGOUT : 18, // RFC 727 - //BYTE_MACRO : 19, // RFC 753 - //DATA_ENTRY_TERMINAL : 20, // RFC 1043 - //SUPDUP : 21, // RFC 736 - //SUPDUP_OUTPUT : 22, // RFC 749 - SEND_LOCATION : 23, // RFC 779 - TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 - //END_OF_RECORD : 25, // RFC 885 - //TACACS_USER_ID : 26, // RFC 927 - //OUTPUT_MARKING : 27, // RFC 933 - //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 - //TELNET_3270_REGIME : 29, // RFC 1041 - WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 - TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 - REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 - LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 - X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 - NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) - AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 - ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 - NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) - //TN3270E : 40, // RFC 2355 - //XAUTH : 41, - //CHARSET : 42, // RFC 2066 - //REMOTE_SERIAL_PORT : 43, - //COM_PORT_CONTROL : 44, // RFC 2217 - //SUPRESS_LOCAL_ECHO : 45, - //START_TLS : 46, - //KERMIT : 47, // RFC 2840 - //SEND_URL : 48, - //FORWARD_X : 49, + TRANSMIT_BINARY : 0, // http://tools.ietf.org/html/rfc856 + ECHO : 1, // http://tools.ietf.org/html/rfc857 + // RECONNECTION : 2 + SUPPRESS_GO_AHEAD : 3, // aka 'SGA': RFC 858 @ http://tools.ietf.org/html/rfc858 + //APPROX_MESSAGE_SIZE : 4 + STATUS : 5, // http://tools.ietf.org/html/rfc859 + TIMING_MARK : 6, // http://tools.ietf.org/html/rfc860 + //RC_TRANS_AND_ECHO : 7, // aka 'RCTE' @ http://www.rfc-base.org/txt/rfc-726.txt + //OUPUT_LINE_WIDTH : 8, + //OUTPUT_PAGE_SIZE : 9, // + //OUTPUT_CARRIAGE_RETURN_DISP : 10, // RFC 652 + //OUTPUT_HORIZ_TABSTOPS : 11, // RFC 653 + //OUTPUT_HORIZ_TAB_DISP : 12, // RFC 654 + //OUTPUT_FORMFEED_DISP : 13, // RFC 655 + //OUTPUT_VERT_TABSTOPS : 14, // RFC 656 + //OUTPUT_VERT_TAB_DISP : 15, // RFC 657 + //OUTPUT_LF_DISP : 16, // RFC 658 + //EXTENDED_ASCII : 17, // RFC 659 + //LOGOUT : 18, // RFC 727 + //BYTE_MACRO : 19, // RFC 753 + //DATA_ENTRY_TERMINAL : 20, // RFC 1043 + //SUPDUP : 21, // RFC 736 + //SUPDUP_OUTPUT : 22, // RFC 749 + SEND_LOCATION : 23, // RFC 779 + TERMINAL_TYPE : 24, // aka 'TTYPE': RFC 1091 @ http://tools.ietf.org/html/rfc1091 + //END_OF_RECORD : 25, // RFC 885 + //TACACS_USER_ID : 26, // RFC 927 + //OUTPUT_MARKING : 27, // RFC 933 + //TERMINCAL_LOCATION_NUMBER : 28, // RFC 946 + //TELNET_3270_REGIME : 29, // RFC 1041 + WINDOW_SIZE : 31, // aka 'NAWS': RFC 1073 @ http://tools.ietf.org/html/rfc1073 + TERMINAL_SPEED : 32, // RFC 1079 @ http://tools.ietf.org/html/rfc1079 + REMOTE_FLOW_CONTROL : 33, // RFC 1072 @ http://tools.ietf.org/html/rfc1372 + LINEMODE : 34, // RFC 1184 @ http://tools.ietf.org/html/rfc1184 + X_DISPLAY_LOCATION : 35, // aka 'XDISPLOC': RFC 1096 @ http://tools.ietf.org/html/rfc1096 + NEW_ENVIRONMENT_DEP : 36, // aka 'NEW-ENVIRON': RFC 1408 @ http://tools.ietf.org/html/rfc1408 (note: RFC 1572 is an update to this) + AUTHENTICATION : 37, // RFC 2941 @ http://tools.ietf.org/html/rfc2941 + ENCRYPT : 38, // RFC 2946 @ http://tools.ietf.org/html/rfc2946 + NEW_ENVIRONMENT : 39, // aka 'NEW-ENVIRON': RFC 1572 @ http://tools.ietf.org/html/rfc1572 (note: update to RFC 1408) + //TN3270E : 40, // RFC 2355 + //XAUTH : 41, + //CHARSET : 42, // RFC 2066 + //REMOTE_SERIAL_PORT : 43, + //COM_PORT_CONTROL : 44, // RFC 2217 + //SUPRESS_LOCAL_ECHO : 45, + //START_TLS : 46, + //KERMIT : 47, // RFC 2840 + //SEND_URL : 48, + //FORWARD_X : 49, - //PRAGMA_LOGON : 138, - //SSPI_LOGON : 139, - //PRAGMA_HEARTBEAT : 140 + //PRAGMA_LOGON : 138, + //SSPI_LOGON : 139, + //PRAGMA_HEARTBEAT : 140 - ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854 + ARE_YOU_THERE : 246, // aka 'AYT' RFC 854 @ https://tools.ietf.org/html/rfc854 - EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) + EXTENDED_OPTIONS_LIST : 255, // RFC 861 (STD 32) }; -// Commands used within NEW_ENVIRONMENT[_DEP] +// Commands used within NEW_ENVIRONMENT[_DEP] const NEW_ENVIRONMENT_COMMANDS = { - VAR : 0, - VALUE : 1, - ESC : 2, - USERVAR : 3, + VAR : 0, + VALUE : 1, + ESC : 2, + USERVAR : 3, }; -const IAC_BUF = Buffer.from([ COMMANDS.IAC ]); -const IAC_SE_BUF = Buffer.from([ COMMANDS.IAC, COMMANDS.SE ]); +const IAC_BUF = Buffer.from([ COMMANDS.IAC ]); +const IAC_SE_BUF = Buffer.from([ COMMANDS.IAC, COMMANDS.SE ]); const COMMAND_NAMES = Object.keys(COMMANDS).reduce(function(names, name) { names[COMMANDS[name]] = name.toLowerCase(); @@ -178,9 +178,9 @@ const COMMAND_IMPLS = {}; }; }); -// :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode +// :TODO: See TooTallNate's telnet.js: Handle COMMAND_IMPL for IAC in binary mode -// Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY +// Create option names such as 'transmit binary' -> OPTIONS.TRANSMIT_BINARY const OPTION_NAMES = Object.keys(OPTIONS).reduce(function(names, name) { names[OPTIONS[name]] = name.toLowerCase().replace(/_/g, ' '); return names; @@ -193,19 +193,19 @@ function unknownOption(bufs, i, event) { } const OPTION_IMPLS = {}; -// :TODO: fill in the rest... -OPTION_IMPLS.NO_ARGS = -OPTION_IMPLS[OPTIONS.ECHO] = -OPTION_IMPLS[OPTIONS.STATUS] = -OPTION_IMPLS[OPTIONS.LINEMODE] = -OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = -OPTION_IMPLS[OPTIONS.AUTHENTICATION] = -OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] = -OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] = -OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = -OPTION_IMPLS[OPTIONS.SEND_LOCATION] = -OPTION_IMPLS[OPTIONS.ARE_YOU_THERE] = -OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { +// :TODO: fill in the rest... +OPTION_IMPLS.NO_ARGS = +OPTION_IMPLS[OPTIONS.ECHO] = +OPTION_IMPLS[OPTIONS.STATUS] = +OPTION_IMPLS[OPTIONS.LINEMODE] = +OPTION_IMPLS[OPTIONS.TRANSMIT_BINARY] = +OPTION_IMPLS[OPTIONS.AUTHENTICATION] = +OPTION_IMPLS[OPTIONS.TERMINAL_SPEED] = +OPTION_IMPLS[OPTIONS.REMOTE_FLOW_CONTROL] = +OPTION_IMPLS[OPTIONS.X_DISPLAY_LOCATION] = +OPTION_IMPLS[OPTIONS.SEND_LOCATION] = +OPTION_IMPLS[OPTIONS.ARE_YOU_THERE] = +OPTION_IMPLS[OPTIONS.SUPPRESS_GO_AHEAD] = function(bufs, i, event) { event.buf = bufs.splice(0, i).toBuffer(); return event; }; @@ -214,12 +214,12 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { if(event.commandCode !== COMMANDS.SB) { OPTION_IMPLS.NO_ARGS(bufs, i, event); } else { - // We need 4 bytes header + data + IAC SE + // We need 4 bytes header + data + IAC SE if(bufs.length < 7) { return MORE_DATA_REQUIRED; } - const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes + const end = bufs.indexOf(IAC_SE_BUF, 5); // look past header bytes if(-1 === end) { return MORE_DATA_REQUIRED; } @@ -232,10 +232,10 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { .uint8('opt') .uint8('is') .array('ttype', { - type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC + type : 'uint8', + readUntil : b => 255 === b, // 255=COMMANDS.IAC }) - // note we read iac2 above + // note we read iac2 above .uint8('se') .parse(bufs.toBuffer()); } catch(e) { @@ -248,10 +248,10 @@ OPTION_IMPLS[OPTIONS.TERMINAL_TYPE] = function(bufs, i, event) { EnigAssert(OPTIONS.TERMINAL_TYPE === ttypeCmd.opt); EnigAssert(SB_COMMANDS.IS === ttypeCmd.is); EnigAssert(ttypeCmd.ttype.length > 0); - // note we found IAC_SE above + // note we found IAC_SE above - // some terminals such as NetRunner provide a NULL-terminated buffer - // slice to remove IAC + // some terminals such as NetRunner provide a NULL-terminated buffer + // slice to remove IAC event.ttype = stringFromNullTermBuffer(ttypeCmd.ttype.slice(0, -1), 'ascii'); bufs.splice(0, end); @@ -264,7 +264,7 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { if(event.commandCode !== COMMANDS.SB) { OPTION_IMPLS.NO_ARGS(bufs, i, event); } else { - // we need 9 bytes + // we need 9 bytes if(bufs.length < 9) { return MORE_DATA_REQUIRED; } @@ -291,39 +291,39 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { EnigAssert(COMMANDS.IAC === nawsCmd.iac2); EnigAssert(COMMANDS.SE === nawsCmd.se); - event.cols = event.columns = event.width = nawsCmd.width; - event.rows = event.height = nawsCmd.height; + event.cols = event.columns = event.width = nawsCmd.width; + event.rows = event.height = nawsCmd.height; } return event; }; -// Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] +// Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] const NEW_ENVIRONMENT_DELIMITERS = []; Object.keys(NEW_ENVIRONMENT_COMMANDS).forEach(function onKey(k) { NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]); }); -// Handle the deprecated RFC 1408 & the updated RFC 1572: -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = -OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { +// Handle the deprecated RFC 1408 & the updated RFC 1572: +OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = +OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { if(event.commandCode !== COMMANDS.SB) { OPTION_IMPLS.NO_ARGS(bufs, i, event); } else { // - // We need 4 bytes header + + IAC SE - // Many terminals send a empty list: - // IAC SB NEW-ENVIRON IS IAC SE + // We need 4 bytes header + + IAC SE + // Many terminals send a empty list: + // IAC SB NEW-ENVIRON IS IAC SE // if(bufs.length < 6) { return MORE_DATA_REQUIRED; } - let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes + let end = bufs.indexOf(IAC_SE_BUF, 4); // look past header bytes if(-1 === end) { return MORE_DATA_REQUIRED; } - // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. + // :TODO: It's likely that we could do all the env name/value parsing directly in Parser. let envCmd; try { @@ -331,12 +331,12 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { .uint8('iac1') .uint8('sb') .uint8('opt') - .uint8('isOrInfo') // IS=initial, INFO=updates + .uint8('isOrInfo') // IS=initial, INFO=updates .array('envBlock', { type : 'uint8', - readUntil : b => 255 === b, // 255=COMMANDS.IAC + readUntil : b => 255 === b, // 255=COMMANDS.IAC }) - // note we consume IAC above + // note we consume IAC above .uint8('se') .parse(bufs.splice(0, bufs.length).toBuffer()); } catch(e) { @@ -350,34 +350,34 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { EnigAssert(SB_COMMANDS.IS === envCmd.isOrInfo || SB_COMMANDS.INFO === envCmd.isOrInfo); if(OPTIONS.NEW_ENVIRONMENT_DEP === envCmd.opt) { - // :TODO: we should probably support this for legacy clients? + // :TODO: we should probably support this for legacy clients? Log.warn('Handling deprecated RFC 1408 NEW-ENVIRON'); } - const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC + const envBuf = envCmd.envBlock.slice(0, -1); // remove IAC - if(envBuf.length < 4) { // TYPE + single char name + sep + single char value - // empty env block + if(envBuf.length < 4) { // TYPE + single char name + sep + single char value + // empty env block return event; } const States = { - Name : 1, - Value : 2, + Name : 1, + Value : 2, }; let state = States.Name; const setVars = {}; const delVars = []; let varName; - // :TODO: handle ESC type!!! + // :TODO: handle ESC type!!! while(envBuf.length) { switch(state) { case States.Name : { const type = parseInt(envBuf.splice(0, 1)); if(![ NEW_ENVIRONMENT_COMMANDS.VAR, NEW_ENVIRONMENT_COMMANDS.USERVAR, NEW_ENVIRONMENT_COMMANDS.ESC ].includes(type)) { - return event; // fail :( + return event; // fail :( } let nameEnd = envBuf.indexOf(NEW_ENVIRONMENT_COMMANDS.VALUE); @@ -387,7 +387,7 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { varName = envBuf.splice(0, nameEnd); if(!varName) { - return event; // something is wrong. + return event; // something is wrong. } varName = Buffer.from(varName).toString('ascii'); @@ -397,7 +397,7 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { state = States.Value; } else { state = States.Name; - delVars.push(varName); // no value; del this var + delVars.push(varName); // no value; del this var } } break; @@ -423,15 +423,15 @@ OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT] = function(bufs, i, event) { } } - // :TODO: Handle deleting previously set vars via delVars - event.type = envCmd.isOrInfo; - event.envVars = setVars; + // :TODO: Handle deleting previously set vars via delVars + event.type = envCmd.isOrInfo; + event.envVars = setVars; } return event; }; -const MORE_DATA_REQUIRED = 0xfeedface; +const MORE_DATA_REQUIRED = 0xfeedface; function parseBufs(bufs) { EnigAssert(bufs.length >= 2); @@ -440,16 +440,16 @@ function parseBufs(bufs) { } function parseCommand(bufs, i, event) { - const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.commandCode = command; - event.command = COMMAND_NAMES[command]; + const command = bufs.get(i); // :TODO: fix deprecation... [i] is not the same + event.commandCode = command; + event.command = COMMAND_NAMES[command]; const handler = COMMAND_IMPLS[command]; if(handler) { return handler(bufs, i + 1, event); } else { if(2 !== bufs.length) { - Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND + Log.warn( { bufsLength : bufs.length }, 'Expected bufs length of 2'); // expected: IAC + COMMAND } event.buf = bufs.splice(0, 2).toBuffer(); @@ -458,9 +458,9 @@ function parseCommand(bufs, i, event) { } function parseOption(bufs, i, event) { - const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same - event.optionCode = option; - event.option = OPTION_NAMES[option]; + const option = bufs.get(i); // :TODO: fix deprecation... [i] is not the same + event.optionCode = option; + event.option = OPTION_NAMES[option]; const handler = OPTION_IMPLS[option]; return handler ? handler(bufs, i + 1, event) : unknownOption(bufs, i + 1, event); @@ -470,20 +470,20 @@ function parseOption(bufs, i, event) { function TelnetClient(input, output) { baseClient.Client.apply(this, arguments); - const self = this; + const self = this; - let bufs = buffers(); - this.bufs = bufs; + let bufs = buffers(); + this.bufs = bufs; - this.sentDont = {}; // DON'T's we've already sent + this.sentDont = {}; // DON'T's we've already sent this.setInputOutput(input, output); - this.negotiationsComplete = false; // are we in the 'negotiation' phase? - this.didReady = false; // have we emit the 'ready' event? + this.negotiationsComplete = false; // are we in the 'negotiation' phase? + this.didReady = false; // have we emit the 'ready' event? this.subNegotiationState = { - newEnvironRequested : false, + newEnvironRequested : false, }; this.dataHandler = function(b) { @@ -498,7 +498,7 @@ function TelnetClient(input, output) { while((i = bufs.indexOf(IAC_BUF)) >= 0) { // - // Some clients will send even IAC separate from data + // Some clients will send even IAC separate from data // if(bufs.length <= (i + 1)) { i = MORE_DATA_REQUIRED; @@ -517,7 +517,7 @@ function TelnetClient(input, output) { break; } else if(i) { if(i.option) { - self.emit(i.option, i); // "transmit binary", "echo", ... + self.emit(i.option, i); // "transmit binary", "echo", ... } self.handleTelnetEvent(i); @@ -530,8 +530,8 @@ function TelnetClient(input, output) { if(MORE_DATA_REQUIRED !== i && bufs.length > 0) { // - // Standard data payload. This can still be "non-user" data - // such as ANSI control, but we don't handle that here. + // Standard data payload. This can still be "non-user" data + // such as ANSI control, but we don't handle that here. // self.emit('data', bufs.splice(0).toBuffer()); } @@ -576,7 +576,7 @@ function TelnetClient(input, output) { util.inherits(TelnetClient, baseClient.Client); /////////////////////////////////////////////////////////////////////////////// -// Telnet Command/Option handling +// Telnet Command/Option handling /////////////////////////////////////////////////////////////////////////////// TelnetClient.prototype.handleTelnetEvent = function(evt) { @@ -584,14 +584,14 @@ TelnetClient.prototype.handleTelnetEvent = function(evt) { return this.connectionWarn( { evt : evt }, 'No command for event'); } - // handler name e.g. 'handleWontCommand' + // handler name e.g. 'handleWontCommand' const handlerName = `handle${evt.command.charAt(0).toUpperCase()}${evt.command.substr(1)}Command`; if(this[handlerName]) { - // specialized + // specialized this[handlerName](evt); } else { - // generic-ish + // generic-ish this.handleMiscCommand(evt); } }; @@ -599,16 +599,16 @@ TelnetClient.prototype.handleTelnetEvent = function(evt) { TelnetClient.prototype.handleWillCommand = function(evt) { if('terminal type' === evt.option) { // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html // this.requestTerminalType(); } else if('new environment' === evt.option) { // - // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html + // See RFC 1572 @ http://www.faqs.org/rfcs/rfc1572.html // this.requestNewEnvironment(); } else { - // :TODO: temporary: + // :TODO: temporary: this.connectionTrace(evt, 'WILL'); } }; @@ -628,20 +628,20 @@ TelnetClient.prototype.handleWontCommand = function(evt) { }; TelnetClient.prototype.handleDoCommand = function(evt) { - // :TODO: handle the rest, e.g. echo nd the like + // :TODO: handle the rest, e.g. echo nd the like if('linemode' === evt.option) { // - // Client wants to enable linemode editing. Denied. + // Client wants to enable linemode editing. Denied. // this.wont.linemode(); } else if('encrypt' === evt.option) { // - // Client wants to enable encryption. Denied. + // Client wants to enable encryption. Denied. // this.wont.encrypt(); } else { - // :TODO: temporary: + // :TODO: temporary: this.connectionTrace(evt, 'DO'); } }; @@ -655,33 +655,33 @@ TelnetClient.prototype.handleSbCommand = function(evt) { if('terminal type' === evt.option) { // - // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // See RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html // - // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html - // We should keep asking until we see a repeat. From there, determine the best type/etc. + // :TODO: According to RFC 1091 @ http://www.faqs.org/rfcs/rfc1091.html + // We should keep asking until we see a repeat. From there, determine the best type/etc. self.setTermType(evt.ttype); - self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout + self.negotiationsComplete = true; // :TODO: throw in a array of what we've taken care. Complete = array satisified or timeout self.readyNow(); } else if('new environment' === evt.option) { // - // Handling is as follows: - // * Map 'TERM' -> 'termType' and only update if ours is 'unknown' - // * Map COLUMNS -> 'termWidth' and only update if ours is 0 - // * Map ROWS -> 'termHeight' and only update if ours is 0 - // * Add any new variables, ignore any existing + // Handling is as follows: + // * Map 'TERM' -> 'termType' and only update if ours is 'unknown' + // * Map COLUMNS -> 'termWidth' and only update if ours is 0 + // * Map ROWS -> 'termHeight' and only update if ours is 0 + // * Add any new variables, ignore any existing // Object.keys(evt.envVars || {} ).forEach(function onEnv(name) { if('TERM' === name && 'unknown' === self.term.termType) { self.setTermType(evt.envVars[name]); } else if('COLUMNS' === name && 0 === self.term.termWidth) { self.term.termWidth = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache + self.clearMciCache(); // term size changes = invalidate cache self.connectionDebug({ termWidth : self.term.termWidth, source : 'NEW-ENVIRON'}, 'Window width updated'); } else if('ROWS' === name && 0 === self.term.termHeight) { self.term.termHeight = parseInt(evt.envVars[name]); - self.clearMciCache(); // term size changes = invalidate cache + self.clearMciCache(); // term size changes = invalidate cache self.connectionDebug({ termHeight : self.term.termHeight, source : 'NEW-ENVIRON'}, 'Window height updated'); } else { if(name in self.term.env) { @@ -704,11 +704,11 @@ TelnetClient.prototype.handleSbCommand = function(evt) { } else if('window size' === evt.option) { // - // Update termWidth & termHeight. - // Set LINES and COLUMNS environment variables as well. + // Update termWidth & termHeight. + // Set LINES and COLUMNS environment variables as well. // - self.term.termWidth = evt.width; - self.term.termHeight = evt.height; + self.term.termWidth = evt.width; + self.term.termHeight = evt.height; if(evt.width > 0) { self.term.env.COLUMNS = evt.height; @@ -718,7 +718,7 @@ TelnetClient.prototype.handleSbCommand = function(evt) { self.term.env.ROWS = evt.height; } - self.clearMciCache(); // term size changes = invalidate cache + self.clearMciCache(); // term size changes = invalidate cache self.connectionDebug({ termWidth : evt.width , termHeight : evt.height, source : 'NAWS' }, 'Window size updated'); } else { @@ -736,11 +736,11 @@ TelnetClient.prototype.handleMiscCommand = function(evt) { EnigAssert(evt.command !== 'undefined' && evt.command.length > 0); // - // See: - // * RFC 854 @ http://tools.ietf.org/html/rfc854 + // See: + // * RFC 854 @ http://tools.ietf.org/html/rfc854 // if('ip' === evt.command) { - // Interrupt Process (IP) + // Interrupt Process (IP) this.log.debug('Interrupt Process (IP) - Ending'); this.input.end(); @@ -817,33 +817,33 @@ TelnetClient.prototype.banner = function() { }; function Command(command, client) { - this.command = COMMANDS[command.toUpperCase()]; - this.client = client; + this.command = COMMANDS[command.toUpperCase()]; + this.client = client; } -// Create Command objects with echo, transmit_binary, ... +// Create Command objects with echo, transmit_binary, ... Object.keys(OPTIONS).forEach(function(name) { const code = OPTIONS[name]; Command.prototype[name.toLowerCase()] = function() { const buf = Buffer.alloc(3); - buf[0] = COMMANDS.IAC; - buf[1] = this.command; - buf[2] = code; + buf[0] = COMMANDS.IAC; + buf[1] = this.command; + buf[2] = code; return this.client.output.write(buf); }; }); -// Create do, dont, etc. methods on Client +// Create do, dont, etc. methods on Client ['do', 'dont', 'will', 'wont'].forEach(function(command) { const get = function() { return new Command(command, this); }; Object.defineProperty(TelnetClient.prototype, command, { - get : get, - enumerable : true, - configurable : true + get : get, + enumerable : true, + configurable : true }); }); @@ -861,9 +861,9 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { this.handleNewClient(client, sock, ModuleInfo); // - // Set a timeout and attempt to proceed even if we don't know - // the term type yet, which is the preferred trigger - // for moving along + // Set a timeout and attempt to proceed even if we don't know + // the term type yet, which is the preferred trigger + // for moving along // setTimeout( () => { if(!client.didReady) { diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index f7dac07d..fa7d97a3 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -1,25 +1,25 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('../../config.js').get; -const TelnetClient = require('./telnet.js').TelnetClient; -const Log = require('../../logger.js').log; -const LoginServerModule = require('../../login_server_module.js'); +// ENiGMA½ +const Config = require('../../config.js').get; +const TelnetClient = require('./telnet.js').TelnetClient; +const Log = require('../../logger.js').log; +const LoginServerModule = require('../../login_server_module.js'); -// deps -const _ = require('lodash'); -const WebSocketServer = require('ws').Server; -const http = require('http'); -const https = require('https'); -const fs = require('graceful-fs'); -const Writable = require('stream'); +// deps +const _ = require('lodash'); +const WebSocketServer = require('ws').Server; +const http = require('http'); +const https = require('https'); +const fs = require('graceful-fs'); +const Writable = require('stream'); const ModuleInfo = exports.moduleInfo = { - name : 'WebSocket', - desc : 'WebSocket Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.websocket.server', + name : 'WebSocket', + desc : 'WebSocket Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.websocket.server', }; function WebSocketClient(ws, req, serverType) { @@ -35,8 +35,8 @@ function WebSocketClient(ws, req, serverType) { }; // - // This bridge makes accessible various calls that client sub classes - // want to access on I/O socket + // This bridge makes accessible various calls that client sub classes + // want to access on I/O socket // this.socketBridge = new class SocketBridge extends Writable { constructor(ws) { @@ -49,12 +49,12 @@ function WebSocketClient(ws, req, serverType) { } write(data, cb) { - cb = cb || ( () => { /* eat it up */} ); // handle data writes after close + cb = cb || ( () => { /* eat it up */} ); // handle data writes after close return this.ws.send(data, { binary : true }, cb); } - // we need to fake some streaming work + // we need to fake some streaming work unpipe() { Log.trace('WebSocket SocketBridge unpipe()'); } @@ -64,7 +64,7 @@ function WebSocketClient(ws, req, serverType) { } get remoteAddress() { - // Support X-Forwarded-For and X-Real-IP headers for proxied connections + // Support X-Forwarded-For and X-Real-IP headers for proxied connections return (self.proxied && (req.headers['x-forwarded-for'] || req.headers['x-real-ip'])) || req.connection.remoteAddress; } }(ws); @@ -72,12 +72,12 @@ function WebSocketClient(ws, req, serverType) { ws.on('message', this.dataHandler); ws.on('close', () => { - // we'll remove client connection which will in turn end() via our SocketBridge above + // we'll remove client connection which will in turn end() via our SocketBridge above return this.emit('end'); }); // - // Montior connection status with ping/pong + // Montior connection status with ping/pong // ws.on('pong', () => { Log.trace(`Pong from ${this.socketBridge.remoteAddress}`); @@ -89,11 +89,11 @@ function WebSocketClient(ws, req, serverType) { Log.trace( { headers : req.headers }, 'WebSocket connection headers' ); // - // If the config allows it, look for 'x-forwarded-proto' as "https" - // to override |isSecure| + // If the config allows it, look for 'x-forwarded-proto' as "https" + // to override |isSecure| // if(true === _.get(Config(), 'loginServers.webSocket.proxied') && - 'https' === req.headers['x-forwarded-proto']) + 'https' === req.headers['x-forwarded-proto']) { Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); this.proxied = true; @@ -101,7 +101,7 @@ function WebSocketClient(ws, req, serverType) { this.proxied = false; } - // start handshake process + // start handshake process this.banner(); } @@ -116,40 +116,40 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { createServer() { // - // We will actually create up to two servers: - // * insecure websocket (ws://) - // * secure (tls) websocket (wss://) + // We will actually create up to two servers: + // * insecure websocket (ws://) + // * secure (tls) websocket (wss://) // const config = _.get(Config(), 'loginServers.webSocket'); if(!_.isObject(config)) { return; } - const wsPort = _.get(config, 'ws.port'); - const wssPort = _.get(config, 'wss.port'); + const wsPort = _.get(config, 'ws.port'); + const wssPort = _.get(config, 'wss.port'); if(true === _.get(config, 'ws.enabled') && _.isNumber(wsPort)) { const httpServer = http.createServer( (req, resp) => { - // dummy handler + // dummy handler resp.writeHead(200); return resp.end('ENiGMA½ BBS WebSocket Server!'); }); this.insecure = { - httpServer : httpServer, - wsServer : new WebSocketServer( { server : httpServer } ), + httpServer : httpServer, + wsServer : new WebSocketServer( { server : httpServer } ), }; } if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) { const httpServer = https.createServer({ - key : fs.readFileSync(config.wss.keyPem), - cert : fs.readFileSync(config.wss.certPem), + key : fs.readFileSync(config.wss.keyPem), + cert : fs.readFileSync(config.wss.certPem), }); this.secure = { - httpServer : httpServer, - wsServer : new WebSocketServer( { server : httpServer } ), + httpServer : httpServer, + wsServer : new WebSocketServer( { server : httpServer } ), }; } } @@ -161,8 +161,8 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { return; } - const serverName = `${ModuleInfo.name} (${serverType})`; - const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); + const serverName = `${ModuleInfo.name} (${serverType})`; + const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); if(isNaN(port)) { Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); @@ -180,7 +180,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { }); // - // Send pings every 30s + // Send pings every 30s // setInterval( () => { WSS_SERVER_TYPES.forEach(serverType => { @@ -191,10 +191,10 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { return ws.terminate(); } - ws.isConnectionAlive = false; // pong will reset this + ws.isConnectionAlive = false; // pong will reset this Log.trace('Ping to remote WebSocket client'); - return ws.ping('', false); // false=don't mask + return ws.ping('', false); // false=don't mask }); } }); diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js index 85b9cfcd..27a27c21 100644 --- a/core/set_newscan_date.js +++ b/core/set_newscan_date.js @@ -1,40 +1,40 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const Errors = require('./enig_error.js').Errors; -const FileEntry = require('./file_entry.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const { getAvailableFileAreaTags } = require('./file_base_area.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const Errors = require('./enig_error.js').Errors; +const FileEntry = require('./file_entry.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const { getAvailableFileAreaTags } = require('./file_base_area.js'); const { getSortedAvailMessageConferences, getSortedAvailMessageAreasByConfTag, updateMessageAreaLastReadId, getMessageIdNewerThanTimestampByArea -} = require('./message_area.js'); -const stringFormat = require('./string_format.js'); +} = require('./message_area.js'); +const stringFormat = require('./string_format.js'); -// deps -const async = require('async'); -const moment = require('moment'); -const _ = require('lodash'); +// deps +const async = require('async'); +const moment = require('moment'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Set New Scan Date', - desc : 'Sets new scan date for applicable scans', - author : 'NuSkooler', + name : 'Set New Scan Date', + desc : 'Sets new scan date for applicable scans', + author : 'NuSkooler', }; const MciViewIds = { main : { - scanDate : 1, - targetSelection : 2, + scanDate : 1, + targetSelection : 2, } }; -// :TODO: for messages, we could insert "conf - all areas" into targets, and allow such +// :TODO: for messages, we could insert "conf - all areas" into targets, and allow such exports.getModule = class SetNewScanDate extends MenuModule { constructor(options) { @@ -42,8 +42,8 @@ exports.getModule = class SetNewScanDate extends MenuModule { const config = this.menuConfig.config; - this.target = config.target || 'message'; - this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; + this.target = config.target || 'message'; + this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; this.menuMethods = { scanDateSubmit : (formData, extraArgs, cb) => { @@ -57,7 +57,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { return cb(Errors.Invalid(`"${_.get(formData, 'value.scanDate')}" is not a valid date`)); } - const targetSelection = _.get(formData, 'value.targetSelection'); // may be undefined if N/A + const targetSelection = _.get(formData, 'value.targetSelection'); // may be undefined if N/A this[`setNewScanDateFor${_.capitalize(this.target)}Base`](targetSelection, scanDate, () => { return this.prevMenu(cb); @@ -72,12 +72,12 @@ exports.getModule = class SetNewScanDate extends MenuModule { return cb(Errors.UnexpectedState('Unable to get target in which to set new scan')); } - // selected area, or all of 'em + // selected area, or all of 'em let updateAreaTags; if('' === target.area.areaTag) { updateAreaTags = this.targetSelections .map( targetSelection => targetSelection.area.areaTag ) - .filter( areaTag => areaTag ); // remove the blank 'all' entry + .filter( areaTag => areaTag ); // remove the blank 'all' entry } else { updateAreaTags = [ target.area.areaTag ]; } @@ -89,7 +89,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { } if(!messageId) { - return nextAreaTag(null); // nothing to do + return nextAreaTag(null); // nothing to do } messageId = Math.max(messageId - 1, 0); @@ -98,7 +98,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { this.client.user.userId, areaTag, messageId, - true, // allowOlder + true, // allowOlder nextAreaTag ); }); @@ -109,16 +109,16 @@ exports.getModule = class SetNewScanDate extends MenuModule { setNewScanDateForFileBase(targetSelection, scanDate, cb) { // - // ENiGMA doesn't currently have the concept of per-area - // scan pointers for users, so we use all areas avail - // to the user. + // ENiGMA doesn't currently have the concept of per-area + // scan pointers for users, so we use all areas avail + // to the user. // const filterCriteria = { - areaTag : getAvailableFileAreaTags(this.client), - newerThanTimestamp : scanDate, - limit : 1, - orderBy : 'upload_timestamp', - order : 'ascending', + areaTag : getAvailableFileAreaTags(this.client), + newerThanTimestamp : scanDate, + limit : 1, + orderBy : 'upload_timestamp', + order : 'ascending', }; FileEntry.findFiles(filterCriteria, (err, fileIds) => { @@ -127,7 +127,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { } if(!fileIds || 0 === fileIds.length) { - // nothing to do + // nothing to do return cb(null); } @@ -136,7 +136,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { return FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, pointerFileId, - true, // allowOlder + true, // allowOlder cb ); }); @@ -144,22 +144,22 @@ exports.getModule = class SetNewScanDate extends MenuModule { loadAvailMessageBaseSelections(cb) { // - // Create an array of objects with conf/area information per entry, - // sorted naturally or via the 'sort' member in config + // Create an array of objects with conf/area information per entry, + // sorted naturally or via the 'sort' member in config // const selections = []; getSortedAvailMessageConferences(this.client).forEach(conf => { getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).forEach(area => { selections.push({ - conf : { + conf : { confTag : conf.confTag, - name : conf.conf.name, - desc : conf.conf.desc, + name : conf.conf.name, + desc : conf.conf.desc, }, - area : { - areaTag : area.areaTag, - name : area.area.name, - desc : area.area.desc, + area : { + areaTag : area.areaTag, + name : area.area.name, + desc : area.area.desc, } }); }); @@ -167,20 +167,20 @@ exports.getModule = class SetNewScanDate extends MenuModule { selections.unshift({ conf : { - confTag : '', - name : 'All conferences', - desc : 'All conferences', + confTag : '', + name : 'All conferences', + desc : 'All conferences', }, - area : { - areaTag : '', - name : 'All areas', - desc : 'All areas', + area : { + areaTag : '', + name : 'All areas', + desc : 'All areas', } }); - // Find current conf/area & move it directly under "All" - const currConfTag = this.client.user.properties.message_conf_tag; - const currAreaTag = this.client.user.properties.message_area_tag; + // Find current conf/area & move it directly under "All" + const currConfTag = this.client.user.properties.message_conf_tag; + const currAreaTag = this.client.user.properties.message_area_tag; if(currConfTag && currAreaTag) { const confAreaIndex = selections.findIndex( confArea => { return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag; @@ -202,8 +202,8 @@ exports.getModule = class SetNewScanDate extends MenuModule { return cb(err); } - const self = this; - const vc = self.addViewController( 'main', new ViewController( { client : this.client } ) ); + const self = this; + const vc = self.addViewController( 'main', new ViewController( { client : this.client } ) ); async.series( [ @@ -211,7 +211,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { if(![ 'message', 'file' ].includes(self.target)) { return callback(Errors.Invalid(`Invalid "target" in config: ${self.target}`)); } - // :TOD0: validate scanDateFormat + // :TOD0: validate scanDateFormat return callback(null); }, function loadFromConfig(callback) { @@ -231,13 +231,13 @@ exports.getModule = class SetNewScanDate extends MenuModule { const scanDateView = vc.getView(MciViewIds.main.scanDate); - // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now + // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now const scanDateFormat = self.scanDateFormat.replace(/[/\-. ]/g, ''); scanDateView.setText(today.format(scanDateFormat)); if('message' === self.target) { - const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}'; - const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat; + const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}'; + const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat; const targetSelectionView = vc.getView(MciViewIds.main.targetSelection); diff --git a/core/show_art.js b/core/show_art.js index bb917e91..66e330db 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -1,20 +1,20 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const Errors = require('../core/enig_error.js').Errors; -const ANSI = require('./ansi_term.js'); -const Config = require('./config.js').get; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const Errors = require('../core/enig_error.js').Errors; +const ANSI = require('./ansi_term.js'); +const Config = require('./config.js').get; -// deps -const async = require('async'); -const _ = require('lodash'); +// deps +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Show Art', - desc : 'Module for more advanced methods of displaying art', - author : 'NuSkooler', + name : 'Show Art', + desc : 'Module for more advanced methods of displaying art', + author : 'NuSkooler', }; exports.getModule = class ShowArtModule extends MenuModule { @@ -36,13 +36,13 @@ exports.getModule = class ShowArtModule extends MenuModule { }, function showArt(callback) { // - // How we show art depends on our configuration + // How we show art depends on our configuration // let handler = { - extraArgs : self.showByExtraArgs, - sequence : self.showBySequence, - random : self.showByRandom, - fileBaseArea : self.showByFileBaseArea, + extraArgs : self.showByExtraArgs, + sequence : self.showBySequence, + random : self.showByRandom, + fileBaseArea : self.showByFileBaseArea, }[self.config.method] || self.showRandomArt; handler = handler.bind(self); @@ -68,8 +68,8 @@ exports.getModule = class ShowArtModule extends MenuModule { return cb(err); } const options = { - pause : this.shouldPause(), - desc : 'extraArgs', + pause : this.shouldPause(), + desc : 'extraArgs', }; return this.displaySingleArtWithOptions(artSpec, options, cb); }); @@ -89,14 +89,14 @@ exports.getModule = class ShowArtModule extends MenuModule { return cb(err); } - // further resolve key -> file base area art + // further resolve key -> file base area art const artSpec = _.get(Config(), [ 'fileBase', 'areas', key, 'art' ]); if(!artSpec) { return cb(Errors.MissingConfig(`No art defined for file base area "${key}"`)); } const options = { - pause : this.shouldPause(), - desc : 'fileBaseArea', + pause : this.shouldPause(), + desc : 'fileBaseArea', }; return this.displaySingleArtWithOptions(artSpec, options, cb); }); @@ -122,7 +122,7 @@ exports.getModule = class ShowArtModule extends MenuModule { async.waterfall( [ function art(callback) { - // :TODO: we really need a way to supply an explicit path to look in, e.g. general/area_art/ + // :TODO: we really need a way to supply an explicit path to look in, e.g. general/area_art/ self.displayAsset( artSpec, self.menuConfig.options, @@ -137,7 +137,7 @@ exports.getModule = class ShowArtModule extends MenuModule { }, function recordCursorPosition(mciData, callback) { if(!options.pause) { - return callback(null, mciData, null); // cursor position not needed + return callback(null, mciData, null); // cursor position not needed } self.client.once('cursor position report', pos => { diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index b46dda51..e6be1232 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -1,29 +1,29 @@ /* jslint node: true */ 'use strict'; -const MenuView = require('./menu_view.js').MenuView; -const ansi = require('./ansi_term.js'); -const strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.SpinnerMenuView = SpinnerMenuView; +exports.SpinnerMenuView = SpinnerMenuView; function SpinnerMenuView(options) { - options.justify = options.justify || 'left'; - options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; + options.cursor = options.cursor || 'hide'; MenuView.call(this, options); var self = this; /* - this.cachePositions = function() { - self.positionCacheExpired = false; - }; - */ + this.cachePositions = function() { + self.positionCacheExpired = false; + }; + */ this.updateSelection = function() { //assert(!self.positionCacheExpired); @@ -66,9 +66,9 @@ SpinnerMenuView.prototype.setFocus = function(focused) { }; SpinnerMenuView.prototype.setFocusItemIndex = function(index) { - SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - this.updateSelection(); // will redraw + this.updateSelection(); // will redraw }; SpinnerMenuView.prototype.onKeyPress = function(ch, key) { diff --git a/core/standard_menu.js b/core/standard_menu.js index f22153b2..a4dacb95 100644 --- a/core/standard_menu.js +++ b/core/standard_menu.js @@ -1,12 +1,12 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; +const MenuModule = require('./menu_module.js').MenuModule; exports.moduleInfo = { - name : 'Standard Menu Module', - desc : 'A Menu Module capable of handing standard configurations', - author : 'NuSkooler', + name : 'Standard Menu Module', + desc : 'A Menu Module capable of handing standard configurations', + author : 'NuSkooler', }; exports.getModule = class StandardMenuModule extends MenuModule { @@ -20,7 +20,7 @@ exports.getModule = class StandardMenuModule extends MenuModule { return cb(err); } - // we do this so other modules can be both customized and still perform standard tasks + // we do this so other modules can be both customized and still perform standard tasks return this.standardMCIReadyHandler(mciData, cb); }); } diff --git a/core/stat_log.js b/core/stat_log.js index 849404c2..4b076e6d 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -1,23 +1,23 @@ /* jslint node: true */ 'use strict'; -const sysDb = require('./database.js').dbs.system; +const sysDb = require('./database.js').dbs.system; -// deps -const _ = require('lodash'); -const moment = require('moment'); +// deps +const _ = require('lodash'); +const moment = require('moment'); /* - System Event Log & Stats - ------------------------ + System Event Log & Stats + ------------------------ - System & user specific: - * Events for generating various statistics, logs such as last callers, etc. - * Stats such as counters + System & user specific: + * Events for generating various statistics, logs such as last callers, etc. + * Stats such as counters - User specific stats are simply an alternate interface to user properties, while - system wide entries are handled on their own. Both are read accessible non-blocking - making them easily available for MCI codes for example. + User specific stats are simply an alternate interface to user properties, while + system wide entries are handled on their own. Both are read accessible non-blocking + making them easily available for MCI codes for example. */ class StatLog { constructor() { @@ -26,13 +26,13 @@ class StatLog { init(cb) { // - // Load previous state/values of |this.systemStats| + // Load previous state/values of |this.systemStats| // const self = this; sysDb.each( `SELECT stat_name, stat_value - FROM system_stat;`, + FROM system_stat;`, (err, row) => { if(row) { self.systemStats[row.stat_name] = row.stat_value; @@ -52,19 +52,19 @@ class StatLog { get KeepType() { return { - Forever : 'forever', - Days : 'days', - Max : 'max', - Count : 'max', + Forever : 'forever', + Days : 'days', + Max : 'max', + Count : 'max', }; } get Order() { return { - Timestamp : 'timestamp_asc', - TimestampAsc : 'timestamp_asc', - TimestampDesc : 'timestamp_desc', - Random : 'random', + Timestamp : 'timestamp_asc', + TimestampAsc : 'timestamp_asc', + TimestampDesc : 'timestamp_desc', + Random : 'random', }; } @@ -73,16 +73,16 @@ class StatLog { } setSystemStat(statName, statValue, cb) { - // live stats + // live stats this.systemStats[statName] = statValue; - // persisted stats + // persisted stats sysDb.run( `REPLACE INTO system_stat (stat_name, stat_value) - VALUES (?, ?);`, + VALUES (?, ?);`, [ statName, statValue ], err => { - // cb optional - callers may fire & forget + // cb optional - callers may fire & forget if(cb) { return cb(err); } @@ -114,11 +114,11 @@ class StatLog { } // - // User specific stats - // These are simply convience methods to the user's properties + // User specific stats + // These are simply convience methods to the user's properties // setUserStat(user, statName, statValue, cb) { - // note: cb is optional in PersistUserProperty + // note: cb is optional in PersistUserProperty return user.persistProperty(statName, statValue, cb); } @@ -147,17 +147,17 @@ class StatLog { return this.setUserStat(user, statName, newValue, cb); } - // the time "now" in the ISO format we use and love :) + // the time "now" in the ISO format we use and love :) get now() { return moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } appendSystemLogEntry(logName, logValue, keep, keepType, cb) { sysDb.run( `INSERT INTO system_event_log (timestamp, log_name, log_value) - VALUES (?, ?, ?);`, + VALUES (?, ?, ?);`, [ this.now, logName, logValue ], () => { // - // Handle keep + // Handle keep // if(-1 === keep) { if(cb) { @@ -167,14 +167,14 @@ class StatLog { } switch(keepType) { - // keep # of days + // keep # of days case 'days' : sysDb.run( `DELETE FROM system_event_log - WHERE log_name = ? AND timestamp <= DATETIME("now", "-${keep} day");`, + WHERE log_name = ? AND timestamp <= DATETIME("now", "-${keep} day");`, [ logName ], err => { - // cb optional - callers may fire & forget + // cb optional - callers may fire & forget if(cb) { return cb(err); } @@ -184,16 +184,16 @@ class StatLog { case 'count': case 'max' : - // keep max of N/count + // keep max of N/count sysDb.run( `DELETE FROM system_event_log - WHERE id IN( - SELECT id - FROM system_event_log - WHERE log_name = ? - ORDER BY id DESC - LIMIT -1 OFFSET ${keep} - );`, + WHERE id IN( + SELECT id + FROM system_event_log + WHERE log_name = ? + ORDER BY id DESC + LIMIT -1 OFFSET ${keep} + );`, [ logName ], err => { if(cb) { @@ -205,7 +205,7 @@ class StatLog { case 'forever' : default : - // nop + // nop break; } } @@ -214,9 +214,9 @@ class StatLog { getSystemLogEntries(logName, order, limit, cb) { let sql = - `SELECT timestamp, log_value - FROM system_event_log - WHERE log_name = ?`; + `SELECT timestamp, log_value + FROM system_event_log + WHERE log_name = ?`; switch(order) { case 'timestamp' : @@ -228,13 +228,13 @@ class StatLog { sql += ' ORDER BY timestamp DESC'; break; - case 'random' : + case 'random' : sql += ' ORDER BY RANDOM()'; } if(!cb && _.isFunction(limit)) { - cb = limit; - limit = 0; + cb = limit; + limit = 0; } else { limit = limit || 0; } @@ -253,11 +253,11 @@ class StatLog { appendUserLogEntry(user, logName, logValue, keepDays, cb) { sysDb.run( `INSERT INTO user_event_log (timestamp, user_id, log_name, log_value) - VALUES (?, ?, ?, ?);`, + VALUES (?, ?, ?, ?);`, [ this.now, user.userId, logName, logValue ], () => { // - // Handle keepDays + // Handle keepDays // if(-1 === keepDays) { if(cb) { @@ -268,10 +268,10 @@ class StatLog { sysDb.run( `DELETE FROM user_event_log - WHERE user_id = ? AND log_name = ? AND timestamp <= DATETIME("now", "-${keepDays} day");`, + WHERE user_id = ? AND log_name = ? AND timestamp <= DATETIME("now", "-${keepDays} day");`, [ user.userId, logName ], err => { - // cb optional - callers may fire & forget + // cb optional - callers may fire & forget if(cb) { return cb(err); } diff --git a/core/string_format.js b/core/string_format.js index 7857f2b3..cef937c4 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -const EnigError = require('./enig_error.js').EnigError; +const EnigError = require('./enig_error.js').EnigError; const { pad, @@ -10,41 +10,41 @@ const { renderSubstr, formatByteSize, formatByteSizeAbbr, formatCount, formatCountAbbr, -} = require('./string_util.js'); +} = require('./string_util.js'); -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); /* - String formatting HEAVILY inspired by David Chambers string-format library - and the mini-language branch specifically which was gratiously released - under the DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE. + String formatting HEAVILY inspired by David Chambers string-format library + and the mini-language branch specifically which was gratiously released + under the DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE. - We need some extra functionality. Namely, support for RA style pipe codes - and ANSI escape sequences. + We need some extra functionality. Namely, support for RA style pipe codes + and ANSI escape sequences. */ class ValueError extends EnigError { } class KeyError extends EnigError { } const SpecRegExp = { - FillAlign : /^(.)?([<>=^])/, - Sign : /^[ +-]/, - Width : /^\d*/, - Precision : /^\d+/, + FillAlign : /^(.)?([<>=^])/, + Sign : /^[ +-]/, + Width : /^\d*/, + Precision : /^\d+/, }; function tokenizeFormatSpec(spec) { const tokens = { - fill : '', - align : '', - sign : '', - '#' : false, - '0' : false, - width : '', - ',' : false, - precision : '', - type : '', + fill : '', + align : '', + sign : '', + '#' : false, + '0' : false, + width : '', + ',' : false, + precision : '', + type : '', }; let index = 0; @@ -110,7 +110,7 @@ function tokenizeFormatSpec(spec) { } if(tokens[','] && 's' === tokens.type) { - throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes + throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes } return tokens; @@ -129,16 +129,16 @@ function getPadAlign(align) { } function formatString(value, tokens) { - const fill = tokens.fill || (tokens['0'] ? '0' : ' '); - const align = tokens.align || (tokens['0'] ? '=' : '<'); - const precision = Number(tokens.precision || renderStringLength(value) + 1); + const fill = tokens.fill || (tokens['0'] ? '0' : ' '); + const align = tokens.align || (tokens['0'] ? '=' : '<'); + const precision = Number(tokens.precision || renderStringLength(value) + 1); if('' !== tokens.type && 's' !== tokens.type) { throw new ValueError(`Unknown format code "${tokens.type}" for String object`); } if(tokens[',']) { - throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes + throw new ValueError(`Cannot specify ',' with 's'`); // eslint-disable-line quotes } if(tokens.sign) { @@ -157,8 +157,8 @@ function formatString(value, tokens) { } const FormatNumRegExp = { - UpperType : /[A-Z]/, - ExponentRep : /e[+-](?=\d$)/, + UpperType : /[A-Z]/, + ExponentRep : /e[+-](?=\d$)/, }; function formatNumberHelper(n, precision, type) { @@ -175,7 +175,7 @@ function formatNumberHelper(n, precision, type) { case 'e' : return n.toExponential(precision).replace(FormatNumRegExp.ExponentRep, '$&0'); case 'f' : return n.toFixed(precision); case 'g' : - // we don't want useless trailing zeros. parseFloat -> back to string fixes this for us + // we don't want useless trailing zeros. parseFloat -> back to string fixes this for us return parseFloat(n.toPrecision(precision || 1)).toString(); case '%' : return formatNumberHelper(n * 100, precision, 'f') + '%'; @@ -187,10 +187,10 @@ function formatNumberHelper(n, precision, type) { } function formatNumber(value, tokens) { - const fill = tokens.fill || (tokens['0'] ? '0' : ' '); - const align = tokens.align || (tokens['0'] ? '=' : '>'); - const width = Number(tokens.width); - const type = tokens.type || (tokens.precision ? 'g' : ''); + const fill = tokens.fill || (tokens['0'] ? '0' : ' '); + const align = tokens.align || (tokens['0'] ? '=' : '>'); + const width = Number(tokens.width); + const type = tokens.type || (tokens.precision ? 'g' : ''); if( [ 'c', 'd', 'b', 'o', 'x', 'X' ].indexOf(type) > -1) { if(0 !== value % 1) { @@ -198,7 +198,7 @@ function formatNumber(value, tokens) { } if('' !== tokens.sign && 'c' !== type) { - throw new ValueError(`Sign not allowed with integer format specifier 'c'`); // eslint-disable-line quotes + throw new ValueError(`Sign not allowed with integer format specifier 'c'`); // eslint-disable-line quotes } if(tokens[','] && 'd' !== type) { @@ -214,16 +214,16 @@ function formatNumber(value, tokens) { } } - const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type); - const sign = value < 0 || 1 / value < 0 ? + const s = formatNumberHelper(Math.abs(value), Number(tokens.precision || 6), type); + const sign = value < 0 || 1 / value < 0 ? '-' : '-' === tokens.sign ? '' : tokens.sign; - const prefix = tokens['#'] && ( [ 'b', 'o', 'x', 'X' ].indexOf(type) > -1 ) ? '0' + type : ''; + const prefix = tokens['#'] && ( [ 'b', 'o', 'x', 'X' ].indexOf(type) > -1 ) ? '0' + type : ''; if(tokens[',']) { - const match = /^(\d*)(.*)$/.exec(s); - const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; + const match = /^(\d*)(.*)$/.exec(s); + const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; if('=' !== align) { return pad(sign + separated, width, fill, getPadAlign(align)); @@ -231,9 +231,9 @@ function formatNumber(value, tokens) { if('0' === fill) { const shortfall = Math.max(0, width - sign.length - separated.length); - const digits = /^\d*/.exec(separated)[0].length; - let padding = ''; - // :TODO: do this differntly... + const digits = /^\d*/.exec(separated)[0].length; + let padding = ''; + // :TODO: do this differntly... for(let n = 0; n < shortfall; n++) { padding = ((digits + n) % 4 === 3 ? ',' : '0') + padding; } @@ -256,31 +256,31 @@ function formatNumber(value, tokens) { } const transformers = { - // String standard - toUpperCase : String.prototype.toUpperCase, - toLowerCase : String.prototype.toLowerCase, + // String standard + toUpperCase : String.prototype.toUpperCase, + toLowerCase : String.prototype.toLowerCase, - // some super l33b BBS styles!! - styleUpper : (s) => stylizeString(s, 'upper'), - styleLower : (s) => stylizeString(s, 'lower'), - styleTitle : (s) => stylizeString(s, 'title'), - styleFirstLower : (s) => stylizeString(s, 'first lower'), - styleSmallVowels : (s) => stylizeString(s, 'small vowels'), - styleBigVowels : (s) => stylizeString(s, 'big vowels'), - styleSmallI : (s) => stylizeString(s, 'small i'), - styleMixed : (s) => stylizeString(s, 'mixed'), - styleL33t : (s) => stylizeString(s, 'l33t'), + // some super l33b BBS styles!! + styleUpper : (s) => stylizeString(s, 'upper'), + styleLower : (s) => stylizeString(s, 'lower'), + styleTitle : (s) => stylizeString(s, 'title'), + styleFirstLower : (s) => stylizeString(s, 'first lower'), + styleSmallVowels : (s) => stylizeString(s, 'small vowels'), + styleBigVowels : (s) => stylizeString(s, 'big vowels'), + styleSmallI : (s) => stylizeString(s, 'small i'), + styleMixed : (s) => stylizeString(s, 'mixed'), + styleL33t : (s) => stylizeString(s, 'l33t'), - // :TODO: - // toMegs(), toKilobytes(), ... - // toList(), toCommaList(), + // :TODO: + // toMegs(), toKilobytes(), ... + // toList(), toCommaList(), - sizeWithAbbr : (n) => formatByteSize(n, true, 2), - sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), - sizeAbbr : (n) => formatByteSizeAbbr(n), - countWithAbbr : (n) => formatCount(n, true, 0), - countWithoutAbbr : (n) => formatCount(n, false, 0), - countAbbr : (n) => formatCountAbbr(n), + sizeWithAbbr : (n) => formatByteSize(n, true, 2), + sizeWithoutAbbr : (n) => formatByteSize(n, false, 2), + sizeAbbr : (n) => formatByteSizeAbbr(n), + countWithAbbr : (n) => formatCount(n, true, 0), + countWithoutAbbr : (n) => formatCount(n, false, 0), + countAbbr : (n) => formatCountAbbr(n), }; function transformValue(transformerName, value) { @@ -292,8 +292,8 @@ function transformValue(transformerName, value) { return value; } -// :TODO: Use explicit set of chars for paths & function/transforms such that } is allowed as fill/etc. -const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:!([^:}]+))?(?::([^}]+))?}/g; +// :TODO: Use explicit set of chars for paths & function/transforms such that } is allowed as fill/etc. +const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:!([^:}]+))?(?::([^}]+))?}/g; function getValue(obj, path) { const value = _.get(obj, path); @@ -307,7 +307,7 @@ function getValue(obj, path) { module.exports = function format(fmt, obj) { const re = REGEXP_BASIC_FORMAT; - re.lastIndex = 0; // reset from prev + re.lastIndex = 0; // reset from prev let match; let pos; @@ -319,17 +319,17 @@ module.exports = function format(fmt, obj) { let tokens; do { - pos = re.lastIndex; - match = re.exec(fmt); + pos = re.lastIndex; + match = re.exec(fmt); if(match) { if(match.index > pos) { out += fmt.slice(pos, match.index); } - objPath = match[1]; - transformer = match[2]; - formatSpec = match[3]; + objPath = match[1]; + transformer = match[2]; + formatSpec = match[3]; value = getValue(obj, objPath); if(transformer) { @@ -347,7 +347,7 @@ module.exports = function format(fmt, obj) { } while(0 !== re.lastIndex); - // remainder + // remainder if(pos < fmt.length) { out += fmt.slice(pos); } diff --git a/core/string_util.js b/core/string_util.js index 88f0e57e..8fab88b8 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -1,35 +1,35 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const ANSI = require('./ansi_term.js'); +// ENiGMA½ +const ANSI = require('./ansi_term.js'); -// deps -const iconv = require('iconv-lite'); -const _ = require('lodash'); +// deps +const iconv = require('iconv-lite'); +const _ = require('lodash'); -exports.stylizeString = stylizeString; -exports.pad = pad; -exports.insert = insert; -exports.replaceAt = replaceAt; -exports.isPrintable = isPrintable; -exports.stripAllLineFeeds = stripAllLineFeeds; -exports.debugEscapedString = debugEscapedString; -exports.stringFromNullTermBuffer = stringFromNullTermBuffer; -exports.stringToNullTermBuffer = stringToNullTermBuffer; -exports.renderSubstr = renderSubstr; -exports.renderStringLength = renderStringLength; -exports.formatByteSizeAbbr = formatByteSizeAbbr; -exports.formatByteSize = formatByteSize; -exports.formatCountAbbr = formatCountAbbr; -exports.formatCount = formatCount; -exports.cleanControlCodes = cleanControlCodes; -exports.isAnsi = isAnsi; -exports.isAnsiLine = isAnsiLine; -exports.isFormattedLine = isFormattedLine; -exports.splitTextAtTerms = splitTextAtTerms; +exports.stylizeString = stylizeString; +exports.pad = pad; +exports.insert = insert; +exports.replaceAt = replaceAt; +exports.isPrintable = isPrintable; +exports.stripAllLineFeeds = stripAllLineFeeds; +exports.debugEscapedString = debugEscapedString; +exports.stringFromNullTermBuffer = stringFromNullTermBuffer; +exports.stringToNullTermBuffer = stringToNullTermBuffer; +exports.renderSubstr = renderSubstr; +exports.renderStringLength = renderStringLength; +exports.formatByteSizeAbbr = formatByteSizeAbbr; +exports.formatByteSize = formatByteSize; +exports.formatCountAbbr = formatCountAbbr; +exports.formatCount = formatCount; +exports.cleanControlCodes = cleanControlCodes; +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' ]; VOWELS.concat(VOWELS.map(l => l.toUpperCase())); @@ -49,36 +49,36 @@ function stylizeString(s, style) { var stylized = ''; switch(style) { - // None/normal + // None/normal case 'normal' : case 'N' : return s; - // UPPERCASE + // UPPERCASE case 'upper' : case 'U' : return s.toUpperCase(); - // lowercase + // lowercase case 'lower' : case 'l' : return s.toLowerCase(); - // Title Case + // Title Case case 'title' : case 'T' : return s.replace(/\w\S*/g, function onProperCaseChar(t) { return t.charAt(0).toUpperCase() + t.substr(1).toLowerCase(); }); - // fIRST lOWER + // fIRST lOWER case 'first lower' : case 'f' : return s.replace(/\w\S*/g, function onFirstLowerChar(t) { return t.charAt(0).toLowerCase() + t.substr(1).toUpperCase(); }); - // SMaLL VoWeLS + // SMaLL VoWeLS case 'small vowels' : case 'v' : for(i = 0; i < len; ++i) { @@ -91,7 +91,7 @@ function stylizeString(s, style) { } return stylized; - // bIg vOwELS + // bIg vOwELS case 'big vowels' : case 'V' : for(i = 0; i < len; ++i) { @@ -104,12 +104,12 @@ function stylizeString(s, style) { } return stylized; - // Small i's: DEMENTiA + // Small i's: DEMENTiA case 'small i' : case 'i' : return s.toUpperCase().replace(/I/g, 'i'); - // mIxeD CaSE (random upper/lower) + // mIxeD CaSE (random upper/lower) case 'mixed' : case 'M' : for(i = 0; i < len; i++) { @@ -121,7 +121,7 @@ function stylizeString(s, style) { } return stylized; - // l337 5p34k + // l337 5p34k case 'l33t' : case '3' : for(i = 0; i < len; ++i) { @@ -135,15 +135,15 @@ function stylizeString(s, style) { } function pad(s, len, padChar, justify, stringSGR, padSGR, useRenderLen) { - len = len || 0; - padChar = padChar || ' '; - justify = justify || 'left'; - stringSGR = stringSGR || ''; - padSGR = padSGR || ''; - useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; + len = len || 0; + padChar = padChar || ' '; + justify = justify || 'left'; + stringSGR = stringSGR || ''; + padSGR = padSGR || ''; + useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; - const renderLen = useRenderLen ? renderStringLength(s) : s.length; - const padlen = len >= renderLen ? len - renderLen : 0; + const renderLen = useRenderLen ? renderStringLength(s) : s.length; + const padlen = len >= renderLen ? len - renderLen : 0; switch(justify) { case 'L' : @@ -155,9 +155,9 @@ function pad(s, len, padChar, justify, stringSGR, padSGR, useRenderLen) { case 'center' : case 'both' : { - const right = Math.ceil(padlen / 2); - const left = padlen - right; - s = `${padSGR}${Array(left + 1).join(padChar)}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; + const right = Math.ceil(padlen / 2); + const left = padlen - right; + s = `${padSGR}${Array(left + 1).join(padChar)}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; } break; @@ -181,16 +181,16 @@ function replaceAt(s, n, t) { } 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 + /[\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) { // - // See the following: - // https://mathiasbynens.be/notes/javascript-unicode - // http://stackoverflow.com/questions/11598786/how-to-replace-non-printable-unicode-characters-javascript - // http://stackoverflow.com/questions/12052825/regular-expression-for-all-printable-characters-in-javascript + // See the following: + // https://mathiasbynens.be/notes/javascript-unicode + // http://stackoverflow.com/questions/11598786/how-to-replace-non-printable-unicode-characters-javascript + // http://stackoverflow.com/questions/12052825/regular-expression-for-all-printable-characters-in-javascript // - // :TODO: Probably need somthing better here. + // :TODO: Probably need somthing better here. return !RE_NON_PRINTABLE.test(s); } @@ -213,29 +213,29 @@ function stringFromNullTermBuffer(buf, encoding) { 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 + 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 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.getFullMatchRegExp().source, '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_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 // function renderSubstr(str, start, length) { - // shortcut for empty strings + // shortcut for empty strings if(0 === str.length) { return str; } - start = start || 0; - length = length || str.length - start; + start = start || 0; + length = length || str.length - start; const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the obj; must reset! + re.lastIndex = 0; // we recycle the obj; must reset! let pos = 0; let match; @@ -243,22 +243,22 @@ function renderSubstr(str, start, length) { let renderLen = 0; let s; do { - pos = re.lastIndex; - match = re.exec(str); + pos = re.lastIndex; + match = re.exec(str); if(match) { if(match.index > pos) { s = str.slice(pos + start, Math.min(match.index, pos + (length - renderLen))); - start = 0; // start offset applies only once - out += s; - renderLen += s.length; + start = 0; // start offset applies only once + out += s; + renderLen += s.length; } out += match[0]; } } while(renderLen < length && 0 !== re.lastIndex); - // remainder + // remainder if(pos + start < str.length && renderLen < length) { out += str.slice(pos + start, (pos + start + (length - renderLen))); //out += str.slice(pos + start, Math.max(1, pos + (length - renderLen - 1))); @@ -268,12 +268,12 @@ function renderSubstr(str, start, length) { } // -// Method to return the "rendered" length taking into account Pipe and ANSI color codes. +// Method to return the "rendered" length taking into account Pipe and ANSI color codes. // -// We additionally account for ANSI *forward* movement ESC sequences -// in the form of ESC[C where is the "go forward" character count. +// We additionally account for ANSI *forward* movement ESC sequences +// in the form of ESC[C where is the "go forward" character count. // -// See also https://github.com/chalk/ansi-regex/blob/master/index.js +// See also https://github.com/chalk/ansi-regex/blob/master/index.js // function renderStringLength(s) { let m; @@ -281,22 +281,22 @@ function renderStringLength(s) { let len = 0; const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the rege; reset + re.lastIndex = 0; // we recycle the rege; reset // - // Loop counting only literal (non-control) sequences - // paying special attention to ESC[C which means forward + // Loop counting only literal (non-control) sequences + // paying special attention to ESC[C which means forward // do { - pos = re.lastIndex; - m = re.exec(s); + pos = re.lastIndex; + m = re.exec(s); if(m) { if(m.index > pos) { len += s.slice(pos, m.index).length; } - if('C' === m[3]) { // ESC[C is foward/right + if('C' === m[3]) { // ESC[C is foward/right len += parseInt(m[2], 10) || 0; } } @@ -309,19 +309,19 @@ function renderStringLength(s) { return len; } -const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) +const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) function formatByteSizeAbbr(byteSize) { if(0 === byteSize) { - return BYTE_SIZE_ABBRS[0]; // B + return BYTE_SIZE_ABBRS[0]; // B } return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; } function formatByteSize(byteSize, withAbbr = false, decimals = 2) { - const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024)); - let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); + const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024)); + let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); if(withAbbr) { result += ` ${BYTE_SIZE_ABBRS[i]}`; } @@ -339,8 +339,8 @@ function formatCountAbbr(count) { } function formatCount(count, withAbbr = false, decimals = 2) { - const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); - let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); + const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); + let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); if(withAbbr) { result += `${COUNT_ABBRS[i]}`; } @@ -348,13 +348,13 @@ function formatCount(count, withAbbr = false, decimals = 2) { } -// :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]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex -const ANSI_OPCODES_ALLOWED_CLEAN = [ - //'A', 'B', // up, down - //'C', 'D', // right, left - 'm', // color +// :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]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex +const ANSI_OPCODES_ALLOWED_CLEAN = [ + //'A', 'B', // up, down + //'C', 'D', // right, left + 'm', // color ]; function cleanControlCodes(input, options) { @@ -365,12 +365,12 @@ function cleanControlCodes(input, options) { options = options || {}; // - // Loop through |input| adding only allowed ESC - // sequences and literals to |cleaned| + // Loop through |input| adding only allowed ESC + // sequences and literals to |cleaned| // do { - pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; - m = REGEXP_ANSI_CONTROL_CODES.exec(input); + pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; + m = REGEXP_ANSI_CONTROL_CODES.exec(input); if(m) { if(m.index > pos) { @@ -388,7 +388,7 @@ function cleanControlCodes(input, options) { } while(0 !== REGEXP_ANSI_CONTROL_CODES.lastIndex); - // remainder + // remainder if(pos < input.length) { cleaned += input.slice(pos); } @@ -401,20 +401,20 @@ function isAnsiLine(line) { } // -// Returns true if the line is considered "formatted". A line is -// 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 +// Returns true if the line is considered "formatted". A line is +// 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 + return true; // ANSI or Pipe Codes } - if(line.match(/[\t\x00-\x1f\x80-\xff]/)) { // eslint-disable-line no-control-regex + if(line.match(/[\t\x00-\x1f\x80-\xff]/)) { // eslint-disable-line no-control-regex return true; } @@ -425,35 +425,35 @@ function isFormattedLine(line) { return false; } -// :TODO: rename to containsAnsi() +// :TODO: rename to containsAnsi() function isAnsi(input) { if(!input || 0 === input.length) { return false; } // - // * ANSI found - limited, just colors - // * Full ANSI art - // * + // * 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.) + // 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'); - } - }); - */ + 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 + // :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 + return m.length >= 4; // :TODO: do this reasonably, e.g. a percent or soemthing } function splitTextAtTerms(s) { diff --git a/core/system_events.js b/core/system_events.js index e471c712..95316c95 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -2,24 +2,24 @@ 'use strict'; module.exports = { - ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } - ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } - TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } + ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } + ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } + TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } - ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // { themeId } - ConfigChanged : 'codes.l33t.enigma.system.config_changed', - MenusChanged : 'codes.l33t.enigma.system.menus_changed', - PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', + ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // { themeId } + ConfigChanged : 'codes.l33t.enigma.system.config_changed', + MenusChanged : 'codes.l33t.enigma.system.menus_changed', + PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', - // User - includes { user, ...} - NewUser : 'codes.l33t.enigma.system.new_user', - UserLogin : 'codes.l33t.enigma.system.user_login', - UserLogoff : 'codes.l33t.enigma.system.user_logoff', - UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } - UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } + // User - includes { user, ...} + NewUser : 'codes.l33t.enigma.system.new_user', + UserLogin : 'codes.l33t.enigma.system.user_login', + UserLogoff : 'codes.l33t.enigma.system.user_logoff', + UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } + UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } - // NYI below here: - UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', - UserSendMail : 'codes.l33t.enigma.system.user_send_mail', - UserSendRunDoor : 'codes.l33t.enigma.system.user_run_door', + // NYI below here: + UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', + UserSendMail : 'codes.l33t.enigma.system.user_send_mail', + UserSendRunDoor : 'codes.l33t.enigma.system.user_run_door', }; diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 55da7430..9c9cd6d3 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -1,61 +1,61 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const removeClient = require('./client_connections.js').removeClient; -const ansiNormal = require('./ansi_term.js').normal; -const userLogin = require('./user_login.js').userLogin; -const messageArea = require('./message_area.js'); +// ENiGMA½ +const removeClient = require('./client_connections.js').removeClient; +const ansiNormal = require('./ansi_term.js').normal; +const userLogin = require('./user_login.js').userLogin; +const messageArea = require('./message_area.js'); -// deps -const _ = require('lodash'); -const iconv = require('iconv-lite'); +// deps +const _ = require('lodash'); +const iconv = require('iconv-lite'); -exports.login = login; -exports.logoff = logoff; -exports.prevMenu = prevMenu; -exports.nextMenu = nextMenu; -exports.prevConf = prevConf; -exports.nextConf = nextConf; -exports.prevArea = prevArea; -exports.nextArea = nextArea; -exports.sendForgotPasswordEmail = sendForgotPasswordEmail; +exports.login = login; +exports.logoff = logoff; +exports.prevMenu = prevMenu; +exports.nextMenu = nextMenu; +exports.prevConf = prevConf; +exports.nextConf = nextConf; +exports.prevArea = prevArea; +exports.nextArea = nextArea; +exports.sendForgotPasswordEmail = sendForgotPasswordEmail; function login(callingMenu, formData, extraArgs, cb) { userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { if(err) { - // login failure + // login failure if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); } else { - // Other error + // Other error return callingMenu.prevMenu(cb); } } - // success! + // success! return callingMenu.nextMenu(cb); }); } function logoff(callingMenu, formData, extraArgs, cb) { // - // Simple logoff. Note that recording of @ logoff properties/stats - // occurs elsewhere! + // Simple logoff. Note that recording of @ logoff properties/stats + // occurs elsewhere! // const client = callingMenu.client; setTimeout( () => { // - // For giggles... + // For giggles... // client.term.write( - ansiNormal() + '\n' + - iconv.decode(require('crypto').randomBytes(Math.floor(Math.random() * 65) + 20), client.term.outputEncoding) + - 'NO CARRIER', null, () => { + ansiNormal() + '\n' + + iconv.decode(require('crypto').randomBytes(Math.floor(Math.random() * 65) + 20), client.term.outputEncoding) + + 'NO CARRIER', null, () => { - // after data is written, disconnect & remove the client + // after data is written, disconnect & remove the client removeClient(client); return cb(null); } @@ -65,7 +65,7 @@ function logoff(callingMenu, formData, extraArgs, cb) { function prevMenu(callingMenu, formData, extraArgs, cb) { - // :TODO: this is a pretty big hack -- need the whole key map concep there like other places + // :TODO: this is a pretty big hack -- need the whole key map concep there like other places if(formData.key && 'return' === formData.key.name) { callingMenu.submitFormData = formData; } @@ -87,7 +87,7 @@ function nextMenu(callingMenu, formData, extraArgs, cb) { }); } -// :TODO: prev/nextConf, prev/nextArea should use a NYI MenuModule.redraw() or such -- avoid pop/goto() hack! +// :TODO: prev/nextConf, prev/nextArea should use a NYI MenuModule.redraw() or such -- avoid pop/goto() hack! function reloadMenu(menu, cb) { const prevMenu = menu.client.menuStack.pop(); prevMenu.instance.leave(); @@ -95,12 +95,12 @@ function reloadMenu(menu, cb) { } function prevConf(callingMenu, formData, extraArgs, cb) { - const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); + const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag) || confs.length; messageArea.changeMessageConference(callingMenu.client, confs[currIndex - 1].confTag, err => { if(err) { - return cb(err); // logged within changeMessageConference() + return cb(err); // logged within changeMessageConference() } return reloadMenu(callingMenu, cb); @@ -108,8 +108,8 @@ function prevConf(callingMenu, formData, extraArgs, cb) { } function nextConf(callingMenu, formData, extraArgs, cb) { - const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); - let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag); + const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); + let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag); if(currIndex === confs.length - 1) { currIndex = -1; @@ -117,7 +117,7 @@ function nextConf(callingMenu, formData, extraArgs, cb) { messageArea.changeMessageConference(callingMenu.client, confs[currIndex + 1].confTag, err => { if(err) { - return cb(err); // logged within changeMessageConference() + return cb(err); // logged within changeMessageConference() } return reloadMenu(callingMenu, cb); @@ -125,12 +125,12 @@ function nextConf(callingMenu, formData, extraArgs, cb) { } function prevArea(callingMenu, formData, extraArgs, cb) { - const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); + const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag) || areas.length; messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => { if(err) { - return cb(err); // logged within changeMessageArea() + return cb(err); // logged within changeMessageArea() } return reloadMenu(callingMenu, cb); @@ -138,8 +138,8 @@ function prevArea(callingMenu, formData, extraArgs, cb) { } function nextArea(callingMenu, formData, extraArgs, cb) { - const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); - let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag); + const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); + let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag); if(currIndex === areas.length - 1) { currIndex = -1; @@ -147,7 +147,7 @@ function nextArea(callingMenu, formData, extraArgs, cb) { messageArea.changeMessageArea(callingMenu.client, areas[currIndex + 1].areaTag, err => { if(err) { - return cb(err); // logged within changeMessageArea() + return cb(err); // logged within changeMessageArea() } return reloadMenu(callingMenu, cb); diff --git a/core/system_view_validate.js b/core/system_view_validate.js index 397f08c0..5eb0fc4a 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -1,25 +1,25 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const User = require('./user.js'); -const Config = require('./config.js').get; -const Log = require('./logger.js').log; -const { getAddressedToInfo } = require('./mail_util.js'); -const Message = require('./message.js'); +// ENiGMA½ +const User = require('./user.js'); +const Config = require('./config.js').get; +const Log = require('./logger.js').log; +const { getAddressedToInfo } = require('./mail_util.js'); +const Message = require('./message.js'); -// deps -const fs = require('graceful-fs'); +// deps +const fs = require('graceful-fs'); -exports.validateNonEmpty = validateNonEmpty; -exports.validateMessageSubject = validateMessageSubject; -exports.validateUserNameAvail = validateUserNameAvail; -exports.validateUserNameExists = validateUserNameExists; -exports.validateUserNameOrRealNameExists = validateUserNameOrRealNameExists; -exports.validateGeneralMailAddressedTo = validateGeneralMailAddressedTo; -exports.validateEmailAvail = validateEmailAvail; -exports.validateBirthdate = validateBirthdate; -exports.validatePasswordSpec = validatePasswordSpec; +exports.validateNonEmpty = validateNonEmpty; +exports.validateMessageSubject = validateMessageSubject; +exports.validateUserNameAvail = validateUserNameAvail; +exports.validateUserNameExists = validateUserNameExists; +exports.validateUserNameOrRealNameExists = validateUserNameOrRealNameExists; +exports.validateGeneralMailAddressedTo = validateGeneralMailAddressedTo; +exports.validateEmailAvail = validateEmailAvail; +exports.validateBirthdate = validateBirthdate; +exports.validatePasswordSpec = validatePasswordSpec; function validateNonEmpty(data, cb) { return cb(data && data.length > 0 ? null : new Error('Field cannot be empty')); @@ -34,11 +34,11 @@ function validateUserNameAvail(data, cb) { if(!data || data.length < config.users.usernameMin) { cb(new Error('Username too short')); } else if(data.length > config.users.usernameMax) { - // generally should be unreached due to view restraints + // generally should be unreached due to view restraints return cb(new Error('Username too long')); } else { - const usernameRegExp = new RegExp(config.users.usernamePattern); - const invalidNames = config.users.newUserNames + config.users.badUserNames; + const usernameRegExp = new RegExp(config.users.usernamePattern); + const invalidNames = config.users.newUserNames + config.users.badUserNames; if(!usernameRegExp.test(data)) { return cb(new Error('Username contains invalid characters')); @@ -47,9 +47,9 @@ function validateUserNameAvail(data, cb) { } else if(/^[0-9]+$/.test(data)) { return cb(new Error('Username cannot be a number')); } else { - // a new user name cannot be an existing user name or an existing real name + // a new user name cannot be an existing user name or an existing real name User.getUserIdAndNameByLookup(data, function userIdAndName(err) { - if(!err) { // err is null if we succeeded -- meaning this user exists already + if(!err) { // err is null if we succeeded -- meaning this user exists already return cb(new Error('Username unavailable')); } @@ -83,11 +83,11 @@ function validateUserNameOrRealNameExists(data, cb) { function validateGeneralMailAddressedTo(data, cb) { // - // Allow any supported addressing: - // - Local username or real name - // - Supported remote flavors such as FTN, email, ... + // Allow any supported addressing: + // - Local username or real name + // - Supported remote flavors such as FTN, email, ... // - // :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules. + // :TODO: remove hard-coded FTN check here. We need a decent way to register global supported flavors with modules. const addressedToInfo = getAddressedToInfo(data); if(Message.AddressFlavor.FTN === addressedToInfo.flavor) { @@ -99,17 +99,17 @@ function validateGeneralMailAddressedTo(data, cb) { function validateEmailAvail(data, cb) { // - // This particular method allows empty data - e.g. no email entered + // This particular method allows empty data - e.g. no email entered // if(!data || 0 === data.length) { return cb(null); } // - // Otherwise, it must be a valid email. We'll be pretty lose here, like - // the HTML5 spec. + // Otherwise, it must be a valid email. We'll be pretty lose here, like + // the HTML5 spec. // - // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation + // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation // const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; if(!emailRegExp.test(data)) { @@ -129,7 +129,7 @@ function validateEmailAvail(data, cb) { function validateBirthdate(data, cb) { - // :TODO: check for dates in the future, or > reasonable values + // :TODO: check for dates in the future, or > reasonable values return cb(isNaN(Date.parse(data)) ? new Error('Invalid birthdate') : null); } @@ -139,7 +139,7 @@ function validatePasswordSpec(data, cb) { return cb(new Error('Password too short')); } - // check badpass, if avail + // check badpass, if avail fs.readFile(config.users.badPassFile, 'utf8', (err, passwords) => { if(err) { Log.warn( { error : err.message }, 'Cannot read bad pass file'); diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 5276ebb0..a3a4d672 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -1,36 +1,36 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const resetScreen = require('./ansi_term.js').resetScreen; -const setSyncTermFontWithAlias = require('./ansi_term.js').setSyncTermFontWithAlias; +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const resetScreen = require('./ansi_term.js').resetScreen; +const setSyncTermFontWithAlias = require('./ansi_term.js').setSyncTermFontWithAlias; -// deps -const async = require('async'); -const _ = require('lodash'); -const net = require('net'); -const EventEmitter = require('events'); -const buffers = require('buffers'); +// deps +const async = require('async'); +const _ = require('lodash'); +const net = require('net'); +const EventEmitter = require('events'); +const buffers = require('buffers'); /* - Expected configuration block: + Expected configuration block: - { - module: telnet_bridge - ... - config: { - host: somehost.net - port: 23 - } - } + { + module: telnet_bridge + ... + config: { + host: somehost.net + port: 23 + } + } */ -// :TODO: ENH: Support nodeMax and tooManyArt +// :TODO: ENH: Support nodeMax and tooManyArt exports.moduleInfo = { - name : 'Telnet Bridge', - desc : 'Connect to other Telnet Systems', - author : 'Andrew Pamment', + name : 'Telnet Bridge', + desc : 'Connect to other Telnet Systems', + author : 'Andrew Pamment', }; const IAC_DO_TERM_TYPE = Buffer.from( [ 255, 253, 24 ] ); @@ -39,7 +39,7 @@ class TelnetClientConnection extends EventEmitter { constructor(client) { super(); - this.client = client; + this.client = client; } @@ -47,7 +47,7 @@ class TelnetClientConnection extends EventEmitter { if(!this.pipeRestored) { this.pipeRestored = true; - // client may have bailed + // client may have bailed if(null !== _.get(this, 'client.term.output', null)) { if(this.bridgeConnection) { this.client.term.output.unpipe(this.bridgeConnection); @@ -69,9 +69,9 @@ class TelnetClientConnection extends EventEmitter { this.client.term.rawWrite(data); // - // Wait for a terminal type request, and send it eactly once. - // This is enough (in additional to other negotiations handled in telnet.js) - // to get us in on most systems + // Wait for a terminal type request, and send it eactly once. + // This is enough (in additional to other negotiations handled in telnet.js) + // to get us in on most systems // if(!this.termSent && data.indexOf(IAC_DO_TERM_TYPE) > -1) { this.termSent = true; @@ -107,23 +107,23 @@ class TelnetClientConnection extends EventEmitter { getTermTypeNegotiationBuffer() { // - // Create a TERMINAL-TYPE sub negotiation buffer using the - // actual/current terminal type. + // Create a TERMINAL-TYPE sub negotiation buffer using the + // actual/current terminal type. // let bufs = buffers(); bufs.push(Buffer.from( [ - 255, // IAC - 250, // SB - 24, // TERMINAL-TYPE - 0, // IS + 255, // IAC + 250, // SB + 24, // TERMINAL-TYPE + 0, // IS ] )); bufs.push( - Buffer.from(this.client.term.termType), // e.g. "ansi" - Buffer.from( [ 255, 240 ] ) // IAC, SE + Buffer.from(this.client.term.termType), // e.g. "ansi" + Buffer.from( [ 255, 240 ] ) // IAC, SE ); return bufs.toBuffer(); @@ -135,8 +135,8 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.config.port = this.config.port || 23; + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + this.config.port = this.config.port || 23; } initSequence() { @@ -147,7 +147,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { [ function validateConfig(callback) { if(_.isString(self.config.host) && - _.isNumber(self.config.port)) + _.isNumber(self.config.port)) { callback(null); } else { @@ -156,8 +156,8 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { }, function createTelnetBridge(callback) { const connectOpts = { - port : self.config.port, - host : self.config.host, + port : self.config.port, + host : self.config.host, }; self.client.term.write(resetScreen()); diff --git a/core/text_view.js b/core/text_view.js index 69908e50..4611821a 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -1,26 +1,26 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const ansi = require('./ansi_term.js'); -const padStr = require('./string_util.js').pad; -const stylizeString = require('./string_util.js').stylizeString; -const renderSubstr = require('./string_util.js').renderSubstr; -const renderStringLength = require('./string_util.js').renderStringLength; -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; -const stripAllLineFeeds = require('./string_util.js').stripAllLineFeeds; +// ENiGMA½ +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const ansi = require('./ansi_term.js'); +const padStr = require('./string_util.js').pad; +const stylizeString = require('./string_util.js').stylizeString; +const renderSubstr = require('./string_util.js').renderSubstr; +const renderStringLength = require('./string_util.js').renderStringLength; +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const stripAllLineFeeds = require('./string_util.js').stripAllLineFeeds; -// deps -const util = require('util'); -const _ = require('lodash'); +// deps +const util = require('util'); +const _ = require('lodash'); -exports.TextView = TextView; +exports.TextView = TextView; function TextView(options) { if(options.dimens) { - options.dimens.height = 1; // force height of 1 for TextView's & sub classes + options.dimens.height = 1; // force height of 1 for TextView's & sub classes } View.call(this, options); @@ -31,10 +31,10 @@ function TextView(options) { this.maxLength = this.client.term.termWidth - this.position.col; } - this.fillChar = renderSubstr(miscUtil.valueWithDefault(options.fillChar, ' '), 0, 1); - this.justify = options.justify || 'left'; - this.resizable = miscUtil.valueWithDefault(options.resizable, true); - this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); + this.fillChar = renderSubstr(miscUtil.valueWithDefault(options.fillChar, ' '), 0, 1); + this.justify = options.justify || 'left'; + this.resizable = miscUtil.valueWithDefault(options.resizable, true); + this.horizScroll = miscUtil.valueWithDefault(options.horizScroll, true); if(_.isString(options.textOverflow)) { this.textOverflow = options.textOverflow; @@ -45,57 +45,57 @@ function TextView(options) { } /* - this.drawText = function(s) { + this.drawText = function(s) { - // - // |<- this.maxLength - // ABCDEFGHIJK - // |ABCDEFG| ^_ this.text.length - // ^-- this.dimens.width - // - let textToDraw = _.isString(this.textMaskChar) ? - new Array(s.length + 1).join(this.textMaskChar) : - stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); + // + // |<- this.maxLength + // ABCDEFGHIJK + // |ABCDEFG| ^_ this.text.length + // ^-- this.dimens.width + // + let textToDraw = _.isString(this.textMaskChar) ? + new Array(s.length + 1).join(this.textMaskChar) : + stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - if(textToDraw.length > this.dimens.width) { - if(this.hasFocus) { - if(this.horizScroll) { - textToDraw = textToDraw.substr(textToDraw.length - this.dimens.width, textToDraw.length); - } - } else { - if(textToDraw.length > this.dimens.width) { - if(this.textOverflow && - this.dimens.width > this.textOverflow.length && - textToDraw.length - this.textOverflow.length >= this.textOverflow.length) - { - textToDraw = textToDraw.substr(0, this.dimens.width - this.textOverflow.length) + this.textOverflow; - } else { - textToDraw = textToDraw.substr(0, this.dimens.width); - } - } - } - } + if(textToDraw.length > this.dimens.width) { + if(this.hasFocus) { + if(this.horizScroll) { + textToDraw = textToDraw.substr(textToDraw.length - this.dimens.width, textToDraw.length); + } + } else { + if(textToDraw.length > this.dimens.width) { + if(this.textOverflow && + this.dimens.width > this.textOverflow.length && + textToDraw.length - this.textOverflow.length >= this.textOverflow.length) + { + textToDraw = textToDraw.substr(0, this.dimens.width - this.textOverflow.length) + this.textOverflow; + } else { + textToDraw = textToDraw.substr(0, this.dimens.width); + } + } + } + } - this.client.term.write(padStr( - textToDraw, - this.dimens.width + 1, - this.fillChar, - this.justify, - this.hasFocus ? this.getFocusSGR() : this.getSGR(), - this.getStyleSGR(1) || this.getSGR() - ), false); - }; + this.client.term.write(padStr( + textToDraw, + this.dimens.width + 1, + this.fillChar, + this.justify, + this.hasFocus ? this.getFocusSGR() : this.getSGR(), + this.getStyleSGR(1) || this.getSGR() + ), false); + }; */ this.drawText = function(s) { // // |<- this.maxLength - // ABCDEFGHIJK - // |ABCDEFG| ^_ this.text.length - // ^-- this.dimens.width + // ABCDEFGHIJK + // |ABCDEFG| ^_ this.text.length + // ^-- this.dimens.width // - let renderLength = renderStringLength(s); // initial; may be adjusted below: + let renderLength = renderStringLength(s); // initial; may be adjusted below: let textToDraw = _.isString(this.textMaskChar) ? new Array(renderLength + 1).join(this.textMaskChar) : @@ -110,8 +110,8 @@ function TextView(options) { } } else { if(this.textOverflow && - this.dimens.width > this.textOverflow.length && - renderLength - this.textOverflow.length >= this.textOverflow.length) + this.dimens.width > this.textOverflow.length && + renderLength - this.textOverflow.length >= this.textOverflow.length) { textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; } else { @@ -130,9 +130,9 @@ function TextView(options) { this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), this.getStyleSGR(1) || this.getSGR(), - true // use render len + true // use render len ), - false // no converting CRLF needed + false // no converting CRLF needed ); }; @@ -142,16 +142,16 @@ function TextView(options) { return this.position.col + offset; }; - this.setText(options.text || '', false); // false=do not redraw now + this.setText(options.text || '', false); // false=do not redraw now } util.inherits(TextView, View); TextView.prototype.redraw = function() { // - // A lot of views will get an initial redraw() with empty text (''). We can short - // circuit this by NOT doing any of the work if this is the initial drawText - // and there is no actual text (e.g. save SGR's and processing) + // A lot of views will get an initial redraw() with empty text (''). We can short + // circuit this by NOT doing any of the work if this is the initial drawText + // and there is no actual text (e.g. save SGR's and processing) // if(!this.hasDrawnOnce) { if(_.isUndefined(this.text)) { @@ -187,7 +187,7 @@ TextView.prototype.setText = function(text, redraw) { text = text.toString(); } - text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. + text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. var widthDelta = 0; if(this.text && this.text !== text) { @@ -201,7 +201,7 @@ TextView.prototype.setText = function(text, redraw) { //this.text = this.text.substr(0, this.maxLength); } - // :TODO: it would be nice to be able to stylize strings with MCI and {special} MCI syntax, e.g. "|BN {UN!toUpper}" + // :TODO: it would be nice to be able to stylize strings with MCI and {special} MCI syntax, e.g. "|BN {UN!toUpper}" this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); if(this.autoScale.width) { @@ -215,32 +215,32 @@ TextView.prototype.setText = function(text, redraw) { /* TextView.prototype.setText = function(text) { - if(!_.isString(text)) { - text = text.toString(); - } + if(!_.isString(text)) { + text = text.toString(); + } - var widthDelta = 0; - if(this.text && this.text !== text) { - widthDelta = Math.abs(this.text.length - text.length); - } + var widthDelta = 0; + if(this.text && this.text !== text) { + widthDelta = Math.abs(this.text.length - text.length); + } - this.text = text; + this.text = text; - if(this.maxLength > 0) { - this.text = this.text.substr(0, this.maxLength); - } + if(this.maxLength > 0) { + this.text = this.text.substr(0, this.maxLength); + } - this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); + this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); - //if(this.resizable) { - // this.dimens.width = this.text.length + widthDelta; - //} + //if(this.resizable) { + // this.dimens.width = this.text.length + widthDelta; + //} - if(this.autoScale.width) { - this.dimens.width = this.text.length + widthDelta; - } + if(this.autoScale.width) { + this.dimens.width = this.text.length + widthDelta; + } - this.redraw(); + this.redraw(); }; */ @@ -251,9 +251,9 @@ TextView.prototype.clearText = function() { TextView.prototype.setPropertyValue = function(propName, value) { switch(propName) { case 'textMaskChar' : this.textMaskChar = value.substr(0, 1); break; - case 'textOverflow' : this.textOverflow = value; break; - case 'maxLength' : this.maxLength = parseInt(value, 10); break; - case 'password' : + case 'textOverflow' : this.textOverflow = value; break; + case 'maxLength' : this.maxLength = parseInt(value, 10); break; + case 'password' : if(true === value) { this.textMaskChar = this.client.currentTheme.helpers.getPasswordChar(); } diff --git a/core/theme.js b/core/theme.js index aca8ca2c..6d2bcff6 100644 --- a/core/theme.js +++ b/core/theme.js @@ -1,37 +1,37 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').get; -const art = require('./art.js'); -const ansi = require('./ansi_term.js'); -const Log = require('./logger.js').log; -const ConfigCache = require('./config_cache.js'); -const getFullConfig = require('./config_util.js').getFullConfig; -const asset = require('./asset.js'); -const ViewController = require('./view_controller.js').ViewController; -const Errors = require('./enig_error.js').Errors; -const ErrorReasons = require('./enig_error.js').ErrorReasons; -const Events = require('./events.js'); +const Config = require('./config.js').get; +const art = require('./art.js'); +const ansi = require('./ansi_term.js'); +const Log = require('./logger.js').log; +const ConfigCache = require('./config_cache.js'); +const getFullConfig = require('./config_util.js').getFullConfig; +const asset = require('./asset.js'); +const ViewController = require('./view_controller.js').ViewController; +const Errors = require('./enig_error.js').Errors; +const ErrorReasons = require('./enig_error.js').ErrorReasons; +const Events = require('./events.js'); -const fs = require('graceful-fs'); -const paths = require('path'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); +const fs = require('graceful-fs'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); -exports.getThemeArt = getThemeArt; -exports.getAvailableThemes = getAvailableThemes; -exports.getRandomTheme = getRandomTheme; +exports.getThemeArt = getThemeArt; +exports.getAvailableThemes = getAvailableThemes; +exports.getRandomTheme = getRandomTheme; exports.setClientTheme = setClientTheme; -exports.initAvailableThemes = initAvailableThemes; -exports.displayThemeArt = displayThemeArt; -exports.displayThemedPause = displayThemedPause; -exports.displayThemedPrompt = displayThemedPrompt; -exports.displayThemedAsset = displayThemedAsset; +exports.initAvailableThemes = initAvailableThemes; +exports.displayThemeArt = displayThemeArt; +exports.displayThemedPause = displayThemedPause; +exports.displayThemedPrompt = displayThemedPrompt; +exports.displayThemedAsset = displayThemedAsset; function refreshThemeHelpers(theme) { // - // Create some handy helpers + // Create some handy helpers // theme.helpers = { getPasswordChar : function() { @@ -75,9 +75,9 @@ function loadTheme(themeId, cb) { }; const getOpts = { - filePath : path, - forceReCache : true, - callback : changed, + filePath : path, + forceReCache : true, + callback : changed, }; ConfigCache.getConfigWithOptions(getOpts, (err, theme) => { @@ -86,8 +86,8 @@ function loadTheme(themeId, cb) { } if(!_.isObject(theme.info) || - !_.isString(theme.info.name) || - !_.isString(theme.info.author)) + !_.isString(theme.info.name) || + !_.isString(theme.info.author)) { return cb(Errors.Invalid('Invalid or missing "info" section')); } @@ -113,7 +113,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { assert(_.isObject(theme)); // :TODO: merge in defaults (customization.defaults{} ) - // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") + // :TODO: apply generic stuff, e.g. "VM" (vs "VM1") // // Create a *clone* of menuConfig (menu.hjson) then bring in @@ -213,7 +213,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { if(_.has(theme, [ 'customization', sectionName, menuName ])) { const menuTheme = theme.customization[sectionName][menuName]; - // config block is direct assign/overwrite + // config block is direct assign/overwrite // :TODO: should probably be _.merge() if(menuTheme.config) { mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); @@ -250,7 +250,7 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // * There is no 'prompt' specified // if('menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && - (createdFormSection || !_.isObject(mergedThemeMenu.form))) + (createdFormSection || !_.isObject(mergedThemeMenu.form))) { mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); } @@ -337,21 +337,21 @@ function initAvailableThemes(cb) { menuConfig, promptConfig, files.filter( f => { - // sync normally not allowed -- initAvailableThemes() is a startup-only method, however + // sync normally not allowed -- initAvailableThemes() is a startup-only method, however return fs.statSync(paths.join(config.paths.themes, f)).isDirectory(); }) ); }); }, function populateAvailable(menuConfig, promptConfig, themeDirectories, callback) { - async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID + async.each(themeDirectories, (themeId, nextThemeDir) => { // theme dir = theme ID loadTheme(themeId, (err, theme) => { if(err) { if(ErrorReasons.NotEnabled !== err.reasonCode) { Log.warn( { themeId : themeId, err : err.message }, 'Failed loading theme'); } - return nextThemeDir(null); // try next + return nextThemeDir(null); // try next } Object.assign(theme.info, { themeId } ); @@ -413,15 +413,15 @@ function setClientTheme(client, themeId) { function getThemeArt(options, cb) { // - // options - required: - // name + // options - required: + // name // - // options - optional - // client - needed for user's theme/etc. - // themeId - // asAnsi - // readSauce - // random + // options - optional + // client - needed for user's theme/etc. + // themeId + // asAnsi + // readSauce + // random // const config = Config(); if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { @@ -430,30 +430,30 @@ function getThemeArt(options, cb) { options.themeId = config.defaults.theme; } - // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... - // :TODO: Some of these options should only be set if not provided! - options.asAnsi = true; // always convert to ANSI - options.readSauce = true; // read SAUCE, if avail - options.random = _.get(options, 'random', true); // FILENAME.EXT support + // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... + // :TODO: Some of these options should only be set if not provided! + options.asAnsi = true; // always convert to ANSI + options.readSauce = true; // read SAUCE, if avail + options.random = _.get(options, 'random', true); // FILENAME.EXT support // - // We look for themed art in the following order: - // 1) Direct/relative path - // 2) Via theme supplied by |themeId| - // 3) Via default theme - // 4) General art directory + // We look for themed art in the following order: + // 1) Direct/relative path + // 2) Via theme supplied by |themeId| + // 3) Via default theme + // 4) General art directory // async.waterfall( [ function fromPath(callback) { // - // We allow relative (to enigma-bbs) or full paths + // We allow relative (to enigma-bbs) or full paths // if('/' === options.name.charAt(0)) { - // just take the path as-is + // just take the path as-is options.basePath = paths.dirname(options.name); } else if(options.name.indexOf('/') > -1) { - // make relative to base BBS dir + // make relative to base BBS dir options.basePath = paths.join(__dirname, '../', paths.dirname(options.name)); } else { return callback(null, null); @@ -513,11 +513,11 @@ function displayThemeArt(options, cb) { if(err) { return cb(err); } - // :TODO: just use simple merge of options -> displayOptions + // :TODO: just use simple merge of options -> displayOptions const displayOpts = { - sauce : artInfo.sauce, - font : options.font, - trailingLF : options.trailingLF, + sauce : artInfo.sauce, + font : options.font, + trailingLF : options.trailingLF, }; art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { @@ -543,14 +543,14 @@ function displayThemedPrompt(name, client, options, cb) { } // - // If we did *not* clear the screen, don't let the font change - // doing so messes things up -- most terminals that support font - // changing can only display a single font at at time. + // If we did *not* clear the screen, don't let the font change + // 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: + // :TODO: We can use term detection to do nifty things like avoid this kind of kludge: const dispOptions = Object.assign( {}, promptConfig.options ); if(!options.clearScreen) { - dispOptions.font = 'not_really_a_font!'; // kludge :) + dispOptions.font = 'not_really_a_font!'; // kludge :) } displayThemedAsset( @@ -568,7 +568,7 @@ function displayThemedPrompt(name, client, options, cb) { }, function discoverCursorPosition(promptConfig, artInfo, callback) { if(!options.clearPrompt) { - // no need to query cursor - we're not gonna use it + // no need to query cursor - we're not gonna use it return callback(null, promptConfig, artInfo); } @@ -583,9 +583,9 @@ function displayThemedPrompt(name, client, options, cb) { const tempViewController = useTempViewController ? new ViewController( { client : client } ) : options.viewController; const loadOpts = { - promptName : name, - mciMap : artInfo.mciMap, - config : promptConfig, + promptName : name, + mciMap : artInfo.mciMap, + config : promptConfig, }; tempViewController.loadFromPromptConfig(loadOpts, () => { @@ -606,7 +606,7 @@ function displayThemedPrompt(name, client, options, cb) { if(artInfo.startRow && artInfo.height) { client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); - // Note: Does not work properly in NetRunner < 2.0b17: + // Note: Does not work properly in NetRunner < 2.0b17: client.term.rawWrite(ansi.deleteLine(artInfo.height)); } else { client.term.rawWrite(ansi.eraseLine(1)); @@ -631,7 +631,7 @@ function displayThemedPrompt(name, client, options, cb) { } // -// Pause prompts are a special prompt by the name 'pause'. +// Pause prompts are a special prompt by the name 'pause'. // function displayThemedPause(client, options, cb) { @@ -651,7 +651,7 @@ function displayThemedPause(client, options, cb) { function displayThemedAsset(assetSpec, client, options, cb) { assert(_.isObject(client)); - // options are... optional + // options are... optional if(3 === arguments.length) { cb = options; options = {}; @@ -666,12 +666,12 @@ function displayThemedAsset(assetSpec, client, options, cb) { return cb(new Error('Asset not found: ' + assetSpec)); } - // :TODO: just use simple merge of options -> displayOptions + // :TODO: just use simple merge of options -> displayOptions var dispOpts = { - name : artAsset.asset, - client : client, - font : options.font, - trailingLF : options.trailingLF, + name : artAsset.asset, + client : client, + font : options.font, + trailingLF : options.trailingLF, }; switch(artAsset.type) { @@ -682,13 +682,13 @@ function displayThemedAsset(assetSpec, client, options, cb) { break; case 'method' : - // :TODO: fetch & render via method + // :TODO: fetch & render via method break; case 'inline ' : - // :TODO: think about this more in relation to themes, etc. How can this come - // from a theme (with override from menu.json) ??? - // look @ client.currentTheme.inlineArt[name] -> menu/prompt[name] + // :TODO: think about this more in relation to themes, etc. How can this come + // from a theme (with override from menu.json) ??? + // look @ client.currentTheme.inlineArt[name] -> menu/prompt[name] break; default : diff --git a/core/tic_file_info.js b/core/tic_file_info.js index 37aec134..5db24cc2 100644 --- a/core/tic_file_info.js +++ b/core/tic_file_info.js @@ -1,25 +1,25 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Address = require('./ftn_address.js'); -const Errors = require('./enig_error.js').Errors; -const EnigAssert = require('./enigma_assert.js'); +// ENiGMA½ +const Address = require('./ftn_address.js'); +const Errors = require('./enig_error.js').Errors; +const EnigAssert = require('./enigma_assert.js'); -// deps -const fs = require('graceful-fs'); -const CRC32 = require('./crc.js').CRC32; -const _ = require('lodash'); -const async = require('async'); -const paths = require('path'); -const crypto = require('crypto'); +// deps +const fs = require('graceful-fs'); +const CRC32 = require('./crc.js').CRC32; +const _ = require('lodash'); +const async = require('async'); +const paths = require('path'); +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 -// * FSC-0087.001 @ http://ftsc.org/docs/fsc-0087.001 +// * FTS-5006.001 @ http://www.filegate.net/ftsc/FTS-5006.001 +// * FSP-1039.001 @ http://ftsc.org/docs/old/fsp-1039.001 +// * FSC-0087.001 @ http://ftsc.org/docs/fsc-0087.001 // module.exports = class TicFileInfo { constructor() { @@ -29,8 +29,8 @@ module.exports = class TicFileInfo { static get requiredFields() { return [ 'Area', 'Origin', 'From', 'File', 'Crc', - // :TODO: validate this: - //'Path', 'Seenby' // these two are questionable; some systems don't send them? + // :TODO: validate this: + //'Path', 'Seenby' // these two are questionable; some systems don't send them? ]; } @@ -42,7 +42,7 @@ module.exports = class TicFileInfo { const value = this.get(key); if(value) { // - // We call toString() on values to ensure numbers, addresses, etc. are converted + // We call toString() on values to ensure numbers, addresses, etc. are converted // joinWith = joinWith || ''; if(Array.isArray(value)) { @@ -67,9 +67,9 @@ module.exports = class TicFileInfo { } validate(config, cb) { - // config.nodes - // config.defaultPassword (optional) - // config.localAreaTags + // config.nodes + // config.defaultPassword (optional) + // config.localAreaTags EnigAssert(config.nodes && config.localAreaTags); const self = this; @@ -84,7 +84,7 @@ module.exports = class TicFileInfo { const area = self.getAsString('Area').toUpperCase(); const localInfo = { - areaTag : config.localAreaTags.find( areaTag => areaTag.toUpperCase() === area ), + areaTag : config.localAreaTags.find( areaTag => areaTag.toUpperCase() === area ), }; if(!localInfo.areaTag) { @@ -96,17 +96,17 @@ module.exports = class TicFileInfo { return callback(Errors.Invalid(`Invalid "From" address: ${self.getAsString('From')}`)); } - // note that our config may have wildcards, such as "80:774/*" + // note that our config may have wildcards, such as "80:774/*" localInfo.node = Object.keys(config.nodes).find( nodeAddrWildcard => from.isPatternMatch(nodeAddrWildcard) ); if(!localInfo.node) { return callback(Errors.Invalid('TIC is not from a known node')); } - // if we require a password, "PW" must match + // if we require a password, "PW" must match const passActual = _.get(config.nodes, [ localInfo.node, 'tic', 'password' ] ) || config.defaultPassword; if(!passActual) { - return callback(null, localInfo); // no pw validation + return callback(null, localInfo); // no pw validation } const passTic = self.getAsString('Pw'); @@ -117,22 +117,22 @@ module.exports = class TicFileInfo { return callback(null, localInfo); }, function checksumAndSize(localInfo, callback) { - const crcTic = self.get('Crc'); - const stream = fs.createReadStream(self.filePath); - const crc = new CRC32(); - let sizeActual = 0; + const crcTic = self.get('Crc'); + const stream = fs.createReadStream(self.filePath); + const crc = new CRC32(); + let sizeActual = 0; - let sha256Tic = self.getAsString('Sha256'); + let sha256Tic = self.getAsString('Sha256'); let sha256; if(sha256Tic) { - sha256Tic = sha256Tic.toLowerCase(); - sha256 = crypto.createHash('sha256'); + sha256Tic = sha256Tic.toLowerCase(); + sha256 = crypto.createHash('sha256'); } stream.on('data', data => { sizeActual += data.length; - // sha256 if possible, else crc32 + // sha256 if possible, else crc32 if(sha256) { sha256.update(data); } else { @@ -141,7 +141,7 @@ module.exports = class TicFileInfo { }); stream.on('end', () => { - // again, use sha256 if possible + // again, use sha256 if possible if(sha256) { const sha256Actual = sha256.digest('hex'); if(sha256Tic != sha256Actual) { @@ -182,20 +182,20 @@ module.exports = class TicFileInfo { isToAddress(address, allowNonExplicit) { // - // FSP-1039.001: - // "This keyword specifies the FTN address of the system where to - // send the file to be distributed and the accompanying TIC file. - // Some File processors (Allfix) only insert a line with this - // keyword when the file and the associated TIC file are to be - // file routed through a third sysem instead of being processed - // by a file processor on that system. Others always insert it. - // Note that the To keyword may cause problems when the TIC file - // is proecessed by software that does not recognise it and - // passes the line "as is" to other systems. + // FSP-1039.001: + // "This keyword specifies the FTN address of the system where to + // send the file to be distributed and the accompanying TIC file. + // Some File processors (Allfix) only insert a line with this + // keyword when the file and the associated TIC file are to be + // file routed through a third sysem instead of being processed + // by a file processor on that system. Others always insert it. + // Note that the To keyword may cause problems when the TIC file + // is proecessed by software that does not recognise it and + // passes the line "as is" to other systems. // - // Example: To 292/854 + // Example: To 292/854 // - // This is an optional keyword." + // This is an optional keyword." // const to = this.get('To'); @@ -212,12 +212,12 @@ module.exports = class TicFileInfo { return cb(err); } - const ticFileInfo = new TicFileInfo(); - ticFileInfo.path = path; + const ticFileInfo = new TicFileInfo(); + ticFileInfo.path = path; // - // Lines in a TIC file should be separated by CRLF (DOS) - // may be separated by LF (UNIX) + // Lines in a TIC file should be separated by CRLF (DOS) + // may be separated by LF (UNIX) // const lines = ticData.split(/\r\n|\n/g); let keyEnd; @@ -226,7 +226,7 @@ module.exports = class TicFileInfo { let entry; lines.forEach(line => { - keyEnd = line.search(/\s/); + keyEnd = line.search(/\s/); if(keyEnd < 0) { keyEnd = line.length; @@ -240,12 +240,12 @@ module.exports = class TicFileInfo { value = line.substr(keyEnd + 1); - // don't trim Ldesc; may mess with FILE_ID.DIZ type descriptions + // don't trim Ldesc; may mess with FILE_ID.DIZ type descriptions if('ldesc' !== key) { value = value.trim(); } - // convert well known keys to a more reasonable format + // convert well known keys to a more reasonable format switch(key) { case 'origin' : case 'from' : diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index 28818189..ae163bd7 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -1,13 +1,13 @@ /* jslint node: true */ 'use strict'; -const MenuView = require('./menu_view.js').MenuView; -const strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); -const util = require('util'); -const assert = require('assert'); +const util = require('util'); +const assert = require('assert'); -exports.ToggleMenuView = ToggleMenuView; +exports.ToggleMenuView = ToggleMenuView; function ToggleMenuView (options) { options.cursor = options.cursor || 'hide'; @@ -17,10 +17,10 @@ function ToggleMenuView (options) { var self = this; /* - this.cachePositions = function() { - self.positionCacheExpired = false; - }; - */ + this.cachePositions = function() { + self.positionCacheExpired = false; + }; + */ this.updateSelection = function() { assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); @@ -47,8 +47,8 @@ ToggleMenuView.prototype.redraw = function() { //console.log(this.styleColor1) //var sepColor = this.getANSIColor(this.styleColor1 || this.getColor()); //console.log(sepColor.substr(1)) - //var sepColor = '\u001b[0m\u001b[1;30m'; // :TODO: FIX ME!!! - // :TODO: sepChar needs to be configurable!!! + //var sepColor = '\u001b[0m\u001b[1;30m'; // :TODO: FIX ME!!! + // :TODO: sepChar needs to be configurable!!! this.client.term.write(this.styleSGR1 + ' / '); //this.client.term.write(sepColor + ' / '); } @@ -59,7 +59,7 @@ ToggleMenuView.prototype.redraw = function() { }; ToggleMenuView.prototype.setFocusItemIndex = function(index) { - ToggleMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + ToggleMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex this.updateSelection(); }; @@ -113,9 +113,9 @@ ToggleMenuView.prototype.getData = function() { }; ToggleMenuView.prototype.setItems = function(items) { - items = items.slice(0, 2); // switch/toggle only works with two elements + items = items.slice(0, 2); // switch/toggle only works with two elements ToggleMenuView.super_.prototype.setItems.call(this, items); - this.dimens.width = items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) + this.dimens.width = items.join(' / ').length; // :TODO: allow configurable seperator... string & color, e.g. styleColor1 (same as fillChar color) }; diff --git a/core/upload.js b/core/upload.js index 2279e6b4..87bdcb4a 100644 --- a/core/upload.js +++ b/core/upload.js @@ -1,70 +1,70 @@ /* jslint node: true */ 'use strict'; -// enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const stringFormat = require('./string_format.js'); -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; -const getAreaDefaultStorageDirectory = require('./file_base_area.js').getAreaDefaultStorageDirectory; -const scanFile = require('./file_base_area.js').scanFile; -const getFileAreaByTag = require('./file_base_area.js').getFileAreaByTag; -const getDescFromFileName = require('./file_base_area.js').getDescFromFileName; -const ansiGoto = require('./ansi_term.js').goto; -const moveFileWithCollisionHandling = require('./file_util.js').moveFileWithCollisionHandling; -const pathWithTerminatingSeparator = require('./file_util.js').pathWithTerminatingSeparator; -const Log = require('./logger.js').log; -const Errors = require('./enig_error.js').Errors; -const FileEntry = require('./file_entry.js'); -const isAnsi = require('./string_util.js').isAnsi; -const Events = require('./events.js'); +// enigma-bbs +const MenuModule = require('./menu_module.js').MenuModule; +const stringFormat = require('./string_format.js'); +const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; +const getAreaDefaultStorageDirectory = require('./file_base_area.js').getAreaDefaultStorageDirectory; +const scanFile = require('./file_base_area.js').scanFile; +const getFileAreaByTag = require('./file_base_area.js').getFileAreaByTag; +const getDescFromFileName = require('./file_base_area.js').getDescFromFileName; +const ansiGoto = require('./ansi_term.js').goto; +const moveFileWithCollisionHandling = require('./file_util.js').moveFileWithCollisionHandling; +const pathWithTerminatingSeparator = require('./file_util.js').pathWithTerminatingSeparator; +const Log = require('./logger.js').log; +const Errors = require('./enig_error.js').Errors; +const FileEntry = require('./file_entry.js'); +const isAnsi = require('./string_util.js').isAnsi; +const Events = require('./events.js'); -// deps -const async = require('async'); -const _ = require('lodash'); -const temptmp = require('temptmp').createTrackedSession('upload'); -const paths = require('path'); -const sanatizeFilename = require('sanitize-filename'); +// deps +const async = require('async'); +const _ = require('lodash'); +const temptmp = require('temptmp').createTrackedSession('upload'); +const paths = require('path'); +const sanatizeFilename = require('sanitize-filename'); exports.moduleInfo = { - name : 'Upload', - desc : 'Module for classic file uploads', - author : 'NuSkooler', + name : 'Upload', + desc : 'Module for classic file uploads', + author : 'NuSkooler', }; const FormIds = { - options : 0, - processing : 1, - fileDetails : 2, - dupes : 3, + options : 0, + processing : 1, + fileDetails : 2, + dupes : 3, }; const MciViewIds = { options : { - area : 1, // area selection - uploadType : 2, // blind vs specify filename - fileName : 3, // for non-blind; not editable for blind - navMenu : 4, // next/cancel/etc. - errMsg : 5, // errors (e.g. filename cannot be blank) + area : 1, // area selection + uploadType : 2, // blind vs specify filename + fileName : 3, // for non-blind; not editable for blind + navMenu : 4, // next/cancel/etc. + errMsg : 5, // errors (e.g. filename cannot be blank) }, processing : { - calcHashIndicator : 1, - archiveListIndicator : 2, - descFileIndicator : 3, - logStep : 4, - customRangeStart : 10, // 10+ = customs + calcHashIndicator : 1, + archiveListIndicator : 2, + descFileIndicator : 3, + logStep : 4, + customRangeStart : 10, // 10+ = customs }, fileDetails : { - desc : 1, // defaults to 'desc' (e.g. from FILE_ID.DIZ) - tags : 2, // tag(s) for item - estYear : 3, - accept : 4, // accept fields & continue - customRangeStart : 10, // 10+ = customs + desc : 1, // defaults to 'desc' (e.g. from FILE_ID.DIZ) + tags : 2, // tag(s) for item + estYear : 3, + accept : 4, // accept fields & continue + customRangeStart : 10, // 10+ = customs }, dupes : { - dupeList : 1, + dupeList : 1, } }; @@ -85,14 +85,14 @@ exports.getModule = class UploadModule extends MenuModule { }, fileDetailsContinue : (formData, extraArgs, cb) => { - // see displayFileDetailsPageForUploadEntry() for this hackery: + // see displayFileDetailsPageForUploadEntry() for this hackery: cb(null); - return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any + return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any }, - // validation + // validation validateNonBlindFileName : (fileName, cb) => { - fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. + fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. if(0 === fileName.length) { return cb(new Error('Invalid filename')); } @@ -101,7 +101,7 @@ exports.getModule = class UploadModule extends MenuModule { return cb(new Error('Filename cannot be empty')); } - // At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused + // At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused if(/^[0-9].*$/.test(fileName)) { return cb(new Error('Invalid filename')); } @@ -124,21 +124,21 @@ exports.getModule = class UploadModule extends MenuModule { } getSaveState() { - // if no areas, we're falling back due to lack of access/areas avail to upload to + // if no areas, we're falling back due to lack of access/areas avail to upload to if(this.availAreas.length > 0) { return { - uploadType : this.uploadType, - tempRecvDirectory : this.tempRecvDirectory, - areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ], + uploadType : this.uploadType, + tempRecvDirectory : this.tempRecvDirectory, + areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ], }; } } restoreSavedState(savedState) { if(savedState.areaInfo) { - this.uploadType = savedState.uploadType; - this.areaInfo = savedState.areaInfo; - this.tempRecvDirectory = savedState.tempRecvDirectory; + this.uploadType = savedState.uploadType; + this.areaInfo = savedState.areaInfo; + this.tempRecvDirectory = savedState.tempRecvDirectory; } } @@ -184,24 +184,24 @@ exports.getModule = class UploadModule extends MenuModule { return cb(err); } - // need a terminator for various external protocols + // need a terminator for various external protocols this.tempRecvDirectory = pathWithTerminatingSeparator(tempRecvDirectory); const modOpts = { extraArgs : { - recvDirectory : this.tempRecvDirectory, // we'll move files from here to their area container once processed/confirmed - direction : 'recv', + recvDirectory : this.tempRecvDirectory, // we'll move files from here to their area container once processed/confirmed + direction : 'recv', } }; if(!this.isBlindUpload()) { - // data has been sanatized at this point + // data has been sanatized at this point modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData(); } // - // Move along to protocol selection -> file transfer - // Upon completion, we'll re-enter the module with some file paths handed to us + // Move along to protocol selection -> file transfer + // Upon completion, we'll re-enter the module with some file paths handed to us // return this.gotoMenu( this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', @@ -216,7 +216,7 @@ exports.getModule = class UploadModule extends MenuModule { } updateScanStepInfoViews(stepInfo) { - // :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC + // :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC const fmtObj = Object.assign( {}, stepInfo); let stepIndicatorFmt = ''; @@ -224,8 +224,8 @@ exports.getModule = class UploadModule extends MenuModule { const fmtConfig = this.menuConfig.config; - const indicatorStates = fmtConfig.indicatorStates || [ '|', '/', '-', '\\' ]; - const indicatorFinished = fmtConfig.indicatorFinished || '√'; + const indicatorStates = fmtConfig.indicatorStates || [ '|', '/', '-', '\\' ]; + const indicatorFinished = fmtConfig.indicatorFinished || '√'; const indicator = { }; const self = this; @@ -310,8 +310,8 @@ exports.getModule = class UploadModule extends MenuModule { const self = this; const results = { - newEntries : [], - dupes : [], + newEntries : [], + dupes : [], }; self.client.log.debug('Scanning upload(s)', { paths : this.recvFilePaths } ); @@ -319,22 +319,22 @@ exports.getModule = class UploadModule extends MenuModule { let currentFileNum = 0; async.eachSeries(this.recvFilePaths, (filePath, nextFilePath) => { - // :TODO: virus scanning/etc. should occur around here + // :TODO: virus scanning/etc. should occur around here currentFileNum += 1; self.scanStatus = { - indicatorPos : 0, + indicatorPos : 0, }; const scanOpts = { - areaTag : self.areaInfo.areaTag, - storageTag : self.areaInfo.storageTags[0], + areaTag : self.areaInfo.areaTag, + storageTag : self.areaInfo.storageTags[0], }; function handleScanStep(stepInfo, nextScanStep) { - stepInfo.totalFileNum = self.recvFilePaths.length; - stepInfo.currentFileNum = currentFileNum; + stepInfo.totalFileNum = self.recvFilePaths.length; + stepInfo.currentFileNum = currentFileNum; self.updateScanStepInfoViews(stepInfo); return nextScanStep(null); @@ -347,14 +347,14 @@ exports.getModule = class UploadModule extends MenuModule { return nextFilePath(err); } - // new or dupe? + // new or dupe? if(dupeEntries.length > 0) { - // 1:n dupes found + // 1:n dupes found self.client.log.debug('Duplicate file(s) found', { dupeEntries : dupeEntries } ); results.dupes = results.dupes.concat(dupeEntries); } else { - // new one + // new one results.newEntries.push(fileEntry); } @@ -377,37 +377,37 @@ exports.getModule = class UploadModule extends MenuModule { const self = this; async.eachSeries(newEntries, (newEntry, nextEntry) => { - const src = paths.join(self.tempRecvDirectory, newEntry.fileName); - const dst = paths.join(areaStorageDir, newEntry.fileName); + const src = paths.join(self.tempRecvDirectory, newEntry.fileName); + const dst = paths.join(areaStorageDir, newEntry.fileName); - moveFileWithCollisionHandling(src, dst, (err, finalPath) => { + moveFileWithCollisionHandling(src, dst, (err, finalPath) => { if(err) { self.client.log.error( 'Failed moving physical upload file', { error : err.message, fileName : newEntry.fileName, source : src, dest : dst } ); if(dst !== finalPath) { - // name changed; ajust before persist + // name changed; ajust before persist newEntry.fileName = paths.basename(finalPath); } - return nextEntry(null); // still try next file + return nextEntry(null); // still try next file } self.client.log.debug('Moved upload to area', { path : finalPath } ); - // persist to DB + // persist to DB newEntry.persist(err => { if(err) { self.client.log.error('Failed persisting upload to database', { path : finalPath, error : err.message } ); } - return nextEntry(null); // still try next file + return nextEntry(null); // still try next file }); }); }, () => { // - // Finally, we can remove any temp files that we may have created + // Finally, we can remove any temp files that we may have created // self.cleanupTempFiles(); }); @@ -415,8 +415,8 @@ exports.getModule = class UploadModule extends MenuModule { prepDetailsForUpload(scanResults, cb) { async.eachSeries(scanResults.newEntries, (newEntry, nextEntry) => { - newEntry.meta.upload_by_username = this.client.user.username; - newEntry.meta.upload_by_user_id = this.client.user.userId; + newEntry.meta.upload_by_username = this.client.user.username; + newEntry.meta.upload_by_user_id = this.client.user.userId; this.displayFileDetailsPageForUploadEntry(newEntry, (err, newValues) => { if(err) { @@ -445,8 +445,8 @@ exports.getModule = class UploadModule extends MenuModule { displayDupesPage(dupes, cb) { // - // If we have custom art to show, use it - else just dump basic info. - // Pause at the end in either case. + // If we have custom art to show, use it - else just dump basic info. + // Pause at the end in either case. // const self = this; @@ -469,7 +469,7 @@ exports.getModule = class UploadModule extends MenuModule { ); }, function prepDupeObjects(dupeListView, callback) { - // update dupe objects with additional info that can be used for formatString() and the like + // update dupe objects with additional info that can be used for formatString() and the like async.each(dupes, (dupe, nextDupe) => { FileEntry.loadBasicEntry(dupe.fileId, dupe, err => { if(err) { @@ -478,8 +478,8 @@ exports.getModule = class UploadModule extends MenuModule { const areaInfo = getFileAreaByTag(dupe.areaTag); if(areaInfo) { - dupe.areaName = areaInfo.name; - dupe.areaDesc = areaInfo.desc; + dupe.areaName = areaInfo.name; + dupe.areaDesc = areaInfo.desc; } return nextDupe(null); }); @@ -513,7 +513,7 @@ exports.getModule = class UploadModule extends MenuModule { processUploadedFiles() { // - // For each file uploaded, we need to process & gather information + // For each file uploaded, we need to process & gather information // const self = this; @@ -525,8 +525,8 @@ exports.getModule = class UploadModule extends MenuModule { } // - // For non-blind uploads, batch is not supported, we expect a single file - // in |recvFilePaths|. If not, it's an error (we don't want to process the wrong thing) + // For non-blind uploads, batch is not supported, we expect a single file + // in |recvFilePaths|. If not, it's an error (we don't want to process the wrong thing) // if(self.recvFilePaths.length > 1) { self.client.log.warn( { recvFilePaths : self.recvFilePaths }, 'Non-blind upload received 2:n files' ); @@ -563,9 +563,9 @@ exports.getModule = class UploadModule extends MenuModule { }, function startMovingAndPersistingToDatabase(scanResults, callback) { // - // *Start* the process of moving files from their current |tempRecvDirectory| - // locations -> their final area destinations. Don't make the user wait - // here as I/O can take quite a bit of time. Log any failures. + // *Start* the process of moving files from their current |tempRecvDirectory| + // locations -> their final area destinations. Don't make the user wait + // here as I/O can take quite a bit of time. Log any failures. // self.moveAndPersistUploadsToDatabase(scanResults.newEntries); return callback(null, scanResults.newEntries); @@ -574,8 +574,8 @@ exports.getModule = class UploadModule extends MenuModule { Events.emit( Events.getSystemEvents().UserUpload, { - user : self.client.user, - files : uploadedEntries, + user : self.client.user, + files : uploadedEntries, } ); return callback(null); @@ -584,7 +584,7 @@ exports.getModule = class UploadModule extends MenuModule { err => { if(err) { self.client.log.warn('File upload error encountered', { error : err.message } ); - self.cleanupTempFiles(); // normally called after moveAndPersistUploadsToDatabase() is completed. + self.cleanupTempFiles(); // normally called after moveAndPersistUploadsToDatabase() is completed. } return self.prevMenu(); @@ -609,8 +609,8 @@ exports.getModule = class UploadModule extends MenuModule { const areaSelectView = self.viewControllers.options.getView(MciViewIds.options.area); areaSelectView.setItems( self.availAreas.map(areaInfo => areaInfo.name ) ); - const uploadTypeView = self.viewControllers.options.getView(MciViewIds.options.uploadType); - const fileNameView = self.viewControllers.options.getView(MciViewIds.options.fileName); + const uploadTypeView = self.viewControllers.options.getView(MciViewIds.options.uploadType); + const fileNameView = self.viewControllers.options.getView(MciViewIds.options.fileName); const blindFileNameText = self.menuConfig.config.blindFileNameText || '(blind - filename ignored)'; @@ -626,7 +626,7 @@ exports.getModule = class UploadModule extends MenuModule { } }); - // sanatize filename for display when leaving the view + // sanatize filename for display when leaving the view self.viewControllers.options.on('leave', prevView => { if(prevView.id === MciViewIds.options.fileName) { fileNameView.setText(sanatizeFilename(fileNameView.getData())); @@ -634,7 +634,7 @@ exports.getModule = class UploadModule extends MenuModule { }); self.uploadType = 'blind'; - uploadTypeView.setFocusItemIndex(0); // default to blind + uploadTypeView.setFocusItemIndex(0); // default to blind fileNameView.setText(blindFileNameText); areaSelectView.redraw(); @@ -655,7 +655,7 @@ exports.getModule = class UploadModule extends MenuModule { FormIds.processing, { clearScreen : true, trailingLF : false }, err => { - // note: this art is not required + // note: this art is not required this.hasProcessingArt = !err; return cb(null); @@ -689,7 +689,7 @@ exports.getModule = class UploadModule extends MenuModule { self.updateCustomViewTextsWithFilter('fileDetails', MciViewIds.fileDetails.customRangeStart, fileEntry ); - tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse + tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse yearView.setText(fileEntry.meta.est_release_year || ''); if(isAnsi(fileEntry.desc)) { @@ -698,8 +698,8 @@ exports.getModule = class UploadModule extends MenuModule { return descView.setAnsi( fileEntry.desc, { - prepped : false, - forceLineTerm : true, + prepped : false, + forceLineTerm : true, }, () => { return callback(null, descView, 'preview', MciViewIds.fileDetails.tags); @@ -709,29 +709,29 @@ exports.getModule = class UploadModule extends MenuModule { const hasDesc = self.fileEntryHasDetectedDesc(fileEntry); descView.setText( hasDesc ? fileEntry.desc : getDescFromFileName(fileEntry.fileName), - { scrollMode : 'top' } // override scroll mode; we want to be @ top + { scrollMode : 'top' } // override scroll mode; we want to be @ top ); return callback(null, descView, 'edit', hasDesc ? MciViewIds.fileDetails.tags : MciViewIds.fileDetails.desc); } }, function finalizeViews(descView, descViewMode, focusId, callback) { descView.setPropertyValue('mode', descViewMode); - descView.acceptsFocus = 'preview' === descViewMode ? false : true; + descView.acceptsFocus = 'preview' === descViewMode ? false : true; self.viewControllers.fileDetails.switchFocus(focusId); return callback(null); } ], err => { // - // we only call |cb| here if there is an error - // else, wait for the current from to be submit - then call - - // this way we'll move on to the next file entry when ready + // we only call |cb| here if there is an error + // else, wait for the current from to be submit - then call - + // this way we'll move on to the next file entry when ready // if(err) { return cb(err); } - self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue + self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue } ); } diff --git a/core/user.js b/core/user.js index 72808f00..e3314cc7 100644 --- a/core/user.js +++ b/core/user.js @@ -1,53 +1,53 @@ /* jslint node: true */ 'use strict'; -const userDb = require('./database.js').dbs.user; -const Config = require('./config.js').get; -const userGroup = require('./user_group.js'); -const Errors = require('./enig_error.js').Errors; -const Events = require('./events.js'); +const userDb = require('./database.js').dbs.user; +const Config = require('./config.js').get; +const userGroup = require('./user_group.js'); +const Errors = require('./enig_error.js').Errors; +const Events = require('./events.js'); -// deps -const crypto = require('crypto'); -const assert = require('assert'); -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +// deps +const crypto = require('crypto'); +const assert = require('assert'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); exports.isRootUserId = function(id) { return 1 === id; }; module.exports = class User { constructor() { - this.userId = 0; - this.username = ''; - this.properties = {}; // name:value - this.groups = []; // group membership(s) + this.userId = 0; + this.username = ''; + this.properties = {}; // name:value + this.groups = []; // group membership(s) } - // static property accessors + // static property accessors static get RootUserID() { return 1; } static get PBKDF2() { return { - iterations : 1000, - keyLen : 128, - saltLen : 32, + iterations : 1000, + keyLen : 128, + saltLen : 32, }; } static get StandardPropertyGroups() { return { - password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], + password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], }; } static get AccountStatus() { return { - disabled : 0, - inactive : 1, - active : 2, + disabled : 0, + inactive : 1, + active : 2, }; } @@ -69,14 +69,14 @@ module.exports = class User { } return ((this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2) && - (this.properties.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2)); + (this.properties.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2)); } isRoot() { return User.isRootUserId(this.userId); } - isSysOp() { // alias to isRoot() + isSysOp() { // alias to isRoot() return this.isRoot(); } @@ -98,7 +98,7 @@ module.exports = class User { return 30; } - return 10; // :TODO: Is this what we want? + return 10; // :TODO: Is this what we want? } authenticate(username, password, cb) { @@ -108,32 +108,32 @@ module.exports = class User { async.waterfall( [ function fetchUserId(callback) { - // get user ID + // get user ID User.getUserIdAndName(username, (err, uid, un) => { - cachedInfo.userId = uid; - cachedInfo.username = un; + cachedInfo.userId = uid; + cachedInfo.username = un; return callback(err); }); }, function getRequiredAuthProperties(callback) { - // fetch properties required for authentication + // fetch properties required for authentication User.loadProperties(cachedInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { return callback(err, props); }); }, function getDkWithSalt(props, callback) { - // get DK from stored salt and password provided + // get DK from stored salt and password provided User.generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, (err, dk) => { return callback(err, dk, props.pw_pbkdf2_dk); }); }, function validateAuth(passDk, propsDk, callback) { // - // Use constant time comparison here for security feel-goods + // Use constant time comparison here for security feel-goods // - const passDkBuf = Buffer.from(passDk, 'hex'); - const propsDkBuf = Buffer.from(propsDk, 'hex'); + const passDkBuf = Buffer.from(passDk, 'hex'); + const propsDkBuf = Buffer.from(propsDk, 'hex'); if(passDkBuf.length !== propsDkBuf.length) { return callback(Errors.AccessDenied('Invalid password')); @@ -167,11 +167,11 @@ module.exports = class User { ], err => { if(!err) { - self.userId = cachedInfo.userId; - self.username = cachedInfo.username; - self.properties = cachedInfo.properties; - self.groups = cachedInfo.groups; - self.authenticated = true; + self.userId = cachedInfo.userId; + self.username = cachedInfo.username; + self.properties = cachedInfo.properties; + self.groups = cachedInfo.groups; + self.authenticated = true; } return cb(err); @@ -189,7 +189,7 @@ module.exports = class User { const self = this; - // :TODO: set various defaults, e.g. default activation status, etc. + // :TODO: set various defaults, e.g. default activation status, etc. self.properties.account_status = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; async.waterfall( @@ -200,16 +200,16 @@ module.exports = class User { function createUserRec(trans, callback) { trans.run( `INSERT INTO user (user_name) - VALUES (?);`, + VALUES (?);`, [ self.username ], - function inserted(err) { // use classic function for |this| + function inserted(err) { // use classic function for |this| if(err) { return callback(err); } self.userId = this.lastID; - // Do not require activation for userId 1 (root/admin) + // Do not require activation for userId 1 (root/admin) if(User.RootUserID === self.userId) { self.properties.account_status = User.AccountStatus.active; } @@ -224,15 +224,15 @@ module.exports = class User { return callback(err); } - self.properties.pw_pbkdf2_salt = info.salt; - self.properties.pw_pbkdf2_dk = info.dk; + self.properties.pw_pbkdf2_salt = info.salt; + self.properties.pw_pbkdf2_dk = info.dk; return callback(null, trans); }); }, function setInitialGroupMembership(trans, callback) { self.groups = config.users.defaultGroups; - if(User.RootUserID === self.userId) { // root/SysOp? + if(User.RootUserID === self.userId) { // root/SysOp? self.groups.push('sysops'); } @@ -285,12 +285,12 @@ module.exports = class User { } persistProperty(propName, propValue, cb) { - // update live props + // update live props this.properties[propName] = propValue; userDb.run( `REPLACE INTO user_property (user_id, prop_name, prop_value) - VALUES (?, ?, ?);`, + VALUES (?, ?, ?);`, [ this.userId, propName, propValue ], err => { if(cb) { @@ -301,12 +301,12 @@ module.exports = class User { } removeProperty(propName, cb) { - // update live + // update live delete this.properties[propName]; userDb.run( `DELETE FROM user_property - WHERE user_id = ? AND prop_name = ?;`, + WHERE user_id = ? AND prop_name = ?;`, [ this.userId, propName ], err => { if(cb) { @@ -324,12 +324,12 @@ module.exports = class User { const self = this; - // update live props + // update live props _.merge(this.properties, properties); const stmt = transOrDb.prepare( `REPLACE INTO user_property (user_id, prop_name, prop_value) - VALUES (?, ?, ?);` + VALUES (?, ?, ?);` ); async.each(Object.keys(properties), (propName, nextProp) => { @@ -355,8 +355,8 @@ module.exports = class User { } const newProperties = { - pw_pbkdf2_salt : info.salt, - pw_pbkdf2_dk : info.dk, + pw_pbkdf2_salt : info.salt, + pw_pbkdf2_dk : info.dk, }; this.persistProperties(newProperties, err => { @@ -392,11 +392,11 @@ module.exports = class User { ], (err, userName, properties, groups) => { const user = new User(); - user.userId = userId; - user.username = userName; - user.properties = properties; - user.groups = groups; - user.authenticated = false; // this is NOT an authenticated user! + user.userId = userId; + user.username = userName; + user.properties = properties; + user.groups = groups; + user.authenticated = false; // this is NOT an authenticated user! return cb(err, user); } @@ -410,8 +410,8 @@ module.exports = class User { static getUserIdAndName(username, cb) { userDb.get( `SELECT id, user_name - FROM user - WHERE user_name LIKE ?;`, + FROM user + WHERE user_name LIKE ?;`, [ username ], (err, row) => { if(err) { @@ -430,12 +430,12 @@ module.exports = class User { static getUserIdAndNameByRealName(realName, cb) { userDb.get( `SELECT id, user_name - FROM user - WHERE id = ( - SELECT user_id - FROM user_property - WHERE prop_name='real_name' AND prop_value LIKE ? - );`, + FROM user + WHERE id = ( + SELECT user_id + FROM user_property + WHERE prop_name='real_name' AND prop_value LIKE ? + );`, [ realName ], (err, row) => { if(err) { @@ -466,8 +466,8 @@ module.exports = class User { static getUserName(userId, cb) { userDb.get( `SELECT user_name - FROM user - WHERE id = ?;`, + FROM user + WHERE id = ?;`, [ userId ], (err, row) => { if(err) { @@ -490,9 +490,9 @@ module.exports = class User { } let sql = - `SELECT prop_name, prop_value - FROM user_property - WHERE user_id = ?`; + `SELECT prop_name, prop_value + FROM user_property + WHERE user_id = ?`; if(options.names) { sql += ` AND prop_name IN("${options.names.join('","')}");`; @@ -511,14 +511,14 @@ module.exports = class User { }); } - // :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. + // :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc. static getUserIdsWithProperty(propName, propValue, cb) { let userIds = []; userDb.each( `SELECT user_id - FROM user_property - WHERE prop_name = ? AND prop_value = ?;`, + FROM user_property + WHERE prop_name = ? AND prop_value = ?;`, [ propName, propValue ], (err, row) => { if(row) { @@ -537,13 +537,13 @@ module.exports = class User { userDb.each( `SELECT id, user_name - FROM user - ${orderClause};`, + FROM user + ${orderClause};`, (err, row) => { if(row) { userList.push({ - userId : row.id, - userName : row.user_name, + userId : row.id, + userName : row.user_name, }); } }, @@ -552,8 +552,8 @@ module.exports = class User { async.map(userList, (user, nextUser) => { userDb.each( `SELECT prop_name, prop_value - FROM user_property - WHERE user_id = ? AND prop_name IN ("${options.properties.join('","')}");`, + FROM user_property + WHERE user_id = ? AND prop_name IN ("${options.properties.join('","')}");`, [ user.userId ], (err, row) => { if(row) { diff --git a/core/user_config.js b/core/user_config.js index b5e124b6..f9aad6c4 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -1,38 +1,38 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const theme = require('./theme.js'); -const sysValidate = require('./system_view_validate.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const sysValidate = require('./system_view_validate.js'); -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { - name : 'User Configuration', - desc : 'Module for user configuration', - author : 'NuSkooler', + name : 'User Configuration', + desc : 'Module for user configuration', + author : 'NuSkooler', }; const MciCodeIds = { - RealName : 1, - BirthDate : 2, - Sex : 3, - Loc : 4, - Affils : 5, - Email : 6, - Web : 7, - TermHeight : 8, - Theme : 9, - Password : 10, - PassConfirm : 11, - ThemeInfo : 20, - ErrorMsg : 21, + RealName : 1, + BirthDate : 2, + Sex : 3, + Loc : 4, + Affils : 5, + Email : 6, + Web : 7, + TermHeight : 8, + Theme : 9, + Password : 10, + PassConfirm : 11, + ThemeInfo : 20, + ErrorMsg : 21, - SaveCancel : 25, + SaveCancel : 25, }; exports.getModule = class UserConfigModule extends MenuModule { @@ -43,29 +43,29 @@ exports.getModule = class UserConfigModule extends MenuModule { this.menuMethods = { // - // Validation support + // Validation support // validateEmailAvail : function(data, cb) { // - // If nothing changed, we know it's OK + // If nothing changed, we know it's OK // if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { return cb(null); } - // Otherwise we can use the standard system method + // Otherwise we can use the standard system method return sysValidate.validateEmailAvail(data, cb); }, validatePassword : function(data, cb) { // - // Blank is OK - this means we won't be changing it + // Blank is OK - this means we won't be changing it // if(!data || 0 === data.length) { return cb(null); } - // Otherwise we can use the standard system method + // Otherwise we can use the standard system method return sysValidate.validatePasswordSpec(data, cb); }, @@ -95,21 +95,21 @@ exports.getModule = class UserConfigModule extends MenuModule { }, // - // Handlers + // Handlers // saveChanges : function(formData, extraArgs, cb) { assert(formData.value.password === formData.value.passwordConfirm); const newProperties = { - real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), - sex : formData.value.sex, - location : formData.value.location, - affiliation : formData.value.affils, - email_address : formData.value.email, - web_address : formData.value.web, - term_height : formData.value.termHeight.toString(), - theme_id : self.availThemeInfo[formData.value.theme].themeId, + real_name : formData.value.realName, + birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), + sex : formData.value.sex, + location : formData.value.location, + affiliation : formData.value.affils, + email_address : formData.value.email, + web_address : formData.value.web, + term_height : formData.value.termHeight.toString(), + theme_id : self.availThemeInfo[formData.value.theme].themeId, }; // runtime set theme @@ -119,11 +119,11 @@ exports.getModule = class UserConfigModule extends MenuModule { self.client.user.persistProperties(newProperties, err => { if(err) { self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); - // :TODO: warn end user! + // :TODO: warn end user! return self.prevMenu(cb); } // - // New password if it's not empty + // New password if it's not empty // self.client.log.info('User updated properties'); @@ -154,8 +154,8 @@ exports.getModule = class UserConfigModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); + const self = this; + const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); let currentThemeIdIndex = 0; async.series( @@ -167,11 +167,11 @@ exports.getModule = class UserConfigModule extends MenuModule { self.availThemeInfo = _.sortBy([...theme.getAvailableThemes()].map(entry => { const theme = entry[1]; return { - themeId : theme.info.themeId, - name : theme.info.name, - author : theme.info.author, - desc : _.isString(theme.info.desc) ? theme.info.desc : '', - group : _.isString(theme.info.group) ? theme.info.group : '', + themeId : theme.info.themeId, + name : theme.info.name, + author : theme.info.author, + desc : _.isString(theme.info.desc) ? theme.info.desc : '', + group : _.isString(theme.info.group) ? theme.info.group : '', }; }), 'name'); @@ -202,7 +202,7 @@ exports.getModule = class UserConfigModule extends MenuModule { var realNameView = self.getView(MciCodeIds.RealName); if(realNameView) { - realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! + realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! } callback(null); diff --git a/core/user_group.js b/core/user_group.js index a350e5b2..4b1548b8 100644 --- a/core/user_group.js +++ b/core/user_group.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -const userDb = require('./database.js').dbs.user; +const userDb = require('./database.js').dbs.user; -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); -exports.getGroupsForUser = getGroupsForUser; -exports.addUserToGroup = addUserToGroup; -exports.addUserToGroups = addUserToGroups; -exports.removeUserFromGroup = removeUserFromGroup; +exports.getGroupsForUser = getGroupsForUser; +exports.addUserToGroup = addUserToGroup; +exports.addUserToGroups = addUserToGroups; +exports.removeUserFromGroup = removeUserFromGroup; function getGroupsForUser(userId, cb) { const sql = - `SELECT group_name - FROM user_group_member - WHERE user_id=?;`; + `SELECT group_name + FROM user_group_member + WHERE user_id=?;`; const groups = []; @@ -39,7 +39,7 @@ function addUserToGroup(userId, groupName, transOrDb, cb) { transOrDb.run( `REPLACE INTO user_group_member (group_name, user_id) - VALUES(?, ?);`, + VALUES(?, ?);`, [ groupName, userId ], err => { return cb(err); @@ -59,7 +59,7 @@ function addUserToGroups(userId, groups, transOrDb, cb) { function removeUserFromGroup(userId, groupName, cb) { userDb.run( `DELETE FROM user_group_member - WHERE group_name=? AND user_id=?;`, + WHERE group_name=? AND user_id=?;`, [ groupName, userId ], err => { return cb(err); diff --git a/core/user_list.js b/core/user_list.js index 212eb9ea..8af9690b 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -1,35 +1,35 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; -const User = require('./user.js'); -const ViewController = require('./view_controller.js').ViewController; -const stringFormat = require('./string_format.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const User = require('./user.js'); +const ViewController = require('./view_controller.js').ViewController; +const stringFormat = require('./string_format.js'); -const moment = require('moment'); -const async = require('async'); -const _ = require('lodash'); +const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); /* - Available listFormat/focusListFormat object members: + Available listFormat/focusListFormat object members: - userId : User ID - userName : User name/handle - lastLoginTs : Last login timestamp - status : Status: active | inactive - location : Location - affiliation : Affils - note : User note + userId : User ID + userName : User name/handle + lastLoginTs : Last login timestamp + status : Status: active | inactive + location : Location + affiliation : Affils + note : User note */ exports.moduleInfo = { - name : 'User List', - desc : 'Lists all system users', - author : 'NuSkooler', + name : 'User List', + desc : 'Lists all system users', + author : 'NuSkooler', }; const MciViewIds = { - UserList : 1, + UserList : 1, }; exports.getModule = class UserListModule extends MenuModule { @@ -43,8 +43,8 @@ exports.getModule = class UserListModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); let userList = []; @@ -56,14 +56,14 @@ exports.getModule = class UserListModule extends MenuModule { [ function loadFromConfig(callback) { var loadOpts = { - callingMenu : self, - mciMap : mciData.menu, + callingMenu : self, + mciMap : mciData.menu, }; vc.loadFromMenuConfig(loadOpts, callback); }, function fetchUserList(callback) { - // :TODO: Currently fetching all users - probably always OK, but this could be paged + // :TODO: Currently fetching all users - probably always OK, but this could be paged User.getUserList(USER_LIST_OPTS, function got(err, ul) { userList = ul; callback(err); @@ -72,19 +72,19 @@ exports.getModule = class UserListModule extends MenuModule { function populateList(callback) { var userListView = vc.getView(MciViewIds.UserList); - var listFormat = self.menuConfig.config.listFormat || '{userName} - {affils}'; - var focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default changed color! - var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; + var listFormat = self.menuConfig.config.listFormat || '{userName} - {affils}'; + var focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default changed color! + var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; function getUserFmtObj(ue) { return { - userId : ue.userId, - userName : ue.userName, - affils : ue.affiliation, - location : ue.location, - // :TODO: the rest! - note : ue.note || '', - lastLoginTs : moment(ue.last_login_timestamp).format(dateTimeFormat), + userId : ue.userId, + userName : ue.userName, + affils : ue.affiliation, + location : ue.location, + // :TODO: the rest! + note : ue.note || '', + lastLoginTs : moment(ue.last_login_timestamp).format(dateTimeFormat), }; } diff --git a/core/user_login.js b/core/user_login.js index aa3cfe7b..07a0a2d2 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -1,34 +1,34 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const setClientTheme = require('./theme.js').setClientTheme; -const clientConnections = require('./client_connections.js').clientConnections; -const StatLog = require('./stat_log.js'); -const logger = require('./logger.js'); -const Events = require('./events.js'); +// ENiGMA½ +const setClientTheme = require('./theme.js').setClientTheme; +const clientConnections = require('./client_connections.js').clientConnections; +const StatLog = require('./stat_log.js'); +const logger = require('./logger.js'); +const Events = require('./events.js'); -// deps -const async = require('async'); +// deps +const async = require('async'); -exports.userLogin = userLogin; +exports.userLogin = userLogin; function userLogin(client, username, password, cb) { client.user.authenticate(username, password, function authenticated(err) { if(err) { client.log.info( { username : username, error : err.message }, 'Failed login attempt'); - // :TODO: if username exists, record failed login attempt to properties - // :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true + // :TODO: if username exists, record failed login attempt to properties + // :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true return cb(err); } - const user = client.user; + const user = client.user; // - // Ensure this user is not already logged in. - // Loop through active connections -- which includes the current -- - // and check for matching user ID. If the count is > 1, disallow. + // Ensure this user is not already logged in. + // Loop through active connections -- which includes the current -- + // and check for matching user ID. If the count is > 1, disallow. // let existingClientConnection; clientConnections.forEach(function connEntry(cc) { @@ -40,9 +40,9 @@ function userLogin(client, username, password, cb) { if(existingClientConnection) { client.log.info( { - existingClientId : existingClientConnection.session.id, - username : user.username, - userId : user.userId + existingClientId : existingClientConnection.session.id, + username : user.username, + userId : user.userId }, 'Already logged in' ); @@ -50,23 +50,23 @@ function userLogin(client, username, password, cb) { const existingConnError = new Error('Already logged in as supplied user'); 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 return cb(existingConnError); } - // update client logger with addition of username + // update client logger with addition of username client.log = logger.log.child( { - clientId : client.log.fields.clientId, - sessionId : client.log.fields.sessionId, - username : user.username, + clientId : client.log.fields.clientId, + sessionId : client.log.fields.sessionId, + username : user.username, } ); client.log.info('Successful login'); - // User's unique session identifier is the same as the connection itself - user.sessionId = client.session.uniqueId; // convienence + // User's unique session identifier is the same as the connection itself + user.sessionId = client.session.uniqueId; // convienence Events.emit(Events.getSystemEvents().UserLogin, { user } ); @@ -86,7 +86,7 @@ function userLogin(client, username, password, cb) { return StatLog.incrementUserStat(user, 'login_count', 1, 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 return StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); } ], diff --git a/core/uuid_util.js b/core/uuid_util.js index 7cf582f5..f731ecc0 100644 --- a/core/uuid_util.js +++ b/core/uuid_util.js @@ -1,14 +1,14 @@ /* jslint node: true */ 'use strict'; -const createHash = require('crypto').createHash; +const createHash = require('crypto').createHash; -exports.createNamedUUID = createNamedUUID; +exports.createNamedUUID = createNamedUUID; function createNamedUUID(namespaceUuid, key) { // - // v5 UUID generation code based on the work here: - // https://github.com/download13/uuidv5/blob/master/uuid.js + // v5 UUID generation code based on the work here: + // https://github.com/download13/uuidv5/blob/master/uuid.js // if(!Buffer.isBuffer(namespaceUuid)) { namespaceUuid = Buffer.from(namespaceUuid); @@ -24,12 +24,12 @@ function createNamedUUID(namespaceUuid, key) { let u = Buffer.alloc(16); // bbbb - bb - bb - bb - bbbbbb - digest.copy(u, 0, 0, 4); // time_low - digest.copy(u, 4, 4, 6); // time_mid - digest.copy(u, 6, 6, 8); // time_hi_and_version + digest.copy(u, 0, 0, 4); // time_low + digest.copy(u, 4, 4, 6); // time_mid + digest.copy(u, 6, 6, 8); // time_hi_and_version - u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) - u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 + u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) + u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 u[9] = digest[9]; digest.copy(u, 10, 10, 16); diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index e56207a2..ea071010 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -1,32 +1,32 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuView = require('./menu_view.js').MenuView; -const ansi = require('./ansi_term.js'); -const strUtil = require('./string_util.js'); -const formatString = require('./string_format'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +// ENiGMA½ +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; -// deps -const util = require('util'); -const _ = require('lodash'); +// deps +const util = require('util'); +const _ = require('lodash'); -exports.VerticalMenuView = VerticalMenuView; +exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { - options.cursor = options.cursor || 'hide'; + options.cursor = options.cursor || 'hide'; options.justify = options.justify || 'left'; MenuView.call(this, options); const self = this; - // we want page up/page down by default + // we want page up/page down by default if(!_.isObject(options.specialKeyMap)) { Object.assign(this.specialKeyMap, { - 'page up' : [ 'page up' ], - 'page down' : [ 'page down' ], + 'page up' : [ 'page up' ], + 'page down' : [ 'page down' ], }); } @@ -53,8 +53,8 @@ function VerticalMenuView(options) { self.maxVisibleItems = Math.ceil(self.dimens.height / (self.itemSpacing + 1)); self.viewWindow = { - top : self.focusedItemIndex, - bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1, + top : self.focusedItemIndex, + bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1, }; }; @@ -94,7 +94,7 @@ util.inherits(VerticalMenuView, MenuView); VerticalMenuView.prototype.redraw = function() { VerticalMenuView.super_.prototype.redraw.call(this); - // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such + // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such if(this.positionCacheExpired) { this.performAutoScale(); this.updateViewVisibleItems(); @@ -102,13 +102,13 @@ VerticalMenuView.prototype.redraw = function() { this.positionCacheExpired = false; } - // erase old items - // :TODO: optimize this: only needed if a item is removed or new max width < old. + // erase old items + // :TODO: optimize this: only needed if a item is removed or new max width < old. if(this.oldDimens) { - const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); - let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; - let row = this.position.row + 1; - const endRow = (row + this.oldDimens.height) - 2; + const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); + let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; + let row = this.position.row + 1; + const endRow = (row + this.oldDimens.height) - 2; while(row <= endRow) { seq += ansi.goto(row, this.position.col) + blank; @@ -148,16 +148,16 @@ VerticalMenuView.prototype.setFocus = function(focused) { }; VerticalMenuView.prototype.setFocusItemIndex = function(index) { - VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex const remainAfterFocus = this.items.length - index; if(remainAfterFocus >= this.maxVisibleItems) { this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + top : this.focusedItemIndex, + bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 }; - this.positionCacheExpired = false; // skip standard behavior + this.positionCacheExpired = false; // skip standard behavior this.performAutoScale(); } @@ -190,7 +190,7 @@ VerticalMenuView.prototype.getData = function() { }; VerticalMenuView.prototype.setItems = function(items) { - // if we have items already, save off their drawing area so we don't leave fragments at redraw + // if we have items already, save off their drawing area so we don't leave fragments at redraw if(this.items && this.items.length) { this.oldDimens = Object.assign({}, this.dimens); } @@ -208,15 +208,15 @@ VerticalMenuView.prototype.removeItem = function(index) { VerticalMenuView.super_.prototype.removeItem.call(this, index); }; -// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! +// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! VerticalMenuView.prototype.focusNext = function() { if(this.items.length - 1 === this.focusedItemIndex) { this.focusedItemIndex = 0; this.viewWindow = { - top : 0, - bottom : Math.min(this.maxVisibleItems, this.items.length) - 1 + top : 0, + bottom : Math.min(this.maxVisibleItems, this.items.length) - 1 }; } else { this.focusedItemIndex++; @@ -237,9 +237,9 @@ VerticalMenuView.prototype.focusPrevious = function() { this.focusedItemIndex = this.items.length - 1; this.viewWindow = { - //top : this.items.length - this.maxVisibleItems, - top : Math.max(this.items.length - this.maxVisibleItems, 0), - bottom : this.items.length - 1 + //top : this.items.length - this.maxVisibleItems, + top : Math.max(this.items.length - this.maxVisibleItems, 0), + bottom : this.items.length - 1 }; } else { @@ -249,7 +249,7 @@ VerticalMenuView.prototype.focusPrevious = function() { this.viewWindow.top--; this.viewWindow.bottom--; - // adjust for focus index being set & window needing expansion as we scroll up + // adjust for focus index being set & window needing expansion as we scroll up const rem = (this.viewWindow.bottom - this.viewWindow.top) + 1; if(rem < this.maxVisibleItems && (this.items.length - 1) > this.focusedItemIndex) { this.viewWindow.bottom = this.items.length - 1; @@ -264,11 +264,11 @@ VerticalMenuView.prototype.focusPrevious = function() { VerticalMenuView.prototype.focusPreviousPageItem = function() { // - // Jump to current - up to page size or top - // If already at the top, jump to bottom + // 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 + return this.focusPrevious(); // will jump to bottom } const index = Math.max(this.focusedItemIndex - this.dimens.height, 0); @@ -284,11 +284,11 @@ VerticalMenuView.prototype.focusPreviousPageItem = function() { VerticalMenuView.prototype.focusNextPageItem = function() { // - // Jump to current + up to page size or bottom - // If already at the bottom, jump to top + // 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 + return this.focusNext(); // will jump to top } const index = Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length - 1); @@ -299,8 +299,8 @@ VerticalMenuView.prototype.focusNextPageItem = function() { this.focusedItemIndex = index; this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + top : this.focusedItemIndex, + bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 }; this.redraw(); @@ -328,8 +328,8 @@ VerticalMenuView.prototype.focusLast = function() { this.focusedItemIndex = index; this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + top : this.focusedItemIndex, + bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 }; this.redraw(); diff --git a/core/view.js b/core/view.js index fd46428f..702be949 100644 --- a/core/view.js +++ b/core/view.js @@ -1,35 +1,35 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const events = require('events'); -const util = require('util'); -const ansi = require('./ansi_term.js'); -const colorCodes = require('./color_codes.js'); -const enigAssert = require('./enigma_assert.js'); -const { renderSubstr } = require('./string_util.js'); +// ENiGMA½ +const events = require('events'); +const util = require('util'); +const ansi = require('./ansi_term.js'); +const colorCodes = require('./color_codes.js'); +const enigAssert = require('./enigma_assert.js'); +const { renderSubstr } = require('./string_util.js'); -// deps -const _ = require('lodash'); +// deps +const _ = require('lodash'); -exports.View = View; +exports.View = View; const VIEW_SPECIAL_KEY_MAP_DEFAULT = { - accept : [ 'return' ], - exit : [ 'esc' ], - backspace : [ 'backspace', 'del' ], - del : [ 'del' ], - next : [ 'tab' ], - up : [ 'up arrow' ], - down : [ 'down arrow' ], - end : [ 'end' ], - home : [ 'home' ], - left : [ 'left arrow' ], - right : [ 'right arrow' ], - clearLine : [ 'ctrl + y' ], + accept : [ 'return' ], + exit : [ 'esc' ], + backspace : [ 'backspace', 'del' ], + del : [ 'del' ], + next : [ 'tab' ], + up : [ 'up arrow' ], + down : [ 'down arrow' ], + end : [ 'end' ], + home : [ 'home' ], + left : [ 'left arrow' ], + right : [ 'right arrow' ], + clearLine : [ 'ctrl + y' ], }; -exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT; +exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT; function View(options) { events.EventEmitter.call(this); @@ -37,21 +37,21 @@ function View(options) { enigAssert(_.isObject(options)); enigAssert(_.isObject(options.client)); - var self = this; + var self = this; - this.client = options.client; + this.client = options.client; - this.cursor = options.cursor || 'show'; - this.cursorStyle = options.cursorStyle || 'default'; + this.cursor = options.cursor || 'show'; + this.cursorStyle = options.cursorStyle || 'default'; - this.acceptsFocus = options.acceptsFocus || false; - this.acceptsInput = options.acceptsInput || false; + this.acceptsFocus = options.acceptsFocus || false; + this.acceptsInput = options.acceptsInput || false; - this.position = { x : 0, y : 0 }; - this.dimens = { height : 1, width : 0 }; + this.position = { x : 0, y : 0 }; + this.dimens = { height : 1, width : 0 }; - this.textStyle = options.textStyle || 'normal'; - this.focusTextStyle = options.focusTextStyle || this.textStyle; + this.textStyle = options.textStyle || 'normal'; + this.focusTextStyle = options.focusTextStyle || this.textStyle; if(options.id) { this.setId(options.id); @@ -72,17 +72,17 @@ function View(options) { this.autoScale = { height : false, width : false }; } else { this.dimens = { - width : options.width || 0, - height : 0 + width : options.width || 0, + height : 0 }; } - // :TODO: Just use styleSGRx for these, e.g. styleSGR0, styleSGR1 = norm/focus - this.ansiSGR = options.ansiSGR || ansi.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); - this.ansiFocusSGR = options.ansiFocusSGR || this.ansiSGR; + // :TODO: Just use styleSGRx for these, e.g. styleSGR0, styleSGR1 = norm/focus + this.ansiSGR = options.ansiSGR || ansi.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); + this.ansiFocusSGR = options.ansiFocusSGR || this.ansiSGR; - this.styleSGR1 = options.styleSGR1 || this.ansiSGR; - this.styleSGR2 = options.styleSGR2 || this.ansiFocusSGR; + this.styleSGR1 = options.styleSGR1 || this.ansiSGR; + this.styleSGR2 = options.styleSGR2 || this.ansiFocusSGR; if(this.acceptsInput) { this.specialKeyMap = options.specialKeyMap || VIEW_SPECIAL_KEY_MAP_DEFAULT; @@ -126,7 +126,7 @@ View.prototype.getId = function() { View.prototype.setPosition = function(pos) { // - // Allow the following forms: [row, col], { row : r, col : c }, or (row, col) + // Allow the following forms: [row, col], { row : r, col : c }, or (row, col) // if(util.isArray(pos)) { this.position.row = pos[0]; @@ -139,34 +139,34 @@ View.prototype.setPosition = function(pos) { this.position.col = parseInt(arguments[1], 10); } - // sanatize - this.position.row = Math.max(this.position.row, 1); - this.position.col = Math.max(this.position.col, 1); - this.position.row = Math.min(this.position.row, this.client.term.termHeight); - this.position.col = Math.min(this.position.col, this.client.term.termWidth); + // sanatize + this.position.row = Math.max(this.position.row, 1); + this.position.col = Math.max(this.position.col, 1); + this.position.row = Math.min(this.position.row, this.client.term.termHeight); + this.position.col = Math.min(this.position.col, this.client.term.termWidth); }; View.prototype.setDimension = function(dimens) { enigAssert(_.isObject(dimens) && _.isNumber(dimens.height) && _.isNumber(dimens.width)); - this.dimens = dimens; - this.autoScale = { height : false, width : false }; + this.dimens = dimens; + this.autoScale = { height : false, width : false }; }; View.prototype.setHeight = function(height) { - height = parseInt(height) || 1; - height = Math.min(height, this.client.term.termHeight); + height = parseInt(height) || 1; + height = Math.min(height, this.client.term.termHeight); - this.dimens.height = height; - this.autoScale.height = false; + this.dimens.height = height; + this.autoScale.height = false; }; View.prototype.setWidth = function(width) { - width = parseInt(width) || 1; - width = Math.min(width, this.client.term.termWidth); + width = parseInt(width) || 1; + width = Math.min(width, this.client.term.termWidth); - this.dimens.width = width; - this.autoScale.width = false; + this.dimens.width = width; + this.autoScale.width = false; }; View.prototype.getSGR = function() { @@ -188,20 +188,20 @@ View.prototype.setSpecialKeyMapOverride = function(specialKeyMapOverride) { View.prototype.setPropertyValue = function(propName, value) { switch(propName) { - case 'height' : this.setHeight(value); break; - case 'width' : this.setWidth(value); break; - case 'focus' : this.setFocus(value); break; + case 'height' : this.setHeight(value); break; + case 'width' : this.setWidth(value); break; + case 'focus' : this.setFocus(value); break; - case 'text' : + case 'text' : if('setText' in this) { this.setText(value); } break; - case 'textStyle' : this.textStyle = value; break; - case 'focusTextStyle' : this.focusTextStyle = value; break; + case 'textStyle' : this.textStyle = value; break; + case 'focusTextStyle' : this.focusTextStyle = value; break; - case 'justify' : this.justify = value; break; + case 'justify' : this.justify = value; break; case 'fillChar' : if('fillChar' in this) { @@ -217,9 +217,9 @@ View.prototype.setPropertyValue = function(propName, value) { if(_.isBoolean(value)) { this.submit = value; }/* else { - this.submit = _.isArray(value) && value.length > 0; - } - */ + this.submit = _.isArray(value) && value.length > 0; + } + */ break; case 'resizable' : @@ -258,8 +258,8 @@ View.prototype.setFocus = function(focused) { }; View.prototype.onKeyPress = function(ch, key) { - enigAssert(this.hasFocus, 'View does not have focus'); - enigAssert(this.acceptsInput, 'View does not accept input'); + enigAssert(this.hasFocus, 'View does not have focus'); + enigAssert(this.acceptsInput, 'View does not accept input'); if(!this.hasFocus || !this.acceptsInput) { return; diff --git a/core/view_controller.js b/core/view_controller.js index f6a2bc2b..0df09084 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -1,23 +1,23 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; -var menuUtil = require('./menu_util.js'); -var asset = require('./asset.js'); -var ansi = require('./ansi_term.js'); +// ENiGMA½ +var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; +var menuUtil = require('./menu_util.js'); +var asset = require('./asset.js'); +var ansi = require('./ansi_term.js'); -// deps -var events = require('events'); -var util = require('util'); -var assert = require('assert'); -var async = require('async'); -var _ = require('lodash'); -var paths = require('path'); +// deps +var events = require('events'); +var util = require('util'); +var assert = require('assert'); +var async = require('async'); +var _ = require('lodash'); +var paths = require('path'); -exports.ViewController = ViewController; +exports.ViewController = ViewController; -var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/; +var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/; function ViewController(options) { assert(_.isObject(options)); @@ -25,29 +25,29 @@ function ViewController(options) { events.EventEmitter.call(this); - var self = this; + var self = this; - this.client = options.client; - this.views = {}; // map of ID -> view - this.formId = options.formId || 0; - this.mciViewFactory = new MCIViewFactory(this.client); // :TODO: can this not be a singleton? - this.noInput = _.isBoolean(options.noInput) ? options.noInput : false; + this.client = options.client; + this.views = {}; // map of ID -> view + this.formId = options.formId || 0; + this.mciViewFactory = new MCIViewFactory(this.client); // :TODO: can this not be a singleton? + this.noInput = _.isBoolean(options.noInput) ? options.noInput : false; - this.actionKeyMap = {}; + this.actionKeyMap = {}; // - // Small wrapper/proxy around handleAction() to ensure we do not allow - // input/additional actions queued while performing an action + // Small wrapper/proxy around handleAction() to ensure we do not allow + // input/additional actions queued while performing an action // this.handleActionWrapper = function(formData, actionBlock) { if(self.waitActionCompletion) { - return; // ignore until this is finished! + return; // ignore until this is finished! } self.waitActionCompletion = true; menuUtil.handleAction(self.client, formData, actionBlock, (err) => { if(err) { - // :TODO: What can we really do here? + // :TODO: What can we really do here? if('ALREADYTHERE' === err.reasonCode) { self.client.log.trace( err.reason ); } else { @@ -61,22 +61,22 @@ function ViewController(options) { this.clientKeyPressHandler = function(ch, key) { // - // Process key presses treating form submit mapped keys special. - // Everything else is forwarded on to the focused View, if any. + // Process key presses treating form submit mapped keys special. + // Everything else is forwarded on to the focused View, if any. // var actionForKey = key ? self.actionKeyMap[key.name] : self.actionKeyMap[ch]; if(actionForKey) { if(_.isNumber(actionForKey.viewId)) { // - // Key works on behalf of a view -- switch focus & submit + // Key works on behalf of a view -- switch focus & submit // self.switchFocus(actionForKey.viewId); self.submitForm(key); } else if(_.isString(actionForKey.action)) { const formData = self.getFocusedView() ? self.getFormData() : { }; self.handleActionWrapper( - Object.assign( { ch : ch, key : key }, formData ), // formData + key info - actionForKey); // actionBlock + Object.assign( { ch : ch, key : key }, formData ), // formData + key info + actionForKey); // actionBlock } } else { if(self.focusedView && self.focusedView.acceptsInput) { @@ -94,7 +94,7 @@ function ViewController(options) { case 'accept' : if(self.focusedView && self.focusedView.submit) { - // :TODO: need to do validation here!!! + // :TODO: need to do validation here!!! var focusedView = self.focusedView; self.validateView(focusedView, function validated(err, newFocusedViewId) { if(err) { @@ -116,9 +116,9 @@ function ViewController(options) { self.emit('submit', this.getFormData(key)); }; - // :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them + // :TODO: replace this in favor of overriding toJSON() for various things such that logging will *never* output them this.getLogFriendlyFormData = function(formData) { - // :TODO: these fields should be part of menu.json sensitiveMembers[] + // :TODO: these fields should be part of menu.json sensitiveMembers[] var safeFormData = _.cloneDeep(formData); if(safeFormData.value.password) { safeFormData.value.password = '*****'; @@ -141,8 +141,8 @@ function ViewController(options) { this.createViewsFromMCI = function(mciMap, cb) { async.each(Object.keys(mciMap), (name, nextItem) => { - const mci = mciMap[name]; - const view = self.mciViewFactory.createFromMCI(mci); + const mci = mciMap[name]; + const view = self.mciViewFactory.createFromMCI(mci); if(view) { if(false === self.noInput) { @@ -160,7 +160,7 @@ function ViewController(options) { }); }; - // :TODO: move this elsewhere + // :TODO: move this elsewhere this.setViewPropertiesFromMCIConf = function(view, conf) { var propAsset; @@ -178,14 +178,14 @@ function ViewController(options) { propValue = asset.resolveSystemStatAsset(conf[propName]); break; - // :TODO: handle @art (e.g. text : @art ...) + // :TODO: handle @art (e.g. text : @art ...) case 'method' : case 'systemMethod' : if('validate' === propName) { - // :TODO: handle propAsset.location for @method script specification + // :TODO: handle propAsset.location for @method script specification if('systemMethod' === propAsset.type) { - // :TODO: implementation validation @systemMethod handling! + // :TODO: implementation validation @systemMethod handling! var methodModule = require(paths.join(__dirname, 'system_view_validate.js')); if(_.isFunction(methodModule[propAsset.asset])) { propValue = methodModule[propAsset.asset]; @@ -200,12 +200,12 @@ function ViewController(options) { // :TODO: clean this code up! } else { if('systemMethod' === propAsset.type) { - // :TODO: + // :TODO: } else { - // local to current module + // local to current module var currentModule = self.client.currentMenuModule; if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { - // :TODO: Fix formData & extraArgs... this all needs general processing + // :TODO: Fix formData & extraArgs... this all needs general processing propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); } } @@ -233,14 +233,14 @@ function ViewController(options) { let initialFocusId = 1; async.each(Object.keys(config.mci || {}), function entry(mci, nextItem) { - const mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? + const mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? if(null === mciMatch) { self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); return; } const viewId = parseInt(mciMatch[2]); - assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used + assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used if(viewId > highestId) { highestId = viewId; @@ -269,7 +269,7 @@ function ViewController(options) { nextItem(null); }, err => { - // default to highest ID if no 'submit' entry present + // default to highest ID if no 'submit' entry present if(!submitId) { var highestIdView = self.getView(highestId); if(highestIdView) { @@ -283,19 +283,19 @@ function ViewController(options) { }); }; - // method for comparing submitted form data to configuration entries + // method for comparing submitted form data to configuration entries this.actionBlockValueComparator = function(formValue, actionValue) { // - // For a match to occur, one of the following must be true: + // For a match to occur, one of the following must be true: // - // * actionValue is a Object: - // a) All key/values must exactly match - // b) value is null; The key (view ID or "argName") must be present - // in formValue. This is a wildcard/any match. - // * actionValue is a Number: This represents a view ID that - // must be present in formValue. - // * actionValue is a string: This represents a view with - // "argName" set that must be present in formValue. + // * actionValue is a Object: + // a) All key/values must exactly match + // b) value is null; The key (view ID or "argName") must be present + // in formValue. This is a wildcard/any match. + // * actionValue is a Number: This represents a view ID that + // must be present in formValue. + // * actionValue is a string: This represents a view with + // "argName" set that must be present in formValue. // if(_.isUndefined(actionValue)) { return false; @@ -307,12 +307,12 @@ function ViewController(options) { } } else { /* - :TODO: support: - value: { - someArgName: [ "key1", "key2", ... ], - someOtherArg: [ "key1, ... ] - } - */ + :TODO: support: + value: { + someArgName: [ "key1", "key2", ... ], + someOtherArg: [ "key1, ... ] + } + */ var actionValueKeys = Object.keys(actionValue); for(var i = 0; i < actionValueKeys.length; ++i) { var viewId = actionValueKeys[i]; @@ -328,8 +328,8 @@ function ViewController(options) { self.client.log.trace( { - formValue : formValue, - actionValue : actionValue + formValue : formValue, + actionValue : actionValue }, 'Action match' ); @@ -362,7 +362,7 @@ function ViewController(options) { var viewValidationListener = self.client.currentMenuModule.menuMethods.viewValidationListener; if(_.isFunction(viewValidationListener)) { if(err) { - err.view = view; // pass along the view that failed + err.view = view; // pass along the view that failed } viewValidationListener(err, function validationComplete(newViewFocusId) { @@ -390,7 +390,7 @@ ViewController.prototype.attachClientEvents = function() { this.client.on('key press', this.clientKeyPressHandler); Object.keys(this.views).forEach(function vid(i) { - // remove, then add to ensure we only have one listener + // remove, then add to ensure we only have one listener self.views[i].removeListener('action', self.viewActionListener); self.views[i].on('action', self.viewActionListener); }); @@ -462,10 +462,10 @@ ViewController.prototype.resetInitialFocus = function() { ViewController.prototype.switchFocus = function(id) { // - // Perform focus switching validation now + // Perform focus switching validation now // - var self = this; - var focusedView = self.focusedView; + var self = this; + var focusedView = self.focusedView; self.validateView(focusedView, function validated(err, newFocusedViewId) { if(err) { @@ -474,10 +474,10 @@ ViewController.prototype.switchFocus = function(id) { } else { self.attachClientEvents(); - // remove from old + // remove from old self.setViewFocusWithEvents(focusedView, false); - // set to new + // set to new self.setViewFocusWithEvents(self.getView(id), true); } }); @@ -486,7 +486,7 @@ ViewController.prototype.switchFocus = function(id) { ViewController.prototype.nextFocus = function() { let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId]; - // find the next view that accepts focus + // find the next view that accepts focus while(nextFocusView && nextFocusView.nextId) { nextFocusView = this.getView(nextFocusView.nextId); if(!nextFocusView || nextFocusView.acceptsFocus) { @@ -531,7 +531,7 @@ ViewController.prototype.redrawAll = function(initialFocusId) { for(var id in this.views) { if(initialFocusId === id) { - continue; // will draw @ focus + continue; // will draw @ focus } this.views[id].redraw(); } @@ -543,9 +543,9 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { assert(_.isObject(options)); assert(_.isObject(options.mciMap)); - var self = this; - var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig; - var initialFocusId = 1; // default to first + var self = this; + var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig; + var initialFocusId = 1; // default to first async.waterfall( [ @@ -574,12 +574,12 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { self.handleActionWrapper(formData, self.client.currentMenuModule.menuConfig); } else { // - // Menus that reference prompts can have a sepcial "submit" block without the - // hassle of by-form-id configurations, etc. + // Menus that reference prompts can have a sepcial "submit" block without the + // hassle of by-form-id configurations, etc. // - // "submit" : [ - // { ... } - // ] + // "submit" : [ + // { ... } + // ] // var menuSubmit = self.client.currentMenuModule.menuConfig.submit; if(!_.isArray(menuSubmit)) { @@ -588,15 +588,15 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { } // - // Locate matching action block + // Locate matching action block // - // :TODO: this is basically the same as for menus -- DRY it up! + // :TODO: this is basically the same as for menus -- DRY it up! for(var c = 0; c < menuSubmit.length; ++c) { var actionBlock = menuSubmit[c]; if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { self.handleActionWrapper(formData, actionBlock); - break; // there an only be one... + break; // there an only be one... } } } @@ -612,13 +612,13 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { promptConfig.actionKeys.forEach(ak => { // - // * 'keys' must be present and be an array of key names - // * If 'viewId' is present, key(s) will focus & submit on behalf - // of the specified view. - // * If 'action' is present, that action will be procesed when - // triggered by key(s) + // * 'keys' must be present and be an array of key names + // * If 'viewId' is present, key(s) will focus & submit on behalf + // of the specified view. + // * If 'action' is present, that action will be procesed when + // triggered by key(s) // - // Ultimately, create a map of key -> { action block } + // Ultimately, create a map of key -> { action block } // if(!_.isArray(ak.keys)) { return; @@ -657,12 +657,12 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { return; } - var self = this; - var formIdKey = options.formId ? options.formId.toString() : '0'; - this.formInitialFocusId = 1; // default to first + var self = this; + var formIdKey = options.formId ? options.formId.toString() : '0'; + this.formInitialFocusId = 1; // default to first var formConfig; - // :TODO: honor options.withoutForm + // :TODO: honor options.withoutForm async.waterfall( [ @@ -671,7 +671,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { formConfig = fc; if(err) { - // non-fatal + // non-fatal self.client.log.trace( { reason : err.message, mci : Object.keys(options.mciMap), formId : formIdKey }, 'Unable to find matching form configuration'); @@ -686,28 +686,28 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { }); }, /* - function applyThemeCustomization(callback) { - formConfig = formConfig || {}; - formConfig.mci = formConfig.mci || {}; - //self.client.currentMenuModule.menuConfig.config = self.client.currentMenuModule.menuConfig.config || {}; + function applyThemeCustomization(callback) { + formConfig = formConfig || {}; + formConfig.mci = formConfig.mci || {}; + //self.client.currentMenuModule.menuConfig.config = self.client.currentMenuModule.menuConfig.config || {}; - //console.log('menu config.....'); - //console.log(self.client.currentMenuModule.menuConfig) + //console.log('menu config.....'); + //console.log(self.client.currentMenuModule.menuConfig) - menuUtil.applyMciThemeCustomization({ - name : self.client.currentMenuModule.menuName, - type : 'menus', - client : self.client, - mci : formConfig.mci, - //config : self.client.currentMenuModule.menuConfig.config, - formId : formIdKey, - }); + menuUtil.applyMciThemeCustomization({ + name : self.client.currentMenuModule.menuName, + type : 'menus', + client : self.client, + mci : formConfig.mci, + //config : self.client.currentMenuModule.menuConfig.config, + formId : formIdKey, + }); - //console.log('after theme...') - //console.log(self.client.currentMenuModule.menuConfig.config) + //console.log('after theme...') + //console.log(self.client.currentMenuModule.menuConfig.config) - callback(null); - }, + callback(null); + }, */ function applyViewConfiguration(callback) { if(_.isObject(formConfig)) { @@ -730,7 +730,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Form submit'); // - // Locate configuration for this form ID + // Locate configuration for this form ID // var confForFormId; if(_.isObject(formConfig.submit[formData.submitId])) { @@ -738,20 +738,20 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { } else if(_.isObject(formConfig.submit['*'])) { confForFormId = formConfig.submit['*']; } else { - // no configuration for this submitId + // no configuration for this submitId self.client.log.debug( { formId : formData.submitId }, 'No configuration for form ID'); return; } // - // Locate a matching action block based on the submitted data + // Locate a matching action block based on the submitted data // for(var c = 0; c < confForFormId.length; ++c) { var actionBlock = confForFormId[c]; if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { self.handleActionWrapper(formData, actionBlock); - break; // there an only be one... + break; // there an only be one... } } }); @@ -766,13 +766,13 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { formConfig.actionKeys.forEach(function akEntry(ak) { // - // * 'keys' must be present and be an array of key names - // * If 'viewId' is present, key(s) will focus & submit on behalf - // of the specified view. - // * If 'action' is present, that action will be procesed when - // triggered by key(s) + // * 'keys' must be present and be an array of key names + // * If 'viewId' is present, key(s) will focus & submit on behalf + // of the specified view. + // * If 'action' is present, that action will be procesed when + // triggered by key(s) // - // Ultimately, create a map of key -> { action block } + // Ultimately, create a map of key -> { action block } // if(!_.isArray(ak.keys)) { return; @@ -822,23 +822,23 @@ ViewController.prototype.formatMCIString = function(format) { ViewController.prototype.getFormData = function(key) { /* - Example form data: - { - id : 0, - submitId : 1, - value : { - "1" : "hurp", - "2" : [ 'a', 'b', ... ], - "3" 2, - "pants" : "no way" - } + Example form data: + { + id : 0, + submitId : 1, + value : { + "1" : "hurp", + "2" : [ 'a', 'b', ... ], + "3" 2, + "pants" : "no way" + } - } - */ + } + */ const formData = { - id : this.formId, - submitId : this.focusedView.id, - value : {}, + id : this.formId, + submitId : this.focusedView.id, + value : {}, }; if(key) { @@ -848,7 +848,7 @@ ViewController.prototype.getFormData = function(key) { let viewData; _.each(this.views, view => { try { - // don't fill forms with static, non user-editable data data + // don't fill forms with static, non user-editable data data if(!view.acceptsInput) { return; } diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 7f30425f..6ca916da 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -1,29 +1,29 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const getServer = require('./listening_server.js').getServer; -const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const User = require('./user.js'); -const userDb = require('./database.js').dbs.user; -const getISOTimestampString = require('./database.js').getISOTimestampString; -const Log = require('./logger.js').log; +// ENiGMA½ +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const getServer = require('./listening_server.js').getServer; +const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const User = require('./user.js'); +const userDb = require('./database.js').dbs.user; +const getISOTimestampString = require('./database.js').getISOTimestampString; +const Log = require('./logger.js').log; -// deps -const async = require('async'); -const crypto = require('crypto'); -const fs = require('graceful-fs'); -const url = require('url'); -const querystring = require('querystring'); +// deps +const async = require('async'); +const crypto = require('crypto'); +const fs = require('graceful-fs'); +const url = require('url'); +const querystring = require('querystring'); const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = - `%USERNAME%: + `%USERNAME%: a password reset has been requested for your account on %BOARDNAME%. - * If this was not you, please ignore this email. - * Otherwise, follow this link: %RESET_URL% + * If this was not you, please ignore this email. + * Otherwise, follow this link: %RESET_URL% `; function getWebServer() { @@ -67,7 +67,7 @@ class WebPasswordReset { }, function generateAndStoreResetToken(user, callback) { // - // Reset "token" is simply HEX encoded cryptographically generated bytes + // Reset "token" is simply HEX encoded cryptographically generated bytes // crypto.randomBytes(256, (err, token) => { if(err) { @@ -77,11 +77,11 @@ class WebPasswordReset { token = token.toString('hex'); const newProperties = { - email_password_reset_token : token, - email_password_reset_token_ts : getISOTimestampString(), + email_password_reset_token : token, + email_password_reset_token_ts : getISOTimestampString(), }; - // we simply place the reset token in the user's properties + // we simply place the reset token in the user's properties user.persistProperties(newProperties, err => { return callback(err, user); }); @@ -107,10 +107,10 @@ class WebPasswordReset { function replaceTokens(s) { return s - .replace(/%BOARDNAME%/g, Config().general.boardName) - .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, user.properties.email_password_reset_token) - .replace(/%RESET_URL%/g, resetUrl) + .replace(/%BOARDNAME%/g, Config().general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace(/%TOKEN%/g, user.properties.email_password_reset_token) + .replace(/%RESET_URL%/g, resetUrl) ; } @@ -120,11 +120,11 @@ class WebPasswordReset { } const message = { - to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, - // from will be filled in - subject : 'Forgot Password', - text : textTemplate, - html : htmlTemplate, + to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, + // from will be filled in + subject : 'Forgot Password', + text : textTemplate, + html : htmlTemplate, }; sendMail(message, (err, info) => { @@ -145,32 +145,32 @@ class WebPasswordReset { } static scheduleEvents(cb) { - // :TODO: schedule ~daily cleanup task + // :TODO: schedule ~daily cleanup task return cb(null); } static registerRoutes(cb) { const webServer = getWebServer(); if(!webServer) { - return cb(null); // no webserver enabled + return cb(null); // no webserver enabled } if(!webServer.instance.isEnabled()) { - return cb(null); // no error, but we're not serving web stuff + return cb(null); // no error, but we're not serving web stuff } [ { - // this is the page displayed to user when they GET it - method : 'GET', - path : '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate - handler : WebPasswordReset.routeResetPasswordGet, + // this is the page displayed to user when they GET it + method : 'GET', + path : '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate + handler : WebPasswordReset.routeResetPasswordGet, }, - // POST handler for performing the actual reset + // POST handler for performing the actual reset { - method : 'POST', - path : '^\\/reset_password$', - handler : WebPasswordReset.routeResetPasswordPost, + method : 'POST', + path : '^\\/reset_password$', + handler : WebPasswordReset.routeResetPasswordPost, } ].forEach(r => { webServer.instance.addRoute(r); @@ -213,10 +213,10 @@ class WebPasswordReset { } static routeResetPasswordGet(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + const webServer = getWebServer(); // must be valid, we just got a req! - const urlParts = url.parse(req.url, true); - const token = urlParts.query && urlParts.query.token; + const urlParts = url.parse(req.url, true); + const token = urlParts.query && urlParts.query.token; if(!token) { return WebPasswordReset.accessDenied(webServer, resp); @@ -224,7 +224,7 @@ class WebPasswordReset { WebPasswordReset.getUserByToken(token, (err, user) => { if(err) { - // assume it's expired + // assume it's expired return webServer.instance.respondWithError(resp, 410, 'Invalid or expired reset link.', 'Expired Link'); } @@ -236,11 +236,11 @@ class WebPasswordReset { (templateData, preprocessFinished) => { const finalPage = templateData - .replace(/%BOARDNAME%/g, config.general.boardName) - .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, token) - .replace(/%RESET_URL%/g, postResetUrl) - ; + .replace(/%BOARDNAME%/g, config.general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace(/%TOKEN%/g, token) + .replace(/%RESET_URL%/g, postResetUrl) + ; return preprocessFinished(null, finalPage); }, @@ -250,7 +250,7 @@ class WebPasswordReset { } static routeResetPasswordPost(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + const webServer = getWebServer(); // must be valid, we just got a req! let bodyData = ''; req.on('data', data => { @@ -266,8 +266,8 @@ class WebPasswordReset { const config = Config(); if(!formData.token || !formData.password || !formData.confirm_password || - formData.password !== formData.confirm_password || - formData.password.length < config.users.passwordMin || formData.password.length > config.users.passwordMax) + formData.password !== formData.confirm_password || + formData.password.length < config.users.passwordMin || formData.password.length > config.users.passwordMax) { return badRequest(); } @@ -282,7 +282,7 @@ class WebPasswordReset { return badRequest(); } - // delete assoc properties - no need to wait for completion + // delete assoc properties - no need to wait for completion user.removeProperty('email_password_reset_token'); user.removeProperty('email_password_reset_token_ts'); @@ -298,15 +298,15 @@ function performMaintenanceTask(args, cb) { const forgotPassExpireTime = args[0] || '24 hours'; - // remove all reset token associated properties older than |forgotPassExpireTime| + // remove all reset token associated properties older than |forgotPassExpireTime| userDb.run( `DELETE FROM user_property - WHERE user_id IN ( - SELECT user_id - FROM user_property - WHERE prop_name = "email_password_reset_token_ts" - AND DATETIME("now") >= DATETIME(prop_value, "+${forgotPassExpireTime}") - ) AND prop_name IN ("email_password_reset_token_ts", "email_password_reset_token");`, + WHERE user_id IN ( + SELECT user_id + FROM user_property + WHERE prop_name = "email_password_reset_token_ts" + AND DATETIME("now") >= DATETIME(prop_value, "+${forgotPassExpireTime}") + ) AND prop_name IN ("email_password_reset_token_ts", "email_password_reset_token");`, err => { if(err) { Log.warn( { error : err.message }, 'Failed deleting old email reset tokens'); @@ -316,5 +316,5 @@ function performMaintenanceTask(args, cb) { ); } -exports.WebPasswordReset = WebPasswordReset; -exports.performMaintenanceTask = performMaintenanceTask; \ No newline at end of file +exports.WebPasswordReset = WebPasswordReset; +exports.performMaintenanceTask = performMaintenanceTask; \ No newline at end of file diff --git a/core/whos_online.js b/core/whos_online.js index edda6c20..28b71703 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -1,25 +1,25 @@ /* jslint node: true */ 'use strict'; -// ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const getActiveNodeList = require('./client_connections.js').getActiveNodeList; -const stringFormat = require('./string_format.js'); +// ENiGMA½ +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getActiveNodeList = require('./client_connections.js').getActiveNodeList; +const stringFormat = require('./string_format.js'); -// deps -const async = require('async'); -const _ = require('lodash'); +// deps +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Who\'s Online', - desc : 'Who is currently online', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.whosonline' + name : 'Who\'s Online', + desc : 'Who is currently online', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.whosonline' }; const MciViewIds = { - OnlineList : 1, + OnlineList : 1, }; exports.getModule = class WhosOnlineModule extends MenuModule { @@ -33,26 +33,26 @@ exports.getModule = class WhosOnlineModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); async.series( [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, + callingMenu : self, + mciMap : mciData.menu, + noInput : true, }; return vc.loadFromMenuConfig(loadOpts, callback); }, function populateList(callback) { - const onlineListView = vc.getView(MciViewIds.OnlineList); - const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}'; - const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In'; - const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A'; - const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height); + const onlineListView = vc.getView(MciViewIds.OnlineList); + const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}'; + const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In'; + const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A'; + const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height); onlineListView.setItems(_.map(onlineList, oe => { if(oe.authenticated) { diff --git a/core/word_wrap.js b/core/word_wrap.js index a42dd2ea..94773283 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -1,15 +1,15 @@ /* jslint node: true */ 'use strict'; -const renderStringLength = require('./string_util.js').renderStringLength; +const renderStringLength = require('./string_util.js').renderStringLength; -// deps -const assert = require('assert'); -const _ = require('lodash'); +// deps +const assert = require('assert'); +const _ = require('lodash'); -exports.wordWrapText = wordWrapText; +exports.wordWrapText = wordWrapText; -const SPACE_CHARS = [ +const SPACE_CHARS = [ ' ', '\f', '\n', '\r', '\v', '​\u00a0', '\u1680', '​\u180e', '\u2000​', '\u2001', '\u2002', '​\u2003', '\u2004', '\u2005', '\u2006​', '\u2007', '\u2008​', '\u2009', '\u200a​', '\u2028', '\u2029​', @@ -22,25 +22,25 @@ function wordWrapText(text, options) { assert(_.isObject(options)); assert(_.isNumber(options.width)); - options.tabHandling = options.tabHandling || 'expand'; - options.tabWidth = options.tabWidth || 4; - options.tabChar = options.tabChar || ' '; + options.tabHandling = options.tabHandling || 'expand'; + options.tabWidth = options.tabWidth || 4; + options.tabChar = options.tabChar || ' '; //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! + // 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 + // :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 word; let c; let renderLen; - let i = 0; - let wordStart = 0; - let result = { wrapped : [ '' ], renderLen : [ 0 ] }; + let i = 0; + let wordStart = 0; + let result = { wrapped : [ '' ], renderLen : [ 0 ] }; function expandTab(column) { const remainWidth = options.tabWidth - (column % options.tabWidth); @@ -56,26 +56,26 @@ function wordWrapText(text, options) { result.firstWrapRange = { start : wordStart, end : wordStart + w.length }; } - result.wrapped[++i] = w; - result.renderLen[i] = renderLen; + result.wrapped[++i] = w; + result.renderLen[i] = renderLen; } else { - result.wrapped[i] += w; + result.wrapped[i] += w; result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; } }); } // - // Some of the way we word wrap is modeled after Sublime Test 3: + // Some of the way we word wrap is modeled after Sublime Test 3: // - // * Sublime Text 3 for example considers spaces after a word - // part of said word. For example, "word " would be wraped - // in it's entirity. + // * Sublime Text 3 for example considers spaces after a word + // part of said word. For example, "word " would be wraped + // in it's entirity. // - // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. - // "\t" may resolve to " " and must fit within the space. + // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. + // "\t" may resolve to " " and must fit within the space. // - // * If a word is ultimately too long to fit, break it up until it does. + // * If a word is ultimately too long to fit, break it up until it does. // while(null !== (m = REGEXP_WORD_WRAP.exec(text))) { word = text.substring(wordStart, REGEXP_WORD_WRAP.lastIndex - 1); @@ -85,7 +85,7 @@ function wordWrapText(text, options) { word += m[0]; } else if('\t' === c) { if('expand' === options.tabHandling) { - // Good info here: http://c-for-dummies.com/blog/?p=424 + // Good info here: http://c-for-dummies.com/blog/?p=424 word += expandTab(result.wrapped[i].length + word.length) + options.tabChar; } else { word += m[0]; From d8c5c8e6347144e91adb573cfbff09c06b4fc054 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 22 Jun 2018 23:25:31 -0600 Subject: [PATCH 155/569] MagiTerm and Undercurrents info to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 82f94ce6..ec879c23 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,12 @@ ENiGMA has been tested with many terminals. However, the following are suggested * [SyncTERM](http://syncterm.bbsdev.net/) * [EtherTerm](https://github.com/M-griffin/EtherTerm) * [NetRunner](http://mysticbbs.com/downloads.html) +* [MagiTerm](https://magickabbs.com/index.php/magiterm/) ## Boards * WQH: :skull: [Xibalba](https://l33t.codes/xibalba-bbs) :skull: (**telnet://xibalba.l33t.codes:44510** or via SSH secure on port 44511) * [fORCE9](http://bbs.force9.org/): (**telnet://bbs.force9.org**) +* [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**) ## Installation From ff48bd74315e9047941edeac53ebedb78e7cefcc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 10:23:21 -0600 Subject: [PATCH 156/569] Add more thanks --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ec879c23..24b2e810 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ Please see the [Quickstart](docs/index.md) for more information. * Avon of [Agency BBS](http://bbs.geek.nz/) and [fsxNet](http://bbs.geek.nz/#fsxNet) for putting up with my experiments to his system * Maskreet of [Throwback BBS](http://www.throwbackbbs.com/) hosting [DoorParty](http://forums.throwbackbbs.com/)! * [Apam](https://github.com/apamment) of [Magicka](https://magickabbs.com/) +* [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)! +* [Whazzit/blocktronics](http://blocktronics.org/tag/whazzit/) for the amazing Mayan ANSI pieces scattered about Xibalba BBS! ## License Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license: From 9af19428c50cc1b7e8b2e7433ddc241d2289e0da Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 10:26:32 -0600 Subject: [PATCH 157/569] Yet more README updates --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 24b2e810..d9f3da0a 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,10 @@ 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 ``` -Please see the [Quickstart](docs/index.md) for more information. +Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/installation/installation-methods.html) in the docs for more information. ## Special Thanks -* [Dave Stephens aka RiPuk](https://github.com/davestephens) for the [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc. +* [Dave Stephens aka RiPuk](https://github.com/davestephens) for the awesome [ENiGMA website](https://enigma-bbs.github.io/) [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc. * [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. 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)! From 812fd28d8232ee98bad6cd26847223881bc67a7c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 10:27:12 -0600 Subject: [PATCH 158/569] Doh! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9f3da0a..e4b76e9c 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/inst Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/installation/installation-methods.html) in the docs for more information. ## Special Thanks -* [Dave Stephens aka RiPuk](https://github.com/davestephens) for the awesome [ENiGMA website](https://enigma-bbs.github.io/) [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc. +* [Dave Stephens aka RiPuk](https://github.com/davestephens) for the awesome [ENiGMA website](https://enigma-bbs.github.io/) and [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc. * [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. 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)! From 4ef1061fc59e3be9ace487acfa37a0ccf200491b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 11:17:21 -0600 Subject: [PATCH 159/569] Use standard list format for message area select --- core/msg_area_list.js | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 27cbcbb1..1b9870bd 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -136,31 +136,20 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }); }, function populateAreaListView(callback) { - const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; - const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; - const areaListView = vc.getView(MciViewIds.AreaList); if(!areaListView) { return callback(Errors.MissingMci('A MenuView compatible MCI code is required')); } - let i = 1; - areaListView.setItems(_.map(self.messageAreas, v => { - return stringFormat(listFormat, { - index : i++, - areaTag : v.area.areaTag, - name : v.area.name, - desc : v.area.desc, - }); - })); - i = 1; - areaListView.setFocusItems(_.map(self.messageAreas, v => { - return stringFormat(focusListFormat, { - index : i++, - areaTag : v.area.areaTag, - name : v.area.name, - desc : v.area.desc, - }); + let i = 1; + areaListView.setItems(self.messageAreas.map(a => { + return { + index : i++, + areaTag : a.area.areaTag, + text : a.area.name, // standard + name : a.area.name, + desc : a.area.desc, + }; })); areaListView.on('index update', areaIndex => { @@ -168,8 +157,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }); areaListView.redraw(); - - callback(null); + return callback(null); } ], function complete(err) { From d1dd13ea990fdd85f5e646567a5d556e6001291f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 11:50:51 -0600 Subject: [PATCH 160/569] Update luciano_blocktronics theme for message area change updates --- art/themes/luciano_blocktronics/CHANGE.ANS | Bin 2035 -> 2159 bytes art/themes/luciano_blocktronics/theme.hjson | 6 ++---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/art/themes/luciano_blocktronics/CHANGE.ANS b/art/themes/luciano_blocktronics/CHANGE.ANS index 885b3cc9bc181f04ca6b953688abc2511344fa5c..056dd1cfc55e7030c282078b2f68d4814b3a6e18 100644 GIT binary patch delta 428 zcmey&|6agZI@-Y6#K79vJXbo}&;ZCaHp;zo2S|YgOrd-z*AmL*g7VEF{E4da9$@8A z{(W@qcNFd`C@9q!_?M~j7x#p_KC0FG{8^cv45oXe4Ts6{I zAq4J!62kb75!MEQ4HIZMA%a1PUM(%8a4wy5qw(rD{2~=?S?l=+XO*1W14P=66x~Pb zDG(e0y+>!Pk+j7IR!e$ipKBFt01HGoVF~D5tDwiF^Egz?qlaYbW%M$-O~M2f@iUuB z`i`%iW1n>Nm7HWi@c%9)&CUO(&2dH4^Fjkd;kCd diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 9a8d3b6c..e97c8e97 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -233,14 +233,12 @@ } messageAreaChangeCurrentArea: { - config: { - listFormat: "|00|15{index} |07- |03{name}" - focusListFormat: "|00|19|15{index} - {name}" - } mci: { VM1: { width: 26 height: 19 + itemFormat: "|00|15{index:.2} |07- |03{name}" + focusItemFormat: "|00|09|15{index:.2} - {name}" } } } From 6d4b8abc9c461d928e8f601d5a4d0cde7ffd1fd1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 18:17:15 -0600 Subject: [PATCH 161/569] Remove ERC: It's not maintained. Can be added to boards as a mod anyway --- art/general/erc.ans | Bin 376 -> 0 bytes config/menu.hjson | 56 -------------- core/erc_client.js | 179 -------------------------------------------- 3 files changed, 235 deletions(-) delete mode 100644 art/general/erc.ans delete mode 100644 core/erc_client.js diff --git a/art/general/erc.ans b/art/general/erc.ans deleted file mode 100644 index d2f336d2255ffac5d5eed3b240d40fe2d6e267ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 376 zcmb1+Hn27^ur@Z$y-j5x9c^r$tLhtK$b}66Wr0>oN1K>h8yn=N1|=&vXC#&=IOk-h z=9MVu7nWw0D3s(YfN3jDpgMBgC>?ER4RnN^G|-bk>l7ePat$%&k_vVVb#^r{P@o<# jGB7kVFf%gaWn^GrWDH=CU;qPQPbXi6Fn31?4^9FA1c{gz diff --git a/config/menu.hjson b/config/menu.hjson index 0a57414c..1ee13d07 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1019,10 +1019,6 @@ value: { command: "R" } action: @menu:mainMenuRumorz } - { - value: { command: "CHAT"} - action: @menu:ercClient - } { value: { command: "BBS"} action: @menu:bbsList @@ -1473,58 +1469,6 @@ } } - ercClient: { - art: erc - module: erc_client - config: { - host: localhost - port: 5001 - bbsTag: CHANGEME - } - - form: { - 0: { - mci: { - MT1: { - width: 79 - height: 21 - mode: preview - autoScroll: true - } - ET3: { - autoScale: false - width: 77 - argName: inputArea - focus: true - submit: true - } - } - - submit: { - *: [ - { - value: { inputArea: null } - action: @method:inputAreaSubmit - } - ] - } - actionKeys: [ - { - keys: [ "tab" ] - } - { - keys: [ "up arrow" ] - action: @method:scrollDown - } - { - keys: [ "down arrow" ] - action: @method:scrollUp - } - ] - } - } - } - bbsList: { desc: Viewing BBS List module: bbs_list diff --git a/core/erc_client.js b/core/erc_client.js deleted file mode 100644 index 018c9372..00000000 --- a/core/erc_client.js +++ /dev/null @@ -1,179 +0,0 @@ -/* jslint node: true */ -'use strict'; - -const MenuModule = require('./menu_module.js').MenuModule; -const stringFormat = require('./string_format.js'); - -// deps -const async = require('async'); -const _ = require('lodash'); -const net = require('net'); - -/* - Expected configuration block example: - - config: { - host: 192.168.1.171 - port: 5001 - bbsTag: SOME_TAG - } - -*/ - -exports.getModule = ErcClientModule; - -exports.moduleInfo = { - name : 'ENiGMA Relay Chat Client', - desc : 'Chat with other ENiGMA BBSes', - author : 'Andrew Pamment', -}; - -var MciViewIds = { - ChatDisplay : 1, - InputArea : 3, -}; - -// :TODO: needs converted to ES6 MenuModule subclass -function ErcClientModule(options) { - MenuModule.prototype.ctorShim.call(this, options); - - const self = this; - this.config = options.menuConfig.config; - - this.chatEntryFormat = this.config.chatEntryFormat || '[{bbsTag}] {userName}: {message}'; - this.systemEntryFormat = this.config.systemEntryFormat || '[*SYSTEM*] {message}'; - - this.finishedLoading = function() { - async.waterfall( - [ - function validateConfig(callback) { - if(_.isString(self.config.host) && - _.isNumber(self.config.port) && - _.isString(self.config.bbsTag)) - { - return callback(null); - } else { - return callback(new Error('Configuration is missing required option(s)')); - } - }, - function connectToServer(callback) { - const connectOpts = { - port : self.config.port, - host : self.config.host, - }; - - const chatMessageView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); - - chatMessageView.setText('Connecting to server...'); - chatMessageView.redraw(); - - self.viewControllers.menu.switchFocus(MciViewIds.InputArea); - - // :TODO: Track actual client->enig connection for optional prevMenu @ final CB - self.chatConnection = net.createConnection(connectOpts.port, connectOpts.host); - - self.chatConnection.on('data', data => { - data = data.toString(); - - if(data.startsWith('ERCHANDSHAKE')) { - self.chatConnection.write(`ERCMAGIC|${self.config.bbsTag}|${self.client.user.username}\r\n`); - } else if(data.startsWith('{')) { - try { - data = JSON.parse(data); - } catch(e) { - return self.client.log.warn( { error : e.message }, 'ERC: Error parsing ERC data from server'); - } - - let text; - try { - if(data.userName) { - // user message - text = stringFormat(self.chatEntryFormat, data); - } else { - // system message - text = stringFormat(self.systemEntryFormat, data); - } - } catch(e) { - return self.client.log.warn( { error : e.message }, 'ERC: chatEntryFormat error'); - } - - chatMessageView.addText(text); - - if(chatMessageView.getLineCount() > 30) { // :TODO: should probably be ChatDisplay.height? - chatMessageView.deleteLine(0); - chatMessageView.scrollDown(); - } - - chatMessageView.redraw(); - self.viewControllers.menu.switchFocus(MciViewIds.InputArea); - } - }); - - self.chatConnection.once('end', () => { - return callback(null); - }); - - self.chatConnection.once('error', err => { - self.client.log.info(`ERC connection error: ${err.message}`); - return callback(new Error('Failed connecting to ERC server!')); - }); - } - ], - err => { - if(err) { - self.client.log.warn( { error : err.message }, 'ERC error'); - } - - self.prevMenu(); - } - ); - }; - - this.scrollHandler = function(keyName) { - const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); - const chatDisplayView = self.viewControllers.menu.getView(MciViewIds.ChatDisplay); - - if('up arrow' === keyName) { - chatDisplayView.scrollUp(); - } else { - chatDisplayView.scrollDown(); - } - - chatDisplayView.redraw(); - inputAreaView.setFocus(true); - }; - - - this.menuMethods = { - inputAreaSubmit : function(formData, extraArgs, cb) { - const inputAreaView = self.viewControllers.menu.getView(MciViewIds.InputArea); - const inputData = inputAreaView.getData(); - - if('/quit' === inputData.toLowerCase()) { - self.chatConnection.end(); - } else { - try { - self.chatConnection.write(`${inputData}\r\n`); - } catch(e) { - self.client.log.warn( { error : e.message }, 'ERC error'); - } - inputAreaView.clearText(); - } - return cb(null); - }, - scrollUp : function(formData, extraArgs, cb) { - self.scrollHandler(formData.key.name); - return cb(null); - }, - scrollDown : function(formData, extraArgs, cb) { - self.scrollHandler(formData.key.name); - return cb(null); - } - }; -} - -require('util').inherits(ErcClientModule, MenuModule); - -ErcClientModule.prototype.mciReady = function(mciData, cb) { - this.standardMCIReadyHandler(mciData, cb); -}; From 611a52e9467d630d4a10f8db84cd4a39a8896e2e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 20:16:44 -0600 Subject: [PATCH 162/569] * Did add a tweak to the concept for height only: autoAdjustHeight can be used where it makes sense * See also: #159 --- core/horizontal_menu_view.js | 11 --------- core/text_view.js | 48 ++---------------------------------- core/vertical_menu_view.js | 31 +++++++++-------------- core/view.js | 44 +++++++++++---------------------- 4 files changed, 29 insertions(+), 105 deletions(-) diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index b7d0aba3..5c83eb16 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -31,17 +31,6 @@ function HorizontalMenuView(options) { return new Array(self.itemSpacing + 1).join(' '); }; - this.performAutoScale = function() { - if(self.autoScale.width) { - var spacer = self.getSpacer(); - var width = self.items.join(spacer).length + (spacer.length * 2); - assert(width <= self.client.term.termWidth - self.position.col); - self.dimens.width = width; - } - }; - - this.performAutoScale(); - this.cachePositions = function() { if(this.positionCacheExpired) { var col = self.position.col; diff --git a/core/text_view.js b/core/text_view.js index 4611821a..2a5c93c5 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -183,67 +183,23 @@ TextView.prototype.getData = function() { TextView.prototype.setText = function(text, redraw) { redraw = _.isBoolean(redraw) ? redraw : true; - if(!_.isString(text)) { + if(!_.isString(text)) { // allow |text| to be numbers/etc. text = text.toString(); } - text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. - - var widthDelta = 0; - if(this.text && this.text !== text) { - widthDelta = Math.abs(renderStringLength(this.text) - renderStringLength(text)); - } - - this.text = text; - + this.text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. if(this.maxLength > 0) { this.text = renderSubstr(this.text, 0, this.maxLength); - //this.text = this.text.substr(0, this.maxLength); } // :TODO: it would be nice to be able to stylize strings with MCI and {special} MCI syntax, e.g. "|BN {UN!toUpper}" this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); - if(this.autoScale.width) { - this.dimens.width = renderStringLength(this.text) + widthDelta; - } - if(redraw) { this.redraw(); } }; -/* -TextView.prototype.setText = function(text) { - if(!_.isString(text)) { - text = text.toString(); - } - - var widthDelta = 0; - if(this.text && this.text !== text) { - widthDelta = Math.abs(this.text.length - text.length); - } - - this.text = text; - - if(this.maxLength > 0) { - this.text = this.text.substr(0, this.maxLength); - } - - this.text = stylizeString(this.text, this.hasFocus ? this.focusTextStyle : this.textStyle); - - //if(this.resizable) { - // this.dimens.width = this.text.length + widthDelta; - //} - - if(this.autoScale.width) { - this.dimens.width = this.text.length + widthDelta; - } - - this.redraw(); -}; -*/ - TextView.prototype.clearText = function() { this.setText(''); }; diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index ea071010..e91e86f2 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -15,11 +15,13 @@ const _ = require('lodash'); exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { - options.cursor = options.cursor || 'hide'; - options.justify = options.justify || 'left'; + options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; MenuView.call(this, options); + this.dimens.width = this.dimens.width || Math.min(15, this.client.term.termWidth - this.position.col); + const self = this; // we want page up/page down by default @@ -30,24 +32,14 @@ function VerticalMenuView(options) { }); } - this.performAutoScale = function() { - if(this.autoScale.height) { - this.dimens.height = (self.items.length * (self.itemSpacing + 1)) - (self.itemSpacing); - this.dimens.height = Math.min(self.dimens.height, self.client.term.termHeight - self.position.row); - } - - if(self.autoScale.width) { - let maxLen = 0; - self.items.forEach( item => { - if(item.text.length > maxLen) { - maxLen = Math.min(item.text.length, self.client.term.termWidth - self.position.col); - } - }); - self.dimens.width = maxLen + 1; + this.autoAdjustHeightIfEnabled = function() { + if(this.autoAdjustHeight) { + this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); + this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); } }; - this.performAutoScale(); + this.autoAdjustHeightIfEnabled(); this.updateViewVisibleItems = function() { self.maxVisibleItems = Math.ceil(self.dimens.height / (self.itemSpacing + 1)); @@ -96,7 +88,7 @@ VerticalMenuView.prototype.redraw = function() { // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such if(this.positionCacheExpired) { - this.performAutoScale(); + this.autoAdjustHeightIfEnabled(); this.updateViewVisibleItems(); this.positionCacheExpired = false; @@ -133,6 +125,7 @@ VerticalMenuView.prototype.setHeight = function(height) { VerticalMenuView.super_.prototype.setHeight.call(this, height); this.positionCacheExpired = true; + this.autoAdjustHeight = false; }; VerticalMenuView.prototype.setPosition = function(pos) { @@ -158,7 +151,7 @@ VerticalMenuView.prototype.setFocusItemIndex = function(index) { }; this.positionCacheExpired = false; // skip standard behavior - this.performAutoScale(); + this.autoAdjustHeightIfEnabled(); } this.redraw(); diff --git a/core/view.js b/core/view.js index 702be949..e32b402b 100644 --- a/core/view.js +++ b/core/view.js @@ -37,21 +37,16 @@ function View(options) { enigAssert(_.isObject(options)); enigAssert(_.isObject(options.client)); - var self = this; + this.client = options.client; + this.cursor = options.cursor || 'show'; + this.cursorStyle = options.cursorStyle || 'default'; - this.client = options.client; - - this.cursor = options.cursor || 'show'; - this.cursorStyle = options.cursorStyle || 'default'; - - this.acceptsFocus = options.acceptsFocus || false; - this.acceptsInput = options.acceptsInput || false; - - this.position = { x : 0, y : 0 }; - this.dimens = { height : 1, width : 0 }; - - this.textStyle = options.textStyle || 'normal'; - this.focusTextStyle = options.focusTextStyle || this.textStyle; + this.acceptsFocus = options.acceptsFocus || false; + this.acceptsInput = options.acceptsInput || false; + this.autoAdjustHeight = _.get(options, 'dimens.height') ? false : _.get(options, 'autoAdjustHeight', true); + this.position = { x : 0, y : 0 }; + this.textStyle = options.textStyle || 'normal'; + this.focusTextStyle = options.focusTextStyle || this.textStyle; if(options.id) { this.setId(options.id); @@ -61,15 +56,8 @@ function View(options) { this.setPosition(options.position); } - if(_.isObject(options.autoScale)) { - this.autoScale = options.autoScale; - } else { - this.autoScale = { height : true, width : true }; - } - if(options.dimens) { this.setDimension(options.dimens); - this.autoScale = { height : false, width : false }; } else { this.dimens = { width : options.width || 0, @@ -105,7 +93,7 @@ function View(options) { }; this.hideCusor = function() { - self.client.term.rawWrite(ansi.hideCursor()); + this.client.term.rawWrite(ansi.hideCursor()); }; this.restoreCursor = function() { @@ -148,25 +136,23 @@ View.prototype.setPosition = function(pos) { View.prototype.setDimension = function(dimens) { enigAssert(_.isObject(dimens) && _.isNumber(dimens.height) && _.isNumber(dimens.width)); - - this.dimens = dimens; - this.autoScale = { height : false, width : false }; + this.dimens = dimens; + this.autoAdjustHeight = false; }; View.prototype.setHeight = function(height) { height = parseInt(height) || 1; height = Math.min(height, this.client.term.termHeight); - this.dimens.height = height; - this.autoScale.height = false; + this.dimens.height = height; + this.autoAdjustHeight = false; }; View.prototype.setWidth = function(width) { width = parseInt(width) || 1; width = Math.min(width, this.client.term.termWidth); - this.dimens.width = width; - this.autoScale.width = false; + this.dimens.width = width; }; View.prototype.getSGR = function() { From 469c08b0f2d02cf2072fc0a1f32d11961a8d07ce Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 21:01:36 -0600 Subject: [PATCH 163/569] Note on auto scaling --- WHATSNEW.md | 1 + 1 file changed, 1 insertion(+) diff --git a/WHATSNEW.md b/WHATSNEW.md index 27f9380d..be4e90cd 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -12,6 +12,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * You can now set the `sort` property on a menu to sort items. If `true` items are sorted by `text`. If the value is a string, it represents the key in menu objects to sort by. * Hot-reload of configuration files such as menu.hjson, config.hjson, your themes.hjson, etc.: When a file is saved, it will be hot-reloaded into the running system * Note that any custom modules should make use of the new Config.get() method. +* The old concept of `autoScale` has been removed. See https://github.com/NuSkooler/enigma-bbs/issues/166 ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. From 5bb4f9b903169aed986956cd9773303c93c4ab2a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 21:02:16 -0600 Subject: [PATCH 164/569] Fix archive util config reload --- core/archive_util.js | 11 ++++++++--- core/config.js | 7 ++++++- core/system_events.js | 8 ++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index c0076dba..32425eaa 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -6,6 +6,7 @@ const Config = require('./config.js').get; const stringFormat = require('./string_format.js'); const Errors = require('./enig_error.js').Errors; const resolveMimeType = require('./mime_util.js').resolveMimeType; +const Events = require('./events.js'); // base/modules const fs = require('graceful-fs'); @@ -58,9 +59,13 @@ module.exports = class ArchiveUtil { } init() { - // - // Load configuration - // + this.reloadConfig(); + Events.on(Events.getSystemEvents().ConfigChanged, () => { + this.reloadConfig(); + }); + } + + reloadConfig() { const config = Config(); if(_.has(config, 'archives.archivers')) { Object.keys(config.archives.archivers).forEach(archKey => { diff --git a/core/config.js b/core/config.js index f85ffe2a..1e69b6ef 100644 --- a/core/config.js +++ b/core/config.js @@ -94,7 +94,12 @@ function init(configPath, options, cb) { const reCachedPath = paths.join(fileRoot, fileName); ConfigCache.getConfig(reCachedPath, (err, config) => { if(!err) { - mergeValidateAndFinalize(config); + mergeValidateAndFinalize(config, err => { + if(!err) { + const Events = require('./events.js'); + Events.emit(Events.getSystemEvents().ConfigChanged); + } + }); } }); }; diff --git a/core/system_events.js b/core/system_events.js index 95316c95..42aa0700 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -6,10 +6,10 @@ module.exports = { ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } - ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // { themeId } - ConfigChanged : 'codes.l33t.enigma.system.config_changed', - MenusChanged : 'codes.l33t.enigma.system.menus_changed', - PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', + ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } + ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson) + MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) + PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson) // User - includes { user, ...} NewUser : 'codes.l33t.enigma.system.new_user', From 359f21914fff47d6e04a2fedbb34e783acdd13bd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 21:02:33 -0600 Subject: [PATCH 165/569] Defualt width --- core/button_view.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/button_view.js b/core/button_view.js index 63de435a..8e8d6ebc 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -14,6 +14,8 @@ function ButtonView(options) { options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); TextView.call(this, options); + + this.dimens.width = this.dimens.width || Math.min(10, this.client.term.termWidth - this.position.col); } util.inherits(ButtonView, TextView); From ff3ab38a7aec01351bf6f81c9b3ffa940b758522 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 21:03:05 -0600 Subject: [PATCH 166/569] Fix moveOrCopyFileWithCollisions() for fse-extra with kludge --- core/file_util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_util.js b/core/file_util.js index 64167771..fdea4e45 100644 --- a/core/file_util.js +++ b/core/file_util.js @@ -51,7 +51,7 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { if(err) { // for some reason fs-extra copy doesn't pass err.code // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST - if('EEXIST' === err.code || 'copy' === operation) { + if('EEXIST' === err.code || 'dest already exists.' === err.message) { renameIndex += 1; return cb(null); // keep trying } From 0cfd45d8a92e991d29853b703e56273ded988abd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 21:03:32 -0600 Subject: [PATCH 167/569] Fix undefined ref if we fail to copy/move a upload file --- core/upload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/upload.js b/core/upload.js index 87bdcb4a..7dd5dd51 100644 --- a/core/upload.js +++ b/core/upload.js @@ -386,7 +386,7 @@ exports.getModule = class UploadModule extends MenuModule { 'Failed moving physical upload file', { error : err.message, fileName : newEntry.fileName, source : src, dest : dst } ); - if(dst !== finalPath) { + if(!err && dst !== finalPath) { // name changed; ajust before persist newEntry.fileName = paths.basename(finalPath); } From 3aa23db306a04f689d225f33f8cfe0de9f75900e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 23 Jun 2018 22:23:22 -0600 Subject: [PATCH 168/569] initDefaultWidht() for various views --- core/button_view.js | 2 +- core/edit_text_view.js | 2 ++ core/mask_edit_text_view.js | 2 ++ core/multi_line_edit_text_view.js | 2 ++ core/spinner_menu_view.js | 2 ++ core/toggle_menu_view.js | 2 ++ core/vertical_menu_view.js | 2 +- core/view.js | 4 ++++ 8 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/button_view.js b/core/button_view.js index 8e8d6ebc..edb32e12 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -15,7 +15,7 @@ function ButtonView(options) { TextView.call(this, options); - this.dimens.width = this.dimens.width || Math.min(10, this.client.term.termWidth - this.position.col); + this.initDefaultWidth(); } util.inherits(ButtonView, TextView); diff --git a/core/edit_text_view.js b/core/edit_text_view.js index 05868e92..db01b9f5 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -19,6 +19,8 @@ function EditTextView(options) { TextView.call(this, options); + this.initDefaultWidth(); + this.cursorPos = { row : 0, col : 0 }; this.clientBackspace = function() { diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index 6cdf40b0..dbc782a0 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -35,6 +35,8 @@ function MaskEditTextView(options) { TextView.call(this, options); + this.initDefaultWidth(); + this.cursorPos = { x : 0 }; this.patternArrayPos = 0; diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 1d16e201..bd49a73f 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -97,6 +97,8 @@ function MultiLineEditTextView(options) { View.call(this, options); + this.initDefaultWidth(); + var self = this; // diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index e6be1232..7c38b195 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -17,6 +17,8 @@ function SpinnerMenuView(options) { MenuView.call(this, options); + this.initDefaultWidth(); + var self = this; /* diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index ae163bd7..aadfe9c3 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -14,6 +14,8 @@ function ToggleMenuView (options) { MenuView.call(this, options); + this.initDefaultWidth(); + var self = this; /* diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index e91e86f2..1837b718 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -20,7 +20,7 @@ function VerticalMenuView(options) { MenuView.call(this, options); - this.dimens.width = this.dimens.width || Math.min(15, this.client.term.termWidth - this.position.col); + this.initDefaultWidth(); const self = this; diff --git a/core/view.js b/core/view.js index e32b402b..7d3c5693 100644 --- a/core/view.js +++ b/core/view.js @@ -100,6 +100,10 @@ function View(options) { //this.client.term.write(ansi.setCursorStyle(this.cursorStyle)); this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor()); }; + + this.initDefaultWidth = function(width = 15) { + this.dimens.width = this.dimens.width || Math.min(width, this.client.term.termWidth - this.position.col); + }; } util.inherits(View, events.EventEmitter); From c75845413436f76fda18f74b0c76616bc2584409 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 25 Jun 2018 18:08:41 -0600 Subject: [PATCH 169/569] Fix empty filename check --- core/upload.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/upload.js b/core/upload.js index 7dd5dd51..a2f2c9ea 100644 --- a/core/upload.js +++ b/core/upload.js @@ -92,16 +92,16 @@ exports.getModule = class UploadModule extends MenuModule { // validation validateNonBlindFileName : (fileName, cb) => { - fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. - if(0 === fileName.length) { - return cb(new Error('Invalid filename')); - } - if(0 === fileName.length) { return cb(new Error('Filename cannot be empty')); } - // At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused + fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. + if(0 === fileName.length) { // sanatize nuked everything? + return cb(new Error('Invalid filename')); + } + + // At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused ;-( if(/^[0-9].*$/.test(fileName)) { return cb(new Error('Invalid filename')); } From 851da9e8c8ba04571aea6673dc4a5d0d8a75da10 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 25 Jun 2018 18:09:32 -0600 Subject: [PATCH 170/569] Do not require MCI configurations in theme.hjson for theme.hjson to apply #167 --- core/theme.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/core/theme.js b/core/theme.js index 6d2bcff6..ebdcf1ca 100644 --- a/core/theme.js +++ b/core/theme.js @@ -139,14 +139,18 @@ function getMergedTheme(menuConfig, promptConfig, theme) { }; function getFormKeys(fromObj) { - return _.remove(_.keys(fromObj), function pred(k) { - return !isNaN(k); // remove all non-numbers - }); + // remove all non-numbers + return _.remove(_.keys(fromObj), k => !isNaN(k)); } function mergeMciProperties(dest, src) { - Object.keys(src).forEach(function mciEntry(mci) { - _.mergeWith(dest[mci], src[mci], mciCustomizer); + Object.keys(src).forEach(mci => { + if(dest[mci]) { + _.mergeWith(dest[mci], src[mci], mciCustomizer); + } else { + // theme contains MCI not in menu; bring in as-is + dest[mci] = src[mci]; + } }); } @@ -186,11 +190,9 @@ function getMergedTheme(menuConfig, promptConfig, theme) { if(_.isObject(form.mci)) { // non-explicit: no MCI code(s) key assumed since we found 'mci' directly under form ID applyThemeMciBlock(form.mci, menuTheme, formKey); - } else { - const menuMciCodeKeys = _.remove(_.keys(form), function pred(k) { - return k === k.toUpperCase(); // remove anything not uppercase - }); + // remove anything not uppercase + const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase()); menuMciCodeKeys.forEach(function mciKeyEntry(mciKey) { let applyFrom; From 7cdb362c32eb911cb946938a4cc4b3ac19836ce7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 25 Jun 2018 18:10:27 -0600 Subject: [PATCH 171/569] Limit filename length in luciano_blocktronics theme --- art/themes/luciano_blocktronics/theme.hjson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index e97c8e97..abb15955 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -492,7 +492,7 @@ fileBaseListEntries: { config: { hashTagsSep: "|08, |07" - browseInfoFormat10: "|00|10{fileName} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}" + browseInfoFormat10: "|00|10{fileName:<.44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}" browseInfoFormat11: "|00|15{areaName}" browseInfoFormat12: "|00|07{hashTags}" browseInfoFormat13: "|00|07{estReleaseYear}" @@ -593,7 +593,7 @@ newScanFileBaseList: { config: { hashTagsSep: "|08, |07" - browseInfoFormat10: "|00|10{fileName} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}" + browseInfoFormat10: "|00|10{fileName:<44} |08- |03{byteSize!sizeWithoutAbbr} |11{byteSize!sizeAbbr} |08- |03uploaded |11{uploadTimestamp}" browseInfoFormat11: "|00|15{areaName}" browseInfoFormat12: "|00|07{hashTags}" browseInfoFormat13: "|00|07{estReleaseYear}" From c99074ba2743934bfe3a2532a02c6cee41019b61 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 25 Jun 2018 18:10:43 -0600 Subject: [PATCH 172/569] Remove empty MCI {} now that #167 is fixed --- config/menu.hjson | 52 +---------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index 1ee13d07..4f88f6df 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -818,17 +818,6 @@ ] focusItemIndex: 1 } - - // :TODO: these can be removed once the hack is not required: - TL10: {} - TL11: {} - TL12: {} - TL13: {} - TL14: {} - TL15: {} - TL16: {} - TL17: {} - TL18: {} } submit: { @@ -902,17 +891,6 @@ "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: [ @@ -2576,17 +2554,6 @@ ] focusItemIndex: 1 } - - // :TODO: these can be removed once the hack is not required: - TL10: {} - TL11: {} - TL12: {} - TL13: {} - TL14: {} - TL15: {} - TL16: {} - TL17: {} - TL18: {} } submit: { @@ -2668,17 +2635,6 @@ "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: [ @@ -3152,13 +3108,7 @@ } 1: { - mci: { - TL1: {} - TL2: {} - TL3: {} - MT4: {} - TL10: {} - } + mci: { } } // file details entry From 2fa9d6a3c28518e40417fc44bc6e597a2b80d654 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 25 Jun 2018 19:09:08 -0600 Subject: [PATCH 173/569] Clean up a couple DEP0013 spots --- core/file_base_area.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 7d7ce6fb..faa2dc6a 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -280,12 +280,20 @@ function attemptSetEstimatedReleaseDate(fileEntry) { } // a simple log proxy for when we call from oputil.js -function logDebug(obj, msg) { +const logDebug = (obj, msg) => { if(Log) { Log.debug(obj, msg); } } +const logError = (obj, msg) => { + if(Log) { + Log.error(obj, msg); + } else { + console.error(`${msg}: ${JSON.stringify(obj)}`); + } +} + function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { async.waterfall( [ @@ -709,8 +717,12 @@ function scanFile(filePath, options, iterator, cb) { const nextChunk = () => { fs.read(fd, buffer, 0, chunkSize, null, (err, bytesRead) => { if(err) { - fs.close(fd); - return readErrorCallIter(err, callback); + return fs.close(fd, closeErr => { + if(closeErr) { + logError( { filePath, error : err.message }, 'Failed to close file'); + } + return readErrorCallIter(err, callback); + }); } if(0 === bytesRead) { @@ -729,8 +741,12 @@ function scanFile(filePath, options, iterator, cb) { } stepInfo.step = 'hash_finish'; - fs.close(fd); - return callIter(callback); + return fs.close(fd, closeErr => { + if(closeErr) { + logError( { filePath, error : err.message }, 'Failed to close file'); + } + return callIter(callback); + }); } stepInfo.bytesProcessed += bytesRead; From fa100c2da154d51c15c9296ead1ddb03511681f8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 25 Jun 2018 19:25:07 -0600 Subject: [PATCH 174/569] Resolve TODO: set cwd when launching doors; allow user to set in config --- core/abracadabra.js | 1 + core/door.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 48b80d65..676600e4 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -149,6 +149,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { const exeInfo = { cmd : this.config.cmd, + cwd : this.config.cwd, // null/undefined=path of |cwd| args : this.config.args, io : this.config.io || 'stdio', encoding : this.config.encoding || this.client.term.outputEncoding, diff --git a/core/door.js b/core/door.js index 79d79b22..5ad5855a 100644 --- a/core/door.js +++ b/core/door.js @@ -7,6 +7,7 @@ const { Errors } = require('./enig_error.js'); const pty = require('node-pty'); const decode = require('iconv-lite').decode; const createServer = require('net').createServer; +const paths = require('path'); module.exports = class Door { constructor(client) { @@ -55,12 +56,15 @@ module.exports = class Door { return cb(Errors.UnexpectedState('Socket server is not running')); } + const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd); + const formatObj = { dropFile : exeInfo.dropFile, dropFilePath : exeInfo.dropFilePath, node : exeInfo.node.toString(), srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', userId : this.client.user.userId.toString(), + cwd : cwd, }; const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); @@ -68,7 +72,7 @@ module.exports = class Door { const door = pty.spawn(exeInfo.cmd, args, { cols : this.client.term.termWidth, rows : this.client.term.termHeight, - // :TODO: cwd + cwd : cwd, env : exeInfo.env, encoding : null, // we want to handle all encoding ourself }); From a8e27b4cf5dde683dd52e32f8a0af9499d1fc2d5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 Jun 2018 22:39:36 -0600 Subject: [PATCH 175/569] Tabs -> Spaces --- util/exiftool2desc.js | 140 +++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/util/exiftool2desc.js b/util/exiftool2desc.js index 4b3b350f..5299bcb5 100755 --- a/util/exiftool2desc.js +++ b/util/exiftool2desc.js @@ -20,96 +20,96 @@ const FILETYPE_HANDLERS = {}; [ 'MP4', 'MOV', 'AVI', 'MKV', 'MPG', 'MPEG', 'M4V', 'WMV' ].forEach(ext => FILETYPE_HANDLERS[ext] = videoFile); function audioFile(metadata) { - // nothing if we don't know at least the author or title - if(!metadata.author && !metadata.title) { - return; - } - - let desc = `${metadata.artist||'Unknown Artist'} - ${metadata.title||'Unknown'} (`; - if(metadata.year) { - desc += `${metadata.year}, `; - } - desc += `${metadata.audioBitrate})`; - return desc; + // nothing if we don't know at least the author or title + if(!metadata.author && !metadata.title) { + return; + } + + let desc = `${metadata.artist||'Unknown Artist'} - ${metadata.title||'Unknown'} (`; + if(metadata.year) { + desc += `${metadata.year}, `; + } + desc += `${metadata.audioBitrate})`; + return desc; } function videoFile(metadata) { - return `${metadata.fileType} video(${metadata.imageSize}px, ${metadata.duration}, ${metadata.audioBitsPerSample}/${metadata.audioSampleRate} audio)`; + return `${metadata.fileType} video(${metadata.imageSize}px, ${metadata.duration}, ${metadata.audioBitsPerSample}/${metadata.audioSampleRate} audio)`; } function documentFile(metadata) { - // nothing if we don't know at least the author or title - if(!metadata.author && !metadata.title) { - return; - } + // nothing if we don't know at least the author or title + if(!metadata.author && !metadata.title) { + return; + } - let result = metadata.author || ''; - if(result) { - result += ' - '; - } - result += metadata.title || 'Unknown Title'; - return result; + let result = metadata.author || ''; + if(result) { + result += ' - '; + } + result += metadata.title || 'Unknown Title'; + return result; } function imageFile(metadata) { - let desc = `${metadata.fileType} image (`; - if(metadata.animationIterations) { - desc += 'Animated, '; - } - desc += `${metadata.imageSize}px`; - const created = moment(metadata.createdate); - if(created.isValid()) { - desc += `, ${created.format('YYYY')})`; - } else { - desc += ')'; - } - return desc; + let desc = `${metadata.fileType} image (`; + if(metadata.animationIterations) { + desc += 'Animated, '; + } + desc += `${metadata.imageSize}px`; + const created = moment(metadata.createdate); + if(created.isValid()) { + desc += `, ${created.format('YYYY')})`; + } else { + desc += ')'; + } + return desc; } function main() { - const argv = exports.argv = require('minimist')(process.argv.slice(2), { - alias : { - h : 'help', - v : 'version', - } - }); + const argv = exports.argv = require('minimist')(process.argv.slice(2), { + alias : { + h : 'help', + v : 'version', + } + }); - if(argv.version) { - console.info(TOOL_VERSION); - return 0; - } + if(argv.version) { + console.info(TOOL_VERSION); + return 0; + } - if(0 === argv._.length || argv.help) { - console.info('usage: exiftool2desc.js [--version] [--help] PATH'); - return 0; - } + if(0 === argv._.length || argv.help) { + console.info('usage: exiftool2desc.js [--version] [--help] PATH'); + return 0; + } - const path = argv._[0]; + const path = argv._[0]; - fs.readFile(path, (err, data) => { - if(err) { - return -1; - } + fs.readFile(path, (err, data) => { + if(err) { + return -1; + } - exiftool.metadata(data, (err, metadata) => { - if(err) { - return -1; - } + exiftool.metadata(data, (err, metadata) => { + if(err) { + return -1; + } - const handler = FILETYPE_HANDLERS[metadata.fileType]; - if(!handler) { - return -1; - } - - const info = handler(metadata); - if(!info) { - return -1; - } + const handler = FILETYPE_HANDLERS[metadata.fileType]; + if(!handler) { + return -1; + } - console.info(info); - return 0; - }); - }); + const info = handler(metadata); + if(!info) { + return -1; + } + + console.info(info); + return 0; + }); + }); } return main(); \ No newline at end of file From b6e7ecb193cf39ed33757b067a625ea439bf999c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 Jun 2018 22:40:13 -0600 Subject: [PATCH 176/569] Atari Disk Image (.atr) support via (modified for now; PR open) atr tool --- core/config.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 1e69b6ef..e425bdfa 100644 --- a/core/config.js +++ b/core/config.js @@ -319,7 +319,7 @@ function getDefaultConfig() { // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html cmd : 'xdms', args : [ 'f', '{filePath}' ] - } + }, }, fileTypes : { @@ -460,6 +460,12 @@ function getDefaultConfig() { ext : '.dms', shortDescUtil : 'XDMS2Desc', longDescUtil : 'XDMS2LongDesc', + }, + { + desc : 'SIO2PC Atari Disk Image', + sig : '9602', // 16bit sum of "NICKATARI" + ext : '.atr', + archiveHandler : 'Atr', } ] }, @@ -581,6 +587,26 @@ function getDefaultConfig() { cmd : 'tar', args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ], } + }, + + Atr : { + decompress : { + cmd : 'atr', + args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}' ] + }, + list : { + cmd : 'atr', + args : [ '{archivePath}', 'ls', '-la1' ], + entryMatch : '^[rwxs-]{5}\\s+([0-9]+)\\s\\([0-9\\s]+\\)\\s([^\\r\\n\\s]*)(?:[^\\r\\n]+)?$', + }, + extract : { + cmd : 'atr', + // :TODO: If text, we need to ensure to normalize + // :TODO: can only do a single file & need full path. May need e.g. {fileName}, '{extractPath}\{fileName}' ??? + // ....create a small wrapper .js: atrex.js --extract {archivePath} --to {extractPath} + // note: -l converts Atari 0x9b line feeds to 0x0a; not ideal if we're dealing with a binary of course. + args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}', '{fileList}' ] + } } }, }, From 2cef12f47e4d090e1c1fa1a1ba3ed80d9ca6d5d8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 Jun 2018 23:04:03 -0600 Subject: [PATCH 177/569] * Fix file descriptor leak * Allow noWatch init (e.g. for oputil) --- core/archive_util.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index 32425eaa..6a58f935 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -50,19 +50,21 @@ module.exports = class ArchiveUtil { } // singleton access - static getInstance() { + static getInstance(noWatch = false) { if(!archiveUtil) { archiveUtil = new ArchiveUtil(); - archiveUtil.init(); + archiveUtil.init(noWatch); } return archiveUtil; } - init() { + init(noWatch = false) { this.reloadConfig(); - Events.on(Events.getSystemEvents().ConfigChanged, () => { - this.reloadConfig(); - }); + if(!noWatch) { + Events.on(Events.getSystemEvents().ConfigChanged, () => { + this.reloadConfig(); + }); + } } reloadConfig() { @@ -147,6 +149,10 @@ module.exports = class ArchiveUtil { */ detectType(path, cb) { + const closeFile = (fd) => { + fs.close(fd, () => { /* sadface */ }); + }; + fs.open(path, 'r', (err, fd) => { if(err) { return cb(err); @@ -155,6 +161,7 @@ module.exports = class ArchiveUtil { const buf = Buffer.alloc(this.longestSignature); fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { if(err) { + closeFile(fd); return cb(err); } @@ -176,6 +183,7 @@ module.exports = class ArchiveUtil { }); }); + closeFile(fd); return cb(archFormat ? null : Errors.General('Unknown type'), archFormat); }); }); From 35c5c5dd0fcca45a79ddb3382230a82091adba88 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 29 Jun 2018 23:04:31 -0600 Subject: [PATCH 178/569] Init ArchiveUtil with noWatch --- core/oputil/oputil_common.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index 13f9ec91..90328b07 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -67,6 +67,11 @@ function initConfigAndDatabases(cb) { function initDb(callback) { db.initializeDatabases(callback); }, + function initArchiveUtil(callback) { + // ensure we init ArchiveUtil without events + require('../../core/archive_util').getInstance(true); // true=noWatch + return callback(null); + } ], err => { return cb(err); From c894ed17ecdcf4e9b0a480d783e6407faa6c7081 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 30 Jun 2018 11:55:13 -0600 Subject: [PATCH 179/569] Convert line endings when using Atr --- core/config.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/config.js b/core/config.js index e425bdfa..92016731 100644 --- a/core/config.js +++ b/core/config.js @@ -601,11 +601,8 @@ function getDefaultConfig() { }, extract : { cmd : 'atr', - // :TODO: If text, we need to ensure to normalize - // :TODO: can only do a single file & need full path. May need e.g. {fileName}, '{extractPath}\{fileName}' ??? - // ....create a small wrapper .js: atrex.js --extract {archivePath} --to {extractPath} // note: -l converts Atari 0x9b line feeds to 0x0a; not ideal if we're dealing with a binary of course. - args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}', '{fileList}' ] + args : [ '{archivePath}', 'x', '-a', '-l', '-o', '{extractPath}', '{fileList}' ] } } }, From 7c741481e1222ce11cf055ea367d5357f438f7de Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 30 Jun 2018 13:03:08 -0600 Subject: [PATCH 180/569] Much improved getDescFromFileName() --- core/file_base_area.js | 45 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index faa2dc6a..384d4e36 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -916,11 +916,48 @@ function scanFileAreaForChanges(areaInfo, options, iterator, cb) { } function getDescFromFileName(fileName) { - // :TODO: this method could use some more logic to really be nice. - const ext = paths.extname(fileName); - const name = paths.basename(fileName, ext); + // + // Example filenames: + // + // input desired output + // ----------------------------------------------------------------------------------------- + // Nintendo_Power_Issue_011_March-April_1990.cbr Nintendo Power Issue 011 March-April 1990 + // Atari User Issue 3 (July 1985).pdf Atari User Issue 3 (July 1985) + // Out_Of_The_Shadows_010__1953_.cbz Out Of The Shadows 010 1953 + // ABC A Basic Compiler 1.03 [pro].atr ABC A Basic Compiler 1.03 [pro] + // 221B Baker Street v1.0 (1987)(Datasoft)(Side B)[cr The Bounty].zip 221B Baker Street v1.0 (1987)(Datasoft)(Side B)[cr the Bounty] + // + // See also: + // * https://scenerules.org/ + // - return _.upperFirst(name.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' ')); + const ext = paths.extname(fileName); + const name = paths.basename(fileName, ext); + const asIsRe = /([vV]?(?:[0-9]{1,4})(?:\.[0-9]{1,4})+[-+]?(?:[a-z]{1,4})?)|(Incl\.)|(READ\.NFO)/g; + + const normalize = (s) => { + return _.upperFirst(s.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' ')); + }; + + let out = ''; + let m; + let pos; + do { + pos = asIsRe.lastIndex; + m = asIsRe.exec(name); + if(m) { + if(m.index > pos) { + out += normalize(name.slice(pos, m.index)); + } + out += m[0]; // as-is + } + } while(0 != asIsRe.lastIndex); + + if(pos < name.length) { + out += normalize(name.slice(pos)); + } + + return out; } // From a9e2551ae55859e0d2274740218356dae38293d4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 30 Jun 2018 16:52:54 -0600 Subject: [PATCH 181/569] Spelling --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e4b76e9c..0b70f71b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * [Door support](docs/modding/door-servers.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) support! * [Bunyan](https://github.com/trentm/node-bunyan) logging * [Message networks](docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be set to read-only viewable using a built in Gopher server! - * [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/servers/web-server.md). Legacy X/Y/Z modem also supported! + * [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/servers/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! * ANSI support in the Full Screen Editor (FSE), file descriptions, and so on diff --git a/docs/index.md b/docs/index.md index a49f8ab1..b19f1b7d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,6 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * [Door support](docs/doors.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) support! * [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 - * [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) inspired 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! * ANSI support in the Full Screen Editor (FSE), file descriptions, and so on From f4afe9847d8f7e1487d99b127ac56be5541dd243 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 2 Jul 2018 19:32:27 -0600 Subject: [PATCH 182/569] Handle bad config.hjson at startup/re-cache: output to stderr --- core/bbs.js | 22 ++++++++++++---------- core/config.js | 2 ++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 86134c38..4436eb02 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -75,10 +75,10 @@ function main() { configPathSupplied = null; // make non-fatal; we'll go with defaults } } else { - console.error(err.toString()); + console.error(err.message); } } - callback(err); + return callback(err); }); }, function initSystem(callback) { @@ -91,14 +91,16 @@ function main() { } ], function complete(err) { - // note this is escaped: - fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { - console.info(FULL_COPYRIGHT); - if(!err) { - console.info(banner); - } - console.info('System started!'); - }); + if(!err) { + // note this is escaped: + fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { + console.info(FULL_COPYRIGHT); + if(!err) { + console.info(banner); + } + console.info('System started!'); + }); + } if(err) { console.error('Error initializing: ' + util.inspect(err)); diff --git a/core/config.js b/core/config.js index 92016731..6d5251a3 100644 --- a/core/config.js +++ b/core/config.js @@ -100,6 +100,8 @@ function init(configPath, options, cb) { Events.emit(Events.getSystemEvents().ConfigChanged); } }); + } else { + console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); // eslint-disable-line no-console } }); }; From 0f6924a9165a066ff248d846d782092b32046509 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 14:59:15 -0600 Subject: [PATCH 183/569] XY MCI code now properly c reates vanilla view for later lookup/retrieval --- core/mci_view_factory.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 86246d45..85c440e5 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -3,6 +3,7 @@ // ENiGMA½ const TextView = require('./text_view.js').TextView; +const View = require('./view.js').View; const EditTextView = require('./edit_text_view.js').EditTextView; const ButtonView = require('./button_view.js').ButtonView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; @@ -186,6 +187,10 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new KeyEntryView(options); break; + case 'XY' : + view = new View(options); + break; + default : options.text = getPredefinedMCIValue(this.client, mci.code); if(_.isString(options.text)) { From 8922bb66834c0e9cb66da5caad30ed00fa63c11b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 17:58:38 -0600 Subject: [PATCH 184/569] Catch exception if we try to log due to config.hjson error --- core/config_cache.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/config_cache.js b/core/config_cache.js index e8e06f14..62c6bb55 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -62,7 +62,11 @@ module.exports = new class ConfigCache parsed = hjson.parse(data); this.cache.set(path, parsed); } catch(e) { - require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); + try { + require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); + } catch(ignored) { + // nothing - we may be failing to parse the config in which we can't log here! + } return cb(e); } From e24511678d92949548da4718421495f73ebffb51 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:42:59 -0600 Subject: [PATCH 185/569] Ensure 'tag' exists --- core/scanner_tossers/ftn_bso.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 2e08b6e2..f20e61ea 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -114,7 +114,7 @@ function FTNMessageScanTossModule() { this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) { ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper return _.findKey(Config().messageNetworks.ftn.areas, areaConf => { - return areaConf.tag.toUpperCase() === ftnAreaTag; + return _.isString(areaConf.tag) && areaConf.tag.toUpperCase() === ftnAreaTag; }); }; From fbffe2873c7398e8dff4f78edc4d8028e988b62a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:43:40 -0600 Subject: [PATCH 186/569] * ansiPrepOptions support for displaying art * simplify proxy of options along call path * general improvements --- core/theme.js | 94 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/core/theme.js b/core/theme.js index ebdcf1ca..c7a2b06d 100644 --- a/core/theme.js +++ b/core/theme.js @@ -12,6 +12,7 @@ const ViewController = require('./view_controller.js').ViewController; const Errors = require('./enig_error.js').Errors; const ErrorReasons = require('./enig_error.js').ErrorReasons; const Events = require('./events.js'); +const AnsiPrep = require('./ansi_prep.js'); const fs = require('graceful-fs'); const paths = require('path'); @@ -511,26 +512,47 @@ function displayThemeArt(options, cb) { assert(_.isObject(options.client)); assert(_.isString(options.name)); - getThemeArt(options, (err, artInfo) => { - if(err) { - return cb(err); + async.waterfall( + [ + function getArt(callback) { + return getThemeArt(options, callback); + }, + function prepWork(artInfo, callback) { + if(_.isObject(options.ansiPrepOptions)) { + AnsiPrep( + artInfo.data, + options.ansiPrepOptions, + (err, prepped) => { + if(!err && prepped) { + artInfo.data = prepped; + return callback(null, artInfo); + } + } + ); + } else { + return callback(null, artInfo); + } + }, + function disp(artInfo, callback) { + const displayOpts = { + sauce : artInfo.sauce, + font : options.font, + trailingLF : options.trailingLF, + }; + art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { + return callback(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } ); + }); + } + ], + (err, artData) => { + return cb(err, artData); } - // :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 } ); - }); - }); + ); } function displayThemedPrompt(name, client, options, cb) { - const useTempViewController = _.isUndefined(options.viewController); + const usingTempViewController = _.isUndefined(options.viewController); async.waterfall( [ @@ -549,8 +571,8 @@ function displayThemedPrompt(name, client, options, cb) { // doing so messes things up -- most terminals that support font // changing can only display a single font at at time. // + const dispOptions = Object.assign( {}, options, promptConfig.options ); // :TODO: We can use term detection to do nifty things like avoid this kind of kludge: - const dispOptions = Object.assign( {}, promptConfig.options ); if(!options.clearScreen) { dispOptions.font = 'not_really_a_font!'; // kludge :) } @@ -582,28 +604,29 @@ function displayThemedPrompt(name, client, options, cb) { client.term.rawWrite(ansi.queryPos()); }, function createMCIViews(promptConfig, artInfo, callback) { - const tempViewController = useTempViewController ? new ViewController( { client : client } ) : options.viewController; + const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController; const loadOpts = { - promptName : name, - mciMap : artInfo.mciMap, - config : promptConfig, + promptName : name, + mciMap : artInfo.mciMap, + config : promptConfig, + submitNotify : options.submitNotify, }; - tempViewController.loadFromPromptConfig(loadOpts, () => { - return callback(null, artInfo, tempViewController); + assocViewController.loadFromPromptConfig(loadOpts, () => { + return callback(null, artInfo, assocViewController); }); }, - function pauseForUserInput(artInfo, tempViewController, callback) { + function pauseForUserInput(artInfo, assocViewController, callback) { if(!options.pause) { - return callback(null, artInfo, tempViewController); + return callback(null, artInfo, assocViewController); } client.waitForKeyPress( () => { - return callback(null, artInfo, tempViewController); + return callback(null, artInfo, assocViewController); }); }, - function clearPauseArt(artInfo, tempViewController, callback) { + function clearPauseArt(artInfo, assocViewController, callback) { if(options.clearPrompt) { if(artInfo.startRow && artInfo.height) { client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); @@ -615,19 +638,19 @@ function displayThemedPrompt(name, client, options, cb) { } } - return callback(null, tempViewController); + return callback(null, assocViewController, artInfo); } ], - (err, tempViewController) => { + (err, assocViewController, artInfo) => { if(err) { client.log.warn( { error : err.message }, `Failed displaying "${name}" prompt` ); } - if(tempViewController && useTempViewController) { - tempViewController.detachClientEvents(); + if(assocViewController && usingTempViewController) { + assocViewController.detachClientEvents(); } - return cb(null); + return cb(null, artInfo); } ); } @@ -668,14 +691,7 @@ function displayThemedAsset(assetSpec, client, options, cb) { return cb(new Error('Asset not found: ' + assetSpec)); } - // :TODO: just use simple merge of options -> displayOptions - var dispOpts = { - name : artAsset.asset, - client : client, - font : options.font, - trailingLF : options.trailingLF, - }; - + const dispOpts = Object.assign( {}, options, { client, name : artAsset.asset } ); switch(artAsset.type) { case 'art' : displayThemeArt(dispOpts, function displayed(err, artData) { From 3e06e2fa6bf0a7da0cd455f4e8187b138091ae56 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:45:14 -0600 Subject: [PATCH 187/569] + promptForInput() support * removeViewController() support --- core/menu_module.js | 56 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 2a3bf23b..8c7516f6 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -194,6 +194,13 @@ exports.MenuModule = class MenuModule extends PluginModule { return vc; } + removeViewController(name) { + if(this.viewControllers[name]) { + this.viewControllers[name].detachClientEvents(); + delete this.viewControllers[name]; + } + } + detachViewControllers() { Object.keys(this.viewControllers).forEach( name => { this.viewControllers[name].detachClientEvents(); @@ -370,21 +377,56 @@ exports.MenuModule = class MenuModule extends PluginModule { return theme.displayThemedPause(this.client, cb); } - /* - :TODO: this needs quite a bit of work - but would be nice: promptForInput(..., (err, formData) => ... ) - promptForInput(formName, name, options, cb) { + promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) { if(!cb && _.isFunction(options)) { cb = options; options = {}; } - options.viewController = this.viewControllers[formName]; + options.viewController = this.addViewController( + formName, + new ViewController( { client : this.client, formId } ) + ); - this.optionalMoveToPosition(options.position); + options.trailingLF = _.get(options, 'trailingLF', false); - return theme.displayThemedPrompt(name, this.client, options, cb); + let prevVc; + if(prevFormName) { + prevVc = this.viewControllers[prevFormName]; + if(prevVc) { + prevVc.setFocus(false); + } + } + + //let artHeight; + options.submitNotify = () => { + if(prevVc) { + prevVc.setFocus(true); + } + this.removeViewController(formName); + if(options.clearAtSubmit) { + this.optionalMoveToPosition(position); + if(options.clearWidth) { + this.client.term.rawWrite(`${ansi.reset()}${' '.repeat(options.clearWidth)}`); + } else { + // :TODO: handle multi-rows via artHeight + this.client.term.rawWrite(ansi.eraseLine()); + } + } + }; + + options.viewController.setFocus(true); + + this.optionalMoveToPosition(position); + theme.displayThemedPrompt(promptName, this.client, options, (err, artInfo) => { + /* + if(artInfo) { + artHeight = artInfo.height; + } + */ + return cb(err, artInfo); + }); } - */ setViewText(formName, mciId, text, appendMultiLine) { const view = this.viewControllers[formName].getView(mciId); From ab9ffc715a7740a364b0f0e589527dc0e708b8ae Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:46:40 -0600 Subject: [PATCH 188/569] * Cleaner action block discovery for 'submit' * Allow "embedded" prompts to use form action matching --- core/view_controller.js | 87 +++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/core/view_controller.js b/core/view_controller.js index 0df09084..cd0dd3b0 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -32,15 +32,17 @@ function ViewController(options) { this.formId = options.formId || 0; this.mciViewFactory = new MCIViewFactory(this.client); // :TODO: can this not be a singleton? this.noInput = _.isBoolean(options.noInput) ? options.noInput : false; - this.actionKeyMap = {}; // // Small wrapper/proxy around handleAction() to ensure we do not allow // input/additional actions queued while performing an action // - this.handleActionWrapper = function(formData, actionBlock) { + this.handleActionWrapper = function(formData, actionBlock, cb) { if(self.waitActionCompletion) { + if(cb) { + return cb(null); + } return; // ignore until this is finished! } @@ -56,6 +58,9 @@ function ViewController(options) { } self.waitActionCompletion = false; + if(cb) { + return cb(null); + } }); }; @@ -570,34 +575,51 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { self.on('submit', function promptSubmit(formData) { self.client.log.trace( { formData : self.getLogFriendlyFormData(formData) }, 'Prompt submit'); + const doSubmitNotify = () => { + if(options.submitNotify) { + options.submitNotify(); + } + }; + + const handleIt = (fd, conf) => { + self.handleActionWrapper(fd, conf, () => { + doSubmitNotify(); + }); + }; + if(_.isString(self.client.currentMenuModule.menuConfig.action)) { - self.handleActionWrapper(formData, self.client.currentMenuModule.menuConfig); + handleIt(formData, self.client.currentMenuModule.menuConfig); } else { // - // Menus that reference prompts can have a sepcial "submit" block without the + // Menus that reference prompts can have a special "submit" block without the // hassle of by-form-id configurations, etc. // // "submit" : [ // { ... } // ] // - var menuSubmit = self.client.currentMenuModule.menuConfig.submit; - if(!_.isArray(menuSubmit)) { - self.client.log.debug('No configuration to handle submit'); - return; + const menuConfig = self.client.currentMenuModule.menuConfig; + let submitConf; + if(Array.isArray(menuConfig.submit)) { // standalone prompts)) { + submitConf = menuConfig.submit; + } else { + // look for embedded prompt configurations - using their own form ID within the menu + submitConf = + _.get(menuConfig, [ 'form', formData.id, 'submit', formData.submitId ]) || + _.get(menuConfig, [ 'form', formData.id, 'submit', '*' ]); } - // - // Locate matching action block - // - // :TODO: this is basically the same as for menus -- DRY it up! - for(var c = 0; c < menuSubmit.length; ++c) { - var actionBlock = menuSubmit[c]; + if(!Array.isArray(submitConf)) { + doSubmitNotify(); + return self.client.log.debug('No configuration to handle submit'); + } - if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { - self.handleActionWrapper(formData, actionBlock); - break; // there an only be one... - } + // locate any matching action block + const actionBlock = submitConf.find(actionBlock => _.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)); + if(actionBlock) { + handleIt(formData, actionBlock); + } else { + doSubmitNotify(); } } }); @@ -732,27 +754,18 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { // // Locate configuration for this form ID // - var confForFormId; - if(_.isObject(formConfig.submit[formData.submitId])) { - confForFormId = formConfig.submit[formData.submitId]; - } else if(_.isObject(formConfig.submit['*'])) { - confForFormId = formConfig.submit['*']; - } else { - // no configuration for this submitId - self.client.log.debug( { formId : formData.submitId }, 'No configuration for form ID'); - return; + const confForFormId = + _.get(formConfig, [ 'submit', formData.submitId ]) || + _.get(formConfig, [ 'submit', '*' ]); + + if(!Array.isArray(confForFormId)) { + return self.client.log.debug( { formId : formData.submitId }, 'No configuration for form ID'); } - // - // Locate a matching action block based on the submitted data - // - for(var c = 0; c < confForFormId.length; ++c) { - var actionBlock = confForFormId[c]; - - if(_.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)) { - self.handleActionWrapper(formData, actionBlock); - break; // there an only be one... - } + // locate a matching action block, if any + const actionBlock = confForFormId.find(actionBlock => _.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)); + if(actionBlock) { + self.handleActionWrapper(formData, actionBlock); } }); From 514edb984fdd9a4078b49ccb4baa5f8409c806dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:47:58 -0600 Subject: [PATCH 189/569] + userHasDeleteRights() + deleteMessage() --- core/message.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/core/message.js b/core/message.js index 53b82e73..768cd116 100644 --- a/core/message.js +++ b/core/message.js @@ -137,6 +137,32 @@ module.exports = class Message { return null !== _.get(this, 'meta.System.remote_from_user', null); } + /* + :TODO: finish me + static checkUserHasDeleteRights(user, messageIdOrUuid, cb) { + const isMessageId = _.isNumber(messageIdOrUuid); + const getMetaName = isMessageId ? 'getMetaValuesByMessageId' : 'getMetaValuesByMessageUuid'; + + Message[getMetaName](messageIdOrUuid, 'System', Message.SystemMetaNames.LocalToUserID, (err, localUserId) => { + if(err) { + return cb(err); + } + + // expect single value + if(!_.isString(localUserId)) { + return cb(Errors.Invalid(`Invalid ${Message.SystemMetaNames.LocalToUserID} value: ${localUserId}`)); + } + + localUserId = parseInt(localUserId); + }); + } + */ + + userHasDeleteRights(user) { + const messageLocalUserId = parseInt(this.meta.System[Message.SystemMetaNames.LocalToUserID]); + return (this.isPrivate() && user.userId === messageLocalUserId) || user.isSysOp(); + } + static get WellKnownAreaTags() { return WELL_KNOWN_AREA_TAGS; } @@ -660,6 +686,21 @@ module.exports = class Message { ); } + deleteMessage(requestingUser, cb) { + if(!this.userHasDeleteRights(requestingUser)) { + return cb(Errors.AccessDenied('User does not have rights to delete this message')); + } + + msgDb.run( + `DELETE FROM message + WHERE message_uuid = ?;`, + [ this.messageUuid ], + err => { + return cb(err); + } + ); + } + // :TODO: FTN stuff doesn't have any business here getFTNQuotePrefix(source) { source = source || 'fromUserName'; From 2408d4c5c0017d5c9287b8e47343b0ad54d1b372 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:48:35 -0600 Subject: [PATCH 190/569] Fixes around render cache --- core/menu_view.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/menu_view.js b/core/menu_view.js index 73e56a4a..d9016153 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -102,6 +102,8 @@ MenuView.prototype.setItems = function(items) { if(this.complexItems) { this.itemFormat = this.itemFormat || '{text}'; } + + this.invalidateRenderCache(); } }; @@ -110,11 +112,19 @@ MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { return item && item[focusItem ? 'focus' : 'standard']; }; +MenuView.prototype.removeRenderCacheItem = function(index) { + delete this.renderCache[index]; +}; + MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { this.renderCache[index] = this.renderCache[index] || {}; this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; +MenuView.prototype.invalidateRenderCache = function() { + this.renderCache = {}; +}; + MenuView.prototype.setSort = function(sort) { if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { return; @@ -152,6 +162,8 @@ MenuView.prototype.removeItem = function(index) { this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); } + this.removeRenderCacheItem(index); + this.positionCacheExpired = true; }; From 0d55daabe48ea3142b8c96d4e88d813b439c5193 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:49:03 -0600 Subject: [PATCH 191/569] Ability to delete private (aka inbox) messages --- core/msg_list.js | 162 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 130 insertions(+), 32 deletions(-) diff --git a/core/msg_list.js b/core/msg_list.js index a4eb18aa..05b48ab9 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -7,6 +7,8 @@ const ViewController = require('./view_controller.js').ViewContro const messageArea = require('./message_area.js'); const stringFormat = require('./string_format.js'); const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; +const Errors = require('./enig_error.js').Errors; +const Message = require('./message.js'); // deps const async = require('async'); @@ -14,7 +16,7 @@ const _ = require('lodash'); const moment = require('moment'); /* - Available listFormat/focusListFormat members (VM1): + Available listFormat/focusListFormat members for |msgList| msgNum : Message number to : To username/handle @@ -22,22 +24,28 @@ const moment = require('moment'); subj : Subject ts : Message mod timestamp (format with config.dateTimeFormat) newIndicator : New mark/indicator (config.newIndicator) - - MCI codes: - - VM1 : Message list - TL2 : Message info 1: { msgNumSelected, msgNumTotal } */ - exports.moduleInfo = { name : 'Message List', desc : 'Module for listing/browsing available messages', author : 'NuSkooler', }; +const FormIds = { + allViews : 0, + delPrompt : 1, +}; + const MciViewIds = { - msgList : 1, // VM1 - msgInfo1 : 2, // TL2 + allViews : { + msgList : 1, // VM1 - see above + delPromptXy : 2, // %XY2, e.g: delete confirmation + customRangeStart : 10, // Everything |msgList| has plus { msgNumSelected, msgNumTotal } + }, + + delPrompt: { + prompt : 1, + } }; exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) { @@ -51,7 +59,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( this.menuMethods = { selectMessage : (formData, extraArgs, cb) => { - if(MciViewIds.msgList === formData.submitId) { + if(MciViewIds.allViews.msgList === formData.submitId) { this.initialFocusIndex = formData.value.message; const modOpts = { @@ -91,10 +99,40 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( return cb(null); } }, - fullExit : (formData, extraArgs, cb) => { this.menuResult = { fullExit : true }; return this.prevMenu(cb); + }, + deleteSelected : (formData, extraArgs, cb) => { + if(MciViewIds.allViews.msgList != formData.submitId) { + return cb(null); + } + const messageIndex = _.get(formData, 'value.message'); + return this.promptDeleteMessageConfirm(messageIndex, cb); + }, + deleteMessageYes : (formData, extraArgs, cb) => { + const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); + this.enableMessageListIndexUpdates(msgListView); + if(this.selectedMessageForDelete) { + this.selectedMessageForDelete.deleteMessage(this.client.user, err => { + if(err) { + this.client.log.error(`Failed to delete message: ${this.selectedMessageForDelete.messageUuid}`); + } else { + this.client.log.info(`User deleted message: ${this.selectedMessageForDelete.messageUuid}`); + this.config.messageList.splice(msgListView.focusedItemIndex, 1); + this.updateMessageNumbersAfterDelete(msgListView.focusedItemIndex); + msgListView.setItems(this.config.messageList); + } + this.selectedMessageForDelete = null; + msgListView.redraw(); + return cb(null); + }); + } else { + return cb(null); + } + }, + deleteMessageNo : (formData, extraArgs, cb) => { + return cb(null); } }; } @@ -130,6 +168,17 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( super.leave(); } + populateCustomLabelsForSelected(selectedIndex) { + const formatObj = Object.assign( + { + msgNumSelected : (selectedIndex + 1), + msgNumTotal : this.config.messageList.length, + }, + this.config.messageList[selectedIndex] // plus, all the selected message props + ); + return this.updateCustomViewTextsWithFilter('allViews', MciViewIds.allViews.customRangeStart, formatObj); + } + mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { @@ -183,7 +232,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( function updateMessageListObjects(callback) { const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); const newIndicator = self.menuConfig.config.newIndicator || '*'; - const regIndicator = new Array(newIndicator.length + 1).join(' '); // fill with space to avoid draw issues + const regIndicator = ' '.repeat(newIndicator.length); // fill with space to avoid draw issues let msgNum = 1; self.config.messageList.forEach( (listItem, index) => { @@ -200,19 +249,10 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( }); return callback(null); }, - function populateList(callback) { - const msgListView = vc.getView(MciViewIds.msgList); - // :TODO: replace with standard custom info MCI - msgNumSelected, msgNumTotal, areaName, areaDesc, confName, confDesc, ... - const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; - + function populateAndDrawViews(callback) { + const msgListView = vc.getView(MciViewIds.allViews.msgList); msgListView.setItems(self.config.messageList); - - msgListView.on('index update', idx => { - self.setViewText( - 'allViews', - MciViewIds.msgInfo1, - stringFormat(messageInfo1Format, { msgNumSelected : (idx + 1), msgNumTotal : self.config.messageList.length } )); - }); + self.enableMessageListIndexUpdates(msgListView); if(self.initialFocusIndex > 0) { // note: causes redraw() @@ -221,14 +261,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( msgListView.redraw(); } - return callback(null); - }, - function drawOtherViews(callback) { - const messageInfo1Format = self.menuConfig.config.messageInfo1Format || '{msgNumSelected} / {msgNumTotal}'; - self.setViewText( - 'allViews', - MciViewIds.msgInfo1, - stringFormat(messageInfo1Format, { msgNumSelected : self.initialFocusIndex + 1, msgNumTotal : self.config.messageList.length } )); + self.populateCustomLabelsForSelected(self.initialFocusIndex || 0); return callback(null); }, ], @@ -255,4 +288,69 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( getMenuResult() { return this.menuResult; } + + enableMessageListIndexUpdates(msgListView) { + msgListView.on('index update', idx => this.populateCustomLabelsForSelected(idx) ); + } + + updateMessageNumbersAfterDelete(startIndex) { + // all index -= 1 from this point on. + for(let i = startIndex; i < this.config.messageList.length; ++i) { + const msgItem = this.config.messageList[i]; + msgItem.msgNum -= 1; + msgItem.text = `${msgItem.msgNum} - ${msgItem.subject} from ${msgItem.fromUserName}`; // default text + } + } + + promptDeleteMessageConfirm(messageIndex, cb) { + const messageInfo = this.config.messageList[messageIndex]; + if(!_.isObject(messageInfo)) { + return cb(Errors.Invalid(`Invalid message index: ${messageIndex}`)); + } + + // :TODO: create static userHasDeleteRights() that takes id || uuid that doesn't require full msg load + this.selectedMessageForDelete = new Message(); + this.selectedMessageForDelete.load( { uuid : messageInfo.messageUuid }, err => { + if(err) { + this.selectedMessageForDelete = null; + return cb(err); + } + + if(!this.selectedMessageForDelete.userHasDeleteRights(this.client.user)) { + this.selectedMessageForDelete = null; + return cb(Errors.AccessDenied('User does not have rights to delete this message')); + } + + // user has rights to delete -- prompt/confirm then proceed + return this.promptConfirmDelete(cb); + }); + } + + promptConfirmDelete(cb) { + const promptXyView = this.viewControllers.allViews.getView(MciViewIds.allViews.delPromptXy); + if(!promptXyView) { + return cb(Errors.MissingMci(`Missing prompt XY${MciViewIds.allViews.delPromptXy} MCI`)); + } + + const promptOpts = { + clearAtSubmit : true, + }; + if(promptXyView.dimens.width) { + promptOpts.clearWidth = promptXyView.dimens.width; + } + + return this.promptForInput( + { + formName : 'delPrompt', + formId : FormIds.delPrompt, + promptName : this.config.deleteMessageFromListPrompt || 'deleteMessageFromListPrompt', + prevFormName : 'allViews', + position : promptXyView.position, + }, + promptOpts, + err => { + return cb(err); + } + ); + } }; From c7739c0503400ee99650042fc2b0d5982a888621 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 Jul 2018 18:49:26 -0600 Subject: [PATCH 192/569] * Clean up themes a bit more with new features * Theme related work for deleting user messages --- .../luciano_blocktronics/MSGDELPMPT.ANS | Bin 0 -> 197 bytes art/themes/luciano_blocktronics/MSGLIST.ANS | Bin 2249 -> 2264 bytes art/themes/luciano_blocktronics/NEWMSGS.ANS | Bin 2312 -> 2312 bytes art/themes/luciano_blocktronics/theme.hjson | 9 +++++++++ config/prompt.hjson | 13 +++++++++++++ 5 files changed, 22 insertions(+) create mode 100644 art/themes/luciano_blocktronics/MSGDELPMPT.ANS diff --git a/art/themes/luciano_blocktronics/MSGDELPMPT.ANS b/art/themes/luciano_blocktronics/MSGDELPMPT.ANS new file mode 100644 index 0000000000000000000000000000000000000000..74713b1f548e89ed4a303e8a421d6310661ea6ee GIT binary patch literal 197 zcmb1+Hn29dHZia^HpsQIvXYKA$W@SzHa5#mNzF+uNma;AEiO(>PnC`~1gWqG$^n&{ z<*J7G8Un?@G?!GcW2m#Mfq??`fRTZrg@Ku|fhz+810!Pq10#?I0%1=lUxhGtM+gs2 F0stf3A(j9D literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/MSGLIST.ANS b/art/themes/luciano_blocktronics/MSGLIST.ANS index e1e0ddf86c304f43a5d013de1c28e9a6b0bbad54..b67b31a6746fb981bbc768d3b07c89fb036c5cbb 100644 GIT binary patch delta 72 zcmX>pctdc)VLd|w>1cyo>1cCj>1ac1V}sn=x49H<-v$XPNJkqR<*G(R8UY#RKs9ET Y8z0FsF^Bk=Zf0U$$jVqSc?pLq09xl3V*mgE delta 49 zcmca1cv5h}VPPZbXme-jXoFk@>1ac1V}sn=wJXZ6n9I;WI@%ytI@;WM<5_DC07>o!w*UYD delta 21 ccmeAW>JXZ6n9E2y+T2+>+8|e9<5_DC07;_;r~m)} diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index abb15955..6a36ae05 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -209,10 +209,12 @@ messageAreaMessageList: { config: { dateTimeFormat: ddd MMM Do + allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" } mci: { VM1: { height: 14 + width: 70 itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}" focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" } @@ -261,13 +263,18 @@ mailMenuInbox: { config: { dateTimeFormat: ddd MMM Do + allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" } mci: { VM1: { height: 14 + width: 70 itemFormat: "|00|15{msgNum:>4} |03{subject:<28.27} |11{fromUserName:<20.20} |03{ts} |15{newIndicator}" focusItemFormat: "|00|19|15{msgNum:>4} {subject:<28.27} {fromUserName:<20.20} {ts} {newIndicator}" } + XY2: { + width: 30 + } } } @@ -468,10 +475,12 @@ newScanMessageList: { config: { dateTimeFormat: ddd MMM Do + allViewsInfoFormat10: "|00|15{msgNumSelected:>4.4} |08/ |15{msgNumTotal:<4.4}" } mci: { VM1: { height: 14 + width: 70 itemFormat: "|00|15 {msgNum:<5.5}|03{subject:<28.27} |15{fromUserName:<20.20} {ts}" focusItemFormat: "|00|19> |15{msgNum:<5.5}{subject:<28.27} {fromUserName:<20.20} {ts}" } diff --git a/config/prompt.hjson b/config/prompt.hjson index b6bf6691..b165fbd5 100644 --- a/config/prompt.hjson +++ b/config/prompt.hjson @@ -133,6 +133,19 @@ } }, + deleteMessageFromListPrompt: { + art: MSGDELPMPT + mci: { + TM1: { + argName: promptValue + items: [ "yes", "no" ] + focus: true + hotKeys: { Y: 0, N: 1 } + hotKeySubmit: true + } + } + } + "newAreaPostPrompt" : { "art" : "message_area_new_post", "mci" : { From ad71b2385380499dc38dc117b5b6a848c3da9fc6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 5 Jul 2018 18:39:35 -0600 Subject: [PATCH 193/569] Updates for latest changes --- config/menu.hjson | 244 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 242 insertions(+), 2 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index 4f88f6df..8973c833 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1713,6 +1713,10 @@ value: { command: "D" } action: @menu:messageAreaSetNewScanDate } + { + value: { command: "S" } + action: @menu:messageSearch + } { value: 1 action: @menu:messageArea @@ -1720,6 +1724,114 @@ ] } + messageSearch: { + desc: Message Search + module: message_base_search + art: MSEARCH + config: { + messageListMenu: messageAreaSearchMessageList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + SM3: { + argName: confTag + } + SM4: { + argName: areaTag + } + ET5: { + argName: toUserName + maxLength: @config:users.usernameMax + } + ET6: { + argName: fromUserName + maxLength: @config:users.usernameMax + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaSearchMessageList: { + desc: Message Search + module: msg_list + art: MSRCHLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageSearchNoResults: { + desc: Message Search + art: MSRCNORES + options: { + pause: true + } + } + messageAreaChangeCurrentConference: { art: CCHANGE module: msg_conf_list @@ -2411,7 +2523,7 @@ messageAreaTag: private_mail } form: { - 0: { + 0: { // main list mci: { VM1: { focus: true @@ -2432,8 +2544,26 @@ keys: [ "escape", "q", "shift + q" ] action: @systemMethod:prevMenu } + { + keys: [ "delete", "d", "shift + d" ] + action: @method:deleteSelected + } ] } + 1: { // delete prompt form + submit: { + *: [ + { + value: { promptValue: 0 } + action: @method:deleteMessageYes + } + { + value: { promptValue: 1 } + action: @method:deleteMessageNo + } + ] + } + } } } @@ -2486,9 +2616,119 @@ value: { menuOption: "P" } action: @menu:fileBaseSetNewScanDate } + { + value: { menuOption: "E" } + action: @menu:fileBaseExportListFilter + } ] } + fileBaseExportListFilter: { + module: file_base_search + // :TODO: fixme: + art: FSEARCH + config: { + fileBaseListEntriesMenu: fileBaseExportList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename" + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseExportList: { + module: file_base_user_list_export + art: FBLISTEXP + options: { + pause: true + } + config: { + templates: { + entry: file_list_entry.asc + } + } + form: { + 0: { + mci: { + TL1: { } + TL2: { } + } + } + } + } + + fileBaseExportListNoResults: { + desc: Browsing Files + art: FBNORES + options: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + fileBaseSetNewScanDate: { module: set_newscan_date desc: File Base @@ -2694,7 +2934,7 @@ actionKeys: [ { - keys: [ "escape" ] + keys: [ "escape", "q", "shift + q" ] action: @systemMethod:prevMenu } ] From 53cda734e5c7e1d0ee70e1e7e2e22b541556410f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 5 Jul 2018 20:35:38 -0600 Subject: [PATCH 194/569] Minor bugfix --- core/msg_list.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/msg_list.js b/core/msg_list.js index 05b48ab9..d4798012 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -132,6 +132,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } }, deleteMessageNo : (formData, extraArgs, cb) => { + const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); + this.enableMessageListIndexUpdates(msgListView); return cb(null); } }; From 539c25ea83136617840c0afd3451756a8422887b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 Jul 2018 15:20:45 -0600 Subject: [PATCH 195/569] Add ability to omit message and/or file area tags from new scan by config 'omitFileAreaTags' and 'omitMessageAreaTags' arrays --- core/misc_util.js | 8 ++++++++ core/new_scan.js | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/misc_util.js b/core/misc_util.js index 633cc967..6e477821 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -12,6 +12,7 @@ exports.valueWithDefault = valueWithDefault; exports.resolvePath = resolvePath; exports.getCleanEnigmaVersion = getCleanEnigmaVersion; exports.getEnigmaUserAgent = getEnigmaUserAgent; +exports.valueAsArray = valueAsArray; function isProduction() { var env = process.env.NODE_ENV || 'dev'; @@ -49,4 +50,11 @@ function getEnigmaUserAgent() { const nodeVer = process.version.substr(1); // remove 'v' prefix return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; +} + +function valueAsArray(value) { + if(!value) { + return []; + } + return Array.isArray(value) ? value : [ value ]; } \ No newline at end of file diff --git a/core/new_scan.js b/core/new_scan.js index 974519af..4d20ac18 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -10,6 +10,7 @@ const FileEntry = require('./file_entry.js'); const FileBaseFilters = require('./file_base_filter.js'); const Errors = require('./enig_error.js').Errors; const { getAvailableFileAreaTags } = require('./file_base_area.js'); +const { valueAsArray } = require('./misc_util.js'); // deps const _ = require('lodash'); @@ -52,6 +53,7 @@ exports.getModule = class NewScanModule extends MenuModule { this.currentScanAux = {}; // :TODO: Make this conf/area specific: + // :TODO: Use newer custom info format - TL10+ const config = this.menuConfig.config; this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; @@ -109,8 +111,12 @@ exports.getModule = class NewScanModule extends MenuModule { newScanMessageArea(conf, cb) { // :TODO: it would be nice to cache this - must be done by conf! - const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ); - const currentArea = sortedAreas[this.currentScanAux.area]; + const omitMessageAreaTags = valueAsArray(_.get(this, 'menuConfig.config.omitMessageAreaTags', [])); + const sortedAreas = _.omitBy( + msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ), + area => omitMessageAreaTags.includes(area.areaTag) + ); + const currentArea = sortedAreas[this.currentScanAux.area]; // // Scan and update index until we find something. If results are found, @@ -167,9 +173,10 @@ exports.getModule = class NewScanModule extends MenuModule { newScanFileBase(cb) { // :TODO: add in steps + const omitFileAreaTags = valueAsArray(_.get(this, 'menuConfig.config.omitFileAreaTags', [])); const filterCriteria = { newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), - areaTag : getAvailableFileAreaTags(this.client), + areaTag : getAvailableFileAreaTags(this.client).filter(ft => !omitFileAreaTags.includes(ft)), order : 'ascending', // oldest first }; From d392865aa32c614f1cf7c87938ac3aaf492af33f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 Jul 2018 15:22:56 -0600 Subject: [PATCH 196/569] Add note on new omit file/message areas config --- WHATSNEW.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WHATSNEW.md b/WHATSNEW.md index be4e90cd..bce2ba32 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -13,6 +13,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Hot-reload of configuration files such as menu.hjson, config.hjson, your themes.hjson, etc.: When a file is saved, it will be hot-reloaded into the running system * Note that any custom modules should make use of the new Config.get() method. * The old concept of `autoScale` has been removed. See https://github.com/NuSkooler/enigma-bbs/issues/166 +* Ability to delete from personal mailbox (finally!) +* Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. From 89bce2c23c8684c2c218ea53d8fdeb058aecc92b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 Jul 2018 20:01:52 -0600 Subject: [PATCH 197/569] trace -> debug for Gopher access logs --- core/servers/content/gopher.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 019dbffa..240236d1 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -156,7 +156,7 @@ exports.getModule = class GopherModule extends ServerModule { } defaultGenerator(selectorMatch, cb) { - this.log.trace( { selector : selectorMatch[0] }, 'Serving default content'); + this.log.debug( { selector : selectorMatch[0] }, 'Serving default content'); let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc'); bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); @@ -172,7 +172,7 @@ exports.getModule = class GopherModule extends ServerModule { } notFoundGenerator(selector, cb) { - this.log.trace( { selector }, 'Serving not found content'); + this.log.debug( { selector }, 'Serving not found content'); return cb('Not found'); } @@ -205,7 +205,7 @@ exports.getModule = class GopherModule extends ServerModule { } messageAreaGenerator(selectorMatch, cb) { - this.log.trace( { selector : selectorMatch[0] }, 'Serving message area content'); + this.log.debug( { selector : selectorMatch[0] }, 'Serving message area content'); // // Selector should be: // /msgarea - list confs From 73bc211b94d323203940189d833dd327a1d67719 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 Jul 2018 20:03:03 -0600 Subject: [PATCH 198/569] Oops, fix newscan filter --- core/new_scan.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/new_scan.js b/core/new_scan.js index 4d20ac18..55cbedc3 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -112,10 +112,9 @@ exports.getModule = class NewScanModule extends MenuModule { newScanMessageArea(conf, cb) { // :TODO: it would be nice to cache this - must be done by conf! const omitMessageAreaTags = valueAsArray(_.get(this, 'menuConfig.config.omitMessageAreaTags', [])); - const sortedAreas = _.omitBy( - msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ), - area => omitMessageAreaTags.includes(area.areaTag) - ); + const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).filter(area => { + return area => !omitMessageAreaTags.includes(area.areaTag); + }); const currentArea = sortedAreas[this.currentScanAux.area]; // From dd7b234a1d1f1021a0b91da680a2eebbbfbe4058 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 Jul 2018 20:04:51 -0600 Subject: [PATCH 199/569] Add @markAllRead method for msg_list --- art/themes/luciano_blocktronics/NEWMSGS.ANS | Bin 2312 -> 2340 bytes config/menu.hjson | 4 ++ core/msg_list.js | 52 +++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/NEWMSGS.ANS b/art/themes/luciano_blocktronics/NEWMSGS.ANS index 1065e2956d71a721e9764535d78fb360b2cae214..904399929a667f16171632ce890b9ca08f9ed634 100644 GIT binary patch delta 98 zcmeAWS|YUJFq?^~vvjnfwXsodu5`3Pt^$~2kZUC!4HN?M3_wzeIXMbNsfj6EP}#{( g*Zu R=gmLZ*%%oYO#aWI3IOdD7196z diff --git a/config/menu.hjson b/config/menu.hjson index 8973c833..7dfe4bba 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -783,6 +783,10 @@ keys: [ "x", "shift + x" ] action: @method:fullExit } + { + keys: [ "m", "shift + m" ] + action: @method:markAllRead + } ] } } diff --git a/core/msg_list.js b/core/msg_list.js index d4798012..25c9772d 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -5,7 +5,6 @@ const MenuModule = require('./menu_module.js').MenuModule; const ViewController = require('./view_controller.js').ViewController; const messageArea = require('./message_area.js'); -const stringFormat = require('./string_format.js'); const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const Errors = require('./enig_error.js').Errors; const Message = require('./message.js'); @@ -64,7 +63,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const modOpts = { extraArgs : { - messageAreaTag : this.getSelectedAreaTag(formData.value.message),// this.config.messageAreaTag, + messageAreaTag : this.getSelectedAreaTag(formData.value.message), messageList : this.config.messageList, messageIndex : formData.value.message, lastMessageNextExit : true, @@ -135,6 +134,13 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); this.enableMessageListIndexUpdates(msgListView); return cb(null); + }, + markAllRead : (formData, extraArgs, cb) => { + if(this.config.noUpdateLastReadId) { + return cb(null); + } + + return this.markAllMessagesAsRead(cb); } }; } @@ -295,6 +301,48 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( msgListView.on('index update', idx => this.populateCustomLabelsForSelected(idx) ); } + markAllMessagesAsRead(cb) { + if(!this.config.messageList || this.config.messageList.length === 0) { + return cb(null); // nothing to do. + } + + // + // Generally we'll have a message list for a specific area, + // but this is not always the case. For a given area, we need + // to find the highest message ID in the list to set a + // last read pointer. + // + const areaHighestIds = {}; + this.config.messageList.forEach(msg => { + const highestId = areaHighestIds[msg.areaTag]; + if(highestId) { + if(msg.messageId > highestId) { + areaHighestIds[msg.areaTag] = msg.messageId; + } + } else { + areaHighestIds[msg.areaTag] = msg.messageId; + } + }); + + async.forEachOf(areaHighestIds, (highestId, areaTag, nextArea) => { + messageArea.updateMessageAreaLastReadId( + this.client.user.userId, + areaTag, + highestId, + err => { + if(err) { + this.client.log.warn( { error : err.message }, 'Failed marking area as read'); + } else { + this.client.log.info( { highestId, areaTag }, 'User marked area as read'); + } + return nextArea(null); // always continue + } + ); + }, () => { + return cb(null); + }); + } + updateMessageNumbersAfterDelete(startIndex) { // all index -= 1 from this point on. for(let i = startIndex; i < this.config.messageList.length; ++i) { From bf11fc24a3929aed4f37497214f2c3c106a88b7e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 Jul 2018 20:13:24 -0600 Subject: [PATCH 200/569] Update message list after marking all as read for 'newIndicator' --- core/msg_list.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/msg_list.js b/core/msg_list.js index 25c9772d..aec6e852 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -324,6 +324,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } }); + const regIndicator = ' '.repeat( (this.menuConfig.config.newIndicator || '*').length ); async.forEachOf(areaHighestIds, (highestId, areaTag, nextArea) => { messageArea.updateMessageAreaLastReadId( this.client.user.userId, @@ -333,6 +334,15 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( if(err) { this.client.log.warn( { error : err.message }, 'Failed marking area as read'); } else { + // update newIndicator on messages + this.config.messageList.forEach(msg => { + if(areaTag === msg.areaTag) { + msg.newIndicator = regIndicator; + } + }); + const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); + msgListView.setItems(this.config.messageList); + msgListView.redraw(); this.client.log.info( { highestId, areaTag }, 'User marked area as read'); } return nextArea(null); // always continue From be37fb0587ba4d1fa72ade4d2dd8b9384448e400 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 Jul 2018 20:13:39 -0600 Subject: [PATCH 201/569] Notes on itemFormat/etc. --- UPGRADE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index bec189ed..243894e8 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -41,6 +41,8 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or * Development is now against Node.js 8.x LTS. Follow your standard upgrade path to update to Node 8.x before using 0.0.9-alpha. * The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well. * Possible breaking changes in FSE: The MCI code `%TL13` for error indicator is now `%TL4`. This is part of a cleanup and standardization on "custom ranges". You may need to update your `theme.hjson` and related artwork. +* Removed view width auto-size: Some views still can auto-size their height, but in general you should be explicit in your themes +* More standardization using "custom ranges" and `itemFormat` / `focusItemFormat` semantics. Update your themes! # 0.0.7-alpha to 0.0.8-alpha From 3f34f77fcd9e8f767c21eca9275f838be5dc55b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 8 Jul 2018 11:23:56 -0600 Subject: [PATCH 202/569] Implement fake pipe() for WebSocket (doors/etc.) --- core/servers/login/websocket.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index fa7d97a3..3fc16343 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -31,7 +31,11 @@ function WebSocketClient(ws, req, serverType) { const self = this; this.dataHandler = function(data) { - self.socketBridge.emit('data', data); + if(self.pipedDest) { + self.pipedDest.write(data); + } else { + self.socketBridge.emit('data', data); + } }; // @@ -54,9 +58,14 @@ function WebSocketClient(ws, req, serverType) { return this.ws.send(data, { binary : true }, cb); } - // we need to fake some streaming work + pipe(dest) { + Log.trace('WebSocket SocketBridge pipe()'); + self.pipedDest = dest; + } + unpipe() { Log.trace('WebSocket SocketBridge unpipe()'); + self.pipedDest = null; } resume() { From d54267b318630c910fc62471a3cd89b652c177f2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 8 Jul 2018 11:33:39 -0600 Subject: [PATCH 203/569] Update custom views after del --- core/msg_list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/msg_list.js b/core/msg_list.js index aec6e852..6046b4a6 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -124,6 +124,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } this.selectedMessageForDelete = null; msgListView.redraw(); + this.populateCustomLabelsForSelected(msgListView.focusedItemIndex); return cb(null); }); } else { From 2f22d91bcf57962ba5af31d7c93fc46145bd7554 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 8 Jul 2018 19:20:08 -0600 Subject: [PATCH 204/569] Fix comment --- core/msg_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/msg_list.js b/core/msg_list.js index 6046b4a6..2796db88 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -15,7 +15,7 @@ const _ = require('lodash'); const moment = require('moment'); /* - Available listFormat/focusListFormat members for |msgList| + Available itemFormat/focusItemFormat members for |msgList| msgNum : Message number to : To username/handle From 5c826abd5b3822b16ec75246e96083b2f460c1d5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 9 Jul 2018 20:27:09 -0600 Subject: [PATCH 205/569] Onelinerz updates: * Uses standard `itemFormat` * Uses format of {userName} vs {username} (case) * Has preview implemented as %TL2 --- UPGRADE.md | 1 + art/themes/luciano_blocktronics/theme.hjson | 16 ++- core/menu_module.js | 7 +- core/onelinerz.js | 146 ++++++++------------ 4 files changed, 77 insertions(+), 93 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 243894e8..5dfed60d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -43,6 +43,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or * Possible breaking changes in FSE: The MCI code `%TL13` for error indicator is now `%TL4`. This is part of a cleanup and standardization on "custom ranges". You may need to update your `theme.hjson` and related artwork. * Removed view width auto-size: Some views still can auto-size their height, but in general you should be explicit in your themes * More standardization using "custom ranges" and `itemFormat` / `focusItemFormat` semantics. Update your themes! +* In addition to using `itemFormat`, the `onelinerz` module uses `userName` vs `username` (note the case) to match other modules # 0.0.7-alpha to 0.0.8-alpha diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 6a36ae05..c0735e6f 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -88,11 +88,15 @@ fullLoginSequenceOnelinerz: { config: { - listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.59}" + tsFormat: ddd h:mma } 0: { mci: { - VM1: { height: 10, width: 20 } + VM1: { + height: 10 + width: 20 + itemFormat: "|00|11{userName:<12}|08: |03{oneliner:<59.59}" + } TM2: { focusTextStyle: first lower } @@ -183,13 +187,15 @@ } mainMenuOnelinerz: { - // :TODO: Need way to just duplicate entry here & in menu.hjson, e.g. use: someName + must supply next/etc. in menu config: { - listFormat: "|00|11{username:<12}|08: |03{oneliner:<59.59}" + tsFormat: ddd h:mma } 0: { mci: { - VM1: { height: 10, width: 10 } + VM1: { + height: 10 + itemFormat: "|00|11{userName:<12}|08: |03{oneliner:<59.59}" + } TM2: { focusTextStyle: first lower } diff --git a/core/menu_module.js b/core/menu_module.js index 8c7516f6..d284aa64 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -319,7 +319,8 @@ exports.MenuModule = class MenuModule extends PluginModule { } prepViewController(name, formId, mciMap, cb) { - if(_.isUndefined(this.viewControllers[name])) { + const needsCreated = _.isUndefined(this.viewControllers[name]); + if(needsCreated) { const vcOpts = { client : this.client, formId : formId, @@ -334,13 +335,13 @@ exports.MenuModule = class MenuModule extends PluginModule { }; return vc.loadFromMenuConfig(loadOpts, err => { - return cb(err, vc); + return cb(err, vc, true); }); } this.viewControllers[name].setFocus(true); - return cb(null, this.viewControllers[name]); + return cb(null, this.viewControllers[name], false); } prepViewControllerWithArt(name, formId, options, cb) { diff --git a/core/onelinerz.js b/core/onelinerz.js index 2a917ad2..3ecde562 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -9,11 +9,6 @@ const { getTransactionDatabase } = require('./database.js'); -const ViewController = require('./view_controller.js').ViewController; -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const stringFormat = require('./string_format.js'); - // deps const sqlite3 = require('sqlite3'); const async = require('async'); @@ -22,13 +17,9 @@ const moment = require('moment'); /* Module :TODO: - * Add pipe code support - - override max length & monitor *display* len as user types in order to allow for actual display len with color - * Add preview control: Shows preview with pipe codes resolved * Add ability to at least alternate formatStrings -- every other */ - exports.moduleInfo = { name : 'Onelinerz', desc : 'Standard local onelinerz', @@ -37,20 +28,20 @@ exports.moduleInfo = { }; const MciViewIds = { - ViewForm : { - Entries : 1, - AddPrompt : 2, + view : { + entries : 1, + addPrompt : 2, }, - AddForm : { - NewEntry : 1, - EntryPreview : 2, - AddPrompt : 3, + add : { + newEntry : 1, + entryPreview : 2, + addPrompt : 3, } }; const FormIds = { - View : 0, - Add : 1, + view : 0, + add : 1, }; exports.getModule = class OnelinerzModule extends MenuModule { @@ -115,46 +106,29 @@ exports.getModule = class OnelinerzModule extends MenuModule { async.waterfall( [ - function clearAndDisplayArt(callback) { + function prepArtAndViewController(callback) { if(self.viewControllers.add) { self.viewControllers.add.setFocus(false); } - if(clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } - - theme.displayThemedAsset( - self.menuConfig.config.art.entries, - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); + return self.prepViewControllerWithArt( + 'view', + FormIds.view, + { + clearScreen, + trailingLF : false + }, + (err, artInfo, wasCreated) => { + if(!wasCreated) { + self.viewControllers.view.setFocus(true); + self.viewControllers.view.getView(MciViewIds.view.addPrompt).redraw(); + } + return callback(err); } ); }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'view', - new ViewController( { client : self.client, formId : FormIds.View } ) - ); - - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, - }; - - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt).redraw(); - return callback(null); - } - }, function fetchEntries(callback) { - const entriesView = self.viewControllers.view.getView(MciViewIds.ViewForm.Entries); + const entriesView = self.viewControllers.view.getView(MciViewIds.view.entries); const limit = entriesView.dimens.height; let entries = []; @@ -179,24 +153,22 @@ exports.getModule = class OnelinerzModule extends MenuModule { ); }, function populateEntries(entriesView, entries, callback) { - const listFormat = self.menuConfig.config.listFormat || '{username}@{ts}: {oneliner}';// :TODO: should be userName to be consistent - const tsFormat = self.menuConfig.config.timestampFormat || 'ddd h:mma'; + const tsFormat = self.menuConfig.config.timestampFormat || self.client.currentTheme.helpers.getDateFormat('short'); entriesView.setItems(entries.map( e => { - return stringFormat(listFormat, { + return { userId : e.user_id, - username : e.user_name, + userName : e.user_name, oneliner : e.oneliner, ts : e.timestamp.format(tsFormat), - } ); + }; })); entriesView.redraw(); - return callback(null); }, function finalPrep(callback) { - const promptView = self.viewControllers.view.getView(MciViewIds.ViewForm.AddPrompt); + const promptView = self.viewControllers.view.getView(MciViewIds.view.addPrompt); promptView.setFocusItemIndex(1); // default to NO return callback(null); } @@ -216,37 +188,41 @@ exports.getModule = class OnelinerzModule extends MenuModule { [ function clearAndDisplayArt(callback) { self.viewControllers.view.setFocus(false); - self.client.term.rawWrite(ansi.resetScreen()); - theme.displayThemedAsset( - self.menuConfig.config.art.add, - self.client, - { font : self.menuConfig.font }, - (err, artData) => { - return callback(err, artData); + return self.prepViewControllerWithArt( + 'add', + FormIds.add, + { + clearScreen : true, + trailingLF : false + }, + (err, artInfo, wasCreated) => { + if(!wasCreated) { + self.viewControllers.add.setFocus(true); + self.viewControllers.add.redrawAll(); + self.viewControllers.add.switchFocus(MciViewIds.add.newEntry); + } + return callback(err); } ); }, - function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { - const vc = self.addViewController( - 'add', - new ViewController( { client : self.client, formId : FormIds.Add } ) - ); - - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, - }; - - return vc.loadFromMenuConfig(loadOpts, callback); - } else { - self.viewControllers.add.setFocus(true); - self.viewControllers.add.redrawAll(); - self.viewControllers.add.switchFocus(MciViewIds.AddForm.NewEntry); - return callback(null); + function initPreviewUpdates(callback) { + const previewView = self.viewControllers.add.getView(MciViewIds.add.entryPreview); + const entryView = self.viewControllers.add.getView(MciViewIds.add.newEntry); + if(previewView) { + let timerId; + entryView.on('key press', () => { + clearTimeout(timerId); + timerId = setTimeout( () => { + const focused = self.viewControllers.add.getFocusedView(); + if(focused === entryView) { + previewView.setText(entryView.getData()); + focused.setFocus(true); + } + }, 500); + }); } + return callback(null); } ], err => { @@ -258,8 +234,8 @@ exports.getModule = class OnelinerzModule extends MenuModule { } clearAddForm() { - this.setViewText('add', MciViewIds.AddForm.NewEntry, ''); - this.setViewText('add', MciViewIds.AddForm.EntryPreview, ''); + this.setViewText('add', MciViewIds.add.newEntry, ''); + this.setViewText('add', MciViewIds.add.entryPreview, ''); } initDatabase(cb) { From 0599bd32fc13102fadd57648877b81050aae55ce Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 9 Jul 2018 20:35:37 -0600 Subject: [PATCH 206/569] Note on websocket server --- UPGRADE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 5dfed60d..25fe477f 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -44,6 +44,19 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or * Removed view width auto-size: Some views still can auto-size their height, but in general you should be explicit in your themes * More standardization using "custom ranges" and `itemFormat` / `focusItemFormat` semantics. Update your themes! * In addition to using `itemFormat`, the `onelinerz` module uses `userName` vs `username` (note the case) to match other modules +* `loginServers.webSocket` configuration block has changed to be more consistent with other servers. Example: +``` +webSocket: { + ws: { + enabled: true + } + wss: { + enabled: true + port: 1234 + } + proxied: true // X-Forwarded-Proto: https support +} +``` # 0.0.7-alpha to 0.0.8-alpha From 45ca627b4eba59669048bef5cc72f7720ee54e1c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 9 Jul 2018 20:55:47 -0600 Subject: [PATCH 207/569] Minor update --- core/onelinerz.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/onelinerz.js b/core/onelinerz.js index 3ecde562..2d902a8e 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -119,7 +119,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { trailingLF : false }, (err, artInfo, wasCreated) => { - if(!wasCreated) { + if(!err && !wasCreated) { self.viewControllers.view.setFocus(true); self.viewControllers.view.getView(MciViewIds.view.addPrompt).redraw(); } From 3e06c4d2908e6d77141bd247277f03d8ffc4cb6e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 10 Jul 2018 18:57:52 -0600 Subject: [PATCH 208/569] Fix onelinerz art name --- config/menu.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/menu.hjson b/config/menu.hjson index 7dfe4bba..a26634ce 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1287,7 +1287,7 @@ } config: { art: { - entries: ONELINER + view: ONELINER add: ONEADD } } From ea3e0ac3ff617dc7ec72027557583983d7cd0527 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 10 Jul 2018 19:00:02 -0600 Subject: [PATCH 209/569] Fix onelinerz art name...again --- config/menu.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/menu.hjson b/config/menu.hjson index a26634ce..8a14526b 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -631,7 +631,7 @@ } config: { art: { - entries: ONELINER + view: ONELINER add: ONEADD } } From 5d91cfb7d0ea9972ac58ceaeb57da52c906fafd4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 10 Jul 2018 19:11:03 -0600 Subject: [PATCH 210/569] Add {userName} and {userNameRaw} door launch/format options --- WHATSNEW.md | 2 ++ core/door.js | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index bce2ba32..bedc54e4 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -15,6 +15,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * The old concept of `autoScale` has been removed. See https://github.com/NuSkooler/enigma-bbs/issues/166 * Ability to delete from personal mailbox (finally!) * Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson +* `{userName}` (sanatized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door. + ## 0.0.8-alpha * [Mystic BBS style](http://wiki.mysticbbs.com/doku.php?id=displaycodes) extended pipe color codes. These allow for example, to set "iCE" background colors. diff --git a/core/door.js b/core/door.js index 5ad5855a..1ac0754f 100644 --- a/core/door.js +++ b/core/door.js @@ -1,13 +1,15 @@ /* jslint node: true */ 'use strict'; -const stringFormat = require('./string_format.js'); -const { Errors } = require('./enig_error.js'); +const stringFormat = require('./string_format.js'); +const { Errors } = require('./enig_error.js'); -const pty = require('node-pty'); -const decode = require('iconv-lite').decode; -const createServer = require('net').createServer; -const paths = require('path'); +// deps +const pty = require('node-pty'); +const decode = require('iconv-lite').decode; +const createServer = require('net').createServer; +const paths = require('path'); +const sanatizeFilename = require('sanitize-filename'); module.exports = class Door { constructor(client) { @@ -64,6 +66,8 @@ module.exports = class Door { node : exeInfo.node.toString(), srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', userId : this.client.user.userId.toString(), + userName : sanatizeFilename(this.client.user.username), + userNameRaw : this.client.user.username, cwd : cwd, }; From 47e34f9da7570b66467532eae9c764e1b9a9f826 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 Jul 2018 21:14:28 -0600 Subject: [PATCH 211/569] Fix comment --- core/abracadabra.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 676600e4..c0bb2c83 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -149,7 +149,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { const exeInfo = { cmd : this.config.cmd, - cwd : this.config.cwd, // null/undefined=path of |cwd| + cwd : this.config.cwd, // null/undefined = parent_of(cmd) args : this.config.args, io : this.config.io || 'stdio', encoding : this.config.encoding || this.client.term.outputEncoding, From 340c6ccf7633f77c1d8386d89aea23be26e81d3a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 15 Jul 2018 11:49:56 -0600 Subject: [PATCH 212/569] Fix asset parsing for path-to-method, etc. --- core/asset.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/asset.js b/core/asset.js index 2204b1da..b3d62154 100644 --- a/core/asset.js +++ b/core/asset.js @@ -29,19 +29,23 @@ const ALL_ASSETS = [ 'sysStat', ]; -const ASSET_RE = new RegExp('\\@(' + ALL_ASSETS.join('|') + ')\\:([\\w\\d\\.]*)(?:\\/([\\w\\d\\_]+))*'); +const ASSET_RE = new RegExp( + '^@(' + ALL_ASSETS.join('|') + ')' + + /:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source +); function parseAsset(s) { const m = ASSET_RE.exec(s); - if(m) { - let result = { type : m[1] }; + const result = { type : m[1] }; if(m[3]) { - result.location = m[2]; - result.asset = m[3]; + result.asset = m[3]; + if(m[2]) { + result.location = m[2]; + } } else { - result.asset = m[2]; + result.asset = m[2]; } return result; From 7b75f08c7e2d7e410fa56b6111e16a9b6e22441a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 15 Jul 2018 11:50:04 -0600 Subject: [PATCH 213/569] Clean up code a bit --- core/menu_util.js | 77 ++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/core/menu_util.js b/core/menu_util.js index f362041c..7eca129d 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -2,16 +2,17 @@ 'use strict'; // ENiGMA½ -var moduleUtil = require('./module_util.js'); -var Log = require('./logger.js').log; -var Config = require('./config.js').get; -var asset = require('./asset.js'); -var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; +const moduleUtil = require('./module_util.js'); +const Log = require('./logger.js').log; +const Config = require('./config.js').get; +const asset = require('./asset.js'); +const { MCIViewFactory } = require('./mci_view_factory.js'); +const { Errors } = require('./enig_error.js'); -var paths = require('path'); -var async = require('async'); -var assert = require('assert'); -var _ = require('lodash'); +// deps +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); exports.loadMenu = loadMenu; exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; @@ -19,41 +20,37 @@ exports.handleAction = handleAction; exports.handleNext = handleNext; function getMenuConfig(client, name, cb) { - var menuConfig; - async.waterfall( [ function locateMenuConfig(callback) { if(_.has(client.currentTheme, [ 'menus', name ])) { - menuConfig = client.currentTheme.menus[name]; - callback(null); - } else { - callback(new Error('No menu entry for \'' + name + '\'')); + const menuConfig = client.currentTheme.menus[name]; + return callback(null, menuConfig); } + return callback(Errors.DoesNotExist(`No menu entry for "${name}"`)); }, - function locatePromptConfig(callback) { + function locatePromptConfig(menuConfig, callback) { if(_.isString(menuConfig.prompt)) { if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; - callback(null); - } else { - callback(new Error('No prompt entry for \'' + menuConfig.prompt + '\'')); + return callback(null, menuConfig); } - } else { - callback(null); + return callback(Error.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`)); } + return callback(null, menuConfig); } ], - function complete(err) { - cb(err, menuConfig); + (err, menuConfig) => { + return cb(err, menuConfig); } ); } +// :TODO: name/client should not be part of options - they are required always function loadMenu(options, cb) { - assert(_.isObject(options)); - assert(_.isString(options.name)); - assert(_.isObject(options.client)); + if(!_.isString(options.name) || !_.isObject(options.client)) { + return cb(Errors.MissingParam('Missing required options')); + } async.waterfall( [ @@ -117,16 +114,12 @@ function loadMenu(options, cb) { } function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { - assert(_.isObject(menuConfig)); - if(!_.isObject(menuConfig.form)) { - cb(new Error('Invalid or missing \'form\' member for menu')); - return; + return cb(Errors.MissingParam('Invalid or missing "form" member for menu')); } if(!_.isObject(menuConfig.form[formId])) { - cb(new Error('No form found for formId ' + formId)); - return; + return cb(Errors.DoesNotExist(`No form found for formId ${formId}`)); } const formForId = menuConfig.form[formId]; @@ -141,8 +134,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { // if(_.isObject(formForId[mciReqKey])) { Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); - cb(null, formForId[mciReqKey]); - return; + return cb(null, formForId[mciReqKey]); } // @@ -153,7 +145,7 @@ function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { return cb(null, formForId); } - cb(new Error('No matching form configuration found for key \'' + mciReqKey + '\'')); + return cb(Errors.DoesNotExist(`No matching form configuration found for key "${mciReqKey}"`)); } // :TODO: Most of this should be moved elsewhere .... DRY... @@ -176,11 +168,14 @@ function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) { } function handleAction(client, formData, conf, cb) { - assert(_.isObject(conf)); - assert(_.isString(conf.action)); + if(!_.isObject(conf)) { + return cb(Errors.MissingParam('Missing config')); + } const actionAsset = asset.parseAsset(conf.action); - assert(_.isObject(actionAsset)); + if(!_.isObject(actionAsset)) { + return cb(Errors.Invalid('Unable to parse "conf.action"')); + } switch(actionAsset.type) { case 'method' : @@ -210,7 +205,7 @@ function handleAction(client, formData, conf, cb) { return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); } - const err = new Error('Method does not exist'); + const err = Errors.DoesNotExist('Method does not exist'); client.log.warn( { method : actionAsset.asset }, err.message); return cb(err); } @@ -246,7 +241,7 @@ function handleNext(client, nextSpec, conf, cb) { return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb ); } - const err = new Error('Method does not exist'); + const err = Errors.DoesNotExist('Method does not exist'); client.log.warn( { method : nextAsset.asset }, err.message); return cb(err); } @@ -255,7 +250,7 @@ function handleNext(client, nextSpec, conf, cb) { return client.currentMenuModule.gotoMenu(nextAsset.asset, { extraArgs : extraArgs }, cb ); } - const err = new Error('Invalid asset type for "next"'); + const err = Errors.Invalid('Invalid asset type for "next"'); client.log.error( { nextSpec : nextSpec }, err.message); return cb(err); } From 6a3849dbdccc4cc94560b75d5efeaaded091c190 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 15 Jul 2018 19:18:27 -0600 Subject: [PATCH 214/569] Fist version of ArchaicNET support module --- core/archaicnet.js | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 core/archaicnet.js diff --git a/core/archaicnet.js b/core/archaicnet.js new file mode 100644 index 00000000..bc3528d6 --- /dev/null +++ b/core/archaicnet.js @@ -0,0 +1,129 @@ +/* jslint node: true */ +'use strict'; + +// enigma-bbs +const { MenuModule } = require('../core/menu_module.js'); +const { resetScreen } = require('../core/ansi_term.js'); +const { Errors } = require('../core/enig_error.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); +const SSHClient = require('ssh2').Client; + +exports.moduleInfo = { + name : 'ArchaicNET', + desc : 'ArchaicNET Access Module', + author : 'NuSkooler', +}; + +exports.getModule = class ArchaicNETModule extends MenuModule { + constructor(options) { + super(options); + + // establish defaults + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'bbs.archaicbinary.net'; + this.config.sshPort = this.config.sshPort || 8513; + this.config.rloginPort = this.config.rloginPort || 2222; + } + + initSequence() { + let clientTerminated; + const self = this; + + async.series( + [ + function validateConfig(callback) { + const reqConfs = [ 'username', 'password', 'bbsTag' ]; + for(let req of reqConfs) { + if(!_.isString(_.get(self, [ 'config', req ]))) { + return callback(Errors.MissingConfig(`Config requires "${req}"`)); + } + } + return callback(null); + }, + function establishSecureConnection(callback) { + self.client.term.write(resetScreen()); + self.client.term.write('Connecting to ArchaicNET, please wait...\n'); + + const sshClient = new SSHClient(); + + let pipeRestored = false; + let pipedStream; + const restorePipe = function() { + if(pipedStream && !pipeRestored && !clientTerminated) { + self.client.term.output.unpipe(pipedStream); + self.client.term.output.resume(); + } + }; + + sshClient.on('ready', () => { + // track client termination so we can clean up early + self.client.once('end', () => { + self.client.log.info('Connection ended. Terminating ArchaicNET connection'); + clientTerminated = true; + return sshClient.end(); + }); + + // establish tunnel for rlogin + const fwdPort = self.config.rloginPort + self.client.node; + sshClient.forwardOut('127.0.0.1', fwdPort, self.config.host, self.config.rloginPort, (err, stream) => { + if(err) { + return sshClient.end(); + } + + // + // Send rlogin - [] e.g. [Xibalba]NuSkooler + // + const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; + stream.write(rlogin); + + pipedStream = stream; + self.client.term.output.pipe(stream); + + stream.on('data', d => { + return self.client.term.rawWrite(d); + }); + + stream.on('close', () => { + restorePipe(); + return sshClient.end(); + }); + }); + }); + + sshClient.on('error', err => { + return self.client.log.info(`ArchaicNET SSH client error: ${err.message}`); + }); + + sshClient.on('close', hadError => { + if(hadError) { + self.client.warn('Closing ArchaicNET SSH due to error'); + } + restorePipe(); + return callback(null); + }); + + sshClient.connect( { + host : self.config.host, + port : self.config.sshPort, + username : self.config.username, + password : self.config.password, + }); + } + ], + err => { + if(err) { + self.client.log.warn( { error : err.message }, 'ArchaicNET error'); + } + + // if the client is stil here, go to previous + if(!clientTerminated) { + self.prevMenu(); + } + } + ); + } +}; + From 013a947e157ad8a85c35d5c6c9e68ad6eee951cf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 15 Jul 2018 19:18:44 -0600 Subject: [PATCH 215/569] Add release-info.asc used by some modern release groups --- core/config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 6d5251a3..9c33f38f 100644 --- a/core/config.js +++ b/core/config.js @@ -747,7 +747,8 @@ function getDefaultConfig() { // common README filename - https://en.wikipedia.org/wiki/README descLong : [ - '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$' // eslint-disable-line no-useless-escape + '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$',// eslint-disable-line no-useless-escape + '^RELEASE-INFO.ASC$' // eslint-disable-line no-useless-escape ], }, From e3c197c3e11d59f09175cfc451271e97bb9f8397 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 15 Jul 2018 22:08:09 -0600 Subject: [PATCH 216/569] Fix event emitter leak --- core/client.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/client.js b/core/client.js index a2eb4bc3..ec10947f 100644 --- a/core/client.js +++ b/core/client.js @@ -111,12 +111,13 @@ function Client(/*input, output*/) { this.input.on('data', this.dataHandler); }; - Events.on(Events.getSystemEvents().ThemeChanged, ( { themeId } ) => { - if(_.get(this.currentTheme, 'info.themeId') === themeId) { - this.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); + this.themeChangedListener = function( { themeId } ) { + if(_.get(self.currentTheme, 'info.themeId') === themeId) { + self.currentTheme = require('./theme.js').getAvailableThemes().get(themeId); } - }); + }; + Events.on(Events.getSystemEvents().ThemeChanged, this.themeChangedListener); // // Peek at incoming |data| and emit events for any special @@ -458,7 +459,9 @@ Client.prototype.end = function () { this.term.disconnect(); } - var currentModule = this.menuStack.getCurrentModule; + Events.removeListener(Events.getSystemEvents().ThemeChanged, this.themeChangedListener); + + const currentModule = this.menuStack.getCurrentModule; if(currentModule) { currentModule.leave(); @@ -470,10 +473,9 @@ Client.prototype.end = function () { // // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH // - // :TODO: is this OK? return this.output.end.apply(this.output, arguments); } catch(e) { - // TypeError + // ie TypeError } }; From 1f396e198ee04158a2a42fd8de85626d9df069f3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 16 Jul 2018 22:43:19 -0600 Subject: [PATCH 217/569] Fix escape/de-escaping for zmodem & friends --- core/archaicnet.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/core/archaicnet.js b/core/archaicnet.js index bc3528d6..b407c97d 100644 --- a/core/archaicnet.js +++ b/core/archaicnet.js @@ -52,9 +52,12 @@ exports.getModule = class ArchaicNETModule extends MenuModule { let pipeRestored = false; let pipedStream; const restorePipe = function() { - if(pipedStream && !pipeRestored && !clientTerminated) { - self.client.term.output.unpipe(pipedStream); - self.client.term.output.resume(); + if(!pipeRestored) { + if(pipedStream && !clientTerminated) { + self.client.term.output.unpipe(pipedStream); + self.client.term.output.resume(); + } + self.client.restoreDataHandler(); } }; @@ -82,8 +85,15 @@ exports.getModule = class ArchaicNETModule extends MenuModule { pipedStream = stream; self.client.term.output.pipe(stream); - stream.on('data', d => { - return self.client.term.rawWrite(d); + // we need to filter I/O for escape/de-escaping zmodem and the like + self.client.setTemporaryDirectDataHandler(data => { + const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape + stream.write(Buffer.from(tmp, 'binary')); + }); + + stream.on('data', data => { + const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape + self.client.term.rawWrite(Buffer.from(tmp, 'binary')); }); stream.on('close', () => { From 118cb97487e82b1bfbca7e83f66fb019fa3ec786 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 17 Jul 2018 20:00:47 -0600 Subject: [PATCH 218/569] MenuModule.reload(), updated @systemMethod:reloadMenu(), and notes --- core/menu_module.js | 6 ++++++ core/system_menu_method.js | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index d284aa64..67fabb7e 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -187,6 +187,12 @@ exports.MenuModule = class MenuModule extends PluginModule { return this.client.menuStack.goto(name, options, cb); } + reload(cb) { + const prevMenu = this.client.menuStack.pop(); + prevMenu.instance.leave(); + return this.client.menuStack.goto(prevMenu.name, cb); + } + addViewController(name, vc) { assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 9c9cd6d3..9218e34a 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -87,11 +87,9 @@ function nextMenu(callingMenu, formData, extraArgs, cb) { }); } -// :TODO: prev/nextConf, prev/nextArea should use a NYI MenuModule.redraw() or such -- avoid pop/goto() hack! +// :TODO: need redrawMenu() and MenuModule.redraw() function reloadMenu(menu, cb) { - const prevMenu = menu.client.menuStack.pop(); - prevMenu.instance.leave(); - menu.client.menuStack.goto(prevMenu.name, cb); + return menu.reload(cb); } function prevConf(callingMenu, formData, extraArgs, cb) { From 58254ee27b150f218c88cfee228c373131ab8534 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 17 Jul 2018 21:06:27 -0600 Subject: [PATCH 219/569] Fix config --- core/archaicnet.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/core/archaicnet.js b/core/archaicnet.js index b407c97d..31ce4dc2 100644 --- a/core/archaicnet.js +++ b/core/archaicnet.js @@ -24,8 +24,8 @@ exports.getModule = class ArchaicNETModule extends MenuModule { // establish defaults this.config = options.menuConfig.config; this.config.host = this.config.host || 'bbs.archaicbinary.net'; - this.config.sshPort = this.config.sshPort || 8513; - this.config.rloginPort = this.config.rloginPort || 2222; + this.config.sshPort = this.config.sshPort || 2222; + this.config.rloginPort = this.config.rloginPort || 8513; } initSequence() { @@ -49,15 +49,12 @@ exports.getModule = class ArchaicNETModule extends MenuModule { const sshClient = new SSHClient(); - let pipeRestored = false; - let pipedStream; + let needRestore = false; + //let pipedStream; const restorePipe = function() { - if(!pipeRestored) { - if(pipedStream && !clientTerminated) { - self.client.term.output.unpipe(pipedStream); - self.client.term.output.resume(); - } + if(needRestore && !clientTerminated) { self.client.restoreDataHandler(); + needRestore = false; } }; @@ -82,14 +79,12 @@ exports.getModule = class ArchaicNETModule extends MenuModule { const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; stream.write(rlogin); - pipedStream = stream; - self.client.term.output.pipe(stream); - // we need to filter I/O for escape/de-escaping zmodem and the like self.client.setTemporaryDirectDataHandler(data => { const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape stream.write(Buffer.from(tmp, 'binary')); }); + needRestore = true; stream.on('data', data => { const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape @@ -115,6 +110,7 @@ exports.getModule = class ArchaicNETModule extends MenuModule { return callback(null); }); + self.client.log.trace( { host : self.config.host, port : self.config.sshPort }, 'Connecting to ArchaicNET'); sshClient.connect( { host : self.config.host, port : self.config.sshPort, From c1ae3d88bad3fcdec4e328ff926ae42cdbec7788 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 14:28:18 -0600 Subject: [PATCH 220/569] * Fix RunDoor event name * Standardize *.user_* event names --- core/system_events.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/system_events.js b/core/system_events.js index 42aa0700..c8345160 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -12,14 +12,12 @@ module.exports = { PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson) // User - includes { user, ...} - NewUser : 'codes.l33t.enigma.system.new_user', + NewUser : 'codes.l33t.enigma.system.user_new', UserLogin : 'codes.l33t.enigma.system.user_login', UserLogoff : 'codes.l33t.enigma.system.user_logoff', UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } - - // NYI below here: - UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', + UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { areaTag } UserSendMail : 'codes.l33t.enigma.system.user_send_mail', - UserSendRunDoor : 'codes.l33t.enigma.system.user_run_door', + UserRunDoor : 'codes.l33t.enigma.system.user_run_door', }; From 52585c78f04b21d7d75cc56325a94b60cbb9d10f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 14:32:06 -0600 Subject: [PATCH 221/569] Major changes around events, event log, etc. * User event log is now functional & attached to various events * Add additional missing system events * Completely re-write last_callers to have new functionality, etc. * Events.addListenerMultipleEvents() * New 'moduleInitialize' export for module init vs Event specific registerEvents * Add docs on last_callers mod --- UPGRADE.md | 2 + WHATSNEW.md | 2 + core/abracadabra.js | 5 +- core/bbs.js | 6 + core/config.js | 2 +- core/database.js | 13 +- core/events.js | 46 ++---- core/fse.js | 46 +++--- core/last_callers.js | 286 ++++++++++++++++++++++------------- core/mask_edit_text_view.js | 3 +- core/module_util.js | 54 +++++++ core/nua.js | 6 +- core/stat_log.js | 41 ++++- core/user.js | 11 +- core/user_login.js | 6 +- docs/modding/last-callers.md | 34 +++++ 16 files changed, 392 insertions(+), 171 deletions(-) create mode 100644 docs/modding/last-callers.md diff --git a/UPGRADE.md b/UPGRADE.md index 25fe477f..956789c3 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -57,6 +57,8 @@ webSocket: { proxied: true // X-Forwarded-Proto: https support } ``` +* The module export `registerEvents` has been deprecated. If you have a module that depends on this, use the new more generic `moduleInitialize` export instead. +* The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup. # 0.0.7-alpha to 0.0.8-alpha diff --git a/WHATSNEW.md b/WHATSNEW.md index bedc54e4..5e92784e 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -16,6 +16,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Ability to delete from personal mailbox (finally!) * Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson * `{userName}` (sanatized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door. +* Any module may now register for a system startup intiialization via the `initializeModules(initInfo, cb)` export. +* User event log is now functional. Various events a user performs will be persisted to the `system.db` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`! ## 0.0.8-alpha diff --git a/core/abracadabra.js b/core/abracadabra.js index c0bb2c83..9ada9e5a 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -1,11 +1,12 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; +const { MenuModule } = require('./menu_module.js'); const DropFile = require('./dropfile.js'); const Door = require('./door.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); +const Events = require('./events.js'); const async = require('async'); const assert = require('assert'); @@ -145,6 +146,8 @@ exports.getModule = class AbracadabraModule extends MenuModule { } runDoor() { + Events.emit(Events.getSystemEvents().UserRunDoor, { user : this.client.user } ); + this.client.term.write(ansi.resetScreen()); const exeInfo = { diff --git a/core/bbs.js b/core/bbs.js index 4436eb02..f3d59200 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -267,6 +267,9 @@ function initialize(cb) { function readyEvents(callback) { return require('./events.js').startup(callback); }, + function genericModulesInit(callback) { + return require('./module_util.js').initializeModules(callback); + }, function listenConnections(callback) { return require('./listening_server.js').startup(callback); }, @@ -286,6 +289,9 @@ function initialize(cb) { initServices.eventScheduler = modInst; return callback(err); }); + }, + function listenUserEventsForStatLog(callback) { + return require('./stat_log.js').initUserEvents(callback); } ], function onComplete(err) { diff --git a/core/config.js b/core/config.js index 9c33f38f..3d7c6d70 100644 --- a/core/config.js +++ b/core/config.js @@ -147,7 +147,7 @@ function getDefaultConfig() { users : { usernameMin : 2, usernameMax : 16, // Note that FidoNet wants 36 max - usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ ]+$', + usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ .]+$', passwordMin : 6, passwordMax : 128, diff --git a/core/database.js b/core/database.js index ba48b404..8bae6a87 100644 --- a/core/database.js +++ b/core/database.js @@ -18,6 +18,7 @@ const dbs = {}; exports.getTransactionDatabase = getTransactionDatabase; exports.getModDatabasePath = getModDatabasePath; +exports.loadDatabaseForMod = loadDatabaseForMod; exports.getISOTimestampString = getISOTimestampString; exports.sanatizeString = sanatizeString; exports.initializeDatabases = initializeDatabases; @@ -55,6 +56,15 @@ function getModDatabasePath(moduleInfo, suffix) { return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`); } +function loadDatabaseForMod(modInfo, cb) { + const db = getTransactionDatabase(new sqlite3.Database( + getModDatabasePath(modInfo), + err => { + return cb(err, db); + } + )); +} + function getISOTimestampString(ts) { ts = ts || moment(); return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); @@ -131,10 +141,11 @@ const DB_INIT_TABLE = { id INTEGER PRIMARY KEY, timestamp DATETIME NOT NULL, user_id INTEGER NOT NULL, + session_id VARCHAR NOT NULL, log_name VARCHAR NOT NULL, log_value VARCHAR NOT NULL, - UNIQUE(timestamp, user_id, log_name) + UNIQUE(timestamp, user_id, session_id, log_name) );` ); diff --git a/core/events.js b/core/events.js index 6284597e..73253fe3 100644 --- a/core/events.js +++ b/core/events.js @@ -1,20 +1,14 @@ /* jslint node: true */ 'use strict'; -const paths = require('path'); const events = require('events'); const Log = require('./logger.js').log; const SystemEvents = require('./system_events.js'); -// deps -const _ = require('lodash'); -const async = require('async'); -const glob = require('glob'); - module.exports = new class Events extends events.EventEmitter { constructor() { super(); - this.setMaxListeners(32); // :TODO: play with this... + this.setMaxListeners(64); // :TODO: play with this... } getSystemEvents() { @@ -41,39 +35,21 @@ module.exports = new class Events extends events.EventEmitter { return super.once(event, listener); } + addListenerMultipleEvents(events, listener) { + Log.trace( { events }, 'Registring event listeners'); + events.forEach(eventName => { + this.on(eventName, event => { + listener(eventName, event); + }); + }); + } + 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) => { - const fullModulePath = paths.join(modulePath, moduleName); - - try { - const mod = require(fullModulePath); - - if(_.isFunction(mod.registerEvents)) { - // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? - mod.registerEvents(this); - } - } catch(e) { - Log.warn( { error : e }, 'Exception during module "registerEvents"'); - } - - return nextModule(null); - }, err => { - return nextPath(err); - }); - }); - }, err => { - return cb(err); - }); + return cb(null); } }; diff --git a/core/fse.js b/core/fse.js index 7d3565ad..fcf27ad4 100644 --- a/core/fse.js +++ b/core/fse.js @@ -2,26 +2,34 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const Message = require('./message.js'); -const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; -const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; -const User = require('./user.js'); -const StatLog = require('./stat_log.js'); -const stringFormat = require('./string_format.js'); -const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; -const { isAnsi, cleanControlCodes, insert } = require('./string_util.js'); -const Config = require('./config.js').get; -const { getAddressedToInfo } = require('./mail_util.js'); +const { MenuModule } = require('./menu_module.js'); +const { ViewController } = require('./view_controller.js'); +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const Message = require('./message.js'); +const { + updateMessageAreaLastReadId +} = require('./message_area.js'); +const { getMessageAreaByTag } = require('./message_area.js'); +const User = require('./user.js'); +const StatLog = require('./stat_log.js'); +const stringFormat = require('./string_format.js'); +const { + MessageAreaConfTempSwitcher +} = require('./mod_mixins.js'); +const { + isAnsi, cleanControlCodes, + insert +} = require('./string_util.js'); +const Config = require('./config.js').get; +const { getAddressedToInfo } = require('./mail_util.js'); +const Events = require('./events.js'); // deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { name : 'Full Screen Editor (FSE)', @@ -463,12 +471,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul updateUserStats(cb) { if(Message.isPrivateAreaTag(this.message.areaTag)) { + Events.emit(Events.getSystemEvents().UserSendMail, { user : this.client.user }); if(cb) { cb(null); } return; // don't inc stats for private messages } + Events.emit(Events.getSystemEvents().UserPostMessage, { user : this.client.user, areaTag : this.message.areaTag }); return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); } diff --git a/core/last_callers.js b/core/last_callers.js index 52ec08f9..84ca01c3 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -2,27 +2,16 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const StatLog = require('./stat_log.js'); -const User = require('./user.js'); -const stringFormat = require('./string_format.js'); +const { MenuModule } = require('./menu_module.js'); +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const sysDb = require('./database.js').dbs.system; // deps const moment = require('moment'); const async = require('async'); const _ = require('lodash'); -/* - Available listFormat object members: - userId - userName - location - affiliation - ts - -*/ - exports.moduleInfo = { name : 'Last Callers', desc : 'Last callers to the system', @@ -37,6 +26,9 @@ const MciCodeIds = { exports.getModule = class LastCallersModule extends MenuModule { constructor(options) { super(options); + + this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {}); + this.actionIndicatorDefault = _.get(options, 'menuConfig.config.actionIndicatorDefault', '-'); } mciReady(mciData, cb) { @@ -45,107 +37,191 @@ exports.getModule = class LastCallersModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - - let loginHistory; - let callersView; - - async.series( + async.waterfall( [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, - }; - - vc.loadFromMenuConfig(loadOpts, callback); - }, - function fetchHistory(callback) { - callersView = vc.getView(MciCodeIds.CallerList); - - // fetch up - StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { - loginHistory = lh; - - if(self.menuConfig.config.hideSysOpLogin) { - const noOpLoginHistory = loginHistory.filter(lh => { - return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId - }); - - // - // If we have enough items to display, or hideSysOpLogin is set to 'always', - // then set loginHistory to our filtered list. Else, we'll leave it be. - // - if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) { - loginHistory = noOpLoginHistory; - } - } - - // - // Finally, we need to trim up the list to the needed size - // - loginHistory = loginHistory.slice(0, callersView.dimens.height); - - return callback(err); + (next) => { + this.prepViewController('callers', 0, mciData.menu, err => { + return next(err); }); }, - function getUserNamesAndProperties(callback) { - const getPropOpts = { - names : [ 'location', 'affiliation' ] - }; - - const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; - - async.each( - loginHistory, - (item, next) => { - item.userId = parseInt(item.log_value); - item.ts = moment(item.timestamp).format(dateTimeFormat); - - User.getUserName(item.userId, (err, userName) => { - if(err) { - item.deleted = true; - return next(null); - } else { - item.userName = userName || 'N/A'; - - User.loadProperties(item.userId, getPropOpts, (err, props) => { - if(!err && props) { - item.location = props.location || 'N/A'; - item.affiliation = item.affils = (props.affiliation || 'N/A'); - } else { - item.location = 'N/A'; - item.affiliation = item.affils = 'N/A'; - } - return next(null); - }); - } - }); - }, - err => { - loginHistory = loginHistory.filter(lh => true !== lh.deleted); - return callback(err); - } - ); + (next) => { + this.fetchHistory( (err, loginHistory) => { + return next(err, loginHistory); + }); }, - function populateList(callback) { - const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}'; - - callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) ); - + (loginHistory, next) => { + this.loadUserForHistoryItems(loginHistory, (err, updatedHistory) => { + return next(err, updatedHistory); + }); + }, + (loginHistory, next) => { + const callersView = this.viewControllers.callers.getView(MciCodeIds.CallerList); + callersView.setItems(loginHistory); callersView.redraw(); - return callback(null); + return next(null); } ], - (err) => { + err => { if(err) { - self.client.log.error( { error : err.toString() }, 'Error loading last callers'); + this.client.log.warn( { error : err.message }, 'Error loading last callers'); } - cb(err); + return cb(err); } ); }); } + + getCollapse(conf) { + let collapse = _.get(this, conf); + collapse = collapse && collapse.match(/^([0-9]+)\s*(minutes|seconds|hours|days|months)$/); + if(collapse) { + return moment.duration(parseInt(collapse[1]), collapse[2]); + } + } + + fetchHistory(cb) { + const callersView = this.viewControllers.callers.getView(MciCodeIds.CallerList); + if(!callersView || 0 === callersView.dimens.height) { + return cb(null); + } + + StatLog.getSystemLogEntries( + 'user_login_history', + StatLog.Order.TimestampDesc, + 200, // max items to fetch - we need more than max displayed for filtering/etc. + (err, loginHistory) => { + if(err) { + return cb(err); + } + + const dateTimeFormat = _.get( + this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateFormat('short')); + + loginHistory = loginHistory.map(item => { + try { + const historyItem = JSON.parse(item.log_value); + if(_.isObject(historyItem)) { + item.userId = historyItem.userId; + item.sessionId = historyItem.sessionId; + } else { + item.userId = historyItem; // older format + item.sessionId = '-none-'; + } + } catch(e) { + return null; // we'll filter this out + } + + item.timestamp = moment(item.timestamp); + + return Object.assign( + item, + { + ts : moment(item.timestamp).format(dateTimeFormat) + } + ); + }); + + const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide'); + const sysOpCollapse = this.getCollapse('menuConfig.config.sysop.collapse'); + + if(hideSysOp) { + loginHistory = loginHistory.filter(item => false === User.isRootUserId(item.userId)); + } else if(sysOpCollapse) { + // :TODO: DRY op & user collapse code + const maxAge = sysOpCollapse.asSeconds(); + let lastUserId; + let lastTimestamp; + + loginHistory = loginHistory.filter(item => { + const op = User.isRootUserId(item.userId); + const repeat = lastUserId === item.userId; + const recent = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).seconds() < maxAge : false; + + lastUserId = item.userId; + lastTimestamp = item.timestamp; + + return !op || !repeat || !recent; + }); + } + + const userCollapse = this.getCollapse('menuConfig.config.user.collapse'); + if(userCollapse) { + const maxAge = userCollapse.asSeconds(); + let lastUserId; + let lastTimestamp; + + loginHistory = loginHistory.filter(item => { + const repeat = lastUserId === item.userId; + const recent = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).seconds() < maxAge : false; + + lastUserId = item.userId; + lastTimestamp = item.timestamp; + + return !repeat || !recent; + }); + } + + return cb( + null, + loginHistory.slice(0, callersView.dimens.height) // trim the fat + ); + } + ); + } + + loadUserForHistoryItems(loginHistory, cb) { + const getPropOpts = { + names : [ 'real_name', 'location', 'affiliation' ] + }; + + const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k); + let indicatorSumsSql; + if(actionIndicatorNames.length > 0) { + indicatorSumsSql = actionIndicatorNames.map(i => { + return `SUM(CASE WHEN log_value='${_.snakeCase(i)}' THEN 1 ELSE 0 END) AS ${i}`; + }); + } + + async.map(loginHistory, (item, next) => { + User.getUserName(item.userId, (err, userName) => { + if(err) { + return cb(null, null); + } + + item.userName = item.text = (userName || 'N/A'); + + User.loadProperties(item.userId, getPropOpts, (err, props) => { + item.location = (props && props.location) || 'N/A'; + item.affiliation = item.affils = (props && props.affiliation) || 'N/A'; + item.realName = (props && props.real_name) || 'N/A'; + + if(!indicatorSumsSql) { + return next(null, item); + } + + sysDb.get( + `SELECT ${indicatorSumsSql.join(', ')} + FROM user_event_log + WHERE user_id=? AND session_id=? + LIMIT 1;`, + [ item.userId, item.sessionId ], + (err, results) => { + if(_.isObject(results)) { + item.actions = ''; + Object.keys(results).forEach(n => { + const indicator = results[n] > 0 ? this.actionIndicators[n] || this.actionIndicatorDefault : this.actionIndicatorDefault; + item[n] = indicator; + item.actions += indicator; + }); + } + return next(null, item); + } + ); + }); + }); + }, + (err, mapped) => { + return cb(err, mapped.filter(item => item)); // remove deleted + }); + } }; diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index dbc782a0..abd04cb1 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -25,7 +25,8 @@ exports.MaskEditTextView = MaskEditTextView; // :TODO: // * Hint, e.g. YYYY/MM/DD // * Return values with literals in place -// +// * Tab in/out results in oddities such as cursor placement & ability to type in non-pattern chars +// * There exists some sort of condition that allows pattern position to get out of sync function MaskEditTextView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); diff --git a/core/module_util.js b/core/module_util.js index 5a575f3e..ae342d4b 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -3,6 +3,7 @@ // ENiGMA½ const Config = require('./config.js').get; +const Log = require('./logger.js').log; // deps const fs = require('graceful-fs'); @@ -10,12 +11,14 @@ const paths = require('path'); const _ = require('lodash'); const assert = require('assert'); const async = require('async'); +const glob = require('glob'); // exports exports.loadModuleEx = loadModuleEx; exports.loadModule = loadModule; exports.loadModulesForCategory = loadModulesForCategory; exports.getModulePaths = getModulePaths; +exports.initializeModules = initializeModules; function loadModuleEx(options, cb) { assert(_.isObject(options)); @@ -108,3 +111,54 @@ function getModulePaths() { config.paths.scannerTossers, ]; } + +function initializeModules(cb) { + const Events = require('./events.js'); + + const modulePaths = getModulePaths().concat(__dirname); + + async.each(modulePaths, (modulePath, nextPath) => { + glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { + if(err) { + return nextPath(err); + } + + const ourPath = paths.join(__dirname, __filename); + + async.each(files, (moduleName, nextModule) => { + const fullModulePath = paths.join(modulePath, moduleName); + if(ourPath === fullModulePath) { + return nextModule(null); + } + + try { + const mod = require(fullModulePath); + + if(_.isFunction(mod.moduleInitialize)) { + const initInfo = { + events : Events, + }; + + mod.moduleInitialize(initInfo, err => { + if(err) { + Log.warn( { error : err.message, modulePath : fullModulePath }, 'Error during "moduleInitialize"'); + } + return nextModule(null); + }); + } else { + return nextModule(null); + } + } catch(e) { + Log.warn( { error : e }, 'Exception during "moduleInitialize"'); + return nextModule(null); + } + }, + err => { + return nextPath(err); + }); + }); + }, + err => { + return cb(err); + }); +} diff --git a/core/nua.js b/core/nua.js index 7eafe16d..18b9a719 100644 --- a/core/nua.js +++ b/core/nua.js @@ -103,7 +103,11 @@ exports.getModule = class NewUserAppModule extends MenuModule { } // :TODO: User.create() should validate email uniqueness! - newUser.create(formData.value.password, err => { + const createUserInfo = { + password : formData.value.password, + sessionId : self.client.session.uniqueId, // used for events/etc. + }; + newUser.create(createUserInfo, err => { if(err) { self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); diff --git a/core/stat_log.js b/core/stat_log.js index 4b076e6d..9e655999 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -252,10 +252,16 @@ class StatLog { appendUserLogEntry(user, logName, logValue, keepDays, cb) { sysDb.run( - `INSERT INTO user_event_log (timestamp, user_id, log_name, log_value) - VALUES (?, ?, ?, ?);`, - [ this.now, user.userId, logName, logValue ], - () => { + `INSERT INTO user_event_log (timestamp, user_id, session_id, log_name, log_value) + VALUES (?, ?, ?, ?, ?);`, + [ this.now, user.userId, user.sessionId, logName, logValue ], + err => { + if(err) { + if(cb) { + cb(err); + } + return; + } // // Handle keepDays // @@ -280,6 +286,33 @@ class StatLog { } ); } + + initUserEvents(cb) { + // + // We map some user events directly to user stat log entries such that they + // are persisted for a time. + // + const Events = require('./events.js'); + const systemEvents = Events.getSystemEvents(); + + const interestedEvents = [ + systemEvents.NewUser, + systemEvents.UserUpload, systemEvents.UserDownload, + systemEvents.UserPostMessage, systemEvents.UserSendMail, + systemEvents.UserRunDoor, + ]; + + Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => { + this.appendUserLogEntry( + event.user, + 'system_event', + eventName.replace(/^codes\.l33t\.enigma\.system\./, ''), // strip package name prefix + 90 + ); + }); + + return cb(null); + } } module.exports = new StatLog(); diff --git a/core/user.js b/core/user.js index e3314cc7..18acc02c 100644 --- a/core/user.js +++ b/core/user.js @@ -179,7 +179,7 @@ module.exports = class User { ); } - create(password, cb) { + create(createUserInfo , cb) { assert(0 === this.userId); const config = Config(); @@ -219,7 +219,7 @@ module.exports = class User { ); }, function genAuthCredentials(trans, callback) { - User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { + User.generatePasswordDerivedKeyAndSalt(createUserInfo.password, (err, info) => { if(err) { return callback(err); } @@ -244,7 +244,12 @@ module.exports = class User { }); }, function sendEvent(trans, callback) { - Events.emit(Events.getSystemEvents().NewUser, { user : self }); + Events.emit( + Events.getSystemEvents().NewUser, + { + user : Object.assign({}, self, { sessionId : createUserInfo.sessionId } ) + } + ); return callback(null, trans); } ], diff --git a/core/user_login.js b/core/user_login.js index 07a0a2d2..2030152d 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -87,7 +87,11 @@ function userLogin(client, username, password, cb) { }, function recordLoginHistory(callback) { const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers - return StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); + const historyItem = JSON.stringify({ + userId : user.userId, + sessionId : user.sessionId, + }); + return StatLog.appendSystemLogEntry('user_login_history', historyItem, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); } ], err => { diff --git a/docs/modding/last-callers.md b/docs/modding/last-callers.md new file mode 100644 index 00000000..1f7aee8e --- /dev/null +++ b/docs/modding/last-callers.md @@ -0,0 +1,34 @@ +--- +layout: page +title: Last Callers +--- +## The Last Callers Module +The built in `last_callers` module provides flexible retro last callers mod. + +## Configuration +### Config Block +Available `config` block entries: +* `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` format. +* `user`: User options: + * `collapse`: Collapse or roll up entries that fall within the period specified. May be a string in the form of `30 minutes`, `3 weeks`, `1 hour`, etc. +* `sysop`: Sysop options: + * `collapse`: Collapse or roll up entries that fall within the period specified. May be a string in the form of `30 minutes`, `3 weeks`, `1 hour`, etc. + * `hide`: Hide all +op logins +* `actionIndicators`: Maps user actions to indicators. For example: `userDownload` to "D". Available indicators: + * `userDownload` + * `userUpload` + * `userPostMsg` + * `userSendMail` + * `userRunDoor` +* `actionIndicatorDefault`: Default indicator when an action is not set. Defaults to "-". + +### Theming +When in a list view, the following `itemFormat` object is provided: +* `userId`: User ID. +* `realName`: User's real name or "N/A". +* `ts`: Timestamp in `dateTimeFormat` format. +* `location`: User's location or "N/A". +* `affiliation` or `affils`: Users affiliations or "N/A". +* `actions`: A string built by concatenating action indicators for a users logged in session. For example, given a indincator of `userDownload` mapped to "D", the string may be "-D----". The format was made popular on Amiga style boards. + + From f1ffd958a53ca5d459aafb8c346584a5870b497f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 14:36:38 -0600 Subject: [PATCH 222/569] Fix luciano_blocktronics last allers theme with new changes --- art/themes/luciano_blocktronics/theme.hjson | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index c0735e6f..d1c11297 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -143,11 +143,14 @@ mainMenuLastCallers: { config: { - listFormat: "|00|11{userName:<17.17}|03{location:<20.20}|11{affils:<17.17}|03{ts:<15}" dateTimeFormat: MMM Do h:mma } mci: { - VM1: { height: 10, width: 20 } + VM1: { + height: 10, + width: 20 + itemFormat: "|00|11{userName:<17.16} |03{location:<20.19} |11{affils:<18.17} |03{ts:<15}" + } } } @@ -422,11 +425,14 @@ fullLoginSequenceLastCallers: { config: { - listFormat: "|00|11{userName:<17.17}|03{location:<20.20}|11{affils:<17.17}|03{ts:<15}" dateTimeFormat: MMM Do h:mma } mci: { - VM1: { height: 10, width: 20 } + VM1: { + height: 10, + width: 20 + itemFormat: "|00|11{userName:<17.16} |03{location:<20.19} |11{affils:<18.17} |03{ts:<15}" + } } } From bc9a833f1d3b29921c682b4175b6329dcdaf9526 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 15:28:55 -0600 Subject: [PATCH 223/569] Add last callers to nav --- docs/_includes/nav.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index dacb2f00..9e5b432c 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -64,6 +64,7 @@ - Combatnet - Exodus - [Existing Mods]({{ site.baseurl }}{% link modding/existing-mods.md %}) + - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) - [Oputil]({{ site.baseurl }}{% link oputil/index.md %}) From 48f7456d4aa334a8e56aabf8e0808f6a15d5fcfb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 15:31:55 -0600 Subject: [PATCH 224/569] Minor doc updates on terms --- docs/installation/testing.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/installation/testing.md b/docs/installation/testing.md index 9505f863..9238fc61 100644 --- a/docs/installation/testing.md +++ b/docs/installation/testing.md @@ -41,4 +41,7 @@ If you don't have any telnet software, these are compatible with ENiGMA½: * [SyncTERM](http://syncterm.bbsdev.net/) * [EtherTerm](https://github.com/M-griffin/EtherTerm) -* [NetRunner](http://mysticbbs.com/downloads.html) \ No newline at end of file +* [NetRunner](http://mysticbbs.com/downloads.html) +* [MagiTerm](https://magickabbs.com/index.php/magiterm/) +* [VTX](https://github.com/codewar65/VTX_ClientServer) (Browser based) +* [fTelnet](https://www.ftelnet.ca/) (Browser based) \ No newline at end of file From f601fd256b3d19a3ab9ca888dcd7db71289c8503 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 21:38:06 -0600 Subject: [PATCH 225/569] Fix last_callers collapsing & DRY code --- core/last_callers.js | 48 ++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/core/last_callers.js b/core/last_callers.js index 84ca01c3..ebc98d5f 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -124,41 +124,31 @@ exports.getModule = class LastCallersModule extends MenuModule { const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide'); const sysOpCollapse = this.getCollapse('menuConfig.config.sysop.collapse'); + const collapseList = (withUserId, minAge) => { + let lastUserId; + let lastTimestamp; + loginHistory = loginHistory.filter(item => { + const secApart = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).asSeconds() : 0; + const collapse = (null === withUserId ? true : withUserId === item.userId) && + (lastUserId === item.userId) && + (secApart < minAge); + + lastUserId = item.userId; + lastTimestamp = item.timestamp; + + return !collapse; + }); + }; + if(hideSysOp) { loginHistory = loginHistory.filter(item => false === User.isRootUserId(item.userId)); } else if(sysOpCollapse) { - // :TODO: DRY op & user collapse code - const maxAge = sysOpCollapse.asSeconds(); - let lastUserId; - let lastTimestamp; - - loginHistory = loginHistory.filter(item => { - const op = User.isRootUserId(item.userId); - const repeat = lastUserId === item.userId; - const recent = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).seconds() < maxAge : false; - - lastUserId = item.userId; - lastTimestamp = item.timestamp; - - return !op || !repeat || !recent; - }); + collapseList(User.RootUserID, sysOpCollapse.asSeconds()); } - const userCollapse = this.getCollapse('menuConfig.config.user.collapse'); + const userCollapse = this.getCollapse('menuConfig.config.user.collapse'); if(userCollapse) { - const maxAge = userCollapse.asSeconds(); - let lastUserId; - let lastTimestamp; - - loginHistory = loginHistory.filter(item => { - const repeat = lastUserId === item.userId; - const recent = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).seconds() < maxAge : false; - - lastUserId = item.userId; - lastTimestamp = item.timestamp; - - return !repeat || !recent; - }); + collapseList(null, userCollapse.asSeconds()); } return cb( From e732d2b10db03d4e70d268a3db706f430a15db95 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 23:57:59 -0600 Subject: [PATCH 226/569] Code update + use 'itemFormat' standard --- core/whos_online.js | 54 ++++++++++++++------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/core/whos_online.js b/core/whos_online.js index 28b71703..db3fc5c4 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -2,10 +2,9 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const getActiveNodeList = require('./client_connections.js').getActiveNodeList; -const stringFormat = require('./string_format.js'); +const { MenuModule } = require('./menu_module.js'); +const { getActiveNodeList } = require('./client_connections.js'); +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); @@ -33,48 +32,29 @@ exports.getModule = class WhosOnlineModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - async.series( [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, - }; - - return vc.loadFromMenuConfig(loadOpts, callback); + (next) => { + return this.prepViewController('online', 0, mciData.menu, next); }, - function populateList(callback) { - const onlineListView = vc.getView(MciViewIds.OnlineList); - const listFormat = self.menuConfig.config.listFormat || '{node} - {userName} - {action} - {timeOn}'; - const nonAuthUser = self.menuConfig.config.nonAuthUser || 'Logging In'; - const otherUnknown = self.menuConfig.config.otherUnknown || 'N/A'; - const onlineList = getActiveNodeList(self.menuConfig.config.authUsersOnly).slice(0, onlineListView.height); + (next) => { + const onlineListView = this.viewControllers.online.getView(MciViewIds.OnlineList); + if(!onlineListView) { + return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.OnlineList}`)); + } - onlineListView.setItems(_.map(onlineList, oe => { - if(oe.authenticated) { - oe.timeOn = _.upperFirst(oe.timeOn.humanize()); - } else { - [ 'realName', 'location', 'affils', 'timeOn' ].forEach(m => { - oe[m] = otherUnknown; - }); - oe.userName = nonAuthUser; - } - return stringFormat(listFormat, oe); - })); + const onlineList = getActiveNodeList(true).slice(0, onlineListView.height).map( + oe => Object.assign(oe, { timeOn : _.upperFirst(oe.timeOn.humanize()) }) + ); - onlineListView.focusItems = onlineListView.items; + onlineListView.setItems(onlineList); onlineListView.redraw(); - - return callback(null); + return next(null); } ], - function complete(err) { + err => { if(err) { - self.client.log.error( { error : err.message }, 'Error loading who\'s online'); + this.client.log.error( { error : err.message }, 'Error loading who\'s online'); } return cb(err); } From 28afc56d84aea3db91bd1ec27e1b3394dfd64939 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 23:59:14 -0600 Subject: [PATCH 227/569] Update luciano_blocktronics theme for Who's Online 'itemFormat' --- art/themes/luciano_blocktronics/theme.hjson | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index d1c11297..bf28baab 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -166,11 +166,12 @@ } mainMenuWhosOnline: { - config: { - listFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}" - } mci: { - VM1: { height: 10, width: 20 } + VM1: { + height: 10, + width: 20 + itemFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}" + } } } @@ -437,11 +438,12 @@ } fullLoginSequenceWhosOnline: { - config: { - listFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}" - } mci: { - VM1: { height: 10, width: 20 } + VM1: { + height: 10, + width: 20 + itemFormat: "|00|03{node:<6.6}|11{userName:<17.17}|03{affils:<19.19}|11{action:<20.20}|03{timeOn:<8}" + } } } From 0d7a20027cee9aca663b10ca52ac3bf99c7ebb78 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 10:55:39 -0600 Subject: [PATCH 228/569] Add Who's online docs, minor doc updates to last callers, and code cleanup --- core/client_connections.js | 4 ++-- docs/modding/last-callers.md | 2 +- docs/modding/whos-online.md | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 docs/modding/whos-online.md diff --git a/core/client_connections.js b/core/client_connections.js index 4c16d610..93bb9465 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -38,7 +38,7 @@ function getActiveNodeList(authUsersOnly) { node : ac.node, authenticated : ac.user.isAuthenticated(), userId : ac.user.userId, - action : _.has(ac, 'currentMenuModule.menuConfig.desc') ? ac.currentMenuModule.menuConfig.desc : 'Unknown', + action : _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'), }; // @@ -48,7 +48,7 @@ function getActiveNodeList(authUsersOnly) { entry.userName = ac.user.username; entry.realName = ac.user.properties.real_name; entry.location = ac.user.properties.location; - entry.affils = ac.user.properties.affiliation; + entry.affils = entry.affiliation = ac.user.properties.affiliation; const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); entry.timeOn = moment.duration(diff, 'minutes'); diff --git a/docs/modding/last-callers.md b/docs/modding/last-callers.md index 1f7aee8e..6e18d346 100644 --- a/docs/modding/last-callers.md +++ b/docs/modding/last-callers.md @@ -23,7 +23,7 @@ Available `config` block entries: * `actionIndicatorDefault`: Default indicator when an action is not set. Defaults to "-". ### Theming -When in a list view, the following `itemFormat` object is provided: +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `userId`: User ID. * `realName`: User's real name or "N/A". * `ts`: Timestamp in `dateTimeFormat` format. diff --git a/docs/modding/whos-online.md b/docs/modding/whos-online.md new file mode 100644 index 00000000..feec5f7e --- /dev/null +++ b/docs/modding/whos-online.md @@ -0,0 +1,17 @@ +--- +layout: page +title: Who's Online +--- +## The Who's OnlineModule +The built in `whos_online` module provides a basic who's online mod. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): +* `userId`: User ID. +* `node`: Node ID the user is connected to. +* `timeOn`: A human friendly amount of time the user has been online. +* `realName`: User's real name. +* `location`: User's location. +* `affiliation` or `affils`: Users affiliations. +* `action`: Current action/view in the system taken from the `desc` field of the current MenuModule they are interacting with. For example, "Playing L.O.R.D". + From 2e275600b13b3b4041515b06719777e51a560e2d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 12:08:30 -0600 Subject: [PATCH 229/569] Minor doc update --- docs/_includes/nav.md | 1 + docs/modding/last-callers.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 9e5b432c..d781ed45 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -65,6 +65,7 @@ - Exodus - [Existing Mods]({{ site.baseurl }}{% link modding/existing-mods.md %}) - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) + - [Who's Online]({{ site.baseurl }}{% link modding/whos-online.md %}) - [Oputil]({{ site.baseurl }}{% link oputil/index.md %}) diff --git a/docs/modding/last-callers.md b/docs/modding/last-callers.md index 6e18d346..8778b1e0 100644 --- a/docs/modding/last-callers.md +++ b/docs/modding/last-callers.md @@ -22,6 +22,8 @@ Available `config` block entries: * `userRunDoor` * `actionIndicatorDefault`: Default indicator when an action is not set. Defaults to "-". +Remember that entries such as `actionIndicators` and `actionIndicatorDefault` may contain pipe color codes! + ### Theming The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `userId`: User ID. From e6a812cf34575f89acd46e95369fb88f2b4740d9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 12:56:56 -0600 Subject: [PATCH 230/569] Standardization work on built in user list module plus docs & code cleanup * More docs, fix some info * Code cleanup --- art/themes/luciano_blocktronics/theme.hjson | 9 +- core/last_callers.js | 28 +++--- core/user.js | 10 ++- core/user_list.js | 96 +++++++++------------ core/whos_online.js | 6 +- docs/_includes/nav.md | 1 + docs/modding/last-callers.md | 7 +- docs/modding/user-list.md | 24 ++++++ docs/modding/whos-online.md | 1 + 9 files changed, 102 insertions(+), 80 deletions(-) create mode 100644 docs/modding/user-list.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index bf28baab..6555f417 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -156,12 +156,15 @@ mainMenuUserList: { config: { - listFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{note:<19.19}|03{lastLoginTs}" - focusListFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{note:<19.19}{lastLoginTs}" dateTimeFormat: MMM Do h:mma } mci: { - VM1: { height: 15, width: 50} + VM1: { + height: 15, + width: 50 + itemFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{note:<19.19}|03{lastLoginTs}" + focusItemFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{note:<19.19}{lastLoginTs}" + } } } diff --git a/core/last_callers.js b/core/last_callers.js index ebc98d5f..268fde5f 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -2,10 +2,11 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const StatLog = require('./stat_log.js'); -const User = require('./user.js'); -const sysDb = require('./database.js').dbs.system; +const { MenuModule } = require('./menu_module.js'); +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const sysDb = require('./database.js').dbs.system; +const { Errors } = require('./enig_error.js'); // deps const moment = require('moment'); @@ -19,8 +20,8 @@ exports.moduleInfo = { packageName : 'codes.l33t.enigma.lastcallers' }; -const MciCodeIds = { - CallerList : 1, +const MciViewIds = { + callerList : 1, }; exports.getModule = class LastCallersModule extends MenuModule { @@ -55,7 +56,10 @@ exports.getModule = class LastCallersModule extends MenuModule { }); }, (loginHistory, next) => { - const callersView = this.viewControllers.callers.getView(MciCodeIds.CallerList); + const callersView = this.viewControllers.callers.getView(MciViewIds.callerList); + if(!callersView) { + return cb(Errors.MissingMci(`Missing caller list MCI ${MciViewIds.callerList}`)); + } callersView.setItems(loginHistory); callersView.redraw(); return next(null); @@ -80,7 +84,7 @@ exports.getModule = class LastCallersModule extends MenuModule { } fetchHistory(cb) { - const callersView = this.viewControllers.callers.getView(MciCodeIds.CallerList); + const callersView = this.viewControllers.callers.getView(MciViewIds.callerList); if(!callersView || 0 === callersView.dimens.height) { return cb(null); } @@ -178,12 +182,12 @@ exports.getModule = class LastCallersModule extends MenuModule { return cb(null, null); } - item.userName = item.text = (userName || 'N/A'); + item.userName = item.text = userName; User.loadProperties(item.userId, getPropOpts, (err, props) => { - item.location = (props && props.location) || 'N/A'; - item.affiliation = item.affils = (props && props.affiliation) || 'N/A'; - item.realName = (props && props.real_name) || 'N/A'; + item.location = (props && props.location) || ''; + item.affiliation = item.affils = (props && props.affiliation) || ''; + item.realName = (props && props.real_name) || ''; if(!indicatorSumsSql) { return next(null, item); diff --git a/core/user.js b/core/user.js index 18acc02c..6c2b964d 100644 --- a/core/user.js +++ b/core/user.js @@ -537,8 +537,8 @@ module.exports = class User { } static getUserList(options, cb) { - let userList = []; - let orderClause = 'ORDER BY ' + (options.order || 'user_name'); + const userList = []; + const orderClause = 'ORDER BY ' + (options.order || 'user_name'); userDb.each( `SELECT id, user_name @@ -562,7 +562,11 @@ module.exports = class User { [ user.userId ], (err, row) => { if(row) { - user[row.prop_name] = row.prop_value; + if(options.propsCamelCase) { + user[_.camelCase(row.prop_name)] = row.prop_value; + } else { + user[row.prop_name] = row.prop_value; + } } }, err => { diff --git a/core/user_list.js b/core/user_list.js index 8af9690b..43922998 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -1,11 +1,12 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; -const User = require('./user.js'); -const ViewController = require('./view_controller.js').ViewController; -const stringFormat = require('./string_format.js'); +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const { getUserList } = require('./user.js'); +const { Errors } = require('./enig_error.js'); +// deps const moment = require('moment'); const async = require('async'); const _ = require('lodash'); @@ -29,7 +30,7 @@ exports.moduleInfo = { }; const MciViewIds = { - UserList : 1, + userList : 1, }; exports.getModule = class UserListModule extends MenuModule { @@ -43,68 +44,51 @@ exports.getModule = class UserListModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - - let userList = []; - - const USER_LIST_OPTS = { - properties : [ 'location', 'affiliation', 'last_login_timestamp' ], - }; - async.series( [ - function loadFromConfig(callback) { - var loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - }; - - vc.loadFromMenuConfig(loadOpts, callback); + (next) => { + return this.prepViewController('userList', 0, mciData.menu, next); }, - function fetchUserList(callback) { - // :TODO: Currently fetching all users - probably always OK, but this could be paged - User.getUserList(USER_LIST_OPTS, function got(err, ul) { - userList = ul; - callback(err); - }); - }, - function populateList(callback) { - var userListView = vc.getView(MciViewIds.UserList); - - var listFormat = self.menuConfig.config.listFormat || '{userName} - {affils}'; - var focusListFormat = self.menuConfig.config.focusListFormat || listFormat; // :TODO: default changed color! - var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; - - function getUserFmtObj(ue) { - return { - userId : ue.userId, - userName : ue.userName, - affils : ue.affiliation, - location : ue.location, - // :TODO: the rest! - note : ue.note || '', - lastLoginTs : moment(ue.last_login_timestamp).format(dateTimeFormat), - }; + (next) => { + const userListView = this.viewControllers.userList.getView(MciViewIds.userList); + if(!userListView) { + return cb(Errors.MissingMci(`Missing user list MCI ${MciViewIds.userList}`)); } - userListView.setItems(_.map(userList, function formatUserEntry(ue) { - return stringFormat(listFormat, getUserFmtObj(ue)); - })); + const fetchOpts = { + properties : [ 'real_name', 'location', 'affiliation', 'last_login_timestamp' ], + propsCamelCase : true, // e.g. real_name -> realName + }; + getUserList(fetchOpts, (err, userList) => { + if(err) { + return next(err); + } - userListView.setFocusItems(_.map(userList, function formatUserEntry(ue) { - return stringFormat(focusListFormat, getUserFmtObj(ue)); - })); + const dateTimeFormat = _.get( + this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateTimeFormat('short')); - userListView.redraw(); - callback(null); + userList = userList.map(entry => { + return Object.assign( + entry, + { + text : entry.userName, + affils : entry.affiliation, + lastLoginTs : moment(entry.lastLoginTimestamp).format(dateTimeFormat), + } + ); + }); + + userListView.setItems(userList); + userListView.redraw(); + return next(null); + }); } ], - function complete(err) { + err => { if(err) { - self.client.log.error( { error : err.toString() }, 'Error loading user list'); + this.client.log.error( { error : err.message }, 'Error loading user list'); } - cb(err); + return cb(err); } ); }); diff --git a/core/whos_online.js b/core/whos_online.js index db3fc5c4..1078bc51 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -38,13 +38,13 @@ exports.getModule = class WhosOnlineModule extends MenuModule { return this.prepViewController('online', 0, mciData.menu, next); }, (next) => { - const onlineListView = this.viewControllers.online.getView(MciViewIds.OnlineList); + const onlineListView = this.viewControllers.online.getView(MciViewIds.onlineList); if(!onlineListView) { - return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.OnlineList}`)); + return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.onlineList}`)); } const onlineList = getActiveNodeList(true).slice(0, onlineListView.height).map( - oe => Object.assign(oe, { timeOn : _.upperFirst(oe.timeOn.humanize()) }) + oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) }) ); onlineListView.setItems(onlineList); diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index d781ed45..d7ada9e2 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -66,6 +66,7 @@ - [Existing Mods]({{ site.baseurl }}{% link modding/existing-mods.md %}) - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) - [Who's Online]({{ site.baseurl }}{% link modding/whos-online.md %}) + - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) - [Oputil]({{ site.baseurl }}{% link oputil/index.md %}) diff --git a/docs/modding/last-callers.md b/docs/modding/last-callers.md index 8778b1e0..f754a912 100644 --- a/docs/modding/last-callers.md +++ b/docs/modding/last-callers.md @@ -27,10 +27,11 @@ Remember that entries such as `actionIndicators` and `actionIndicatorDefault` ma ### Theming The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `userId`: User ID. -* `realName`: User's real name or "N/A". +* `userName`: Login username. +* `realName`: User's real name. * `ts`: Timestamp in `dateTimeFormat` format. -* `location`: User's location or "N/A". -* `affiliation` or `affils`: Users affiliations or "N/A". +* `location`: User's location. +* `affiliation` or `affils`: Users affiliations. * `actions`: A string built by concatenating action indicators for a users logged in session. For example, given a indincator of `userDownload` mapped to "D", the string may be "-D----". The format was made popular on Amiga style boards. diff --git a/docs/modding/user-list.md b/docs/modding/user-list.md new file mode 100644 index 00000000..9aae4750 --- /dev/null +++ b/docs/modding/user-list.md @@ -0,0 +1,24 @@ +--- +layout: page +title: User List +--- +## The User List Module +The built in `user_list` module provides basic user list functionality. + +## Configuration +### Config Block +Available `config` block entries: +* `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` format. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): +* `userId`: User ID. +* `userName`: Login username. +* `realName`: User's real name. +* `lastLoginTimestamp`: Full last login timestamp for formatting use. +* `lastLoginTs`: Last login timestamp formatted with `dateTimeFormat` style. +* `location`: User's location. +* `affiliation` or `affils`: Users affiliations. + + + diff --git a/docs/modding/whos-online.md b/docs/modding/whos-online.md index feec5f7e..87a2b1ef 100644 --- a/docs/modding/whos-online.md +++ b/docs/modding/whos-online.md @@ -8,6 +8,7 @@ The built in `whos_online` module provides a basic who's online mod. ### Theming The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `userId`: User ID. +* `userName`: Login username. * `node`: Node ID the user is connected to. * `timeOn`: A human friendly amount of time the user has been online. * `realName`: User's real name. From 39b35e8d116252e6a2c6523d88bb7d52c4c9ef91 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 12:59:43 -0600 Subject: [PATCH 231/569] Fix MCI ref --- core/whos_online.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/whos_online.js b/core/whos_online.js index 1078bc51..0ea2321a 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -18,7 +18,7 @@ exports.moduleInfo = { }; const MciViewIds = { - OnlineList : 1, + onlineList : 1, }; exports.getModule = class WhosOnlineModule extends MenuModule { From c5998aa3439c7047e26d3c487cdf78ef8cebf753 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 15:57:51 -0600 Subject: [PATCH 232/569] Add prevMenuOnTimeout() method --- core/menu_module.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/menu_module.js b/core/menu_module.js index 67fabb7e..992c17fb 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -193,6 +193,12 @@ exports.MenuModule = class MenuModule extends PluginModule { return this.client.menuStack.goto(prevMenu.name, cb); } + prevMenuOnTimeout(timeout, cb) { + setTimeout( () => { + return this.prevMenu(cb); + }, timeout); + } + addViewController(name, vc) { assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); From 601433be40618716bbc279ae5133447638f0e53d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 15:58:13 -0600 Subject: [PATCH 233/569] Add messageConf support --- core/show_art.js | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/core/show_art.js b/core/show_art.js index 66e330db..8f323e49 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -43,6 +43,8 @@ exports.getModule = class ShowArtModule extends MenuModule { sequence : self.showBySequence, random : self.showByRandom, fileBaseArea : self.showByFileBaseArea, + messageConf : self.showByMessageConf, + messageArea : self.showByMessageArea, }[self.config.method] || self.showRandomArt; handler = handler.bind(self); @@ -88,20 +90,36 @@ exports.getModule = class ShowArtModule extends MenuModule { if(err) { return cb(err); } - - // further resolve key -> file base area art - const artSpec = _.get(Config(), [ 'fileBase', 'areas', key, 'art' ]); - if(!artSpec) { - return cb(Errors.MissingConfig(`No art defined for file base area "${key}"`)); - } - const options = { - pause : this.shouldPause(), - desc : 'fileBaseArea', - }; - return this.displaySingleArtWithOptions(artSpec, options, cb); + return this.displaySingleArtByConfigPath( [ 'fileBase', 'areas', key, 'art' ], cb); }); } + showByMessageConf(cb) { + this.getArtKeyValue( (err, key) => { + if(err) { + return cb(err); + } + return this.displaySingleArtByConfigPath( [ 'messageConferences', key, 'art' ], cb); + }); + } + + showByMessageArea(cb) { + return cb(null); // NYI + } + + displaySingleArtByConfigPath(configPath, cb) { + const desc = configPath.join('.'); + const artSpec = _.get(Config(), configPath); + if(!artSpec) { + return cb(Errors.MissingConfig(`No art defined at path ${desc}`)); + } + const options = { + desc, + pause : this.shouldPause(), + }; + return this.displaySingleArtWithOptions(artSpec, options, cb); + } + getArtKeyValue(cb) { const key = this.config.key; if(!_.isString(key)) { From 9f85a01a89e9821855f3182473181873feb580a1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 15:58:39 -0600 Subject: [PATCH 234/569] Remove comment on formatting - see docs --- core/user_list.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/core/user_list.js b/core/user_list.js index 43922998..6f005432 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -11,18 +11,6 @@ const moment = require('moment'); const async = require('async'); const _ = require('lodash'); -/* - Available listFormat/focusListFormat object members: - - userId : User ID - userName : User name/handle - lastLoginTs : Last login timestamp - status : Status: active | inactive - location : Location - affiliation : Affils - note : User note -*/ - exports.moduleInfo = { name : 'User List', desc : 'Lists all system users', From c3bd03650905fba644b5530fd22527726bb99a76 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 15:59:00 -0600 Subject: [PATCH 235/569] Update message conf list with standardized + custom formats --- art/themes/luciano_blocktronics/theme.hjson | 6 +- config/menu.hjson | 13 ++ core/msg_area_list.js | 6 - core/msg_conf_list.js | 159 ++++++++------------ docs/_includes/nav.md | 1 + docs/modding/msg-conf-list.md | 18 +++ docs/modding/whos-online.md | 2 +- 7 files changed, 101 insertions(+), 104 deletions(-) create mode 100644 docs/modding/msg-conf-list.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 6555f417..2733e4c8 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -235,14 +235,12 @@ } messageAreaChangeCurrentConference: { - config: { - listFormat: "|00|15{index} |07- |03{name}" - focusListFormat: "|00|19|15{index} - {name}" - } mci: { VM1: { width: 26 height: 19 + itemFormat: "|00|15{index} |07- |03{name}" + focusItemFormat: "|00|19|15{index} - {name}" } } } diff --git a/config/menu.hjson b/config/menu.hjson index 8a14526b..1eb71f7b 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1907,6 +1907,19 @@ } } + messageAreaChangeConfPreArt: { + module: show_art + config: { + method: messageConf + key: confTag + } + options: { + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + messageAreaChangeCurrentArea: { // :TODO: rename this art to ACHANGE art: CHANGE diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 1b9870bd..a6ee7cc1 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -93,12 +93,6 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }; } - prevMenuOnTimeout(timeout, cb) { - setTimeout( () => { - return this.prevMenu(cb); - }, timeout); - } - // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! updateGeneralAreaInfoViews(areaIndex) { /* diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index c20d06ca..c0340fe4 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -2,12 +2,9 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const messageArea = require('./message_area.js'); -const displayThemeArt = require('./theme.js').displayThemeArt; -const resetScreen = require('./ansi_term.js').resetScreen; -const stringFormat = require('./string_format.js'); +const { MenuModule } = require('./menu_module.js'); +const messageArea = require('./message_area.js'); +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); @@ -20,57 +17,40 @@ exports.moduleInfo = { }; const MciViewIds = { - ConfList : 1, - - // :TODO: - // # areas in conf .... see Obv/2, iNiQ, ... - // + confList : 1, + confDesc : 2, // description updated @ index update + customRangeStart : 10, // updated @ index update }; exports.getModule = class MessageConfListModule extends MenuModule { constructor(options) { super(options); - this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client); - const self = this; + this.initList(); this.menuMethods = { - changeConference : function(formData, extraArgs, cb) { + changeConference : (formData, extraArgs, cb) => { if(1 === formData.submitId) { - let conf = self.messageConfs[formData.value.conf]; - const confTag = conf.confTag; - conf = conf.conf; // what we want is embedded + const conf = this.messageConfs[formData.value.conf]; - messageArea.changeMessageConference(self.client, confTag, err => { + messageArea.changeMessageConference(this.client, conf.confTag, err => { if(err) { - self.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); - - setTimeout( () => { - return self.prevMenu(cb); - }, 1000); - } else { - if(_.isString(conf.art)) { - const dispOptions = { - client : self.client, - name : conf.art, - }; - - self.client.term.rawWrite(resetScreen()); - - displayThemeArt(dispOptions, () => { - // pause by default, unless explicitly told not to - if(_.has(conf, 'options.pause') && false === conf.options.pause) { - return self.prevMenuOnTimeout(1000, cb); - } else { - self.pausePrompt( () => { - return self.prevMenu(cb); - }); - } - }); - } else { - return self.prevMenu(cb); - } + this.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); + return this.prevMenuOnTimeout(1000, cb); } + + if(conf.hasArt) { + const menuOpts = { + extraArgs : { + confTag : conf.confTag, + }, + menuFlags : [ 'popParent', 'noHistory' ] + }; + + return this.gotoMenu(this.menuConfig.config.changeConfPreArtMenu || 'messageAreaChangeConfPreArt', menuOpts, cb); + } + + return this.prevMenu(cb); }); } else { return cb(null); @@ -79,70 +59,63 @@ exports.getModule = class MessageConfListModule extends MenuModule { }; } - prevMenuOnTimeout(timeout, cb) { - setTimeout( () => { - return this.prevMenu(cb); - }, timeout); - } - mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { return cb(err); } - const self = this; - const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); - async.series( [ - function loadFromConfig(callback) { - let loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - formId : 0, - }; - - vc.loadFromMenuConfig(loadOpts, callback); + (next) => { + return this.prepViewController('confList', 0, mciData.menu, next); }, - function populateConfListView(callback) { - const listFormat = self.menuConfig.config.listFormat || '{index} ) - {name}'; - const focusListFormat = self.menuConfig.config.focusListFormat || listFormat; + (next) => { + const confListView = this.viewControllers.confList.getView(MciViewIds.confList); + if(!confListView) { + return cb(Errors.MissingMci(`Missing conf list MCI ${MciViewIds.onlineList}`)); + } - const confListView = vc.getView(MciViewIds.ConfList); - let i = 1; - confListView.setItems(_.map(self.messageConfs, v => { - return stringFormat(listFormat, { - index : i++, - confTag : v.conf.confTag, - name : v.conf.name, - desc : v.conf.desc, - }); - })); - - i = 1; - confListView.setFocusItems(_.map(self.messageConfs, v => { - return stringFormat(focusListFormat, { - index : i++, - confTag : v.conf.confTag, - name : v.conf.name, - desc : v.conf.desc, - }); - })); + confListView.on('index update', idx => { + this.selectionIndexUpdate(idx); + }); + confListView.setItems(this.messageConfs); confListView.redraw(); - - callback(null); - }, - function populateTextViews(callback) { - // :TODO: populate other avail MCI, e.g. current conf name - callback(null); + return next(null); } ], - function complete(err) { - cb(err); + err => { + if(err) { + this.client.log.error( { error : err.message }, 'Failed loading message conference list'); + } } ); }); } + + selectionIndexUpdate(idx) { + const conf = this.messageConfs[idx]; + if(!conf) { + return; + } + this.setViewText('confList', MciViewIds.confDesc, conf.desc); + this.updateCustomViewTextsWithFilter('confList', MciViewIds.customRangeStart, conf); + } + + initList() + { + let index = 1; + this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client).map(conf => { + return { + index : index++, + confTag : conf.confTag, + name : conf.conf.name, + text : conf.conf.name, + desc : conf.conf.desc, + areaCount : Object.keys(conf.conf.areas || {}).length, + hasArt : _.isString(conf.conf.art), + }; + }); + } }; diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index d7ada9e2..5af16603 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -67,6 +67,7 @@ - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) - [Who's Online]({{ site.baseurl }}{% link modding/whos-online.md %}) - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) + - [Message Conference List]({{ site.baseurl }}{% link modding/msg-conf-list.md %}) - [Oputil]({{ site.baseurl }}{% link oputil/index.md %}) diff --git a/docs/modding/msg-conf-list.md b/docs/modding/msg-conf-list.md new file mode 100644 index 00000000..c00c890d --- /dev/null +++ b/docs/modding/msg-conf-list.md @@ -0,0 +1,18 @@ +--- +layout: page +title: Message Conference List +--- +## The Message Conference List Module +The built in `msg_conf_list` module provides a menu to display and change between message conferences. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): +* `index`: 1-based index into list. +* `confTag`: Conference tag. +* `name` or `text`: Display name. +* `desc`: Description. +* `areaCount`: Number of areas in this conference. + +The following additional MCIs are updated as the user changes selections in the main list: +* MCI 2 (ie: `%TL2` or `%M%2`) is updated with the conference description. +* MCI 10+ (ie `%TL10`...) are custom ranges updated with the same information available above in `itemFormat`. diff --git a/docs/modding/whos-online.md b/docs/modding/whos-online.md index 87a2b1ef..22fde0ff 100644 --- a/docs/modding/whos-online.md +++ b/docs/modding/whos-online.md @@ -2,7 +2,7 @@ layout: page title: Who's Online --- -## The Who's OnlineModule +## The Who's Online Module The built in `whos_online` module provides a basic who's online mod. ### Theming From c625d25e2a9c1a5d9d00c81c4e8ddb87c5ee52d3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 19:06:43 -0600 Subject: [PATCH 236/569] Clean up msg_area_list module (standardize/etc.), update and add docs --- config/menu.hjson | 2 +- core/msg_area_list.js | 167 +++++++++++++--------------------- core/msg_conf_list.js | 5 +- core/show_art.js | 15 ++- docs/_includes/nav.md | 1 + docs/modding/msg-area-list.md | 17 ++++ 6 files changed, 101 insertions(+), 106 deletions(-) create mode 100644 docs/modding/msg-area-list.md diff --git a/config/menu.hjson b/config/menu.hjson index 1eb71f7b..b857dd38 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1907,7 +1907,7 @@ } } - messageAreaChangeConfPreArt: { + changeMessageConfPreArt: { module: show_art config: { method: messageConf diff --git a/core/msg_area_list.js b/core/msg_area_list.js index a6ee7cc1..5e756fbe 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -2,13 +2,9 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; +const { MenuModule } = require('./menu_module.js'); const messageArea = require('./message_area.js'); -const displayThemeArt = require('./theme.js').displayThemeArt; -const resetScreen = require('./ansi_term.js').resetScreen; -const stringFormat = require('./string_format.js'); -const Errors = require('./enig_error.js').Errors; +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); @@ -20,71 +16,43 @@ exports.moduleInfo = { author : 'NuSkooler', }; -/* - :TODO: - - Obv/2 has the following: - CHANGE .ANS - Message base changing ansi - |SN Current base name - |SS Current base sponsor - |NM Number of messages in current base - |UP Number of posts current user made (total) - |LR Last read message by current user - |DT Current date - |TI Current time -*/ +// :TODO: Obv/2 others can show # of messages in area const MciViewIds = { - AreaList : 1, - SelAreaInfo1 : 2, - SelAreaInfo2 : 3, + areaList : 1, + areaDesc : 2, // area desc updated @ index update + customRangeStart : 10, // updated @ index update }; exports.getModule = class MessageAreaListModule extends MenuModule { constructor(options) { super(options); - this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( - this.client.user.properties.message_conf_tag, - { client : this.client } - ); + this.initList(); - const self = this; this.menuMethods = { - changeArea : function(formData, extraArgs, cb) { + changeArea : (formData, extraArgs, cb) => { if(1 === formData.submitId) { - let area = self.messageAreas[formData.value.area]; - const areaTag = area.areaTag; - area = area.area; // what we want is actually embedded + const area = this.messageAreas[formData.value.area]; - messageArea.changeMessageArea(self.client, areaTag, err => { + messageArea.changeMessageArea(this.client, area.areaTag, err => { if(err) { - self.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); - - self.prevMenuOnTimeout(1000, cb); - } else { - if(_.isString(area.art)) { - const dispOptions = { - client : self.client, - name : area.art, - }; - - self.client.term.rawWrite(resetScreen()); - - displayThemeArt(dispOptions, () => { - // pause by default, unless explicitly told not to - if(_.has(area, 'options.pause') && false === area.options.pause) { - return self.prevMenuOnTimeout(1000, cb); - } else { - self.pausePrompt( () => { - return self.prevMenu(cb); - }); - } - }); - } else { - return self.prevMenu(cb); - } + this.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); + return this.prevMenuOnTimeout(1000, cb); } + + if(area.hasArt) { + const menuOpts = { + extraArgs : { + areaTag : area.areaTag, + }, + menuFlags : [ 'popParent', 'noHistory' ] + }; + + return this.gotoMenu(this.menuConfig.config.changeAreaPreArtMenu || 'changeMessageAreaPreArt', menuOpts, cb); + } + + return this.prevMenu(cb); }); } else { return cb(null); @@ -93,71 +61,66 @@ exports.getModule = class MessageAreaListModule extends MenuModule { }; } - // :TODO: these concepts have been replaced with the {someKey} style formatting - update me! - updateGeneralAreaInfoViews(areaIndex) { - /* - const areaInfo = self.messageAreas[areaIndex]; - - [ MciViewIds.SelAreaInfo1, MciViewIds.SelAreaInfo2 ].forEach(mciId => { - const v = self.viewControllers.areaList.getView(mciId); - if(v) { - v.setFormatObject(areaInfo.area); - } - }); - */ - } - mciReady(mciData, cb) { super.mciReady(mciData, err => { if(err) { return cb(err); } - const self = this; - const vc = self.viewControllers.areaList = new ViewController( { client : self.client } ); - async.series( [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - formId : 0, - }; - - vc.loadFromMenuConfig(loadOpts, function startingViewReady(err) { - callback(err); - }); + (next) => { + return this.prepViewController('areaList', 0, mciData.menu, next); }, - function populateAreaListView(callback) { - const areaListView = vc.getView(MciViewIds.AreaList); + (next) => { + const areaListView = this.viewControllers.areaList.getView(MciViewIds.areaList); if(!areaListView) { - return callback(Errors.MissingMci('A MenuView compatible MCI code is required')); + return cb(Errors.MissingMci(`Missing area list MCI ${MciViewIds.areaList}`)); } - let i = 1; - areaListView.setItems(self.messageAreas.map(a => { - return { - index : i++, - areaTag : a.area.areaTag, - text : a.area.name, // standard - name : a.area.name, - desc : a.area.desc, - }; - })); - - areaListView.on('index update', areaIndex => { - self.updateGeneralAreaInfoViews(areaIndex); + areaListView.on('index update', idx => { + this.selectionIndexUpdate(idx); }); + areaListView.setItems(this.messageAreas); areaListView.redraw(); - return callback(null); + this.selectionIndexUpdate(0); + return next(null); } ], - function complete(err) { + err => { + if(err) { + this.client.log.error( { error : err.message }, 'Failed loading message area list'); + } return cb(err); } ); }); } + + selectionIndexUpdate(idx) { + const area = this.messageAreas[idx]; + if(!area) { + return; + } + this.setViewText('areaList', MciViewIds.areaDesc, area.desc); + this.updateCustomViewTextsWithFilter('areaList', MciViewIds.customRangeStart, area); + } + + initList() { + let index = 1; + this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( + this.client.user.properties.message_conf_tag, + { client : this.client } + ).map(area => { + return { + index : index++, + areaTag : area.area.areaTag, + name : area.area.name, + text : area.area.name, // standard + desc : area.area.desc, + hasArt : _.isString(area.area.art), + }; + }); + } }; diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index c0340fe4..3c9d7152 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -47,7 +47,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { menuFlags : [ 'popParent', 'noHistory' ] }; - return this.gotoMenu(this.menuConfig.config.changeConfPreArtMenu || 'messageAreaChangeConfPreArt', menuOpts, cb); + return this.gotoMenu(this.menuConfig.config.changeConfPreArtMenu || 'changeMessageConfPreArt', menuOpts, cb); } return this.prevMenu(cb); @@ -73,7 +73,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { (next) => { const confListView = this.viewControllers.confList.getView(MciViewIds.confList); if(!confListView) { - return cb(Errors.MissingMci(`Missing conf list MCI ${MciViewIds.onlineList}`)); + return cb(Errors.MissingMci(`Missing conf list MCI ${MciViewIds.confList}`)); } confListView.on('index update', idx => { @@ -82,6 +82,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { confListView.setItems(this.messageConfs); confListView.redraw(); + this.selectionIndexUpdate(0); return next(null); } ], diff --git a/core/show_art.js b/core/show_art.js index 8f323e49..1c7da1b6 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -6,6 +6,9 @@ const MenuModule = require('./menu_module.js').MenuModule; const Errors = require('../core/enig_error.js').Errors; const ANSI = require('./ansi_term.js'); const Config = require('./config.js').get; +const { + getMessageAreaByTag +} = require('./message_area.js'); // deps const async = require('async'); @@ -104,7 +107,17 @@ exports.getModule = class ShowArtModule extends MenuModule { } showByMessageArea(cb) { - return cb(null); // NYI + this.getArtKeyValue( (err, key) => { + if(err) { + return cb(err); + } + + const area = getMessageAreaByTag(key); + if(!area) { + return cb(Errors.DoesNotExist(`No area by areaTag ${key} found`)); + } + return cb(null); // :TODO: REM OVE ME + }); } displaySingleArtByConfigPath(configPath, cb) { diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 5af16603..3938e967 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -68,6 +68,7 @@ - [Who's Online]({{ site.baseurl }}{% link modding/whos-online.md %}) - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) - [Message Conference List]({{ site.baseurl }}{% link modding/msg-conf-list.md %}) + - [Message Area List]({{ site.baseurl }}{% link modding/msg-area-list.md %}) - [Oputil]({{ site.baseurl }}{% link oputil/index.md %}) diff --git a/docs/modding/msg-area-list.md b/docs/modding/msg-area-list.md new file mode 100644 index 00000000..e3f5cde1 --- /dev/null +++ b/docs/modding/msg-area-list.md @@ -0,0 +1,17 @@ +--- +layout: page +title: Message Area List +--- +## The Message Area List Module +The built in `msg_area_list` module provides a menu to display and change between message areas in the users current conference. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): +* `index`: 1-based index into list. +* `areaTag`: Area tag. +* `name` or `text`: Display name. +* `desc`: Description. + +The following additional MCIs are updated as the user changes selections in the main list: +* MCI 2 (ie: `%TL2` or `%M%2`) is updated with the area description. +* MCI 10+ (ie `%TL10`...) are custom ranges updated with the same information available above in `itemFormat`. From c3405c4fdb280b6e41dc05d995dc7051193b833d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 20:14:50 -0600 Subject: [PATCH 237/569] Add missing changeMessageAreaPreArt menu --- config/menu.hjson | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/config/menu.hjson b/config/menu.hjson index b857dd38..39149416 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1951,6 +1951,19 @@ } } + changeMessageAreaPreArt: { + module: show_art + config: { + method: messageArea + key: areaTag + } + options: { + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + messageAreaMessageList: { module: msg_list art: MSGLIST From 339752236dff676f4b5c6308aa5f879a81b89594 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 20:15:19 -0600 Subject: [PATCH 238/569] Fix user list - system does not have {note} currently --- art/themes/luciano_blocktronics/USERLST.ANS | Bin 1838 -> 2047 bytes art/themes/luciano_blocktronics/theme.hjson | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/USERLST.ANS b/art/themes/luciano_blocktronics/USERLST.ANS index fa4e349991301e4c5df17d54dc724eb3f882ecf6..8c67ea58e8305ba470bf187c5a1421597450a665 100644 GIT binary patch delta 723 zcmZuvF-rq66sA3Ur3Ec0f^Z-aD(aSN+8%O~I5>#n3SHbv2F1mH zptECtM*{gD4(fYJS`XSOyf5#2@B6;wW9_$A2@)DNY0RQYWwb?kkhG42B;=|J7f`ja ziYITMt5`**Vht`X3S|BNo>8uqmPC9Pcf@O9YMRZU7_LN8R0laa<&sE3a5HNIF-TbC zRzWRJy(13*@$BvA=fKi0@A1jB;%KC5LbPK$NGZS;P)!ikvtBfX^c*!rq2NJD@S&}~KLi~-e+S2nE%7TPetjl_QN1KJ)2xA$f^s<0ytv5%+SZZ8{HBy1We>vz&& zuYc#jh6U5XkaW^4v#3ivw}i%5pkw#)EOhdX5RhmEAea}1@tW+-PIPJIE{-Wb4R!vK+Wm@XzCw&jH`fdEkq2^2sIw%ayntm5%sk67A7ann_Hjwr66gL&&HMoy QqWNF%ey{ja7S(e74-P-y+yDRo delta 491 zcmZ8eJxjzu6y%aKAt|&GgbOFHIITD4OGzawSlQa7kSHD;WL3B;1x2tCo3Q`GpCQ8i zgk9LuPCHBWZFX~r-R#?)c{8&=8!ziYRFN_zWkTBEVJo9il@v^dQI%^*m=F@6{T#P% zPku^RR27t;4Zz7Me;&L$8Rhqm+$^X70id;#oGGd4{Lww+OLy1M@Zq<6JISb004l07 zDkUO|6=Rq_!;zm5+)qmWF@!Kicepu;DIc#zcirK@IIXACSu^jP8q50OKHkOb z>UwdB5=`5&3UO*!O+8=0Mcp)(i1tMYLi7Q4PEX<$3Dbw|=PFkdvy@GbC zWC(vD-eY{Mw!jWk@0=QD=h1Exq8Ir;P4s_YyS)o*f`572f$l<|b3XEq`?fGEzwu|z JtH*^u`332&oVfr1 diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 2733e4c8..52537323 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -162,8 +162,8 @@ VM1: { height: 15, width: 50 - itemFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{note:<19.19}|03{lastLoginTs}" - focusItemFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{note:<19.19}{lastLoginTs}" + itemFormat: "|00|11{userName:<17.17}|03{affils:<21.21}|11{location:<19.19}|03{lastLoginTs}" + focusItemFormat: "|00|19|15{userName:<17.17}{affils:<21.21}{location:<19.19}{lastLoginTs}" } } } From 3522b8b6f8333a01c1594cb285002495d05bc0d8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Jul 2018 20:15:41 -0600 Subject: [PATCH 239/569] Notes on show_art / config.art changes --- UPGRADE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UPGRADE.md b/UPGRADE.md index 956789c3..5ec42295 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -59,6 +59,7 @@ webSocket: { ``` * The module export `registerEvents` has been deprecated. If you have a module that depends on this, use the new more generic `moduleInitialize` export instead. * The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup. +* If you have art configured for message conference or area selection via the `art` configuration value, you will need to include a `show_art` menu reference. Defaulted to `changeMessageConfPreArt` for conferences and `changeMessageAreaPreArt` for areas & included in the example `menu.hjson`. # 0.0.7-alpha to 0.0.8-alpha From 33790a74e34c062658bab188bff463c73fc3eb7f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 25 Jul 2018 21:18:30 -0600 Subject: [PATCH 240/569] Allow matches of minutes vs minute, etc. as intended --- core/last_callers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/last_callers.js b/core/last_callers.js index 268fde5f..08eb7b71 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -77,7 +77,7 @@ exports.getModule = class LastCallersModule extends MenuModule { getCollapse(conf) { let collapse = _.get(this, conf); - collapse = collapse && collapse.match(/^([0-9]+)\s*(minutes|seconds|hours|days|months)$/); + collapse = collapse && collapse.match(/^([0-9]+)\s*(minutes?|seconds?|hours?|days?|months?)$/); if(collapse) { return moment.duration(parseInt(collapse[1]), collapse[2]); } From 82278a212b2792c46d6b2ac6809a4d52148277f5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Jul 2018 21:50:51 -0600 Subject: [PATCH 241/569] Doc updates --- docs/art/general.md | 3 +-- docs/art/mci.md | 31 ++++++++++++++++++++++++++++--- docs/art/themes.md | 28 ++++++++++++++++------------ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/art/general.md b/docs/art/general.md index c43d915c..f08994e8 100644 --- a/docs/art/general.md +++ b/docs/art/general.md @@ -2,5 +2,4 @@ layout: page title: General --- -General art lives in the `art/general` directory. 'General' art is ANSI you want to stay consistent across themes, -such as a welcome ANSI or a rotation of logoff ANSIs. +General art lives in the `art/general` directory. 'General' art is ANSI you want to stay consistent across themes, such as a welcome ANSI or a rotation of logoff ANSIs. diff --git a/docs/art/mci.md b/docs/art/mci.md index 4befc411..d6080630 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -72,7 +72,7 @@ iNiQUiTY, 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 or to extend an otherwise empty space in an art file down the screen. ## Views @@ -92,7 +92,7 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `TM` | Toggle Menu | A toggle menu commonly used for Yes/No style input | | `KE` | Key Entry | A *single* key input control | - + Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information. @@ -112,10 +112,14 @@ Predefined MCI codes and other Views can have properties set via `menu.hjson` an | `focus` | If set to `true`, establishes initial focus | | `text` | (initial) text of a view | | `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** below | +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** below | These are just a few of the properties set on various views. *Use the source Luke*, as well as taking a look at the default `menu.hjson` and `theme.hjson` files! +### Custom Properties +Often a module will provide custom properties that receive format objects (See **Entry Formatting** below). Custom property formatting can be declared in the `config` block. For example, `browseInfoFormat10`..._N_ (where _N_ is up to 99) in the `file_base_search` module received a fairly extensive format object that contains `{fileName}`, `{estReleaseYear}`, etc. ### Text Styles @@ -132,4 +136,25 @@ Standard style types available for `textStyle` and `focusTextStyle`: | `big vowels` | EniGMa bUllEtIn bOArd sOftwArE | | `small i` | ENiGMA BULLETiN BOARD SOFTWARE | | `mixed` | EnIGma BUlLEtIn BoaRd SOfTWarE (randomly assigned) | -| `l33t` | 3n1gm4 bull371n b04rd 50f7w4r3 | \ No newline at end of file +| `l33t` | 3n1gm4 bull371n b04rd 50f7w4r3 | + +### Entry Fromatting +Various strings can be formatted using a syntax that allows width & precision specifiers, text styling, etc. Depending on the context, various elements can be referenced by `{name}`. Additional text styles can be supplied as well. The syntax is largely modeled after Python's [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language). + +### Additional Text Styles +Additional text styles are available for numbers: + +| Style | Description | +|-------------------|---------------| +| `sizeWithAbbr` | File size (converted from bytes) with abbreviation such as `1 MB`, `2.2 GB`, `34 KB`, etc. | +| `sizeWithoutAbbr` | Just the file size (converted from bytes) without the abbreviation. For example: 1024 becomes 1. | +| `sizeAbbr` | Just the abbreviation given a file size (converted from bytes) such as `MB` or `GB`. | +| `countWithAbbr` | Count with abbreviation such as `100 K`, `4.3 B`, etc. | +| `countWithoutAbbr` | Just the count | +| `countAbbr` | Just the abbreviation such as `M` for millions. | + + +#### Examples +Suppose a format object contains the following elements: `userName` and `affils`. We could create a `itemFormat` entry that builds a item to our specifications: `|04{userName!styleFirstLower} |08- |13{affils}`. This may produce a string such as "eVIL cURRENT - Razor 1911". + +Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". \ No newline at end of file diff --git a/docs/art/themes.md b/docs/art/themes.md index 37631454..9ef482b6 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -2,24 +2,28 @@ layout: page title: Themes --- +# Creating Your Own :warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included `luciano_blocktronics' theme. Create your own and make changes to that instead: 1. Copy `/art/themes/luciano_blocktronics` to `art/themes/your_board_theme` 2. Update the `info` block at the top of the theme.hjson file: - - info: { - name: Awesome Theme - author: Cool Artist - group: Sick Group - enabled: true - } +``` hjson + info: { + name: Awesome Theme + author: Cool Artist + group: Sick Group + enabled: true + } +``` 3. Specify it in the `defaults` section of `config.hjson`. The name supplied should match the name of the directory you created in step 1: - ```hjson - defaults: { - theme: your_board_theme - } - ``` +``` hjson + defaults: { + theme: your_board_theme + } +``` + +# General Theme Info \ No newline at end of file From 9012ef76c31e15de8546464a360aa662d0489439 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jul 2018 13:49:12 -0600 Subject: [PATCH 242/569] Fix typo in module name --- docs/art/mci.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/art/mci.md b/docs/art/mci.md index d6080630..1bad97d5 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -119,7 +119,7 @@ These are just a few of the properties set on various views. *Use the source Luk `menu.hjson` and `theme.hjson` files! ### Custom Properties -Often a module will provide custom properties that receive format objects (See **Entry Formatting** below). Custom property formatting can be declared in the `config` block. For example, `browseInfoFormat10`..._N_ (where _N_ is up to 99) in the `file_base_search` module received a fairly extensive format object that contains `{fileName}`, `{estReleaseYear}`, etc. +Often a module will provide custom properties that receive format objects (See **Entry Formatting** below). Custom property formatting can be declared in the `config` block. For example, `browseInfoFormat10`..._N_ (where _N_ is up to 99) in the `file_area_list` module received a fairly extensive format object that contains `{fileName}`, `{estReleaseYear}`, etc. ### Text Styles From 6f84ffd7084ec41da231ddd2a62db0dc114532dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jul 2018 13:49:37 -0600 Subject: [PATCH 243/569] Minor updates to file area list to use proper date/time theme formatting + initial docs --- art/themes/luciano_blocktronics/theme.hjson | 10 +-- core/file_area_list.js | 16 ++-- docs/_includes/nav.md | 1 + docs/modding/file-area-list.md | 86 +++++++++++++++++++++ 4 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 docs/modding/file-area-list.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 52537323..f3af6df6 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -548,9 +548,6 @@ 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)" } @@ -609,6 +606,8 @@ VM1: { height: 17 width: 79 + itemFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}" + focusItemFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}" } } } @@ -649,9 +648,6 @@ 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)" } @@ -710,6 +706,8 @@ VM1: { height: 17 width: 79 + itemFormat: "|00|03{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}" + focusItemFormat: "|00|19|15{fileName:<67.66} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}" } } } diff --git a/core/file_area_list.js b/core/file_area_list.js index f3172d4d..3a539882 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -218,7 +218,7 @@ exports.getModule = class FileAreaList extends MenuModule { const config = this.menuConfig.config; const currEntry = this.currentFileEntry; - const uploadTimestampFormat = config.browseUploadTimestampFormat || config.uploadTimestampFormat || 'YYYY-MMM-DD'; + const uploadTimestampFormat = config.browseUploadTimestampFormat || this.client.currentTheme.helpers.getDateFormat('short'); const area = FileArea.getFileAreaByTag(currEntry.areaTag); const hashTagsSep = config.hashTagsSep || ', '; const isQueuedIndicator = config.isQueuedIndicator || 'Y'; @@ -268,7 +268,7 @@ exports.getModule = class FileAreaList extends MenuModule { entryInfo.archiveTypeDesc = 'N/A'; } - entryInfo.uploadByUsername = entryInfo.uploadByUsername || 'N/A'; // may be imported + entryInfo.uploadByUsername = entryInfo.uploadByUserName = entryInfo.uploadByUsername || 'N/A'; // may be imported entryInfo.hashTags = entryInfo.hashTags || '(none)'; // create a rating string, e.g. "**---" @@ -289,7 +289,7 @@ exports.getModule = class FileAreaList extends MenuModule { entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; } } else { - const webDlExpireTimeFormat = config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + const webDlExpireTimeFormat = config.webDlExpireTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); @@ -583,7 +583,8 @@ exports.getModule = class FileAreaList extends MenuModule { return cb(err); } - this.currentFileEntry.archiveEntries = entries; + // assign and add standard "text" member for itemFormat + this.currentFileEntry.archiveEntries = entries.map(e => Object.assign(e, { text : `${e.fileName} (${e.byteSize})` } )); return cb(null, 're-cached'); }); } @@ -600,12 +601,7 @@ exports.getModule = class FileAreaList extends MenuModule { } if('re-cached' === cacheStatus) { - const fileListEntryFormat = this.menuConfig.config.fileListEntryFormat || '{fileName} {fileSize}'; // :TODO: use byteSize here? - const focusFileListEntryFormat = this.menuConfig.config.focusFileListEntryFormat || fileListEntryFormat; - - fileListView.setItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(fileListEntryFormat, entry) ) ); - fileListView.setFocusItems( this.currentFileEntry.archiveEntries.map( entry => stringFormat(focusFileListEntryFormat, entry) ) ); - + fileListView.setItems(this.currentFileEntry.archiveEntries); fileListView.redraw(); } }); diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 3938e967..c366c8fa 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -64,6 +64,7 @@ - Combatnet - Exodus - [Existing Mods]({{ site.baseurl }}{% link modding/existing-mods.md %}) + - [File Area List]({{ site.baseurl }}{% link modding/file-area-list.md %}) - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) - [Who's Online]({{ site.baseurl }}{% link modding/whos-online.md %}) - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) diff --git a/docs/modding/file-area-list.md b/docs/modding/file-area-list.md new file mode 100644 index 00000000..cec9a18e --- /dev/null +++ b/docs/modding/file-area-list.md @@ -0,0 +1,86 @@ +--- +layout: page +title: File Area List +--- +## The File Area List Module +The built in `file_area_list` module provides a very flexible file listing UI. + +## Configuration +### Config Block +Available `config` block entries: +* `art`: Sup configuration block used to establish art files used for file browsing: + * `browse`: The main browse screen. + * `details`: The main file details screen. + * `detailsGeneral`: The "general" tab of the details page. + * `detailsNfo`: The "NFO" viewer tab of the detials page. + * `detailsFileList`: The file listing tab of the details page (ie: used for listing archive contents). + * `help`: The help page. +* `hashTagsSep`: Separator for hash entries. Defaults to ", ". +* `isQueuedIndicator`: Indicator for items that are in the users download queue. Defaults to "Y". +* `isNotQueuedIndicator`: Indicator for items that are _not_ in the users download queue. Defaults to "N". +* `userRatingTicked`: Indicator for a items current _n_/5 "star" rating. Defaults to "*". `userRatingTicked` and `userRatingUnticked` are combined to build strings such as "***--" for 3/5 rating. +* `userRatingUnticked`: Indicator for missing "stars" in a items _n_/5 rating. Defaults to "-". `userRatingTicked` and `userRatingUnticked` are combined to build strings such as "***--" for 3/5 rating. +* `webDlExpireTimeFormat`: Presents the expiration time of a web download URL. Defaults to current theme → system `short` date/time format. +* `webDlLinkNeedsGenerated`: Text to present when no web download link is yet generated. Defaults to "Not yet generated". +* `webDlLinkNoWebserver`: Text to present when no web download is available (ie: webserver not enabled). Defaults to "Web server is not enabled". +* `notAnArchiveFormat`: Presents text for the "archive type" field for non-archives. Defaults to "Not an archive". +* `browseUploadTimestampFormat`: Timestamp format for `browseInfoFormatXXX`. Defaults to current theme → system `short` date format. See also **Browse Info Format** below. + +Remember that entries such as `isQueuedIndicator` and `userRatingTicked` may contain pipe color codes! + +### Browse Info Format +Additional `config` block entries used for the `browse` page are as follows: +* `browseInfoFormatXXX`: Where XXX is 10..._N_ such as `browseInfoFormat10`. See **Browse Page** below for format members. + +### Theming +#### Browse Page +* MCI 1 (ie: `%MT1`): File's short description (user entered, FILE_ID.DIZ, etc.). +* MCI 2 (ie: `%HM2`): Navigation menu. +* MCI 10..._N_: Custom entires with the following format members: + * `{fileId}`: File identifier. + * `{fileName}`: File name (long). + * `{desc}`: File short description (user entered, FILE_ID.DIZ, etc.). + * `{descLong}`: File's long description (README.TXT, SOMEGROUP.NFO, etc.). + * `{uploadByUserName}`: User name of user that uploaded this file, or "N/A". + * `{uploadByUserId}`: User ID of user that uploaded this file, or "N/A". + * `{userRating}`: User rating of file as a number. + * `{userRatingString}`: User rating of this file as a string formatted with `userRatingTicked` and `userRatingUnticked` described above. + * `{areaTag}`: Area tag. + * `{areaName}`: Area name or "N/A". + * `{areaDesc}`: Area description or "N/A". + * `{fileSha256}`: File's SHA-256 value in hex. + * `{fileMd5}`: File's MD5 value in hex. + * `{fileSha1}`: File's SHA1 value in hex. + * `{fileCrc32}`: File's CRC-32 value in hex. + * `{estReleaseYear}`: Estimated release year of this file. + * `{dlCount}`: Number of times this file has been downloaded. + * `{byteSize}`: Size of this file in bytes. + * `{archiveType}`: Archive type of this file determined by system mappings, or "N/A". + * `{archiveTypeDesc}`: A more descriptive archive type based on system mappings, file extention, etc. or "N/A" if it cannot be determined. + * `{shortFileName}`: Short DOS style 8.3 name available for some scenarios such as TIC import, or "N/A". + * `{ticOrigin}`: Origin from TIC imported files "Origin" field, or "N/A". + * `{ticDesc}`: Description from TIC imported files "Desc" field, or "N/A". + * `{ticLDesc}`: Long description from TIC imported files "LDesc" field joined by a line feed, or "N/A". + * `{uploadTimestamp}`: Upload timestamp formatted with `browseUploadTimestampFormat`. + * `{hashTags}`: A string of hash tags(s) separated by `hashTagsSep` described above. "(none)" if there are no tags. + * `{isQueued}`: Indicates if a item is currently in the user's download queue presented as `isQueuedIndicator` or `isNotQueuedIndicator` described above. + * `{webDlLink}`: Web download link if generated else `webDlLinkNeedsGenerated` or `webDlLinkNoWebserver` described above. + * `{webDlExpire}`: Web download link expiration using `webDlExpireTimeFormat` described above. + +#### Details Page +* MCI 1 (ie: `%HM1`): Navigation menu +* `%XY2`: Info area's top X,Y position. +* `%XY3`: Info area's bottom X,Y position. +* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsInfoFormatXXX` `config` block entry. + +#### Details Page - General Tab +* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsGeneralInfoFormatXXX` `config` block entry. + +#### Details Page - NFO/README Viewer Tab +* MCI 1 (ie: `%MT1`): NFO/README viewer using the entries `longDesc`. +* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsNfoInfoFormatXXX` `config` block entry. + +#### Detilas Page - Archive/File Listing Tab +* MCI 1 (ie: `%VM1`): List of entries in archive. Entries are formatted using the standard `itemFormat` and `focusItemFormat` properties of the view and have all of the format options described above in **Browse Page**. +* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsFileListInfoFormatXXX` `config` block entry. + From 096bc5497f450548bdf483e0c3ef4caaf8dd307b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jul 2018 15:54:48 -0600 Subject: [PATCH 244/569] Minor cleanup/standardization in file list, updated docs --- core/file_area_list.js | 2 +- docs/modding/file-area-list.md | 41 +++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/core/file_area_list.js b/core/file_area_list.js index 3a539882..e72589c3 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -218,7 +218,7 @@ exports.getModule = class FileAreaList extends MenuModule { const config = this.menuConfig.config; const currEntry = this.currentFileEntry; - const uploadTimestampFormat = config.browseUploadTimestampFormat || this.client.currentTheme.helpers.getDateFormat('short'); + const uploadTimestampFormat = config.uploadTimestampFormat || this.client.currentTheme.helpers.getDateFormat('short'); const area = FileArea.getFileAreaByTag(currEntry.areaTag); const hashTagsSep = config.hashTagsSep || ', '; const isQueuedIndicator = config.isQueuedIndicator || 'Y'; diff --git a/docs/modding/file-area-list.md b/docs/modding/file-area-list.md index cec9a18e..e46aaaba 100644 --- a/docs/modding/file-area-list.md +++ b/docs/modding/file-area-list.md @@ -6,7 +6,7 @@ title: File Area List The built in `file_area_list` module provides a very flexible file listing UI. ## Configuration -### Config Block +## Config Block Available `config` block entries: * `art`: Sup configuration block used to establish art files used for file browsing: * `browse`: The main browse screen. @@ -24,19 +24,24 @@ Available `config` block entries: * `webDlLinkNeedsGenerated`: Text to present when no web download link is yet generated. Defaults to "Not yet generated". * `webDlLinkNoWebserver`: Text to present when no web download is available (ie: webserver not enabled). Defaults to "Web server is not enabled". * `notAnArchiveFormat`: Presents text for the "archive type" field for non-archives. Defaults to "Not an archive". -* `browseUploadTimestampFormat`: Timestamp format for `browseInfoFormatXXX`. Defaults to current theme → system `short` date format. See also **Browse Info Format** below. +* `uploadTimestampFormat`: Timestamp format for `xxxxxxInfoFormat##`. Defaults to current theme → system `short` date format. See also **Custom Info Formats** below. Remember that entries such as `isQueuedIndicator` and `userRatingTicked` may contain pipe color codes! -### Browse Info Format -Additional `config` block entries used for the `browse` page are as follows: -* `browseInfoFormatXXX`: Where XXX is 10..._N_ such as `browseInfoFormat10`. See **Browse Page** below for format members. +## Custom Info Formats +Additional `config` block entries can set `xxxxxxInfoFormat##` formatting (where xxxxxx is the page name and ## is 10...99 such as `browseInfoFormat10`) for the various available pages: +* `browseInfoFormat##` for the `browse` page. See **Browse Page** below. +* `detailsInfoFormat##` for the `details` page. See **Details Page** below. +* `detailsGeneralInfoFormat##` for the `detailsGeneral` tab. See **Details Page - General Tab** below. +* `detailsNfoInfoFormat##` for the `detialsNfo` tab. See **Details Page - NFO/README Viewer Tab** below. +* `detailsFileListInfoFormat##` for the `detailsFileList` tab. See **Details Page - Archive/File Listing Tab** below. -### Theming -#### Browse Page +## Theming +### Browse Page +The browse page uses the `browse` art described above. The following MCI codes are available: * MCI 1 (ie: `%MT1`): File's short description (user entered, FILE_ID.DIZ, etc.). * MCI 2 (ie: `%HM2`): Navigation menu. -* MCI 10..._N_: Custom entires with the following format members: +* MCI 10...99: Custom entires with the following format members: * `{fileId}`: File identifier. * `{fileName}`: File name (long). * `{desc}`: File short description (user entered, FILE_ID.DIZ, etc.). @@ -67,20 +72,24 @@ Additional `config` block entries used for the `browse` page are as follows: * `{webDlLink}`: Web download link if generated else `webDlLinkNeedsGenerated` or `webDlLinkNoWebserver` described above. * `{webDlExpire}`: Web download link expiration using `webDlExpireTimeFormat` described above. -#### Details Page +### Details Page +The details page uses the `details` art described above. The following MCI codes are available: * MCI 1 (ie: `%HM1`): Navigation menu * `%XY2`: Info area's top X,Y position. * `%XY3`: Info area's bottom X,Y position. -* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsInfoFormatXXX` `config` block entry. +* MCI 10...99: Custom entries with the format options described above in **Browse Page** via the `detailsInfoFormat##` `config` block entry. -#### Details Page - General Tab -* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsGeneralInfoFormatXXX` `config` block entry. +### Details Page - General Tab +The details page general tab uses the `detailsGeneral` art described above. The following MCI codes are available: +* MCI 10...99: Custom entries with the format options described above in **Browse Page** via the `detailsGeneralInfoFormat##` `config` block entry. -#### Details Page - NFO/README Viewer Tab +### Details Page - NFO/README Viewer Tab +The details page nfo tab uses the `detailsNfo` art described above. The following MCI codes are available: * MCI 1 (ie: `%MT1`): NFO/README viewer using the entries `longDesc`. -* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsNfoInfoFormatXXX` `config` block entry. +* MCI 10...99: Custom entries with the format options described above in **Browse Page** via the `detailsNfoInfoFormat##` `config` block entry. -#### Detilas Page - Archive/File Listing Tab +### Details Page - Archive/File Listing Tab +The details page file list tab uses the `detailsFileList` art described above. The following MCI codes are available: * MCI 1 (ie: `%VM1`): List of entries in archive. Entries are formatted using the standard `itemFormat` and `focusItemFormat` properties of the view and have all of the format options described above in **Browse Page**. -* MCI 10..._N_: Custom entries with the format options described above in **Browse Page** via the `detailsFileListInfoFormatXXX` `config` block entry. +* MCI 10...99: Custom entries with the format options described above in **Browse Page** via the `detailsFileListInfoFormat##` `config` block entry. From 9b73380e0494f6051a9b8c8a1ea9c16302c887d9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jul 2018 15:57:36 -0600 Subject: [PATCH 245/569] Fix typo --- docs/modding/file-area-list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modding/file-area-list.md b/docs/modding/file-area-list.md index e46aaaba..a0a58071 100644 --- a/docs/modding/file-area-list.md +++ b/docs/modding/file-area-list.md @@ -8,7 +8,7 @@ The built in `file_area_list` module provides a very flexible file listing UI. ## Configuration ## Config Block Available `config` block entries: -* `art`: Sup configuration block used to establish art files used for file browsing: +* `art`: Sub-configuration block used to establish art files used for file browsing: * `browse`: The main browse screen. * `details`: The main file details screen. * `detailsGeneral`: The "general" tab of the details page. From 96c3a0eb7d7597cd8ea119086bd1d3128de16bf2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 28 Jul 2018 15:58:53 -0600 Subject: [PATCH 246/569] Escape '*' --- docs/modding/file-area-list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modding/file-area-list.md b/docs/modding/file-area-list.md index a0a58071..735986a0 100644 --- a/docs/modding/file-area-list.md +++ b/docs/modding/file-area-list.md @@ -18,7 +18,7 @@ Available `config` block entries: * `hashTagsSep`: Separator for hash entries. Defaults to ", ". * `isQueuedIndicator`: Indicator for items that are in the users download queue. Defaults to "Y". * `isNotQueuedIndicator`: Indicator for items that are _not_ in the users download queue. Defaults to "N". -* `userRatingTicked`: Indicator for a items current _n_/5 "star" rating. Defaults to "*". `userRatingTicked` and `userRatingUnticked` are combined to build strings such as "***--" for 3/5 rating. +* `userRatingTicked`: Indicator for a items current _n_/5 "star" rating. Defaults to "\*". `userRatingTicked` and `userRatingUnticked` are combined to build strings such as "***--" for 3/5 rating. * `userRatingUnticked`: Indicator for missing "stars" in a items _n_/5 rating. Defaults to "-". `userRatingTicked` and `userRatingUnticked` are combined to build strings such as "***--" for 3/5 rating. * `webDlExpireTimeFormat`: Presents the expiration time of a web download URL. Defaults to current theme → system `short` date/time format. * `webDlLinkNeedsGenerated`: Text to present when no web download link is yet generated. Defaults to "Not yet generated". From b5c67ec88f0be736bdb4b6c076916a91237a5f29 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Jul 2018 20:58:26 -0600 Subject: [PATCH 247/569] Fix areaTag bug recently introduced --- core/msg_area_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 5e756fbe..97a2e16e 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -115,7 +115,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { ).map(area => { return { index : index++, - areaTag : area.area.areaTag, + areaTag : area.areaTag, name : area.area.name, text : area.area.name, // standard desc : area.area.desc, From 475fe596f6bb8c9944d3b04aeb63900a676e6698 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 2 Aug 2018 22:13:42 -0600 Subject: [PATCH 248/569] Better handling of 'socket' io --- core/door.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/core/door.js b/core/door.js index 1ac0754f..d6326cbe 100644 --- a/core/door.js +++ b/core/door.js @@ -26,22 +26,20 @@ module.exports = class Door { } this.sockServer = createServer(conn => { - this.sockServer.getConnections( (err, count) => { + conn.once('end', () => { + return this.restoreIo(conn); + }); + conn.once('error', err => { + this.client.log.info( { error : err.message }, 'Door socket server connection'); + return this.restoreIo(conn); + }); + + this.sockServer.getConnections( (err, count) => { // We expect only one connection from our DOOR/emulator/etc. if(!err && count <= 1) { this.client.term.output.pipe(conn); - conn.on('data', this.doorDataHandler.bind(this)); - - conn.once('end', () => { - return this.restoreIo(conn); - }); - - conn.once('error', err => { - this.client.log.info( { error : err.message }, 'Door socket server connection'); - return this.restoreIo(conn); - }); } }); }); From 5bd7ecdb880c9e1f2d34279df48d1c8f763c4b4f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 4 Aug 2018 11:49:44 -0600 Subject: [PATCH 249/569] Add menu-level ACS check --- core/acs.js | 15 +++++- core/menu_stack.js | 7 +++ core/show_art.js | 2 +- docs/configuration/acs.md | 2 +- docs/configuration/menu-hjson.md | 82 ++++++++++++++++++++++---------- 5 files changed, 80 insertions(+), 28 deletions(-) diff --git a/core/acs.js b/core/acs.js index 66f13a08..a86db329 100644 --- a/core/acs.js +++ b/core/acs.js @@ -51,6 +51,19 @@ class ACS { return this.check(area.acs, 'download', ACS.Defaults.FileAreaDownload); } + hasMenuModuleAccess(modInst) { + const acs = _.get(modInst, 'menuConfig.config.acs'); + if(!_.isString(acs)) { + return true; // no ACS check req. + } + try { + return checkAcs(acs, { client : this.client } ); + } catch(e) { + Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); + return false; + } + } + getConditionalValue(condArray, memberName) { if(!Array.isArray(condArray)) { // no cond array, just use the value @@ -68,7 +81,7 @@ class ACS { return false; } } else { - return true; // no acs check req. + return true; // no ACS check req. } }); diff --git a/core/menu_stack.js b/core/menu_stack.js index 5ab5a091..06b53d76 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -127,6 +127,13 @@ module.exports = class MenuStack { } else { self.client.log.debug( { menuName : name }, 'Goto menu module'); + if(!this.client.acs.hasMenuModuleAccess(modInst)) { + if(cb) { + return cb(Errors.AccessDenied('No access to this menu')); + } + return; + } + // // If menuFlags were supplied in menu.hjson, they should win over // anything supplied in code. diff --git a/core/show_art.js b/core/show_art.js index 1c7da1b6..bcd15d7b 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -116,7 +116,7 @@ exports.getModule = class ShowArtModule extends MenuModule { if(!area) { return cb(Errors.DoesNotExist(`No area by areaTag ${key} found`)); } - return cb(null); // :TODO: REM OVE ME + return cb(null); // :TODO: REMOVE ME --- currently NYI }); } diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index 3b7acbc0..eeab8e9c 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -61,6 +61,6 @@ The following touch points exist in the system. Many more are planned: * Message conferences and areas * File base areas -* Menus within `menu.hjson` +* Menus within `menu.hjson`. See [menu.hjson](menu-hjson.md). See the specific areas documentation for information on available ACS checks. diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 3596e95a..58a06d5a 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -31,9 +31,9 @@ Let's look a couple basic menu entries: ```hjson telnetConnected: { - art: CONNECT - next: matrix - options: { nextTimeout: 1500 } + art: CONNECT + next: matrix + options: { nextTimeout: 1500 } } ``` @@ -54,38 +54,38 @@ Now let's look at `matrix`, the `next` entry from `telnetConnected`: ```hjson matrix: { - art: matrix - desc: Login Matrix - form: { + art: matrix + desc: Login Matrix + form: { 0: { - VM: { + VM: { mci: { - VM1: { + VM1: { submit: true focus: true items: [ "login", "apply", "log off" ] argName: matrixSubmit - } + } } submit: { - *: [ - { - value: { matrixSubmit: 0 } - action: @menu:login - } - { - value: { matrixSubmit: 1 }, - action: @menu:newUserApplication - } - { - value: { matrixSubmit: 2 }, - action: @menu:logoff - } - ] + *: [ + { + value: { matrixSubmit: 0 } + action: @menu:login + } + { + value: { matrixSubmit: 1 }, + action: @menu:newUserApplication + } + { + value: { matrixSubmit: 2 }, + action: @menu:logoff + } + ] + } } - } } - } + } } ``` @@ -99,3 +99,35 @@ The `submit` object tells the system to attempt to apply provided match entries Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match causing `action` of `@menu:login` to be executed (go to `login` menu). + +## ACS Checks +Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax. + +### Menu Access +To restrict menu access add an `acs` key to `config`. Example: +``` +opOnlyMenu: { + desc: Ops Only! + config: { + acs: ID1 + } +} +``` + +### Flow Control +The `next` member of a menu may be an array of objects containing an `acs` check as well as the destination. Depending on the current user's ACS, the system will pick the appropriate target. The last element in an array without an `acs` can be used as a catch all. Example: +``` +login: { + desc: Logging In + next: [ + { + // >= 2 calls else you get the full login + acs: NC2 + next: loginSequenceLoginFlavorSelect + } + { + next: fullLoginSequenceLoginArt + } + ] +} +``` \ No newline at end of file From 746bd5abd042e9fe04f88cd4dd2dec00d900ba08 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Aug 2018 10:50:47 -0600 Subject: [PATCH 250/569] * Don't crash with bad string formats * File listing: If we fail to get an archive listing, fix attempt to format the string with a non-object --- core/file_area_list.js | 16 ++++++++++++---- core/string_format.js | 26 +++++++++++++++++--------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/core/file_area_list.js b/core/file_area_list.js index e72589c3..35ce3ea6 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -589,15 +589,22 @@ exports.getModule = class FileAreaList extends MenuModule { }); } + setFileListNoListing(text) { + const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); + if(fileListView) { + fileListView.complexItems = false; + fileListView.setItems( [ text ] ); + fileListView.redraw(); + } + } + populateFileListing() { const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); if(this.currentFileEntry.entryInfo.archiveType) { this.cacheArchiveEntries( (err, cacheStatus) => { if(err) { - // :TODO: Handle me!!! - fileListView.setItems( [ 'Failed getting file listing' ] ); // :TODO: make this not suck - return; + return this.setFileListNoListing('Failed to get file listing'); } if('re-cached' === cacheStatus) { @@ -606,7 +613,8 @@ exports.getModule = class FileAreaList extends MenuModule { } }); } else { - fileListView.setItems( [ stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ) ] ); + const notAnArchiveFileName = stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ); + this.setFileListNoListing(notAnArchiveFileName); } } diff --git a/core/string_format.js b/core/string_format.js index cef937c4..a756db72 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -331,17 +331,25 @@ module.exports = function format(fmt, obj) { transformer = match[2]; formatSpec = match[3]; - value = getValue(obj, objPath); - if(transformer) { - value = transformValue(transformer, value); - } + try { + value = getValue(obj, objPath); + if(transformer) { + value = transformValue(transformer, value); + } - tokens = tokenizeFormatSpec(formatSpec || ''); + tokens = tokenizeFormatSpec(formatSpec || ''); - if(_.isNumber(value)) { - out += formatNumber(value, tokens); - } else { - out += formatString(value, tokens); + if(_.isNumber(value)) { + out += formatNumber(value, tokens); + } else { + out += formatString(value, tokens); + } + } catch(e) { + if(e instanceof KeyError) { + out += match[0]; // preserve full thing + } else if(e instanceof ValueError) { + out += value.toString(); + } } } From dfe1c297b5ea8ddace2aecc1f2b659ef34ecedec Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Aug 2018 14:06:30 -0600 Subject: [PATCH 251/569] Menu "options" block is now deprecated. Move members to "config"! * Deprecate & allow conversion behind the scenes for now + add warning in log * Add some initial docs * Clean up prompt.hjson and menu.hjson --- config/menu.hjson | 78 +++++++++++++------------------- config/prompt.hjson | 2 +- core/menu_module.js | 20 ++++---- core/menu_stack.js | 21 +++++++-- core/menu_util.js | 8 ++-- core/show_art.js | 2 +- core/theme.js | 2 +- docs/configuration/menu-hjson.md | 45 +++++++++++------- 8 files changed, 95 insertions(+), 83 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index 39149416..3d651412 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -32,7 +32,7 @@ telnetConnected: { art: CONNECT next: matrix - options: { nextTimeout: 1500 } + config: { nextTimeout: 1500 } } // @@ -42,7 +42,7 @@ sshConnected: { art: CONNECT next: fullLoginSequenceLoginArt - options: { nextTimeout: 1500 } + config: { nextTimeout: 1500 } } // @@ -53,7 +53,7 @@ sshConnectedNewUser: { art: CONNECT next: newUserApplicationPreSsh - options: { nextTimeout: 1500 } + config: { nextTimeout: 1500 } } // Ye ol' standard matrix @@ -158,7 +158,7 @@ loginAttemptTooNode: { art: TOONODE - options: { + config: { cls: true nextTimeout: 2000 } @@ -179,7 +179,7 @@ forgotPasswordSubmitted: { desc: Forgot password art: FORGOTPWSENT - options: { + config: { cls: true pause: true } @@ -206,7 +206,7 @@ art: PRELOGAD desc: Logging Off next: fullLogoffSequenceRandomBoardAd - options: { + config: { cls: true nextTimeout: 1500 } @@ -216,7 +216,7 @@ art: OTHRBBS desc: Logging Off next: logoff - options: { + config: { baudRate: 57600 pause: true cls: true @@ -234,7 +234,7 @@ art: NEWUSER1 next: newUserApplication desc: Applying - options: { + config: { pause: true cls: true menuFlags: [ "noHistory" ] @@ -349,7 +349,7 @@ art: NEWUSER1 next: newUserApplicationSsh desc: Applying - options: { + config: { pause: true cls: true menuFlags: [ "noHistory" ] @@ -456,7 +456,7 @@ newUserFeedbackToSysOpPreamble: { art: LETTER - options: { pause: true } + config: { pause: true } next: newUserFeedbackToSysOp } @@ -583,14 +583,14 @@ newUserInactiveDone: { desc: Finished with NUA art: DONE - options: { pause: true } + config: { pause: true } next: @menu:logoff } fullLoginSequenceLoginArt: { desc: Logging In art: WELCOME - options: { pause: true } + config: { pause: true } next: fullLoginSequenceLastCallers } @@ -598,7 +598,7 @@ desc: Last Callers module: last_callers art: LASTCALL - options: { + config: { pause: true font: cp437 } @@ -608,7 +608,7 @@ desc: Who's Online module: whos_online art: WHOSON - options: { pause: true } + config: { pause: true } next: fullLoginSequenceOnelinerz } @@ -626,10 +626,8 @@ next: fullLoginSequenceUserStats } ] - options: { - cls: true - } config: { + cls: true art: { view: ONELINER add: ONEADD @@ -737,13 +735,13 @@ fullLoginSequenceSysStats: { desc: System Stats art: SYSSTAT - options: { pause: true } + config: { pause: true } next: fullLoginSequenceUserStats } fullLoginSequenceUserStats: { desc: User Stats art: STATUS - options: { pause: true } + config: { pause: true } next: mainMenu } @@ -937,7 +935,7 @@ art: MMENU desc: Main Menu prompt: menuCommand - options: { + config: { font: cp437 } submit: [ @@ -1016,26 +1014,26 @@ desc: Last Callers module: last_callers art: LASTCALL - options: { pause: true } + config: { pause: true } } mainMenuWhosOnline: { desc: Who's Online module: whos_online art: WHOSON - options: { pause: true } + config: { pause: true } } mainMenuUserStats: { desc: User Stats art: STATUS - options: { pause: true } + config: { pause: true } } mainMenuSystemStats: { desc: System Stats art: SYSSTAT - options: { pause: true } + config: { pause: true } } mainMenuUserList: { @@ -1282,10 +1280,8 @@ mainMenuOnelinerz: { desc: Viewing Onelinerz module: onelinerz - options: { - cls: true - } config: { + cls: true art: { view: ONELINER add: ONEADD @@ -1368,10 +1364,8 @@ mainMenuRumorz: { desc: Rumorz module: rumorz - options: { - cls: true - } config: { + cls: true art: { entries: RUMORS add: RUMORADD @@ -1454,10 +1448,8 @@ bbsList: { desc: Viewing BBS List module: bbs_list - options: { - cls: true - } config: { + cls: true art: { entries: BBSLIST add: BBSADD @@ -1831,7 +1823,7 @@ messageSearchNoResults: { desc: Message Search art: MSRCNORES - options: { + config: { pause: true } } @@ -1912,8 +1904,6 @@ config: { method: messageConf key: confTag - } - options: { pause: true cls: true menuFlags: [ "popParent", "noHistory" ] @@ -1956,8 +1946,6 @@ config: { method: messageArea key: areaTag - } - options: { pause: true cls: true menuFlags: [ "popParent", "noHistory" ] @@ -2732,10 +2720,8 @@ fileBaseExportList: { module: file_base_user_list_export art: FBLISTEXP - options: { - pause: true - } config: { + pause: true templates: { entry: file_list_entry.asc } @@ -2753,7 +2739,7 @@ fileBaseExportListNoResults: { desc: Browsing Files art: FBNORES - options: { + config: { pause: true menuFlags: [ "noHistory", "popParent" ] } @@ -2975,7 +2961,7 @@ fileBaseGetRatingForSelectedEntry: { desc: Rating a File prompt: fileBaseRateEntryPrompt - options: { + config: { cls: true } submit: [ @@ -2991,7 +2977,7 @@ fileBaseListEntriesNoResults: { desc: Browsing Files art: FBNORES - options: { + config: { pause: true menuFlags: [ "noHistory", "popParent" ] } @@ -3282,7 +3268,7 @@ fileBaseDownloadManagerEmptyQueue: { desc: Empty Download Queue art: FEMPTYQ - options: { + config: { pause: true menuFlags: [ "noHistory", "popParent" ] } @@ -3446,7 +3432,7 @@ fileBaseNoUploadAreasAvail: { desc: File Base art: ULNOAREA - options: { + config: { pause: true menuFlags: [ "noHistory", "popParent" ] } diff --git a/config/prompt.hjson b/config/prompt.hjson index b165fbd5..e5a50630 100644 --- a/config/prompt.hjson +++ b/config/prompt.hjson @@ -233,7 +233,7 @@ // Any menu 'pause' will use this prompt // art: pause - options: { + config: { trailingLF: no } /* diff --git a/core/menu_module.js b/core/menu_module.js index 992c17fb..ee1502cb 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -25,11 +25,13 @@ exports.MenuModule = class MenuModule extends PluginModule { this.menuName = options.menuName; this.menuConfig = options.menuConfig; this.client = options.client; - this.menuConfig.options = options.menuConfig.options || {}; + //this.menuConfig.options = options.menuConfig.options || {}; this.menuMethods = {}; // methods called from @method's this.menuConfig.config = this.menuConfig.config || {}; - this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; + this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); + + //this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; this.viewControllers = {}; } @@ -59,7 +61,7 @@ exports.MenuModule = class MenuModule extends PluginModule { self.displayAsset( self.menuConfig.art, - self.menuConfig.options, + self.menuConfig.config, (err, artData) => { if(err) { self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } ); @@ -89,7 +91,7 @@ exports.MenuModule = class MenuModule extends PluginModule { self.displayAsset( self.menuConfig.promptConfig.art, - self.menuConfig.options, + self.menuConfig.config, (err, artData) => { if(artData) { mciData.prompt = artData.mciMap; @@ -137,9 +139,9 @@ exports.MenuModule = class MenuModule extends PluginModule { } beforeArt(cb) { - if(_.isNumber(this.menuConfig.options.baudRate)) { + if(_.isNumber(this.menuConfig.config.baudRate)) { // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here - this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate)); + this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.config.baudRate)); } if(this.cls) { @@ -220,11 +222,11 @@ exports.MenuModule = class MenuModule extends PluginModule { } shouldPause() { - return ('end' === this.menuConfig.options.pause || true === this.menuConfig.options.pause); + return ('end' === this.menuConfig.config.pause || true === this.menuConfig.config.pause); } hasNextTimeout() { - return _.isNumber(this.menuConfig.options.nextTimeout); + return _.isNumber(this.menuConfig.config.nextTimeout); } haveNext() { @@ -246,7 +248,7 @@ exports.MenuModule = class MenuModule extends PluginModule { if(this.hasNextTimeout()) { setTimeout( () => { return gotoNextMenu(); - }, this.menuConfig.options.nextTimeout); + }, this.menuConfig.config.nextTimeout); } else { return gotoNextMenu(); } diff --git a/core/menu_stack.js b/core/menu_stack.js index 06b53d76..3e9c455b 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -134,15 +134,28 @@ module.exports = class MenuStack { return; } + // + // Handle deprecated 'options' block by merging to config and warning user. + // :TODO: Remove in 0.0.10+ + // + if(modInst.menuConfig.options) { + self.client.log.warn( + { options : modInst.menuConfig.options }, + 'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions' + ); + Object.assign(modInst.menuConfig.config || {}, modInst.menuConfig.options); + delete modInst.menuConfig.options; + } + // // If menuFlags were supplied in menu.hjson, they should win over // anything supplied in code. // let menuFlags; - if(0 === modInst.menuConfig.options.menuFlags.length) { + if(0 === modInst.menuConfig.config.menuFlags.length) { menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : []; } else { - menuFlags = modInst.menuConfig.options.menuFlags; + menuFlags = modInst.menuConfig.config.menuFlags; // in code we can ask to merge in if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { @@ -179,8 +192,8 @@ module.exports = class MenuStack { const stackEntries = self.stack.map(stackEntry => { let name = stackEntry.name; - if(stackEntry.instance.menuConfig.options.menuFlags.length > 0) { - name += ` (${stackEntry.instance.menuConfig.options.menuFlags.join(', ')})`; + if(stackEntry.instance.menuConfig.config.menuFlags.length > 0) { + name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(', ')})`; } return name; }); diff --git a/core/menu_util.js b/core/menu_util.js index 7eca129d..c05f90d9 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -61,10 +61,10 @@ function loadMenu(options, cb) { }, function loadMenuModule(menuConfig, callback) { - menuConfig.options = menuConfig.options || {}; - menuConfig.options.menuFlags = menuConfig.options.menuFlags || []; - if(!Array.isArray(menuConfig.options.menuFlags)) { - menuConfig.options.menuFlags = [ menuConfig.options.menuFlags ]; + menuConfig.config = menuConfig.config || {}; + menuConfig.config.menuFlags = menuConfig.config.menuFlags || []; + if(!Array.isArray(menuConfig.config.menuFlags)) { + menuConfig.config.menuFlags = [ menuConfig.config.menuFlags ]; } const modAsset = asset.getModuleAsset(menuConfig.module); diff --git a/core/show_art.js b/core/show_art.js index bcd15d7b..30b6e56e 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -156,7 +156,7 @@ exports.getModule = class ShowArtModule extends MenuModule { // :TODO: we really need a way to supply an explicit path to look in, e.g. general/area_art/ self.displayAsset( artSpec, - self.menuConfig.options, + self.menuConfig.config, (err, artData) => { if(err) { return callback(err); diff --git a/core/theme.js b/core/theme.js index c7a2b06d..bdba70bc 100644 --- a/core/theme.js +++ b/core/theme.js @@ -571,7 +571,7 @@ function displayThemedPrompt(name, client, options, cb) { // doing so messes things up -- most terminals that support font // changing can only display a single font at at time. // - const dispOptions = Object.assign( {}, options, promptConfig.options ); + const dispOptions = Object.assign( {}, options, promptConfig.config ); // :TODO: We can use term detection to do nifty things like avoid this kind of kludge: if(!options.clearScreen) { dispOptions.font = 'not_really_a_font!'; // kludge :) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 58a06d5a..7d1bbed4 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -2,12 +2,11 @@ layout: page title: menu.hjson --- -:warning: ***IMPORTANT!*** Before making any customisations, create your own copy of `/config/menu.hjson`, and specify it in the -`general` section of `config.hjson`: +:warning: ***IMPORTANT!*** Before making any customisations, create your own copy of `/config/menu.hjson`, and specify it in the `general` section of `config.hjson`: ````hjson general: { - menuFile: my-menu.hjson + menuFile: yourboardname.hjson } ```` This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson` @@ -15,16 +14,33 @@ This document and others will refer to `menu.hjson`. This should be seen as an a ## The Basics Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. -Entries in `menu.hjson` are objects defining a menu. A menu in this sense is something the user can see -or visit. Examples include but are not limited to: +Entries in `menu.hjson` are objects or _sections_ defining a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: * Classical Main, Messages, and File menus * Art file display -* Module driven menus such as door launchers +* Module driven menus such as door launchers and other custom mods +Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system. -Each entry in `menu.hjson` defines an object that represents a menu. These objects live within the `menus` -parent object. Each object's *key* is a menu name you can reference within other menus in the system. +## Common Menu Entry Members +* `desc`: A friendly description that can be found in places such as "Who's Online" or the `%MD` MCI code. +* `art`: An art file specification. +* `next`: Specifies the next menu to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. +* `prompt`: Specifies a prompt, by name, to use along with this menu. +* `form`: Defines one or more forms available on this menu. +* `submit`: Defines a submit handler when using `prompt`. +* `config`: May contain any of the following standard configuration members in addition to per-module defined types (see appropriate module for more information): + * `cls`: If `true` the screen will be cleared before showing this menu. + * `pause`: If `true` a pause will occur after showing this menu. Useful for simple menus such as displaying art or status screens. + * `nextTimeout`: Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. + * `baudRate`: Sets the SyncTERM style emulated baud rate. May be `300`, `600`, `1200`, `2400`, `4800`, `9600`, `19200`, `38400`, `57600`, `76800`, or `115200`. A value of `ulimited`, `off`, or `0` resets (disables) the rate. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. + * `font`: Sets the SyncTERM style font. May be one of the following: `cp437`, `cp1251`, `koi8_r`, `iso8859_2`, `iso8859_4`, `cp866`, `iso8859_9`, `haik8`, `iso8859_8`, `koi8_u`, `iso8859_15`, `iso8859_4`, `koi8_r_b`, `iso8859_4`, `iso8859_5`, `ARMSCII_8`, `iso8859_15`, `cp850`, `cp850`, `cp885`, `cp1251`, `iso8859_7`, `koi8-r_c`, `iso8859_4`, `iso8859_1`, `cp866`, `cp437`, `cp866`, `cp885`, `cp866_u`, `iso8859_1`, `cp1131`, `c64_upper`, `c64_lower`, `c128_upper`, `c128_lower`, `atari`, `pot_noodle`, `mo_soul`, `microknight_plus`, `topaz_plus`, `microknight`, `topaz`. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. + +## Forms +TODO + +## Submit Handlers +TODO ## Example Let's look a couple basic menu entries: @@ -37,18 +53,13 @@ telnetConnected: { } ``` -The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in -the Telnet server's config). +The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). -An art pattern of `CONNECT` is set telling the system to look for `CONNECT.*` where `` represents -a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If -desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`. +An art pattern of `CONNECT` is set telling the system to look for `CONNECT.*` where `` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`. -The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after -`telnetConnected`. +The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`. -Finally, an `options` object may contain various common options for menus. In this case, `nextTimeout` -tells the system to proceed to the `next` entry automatically after 1500ms. +Finally, an `options` object may contain various common options for menus. In this case, `nextTimeout` tells the system to proceed to the `next` entry automatically after 1500ms. Now let's look at `matrix`, the `next` entry from `telnetConnected`: From d54c38b9a979cea5a8412037f8f2199648512a6a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Aug 2018 21:22:32 -0600 Subject: [PATCH 252/569] Default to 'cp437' encoding vs current client encoding so we have something stable --- core/abracadabra.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 9ada9e5a..1ddec925 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -155,7 +155,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { cwd : this.config.cwd, // null/undefined = parent_of(cmd) args : this.config.args, io : this.config.io || 'stdio', - encoding : this.config.encoding || this.client.term.outputEncoding, + encoding : this.config.encoding || 'cp437', dropFile : this.dropFile.fileName, dropFilePath : this.dropFile.fullPath, node : this.client.node, From f0aa611904597674aab0cb6e802886b7ed183534 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Aug 2018 21:22:45 -0600 Subject: [PATCH 253/569] Better abracadabra docs! --- docs/modding/local-doors.md | 41 +++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index b9620748..719a992b 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -5,28 +5,43 @@ title: Local Doors ## The abracadabra Module The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and process I/O through stdio or a temporary TCP server. -The `abracadabra` `config` block can contain the following: -* `name`: Used as a key for tracking number of clients using a particular door -* `dropFileType`: Specifies the type of drop file to generate (See table below) -* `cmd`: Path to executable to launch +## Configuration +The `abracadabra` `config` block can contain the following members: +* `name`: Used as a key for tracking number of clients using a particular door. +* `dropFileType`: Specifies the type of drop file to generate (See **Argument Variables** below). +* `cmd`: Path to executable to launch. * `args`: Array of argument(s) to pass to `cmd`. See below for information on variables that can be used here. +* `cwd`: Set the Current Working Directory for `cmd`. Defaults to the directory of `cmd`. * `nodeMax`: Max number of nodes that can access this door at once. Uses `name` as a mapping key * `tooManyArt`: Art file spec to display if too many instances are already in use * `io`: Where to process I/O. Can be `stdio` or `socket` +* `encoding`: Specify the door's encoding. Defaults to `cp437`. Linux binaries for example, often produce `utf8`. +### Drop File Types Drop file types specified by `dropFileType`: * `DOOR`: [DOOR.SYS](http://goldfndr.home.mindspring.com/dropfile/doorsys.htm) * `DOOR32`: [DOOR32.SYS](http://wiki.bbses.info/index.php/DOOR32.SYS) * `DORINFO`: [DORINFOx.DEF](http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm) -Variables for use in `args`: -* `{node}`: Current node number -* `{dropFile}`: Path to generated drop file -* `{userId}`: Current user ID -* `{srvPort}`: Tempoary server port when `io` is `socket` +### Argument Variables +The following variables may be used in `{args}` entries: +* `{node}`: Current node number. +* `{dropFile}`: Drop _filename_ only. +* `{dropFilePath}`: Full path to generated drop file. +* `{userId}`: Current user ID. +* `{userName}`: _Sanatized_ username. Safe for filenames, etc. +* `{userNameRaw}`: _Raw_ username. May not be safe for filenames! +* `{srvPort}`: Tempoary server port when `io` is set to `socket`. +* `{cwd}`: Current Working Directory. +Example: +```hjson +args: [ + "-D", "{dropFile}", "-N", "{node}" +] +``` -### DOSEMU with abracadabra +## DOSEMU with abracadabra [DOSEMU](http://www.dosemu.org/) can provide a good solution for running legacy DOS doors when running on Linux systems. For this, we will create a virtual serial port (COM1) that communicates via stdio. As an example, here are the steps for setting up Pimp Wars: @@ -82,12 +97,12 @@ doorPimpWars: { ``` -### QEMU with abracadabra +## QEMU with abracadabra [QEMU](http://wiki.qemu.org/Main_Page) provides a robust, cross platform solution for launching doors under many platforms (likely anwywhere Node.js is supported and ENiGMA½ can run). Note however that there is an important and major caveat: **Multiple instances of a particular door/OS image should not be run at once!** Being more flexible means being a bit more complex. Let's look at an example for running L.O.R.D. under a UNIX like system such as Linux or FreeBSD. Basically we'll be creating a bootstrap shell script that generates a temporary node specific `go.bat` to launch our door. This will be called from `autoexec.bat` within our QEMU FreeDOS partition. -#### Step 1: Create a FreeDOS image +### Step 1: Create a FreeDOS image [FreeDOS](http://www.freedos.org/) is a free mostly MS-DOS compatible DOS package that works well for running 16bit doors. Follow the [QEMU/FreeDOS](https://en.wikibooks.org/wiki/QEMU/FreeDOS) guide for creating an `freedos_c.img`. This will contain FreeDOS itself and installed BBS doors. After this is complete, copy LORD to C:\DOORS\LORD within FreeDOS. An easy way to tranfer files from host to DOS is to use QEMU's vfat as a drive. For example: @@ -100,7 +115,7 @@ With the above you can now copy files from D: to C: within FreeDOS and add the f CALL E:\GO.BAT ``` -#### Step 2: Create a bootstrap script +### Step 2: Create a bootstrap script Our bootstrap script will prepare `GO.BAT` and launch FreeDOS. Below is an example: From aa2e2e56e31860fe8ca5869a3cf2643f5cd37855 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 Aug 2018 20:45:50 -0600 Subject: [PATCH 254/569] Fix FTN address lookup crash when scanning --- core/scanner_tossers/ftn_bso.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index f20e61ea..e2260d93 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1240,7 +1240,7 @@ function FTNMessageScanTossModule() { // we can only assume the message is to the +op, else we'll have to fail. // const toUserNameAsAddress = Address.fromString(message.toUserName); - if(toUserNameAsAddress.isValid()) { + if(toUserNameAsAddress && toUserNameAsAddress.isValid()) { Log.info( { toUserName : message.toUserName, fromUserName : message.fromUserName }, @@ -1879,7 +1879,7 @@ function FTNMessageScanTossModule() { if(err) { Log.warn( { error : err.message, oldPath : oldPath }, 'Failed removing old physical file during TIC replacement'); } else { - Log.debug( { oldPath : oldPath }, 'Removed old physical file during TIC replacement'); + Log.trace( { oldPath : oldPath }, 'Removed old physical file during TIC replacement'); } return callback(null, localInfo); // continue even if err }); @@ -1889,7 +1889,7 @@ function FTNMessageScanTossModule() { if(err) { Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed import/update TIC record' ); } else { - Log.debug( + Log.info( { tic : ticFileInfo.path, file : ticFileInfo.filePath, area : localInfo.areaTag }, 'TIC imported successfully' ); From fc9271ad03c16cc46ef93abfe650e74f389dbf9a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 Aug 2018 20:46:49 -0600 Subject: [PATCH 255/569] Initial start of oputil stuff --- docs/_includes/nav.md | 4 +++- docs/admin/oputil.md | 47 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 docs/admin/oputil.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index c366c8fa..33702016 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -25,7 +25,6 @@ - [Access Condition System (ACS)]({{ site.baseurl }}{% link configuration/acs.md %}) - Scheduled jobs - - File Base - [About]({{ site.baseurl }}{% link filebase/index.md %}) - [Configuring a File Area]({{ site.baseurl }}{% link filebase/first-file-area.md %}) @@ -70,6 +69,9 @@ - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) - [Message Conference List]({{ site.baseurl }}{% link modding/msg-conf-list.md %}) - [Message Area List]({{ site.baseurl }}{% link modding/msg-area-list.md %}) + + - Administration + - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) - [Oputil]({{ site.baseurl }}{% link oputil/index.md %}) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md new file mode 100644 index 00000000..9935ea68 --- /dev/null +++ b/docs/admin/oputil.md @@ -0,0 +1,47 @@ +--- +layout: page +title: oputil +--- +## The oputil CLI +ENiGMA½ comes with `oputil.js` henceforth known as `oputil`, a command line interface (CLI) tool for sysops to perform general system and user administration. You likely used oputil to do the initial ENiGMA configuration. + +Let's look the main help output as per this writing: + +``` +usage: optutil.js [--version] [--help] + [] + +global args: + -c, --config PATH specify config path (./config/) + -n, --no-prompt assume defaults/don't prompt for input where possible + +commands: + user user utilities + config config file management + fb file base management + mb message base management +``` + +Commands break up operations by groups. Type `./oputil.js --help` for additional help on a particular command. The next sections will describe them. + +## User +``` +usage: optutil.js user [] + +actions: + pw USERNAME PASSWORD set password to PASSWORD for USERNAME + rm USERNAME permanantely removes USERNAME user from system + activate USERNAME sets USERNAME's status to active + deactivate USERNAME sets USERNAME's status to deactive + disable USERNAME sets USERNAME's status to disabled + group USERNAME [+|-]GROUP adds (+) or removes (-) USERNAME from GROUP +``` + +| Action | Description | Examples | Aliases | +|-----------|-------------------|---------------------------------------|-----------| +| `pw` | Set password | `./oputil.js user pw joeuser s3cr37` | `pass`, `passwd`, `password` | +| `rm` | Removes user | `./oputil.js user del joeuser` | `remove`, `del`, `delete` | +| `activate` | Activates user | `./oputil.js user activate joeuser` | N/A | +| `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | +| `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | +| `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser -derp` | N/A | From 1494adc3047cde5fb28462768e010e4c8b91f802 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 Aug 2018 21:50:50 -0600 Subject: [PATCH 256/569] More updates to oputil --- docs/admin/oputil.md | 101 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 9935ea68..ba456025 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -22,9 +22,23 @@ commands: mb message base management ``` -Commands break up operations by groups. Type `./oputil.js --help` for additional help on a particular command. The next sections will describe them. +Commands break up operations by groups: +| Command | Description | +|-----------|---------------| +| `user` | User management | +| `config` | System configuration and maintentance | +| `fb` | File base configuration and management | +| `mb` | Message base configuration and management | + +Global arguments apply to most commands and actions: +* `--config`: Specify configuration directory if it is not the default of `./config/`. +* `--no-prompt`: Assume defaults and do not prompt when posisible. + +Type `./oputil.js --help` for additional help on a particular command. The following sections will describe them. ## User +The `user` command covers various user operations. + ``` usage: optutil.js user [] @@ -45,3 +59,88 @@ actions: | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | | `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | | `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser -derp` | N/A | + +## Configuration +The `config` command allows sysops to perform various system configuration and maintenance tasks. + +``` +usage: optutil.js config [] + +actions: + new generate a new/initial configuration + import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH + +import-areas args: + --conf CONF_TAG specify conference tag in which to import areas + --network NETWORK specify network name/key to associate FTN areas + --uplinks UL1,UL2,... specify one or more comma separated uplinks + --type TYPE specifies area import type. valid options are "bbs" and "na" +``` + + +| Action | Description | Examples | +|-----------|-------------------|---------------------------------------| +| `new` | Generates a new/initial configuration | `./oputil.js config new` (follow the prompts) | +| `import-areas` | Imports areas using a Fidonet style *.NA or AREAS.BBS formatted file | `./oputil.js config import-areas /some/path/l33tnet.na` | + +When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args". + +## File Base Management +The `fb` command provides a powerful file base management interface. + +``` +usage: oputil.js fb [] + +actions: + scan AREA_TAG[@STORAGE_TAG] scan specified area + may also contain optional GLOB as last parameter, + for examle: scan some_area *.zip + + info AREA_TAG|SHA|FILE_ID display information about areas and/or files + SHA may be a full or partial SHA-256 + + mv SRC [SRC...] DST move entry(s) from SRC to DST + SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + DST: AREA_TAG[@STORAGE_TAG] + + rm SRC [SRC...] remove entry(s) from the system matching SRC + SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + +scan args: + --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 + --update attempt to update information for existing entries + --quick perform quick scan + +info args: + --show-desc display short description, if any + +remove args: + --phys-file also remove underlying physical file + +general information: + AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag + example: retro@bbs + + FILENAME_WC filename with * and ? wildcard support. may match 0:n entries + SHA full or partial SHA-256 + FILE_ID a file identifier. see file.sqlite3 +``` + +#### Scan File Area +The `scan` action can (re)scan a file area for new entries as well as update (`--update`) existing entry records (description, etc.). When scanning, a valid area tag must be specified. Optionally, storage tag may also be supplied in order to scan a specific filesystem location using the `@the_storage_tag` syntax. If a [GLOB](http://man7.org/linux/man-pages/man7/glob.7.html) is supplied as the last argument, only file entries with filenames matching will be processed. + +#### Examples +Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extentions: +``` +./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip` +``` + +Update all entries in the "artscene" area supplying the file tags "artscene", and "textmode". +``` +./oputil.js fb scan --update --quick --tags artscene,textmode artscene` +``` \ No newline at end of file From b8ea3bbd5b8fc04852895e310ed33214bad69358 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 11 Aug 2018 21:58:56 -0600 Subject: [PATCH 257/569] Fix table when showing on GitHub...hopefully --- docs/admin/oputil.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index ba456025..c5a8acc7 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -23,6 +23,7 @@ commands: ``` Commands break up operations by groups: + | Command | Description | |-----------|---------------| | `user` | User management | From 13d30827aabc12a137b1ddc901c7dd46bb4b4d05 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 14 Aug 2018 21:53:00 -0600 Subject: [PATCH 258/569] Allow how many to keep in DB by config 'retainCount' --- core/onelinerz.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/onelinerz.js b/core/onelinerz.js index 2d902a8e..19b9ce44 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -287,14 +287,15 @@ exports.getModule = class OnelinerzModule extends MenuModule { ); }, function removeOld(callback) { - // keep 25 max most recent items - remove the older ones + // keep 25 max most recent items by default - remove the older ones + const retainCount = self.menuConfig.config.retainCount || 25; self.db.run( `DELETE FROM onelinerz WHERE id IN ( SELECT id FROM onelinerz ORDER BY id DESC - LIMIT -1 OFFSET 25 + LIMIT -1 OFFSET ${retainCount} );`, callback ); From b6a1008700bc46c6d6fd843fb88986afad53e86d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 11 Sep 2018 14:41:46 -0600 Subject: [PATCH 259/569] Some 'fb info' docs --- docs/admin/oputil.md | 52 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index c5a8acc7..8fbf0a46 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -97,7 +97,9 @@ actions: may also contain optional GLOB as last parameter, for examle: scan some_area *.zip - info AREA_TAG|SHA|FILE_ID display information about areas and/or files + info CRITERIA display information about areas and/or files + where CRITERIA is one of the following: + AREA_TAG|SHA|FILE_ID|FILENAME_WC SHA may be a full or partial SHA-256 mv SRC [SRC...] DST move entry(s) from SRC to DST @@ -135,13 +137,53 @@ general information: #### Scan File Area The `scan` action can (re)scan a file area for new entries as well as update (`--update`) existing entry records (description, etc.). When scanning, a valid area tag must be specified. Optionally, storage tag may also be supplied in order to scan a specific filesystem location using the `@the_storage_tag` syntax. If a [GLOB](http://man7.org/linux/man-pages/man7/glob.7.html) is supplied as the last argument, only file entries with filenames matching will be processed. -#### Examples +##### Examples Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extentions: ``` -./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip` +$ ./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip` ``` Update all entries in the "artscene" area supplying the file tags "artscene", and "textmode". ``` -./oputil.js fb scan --update --quick --tags artscene,textmode artscene` -``` \ No newline at end of file +$ ./oputil.js fb scan --update --quick --tags artscene,textmode artscene` +``` + +Scan "oldschoolbbs" area using the description file at "/path/to/DESCRIPT.ION": +``` +$ ./oputil.js fb scan --desc-file /path/to/DESCRIPT.ION oldschoolbbs +``` + +#### Retrieve Information +The `info` action can retrieve information about an area or file entry(s). + +##### Examples +Information about a particular area: +``` +$ ./oputil.js fb info retro_pc +areaTag: retro_pc +name: Retro PC +desc: Oldschool / retro PC +storageTag: retro_pc_tdc_1990 => /file_base/dos/tdc/1990 +storageTag: retro_pc_tdc_1991 => /file_base/dos/tdc/1991 +storageTag: retro_pc_tdc_1992 => /file_base/dos/tdc/1992 +storageTag: retro_pc_tdc_1993 => /file_base/dos/tdc/1993 +``` + +Perhaps we want to fetch some information about a file in which we know piece of the filename: +``` +$ ./oputil.js fb info "impulse*" +file_id: 143 +sha_256: 547299301254ccd73eba4c0ec9cd6ab8c5929fbb655e72c4cc842f11332792d4 +area_tag: impulse_project +storage_tag: impulse_project +path: /file_base/impulse_project/impulseproject01.tar.gz +hashTags: impulse.project,8bit.music,cid +uploaded: 2018-03-10T11:36:41-07:00 +dl_count: 23 +archive_type: application/gzip +byte_size: 114313 +est_release_year: 2015 +file_crc32: fc6655d +file_md5: 3455f74bbbf9539e69bd38f45e039a4e +file_sha1: 558fab3b49a8ac302486e023a3c2a86bd4e4b948 +``` From 4501759d99eb271df403327ac4d3eca6519f50a4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 11 Sep 2018 14:42:36 -0600 Subject: [PATCH 260/569] Better help for 'fb info' --- core/oputil/oputil_help.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 6f86cdf0..4981f922 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -54,7 +54,9 @@ actions: may also contain optional GLOB as last parameter, for examle: scan some_area *.zip - info AREA_TAG|SHA|FILE_ID display information about areas and/or files + info CRITERIA display information about areas and/or files + where CRITERIA is one of the following: + AREA_TAG|SHA|FILE_ID|FILENAME_WC SHA may be a full or partial SHA-256 mv SRC [SRC...] DST move entry(s) from SRC to DST From e6055e0f11ef5ec0a9b4ce56b10f8159cd860ba4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 11 Sep 2018 14:44:22 -0600 Subject: [PATCH 261/569] Better logging --- core/file_base_area.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 384d4e36..78450501 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -280,19 +280,17 @@ function attemptSetEstimatedReleaseDate(fileEntry) { } // a simple log proxy for when we call from oputil.js -const logDebug = (obj, msg) => { +const maybeLog = (obj, msg, level) => { if(Log) { - Log.debug(obj, msg); + Log[level](obj, msg); + } else if ('error' === level) { + console.error(`${msg}: ${JSON.stringify(obj)}`); // eslint-disable-line no-console } -} +}; -const logError = (obj, msg) => { - if(Log) { - Log.error(obj, msg); - } else { - console.error(`${msg}: ${JSON.stringify(obj)}`); - } -} +const logDebug = (obj, msg) => maybeLog(obj, msg, 'debug'); +const logTrace = (obj, msg) => maybeLog(obj, msg, 'trace'); +const logError = (obj, msg) => maybeLog(obj, msg, 'error'); function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { async.waterfall( @@ -381,7 +379,7 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { // cleanup but don't wait temptmp.cleanup( paths => { // note: don't use client logger here - may not be avail - logDebug( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); + logTrace( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); }); return callback(null); }); From 9bb3557509810c9916d365f12de5493878fc0e6d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Sep 2018 20:34:39 -0600 Subject: [PATCH 262/569] WIP add in some basic movement MCI codes & color/pipe code cleanup --- core/client_term.js | 12 +----- core/color_codes.js | 91 +++++++--------------------------------- core/mci_view_factory.js | 18 +++++--- core/predefined_mci.js | 21 +++++++--- 4 files changed, 44 insertions(+), 98 deletions(-) diff --git a/core/client_term.js b/core/client_term.js index eb1ee4f9..f537c359 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -3,7 +3,6 @@ // ENiGMA½ var Log = require('./logger.js').log; -var enigmaToAnsi = require('./color_codes.js').enigmaToAnsi; var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; var iconv = require('iconv-lite'); @@ -174,15 +173,8 @@ ClientTerminal.prototype.rawWrite = function(s, cb) { } }; -ClientTerminal.prototype.pipeWrite = function(s, spec, cb) { - spec = spec || 'renegade'; - - var conv = { - enigma : enigmaToAnsi, - renegade : renegadeToAnsi, - }[spec] || renegadeToAnsi; - - this.write(conv(s, this), null, cb); // null = use default for |convertLineFeeds| +ClientTerminal.prototype.pipeWrite = function(s, cb) { + this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds| }; ClientTerminal.prototype.encode = function(s, convertLineFeeds) { diff --git a/core/color_codes.js b/core/color_codes.js index 272b611a..31a1f983 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -1,13 +1,12 @@ /* jslint node: true */ 'use strict'; -var ansi = require('./ansi_term.js'); -var getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; +const ANSI = require('./ansi_term.js'); +const { getPredefinedMCIValue } = require('./predefined_mci.js'); -var assert = require('assert'); -var _ = require('lodash'); +// deps +const _ = require('lodash'); -exports.enigmaToAnsi = enigmaToAnsi; exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes; exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen; exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; @@ -15,70 +14,6 @@ exports.controlCodesToAnsi = controlCodesToAnsi; // :TODO: Not really happy with the module name of "color_codes". Would like something better - - - -// Also add: -// * fromCelerity(): | -// * fromPCBoard(): (@X) -// * fromWildcat(): (@@ (same as PCBoard without 'X' prefix and '@' suffix) -// * fromWWIV(): <0-7> -// * fromSyncronet(): -// 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) { - if(-1 == s.indexOf('|')) { - return s; // no pipe codes present - } - - var result = ''; - var re = /\|([A-Z\d]{2}|\|)/g; - var m; - var lastIndex = 0; - while((m = re.exec(s))) { - var val = m[1]; - - if('|' == val) { - result += '|'; - continue; - } - - // convert to number - val = parseInt(val, 10); - if(isNaN(val)) { - // - // ENiGMA MCI code? Only available if |client| - // is supplied. - // - val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal - } - - if(_.isString(val)) { - result += s.substr(lastIndex, m.index - lastIndex) + val; - } else { - assert(val >= 0 && val <= 47); - - var attr = ''; - if(7 == val) { - attr = ansi.sgr('normal'); - } else if (val < 7 || val >= 16) { - attr = ansi.sgr(['normal', val]); - } else if (val <= 15) { - attr = ansi.sgr(['normal', val - 8, 'bold']); - } - - result += s.substr(lastIndex, m.index - lastIndex) + attr; - } - - lastIndex = re.lastIndex; - } - - result = (0 === result.length ? s : result + s.substr(lastIndex)); - - return result; -} - function stripEnigmaCodes(s) { return s.replace(/\|[A-Z\d]{2}/g, ''); } @@ -88,7 +23,7 @@ function enigmaStrLen(s) { } function ansiSgrFromRenegadeColorCode(cc) { - return ansi.sgr({ + return ANSI.sgr({ 0 : [ 'reset', 'black' ], 1 : [ 'reset', 'blue' ], 2 : [ 'reset', 'green' ], @@ -132,10 +67,11 @@ function renegadeToAnsi(s, client) { return s; // no pipe codes present } - var result = ''; - var re = /\|([A-Z\d]{2}|\|)/g; - var m; - var lastIndex = 0; + let result = ''; + const re = /\|([A-Z\d]{2}|\|)/g; + //const re = /\|(?:(C[FBUD])([0-9]{1,2})|([A-Z\d]{2})|(\|))/g; + let m; + let lastIndex = 0; while((m = re.exec(s))) { var val = m[1]; @@ -192,7 +128,7 @@ function controlCodesToAnsi(s, client) { while((m = RE.exec(s))) { switch(m[0].charAt(0)) { case '|' : - // Renegade or ENiGMA MCI + // Renegade |## v = parseInt(m[2], 10); if(isNaN(v)) { @@ -256,17 +192,18 @@ function controlCodesToAnsi(s, client) { F : [ 'bold', 'whiteBG' ], }[v.charAt(1)] || [ 'normal' ]; - v = ansi.sgr(fg.concat(bg)); + v = ANSI.sgr(fg.concat(bg)); result += s.substr(lastIndex, m.index - lastIndex) + v; break; case '\x03' : + // WWIV v = parseInt(m[8], 10); if(isNaN(v)) { v += m[0]; } else { - v = ansi.sgr({ + v = ANSI.sgr({ 0 : [ 'reset', 'black' ], 1 : [ 'bold', 'cyan' ], 2 : [ 'bold', 'yellow' ], diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 85c440e5..037121e5 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -37,6 +37,10 @@ MCIViewFactory.UserViewCodes = [ 'XY', ]; +MCIViewFactory.MovementCodes = [ + 'CF', 'CB', 'CU', 'CD', +]; + MCIViewFactory.prototype.createFromMCI = function(mci) { assert(mci.code); assert(mci.id > 0); @@ -192,14 +196,16 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { break; default : - options.text = getPredefinedMCIValue(this.client, mci.code); - if(_.isString(options.text)) { - setWidth(0); + if(!MCIViewFactory.MovementCodes.includes(mci.code)) { + options.text = getPredefinedMCIValue(this.client, mci.code); + if(_.isString(options.text)) { + setWidth(0); - setOption(1, 'textStyle'); - setOption(2, 'justify'); + setOption(1, 'textStyle'); + setOption(2, 'justify'); - view = new TextView(options); + view = new TextView(options); + } } break; } diff --git a/core/predefined_mci.js b/core/predefined_mci.js index fe8d4b43..9888ba5e 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -4,12 +4,15 @@ // ENiGMA½ const Config = require('./config.js').get; const Log = require('./logger.js').log; -const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; -const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; +const { + getMessageAreaByTag, + getMessageConferenceByTag +} = require('./message_area.js'); const clientConnections = require('./client_connections.js'); const StatLog = require('./stat_log.js'); const FileBaseFilters = require('./file_base_filter.js'); -const formatByteSize = require('./string_util.js').formatByteSize; +const { formatByteSize } = require('./string_util.js'); +const ANSI = require('./ansi_term.js'); // deps const packageJson = require('../package.json'); @@ -227,9 +230,17 @@ const PREDEFINED_MCI_GENERATORS = { // Special handling for XY // XY : function xyHack() { return; /* nothing */ }, + + // + // Various movement by N + // + CF : function cursorForwardBy(client, n = 1) { return ANSI.forward(n); }, + CB : function cursorBackBy(client, n = 1) { return ANSI.back(n); }, + CU : function cursorUpBy(client, n = 1) { return ANSI.up(n); }, + CD : function cursorDownBy(client, n = 1) { return ANSI.down(n); }, }; -function getPredefinedMCIValue(client, code) { +function getPredefinedMCIValue(client, code, extra) { if(!client || !code) { return; @@ -240,7 +251,7 @@ function getPredefinedMCIValue(client, code) { if(generator) { let value; try { - value = generator(client); + value = generator(client, extra); } catch(e) { Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' ); } From 2c7354b4fa73aa64f3bb0486f6553ba14bf0acb8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Sep 2018 21:11:44 -0600 Subject: [PATCH 263/569] Allow movement codes in renegadeToAnsi() --- core/color_codes.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index 31a1f983..a6f7ba9d 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -68,29 +68,22 @@ function renegadeToAnsi(s, client) { } let result = ''; - const re = /\|([A-Z\d]{2}|\|)/g; - //const re = /\|(?:(C[FBUD])([0-9]{1,2})|([A-Z\d]{2})|(\|))/g; + const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g; let m; let lastIndex = 0; while((m = re.exec(s))) { - var val = m[1]; - - if('|' == val) { - result += '|'; - continue; - } - - // convert to number - val = parseInt(val, 10); - if(isNaN(val)) { - val = getPredefinedMCIValue(client, m[1]) || ('|' + m[1]); // value itself or literal - } - - if(_.isString(val)) { - result += s.substr(lastIndex, m.index - lastIndex) + val; - } else { + if(m[3]) { + // |## color + const val = parseInt(m[3], 10); const attr = ansiSgrFromRenegadeColorCode(val); result += s.substr(lastIndex, m.index - lastIndex) + attr; + } else if(m[4] || m[1]) { + // |AA MCI code or |Cx## movement where ## is in m[1] + const val = getPredefinedMCIValue(client, m[4] || m[1], m[2]) || (m[0]); // value itself or literal + result += s.substr(lastIndex, m.index - lastIndex) + val; + } else if(m[5]) { + // || -- literal '|', that is. + result += '|'; } lastIndex = re.lastIndex; From d7aabba847b257983cd3c0908afdccb903accc60 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Sep 2018 13:12:54 -0600 Subject: [PATCH 264/569] Add CNET style Y-Style/Q-Style color code support --- core/color_codes.js | 70 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index a6f7ba9d..2032f057 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -7,19 +7,19 @@ const { getPredefinedMCIValue } = require('./predefined_mci.js'); // deps const _ = require('lodash'); -exports.stripPipeCodes = exports.stripEnigmaCodes = stripEnigmaCodes; -exports.pipeStrLen = exports.enigmaStrLen = enigmaStrLen; -exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; +exports.stripMciColorCodes = stripMciColorCodes; +exports.pipeStringLength = pipeStringLength; +exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; exports.controlCodesToAnsi = controlCodesToAnsi; -// :TODO: Not really happy with the module name of "color_codes". Would like something better +// :TODO: Not really happy with the module name of "color_codes". Would like something better ... control_code_string? -function stripEnigmaCodes(s) { +function stripMciColorCodes(s) { return s.replace(/\|[A-Z\d]{2}/g, ''); } -function enigmaStrLen(s) { - return stripEnigmaCodes(s).length; +function pipeStringLength(s) { + return stripMciColorCodes(s).length; } function ansiSgrFromRenegadeColorCode(cc) { @@ -62,6 +62,37 @@ function ansiSgrFromRenegadeColorCode(cc) { }[cc] || 'normal'); } +function ansiSgrFromCnetStyleColorCode(cc) { + return ANSI.sgr({ + c0 : [ 'reset', 'black' ], + c1 : [ 'reset', 'red' ], + c2 : [ 'reset', 'green' ], + c3 : [ 'reset', 'yellow' ], + c4 : [ 'reset', 'blue' ], + c5 : [ 'reset', 'magenta' ], + c6 : [ 'reset', 'cyan' ], + c7 : [ 'reset', 'white' ], + + c8 : [ 'bold', 'black' ], + c9 : [ 'bold', 'red' ], + ca : [ 'bold', 'green' ], + cb : [ 'bold', 'yellow' ], + cc : [ 'bold', 'blue' ], + cd : [ 'bold', 'magenta' ], + ce : [ 'bold', 'cyan' ], + cf : [ 'bold', 'white' ], + + z0 : [ 'blackBG' ], + z1 : [ 'redBG' ], + z2 : [ 'greenBG' ], + z3 : [ 'yellowBG' ], + z4 : [ 'blueBG' ], + z5 : [ 'magentaBG' ], + z6 : [ 'cyanBG' ], + z7 : [ 'whiteBG' ], + }[cc] || 'normal'); +} + function renegadeToAnsi(s, client) { if(-1 == s.indexOf('|')) { return s; // no pipe codes present @@ -98,10 +129,12 @@ function renegadeToAnsi(s, client) { // MCI codes. // // Supported control code formats: -// * Renegade : |## -// * PCBoard : @X## where the first number/char is FG color, and second is BG -// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix -// * WWIV : ^# +// * Renegade : |## +// * PCBoard : @X## where the first number/char is FG color, and second is BG +// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix +// * WWIV : ^# +// * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format +// * CNET Q-style : 0x11##} where ## is a specific set of codes -- this is the newer format // // TODO: Add Synchronet and Celerity format support // @@ -109,7 +142,7 @@ function renegadeToAnsi(s, client) { // * http://wiki.synchro.net/custom:colors // function controlCodesToAnsi(s, client) { - const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)/g; // eslint-disable-line no-control-regex + const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1)}|\x11)/g; // eslint-disable-line no-control-regex let m; let result = ''; @@ -213,6 +246,19 @@ function controlCodesToAnsi(s, client) { result += s.substr(lastIndex, m.index - lastIndex) + v; break; + + case '\x19' : + case '\0x11' : + // CNET "Y-Style" & "Q-Style" + v = m[9] || m[11]; + if('n1' === v) { + result += '\n'; + } else if('f1' === v) { + result += ANSI.clearScreen(); + } else { + result += ansiSgrFromCnetStyleColorCode(v); + } + break; } lastIndex = RE.lastIndex; From 63b5eed504d1b643a0b987ab3c72f68c66cb40b9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 15:01:27 -0700 Subject: [PATCH 265/569] Minor updates to FileEntry / oputil --- core/file_entry.js | 25 ++++++++++++++++++++++++- core/oputil/oputil_file_base.js | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 0f519e10..bb7a4e12 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -369,7 +369,7 @@ module.exports = class FileEntry { return Object.keys(FILE_WELL_KNOWN_META); } - static findFileBySha(sha, cb) { + static findBySha(sha, cb) { // full or partial SHA-256 fileDb.all( `SELECT file_id @@ -397,6 +397,29 @@ module.exports = class FileEntry { ); } + // Attempt to fine a file by an *existing* full path. + // Checkums may have changed and are not validated here. + static findByFullPath(fullPath, cb) { + // first, basic by-filename lookup. + FileEntry.findByFileNameWildcard(paths.basename(fuillPath), (err, entries) => { + if(err) { + return cb(err); + } + if(!entries || !entries.length || entries.length > 1) { + return cb(Errors.DoesNotExist('No matches')); + } + + // ensure the *full* path has not changed + // :TODO: if FS is case-insensitive, we probably want a better check here + const possibleMatch = entries[0]; + if(possibleMatch.fullPath === fullPath) { + return cb(null, possibleMatch); + } + + return cb(Errors.DoesNotExist('No matches')); + }); + } + static findByFileNameWildcard(wc, cb) { // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html wc = wc.replace(/\*/g, '%').replace(/\?/g, '_'); diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 8568372c..9dfcdeca 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -319,7 +319,7 @@ function getFileEntries(pattern, cb) { return callback(null, entries); // already got it by FILE_ID } - FileEntry.findFileBySha(pattern, (err, fileEntry) => { + FileEntry.findBySha(pattern, (err, fileEntry) => { return callback(null, fileEntry ? [ fileEntry ] : null ); }); }, From 92a7007e9c89cfca7001ee743a4d614b3c97feaf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 16:12:21 -0700 Subject: [PATCH 266/569] Documentation updates around MCI movement --- WHATSNEW.md | 1 + docs/art/mci.md | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 5e92784e..b157a927 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -18,6 +18,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * `{userName}` (sanatized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door. * Any module may now register for a system startup intiialization via the `initializeModules(initInfo, cb)` export. * User event log is now functional. Various events a user performs will be persisted to the `system.db` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`! +* New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md) ## 0.0.8-alpha diff --git a/docs/art/mci.md b/docs/art/mci.md index 1bad97d5..f5876105 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -11,8 +11,7 @@ are set by placing duplicate codes back to back in art files. ## Predefined MCI Codes There are many predefined MCI codes that can be used anywhere on the system (placed in any art file). More are added all the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) -for a full listing. Many codes attempt to pay homage to Oblivion/2, -iNiQUiTY, etc. +for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, etc. | Code | Description | |------|--------------| @@ -72,7 +71,14 @@ iNiQUiTY, 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 or to extend an otherwise empty space in an art file down the screen. +Some additional special case codes also exist: +| Code | Description | +|--------|--------------| +| `CF##` | Moves the cursor position forward _##_ characters | +| `CB##` | Moves the cursor position back _##_ characters | +| `CU##` | Moves the cursor position up _##_ characters | +| `CD##` | Moves the cursor position down _##_ characters | +| `XY` | A special code that may be utilized for placement identification when creating menus or to extend an otherwise empty space in an art file down the screen. ## Views From 206312302ae4885ff5d6eb09b46e5a636f83348e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 18:34:54 -0700 Subject: [PATCH 267/569] Fix generated logging level --- core/oputil/oputil_config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index d28c977f..65ede6bd 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -222,7 +222,9 @@ function askNewConfigQuestions(cb) { } config.logging = { - level : answers.loggingLevel, + rotatingFile : { + level : answers.loggingLevel, + } }; callback(null); From 25560cb47a9f7d015465414e8dfafe5aa9fc4939 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 19:29:51 -0700 Subject: [PATCH 268/569] SyncTERM SSH support + Enabled all ssh2-streams supported KEX, ciphers, etc. for now. Will communicate with Deuce about this. --- core/config.js | 51 +++++++++++++++++++++++++++++++++++++++ core/servers/login/ssh.js | 3 ++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 3d7c6d70..02b34911 100644 --- a/core/config.js +++ b/core/config.js @@ -242,6 +242,57 @@ function getDefaultConfig() { privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), firstMenu : 'sshConnected', firstMenuNewUser : 'sshConnectedNewUser', + + // + // SSH details that can affect security. Stronger ciphers are better for example, + // but terminals such as SyncTERM require KEX diffie-hellman-group14-sha1, + // cipher 3des-cbc, etc. + // + // See https://github.com/mscdex/ssh2-streams for the full list of supported + // algorithms. + // + algorithms : { + kex : [ + 'ecdh-sha2-nistp256', + 'ecdh-sha2-nistp384', + 'ecdh-sha2-nistp521', + 'diffie-hellman-group-exchange-sha256', + 'diffie-hellman-group14-sha1', + 'diffie-hellman-group-exchange-sha1', + 'diffie-hellman-group1-sha1', + ], + cipher : [ + 'aes128-ctr', + 'aes192-ctr', + 'aes256-ctr', + 'aes128-gcm', + 'aes128-gcm@openssh.com', + 'aes256-gcm', + 'aes256-gcm@openssh.com', + 'aes256-cbc', + 'aes192-cbc', + 'aes128-cbc', + 'blowfish-cbc', + '3des-cbc', + 'arcfour256', + 'arcfour128', + 'cast128-cbc', + 'arcfour', + ], + hmac : [ + 'hmac-sha2-256', + 'hmac-sha2-512', + 'hmac-sha1', + 'hmac-md5', + 'hmac-sha2-256-96', + 'hmac-sha2-512-96', + 'hmac-ripemd160', + 'hmac-sha1-96', + 'hmac-md5-96', + ], + // note that we disable compression by default due to issues with many clients. YMMV. + compress : [ 'none' ] + }, }, webSocket : { ws : { diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 016c215e..f9186cf9 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -251,12 +251,13 @@ exports.getModule = class SSHServerModule extends LoginServerModule { ident : 'enigma-bbs-' + enigVersion + '-srv', // Note that sending 'banner' breaks at least EtherTerm! + debug : (sshDebugLine) => { if(true === config.loginServers.ssh.traceConnections) { Log.trace(`SSH: ${sshDebugLine}`); } }, - algorithms: { compress: ['none'] }, + algorithms : config.loginServers.ssh.algorithms, }; this.server = ssh2.Server(serverConf); From 87ac0b0f129c69d30d72d87b169f7e2558cae669 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 20:14:54 -0700 Subject: [PATCH 269/569] Notes on algorithms --- docs/servers/ssh.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/servers/ssh.md b/docs/servers/ssh.md index 676afb51..51471e28 100644 --- a/docs/servers/ssh.md +++ b/docs/servers/ssh.md @@ -16,10 +16,10 @@ You then need to enable the SSH server in your `config.hjson`: { loginServers: { ssh: { - enabled: true + enabled: true port: 8889 privateKeyPem: /path/to/ssh_private_key.pem - privateKeyPass: YOUR_PK_PASS + privateKeyPass: YOUR_PK_PASS } } } @@ -29,9 +29,11 @@ You then need to enable the SSH server in your `config.hjson`: | Option | Description |---------------------|--------------------------------------------------------------------------------------| -| `privateKeyPem` | Path to private key file -| `privateKeyPass` | Password to private key file -| `firstMenu` | First menu an SSH connected user is presented with -| `firstMenuNewUser` | Menu presented to user when logging in with `users::newUserNames` in your config.hjson (defaults to `new` and `apply`) -| `enabled` | Enable/disable SSH server -| `port` | Configure a custom port for the SSH server +| `privateKeyPem` | Path to private key file. +| `privateKeyPass` | Password to private key file. +| `firstMenu` | First menu an SSH connected user is presented with. +| `firstMenuNewUser` | Menu presented to user when logging in with `users::newUserNames` in your config.hjson (defaults to `new` and `apply`). +| `enabled` | Enable/disable SSH server. +| `port` | Configure a custom port for the SSH server. +| `algorithms` | Configuration block for SSH algoritms. Includes arrays with keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config.js`. +| `traceConnections` | Set to `true` to enable full trace-level information on SSH connections. From 46e14685fa1c21cb8e07877da39ccacf882dc63b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 20:15:13 -0700 Subject: [PATCH 270/569] Dep. upgrades --- package-lock.json | 2281 +++++++++++++++++++++------------------------ package.json | 12 +- 2 files changed, 1073 insertions(+), 1220 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0ba359d..ae264090 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,22 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, "ansi-escapes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", @@ -19,7 +35,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { - "color-convert": "1.9.1" + "color-convert": "^1.9.0" } }, "anymatch": { @@ -27,8 +43,22 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "requires": { - "micromatch": "3.1.10", - "normalize-path": "2.1.1" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, "arr-diff": { @@ -51,7 +81,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", "requires": { - "array-uniq": "1.0.3" + "array-uniq": "^1.0.1" } }, "array-uniq": { @@ -74,6 +104,11 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -84,7 +119,7 @@ "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", "requires": { - "lodash": "4.17.10" + "lodash": "^4.17.10" } }, "async-limiter": { @@ -92,10 +127,25 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "balanced-match": { "version": "1.0.0", @@ -107,13 +157,13 @@ "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "pascalcase": "0.1.1" + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" }, "dependencies": { "define-property": { @@ -121,7 +171,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -129,7 +179,7 @@ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -137,7 +187,7 @@ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -145,13 +195,21 @@ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, "binary-parser": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-1.3.2.tgz", @@ -162,7 +220,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -171,16 +229,16 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.2", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "dependencies": { "extend-shallow": { @@ -188,7 +246,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -198,7 +256,7 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", "requires": { - "node-int64": "0.4.0" + "node-int64": "^0.4.0" } }, "buffer-crc32": { @@ -211,10 +269,10 @@ "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", "requires": { - "dtrace-provider": "0.8.6", - "moment": "2.22.2", - "mv": "2.1.1", - "safe-json-stringify": "1.1.0" + "dtrace-provider": "~0.8", + "moment": "^2.10.6", + "mv": "~2", + "safe-json-stringify": "~1" } }, "cache-base": { @@ -222,15 +280,15 @@ "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.2.1", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.0", - "to-object-path": "0.3.0", - "union-value": "1.0.0", - "unset-value": "1.0.0" + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" } }, "capture-exit": { @@ -238,28 +296,38 @@ "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", "requires": { - "rsvp": "3.6.2" + "rsvp": "^3.3.3" } }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.4.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" }, "dependencies": { "define-property": { @@ -267,7 +335,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -277,7 +345,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", "requires": { - "restore-cursor": "2.0.0" + "restore-cursor": "^2.0.0" } }, "cli-width": { @@ -285,13 +353,23 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" } }, "color-convert": { @@ -299,7 +377,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", "requires": { - "color-name": "1.1.3" + "color-name": "^1.1.1" } }, "color-name": { @@ -307,6 +385,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", @@ -317,11 +403,41 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -335,13 +451,18 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" }, "dependencies": { "is-accessor-descriptor": { @@ -349,7 +470,7 @@ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -357,7 +478,7 @@ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -365,9 +486,9 @@ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } @@ -377,22 +498,54 @@ "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.1", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.4.5" + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, "dtrace-provider": { "version": "0.8.6", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz", "integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=", "optional": true, "requires": { - "nan": "2.10.0" + "nan": "^2.3.3" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" } }, "escape-string-regexp": { @@ -401,11 +554,22 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "exec-sh": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", - "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.2.tgz", + "integrity": "sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "requires": { - "merge": "1.2.0" + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" } }, "exiftool": { @@ -418,13 +582,13 @@ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { "define-property": { @@ -432,7 +596,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -440,18 +604,23 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -459,7 +628,7 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -469,14 +638,14 @@ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { "define-property": { @@ -484,7 +653,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "extend-shallow": { @@ -492,7 +661,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "is-accessor-descriptor": { @@ -500,7 +669,7 @@ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -508,7 +677,7 @@ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -516,19 +685,34 @@ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, "fb-watchman": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", "requires": { - "bser": "2.0.0" + "bser": "^2.0.0" } }, "figures": { @@ -536,7 +720,7 @@ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", "requires": { - "escape-string-regexp": "1.0.5" + "escape-string-regexp": "^1.0.5" } }, "fill-range": { @@ -544,10 +728,10 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "dependencies": { "extend-shallow": { @@ -555,7 +739,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -565,22 +749,45 @@ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "requires": { - "map-cache": "0.2.2" + "map-cache": "^0.2.2" } }, "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", + "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.1" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "^2.2.1" } }, "fs.realpath": { @@ -588,484 +795,86 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fsevents": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.3.tgz", - "integrity": "sha512-X+57O5YkDTiEQGiw8i7wYc2nQgweIekqkepI8Q3y4wVlurgBt2SuwxTeYUYMZIGpLZH3r/TsMjczCMXE5ZOt7Q==", - "optional": true, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { - "nan": "2.10.0", - "node-pre-gyp": "0.9.1" + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, "ansi-regex": { "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.9.1", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.6", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.6", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true } } }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "globby": { @@ -1073,32 +882,51 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "requires": { + "ajv": "^5.3.0", + "har-schema": "^2.0.0" + } }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" } }, "has-values": { @@ -1106,8 +934,8 @@ "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" + "is-number": "^3.0.0", + "kind-of": "^4.0.0" }, "dependencies": { "kind-of": { @@ -1115,7 +943,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -1130,12 +958,30 @@ "resolved": "https://registry.npmjs.org/hjson/-/hjson-3.1.1.tgz", "integrity": "sha512-1oGkOq4sssz7HFZ8Is9HuTR47r8gSC46qAzQxVlAkj0lNKpS+W5Lv2eci+c5+fFqL+Idtj5EvprFreUwH29a8A==" }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "requires": { + "minimatch": "^3.0.4" } }, "inflight": { @@ -1143,8 +989,8 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -1152,24 +998,29 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, "inquirer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.0.0.tgz", "integrity": "sha512-tISQWRwtcAgrz+SHPhTH7d3e73k31gsOy6i1csonLc0u1dVK/wYvuOnFeiWqC5OXFIYbmrIFInef31wbT8MEJg==", "requires": { - "ansi-escapes": "3.1.0", - "chalk": "2.4.1", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "3.0.0", - "figures": "2.0.0", - "lodash": "4.17.10", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rxjs": "6.2.0", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" + "run-async": "^2.2.0", + "rxjs": "^6.1.0", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" }, "dependencies": { "chardet": { @@ -1182,9 +1033,9 @@ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.0.tgz", "integrity": "sha512-mpkfj0FEdxrIhOC04zk85X7StNtr0yXnG7zCb+8ikO8OJi2jsHh5YGoknNTyXgsbHOf1WOOcVU3kPFWT2WgCkQ==", "requires": { - "chardet": "0.5.0", - "iconv-lite": "0.4.23", - "tmp": "0.0.33" + "chardet": "^0.5.0", + "iconv-lite": "^0.4.22", + "tmp": "^0.0.33" } }, "rxjs": { @@ -1192,7 +1043,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.0.tgz", "integrity": "sha512-qBzf5uu6eOKiCZuAE0SgZ0/Qp+l54oeVxFfC2t+mJ2SFI6IB8gmMdJHs5DUMu5kqifqcCtsKS2XHjhZu6RKvAw==", "requires": { - "tslib": "1.9.2" + "tslib": "^1.9.0" } } } @@ -1202,7 +1053,7 @@ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -1210,7 +1061,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -1225,7 +1076,7 @@ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -1233,7 +1084,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -1243,9 +1094,9 @@ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "dependencies": { "kind-of": { @@ -1270,7 +1121,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -1278,26 +1129,11 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "requires": { - "is-number": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - } - } - }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -1308,7 +1144,7 @@ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", "requires": { - "is-path-inside": "1.0.1" + "is-path-inside": "^1.0.0" } }, "is-path-inside": { @@ -1316,7 +1152,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", "requires": { - "path-is-inside": "1.0.2" + "path-is-inside": "^1.0.1" } }, "is-plain-object": { @@ -1324,7 +1160,7 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" } }, "is-promise": { @@ -1332,6 +1168,16 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -1342,17 +1188,58 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" } }, "kind-of": { @@ -1375,7 +1262,7 @@ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", "requires": { - "tmpl": "1.0.4" + "tmpl": "1.0.x" } }, "map-cache": { @@ -1388,45 +1275,45 @@ "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "requires": { - "object-visit": "1.0.1" + "object-visit": "^1.0.0" } }, "merge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", - "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==" }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.9", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" } }, "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" }, "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", "requires": { - "mime-db": "1.33.0" + "mime-db": "~1.37.0" } }, "mimic-fn": { @@ -1439,7 +1326,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -1447,13 +1334,30 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", + "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", + "requires": { + "minipass": "^2.2.1" + } + }, "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -1461,7 +1365,7 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -1470,7 +1374,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, "requires": { "minimist": "0.0.8" }, @@ -1478,8 +1381,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, @@ -1504,9 +1406,9 @@ "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", "optional": true, "requires": { - "mkdirp": "0.5.1", - "ncp": "2.0.0", - "rimraf": "2.4.5" + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" } }, "nan": { @@ -1515,22 +1417,21 @@ "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" }, "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-odd": "2.0.0", - "is-windows": "1.0.2", - "kind-of": "6.0.2", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" } }, "ncp": { @@ -1539,17 +1440,59 @@ "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", "optional": true }, + "needle": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" }, + "node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + } + } + }, "node-pty": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.7.4.tgz", "integrity": "sha512-WxMY1BsGcHJ2Z2qWpYL7QbfOSnkkCzV0H/9+dJ7uQEIJyz0A4fVBLymswBCTc7RoweY5ingib2gNvf87KvJxuA==", "requires": { - "nan": "2.10.0" + "nan": "^2.6.2" } }, "nodemailer": { @@ -1557,14 +1500,66 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.5.tgz", "integrity": "sha512-+bt+BgmnOXDz1uIaWXfXuTESth8UHkhtu7+X8+X2W+CHAn0AuuCyCk854qnathYQLWEC2jkpx7/pkVHcfmLKDw==" }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "requires": { - "remove-trailing-separator": "1.1.0" + "remove-trailing-separator": "^1.0.1" } }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + }, + "npm-packlist": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", + "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1575,9 +1570,9 @@ "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" }, "dependencies": { "define-property": { @@ -1585,7 +1580,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "kind-of": { @@ -1593,7 +1588,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -1603,7 +1598,7 @@ "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.0" } }, "object.pick": { @@ -1611,7 +1606,7 @@ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" } }, "once": { @@ -1619,7 +1614,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "onetime": { @@ -1627,14 +1622,33 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", "requires": { - "mimic-fn": "1.2.0" + "mimic-fn": "^1.0.0" } }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -1650,6 +1664,16 @@ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -1665,7 +1689,7 @@ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "posix-character-classes": { @@ -1673,13 +1697,67 @@ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" } }, "remove-trailing-separator": { @@ -1688,15 +1766,49 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -1707,8 +1819,8 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", "requires": { - "onetime": "2.0.1", - "signal-exit": "3.0.2" + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" } }, "ret": { @@ -1721,7 +1833,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", "requires": { - "glob": "6.0.4" + "glob": "^6.0.1" }, "dependencies": { "glob": { @@ -1729,11 +1841,11 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } } } @@ -1753,9 +1865,14 @@ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", "requires": { - "is-promise": "2.1.0" + "is-promise": "^2.1.0" } }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "safe-json-stringify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz", @@ -1764,10 +1881,10 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "requires": { - "ret": "0.1.15" + "ret": "~0.1.10" } }, "safer-buffer": { @@ -1776,19 +1893,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sane": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/sane/-/sane-2.5.2.tgz", - "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.0.2.tgz", + "integrity": "sha512-/3STCUfNSgMVpoREJc1i6ajKFlYZ5OflzZTOhlqPLa+01Ey+QR9iGZK7K5/qIRsQbEDCvqEJH/PL7yZywmnWsA==", "requires": { - "anymatch": "2.0.0", - "capture-exit": "1.2.0", - "exec-sh": "0.2.1", - "fb-watchman": "2.0.0", - "fsevents": "1.2.3", - "micromatch": "3.1.10", - "minimist": "1.2.0", - "walker": "1.0.7", - "watch": "0.18.0" + "anymatch": "^2.0.0", + "capture-exit": "^1.2.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5", + "watch": "~0.18.0" } }, "sanitize-filename": { @@ -1796,23 +1913,33 @@ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", "requires": { - "truncate-utf8-bytes": "1.0.2" + "truncate-utf8-bytes": "^1.0.0" } }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" }, "dependencies": { "extend-shallow": { @@ -1820,11 +1947,24 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -1835,14 +1975,14 @@ "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.1", - "use": "3.1.0" + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" }, "dependencies": { "define-property": { @@ -1850,7 +1990,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -1858,7 +1998,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -1868,9 +2008,9 @@ "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" }, "dependencies": { "define-property": { @@ -1878,7 +2018,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -1886,7 +2026,7 @@ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -1894,7 +2034,7 @@ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -1902,9 +2042,9 @@ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } @@ -1914,7 +2054,7 @@ "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.2.0" }, "dependencies": { "kind-of": { @@ -1922,7 +2062,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -1933,15 +2073,15 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", "requires": { - "atob": "2.1.1", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" } }, "source-map-url": { @@ -1954,417 +2094,17 @@ "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "requires": { - "extend-shallow": "3.0.2" + "extend-shallow": "^3.0.0" } }, "sqlite3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.0.tgz", - "integrity": "sha512-6OlcAQNGaRSBLK1CuaRbKwlMFBb9DEhzmZyQP+fltNRF6XcIMpVIfXCBEcXPe1d4v9LnhkQUYkknDbA5JReqJg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.3.tgz", + "integrity": "sha512-nuWqc26oiJZXyY5MEz+rQbiki1BTibnXsy8Kqo7QD/ut6eksOWi6uWwFMbdnFNME7CZyplWdDXj2fbdQVaEfuA==", "requires": { - "nan": "2.9.2", - "node-pre-gyp": "0.9.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.5" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "requires": { - "minipass": "2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true - }, - "iconv-lite": { - "version": "0.4.19", - "bundled": true - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "ini": { - "version": "1.3.5", - "bundled": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "minipass": { - "version": "2.2.1", - "bundled": true, - "requires": { - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "requires": { - "minipass": "2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true - }, - "nan": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz", - "integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==" - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.19", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.9.0", - "bundled": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.6", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true - }, - "rc": { - "version": "1.2.6", - "bundled": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true - } - } - }, - "readable-stream": { - "version": "2.3.5", - "bundled": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true - }, - "sax": { - "version": "1.2.4", - "bundled": true - }, - "semver": { - "version": "5.5.0", - "bundled": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.3", - "bundled": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true - }, - "tar": { - "version": "4.4.0", - "bundled": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.1", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true - } + "nan": "~2.10.0", + "node-pre-gyp": "^0.10.3", + "request": "^2.87.0" } }, "sqlite3-trans": { @@ -2372,7 +2112,7 @@ "resolved": "https://registry.npmjs.org/sqlite3-trans/-/sqlite3-trans-1.2.0.tgz", "integrity": "sha1-E8/K2wk+1I5m+U7IlWq3RU3clgg=", "requires": { - "lodash": "4.17.10" + "lodash": "^4.17.4" } }, "ssh2": { @@ -2380,7 +2120,7 @@ "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.6.1.tgz", "integrity": "sha512-fNvocq+xetsaAZtBG/9Vhh0GDjw1jQeW7Uq/DPh4fVrJd0XxSfXAqBjOGVk4o2jyWHvyC6HiaPFpfHlR12coDw==", "requires": { - "ssh2-streams": "0.2.1" + "ssh2-streams": "~0.2.0" }, "dependencies": { "ssh2-streams": { @@ -2388,20 +2128,36 @@ "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.2.1.tgz", "integrity": "sha512-3zCOsmunh1JWgPshfhKmBCL3lUtHPoh+a/cyQ49Ft0Q0aF7xgN06b76L+oKtFi0fgO57FLjFztb1GlJcEZ4a3Q==", "requires": { - "asn1": "0.2.3", - "semver": "5.5.0", - "streamsearch": "0.1.2" + "asn1": "~0.2.0", + "semver": "^5.1.0", + "streamsearch": "~0.1.2" } } } }, + "sshpk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" + "define-property": "^0.2.5", + "object-copy": "^0.1.0" }, "dependencies": { "define-property": { @@ -2409,7 +2165,7 @@ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -2424,8 +2180,16 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" } }, "strip-ansi": { @@ -2433,15 +2197,39 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { - "ansi-regex": "3.0.0" + "ansi-regex": "^3.0.0" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", + "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.3", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" } }, "temptmp": { @@ -2449,7 +2237,7 @@ "resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.0.0.tgz", "integrity": "sha1-M7Djbh8nMXyKKBIO6Wufj+tw2UM=", "requires": { - "del": "2.2.2" + "del": "^2.2.2" } }, "through": { @@ -2462,7 +2250,7 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "requires": { - "os-tmpdir": "1.0.2" + "os-tmpdir": "~1.0.2" } }, "tmpl": { @@ -2475,7 +2263,7 @@ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -2483,7 +2271,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -2493,10 +2281,10 @@ "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "requires": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" } }, "to-regex-range": { @@ -2504,8 +2292,17 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" } }, "truncate-utf8-bytes": { @@ -2513,7 +2310,7 @@ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", "requires": { - "utf8-byte-length": "1.0.4" + "utf8-byte-length": "^1.0.1" } }, "tslib": { @@ -2521,15 +2318,28 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" }, "dependencies": { "extend-shallow": { @@ -2537,7 +2347,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "set-value": { @@ -2545,26 +2355,26 @@ "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" } } } }, "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" + "has-value": "^0.3.1", + "isobject": "^3.0.0" }, "dependencies": { "has-value": { @@ -2572,9 +2382,9 @@ "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" }, "dependencies": { "isobject": { @@ -2600,18 +2410,20 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" }, "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "requires": { - "kind-of": "6.0.2" - } + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "uuid": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", @@ -2622,12 +2434,22 @@ "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.0.0.tgz", "integrity": "sha1-9GV3F2JLDkuIrzb5jYlYmlu+5Wk=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "walker": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", "requires": { - "makeerror": "1.0.11" + "makeerror": "1.0.x" } }, "watch": { @@ -2635,8 +2457,34 @@ "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", "requires": { - "exec-sh": "0.2.1", - "minimist": "1.2.0" + "exec-sh": "^0.2.0", + "minimist": "^1.2.0" + }, + "dependencies": { + "exec-sh": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", + "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", + "requires": { + "merge": "^1.2.0" + } + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -2645,11 +2493,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.0.tgz", - "integrity": "sha512-c18dMeW+PEQdDFzkhDsnBAlS4Z8KGStBQQUcQ5mf7Nf689jyGk0594L+i9RaQuf4gog6SvWLJorz2NfSaqxZ7w==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz", + "integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==", "requires": { - "async-limiter": "1.0.0" + "async-limiter": "~1.0.0" } }, "xxhash": { @@ -2657,15 +2505,20 @@ "resolved": "https://registry.npmjs.org/xxhash/-/xxhash-0.2.4.tgz", "integrity": "sha1-i4pIFiz8zCG5IPpQAmEYfUAhbDk=", "requires": { - "nan": "2.10.0" + "nan": "^2.4.0" } }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + }, "yazl": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.4.3.tgz", "integrity": "sha1-7CblzIfVYBud+EMtvdPNLlFzoHE=", "requires": { - "buffer-crc32": "0.2.13" + "buffer-crc32": "~0.2.3" } } } diff --git a/package.json b/package.json index 8b6cf110..0d4e4a9f 100644 --- a/package.json +++ b/package.json @@ -27,30 +27,30 @@ "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "fs-extra": "^6.0.1", + "fs-extra": "^7.0.0", "glob": "^7.1.2", - "graceful-fs": "^4.1.11", + "graceful-fs": "^4.1.15", "hashids": "^1.1.1", "hjson": "^3.1.1", "iconv-lite": "^0.4.23", "inquirer": "^6.0.0", "later": "1.2.0", "lodash": "^4.17.10", - "mime-types": "^2.1.18", + "mime-types": "^2.1.21", "minimist": "1.2.x", "moment": "^2.22.2", "node-pty": "^0.7.4", "nodemailer": "^4.6.5", "rlogin": "^1.0.0", - "sane": "^2.5.2", + "sane": "^4.0.2", "sanitize-filename": "^1.6.1", - "sqlite3": "^4.0.0", + "sqlite3": "^4.0.3", "sqlite3-trans": "^1.2.0", "ssh2": "^0.6.1", "temptmp": "^1.0.0", "uuid": "^3.2.1", "uuid-parse": "^1.0.0", - "ws": "^5.2.0", + "ws": "^6.1.0", "xxhash": "^0.2.4", "yazl": "^2.4.2" }, From 75c952c97694bd24b8f6f9f7ced076fe3229e940 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 21:00:54 -0700 Subject: [PATCH 271/569] Fix sexyz Linux x86_64 binary links --- core/config.js | 1 + docs/configuration/file-transfer-protocols.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 02b34911..fe09b956 100644 --- a/core/config.js +++ b/core/config.js @@ -671,6 +671,7 @@ function getDefaultConfig() { sort : 1, external : { // :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems + // Linux x86_64 binary: https://l33t.codes/outgoing/sexyz sendCmd : 'sexyz', sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], recvCmd : 'sexyz', diff --git a/docs/configuration/file-transfer-protocols.md b/docs/configuration/file-transfer-protocols.md index da5697c7..fd21a938 100644 --- a/docs/configuration/file-transfer-protocols.md +++ b/docs/configuration/file-transfer-protocols.md @@ -11,7 +11,7 @@ File transfer protocols are managed via the `fileTransferProtocols` configuratio The following file transfer protocols are pre-configured in ENiGMA½ as of this writing. System operators may override or extend this list. PRs are welcome for pre-configured additions! #### SEXYZ -[SEXYZ from Synchronet](http://wiki.synchro.net/util:sexyz) offers a nice X, Y, and ZModem implementation including ZModem-8k & works under *nix and Windows based systems. As of this writing, ENiGMA½ is pre-configured to support ZModem-8k, XModem, and YModem using SEXYZ. An x86_64 Linux binary, and hopefully more in the future, [can be downloaded here](http://132.0.0.249/bbs-linux-binaries/). +[SEXYZ from Synchronet](http://wiki.synchro.net/util:sexyz) offers a nice X, Y, and ZModem implementation including ZModem-8k & works under *nix and Windows based systems. As of this writing, ENiGMA½ is pre-configured to support ZModem-8k, XModem, and YModem using SEXYZ. An x86_64 Linux binary, and hopefully more in the future, [can be downloaded here](https://l33t.codes/bbs-linux-binaries/). #### sz/rz ZModem-8k is configured using the standard Linux [sz(1)](https://linux.die.net/man/1/sz) and [rz(1)](https://linux.die.net/man/1/rz) binaries. Note that these binaries also support XModem and YModem, and as such adding the configurations to your system should be fairly straight forward. From 8942eff203c846f2e17c055176f235382f646e8b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 4 Nov 2018 21:09:27 -0700 Subject: [PATCH 272/569] Fix deprecated write() without callback for Node.js 10.x+ --- core/file_transfer.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/file_transfer.js b/core/file_transfer.js index dbd19a8f..340ca6b9 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -314,9 +314,13 @@ exports.getModule = class TransferFileModule extends MenuModule { return callback(err); // failed to create it } - fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL)); - fs.close(tempFileInfo.fd, err => { - return callback(err, tempFileInfo.path); + fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => { + if(err) { + return callback(err); + } + fs.close(tempFileInfo.fd, err => { + return callback(err, tempFileInfo.path); + }); }); }); }, From a98940e967fdbdd032553641fd246533b9fae9eb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 21:08:56 -0700 Subject: [PATCH 273/569] Use standard itemFormat for BBS list. Add docs --- art/themes/luciano_blocktronics/theme.hjson | 6 ++---- core/bbs_list.js | 8 ++------ docs/_includes/nav.md | 1 + 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index f3af6df6..60bede01 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -315,16 +315,14 @@ } bbsList: { - config: { - listFormat: "|00|07{bbsName}" - focusListFormat: "|00|19|15{bbsName!styleFirstLower}" - } 0: { mci: { VM1: { height: 11 width: 22 focusTextStyle: first upper + itemFormat: "|00|07{bbsName}" + focusItemFormat: "|00|19|15{bbsName!styleFirstLower}" } TL2: { width: 28 } TL3: { width: 28 } diff --git a/core/bbs_list.js b/core/bbs_list.js index 2f9b6084..82943a80 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -218,12 +218,7 @@ exports.getModule = class BBSListModule extends MenuModule { } setEntries(entriesView) { - const config = this.menuConfig.config; - const listFormat = config.listFormat || '{bbsName}'; - const focusListFormat = config.focusListFormat || '{bbsName}'; - - entriesView.setItems(this.entries.map( e => stringFormat(listFormat, e) ) ); - entriesView.setFocusItems(this.entries.map( e => stringFormat(focusListFormat, e) ) ); + return entriesView.setItems(this.entries); } displayBBSList(clearScreen, cb) { @@ -277,6 +272,7 @@ exports.getModule = class BBSListModule extends MenuModule { (err, row) => { if (!err) { self.entries.push({ + text : row.bbs_name, // standard field id : row.id, bbsName : row.bbs_name, sysOp : row.sysop, diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 33702016..7d3d4c29 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -69,6 +69,7 @@ - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) - [Message Conference List]({{ site.baseurl }}{% link modding/msg-conf-list.md %}) - [Message Area List]({{ site.baseurl }}{% link modding/msg-area-list.md %}) + - [BBS List]({{ site.baseurl }}{% link modding/bbs-list.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) From 5af2fdc6c55402444029199fc3c25455872a6da1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 21:35:12 -0700 Subject: [PATCH 274/569] More itemFormat & doc work --- art/themes/luciano_blocktronics/theme.hjson | 11 +++++----- core/rumorz.js | 12 +++++------ docs/modding/bbs-list.md | 24 +++++++++++++++++++++ docs/modding/rumorz.md | 12 +++++++++++ 4 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 docs/modding/bbs-list.md create mode 100644 docs/modding/rumorz.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 60bede01..1abcd879 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -290,13 +290,14 @@ } mainMenuRumorz: { - config: { - listFormat: "|00|11 {rumor}" - focusListFormat: "|00|15> |14{rumor}" - } 0: { mci: { - VM1: { height: 14, width: 70 } + VM1: { + height: 14, + width: 70 + itemFormat: "|00|11 {rumor}" + focusItemFormat: "|00|15> |14{rumor}" + } TM2: { focusTextStyle: upper items: [ "yes", "no" ] diff --git a/core/rumorz.js b/core/rumorz.js index 153a74ee..31aea104 100644 --- a/core/rumorz.js +++ b/core/rumorz.js @@ -8,7 +8,6 @@ const theme = require('./theme.js'); const resetScreen = require('./ansi_term.js').resetScreen; const StatLog = require('./stat_log.js'); const renderStringLength = require('./string_util.js').renderStringLength; -const stringFormat = require('./string_format.js'); // deps const async = require('async'); @@ -155,12 +154,13 @@ exports.getModule = class RumorzModule extends MenuModule { }); }, function populateEntries(entriesView, entries, callback) { - const config = self.config; - const listFormat = config.listFormat || '{rumor}'; - const focusListFormat = config.focusListFormat || listFormat; + entriesView.setItems(entries.map(e => { + return { + text : e.log_value, // standard + rumor : e.log_value, + } + })); - entriesView.setItems(entries.map( e => stringFormat(listFormat, { rumor : e.log_value } ) ) ); - entriesView.setFocusItems(entries.map(e => stringFormat(focusListFormat, { rumor : e.log_value } ) ) ); entriesView.redraw(); return callback(null); diff --git a/docs/modding/bbs-list.md b/docs/modding/bbs-list.md new file mode 100644 index 00000000..4b61e616 --- /dev/null +++ b/docs/modding/bbs-list.md @@ -0,0 +1,24 @@ +--- +layout: page +title: BBS List +--- +## The BBS List Module +The built in `bbs_list` module provides the ability for users to manage entries to other Bulletin Board Systems. + +## Configuration +### Config Block +Available `config` block entries: +* `youSubmittedFormat`: Provides a format for entries that were submitted (and therefor ediable) by the current user. Defaults to `'{submitter} (You!)'`. Utilizes the same `itemFormat` object as entries described below. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) (the BBS list): +* `id`: Row ID +* `bbsName`: System name. Note that `{text}` also contains this value. +* `sysOp`: System Operator +* `telnet`: Telnet address +* `www`: Web address +* `location`: System location +* `software`: System's software +* `submitter`: Username of entry submitter +* `submitterUserId`: User ID of submitter +* `notes`: Any additional notes about the system diff --git a/docs/modding/rumorz.md b/docs/modding/rumorz.md new file mode 100644 index 00000000..f930edcc --- /dev/null +++ b/docs/modding/rumorz.md @@ -0,0 +1,12 @@ +--- +layout: page +title: Rumorz +--- +## The Rumorz Module +The built in `rumorz` module provides a classic interface for users to add and view rumorz! + +## Configuration + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) (the rumor list): +* `rumor`: The rumor text. Also available in the standard `{text}` field. From 0e986238c2889aed6847c89522a49a0b20a3dd6b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 21:47:20 -0700 Subject: [PATCH 275/569] Add rumorz to nav --- docs/_includes/nav.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 7d3d4c29..dbca7889 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -70,6 +70,7 @@ - [Message Conference List]({{ site.baseurl }}{% link modding/msg-conf-list.md %}) - [Message Area List]({{ site.baseurl }}{% link modding/msg-area-list.md %}) - [BBS List]({{ site.baseurl }}{% link modding/bbs-list.md %}) + - [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) From 5286c52a263d34e0d2923c173ccc64f5810ecbe9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 21:47:53 -0700 Subject: [PATCH 276/569] Fix file base area selection in Luciano theme --- art/themes/luciano_blocktronics/theme.hjson | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 1abcd879..f8e1faeb 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -713,17 +713,14 @@ } fileBaseBrowseByAreaSelect: { - config: { - protListFormat: "|00|03{name}" - protListFocusFormat: "|00|19|15{name}" - } - 0: { mci: { VM1: { height: 15 width: 30 focusTextStyle: first lower + itemFormat: "|00|03{name}" + focusItemFormat: "|00|19|15{name}" } } } From 047d8fae899e039dc2c5524dd8f30972e14a740b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 21:54:04 -0700 Subject: [PATCH 277/569] File Transfer protocol: use itemFormat + docs --- art/themes/luciano_blocktronics/theme.hjson | 7 ++----- core/file_transfer_protocol_select.js | 9 ++------- docs/_includes/nav.md | 1 + docs/modding/file-transfer-protocol-select.md | 13 +++++++++++++ 4 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 docs/modding/file-transfer-protocol-select.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index f8e1faeb..06e6a011 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -912,17 +912,14 @@ } fileTransferProtocolSelection: { - config: { - protListFormat: "|00|03{name}" - protListFocusFormat: "|00|19|15{name}" - } - 0: { mci: { VM1: { height: 15 width: 30 focusTextStyle: first lower + itemFormat: "|00|03{name}" + focusItemFormat: "|00|19|15{name}" } } } diff --git a/core/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js index d8500dc5..13e30a74 100644 --- a/core/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -4,7 +4,6 @@ // enigma-bbs const MenuModule = require('./menu_module.js').MenuModule; const Config = require('./config.js').get; -const stringFormat = require('./string_format.js'); const ViewController = require('./view_controller.js').ViewController; // deps @@ -110,12 +109,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { function populateList(callback) { const protListView = vc.getView(MciViewIds.protList); - const protListFormat = self.config.protListFormat || '{name}'; - const protListFocusFormat = self.config.protListFocusFormat || protListFormat; - - protListView.setItems(self.protocols.map(p => stringFormat(protListFormat, p) ) ); - protListView.setFocusItems(self.protocols.map(p => stringFormat(protListFocusFormat, p) ) ); - + protListView.setItems(self.protocols); protListView.redraw(); return callback(null); @@ -131,6 +125,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { loadAvailProtocols() { this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { return { + text : protInfo.name, // standard protocol : protocol, name : protInfo.name, hasBatch : _.has(protInfo, 'external.recvArgs'), diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index dbca7889..15641234 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -71,6 +71,7 @@ - [Message Area List]({{ site.baseurl }}{% link modding/msg-area-list.md %}) - [BBS List]({{ site.baseurl }}{% link modding/bbs-list.md %}) - [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %}) + - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/file-transfer-protocol-select.md b/docs/modding/file-transfer-protocol-select.md new file mode 100644 index 00000000..72b8d124 --- /dev/null +++ b/docs/modding/file-transfer-protocol-select.md @@ -0,0 +1,13 @@ +--- +layout: page +title: File Transfer Protocol Select +--- +## The Rumorz Module +The built in `file_transfer_protocol_select` module provides a way to select a legacy file transfer protocol (X/Y/Z-Modem, etc.) for upload/downloads. + +## Configuration + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) (the protocol list): +* `name`: The name of the protocol. Each entry is +op defined in `config.hjson` with defaults found in `config.js`. Note that the standard `{text}` field also contains this value. + From 5859ba0b68cb22caf3b27fc8eac2c7d18fb3b2ce Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 22:33:47 -0700 Subject: [PATCH 278/569] core/onelinerz.js tsFormat -> dateTimeFormat (WIP on standardization) --- art/themes/luciano_blocktronics/theme.hjson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 06e6a011..aa720934 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -88,7 +88,7 @@ fullLoginSequenceOnelinerz: { config: { - tsFormat: ddd h:mma + dateTimeFormat: ddd h:mma } 0: { mci: { @@ -195,7 +195,7 @@ mainMenuOnelinerz: { config: { - tsFormat: ddd h:mma + dateTimeFormat: ddd h:mma } 0: { mci: { From a14c0f42afef5fe14126bc9b3a2bde8ede4d65cd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 22:39:49 -0700 Subject: [PATCH 279/569] Onelinerz standardization work and docs --- core/onelinerz.js | 6 +++++- docs/_includes/nav.md | 1 + docs/modding/onelinerz.md | 18 ++++++++++++++++++ docs/modding/user-list.md | 3 --- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 docs/modding/onelinerz.md diff --git a/core/onelinerz.js b/core/onelinerz.js index 19b9ce44..d840f731 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -153,10 +153,14 @@ exports.getModule = class OnelinerzModule extends MenuModule { ); }, function populateEntries(entriesView, entries, callback) { - const tsFormat = self.menuConfig.config.timestampFormat || self.client.currentTheme.helpers.getDateFormat('short'); + const tsFormat = + self.menuConfig.config.dateTimeFormat || + self.menuConfig.config.timestampFormat || // deprecated + self.client.currentTheme.helpers.getDateFormat('short'); entriesView.setItems(entries.map( e => { return { + text : e.oneliner, // standard userId : e.user_id, userName : e.user_name, oneliner : e.oneliner, diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 15641234..c8cb67c9 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -72,6 +72,7 @@ - [BBS List]({{ site.baseurl }}{% link modding/bbs-list.md %}) - [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %}) - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) + - [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/onelinerz.md b/docs/modding/onelinerz.md new file mode 100644 index 00000000..92515617 --- /dev/null +++ b/docs/modding/onelinerz.md @@ -0,0 +1,18 @@ +--- +layout: page +title: Onelinerz +--- +## The Onelinerz Module +The built in `onelinerz` module provides a retro onelinerz system. + +## Configuration +### Config Block +Available `config` block entries: +* `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` date format. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): +* `userId`: User ID of the onliner entry. +* `userName`: Login username of the onliner entry. +* `oneliner`: The oneliner text. Note that the standard `{text}` field also contains this value. +* `ts`: Timestamp of the entry formatted with `dateTimeFormat` format described above. diff --git a/docs/modding/user-list.md b/docs/modding/user-list.md index 9aae4750..37eb2e97 100644 --- a/docs/modding/user-list.md +++ b/docs/modding/user-list.md @@ -19,6 +19,3 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `lastLoginTs`: Last login timestamp formatted with `dateTimeFormat` style. * `location`: User's location. * `affiliation` or `affils`: Users affiliations. - - - From 818214657428ca41dc54ec7b6a4d6d98d2b55240 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 5 Nov 2018 22:49:27 -0700 Subject: [PATCH 280/569] Notes on Node.js 10.x LTS --- .github/ISSUE_TEMPLATE.md | 2 +- UPGRADE.md | 2 +- WHATSNEW.md | 2 +- docs/installation/manual.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d53a6435..a8e6a08e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ For :bug: bug reports, please fill out the information below plus any additional **Short problem description** **Environment** -- [ ] I am using Node.js v8.x LTS or higher +- [ ] I am using Node.js v10.x LTS or higher - [ ] `npm install` or `yarn` reports success - Actual Node.js version (`node --version`): - Operating system (`uname -a` on *nix systems): diff --git a/UPGRADE.md b/UPGRADE.md index 5ec42295..af9f124a 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -38,7 +38,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). # 0.0.8-alpha to 0.0.9-alpha -* Development is now against Node.js 8.x LTS. Follow your standard upgrade path to update to Node 8.x before using 0.0.9-alpha. +* Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha! * The property `justify` found on various views previously had `left` and `right` values swapped (oops!); you will need to adjust any custom `theme.hjson` that use one or the other and swap them as well. * Possible breaking changes in FSE: The MCI code `%TL13` for error indicator is now `%TL4`. This is part of a cleanup and standardization on "custom ranges". You may need to update your `theme.hjson` and related artwork. * Removed view width auto-size: Some views still can auto-size their height, but in general you should be explicit in your themes diff --git a/WHATSNEW.md b/WHATSNEW.md index b157a927..de8b6383 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -2,7 +2,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. ## 0.0.9-alpha -* Development is now against Node.js 8.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! +* Development is now against Node.js 10.x LTS. While other Node.js series may continue to work, you're own your own and YMMV! * Fixed `justify` properties: `left` and `right` values were formerly swapped (oops!) * Menu items can now be arrays of *objects* not just arrays of strings. * The properties `itemFormat` and `focusItemFormat` allow you to supply the string format for items. For example if a menu object is `{ "userName" : "Bob", "age" : 35 }`, a `itemFormat` might be `|04{userName} |08- |14{age}`. diff --git a/docs/installation/manual.md b/docs/installation/manual.md index f03df762..01c5ca47 100644 --- a/docs/installation/manual.md +++ b/docs/installation/manual.md @@ -6,7 +6,7 @@ For Linux environments it's recommended you run the [install script](install-scr do things manually, read on... ## Prerequisites -* [Node.js](https://nodejs.org/) version **v6.x or higher** +* [Node.js](https://nodejs.org/) version **v10.x LTS or higher** (Note that 8.x LTS *probably* works but is unsupported). * :information_source: It is **highly** recommended to use [nvm](https://github.com/creationix/nvm) to manage your Node.js installation if you're on a Linux/Unix environment. From 7d74556868b22227559dec94ced20cbf95180456 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 7 Nov 2018 18:33:07 -0700 Subject: [PATCH 281/569] Changes to config: defaults -> theme, preLoginTheme -> theme.preLogin, etc. --- UPGRADE.md | 1 + core/config.js | 21 +++++---------------- core/login_server_module.js | 5 +++-- core/nua.js | 8 ++++++-- core/theme.js | 18 +++++++++--------- docs/configuration/config-hjson.md | 6 ++---- 6 files changed, 26 insertions(+), 33 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index af9f124a..8cb98e13 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -60,6 +60,7 @@ webSocket: { * The module export `registerEvents` has been deprecated. If you have a module that depends on this, use the new more generic `moduleInitialize` export instead. * The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup. * If you have art configured for message conference or area selection via the `art` configuration value, you will need to include a `show_art` menu reference. Defaulted to `changeMessageConfPreArt` for conferences and `changeMessageAreaPreArt` for areas & included in the example `menu.hjson`. +* Config `defaults` section was theme related and as such, has been renamed to `theme`. `defaults.theme` is now `theme.default`, and `preLoginTheme` is now `theme.preLogin`. See `config.js` if this isn't clear as mud. # 0.0.7-alpha to 0.0.8-alpha diff --git a/core/config.js b/core/config.js index fe09b956..74148c69 100644 --- a/core/config.js +++ b/core/config.js @@ -141,9 +141,6 @@ function getDefaultConfig() { promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config) }, - // :TODO: see notes below about 'theme' section - move this! - preLoginTheme : 'luciano_blocktronics', - users : { usernameMin : 2, usernameMax : 16, // Note that FidoNet wants 36 max @@ -172,18 +169,10 @@ function getDefaultConfig() { ], }, - // :TODO: better name for "defaults"... which is redundant here! - /* - Concept - "theme" : { - "default" : "defaultThemeName", // or "*" - "preLogin" : "*", - "passwordChar" : "*", - ... - } - */ - defaults : { - theme : 'luciano_blocktronics', + theme : { + default : 'luciano_blocktronics', + preLogin : 'luciano_blocktronics', + passwordChar : '*', // TODO: move to user ? dateFormat : { short : 'MM/DD/YYYY', @@ -202,7 +191,7 @@ function getDefaultConfig() { cls : true, // Clear screen before each menu by default? }, - paths : { + paths : { config : paths.join(__dirname, './../config/'), mods : paths.join(__dirname, './../mods/'), loginServers : paths.join(__dirname, './servers/login/'), diff --git a/core/login_server_module.js b/core/login_server_module.js index 02cd4e41..d1a3552f 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -23,10 +23,11 @@ module.exports = class LoginServerModule extends ServerModule { // // Choose initial theme before we have user context // - if('*' === conf.config.preLoginTheme) { + const preLoginTheme = _.get(conf.config, 'theme.preLogin'); + if('*' === preLoginTheme) { client.user.properties.theme_id = theme.getRandomTheme() || ''; } else { - client.user.properties.theme_id = conf.config.preLoginTheme; + client.user.properties.theme_id = preLoginTheme; } theme.setClientTheme(client, client.user.properties.theme_id); diff --git a/core/nua.js b/core/nua.js index 18b9a719..2d838fcb 100644 --- a/core/nua.js +++ b/core/nua.js @@ -9,6 +9,9 @@ const login = require('./system_menu_method.js').login; const Config = require('./config.js').get; const messageArea = require('./message_area.js'); +// deps +const _ = require('lodash'); + exports.moduleInfo = { name : 'NUA', desc : 'New User Application', @@ -96,10 +99,11 @@ exports.getModule = class NewUserAppModule extends MenuModule { // :TODO: should probably have a place to create defaults/etc. }; - if('*' === config.defaults.theme) { + const defaultTheme = _.get(config, 'theme.default'); + if('*' === defaultTheme) { newUser.properties.theme_id = theme.getRandomTheme(); } else { - newUser.properties.theme_id = config.defaults.theme; + newUser.properties.theme_id = defaultTheme; } // :TODO: User.create() should validate email uniqueness! diff --git a/core/theme.js b/core/theme.js index bdba70bc..2f88027e 100644 --- a/core/theme.js +++ b/core/theme.js @@ -39,7 +39,7 @@ function refreshThemeHelpers(theme) { let pwChar = _.get( theme, 'customization.defaults.general.passwordChar', - Config().defaults.passwordChar + Config().theme.passwordChar ); if(_.isString(pwChar)) { @@ -51,15 +51,15 @@ function refreshThemeHelpers(theme) { return pwChar; }, getDateFormat : function(style = 'short') { - const format = Config().defaults.dateFormat[style] || 'MM/DD/YYYY'; + const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY'; return _.get(theme, `customization.defaults.dateFormat.${style}`, format); }, getTimeFormat : function(style = 'short') { - const format = Config().defaults.timeFormat[style] || 'h:mm a'; + const format = Config().theme.timeFormat[style] || 'h:mm a'; return _.get(theme, `customization.defaults.timeFormat.${style}`, format); }, getDateTimeFormat : function(style = 'short') { - const format = Config().defaults.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); } }; @@ -402,9 +402,9 @@ function setClientTheme(client, themeId) { if(availThemes.has(themeId)) { msg = 'Set client theme'; setThemeId = themeId; - } else if(availThemes.has(config.defaults.theme)) { + } else if(availThemes.has(config.theme.default)) { msg = 'Failed setting theme by supplied ID; Using default'; - setThemeId = config.defaults.theme; + setThemeId = config.theme.default; } else { msg = 'Failed setting theme by system default ID; Using the first one we can find'; setThemeId = availThemes.keys().next().value; @@ -430,7 +430,7 @@ function getThemeArt(options, cb) { if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { options.themeId = options.client.user.properties.theme_id; } else { - options.themeId = config.defaults.theme; + options.themeId = config.theme.default; } // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... @@ -477,11 +477,11 @@ function getThemeArt(options, cb) { }); }, function fromDefaultTheme(artInfo, callback) { - if(artInfo || config.defaults.theme === options.themeId) { + if(artInfo || config.theme.default === options.themeId) { return callback(null, artInfo); } - options.basePath = paths.join(config.paths.themes, config.defaults.theme); + options.basePath = paths.join(config.paths.themes, config.theme.default); art.getArt(options.name, options, (err, artInfo) => { return callback(null, artInfo); }); diff --git a/docs/configuration/config-hjson.md b/docs/configuration/config-hjson.md index 10ca119d..37d07beb 100644 --- a/docs/configuration/config-hjson.md +++ b/docs/configuration/config-hjson.md @@ -46,12 +46,10 @@ Below is a **sample** `config.hjson` illustrating various (but certainly not all menuFile: "your_bbs.hjson" // copy of menu.hjson file (and adapt to your needs) } - defaults: { - theme: "super-fancy-theme" // default-assigned theme (for new users) + theme: { + default: "super-fancy-theme" // default-assigned theme (for new users) } - preLoginTheme: "luciano_blocktronics" // theme used before a user logs in (matrix, NUA, etc.) - messageConferences: { local_general: { name: Local From 93305d44fc53b802a7eddaf1e0bc10b075cb227d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 7 Nov 2018 20:24:05 -0700 Subject: [PATCH 282/569] Initial WIP on better 'oputil config new' for testing --- core/config.js | 9 ++-- core/oputil/oputil_config.js | 79 +++++++++------------------- misc/config_template.in.hjson | 99 +++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 57 deletions(-) create mode 100644 misc/config_template.in.hjson diff --git a/core/config.js b/core/config.js index 74148c69..6a48c78f 100644 --- a/core/config.js +++ b/core/config.js @@ -10,8 +10,9 @@ const async = require('async'); const _ = require('lodash'); const assert = require('assert'); -exports.init = init; -exports.getDefaultPath = getDefaultPath; +exports.init = init; +exports.getDefaultPath = getDefaultPath; +exports.getDefaultConfig = getDefaultConfig; let currentConfiguration = {}; @@ -133,8 +134,8 @@ function getDefaultConfig() { general : { boardName : 'Another Fine ENiGMA½ BBS', + // :TODO: closedSystem and loginAttemps prob belong under users{}? closedSystem : false, // is the system closed to new users? - loginAttempts : 3, menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config) @@ -173,7 +174,7 @@ function getDefaultConfig() { default : 'luciano_blocktronics', preLogin : 'luciano_blocktronics', - passwordChar : '*', // TODO: move to user ? + passwordChar : '*', dateFormat : { short : 'MM/DD/YYYY', long : 'ddd, MMMM Do, YYYY', diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 65ede6bd..2fa7b3a9 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -30,6 +30,10 @@ function getAnswers(questions, cb) { }); } +const ConfigIncludeKeys = [ + 'theme', +]; + const QUESTIONS = { Intro : [ { @@ -72,12 +76,6 @@ const QUESTIONS = { default : 2, filter : s => s.toLowerCase(), }, - { - name : 'sevenZipExe', - message : '7-Zip executable:', - type : 'list', - choices : [ '7z', '7za', 'None' ] - } ], MessageConfAndArea : [ @@ -149,27 +147,34 @@ function askNewConfigQuestions(cb) { function promptOverwrite(needPrompt, callback) { if(needPrompt) { getAnswers(QUESTIONS.OverwriteConfig, answers => { - callback(answers.overwriteConfig ? null : 'exit'); + return callback(answers.overwriteConfig ? null : 'exit'); }); } else { - callback(null); + return callback(null); } }, function basic(callback) { getAnswers(QUESTIONS.Basic, answers => { - config = { - general : { - boardName : answers.boardName, - }, - }; + const defaultConfig = require('../../core/config.js').getDefaultConfig(); - callback(null); + // start by plopping in values we want directly from config.js + const template = hjson.rt.parse(fs.readFileSync(paths.join(__dirname, '../../misc/config_template.in.hjson'), 'utf8')); + + const direct = {}; + _.each(ConfigIncludeKeys, keyPath => { + _.set(direct, keyPath, _.get(defaultConfig, keyPath)); + }); + + config = _.mergeWith(template, direct); + + // we can override/add to it based on user input from this point on... + config.general.boardName = answers.boardName; + + return callback(null); }); }, function msgConfAndArea(callback) { getAnswers(QUESTIONS.MessageConfAndArea, answers => { - config.messageConferences = {}; - const confName = makeMsgConfAreaName(answers.msgConfName); const areaName = makeMsgConfAreaName(answers.msgAreaName); @@ -180,12 +185,6 @@ function askNewConfigQuestions(cb) { default : true, }; - config.messageConferences.another_sample_conf = { - name : 'Another Sample Conference', - desc : 'Another conference example. Change me!', - sort : 2, - }; - config.messageConferences[confName].areas = {}; config.messageConferences[confName].areas[areaName] = { name : answers.msgAreaName, @@ -194,51 +193,25 @@ function askNewConfigQuestions(cb) { default : true, }; - config.messageConferences.another_sample_conf = { - name : 'Another Sample Conference', - desc : 'Another conf sample. Change me!', - - areas : { - another_sample_area : { - name : 'Another Sample Area', - desc : 'Another area example. Change me!', - sort : 2 - } - } - }; - - callback(null); + return callback(null); }); }, function misc(callback) { getAnswers(QUESTIONS.Misc, answers => { - if('None' !== answers.sevenZipExe) { - config.archivers = { - zip : { - compressCmd : answers.sevenZipExe, - decompressCmd : answers.sevenZipExe, - } - }; - } + config.logging.rotatingFile.level = answers.loggingLevel; - config.logging = { - rotatingFile : { - level : answers.loggingLevel, - } - }; - - callback(null); + return callback(null); }); } ], err => { - cb(err, configPath, config); + return cb(err, configPath, config); } ); } function writeConfig(config, path) { - config = hjson.stringify(config, { bracesSameLine : true, spaces : '\t', keepWsc : true, quotes : 'strings' } ); + config = hjson.stringify(config, { bracesSameLine : true, space : '\t', keepWsc : true, quotes : 'strings' } ); try { fs.writeFileSync(path, config, 'utf8'); diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson new file mode 100644 index 00000000..715c81be --- /dev/null +++ b/misc/config_template.in.hjson @@ -0,0 +1,99 @@ +{ + /* + ./\/\.' ENiGMA½ System Configuration -/--/-------- - -- - + + _____________________ _____ ____________________ __________\_ / + \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! + // __|___// | \// |// | \// | | \// \ /___ /_____ + /____ _____| __________ ___|__| ____| \ / _____ \ + ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + + ------------------------------------------------------------------------------- + + General Information + ------------------------------- + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. + + See http://hjson.org/ for more information and syntax. + + Various editors and IDEs have plugins for the HJSON format which can be + very useful. + + Available Configuration + ------------------------------- + ENiGMA½ is highly configurable! By default, this file contains common + configuration elements, examples, etc. To see a full list of settings + available to this file, don't be afraid to open up core/config.js and + look around. Do not make changes there however, you may override any + of the configuration from within this file! + + See the documentation for more information, and don't be shy to ask + for help! + */ + + general: { + // Your BBS Name! + boardName: XXXXX + } + + logging: { + // By default, the system will rotate logs. + // Remember you can pipe logs through bunyan to pretty-print: + // > tail -F enigma/logs/enigma-bbs.log | enigma/node_modules/bunyan/bin/bunyan + rotatingFile: { + // If you're having trouble, try setting this to "trace" + level: XXXXX + } + } + + theme: { + // Default theme applied to new users. "*" indicates random. + default: XXXXX + + // Theme applied before a user has logged in. "*" indicates random. + preLogin: XXXXX + } + + // Message conferences and areas are within this block + messageConferences: { + // An entry here prepresents a conference taka aka confTag + another_sample_conf: { + name: "Another Sample Conference" + desc: "Another conf sample. Change me!" + areas: { + // Similar to confTags, this is a areaTag + another_sample_area: { + name: "Another Sample Area" + desc: "Another area example. Change me!" + // The 'sort' key can override natural sort order and can live at the conference and area levels + sort: 2 + } + } + } + } + + // Archive files and related + archives: { + // External utilities used for import & upload processing archives such + // as .zip, .rar, .arj, etc. + // + // You'll want to have archivers configured for the many old-school archive + // formats that a BBS may encounter! + // + // See config.js for additional configuration + archivers: { + // + // Each key in the "archivers" configuration block represents a specific + // external archive utility. ENiGMA½ has sane configuration by default + // for many archivers, but the tools themselves are likely not yet installed + // on your system! + // + // Please consult the documentation on information as to where to find and + // install these utilities! + // + } + } +} \ No newline at end of file From 97e2d103e2987af7a6dc9559dc6189997524cd20 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 9 Nov 2018 19:02:07 -0700 Subject: [PATCH 283/569] Comments --- core/oputil/oputil_config.js | 2 +- misc/config_template.in.hjson | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 2fa7b3a9..66b11fa6 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -169,7 +169,7 @@ function askNewConfigQuestions(cb) { // we can override/add to it based on user input from this point on... config.general.boardName = answers.boardName; - + return callback(null); }); }, diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 715c81be..79bc2bbf 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -40,9 +40,11 @@ } logging: { + // // By default, the system will rotate logs. // Remember you can pipe logs through bunyan to pretty-print: // > tail -F enigma/logs/enigma-bbs.log | enigma/node_modules/bunyan/bin/bunyan + // rotatingFile: { // If you're having trouble, try setting this to "trace" level: XXXXX @@ -77,6 +79,7 @@ // Archive files and related archives: { + // // External utilities used for import & upload processing archives such // as .zip, .rar, .arj, etc. // @@ -84,6 +87,7 @@ // formats that a BBS may encounter! // // See config.js for additional configuration + // archivers: { // // Each key in the "archivers" configuration block represents a specific @@ -96,4 +100,13 @@ // } } + + users: { + // + // ENiGMA½ utilizes user groups similar to Windows and *nix. Built in groups + // include "users" (for regular users) and "sysops" for +ops. You can add other + // groups to the system as well by adding a 'groups' key in this section: + // groups: [ "leet", "lamerz" ] + // + } } \ No newline at end of file From 2616499c17fc79a962a5967fda154be86b576408 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 9 Nov 2018 19:05:59 -0700 Subject: [PATCH 284/569] Installer script should move along with actual version! --- misc/install.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/misc/install.sh b/misc/install.sh index b50ca8c7..dce7ad21 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -3,6 +3,7 @@ { # this ensures the entire script is downloaded before execution ENIGMA_NODE_VERSION=${ENIGMA_NODE_VERSION:=10} +ENIGMA_BRANCH=0.0.9-alpha ENIGMA_INSTALL_DIR=${ENIGMA_INSTALL_DIR:=$HOME/enigma-bbs} ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` @@ -78,7 +79,7 @@ download_enigma_source() { else log "Downloading ENiGMA½ from git to '$INSTALL_DIR'" mkdir -p "$INSTALL_DIR" - command git clone ${ENIGMA_SOURCE} "$INSTALL_DIR" || { + command git clone ${ENIGMA_SOURCE} "$INSTALL_DIR" && git checkout ${ENIGMA_BRANCH} || { log_error "Failed to clone ENiGMA½ repo. Please report this!" exit 1 } @@ -121,6 +122,8 @@ Additionally, the following support binaires are recommended: Debian/Ubuntu : apt-get install lrzsz CentOS : yum install lrzsz + See docs for more information! + EndOfMessage echo -e "\e[39m" } From 1c216dc4532b42e88ac78812ea4f496ca53e7805 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 9 Nov 2018 19:19:16 -0700 Subject: [PATCH 285/569] Dur. --- misc/install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/install.sh b/misc/install.sh index dce7ad21..42cbbd0d 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -79,7 +79,7 @@ download_enigma_source() { else log "Downloading ENiGMA½ from git to '$INSTALL_DIR'" mkdir -p "$INSTALL_DIR" - command git clone ${ENIGMA_SOURCE} "$INSTALL_DIR" && git checkout ${ENIGMA_BRANCH} || { + command git clone ${ENIGMA_SOURCE} "$INSTALL_DIR" || { log_error "Failed to clone ENiGMA½ repo. Please report this!" exit 1 } @@ -89,7 +89,7 @@ download_enigma_source() { install_node_packages() { log "Installing required Node packages" cd ${ENIGMA_INSTALL_DIR} - npm install + git checkout ${ENIGMA_BRANCH} && npm install if [ $? -eq 0 ]; then log "npm package installation complete" else From a7b506a5959518831e6c0946428eb527f39232b4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 10:17:24 -0700 Subject: [PATCH 286/569] Updated bad password list --- core/config.js | 12 +- ...d_list_top_10000.txt => bad_passwords.txt} | 22499 +++++++++------- 2 files changed, 12583 insertions(+), 9928 deletions(-) rename misc/{10_million_password_list_top_10000.txt => bad_passwords.txt} (53%) diff --git a/core/config.js b/core/config.js index 6a48c78f..bc6be381 100644 --- a/core/config.js +++ b/core/config.js @@ -149,7 +149,17 @@ function getDefaultConfig() { passwordMin : 6, passwordMax : 128, - badPassFile : paths.join(__dirname, '../misc/10_million_password_list_top_10000.txt'), // https://github.com/danielmiessler/SecLists + + // + // The bad password list is a text file containing a password per line. + // Entries in this list are not allowed to be used on the system as they + // are known to be too common. + // + // A great resource can be found at https://github.com/danielmiessler/SecLists + // + // Current list source: https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/probable-v2-top12000.txt + // + badPassFile : paths.join(__dirname, '../misc/bad_passwords.txt'), realNameMax : 32, locationMax : 32, diff --git a/misc/10_million_password_list_top_10000.txt b/misc/bad_passwords.txt similarity index 53% rename from misc/10_million_password_list_top_10000.txt rename to misc/bad_passwords.txt index 7404c69b..864276a9 100644 --- a/misc/10_million_password_list_top_10000.txt +++ b/misc/bad_passwords.txt @@ -1,10000 +1,12645 @@ 123456 password -12345678 -qwerty 123456789 +12345678 12345 -1234 +qwerty +123123 111111 +abc123 1234567 dragon -123123 -baseball -abc123 -football -monkey -letmein -696969 -shadow -master -666666 -qwertyuiop -123321 -mustang -1234567890 -michael -654321 -pussy -superman -1qaz2wsx -7777777 -fuckyou -121212 -000000 -qazwsx -123qwe -killer -trustno1 -jordan -jennifer -zxcvbnm -asdfgh -hunter -buster -soccer -harley -batman -andrew -tigger +1q2w3e4r sunshine -iloveyou -fuckme -2000 -charlie -robert -thomas -hockey -ranger -daniel -starwars -klaster -112233 -george -asshole +654321 +master +1234 +football +1234567890 +000000 computer -michelle +666666 +superman +michael +internet +iloveyou +daniel +1qaz2wsx +monkey +shadow jessica -pepper -1111 -zxcvbn -555555 -11111111 -131313 -freedom -777777 -pass -fuck -maggie -159753 -aaaaaa -ginger +letmein +baseball +whatever princess -joshua -cheese -amanda -summer -love -ashley -6969 -nicole -chelsea -biteme -matthew -access -yankees -987654321 -dallas -austin -thunder -taylor -matrix -william -corvette -hello -martin -heather -secret -fucker -merlin +abcd1234 +123321 +starwars +121212 +thomas +zxcvbnm +trustno1 +killer +welcome +jordan +aaaaaa +123qwe +freedom +password1 +charlie +batman +jennifer +7777777 +michelle diamond -1234qwer -gfhjkm -hammer +oliver +mercedes +benjamin +11111111 +snoopy +samantha +victoria +matrix +george +alexander +secret +cookie +asdfgh +987654321 +123abc +orange +fuckyou +asdf1234 +pepper +hunter silver -222222 +joshua +banana +1q2w3e +chelsea +1234qwer +summer +qwertyuiop +phoenix +andrew +q1w2e3r4 +elephant +rainbow +mustang +merlin +london +garfield +robert +chocolate +112233 +samsung +qazwsx +matthew +buster +jonathan +ginger +flower +555555 +test +caroline +amanda +maverick +midnight +martin +junior 88888888 anthony -justin -test -bailey -q1w2e3r4t5 -patrick -internet -scooter -orange -11111 -golfer -cookie -richard -samantha -bigdog -guitar -jackson -whatever -mickey -chicken -sparky -snoopy -maverick -phoenix -camaro -sexy -peanut -morgan -welcome -falcon -cowboy -ferrari -samsung -andrea -smokey -steelers -joseph -mercedes -dakota -arsenal -eagles -melissa -boomer -booboo -spider -nascar -monster -tigers -yellow -xxxxxx -123123123 -gateway -marina -diablo -bulldog -qwer1234 -compaq -purple -hardcore -banana -junior -hannah -123654 -porsche -lakers -iceman -money -cowboys -987654 -london -tennis -999999 -ncc1701 -coffee -scooby -0000 -miller -boston -q1w2e3r4 -fuckoff -brandon -yamaha -chester -mother -forever -johnny -edward -333333 -oliver -redsox -player -nikita -knight -fender -barney -midnight -please -brandy -chicago -badboy -iwantu -slayer -rangers -charles -angel -flower -bigdaddy -rabbit -wizard -bigdick -jasper -enter -rachel -chris -steven -winner -adidas -victoria -natasha -1q2w3e4r jasmine -winter -prince -panties -marine -ghbdtn -fishing +creative +patrick +mickey +123 +qwerty123 cocacola -casper -james -232323 -raiders -888888 -marlboro -gandalf -asdfasdf -crystal -87654321 -12344321 -sexsex -golden -blowme -bigtits -8675309 -panther -lauren +chicken +passw0rd +forever +william +nicole +hello +yellow +nirvana +justin +friends +cheese +tigger +mother +liverpool +blink182 +asdfghjkl +andrea +spider +scooter +richard +soccer +rachel +purple +morgan +melissa +jackson +arsenal +222222 +qwe123 +gabriel +ferrari +jasper +danielle +bandit angela -bitch -spanky -thx1138 +scorpion +prince +maggie +austin +veronica +nicholas +monster +dexter +carlos +thunder +success +hannah +ashley +131313 +stella +brandon +pokemon +joseph +asdfasdf +999999 +metallica +december +chester +taylor +sophie +samuel +rabbit +crystal +barney +xxxxxx +steven +ranger +patricia +christian +asshole +spiderman +sandra +hockey angels -madison -winston -shannon -mike -toyota -blowjob +security +parker +heather +888888 +victor +harley +333333 +system +slipknot +november jordan23 canada -sophie -Password -apples -dick -tiger -razz -123abc -pokemon -qazxsw -55555 -qwaszx -muffin -johnson -murphy -cooper -jonathan -liverpoo -david -danielle -159357 -jackie -1990 -123456a -789456 -turtle -horny -abcd1234 -scorpion -qazwsxedc -101010 -butter -carlos -password1 -dennis -slipknot -qwerty123 -booger -asdf -1991 -black -startrek -12341234 -cameron -newyork -rainbow -nathan -john -1992 -rocket -viking -redskins -butthead -asdfghjkl -1212 -sierra -peaches -gemini -doctor -wilson -sandra -helpme +tennis qwertyui -victor -florida -dolphin -pookie -captain -tucker -blue -liverpool -theman -bandit -dolphins -maddog -packers -jaguar -lovers -nicholas -united -tiffany -maxwell -zzzzzz -nirvana -jeremy -suckit -stupid -porn -monica -elephant -giants -jackass -hotdog -rosebud -success -debbie -mountain -444444 -xxxxxxxx -warrior -1q2w3e4r5t -q1w2e3 -123456q -albert -metallic -lucky -azerty -7777 -shithead -alex -bond007 -alexis -1111111 -samson -5150 -willie -scorpio -bonnie -gators -benjamin -voodoo -driver -dexter -2112 -jason -calvin -freddy -212121 -creative -12345a -sydney -rush2112 -1989 -asdfghjk -red123 -bubba -4815162342 -passw0rd -trouble -gunner -happy -fucking -gordon -legend -jessie -stella -qwert -eminem -arthur -apple -nissan -bullshit -bear -america -1qazxsw2 -nothing -parker -4444 -rebecca -qweqwe -garfield -01012011 -beavis -69696969 -jack -asdasd -december -2222 -102030 -252525 -11223344 -magic -apollo -skippy -315475 -girls -kitten -golf -copper -braves -shelby -godzilla -beaver -fred -tomcat -august -buddy -airborne -1993 -1988 -lifehack -qqqqqq -brooklyn -animal -platinum -phantom -online -xavier -darkness -blink182 -power -fish -green -789456123 -voyager -police -travis -12qwaszx -heaven -snowball -lover -abcdef -00000 -pakistan -007007 -walter -playboy -blazer -cricket -sniper -hooters -donkey -willow -loveme -saturn -therock -redwings -bigboy -pumpkin -trinity -williams -tits -nintendo -digital -destiny -topgun -runner -marvin -guinness -chance -bubbles -testing -fire -november -minecraft -asdf1234 -lasvegas -sergey -broncos -cartman -private -celtic -birdie -little -cassie -babygirl -donald -beatles -1313 -dickhead -family -12121212 -school -louise -gabriel -eclipse -fluffy -147258369 -lol123 -explorer -beer -nelson -flyers -spencer -scott -lovely -gibson -doggie -cherry -andrey -snickers -buffalo -pantera -metallica -member -carter -qwertyu -peter -alexande -steve -bronco -paradise -goober -5555 -samuel -montana -mexico -dreams -michigan -cock -carolina -yankee -friends -magnum -surfer -poopoo -maximus -genius -cool -vampire -lacrosse +casper +gemini asd123 -aaaa -christin -kimberly -speedy -sharon -carmen -111222 -kristina -sammy -racing -ou812 -sabrina -horses -0987654321 -qwerty1 -pimpin -baby -stalker -enigma -147147 -star -poohbear -boobies -147258 -simple -bollocks -12345q -marcus -brian -1987 -qweasdzxc -drowssap -hahaha -caroline -barbara -dave -viper -drummer -action -einstein -bitches -genesis -hello1 -scotty -friend -forest -010203 -hotrod -google -vanessa -spitfire -badger -maryjane -friday -alaska -1232323q -tester -jester -jake -champion -billy -147852 -rock -hawaii -badass -chevy -420420 -walker -stephen -eagle1 -bill -1986 -october -gregory -svetlana -pamela -1984 -music -shorty -westside -stanley -diesel -courtney -242424 -kevin -porno -hitman -boobs -mark -12345qwert -reddog -frank -qwe123 +winter +hammer +cooper +america +albert +777777 +winner +charles +butterfly +swordfish popcorn -patricia -aaaaaaaa -1969 -teresa -mozart -buddha -anderson -paul -melanie -abcdefg -security -lucky1 -lizard -denise -3333 -a12345 -123789 -ruslan -stargate -simpsons -scarface -eagle -123456789a -thumper +penguin +dolphin +carolina +access +987654 +hardcore +corvette +apples +12341234 +sabrina +remember +qwer1234 +edward +dennis +cherry +sparky +natasha +arthur +vanessa +marina +leonardo +johnny +dallas +antonio +winston +snickers olivia -naruto -1234554321 -general -cherokee -a123456 +nothing +iceman +destiny +coffee +apollo +696969 +windows +williams +school +madison +dakota +angelina +anderson +159753 +1111 +yamaha +trinity +rebecca +nathan +guitar +compaq +123123123 +toyota +shannon +playboy +peanut +pakistan +diablo +abcdef +maxwell +golden +asdasd +123654 +murphy +monica +marlboro +kimberly +gateway +bailey +00000000 +snowball +scooby +nikita +falcon +august +test123 +sebastian +panther +love +johnson +godzilla +genesis +brandy +adidas +zxcvbn +wizard +porsche +online +hello123 +fuckoff +eagles +champion +bubbles +boston +smokey +precious +mercury +lauren +einstein +cricket +cameron +angel +admin +napoleon +mountain +lovely +friend +flowers +dolphins +david +chicago +sierra +knight +yankees +wilson +warrior +simple +nelson +muffin +charlotte +calvin +spencer +newyork +florida +fernando +claudia +basketball +barcelona +87654321 +willow +stupid +samson +police +paradise +motorola +manager +jaguar +jackie +family +doctor +bullshit +brooklyn +tigers +stephanie +slayer +peaches +miller +heaven +elizabeth +bulldog +animal +789456 +scorpio +rosebud +qwerty12 +franklin +claire +american vincent -Usuckballz1 -spooky -qweasd -cumshot -free -frankie -douglas -death -1980 +testing +pumpkin +platinum +louise +kitten +general +united +turtle +marine +icecream +hacker +darkness +cristina +colorado +boomer +alexandra +steelers +serenity +please +montana +mitchell +marcus +lollipop +jessie +happy +cowboy +102030 +marshall +jupiter +jeremy +gibson +fucker +barbara +adrian +1qazxsw2 +12344321 +11111 +startrek +fishing +digital +christine +business +abcdefg +nintendo +genius +12qwaszx +walker +q1w2e3 +player +legend +carmen +booboo +tomcat +ronaldo +people +pamela +marvin +jackass +google +fender +asdfghjk +Password +1q2w3e4r5t +zaq12wsx +scotland +phantom +hercules +fluffy +explorer +alexis +walter +trouble +tester +qwerty1 +melanie +manchester +gordon +firebird +engineer +azerty +147258 +virginia +tiger +simpsons +passion +lakers +james +angelica +55555 +vampire +tiffany +september +private +maximus +loveme +isabelle +isabella +eclipse +dreamer +changeme +cassie +badboy +123456a +stanley +sniper +rocket +passport +pandora +justice +infinity +cookies +barbie +xavier +unicorn +superstar +stephen +rangers +orlando +money +domino +courtney +viking +tucker +travis +scarface +pavilion +nicolas +natalie +gandalf +freddy +donald +captain +abcdefgh +a1b2c3d4 +speedy +peter +nissan loveyou +harrison +friday +francis +dancer +159357 +101010 +spitfire +saturn +nemesis +little +dreams +catherine +brother +birthday +1111111 +wolverine +victory +student +france +fantasy +enigma +copper +bonnie +teresa +mexico +guinness +georgia +california +sweety +logitech +julian +hotdog +emmanuel +butter +beatles +11223344 +tristan +sydney +spirit +october +mozart +lolita +ireland +goldfish +eminem +douglas +cowboys +control +cheyenne +alex +testtest +stargate +raiders +microsoft +diesel +debbie +danger +chance +asdf +anything +aaaaaaaa +welcome1 +qwert +hahaha +forest +eternity +disney +denise +carter +alaska +zzzzzz +titanic +shorty +shelby +pookie +pantera +england +chris +zachary +westside +tamara +password123 +pass +maryjane +lincoln +willie +teacher +pierre +michael1 +leslie +lawrence +kristina +kawasaki +drowssap +college +blahblah +babygirl +avatar +alicia +regina +qqqqqq +poohbear +miranda +madonna +florence +sapphire +norman +hamilton +greenday +galaxy +frankie +black +awesome +suzuki +spring +qazwsxedc +magnum +lovers +liberty +gregory +232323 +twilight +timothy +swimming +super +stardust +sophia +sharon +robbie +predator +penelope +michigan +margaret +jesus +hawaii +green +brittany +brenda +badger +a1b2c3 +444444 +winnie +wesley +voodoo +skippy +shithead +redskins +qwertyu +pussycat +houston +horses +gunner +fireball +donkey +cherokee +australia +arizona +1234abcd +skyline +power +perfect +lovelove +kermit +kenneth +katrina +eugene +christ +thailand +support +special +runner +lasvegas +jason +fuckme +butthead +blizzard +athena +abigail +8675309 +violet +tweety +spanky +shamrock +red123 +rascal +melody +joanna +hello1 +driver +bluebird +biteme +atlantis +arnold +apple +alison +taurus +random +pirate +monitor +maria +lizard +kevin +hummer +holland +buffalo +147258369 +007007 +valentine +roberto +potter +magnolia +juventus +indigo +indian +harvey +duncan +diamonds +daniela +christopher +bradley +bananas +warcraft +sunset +simone +renegade +redsox +philip +monday +mohammed +indiana +energy +bond007 +avalon +terminator +skipper +shopping +scotty +savannah +raymond +morris +mnbvcxz +michele +lucky +lucifer +kingdom +karina +giovanni +cynthia +a123456 +147852 +12121212 +wildcats +ronald +portugal +mike +helpme +froggy +dragons +cancer +bullet +beautiful +alabama +212121 +unknown +sunflower +sports +siemens +santiago +kathleen +hotmail +hamster +golfer +future +father +enterprise +clifford +christina +camille +camaro +beauty +55555555 +vision +tornado +something +rosemary +qweasd +patches +magic +helena +denver +cracker +beaver +basket +atlanta +vacation +smiles +ricardo +pascal +newton +jeffrey +jasmin +january +honey +hollywood +holiday +gloria +element +chandler +booger +angelo +allison +action +99999999 +target +snowman +miguel +marley +lorraine +howard +harmony +children +celtic +beatrice +airborne +wicked +voyager +valentin +thx1138 +thumper +samurai +moonlight +mmmmmm +karate +kamikaze +jamaica +emerald +bubble +brooke +zombie +strawberry +spooky +software +simpson +service +sarah +racing +qazxsw +philips +oscar +minnie +lalala +ironman +goddess +extreme +empire +elaine +drummer +classic +carrie +berlin +asdfg +22222222 +valerie +tintin +therock +sunday +skywalker +salvador +pegasus +panthers +packers +network +mission +mark +legolas +lacrosse kitty kelly -veronica -suzuki -semperfi -penguin -mercury -liberty -spirit -scotland -natalie -marley -vikings -system -sucker -king -allison -marshall -1979 -098765 -qwerty12 -hummer -adrian -1985 -vfhbyf -sandman -rocky -leslie -antonio -98765432 -4321 -softball -passion -mnbvcxz -bastard -passport -horney -rascal -howard -franklin -bigred -assman -alexander -homer -redrum -jupiter -claudia -55555555 -141414 -zaq12wsx -shit -patches -nigger -cunt -raider -infinity -andre -54321 -galore -college -russia -kawasaki -bishop -77777777 -vladimir -money1 -freeuser -wildcats -francis -disney -budlight -brittany -1994 -00000000 -sweet -oksana -honda -domino -bulldogs -brutus -swordfis -norman -monday -jimmy -ironman -ford -fantasy -9999 -7654321 -PASSWORD -hentai -duncan -cougar -1977 -jeffrey -house -dancer -brooke -timothy -super -marines -justice -digger -connor -patriots -karina -202020 -molly -everton -tinker -alicia -rasdzv3 -poop -pearljam -stinky -naughty -colorado -123123a -water -test123 -ncc1701d -motorola -ireland -asdfg -slut -matt -houston -boogie -zombie -accord -vision -bradley -reggie -kermit -froggy -ducati -avalon -6666 -9379992 -sarah -saints -logitech -chopper -852456 -simpson -madonna -juventus -claire -159951 -zachary -yfnfif -wolverin -warcraft -hello123 -extreme -penis -peekaboo -fireman -eugene -brenda -123654789 -russell -panthers -georgia -smith -skyline -jesus -elizabet -spiderma -smooth -pirate -empire -bullet -8888 -virginia -valentin -psycho -predator -arizona -134679 -mitchell -alyssa -vegeta -titanic -christ -goblue -fylhtq -wolf -mmmmmm -kirill -indian -hiphop -baxter -awesome -people -danger -roland -mookie -741852963 -1111111111 -dreamer -bambam -arnold -1981 -skipper -serega -rolltide -elvis -changeme -simon -1q2w3e -lovelove -fktrcfylh -denver -tommy -mine -loverboy -hobbes -happy1 -alison -nemesis -chevelle -cardinal -burton -wanker -picard -151515 -tweety -michael1 -147852369 -12312 -xxxx -windows -turkey -456789 -1974 -vfrcbv -sublime -1975 -galina -bobby -newport -manutd -daddy -american -alexandr -1966 -victory -rooster -qqq111 -madmax -electric -bigcock -a1b2c3 -wolfpack -spring -phpbb -lalala -suckme -spiderman -eric -darkside -classic -raptor -123456789q -hendrix -1982 -wombat -avatar -alpha -zxc123 -crazy -hard -england -brazil -1978 -01011980 -wildcat -polina -freepass -carrie -99999999 -qaz123 -holiday -fyfcnfcbz -brother -taurus -shaggy -raymond -maksim -gundam -admin -vagina -pretty -pickle -good -chronic -alabama -airplane -22222222 -1976 -1029384756 -01011 -time -sports -ronaldo -pandora -cheyenne -caesar -billybob -bigman -1968 -124578 -snowman -lawrence -kenneth -horse -france -bondage -perfect -kristen -devils -alpha1 -pussycat -kodiak -flowers -1973 -01012000 -leather -amber -gracie -chocolat -bubba1 -catch22 -business -2323 -1983 -cjkysirj -1972 -123qweasd -ytrewq -wolves -stingray -ssssss -serenity -ronald -greenday -135790 -010101 -tiger1 -sunset -charlie1 -berlin -bbbbbb -171717 -panzer -lincoln -katana -firebird -blizzard -a1b2c3d4 -white -sterling -redhead -password123 -candy -anna -142536 -sasha -pyramid -outlaw -hercules -garcia -454545 -trevor -teens -maria -kramer -girl -popeye -pontiac -hardon -dude -aaaaa -323232 -tarheels -honey -cobra -buddy1 -remember -lickme -detroit -clinton -basketball -zeppelin -whynot -swimming -strike -service -pavilion -michele -engineer -dodgers -britney -bobafett -adam -741852 -21122112 -xxxxx -robbie -miranda -456123 -future -darkstar -icecream -connie -1970 -jones -hellfire -fisher -fireball -apache -fuckit -blonde -bigmac -abcd -morris -angel1 -666999 -321321 -simone -rockstar -flash -defender -1967 -wallace -trooper -oscar -norton -casino -cancer -beauty -weasel -savage -raven -harvey -bowling -246810 -wutang -theone -swordfish -stewart -airforce -abcdefgh -nipples -nastya -jenny -hacker -753951 -amateur -viktor -srinivas -maxima -lennon -freddie -bluebird -qazqaz -presario -pimp -packard -mouse -looking -lesbian -jeff -cheryl -2001 -wrangler -sandy -machine -lights -eatme -control -tattoo -precious -harrison -duke -beach -tornado -tanner -goldfish -catfish -openup -manager -1971 -street -Soso123aljg -roscoe -paris -natali -light -julian -jerry -dilbert -dbrnjhbz -chris1 -atlanta -xfiles -thailand -sailor -pussies -pervert -lucifer -longhorn -enjoy -dragons -young -target -elaine -dustin -123qweasdzxc -student -madman -lisa -integra -wordpass -prelude -newton -lolita -ladies -hawkeye -corona -bubble -31415926 -trigger -spike -katie -iloveu -herman -design -cannon -999999999 -video -stealth -shooter -nfnmzyf -hottie -browns -314159 -trucks -malibu -bruins -bobcat -barbie -1964 -orlando -letmein1 -freaky -foobar -cthutq -baller -unicorn -scully -pussy1 -potter -cookies -pppppp -philip -gogogo -elena -country -assassin -1010 -zaqwsx -testtest -peewee -moose -microsoft -teacher -sweety -stefan -stacey -shotgun -random -laura -hooker -dfvgbh -devildog -chipper -athena -winnie -valentina -pegasus -kristin -fetish -butterfly -woody -swinger -seattle -lonewolf -joker -booty -babydoll -atlantis -tony -powers -polaris -montreal -angelina -77777 -tickle -regina -pepsi -gizmo -express -dollar -squirt -shamrock -knicks -hotstuff -balls -transam -stinger -smiley -ryan -redneck -mistress -hjvfirf -cessna -bunny -toshiba -single -piglet -fucked -father -deftones -coyote -castle -cadillac -blaster -valerie -samurai -oicu812 -lindsay -jasmin -james1 -ficken -blahblah -birthday -1234abcd -01011990 -sunday -manson -flipper -asdfghj -181818 -wicked -great -daisy -babes -skeeter -reaper -maddie -cavalier -veronika -trucker -qazwsx123 -mustang1 -goldberg -escort -12345678910 -wolfgang -rocks -mylove -mememe -lancer -ibanez -travel -sugar -snake -sister -siemens -savannah -minnie -leonardo -basketba -1963 -trumpet -texas -rocky1 -galaxy -cristina -aardvark -shelly -hotsex -goldie -fatboy -benson -321654 -141627 -sweetpea -ronnie -indigo -13131313 -spartan -roberto -hesoyam -freeman -freedom1 -fredfred -pizza -manchester -lestat -kathleen -hamilton -erotic -blabla -22222 -1995 -skater -pencil -passwor -larisa -hornet -hamlet -gambit -fuckyou2 -alfred -456456 -sweetie -marino -lollol -565656 -techno -special -renegade -insane -indiana -farmer -drpepper -blondie -bigboobs -272727 -1a2b3c -valera -storm -seven -rose -nick -mister -karate -casey -1qaz2wsx3edc -1478963 -maiden -julie -curtis -colors -christia -buckeyes -13579 -0123456789 -toronto -stephani -pioneer -kissme -jungle -jerome -holland -harry -garden -enterpri -dragon1 -diamonds -chrissy -bigone -343434 -wonder -wetpussy -subaru -smitty -racecar -pascal -morpheus -joanne -irina -indians -impala -hamster -charger -change -bigfoot -babylon -66666666 -timber -redman -pornstar -bernie -tomtom -thuglife -millie -buckeye -aaron -virgin -tristan -stormy -rusty -pierre -napoleon -monkey1 -highland -chiefs -chandler -catdog -aurora -1965 -trfnthbyf -sampson -nipple -dudley -cream -consumer -burger -brandi -welcome1 -triumph -joejoe -hunting -dirty -caserta -brown -aragorn -363636 -mariah -element -chichi -2121 -123qwe123 -wrinkle1 -smoke -omega -monika -leonard -justme -hobbit -gloria -doggy -chicks -bass -audrey -951753 -51505150 -11235813 -sakura -philips -griffin -butterfl -artist -66666 -island -goforit -emerald -elizabeth -anakin -watson -poison -none +jester italia -callie -bobbob -autumn -andreas -123 -sherlock -q12345 -pitbull -marathon -kelsey -inside -german -blackie -access14 -123asd -zipper -overlord -nadine -marie -basket -trombone -stones -sammie -nugget -naked -kaiser -isabelle -huskers -bomber -barcelona -babylon5 -babe -alpine -weed -ultimate -pebbles -nicolas -marion -loser -linda -eddie -wesley -warlock -tyler -goddess -fatcat -energy -david1 -bassman -yankees1 -whore -trojan -trixie -superfly -kkkkkk -ybrbnf -warren -sophia -sidney -pussys -nicola -campbell -vfvjxrf -singer -shirley -qawsed -paladin -martha -karen -help -harold -geronimo -forget -concrete -191919 -westham -soldier -q1w2e3r4t5y6 -poiuyt -nikki -mario -juice -jessica1 -global -dodger -123454321 -webster -titans -tintin -tarzan -sexual -sammy1 -portugal -onelove -marcel -manuel -madness -jjjjjj -holly -christy -424242 -yvonne -sundance -sex4me -pleasure -logan -danny -wwwwww -truck -spartak -smile -michel -history -Exigen -65432 -1234321 -sherry -sherman -seminole -rommel -network -ladybug -isabella -holden -harris -germany -fktrctq -cotton -angelo -14789632 -sergio -qazxswedc -moon -jesus1 -trunks -snakes -sluts -kingkong -bluesky -archie -adgjmptw -911911 -112358 -sunny -suck -snatch -planet -panama -ncc1701e -mongoose -head -hansolo -desire -alejandr -1123581321 -whiskey -waters -teen -party -martina -margaret -january -connect +hiphop +freeman +charlie1 +cardinal bluemoon -bianca -andrei -5555555 -smiles -nolimit -long -assass -abigail -555666 -yomama -rocker -plastic -katrina -ghbdtnbr -ferret -emily -bonehead -blessed -beagle -asasas -abgrtyu -sticky -olga -japan -jamaica -home -hector -dddddd -1961 -turbo -stallion -personal -peace -movie -morrison -joanna -geheim -finger -cactus -7895123 -susan -super123 -spyder -mission -anything -aleksandr -zxcvb -shalom -rhbcnbyf -pickles -passat -natalia -moomoo -jumper -inferno -dietcoke -cumming -cooldude -chuck -christop -million -lollipop -fernando -christian -blue22 -bernard -apple1 -unreal -spunky -ripper -open -niners -letmein2 -flatron -faster -deedee -bertha -april -4128 -01012010 -werewolf -rubber -punkrock -orion -mulder -missy -larry -giovanni -gggggg -cdtnkfyf -yoyoyo -tottenha -shaved -newman -lindsey -joey -hongkong -freak -daniela -camera -brianna -blackcat -a1234567 -1q1q1q -zzzzzzzz -stars -pentium -patton -jamie -hollywoo -florence -biscuit -beetle -andy -always -speed -sailing -phillip -legion -gn56gn56 -909090 -martini -dream -darren -clifford -2002 -stocking -solomon -silvia -pirates -office -monitor -monique -milton -matthew1 -maniac -loulou -jackoff -immortal -fossil -dodge -delta -44444444 -121314 -sylvia -sprite -shadow1 -salmon -diana -shasta -patriot -palmer -oxford -nylons -molly1 -irish -holmes -curious -asdzxc -1999 -makaveli -kiki -kennedy -groovy -foster -drizzt -twister -snapper -sebastia -philly -pacific -jersey -ilovesex -dominic -charlott -carrot -anthony1 -africa -111222333 -sharks -serena -satan666 -maxmax -maurice -jacob -gerald -cosmos -columbia -colleen -cjkywt -cantona -brooks -99999 -787878 -rodney -nasty -keeper -infantry -frog -french -eternity -dillon -coolio -condor -anton -waterloo -velvet -vanhalen -teddy -skywalke -sheila -sesame -seinfeld -funtime -012345 -standard -squirrel -qazwsxed -ninja -kingdom -grendel -ghost -fuckfuck -damien -crimson -boeing -bird -biggie -090909 -zaq123 -wolverine -wolfman -trains -sweets -sunrise -maxine -legolas -jericho -isabel -foxtrot -anal -shogun -search -robinson -rfrfirf -ravens -privet -penny -musicman -memphis -megadeth -dogs -butt -brownie -oldman -graham -grace -505050 -verbatim -support -safety -review -newlife -muscle -herbert -colt45 -bottom -2525 -1q2w3e4r5t6y -1960 -159159 -western -twilight -thanks -suzanne -potato -pikachu -murray -master1 -marlin -gilbert -getsome -fuckyou1 -dima -denis -789789 -456852 -stone -stardust -seven7 -peanuts -obiwan -mollie -licker -kansas -frosty -ball -262626 -tarheel -showtime -roman -markus -maestro -lobster -darwin -cindy -chubby -2468 -147896325 -tanker -surfing +bbbbbb +bastard +alyssa +0123456789 +zeppelin +tinker +surfer +smile +rockstar +operator +naruto +freddie +dragonfly +dickhead +connor +anaconda +amsterdam +alfred +a12345 +789456123 +77777777 +trooper skittles -showme -shaney14 -qwerty12345 -magic1 -goblin -fusion -blades -banshee -alberto -123321123 -123098 -powder -malcolm -intrepid -garrett -delete -chaos -bruno -1701 -tequila -short -sandiego -python -punisher -newpass -iverson -clayton -amadeus -1234567a -stimpy -sooners -preston -poopie -photos -neptune -mirage -harmony -gold -fighter -dingdong -cats -whitney -sucks -slick -rick -ricardo -princes -liquid -helena -daytona -clover -blues -anubis -1996 -192837465 -starcraft -roxanne -pepsi1 -mushroom -eatshit -dagger -cracker -capital -brendan -blackdog -25802580 -strider -slapshot -porter -pink -jason1 -hershey -gothic -flight -ekaterina -cody -buffy -boss -bananas -aaaaaaa -123698745 -1234512345 -tracey -miami -kolobok -danni -chargers -cccccc -blue123 -bigguy -33333333 -0.0.000 -warriors -walnut -raistlin -ping -miguel -latino -griffey -green1 -gangster -felix -engine -doodle -coltrane -byteme -buck -asdf123 -123456z -0007 -vertigo -tacobell -shark -portland -penelope -osiris -nymets -nookie -mary -lucky7 -lucas -lester -ledzep -gorilla -coco -bugger -bruce -blood -bentley -battle -1a2b3c4d -19841984 -12369874 -weezer -turner -thegame -stranger -sally -Mailcreated5240 -knights -halflife -ffffff -dorothy -dookie -damian -258456 -women -trance -qwerasdf -playtime -paradox -monroe -kangaroo -henry -dumbass -dublin -charly -butler -brasil -blade -blackman -bender -baggins -wisdom -tazman -swallow -stuart -scruffy -phoebe -panasonic -Michael -masters -ghjcnj -firefly -derrick -christine -beautiful -auburn -archer -aliens -161616 -1122 -woody1 -wheels -test1 -spanking -robin -redred -racerx -postal -parrot -nimrod -meridian -madrid -lonestar -kittycat -hell -goodluck -gangsta -formula -devil -cassidy -camille -buttons -bonjour -bingo -barcelon -allen -98765 -898989 -303030 -2020 -0000000 -tttttt -tamara -scoobydo -samsam -rjntyjr -richie -qwertz -megaman -luther -jazz -crusader -bollox -123qaz -12312312 -102938 -window -sprint -sinner -sadie -rulez -quality -pooper -pass123 -oakland -misty -lvbnhbq -lady -hannibal -guardian -grizzly -fuckface -finish -discover -collins -catalina -carson -black1 -bang -annie -123987 -1122334455 -wookie -volume -tina -rockon -qwer -molson -marco -californ -angelica -2424 -world -william1 -stonecol -shemale -shazam -picasso +shalom +raptor +pioneer +personal +ncc1701 +nascar +music +kristen +kingkong +global +geronimo +germany +country +christmas +bernard +benson +wrestling +warren +techno +sunrise +stefan +sister +savage +russell +robinson oracle -moscow -luke -lorenzo -kitkat -johnjohn -janice -gerard -flames -duck -dark -celica -445566 -234567 -yourmom -topper -stevie -septembe -scarlett -santiago -milano -lowrider -loving -incubus -dogdog -anastasia -1962 -123zxc -vacation -tempest -sithlord -scarlet -rebels -ragnarok -prodigy -mobile +millie +maddog +lightning +kingston +kennedy +hannibal +garcia +download +dollar +darkstar +brutus +bobby +autumn +webster +vanilla +undertaker +tinkerbell +sweetpea +ssssss +softball +rafael +panasonic +pa55word keyboard -golfing -english -carlo -anime -545454 -19921992 -11112222 -vfhecz -sobaka -shiloh -penguins -nuttertools -mystery -lorraine -llllll -lawyer -kiss -jeep -gizmodo -elwood -dkflbvbh -987456 -6751520 -12121 -titleist -tardis -tacoma -smoker -shaman -rootbeer -magnolia -julia -juan -hoover -gotcha -dodgeram -creampie -buffett -bridge -aspirine -456654 -socrates -photo -parola -nopass -megan -lucy -kenwood -kenny -imagine -forgot -cynthia -blondes -ashton -aezakmi -1234567q -viper1 -terry -sabine -redalert -qqqqqqqq -munchkin -monkeys -mersedes -melvin -mallard -lizzie -imperial -honda1 -gremlin -gillian -elliott -defiant -dadada -cooler -bond -blueeyes -birdman -bigballs -analsex -753159 -zaq1xsw2 -xanadu -weather -violet -sergei -sebastian -romeo -research -putter -oooooo +isabel +hector +fisher +dominic +darkside +cleopatra +blue +assassin +amelia +vladimir +roland +nigger national -lexmark -hotboy -greg -garbage -colombia -chucky -carpet -bobo -bobbie -assfuck -88888 -01012001 -smokin -shaolin -roger -rammstein -pussy69 -katerina -hearts -frogger -freckles -dogg -dixie -claude -caliente -amazon -abcde -1221 -wright -willis -spidey -sleepy -sirius -santos -rrrrrr -randy -picture -payton -mason -dusty -director -celeste -broken -trebor -sheena -qazwsxedcrfv -polo +monique +molly +matthew1 +godfather +frank +curtis +change +central +cartman +brothers +boogie +archie +warriors +universe +turkey +topgun +solomon +sherry +sakura +rush2112 +qwaszx +office +mushroom +monika +marion +lorenzo +john +herman +connect +chopper +burton +blondie +bitch +bigdaddy +amber +456789 +1a2b3c4d +ultimate +tequila +tanner +sweetie +scott +rocky +popeye +peterpan +packard +loverboy +leonard +jimmy +harry +griffin +design +buddha +1 +wallace +truelove +trombone +toronto +tarzan +shirley +sammy +pebbles +natalia +marcel +malcolm +madeline +jerome +gilbert +gangster +dingdong +catalina +buddy +blazer +billy +bianca +alejandro +54321 +252525 +111222 +0000 +water +sucker +rooster +potato +norton +lucky1 +loving +lol123 +ladybug +kittycat +fuck +forget +flipper +fireman +digger +bonjour +baxter +audrey +aquarius +1111111111 +pppppp +planet +pencil +patriots +oxford +million +martha +lindsay +laura +jamesbond +ihateyou +goober +giants +garden +diana +cecilia +brazil +blessing +bishop +bigdog +airplane +Password1 +tomtom +stingray +psycho +pickle +outlaw +number1 +mylove +maurice +madman +maddie +lester +hendrix +hellfire +happy1 +guardian +flamingo +enter +chichi +0987654321 +western +twister +trumpet +trixie +socrates +singer +sergio +sandman +richmond +piglet +pass123 +osiris +monkey1 +martina +justine +english +electric +church +castle +caesar +birdie +aurora +artist +amadeus +alberto +246810 +whitney +thankyou +sterling +star +ronnie +pussy +printer +picasso +munchkin +morpheus +madmax +kaiser +julius +imperial +happiness +goodluck +counter +columbia +campbell +blessed +blackjack +alpha +999999999 +142536 +wombat +wildcat +trevor +telephone +smiley +saints +pretty oblivion -mustangs +newcastle +mariana +janice +israel +imagine +freedom1 +detroit +deedee +darren +catfish +adriana +washington +warlock +valentina +valencia +thebest +spectrum +skater +sheila +shaggy +poiuyt +member +jessica1 +jeremiah +jack +insane +iloveu +handsome +goldberg +gabriela +elijah +damien +daisy +buttons +blabla +bigboy +apache +anthony1 +a1234567 +xxxxxxxx +toshiba +tommy +sailor +peekaboo +motherfucker +montreal +manuel +madrid +kramer +katherine +kangaroo +jenny +immortal +harris +hamlet +gracie +fucking +firefly +chocolat +bentley +account +321321 +2222 +1a2b3c +thompson +theman +strike +stacey +science +running +research +polaris +oklahoma +mariposa +marie +leader +julia +island +idontknow +hitman +german +felipe +fatcat +fatboy +defender +applepie +annette +010203 +watson +travel +sublime +stewart +steve +squirrel +simon +sexy +pineapple +phoebe +paris +panzer +nadine +master1 +mario +kelsey +joker +hongkong +gorilla +dinosaur +connie +bowling +bambam +babydoll +aragorn +andreas +456123 +151515 +wolves +wolfgang +turner +semperfi +reaper +patience +marilyn +fletcher +drpepper +dorothy +creation +brian +bluesky +andre +yankee +wordpass +sweet +spunky +sidney +serena +preston +pauline +passwort +original +nightmare +miriam +martinez +labrador +kristin +kissme +henry +gerald +garrett +flash +excalibur +discovery +dddddd +danny +collins +casino +broncos +brendan +brasil +apple123 +yvonne +wonder +window +tomato +sundance +sasha +reggie +redwings +poison +mypassword +monopoly +mariah margarita -letsgo -josh -jimbob -jimbo +lionking +king +football1 +director +darling +bubba +biscuit +44444444 +wisdom +vivian +virgin +sylvester +street +stones +sprite +spike +single +sherlock +sandy +rocker +robin +matt +marianne +linda +lancelot +jeanette +hobbes +fred +ferret +dodger +cotton +corona +clayton +celine +cannabis +bella +andromeda +7654321 +4444 +werewolf +starcraft +sampson +redrum +pyramid +prodigy +paul +michel +martini +marathon +longhorn +leopard +judith +joanne +jesus1 +inferno +holly +harold +happy123 +esther +dudley +dragon1 +darwin +clinton +celeste +catdog +brucelee +argentina +alpine +147852369 +wrangler +william1 +vikings +trigger +stranger +silvia +shotgun +scarlett +scarlet +redhead +raider +qweasdzxc +playstation +mystery +morrison +honda +february +fantasia +designer +coyote +cool +bulldogs +bernie +baby +asdfghj +angel1 +always +adam +202020 +wanker +sullivan +stealth +skeeter +saturday +rodney +prelude +pingpong +phillip +peewee +peanuts +peace +nugget +newport +myself +mouse +memphis +lover +lancer +kristine +james1 +hobbit +halloween +fuckyou1 +finger +fearless +dodgers +delete +cougar +charmed +cassandra +caitlin +bismillah +believe +alice +airforce +7777 +viper +tony +theodore +sylvia +suzanne +starfish +sparkle +server +samsam +qweqwe +public +pass1234 +neptune +marian +krishna +kkkkkk +jungle +cinnamon +bitches +741852 +trojan +theresa +sweetheart +speaker +salmon +powers +pizza +overlord +michaela +meredith +masters +lindsey +history +farmer +express +escape +cuddles +carson +candy +buttercup +brownie +broken +abc12345 +aardvark +Passw0rd +141414 +124578 +123789 +12345678910 +00000 +universal +trinidad +tobias +thursday +surfing +stuart +stinky +standard +roller +porter +pearljam +mobile +mirage +markus +loulou +jjjjjj +herbert +grace +goldie +frosty +fighter +fatima +evelyn +eagle +desire +crimson +coconut +cheryl +beavis +anonymous +andres +africa +134679 +whiskey +velvet +stormy +springer +soldier +ragnarok +portland +oranges +nobody +nathalie +malibu +looking +lemonade +lavender +hitler +hearts +gotohell +gladiator +gggggg +freckles +fashion +david1 +crusader +cosmos +commando +clover +clarence +center +cadillac +brooks +bronco +bonita +babylon +archer +alexandre +123654789 +verbatim +umbrella +thanks +sunny +stalker +splinter +sparrow +selena +russia +roberts +register +qwert123 +penguins +panda +ncc1701d +miracle +melvin +lonely +lexmark +kitkat +julie +graham +frances +estrella +downtown +doodle +deborah +cooler +colombia +chemistry +cactus +bridge +bollocks +beetle +anastasia +741852963 +69696969 +unique +sweets +station +showtime +sheena +santos +rock +revolution +reading +qwerasdf +password2 +mongoose +marlene +maiden +machine +juliet +illusion +hayden +fabian +derrick +crazy +cooldude +chipper +bomber +blonde +bigred +amazing +aliens +abracadabra +123qweasd +wwwwww +treasure +timber +smith +shelly +sesame +pirates +pinkfloyd +passwords +nature +marlin +marines +linkinpark +larissa +laptop +hotrod +gambit +elvis +education +dustin +devils +damian +christy +braves +baller +anarchy +white +valeria +underground +strong +poopoo +monalisa +memory +lizzie +keeper +justdoit +house +homer +gerard +ericsson +emily +divine +colleen +chelsea1 +cccccc +camera +bonbon +billie +bigfoot +badass +asterix +anna +animals +andy +achilles +a1s2d3f4 +violin +veronika +vegeta +tyler +test1234 +teddybear +tatiana +sporting +spartan +shelley +sharks +respect +raven +pentium +papillon +nevermind +marketing +manson +madness +juliette +jericho +gabrielle +fuckyou2 +forgot +firewall +faith +evolution +eric +eduardo +dagger +cristian +cavalier +canadian +bruno +blowjob +blackie +beagle +admin123 +010101 +together +spongebob +snakes +sherman +reddog +reality +ramona +puppies +pedro +pacific +pa55w0rd +omega +noodle +murray +mollie +mister +halflife +franco +foster +formula1 +felix +dragonball +desiree +default +chris1 +bunny +bobcat +asdf123 +951753 +5555 +242424 +thirteen +tattoo +stonecold +stinger +shiloh +seattle +santana +roger +roberta +rastaman +pickles +orion +mustang1 +felicia +dracula +doggie +cucumber +cassidy +britney +brianna +blaster +belinda +apple1 +753951 +teddy +striker +stevie +soleil +snake +skateboard +sheridan +sexsex +roxanne +redman +qqqqqqqq +punisher +panama +paladin +none +lovelife +lights +jerry +iverson +inside +hornet +holden +groovy +gretchen +grandma +gangsta +faster +eddie +chevelle +chester1 +carrot +cannon +button +administrator +a +1212 +zxc123 +wireless +volleyball +vietnam +twinkle +terror +sandiego +rose +pokemon1 +picture +parrot +movies +moose +mirror +milton +mayday +maestro +lollypop +katana +johanna +hunting +hudson +grizzly +gorgeous +garbage +fish +ernest +dolores +conrad +chickens +charity +casey +blueberry +blackman +blackbird +bill +beckham +battle +atlantic +wildfire +weasel +waterloo +trance +storm +singapore +shooter +rocknroll +richie +poop +pitbull +mississippi +kisses +karen +juliana +james123 +iguana +homework +highland +fire +elliot +eldorado +ducati +discover +computer1 +buddy1 +antonia +alphabet +159951 +123456789a +1123581321 +0123456 +zaq1xsw2 +webmaster +vagina +unreal +university +tropical +swimmer +sugar +southpark +silence +sammie +ravens +question +presario +poiuytrewq +palmer +notebook +newman +nebraska +manutd +lucas +hermes +gators +dave +dalton +cheetah +cedric +camilla +bullseye +bridget +bingo +ashton +123asd +yahoo +volume +valhalla +tomorrow +starlight +scruffy +roscoe +richard1 +positive +plymouth +pepsi +patrick1 +paradox +milano +maxima +loser +lestat +gizmo +ghetto +faithful +emerson +elliott +dominique +doberman +dillon +criminal +crackers +converse +chrissy +casanova +blowme +attitude +66666666 +181818 +12345a +098765 +zipper +xfiles +wonderful +weather +utopia +tsunami +stars +shogun +shit +seven +scooter1 +scoobydoo +rochelle +qazqaz +qaz123 +punkrock +onelove +nokia +nicola +moomoo +monkeys +messenger +marco +lobster +kentucky +john316 +jake +insomnia +hooligan +hawkeye +gertrude +freaky +eleanor +capricorn +blueeyes +blackberry +blablabla +balance +anita +allen +aaron +6969 +tiger1 +texas +terminal +snowflake +sirius +sanders +safety +revenge +raphael +poseidon +paranoid +noodles +money1 +minerva +mastermind +light +library +laurence +jersey +istanbul +guest +ghost +games +frederic +forrest +ffffff +doomsday +dancing +courage +chronic +chanel +bradford +bonehead +blacky +apollo13 +answer +alessandro +accord +aaaaaaa +westwood +warning +supernova +strider +satan666 +reynolds +qazwsx123 +q1w2e3r4t5 +penis +number +mookie +monroe +megaman +mckenzie +magician +larry +kipper +jellybean +jayjay +jamie +innocent +hotstuff +hooters +hershey +gremlin +fusion +fountain +foobar +flyers +flames +firefox +death +deadman +daddy +cupcake +concrete +charly +charger +chaos +chacha +cartoon +capslock +boobies +bloody +aussie +april +abcd +tracey +susan +sultan +snuggles +rommel +promise +professor +pontiac +nellie +misty +mermaid +megadeth +medicine +lisa +lionheart +lennon +laurie +kelvin +jackson1 +intrepid +horizon +highlander +hassan +green123 +goodman +geoffrey +francisco +fossil +exodus +dynamite +delta +columbus +cobra +cinderella +chemical +chargers +burger +blues +blossom +bigmac +banshee +amazon +aaaa +13579 +young +vertigo +username +tootsie +theone +tabitha +superman1 +subaru +stone +sherwood +shark +secure +sailing +pisces +picard +nick +natural +moonbeam +meowmeow +maxine +matthias +matilda +llllll +kickass +kenny +kansas +josephine +jeff +jacob +jackson5 +incubus +honolulu +free +eileen +edwards +dream +diamond1 +desmond +crawford +claude +carina +brown +broadway +benny +bear +backspace +assman +asdfjkl +asdasdasd +alpha1 +555666 +zzzzzzzz +woody +whocares +whisper +watermelon +svetlana +southern +sommer +someone +rocky1 +qwertz +president +pleasure +pimpin +painter +nikki +nguyen +myname +missy +mellon +makaveli +journey +jeanne +honeybee +gothic +goodbye +francois +eureka +cindy +chicken1 +bryant +bright +bookworm +bob +PASSWORD +456456 +33333333 +woodstock +wendy +tuesday +trunks +titans +sunlight +stallion +smoke +seven7 +sally +redneck +randy +quality +naughty +mohamed +katie +kathryn +katerina +jefferson +jackpot +international +hidden +hellokitty +hedgehog +happyday +grumpy +frederick +fortune +fallen +demon +davidson +dangerous +clement +cerberus +carol +candle +blackcat +biology +beloved +arsenal1 +annie +angel123 +abraham +aaaaa +171717 +10101010 +0000000 +zigzag +yolanda +typhoon +turbo +training +smooth +rodrigo +roadrunner +republic +recovery +patriot +pacman +molly1 +maradona +lollol +legion +keith +jennie +javier +intruder +hermione +health +hastings +granny +goldstar +fredfred +fiesta +federico +everton +escort +eleven +deftones +cyclone +commander +chuck +chevrolet +butler +blackout +billabong +bigtits +bennett +alexia +abc +789789 +454545 +1234567a +1234554321 +yoyoyo +yesterday +wolfpack +thunder1 +tacobell +sweetness +spyder +solution +shanghai +satellite +sabine +rusty +rootbeer +romance +pikachu +phillips +parola +oakley +nancy +mystic +mulder +morning +monsters +melinda +megan +maximum +mary +marissa +love123 +lorena +lonewolf +krista +kirsten +keystone +kendall +johannes janine jackal -iforgot -hallo -fatass -deadhead -abc12 -zxcv1234 -willy -stud -slappy -roberts -rescue -porkchop -noodles -nellie -mypass -mikey -marvel -laurie -grateful -fuck_inside -formula1 -Dragon -cxfcnmt -bridget -aussie -asterix -a1s2d3f4 -23232323 -123321q -veritas -spankme -shopping -roller -rogers -queen -peterpan -palace -melinda -martinez -lonely -kristi -justdoit -goodtime -frances -camel -beckham -atomic -alexandra -active -223344 -vanilla -thankyou -springer -sommer -Software -sapphire -richmond -printer -ohyeah -massive -lemons -kingston -granny -funfun -evelyn -donnie -deanna -brucelee -bosco -aggies -313131 -wayne -thunder1 -throat -temple -smudge -qqqq -qawsedrf -plymouth -pacman -myself -mariners -israel -hitler -heather1 -faith -Exigent -clancy -chelsea1 -353535 -282828 -123456qwerty -tobias -tatyana -stuff -spectrum -sooner -shitty -sasha1 -pooh -pineappl -mandy -labrador -kisses -katrin -kasper -kaktus -harder -eduard -dylan -dead -chloe -astros -1234567890q -10101010 -stephanie -satan -hudson -commando -bones -bangkok -amsterdam -1959 -webmaster -valley -space -southern -rusty1 -punkin -napass -marian -magnus -lesbians -krishna -hungry -hhhhhh -fuckers -fletcher -content -account -906090 -thompson -simba -scream -q1q1q1 -primus -Passw0rd -mature -ivanov -husker -homerun -esther -ernest -champs -celtics -candyman -bush -boner -asian -aquarius -33333 -zxcv -starfish -pics -peugeot -painter -monopoly -lick -infiniti -goodbye -gangbang -fatman -darling -celine -camelot -boat -blackjac -barkley -area51 -8J4yE3Uz -789654 -19871987 -0000000000 -vader -shelley -scrappy -sarah1 -sailboat -richard1 -moloko -method -mama -kyle -kicker -keith -judith -john316 -horndog -godsmack -flyboy -emmanuel -drago -cosworth -blake -19891989 -writer -usa123 -topdog -timmy -speaker -rosemary -pancho -night -melody -lightnin -life -hidden -gator -farside -falcons -desert -chevrole -catherin -carolyn -bowler -anders -666777 -369369 -yesyes -sabbath -qwerty123456 -power1 -pete -oscar1 -ludwig -jammer -frontier -fallen -dance -bryan -asshole1 -amber1 -aaa111 -123457 -01011991 -terror -telefon -strong -spartans -sara -odessa -luckydog -frank1 -elijah -chang -center -bull -blacks -15426378 -132435 -vivian -tanya -swingers -stick -snuggles -sanchez -redbull -reality -qwertyuio -qwert123 -mandingo -ihateyou -hayden -goose -franco -forrest -double -carol -bohica -bell -beefcake -beatrice -avenger -andrew1 -anarchy -963852 -1366613 -111111111 -whocares -scooter1 -rbhbkk -matilda -labtec -kevin1 -jojo -jesse -hermes -fitness -doberman -dawg -clitoris -camels -5555555555 -1957 -vulcan -vectra -topcat -theking -skiing -nokia -muppet -moocow -leopard -kelley -ivan -grover -gjkbyf -filter -elvis1 -delta1 -dannyboy -conrad -children -catcat -bossman -bacon -amelia -alice -2222222 -viktoria -valhalla -tricky -terminator -soccer1 -ramona -puppy -popopo -oklahoma -ncc1701a -mystic -loveit -looker -latin -laptop -laguna -keystone -iguana -herbie -cupcake -clarence -bunghole -blacky -bennett -bart -19751975 -12332 -000007 -vette -trojans -today -romashka -puppies -possum -pa55word -oakley -moneys -kingpin -golfball -funny -doughboy -dalton -crash -charlotte -carlton -breeze -billie -beast -achilles -tatiana -studio -sterlin -plumber -patrick1 -miles -kotenok -homers -gbpltw -gateway1 -franky -durango -drake -deeznuts -cowboys1 -ccbill -brando -9876543210 -zzzz -zxczxc -vkontakte -tyrone -skinny -rookie -qwqwqw -phillies -lespaul -juliet -jeremiah -igor -homer1 -dilligaf -caitlin -budman -atlantic -989898 -362436 -19851985 -vfrcbvrf -verona -technics -svetik -stripper -soleil -september -pinkfloy -noodle -metal -maynard -maryland -kentucky -hastings -gang -frederic -engage -eileen -butthole -bone -azsxdc -agent007 -474747 -19911991 -01011985 -triton -tractor -somethin -snow -shane -sassy -sabina -russian -porsche9 -pistol -justine -hurrican -gopher -deadman -cutter -coolman -command -chase -california -boris -bicycle -bethany -bearbear -babyboy -73501505 -123456k -zvezda -vortex -vipers -tuesday -traffic -toto -star69 -server -ready -rafael -omega1 -nathalie -microlab -killme -jrcfyf -gizmo1 -function -freaks -flamingo -enterprise -eleven -doobie -deskjet -cuddles -church -breast -19941994 -19781978 -1225 -01011970 -vladik -unknown -truelove -sweden -striker -stoner -sony -SaUn -ranger1 -qqqqq -pauline -nebraska -meatball -marilyn -jethro -hammers -gustav -escape -elliot -dogman -chair -brothers -boots -blow -bella -belinda -babies -1414 -titties -syracuse -river -polska -pilot -oilers -nofear -military -macdaddy -hawk -diamond1 -dddd -danila -central -annette -128500 -zxcasd -warhammer -universe -splash -smut -sentinel -rayray -randall -Password1 -panda -nevada -mighty -meghan -mayday -manchest -madden -kamikaze -jennie -iloveyo -hustler -hunter1 -horny1 -handsome -dthjybrf -designer -demon -cheers -cash -cancel -blueblue -bigger -australia -asdfjkl -321654987 -1qaz1qaz -1955 -1234qwe -01011981 -zaphod -ultima -tolkien -Thomas -thekid -tdutybq -summit -select -saint -rockets -rhonda -retard -rebel -ralph -poncho -pokemon1 -play -pantyhos -nina -momoney -market -lickit -leader -kong -jenna -jayjay -javier -eatpussy -dracula -dawson -daniil -cartoon -capone -bubbas -789123 -19861986 -01011986 -zxzxzx -wendy -tree -superstar -super1 -ssssssss -sonic -sinatra -scottie -sasasa -rush -robert1 -rjirfrgbde -reagan -meatloaf -lifetime -jimmy1 -jamesbon -houses -hilton -gofish -charmed -bowser -betty -525252 -123456789z -1066 -woofwoof -Turkey50 -santana -rugby -rfnthbyf -miracle -mailman -lansing -kathryn -Jennifer -giant -front242 -firefox -check -boxing -bogdan -bizkit -azamat -apollo13 -alan -zidane -tracy -tinman -terminal -starbuck -redhot -oregon -memory -lewis -lancelot -illini -grandma -govols -gordon24 -giorgi -feet -fatima -crunch -creamy -coke -cabbage -bryant -brandon1 -bigmoney -azsxdcfv -3333333 -321123 -warlord -station -sayang -rotten -rightnow -mojo -models -maradona -lololo -lionking -jarhead -hehehe -gary -fast -exodus -crazybab -conner -charlton -catman -casey1 -bonita -arjay -19931993 -19901990 -1001 -100000 -sticks -poiuytrewq -peters -passwort -orioles -oranges -marissa -japanese -holyshit -hohoho -gogo -fabian -donna -cutlass -cthulhu -chewie -chacha -bradford -bigtime -aikido -4runner -21212121 -150781 -wildfire -utopia -sport -sexygirl -rereirf -reebok -raven1 -poontang -poodle -movies -microsof -grumpy -eeyore -down -dong -chocolate -chickens -butch -arsenal1 -adult -adriana -19831983 -zzzzz -volley -tootsie -sparkle -software -sexx -scotch -science -rovers -nnnnnn -mellon -legacy -julius -helen -happyday -fubar -danie -cancun -br0d3r -beverly -beaner -aberdeen -44444 -19951995 -13243546 -123456aa -wilbur -treasure -tomato -theodore -shania -raiders1 -natural -kume -kathy -hamburg -gretchen -frisco -ericsson -daddy1 -cosmo -condom -comics -coconut -cocks -Check -camilla -bikini -albatros -1Passwor -1958 -1919 -143143 -0.0.0.000 -zxcasdqwe -zaqxsw -whisper -vfvekz -tyler1 -Sojdlg123aljg -sixers -sexsexsex -rfhbyf -profit -okokok -nancy -mikemike -michaela -memorex -marlene -kristy -jose -jackson1 -hope -hailey -fugazi -fright -figaro -excalibu -elvira -dildo -denali -cruise -cooter -cheng -candle -bitch1 -attack -armani -anhyeuem -78945612 -222333 -zenith -walleye -tsunami -trinidad -thomas1 -temp -tammy -sultan -steve1 -slacker -selena -samiam -revenge -pooppoop -pillow -nobody -kitty1 -killer1 -jojojo -huskies -greens -greenbay -greatone -fuckin -fortuna -fordf150 -first -fashion -fart -emerson -davis -cloud9 -china -boob -applepie -alien -963852741 -321456 -292929 -1998 -1956 -18436572 -tasha -stocks -rustam -rfrnec -piccolo -orgasm -milana -marisa -marcos -malaka -lisalisa -kelly1 -hithere -harley1 -hardrock -flying -fernand -dinosaur -corrado -coleman -clapton -chief -bloody -anfield -636363 -420247 -332211 -voyeur -toby -texas1 -surf -steele -running -rastaman -pa55w0rd -oleg -number1 -maxell -madeline -keywest -junebug -ingrid -hollywood -hellyeah -hayley -goku -felicia -eeeeee -dicks -dfkthbz -dana -daisy1 -columbus -charli -bonsai -billy1 -aspire -9999999 -987987 -50cent -000001 -xxxxxxx -wolfie -viagra -vfksirf -vernon -tang -swimmer -subway -stolen -sparta -slutty -skywalker -sean -sausage -rockhard -ricky -positive -nyjets -miriam -melissa1 -krista -kipper -kcj9wx5n -jedi -jazzman -hyperion -happy123 -gotohell -garage -football1 -fingers -february -faggot -easy -dragoon -crazy1 -clemson -chanel -canon -bootie -balloon -abc12345 -609609609 -456321 -404040 -162534 -yosemite -slider -shado -sandro -roadkill -quincy -pedro -mayhem -lion -knopka -kingfish -jerkoff +horse hopper -everest -ddddddd -damnit -cunts -chevy1 -cheetah -chaser -billyboy -bigbird -bbbb -789987 -1qa2ws3ed -1954 -135246 -123789456 -122333 -1000 -050505 -wibble -valeria -tunafish -trident -thor -tekken -tara -starship -slave -saratoga -romance -robotech -rich -rasputin -rangers1 -powell -poppop -passwords -p0015123 -nwo4life -murder -milena -midget -megapass -lucky13 -lolipop -koshka -kenworth -jonjon -jenny1 -irish1 -hedgehog -guiness -gmoney -ghetto -fortune -emily1 -duster -ding -davidson -davids -dammit -dale -crysis -bogart -anaconda -alibaba -airbus -7753191 -515151 -20102010 -200000 -123123q -12131415 -10203 -work -wood -vladislav -vfczyz -tundra -Translator -torres -splinter -spears -richards -rachael -pussie -phoenix1 -pearl -monty -lolo -lkjhgf -leelee -karolina -johanna -jensen -helloo -harper -hal9000 -fletch -feather -fang -dfkthf -depeche -barsik -789789789 -757575 -727272 -zorro -xtreme -woman -vitalik -vermont -train -theboss -sword -shearer -sanders -railroad -qwer123 -pupsik -pornos -pippen -pingpong -nikola -nguyen -music1 -magicman -killbill -kickass -kenshin -katie1 -juggalo -jayhawk -java -grapes -fritz -drew -divine -cyclops -critter -coucou -cecilia -bristol -bigsexy -allsop -9876 -1230 -01011989 -wrestlin -twisted -trout -tommyboy -stefano -song -skydive -sherwood -passpass -pass1234 -onlyme -malina -majestic -macross -lillian -heart -guest -gabrie -fuckthis -freeporn -dinamo -deborah -crawford -clipper -city -better -bears -bangbang -asdasdasd -artemis -angie -admiral -2003 -020202 -yousuck -xbox360 -werner -vector -usmc -umbrella -tool -strange -sparks -spank -smelly -small -salvador -sabres -rupert -ramses -presto -pompey -operator -nudist -ne1469 -minime -matador -love69 -kendall -jordan1 -jeanette -hooter -hansen -gunners -gonzo -gggggggg -fktrcfylhf -facial -deepthroat -daniel1 -dang -cruiser -cinnamon -cigars -chico -chester1 -carl -caramel -calico -broadway -batman1 -baddog -778899 -2128506 -123456r -0420 -01011988 -z1x2c3 -wassup -wally -vh5150 -underdog -thesims -thecat -sunnyday -snoopdog -sandy1 -pooter -multiplelo -magick -library -kungfu -kirsten -kimber -jean -jasmine1 -hotshot -gringo -fowler -emma -duchess -damage -cyclone -Computer -chong -chemical -chainsaw -caveman -catherine -carrera -canadian -buster1 -brighton -back -australi -animals -alliance -albion -969696 -555777 -19721972 -19691969 -1024 -trisha -theresa -supersta -steph -static -snowboar -sex123 -scratch -retired -rambler -r2d2c3po -quantum -passme -over -newbie -mybaby -musica -misfit -mechanic -mattie -mathew -mamapapa -looser -jabroni -isaiah -heyhey -hank -hang -golfgolf -ghjcnjnfr -frozen -forfun -fffff -downtown -coolguy -cohiba -christopher -chivas -chicken1 -bullseye -boys -bottle -bob123 -blueboy -believe -becky -beanie -20002000 -yzerman -west -village -vietnam -trader -summer1 -stereo -spurs -solnce -smegma -skorpion -saturday -samara -safari -renault -rctybz -peterson -paper -meredith -marc -louis -lkjhgfdsa -ktyjxrf -kill -kids -jjjj -ivanova -hotred -goalie -fishes -eastside -cypress -cyber -credit -brad -blackhaw -beastie -banker -backdoor -again -192837 -112211 -westwood -venus -steeler -spawn -sneakers -snapple -snake1 -sims -sharky -sexxxx -seeker -scania -sapper -route66 -Robert -q123456 -Passwor1 -mnbvcx -mirror -maureen -marino13 -jamesbond -jade -horizon -haha -getmoney -flounder -fiesta -europa -direct -dean -compute -chrono -chad -boomboom -bobby1 -bing -beerbeer -apple123 -andres -8888888 -777888 -333666 -1357 -12345z -030303 -01011987 -01011984 -wolf359 -whitey -undertaker -topher -tommy1 -tabitha -stroke -staples -sinclair -silence -scout -scanner -samsung1 -rain -poetry -pisces -phil -peter1 -packer -outkast -nike -moneyman -mmmmmmmm -ming -marianne -magpie -love123 -kahuna -jokers -jjjjjjjj -groucho -goodman -gargoyle -fuckher -florian -federico -droopy -dorian -donuts -ddddd -cinder -buttman -benny -barry -amsterda -alfa -656565 -1x2zkg8w -19881988 -19741974 -zerocool -walrus -walmart -vfvfgfgf -user -typhoon -test1234 -studly -Shadow -sexy69 -sadie1 -rtyuehe -rosie -qwert1 -nipper -maximum -klingon -jess -idontknow -heidi -hahahaha -gggg -fucku2 -floppy -flash1 -fghtkm -erotica -erik -doodoo -dharma -deniska -deacon -daphne -daewoo -dada -charley -cambiami -bimmer -bike -bigbear -alucard -absolut -a123456789 -4121 -19731973 -070707 -03082006 -02071986 -vfhufhbnf -sinbad -secret1 -second -seamus -renee -redfish -rabota -pudding -pppppppp -patty -paint -ocean -number -nature -motherlode -micron -maxx -massimo -losers -lokomotiv -ling -kristine -kostya -korn -goldstar -gegcbr -floyd -fallout -dawn -custom -christina -chrisbln -button -bonkers -bogey -belle -bbbbb -barber -audia4 -america1 -abraham -585858 -414141 -336699 -20012001 -12345678q -0123 -whitesox -whatsup -usnavy -tuan -titty -titanium -thursday -thirteen -tazmania -steel -starfire -sparrow -skidoo -senior -reading -qwerqwer -qazwsx12 -peyton -panasoni -paintbal -newcastl -marius -italian -hotpussy -holly1 -goliath -giuseppe -frodo -fresh -buckshot -bounce -babyblue -attitude -answer -90210 -575757 -10203040 -1012 -01011910 -ybrjkfq -wasser -tyson -Superman -sunflowe -steam -ssss -sound -solution -snoop -shou -shawn -sasuke -rules -royals -rivers -respect -poppy -phillips -olivier -moose1 -mondeo -mmmm -knickers -hoosier -greece -grant -godfather -freeze -europe -erica -doogie -danzig -dalejr -contact -clarinet -champ -briana -bluedog -backup -assholes -allmine -aaliyah -12345679 -100100 -zigzag -whisky -weaver -truman -tomorrow -tight -theend -start -southpark -sersolution -roberta -rhfcjnrf -qwerty1234 -quartz -premier -paintball -montgom240 -mommy -mittens -micheal -maggot -loco -laurel -lamont -karma -journey -johannes -intruder -insert -hairy -hacked -groove -gesperrt -francois -focus -felipe -eternal -edwards -doug -dollars -dkflbckfd -dfktynbyf -demons -deejay -cubbies -christie -celeron -cat123 -carbon -callaway -bucket -albina -2004 -19821982 -19811981 -1515 -12qw34er -123qwerty -123aaa -10101 -1007 -080808 -zeus -warthog -tights -simona -shun -salamander -resident -reefer -racer -quattro -public -poseidon -pianoman -nonono -michell -mellow -luis -jillian -havefun -gunnar -goofy -futbol -fucku -eduardo -diehard -dian -chuckles -carla -carina -avalanch -artur -allstar -abc1234 -abby -4545 -1q2w3e4r5 -125125 -123451 -ziggy -yumyum -working -what -wang -wagner -volvo -ufkbyf -twinkle -susanne -superman1 -sunshin -strip -searay -rockford -radio -qwertyqwerty -proxy -prophet -ou8122 -oasis -mylife -monke -monaco -meowmeow -meathead -Master -leanne -kang -joyjoy -joker1 -filthy -emmitt -craig -cornell -changed -cbr600 -builder -budweise -boobie -bobobo -biggles -bigass -bertie -amanda1 -a1s2d3 -784512 -767676 -235689 -1953 -19411945 -14725836 -11223 -01091989 -01011992 -zero -vegas -twins -turbo1 -triangle -thongs -thanatos -sting -starman -spike1 -smokes -shai -sexyman -sex -scuba -runescape -phish -pepper1 -padres -nitram -nickel -napster -lord -jewels -jeanne -gretzky -great1 -gladiator -crjhgbjy -chuang -chou -blossom -bean -barefoot -alina -787898 -567890 -5551212 -25252525 -02071982 -zxcvbnm1 -zhong -woohoo -welder -viewsonic -venice -usarmy -trial -traveler -together -team -tango -swords -starter -sputnik -spongebob -slinky -rover -ripken -rasta -prissy -pinhead -papa -pants -original -mustard -more -mohammed -mian -medicine -mazafaka -lance -juliette -james007 -hawkeyes -goodboy -gong -footbal -feng -derek -deeznutz -dante -combat -cicero -chun -cerberus -beretta -bengals -beaches -3232 -135792468 -12345qwe -01234567 -01011975 -zxasqw12 -xxx123 -xander -will -watcher -thedog -terrapin -stoney -stacy -something -shang -secure -rooney -rodman -redwing -quan -pony -pobeda -pissing -philippe -overkill -monalisa -mishka -lions -lionel -leonid -krystal -kosmos -jessic -jane -illusion -hoosiers -hayabusa -greene -gfhjkm123 -games -francesc -enter1 +gustavo +grateful +figaro +easter +dublin +donovan +continue confused -cobra1 -clevelan -cedric -carole -busted -bonbon -barrett -banane -badgirl +condor +chubby +chase +caramel +bubba1 +brighton +blades +bethany +asdzxc antoine -7779311 -311311 -2345 -187187 -123456s -123456654321 -1005 -0987 -01011993 -zippy -zhei -vinnie -tttttttt -stunner -stoned -smoking -smeghead -sacred -redwood -Pussy1 -moonlight -momomo -mimi -megatron -massage -looney -johnboy -janet -jagger -jacob1 -hurley -hong -hihihi -helmet -heckfy -hambone -gollum -gaston -f**k -death1 -Charlie -chao -cfitymrf -casanova -brent -boricua -blackjack -blablabla -bigmike -bermuda -bbbbbbbb -bayern -amazing -aleksey -717171 -12301230 -zheng -yoyo -wildman -tracker -syncmaster -sascha -rhiannon -reader -queens -qing -purdue -pool -poochie -poker -petra +135790 +0000000000 +yankees1 +triangle +shaman +shadow1 +sander +romeo +pippin +peterson person -orchid -nuts -nice -lola -lightning -leng -lang -lambert -kashmir -jill -idiot -honey1 -fisting -fester -eraser -diao -delphi -dddddddd -cubswin -cong -claudio -clark -chip -buzzard -buzz -butts -brewster -bravo -bookworm -blessing -benfica -because -babybaby -aleksandra -6666666 -1997 -19961996 -19791979 -1717 -1213 -02091987 -02021987 -xiao -wild -valencia -trapper -tongue -thegreat -sancho -really -rainman -piper -peng -peach -passwd -packers1 -newpass6 -neng -mouse1 -motley -morning -midway -Michelle -miao -maste -marin -kaylee -justin1 -hokies -health -glory -five -dutchess -dogfood -comet -clouds -cloud -charles1 -buddah -bacardi -astrid -alphabet -adams -19801980 -147369 -12qwas -02081988 -02051986 -02041986 -02011985 -01011977 -xuan -vedder -valeri -teng -stumpy -squash -snapon -site -ruan -roadrunn -rjycnfynby -rhtdtlrj -rambo -pizzas -paula -novell -mortgage -misha -menace -maxim -lori -kool -hanna -gsxr750 -goldwing -frisky -famous -dodge1 -dbrnjh -christmas -cheese1 -century -candice -booker -beamer -assword -army -angus -andromeda -adrienne -676767 -543210 -2010 -1369 -12345678a -12011987 -02101985 -02031986 -02021988 -zhuang -zhou -wrestling -tinkerbell -thumbs -thedude -teddybea -sssss -sonics -sinister -shannon1 -satana -sang -salomon -remote -qazzaq -playing -piao -pacers -onetime -nong -nikolay -motherfucker -mortimer -misery -madison1 -luan -lovesex -look -Jessica -handyman -hampton -gromit -ghostrider -doghouse -deluxe -clown -chunky -chuai -cgfhnfr -brewer -boxster -balloons -adults -a1a1a1 -794613 -654123 -24682468 -2005 -1492 -1020 -1017 -02061985 -02011987 -***** -zhun -ying -yang -windsor -wedding -wareagle -svoboda -supreme -stalin -sponge -simon1 -roadking -ripple -realmadrid -qiao -PolniyPizdec0211 -pissoff -peacock -norway -nokia6300 -ninjas -misty1 -medusa -medical -maryann -marika -madina -logan1 -lilly -laser -killers -jiang -jaybird -jammin -intel -idontkno -huai -harry1 -goaway -gameover -dino -destroy -deng +moon +marianna +maniac +mandrake +isaiah +inuyasha +home +hardware +goblin +french +freebird +florian +ferguson +dorian +dominick +dick +carolyn +bullfrog +bruce +babylon5 +avenger +13131313 +zanzibar +tricky +transfer +television +sparkles +space +silent +shepherd +search +resident +puppy +property +pictures +piccolo +oooooo +mischief +me +mathew +marcelo +magical +macintosh +logan +lionel +laguna +kristian +kissmyass +jones +iforgot +hurricane +hoover +herbie +heineken +hahahaha +goforit +fuckit +eastside +dude +daffodil collin -claymore -chicago1 -cheater -chai -bunny1 -blackbir -bigbutt -bcfields -athens -antoni -abcd123 -686868 -369963 -1357924680 -12qw12 -1236987 -111333 -02091986 -02021986 -01011983 -000111 -zhuai -yoda -xiang -wrestle +charming +billybob +bigman +attila +aspire +artemis +armstrong +adventure +adelaide +zenith +underdog +time +temple +technics +sweden +subway +sinner +sara +samsung1 +sabina +rooney +remote +qwerty1234 +python +pillow +phoenix1 +passwd +musicman +murder +metal +market +marjorie +linkin +letmein1 +kingpin +jesse +jerusalem +ingrid +information +iloveyou1 +hospital +handball +gopher +gonzales +fortuna +flying +fitness +dylan +dilbert +desert +darkangel +clouds +cascade +camelot +budapest +brandon1 +boss +batista +armando +angie +alliance +alibaba +adrienne +aberdeen +abc123456 +1234512345 +zephyr +wonderland +willis +ultima +triton +thuglife +studio +squirt +splash +sentinel +richards +redrose +rammstein +quincy +queen +project +penny +pearl +oakland +newyork1 +mortimer +micheal +marcello +magazine +luther +jumper +josh +infantry +impala +hopeless +holmes +harrypotter +glitter +fandango +falcons +edison +eagle1 +donna +deadhead +clarissa +christie +chico +charlene +blade +billyboy +bangbang +bamboo +asasas +ariana +absolute +50cent +waters +trucker +titanium +tiger123 +supreme +superior +stefanie +sparks +spaceman +somebody +smudge +sleepy +sinclair +secrets +scrappy +rubber +ricky +pppppppp +poland +pink +paintball +ninja +newlife +nevada +mmmmmmmm +military +medical +marijuana +mackenzie +loveless +louis +lolipop +lilian +lighthouse +lewis +lassie +kristy +knights +karolina +jillian +jesuschrist +jensen +heart +grover +fernanda +felicity +dietcoke +coolio +cleveland +chevy +callie +bryan +brewster +biggie +bessie +bertha +babyblue +ashleigh +andrei +abcde +8888 +852456 +321654 +1q2w3e4r5t6y +14789632 +012345 +willy whiskers valkyrie -toon -tong -ting -talisman -starcraf -sporting -spaceman -southpar -smiths -skate -shell -seng -saleen -ruby -reng -redline -rancid -pepe -optimus -nova -mohamed -meister -marcia -lipstick -kittykat -jktymrf -jenn -jayden -inuyasha -higgins -guai -gonavy -face -eureka -dutch -darkman -courage -cocaine -circus -cheeks -camper -br549 -bagira -babyface -7uGd5HIp2J -5050 -1qaz2ws -123321a -02081987 -02081984 -02061986 -02021984 -01011982 -zhai -xiong -willia -vvvvvv -venera -unique -tian -sveta +triumph +tracker +superfly strength -stories -squall -secrets -seahawks -sauron -ripley -riley -recovery -qweqweqwe -qiong -puddin -playstation -pinky -phone -penny1 -nude -mitch -milkman -mermaid -max123 -maria1 -lust -loaded -lighter -lexus -leavemealone -just4me -jiong -jing -jamie1 -india -hardcock -gobucks -gawker -fytxrf -fuzzy -florida1 -flexible -eleanor -dragonball -doudou -cinema -checkers -charlene -ceng -buffy1 -brian1 -beautifu -baseball1 -ashlee -adonis -adam12 -434343 -02031984 -02021985 -xxxpass -toledo -thedoors -templar -sullivan -stanford -shei -sander -rolling -qqqqqqq -pussey -pothead -pippin -nimbus -niao -mustafa -monte -mollydog -modena -mmmmm -michae -meng -mango -mamama -lynn -love12 -kissing -keegan -jockey -illinois -ib6ub9 -hotbox -hippie -hill -ghblehjr -gamecube -ferris -diggler -crow -circle -chuo -chinook -charity -carmel -caravan -cannabis -cameltoe -buddie -bright -bitchass -bert -beowulf -bartman -asia -armagedon -ariana -alexalex -alenka -ABC123 -987456321 -373737 -2580 -21031988 -123qq123 -12345t -1234567890a -123455 -02081989 -02011986 -01020304 -01011999 -xyz123 -xerxes -wraith -wishbone -warning -todd -ticket -three -subzero -shuang -rong -rider -quest -qiang -pppp -pian -petrov -otto -nuan -ning -myname -matthews -martine -mandarin -magical -latinas -lalalala -kotaku -jjjjj -jeffery -jameson -iamgod -hellos -hassan -Harley -godfathe -geng -gabriela -foryou -ffffffff -divorce -darius -chui -breasts -bluefish -binladen -bigtit -anne -alexia -2727 -19771977 -19761976 -02061989 -02041984 -zhui -zappa -yfnfkmz -weng -tricia -tottenham -tiberius -teddybear -spinner -spice -spectre -solo -silverad -silly -shuo -sherri -samtron -poland -poiuy -pickup -pdtplf -paloma -ntktajy -northern -nasty1 -musashi -missy1 -microphone -meat -manman -lucille -lotus -letter -kendra -iomega -hootie -forward -elite -electron -electra -duan -DRAGON -dotcom -dirtbike -dianne -desiree -deadpool -darrell -cosmic -common -chrome -cathy -carpedie -bilbo -bella1 -beemer -bearcat -bank -ashley1 -asdfzxcv -amateurs -allan -absolute -50spanks -147963 -120676 -1123 -02021983 -zang -virtual -vampires -vadim -tulips -sweet1 -suan -spread -spanish -some -slapper -skylar -shiner -sheng -shanghai -sanfran -ramones -property -pheonix -password2 -pablo -othello -orange1 -nuggets -netscape -ludmila -lost -liang -kakashka -kaitlyn -iscool -huang -hillary -high -hhhh -heater -hawaiian -guang -grease -gfhjkmgfhjkm -gfhjkm1 -fyutkbyf -finance -farley -dogshit -digital1 -crack -counter -corsair -company -colonel -claudi -carolin -caprice -caligula -bulls -blackout -beatle -beans -banzai -banner -artem -9562876 -5656 -1945 -159632 -15151515 -123456qw -1234567891 -02051983 -02041983 -02031987 -02021989 -z1x2c3v4 -xing -vSjasnel12 -twenty -toolman -thing -testpass -stretch -stonecold -soulmate -sonny -snuffy -shutup -shuai -shao -rhino -q2w3e4r5 -polly -poipoi -pierce -piano -pavlov -pang -nicole1 -millions -marsha -lineage2 -liao -lemon -kuai -keller -jimmie -jiao -gregor -ggggg -game -fuckyo -fuckoff1 -friendly -fgtkmcby -evan -edgar -dolores -doitnow -dfcbkbq -criminal -coldbeer -chuckie -chimera -chan -ccccc -cccc -cards -capslock -cang -bullfrog -bonjovi -bobdylan -beth -berger -barker -balance -badman -bacchus -babylove -argentina -annabell -akira -646464 -15975 -1223 -11221122 -1022 -02081986 -02041988 -02041987 -02041982 -02011988 -zong -zhang -yummy -yeahbaby -vasilisa -temp123 -tank -slim -skyler -silent -sergeant -reynolds -qazwsx1 -PUSSY -pasword -nomore -noelle -nicol -newyork1 -mullet -monarch -merlot -mantis -mancity -magazine -llllllll -kinder -kilroy -katherine -jayhawks -jackpot -ipswich -hack -fishing1 -fight -ebony -dragon12 -dog123 -dipshit -crusher -chippy -canyon -bigbig -bamboo -athlon -alisha -abnormal -a11111 -2469 -12365 -1011 -09876543 -02101984 -02081985 -02071984 -02011980 -010180 -01011979 -zhuo -zaraza -wg8e3wjf -triple -tototo -theater -teddy1 -syzygy -susana -sonoma -slavik -shitface -sheba -sexyboy -screen -salasana -rufus -Richard -reds -rebecca1 -pussyman -pringles -preacher -park -oceans -niang -momo -misfits -mikey1 -media -manowar -mack -kayla -jump -jorda -hondas -hollow -here -heineken -halifax -gatorade -gabriell -ferrari1 -fergie -female -eldorado -eagles1 -cygnus -coolness -colton -ciccio -cheech -card -boom -blaze -bhbirf -BASEBALL -barton -655321 -1818 -14141414 -123465 -1224 -1211 -111111a -02021982 -zhao -wings -warner -vsegda -tripod -tiao -thunderb -telephon -tdutybz -talon -speedo -specialk -shepherd -shadows -samsun -redbird -race -promise -persik -patience -paranoid -orient -monster1 -missouri -mets -mazda -masamune -martin1 -marker -march -manning -mamamama -licking -lesley -laurence -jezebel -jetski -hopeless -hooper -homeboy -hole -heynow -forum -foot -ffff -farscape -estrella -entropy -eastwood -dwight -dragonba -door -dododo -deutsch -crystal1 -corleone -cobalt -chopin -chevrolet -cattle -carlitos -buttercu -butcher -bushido -buddyboy -blond -bingo1 -becker -baron -augusta -alex123 -998877 -24242424 -12365478 -02061988 -02031985 -?????? -zuan -yfcntymrf -wowwow -winston1 -vfibyf -ventura -titten -tiburon -thoma -thelma -stroker -snooker -smokie -slippery -shui -shock -seadoo -sandwich -records -rang -puffy -piramida -orion1 -napoli -nang -mouth -monkey12 -millwall -mexican -meme -maxxxx -magician -leon -lala -lakota -jenkins -jackson5 -insomnia -harvard -HARLEY -hardware -giorgio -ginger1 -george1 -gator1 -fountain -fastball -exotic -elizaveta -dialog -davide -channel -castro -bunnies -borussia -asddsa -andromed -alfredo -alejandro -7007 -69696 -4417 -3131 -258852 -1952 -147741 -1234asdf -02081982 -02051982 -zzzzzzz -zeng -zalupa -yong -windsurf -wildcard -weird -violin -universal -sunflower -suicide -strawberry -stepan -sphinx -someone -sassy1 -romano -reddevil -raquel -rachel1 -pornporn -polopolo -pluto -plasma -pinkfloyd -panther1 -north -milo -maxime -matteo -malone -major -mail -lulu -ltybcrf -lena -lassie -july -jiggaman -jelly -islander -inspiron -hopeful -heng -hans -green123 -gore -gooner -goirish -gadget -freeway -fergus -eeeee -diego -dickie -deep -danny1 -cuan -cristian -conover -civic -Buster -bombers -bird33 -bigfish -bigblue -bian -beng -beacon -barnes -astro -artemka -annika -anita -Andrew -747474 -484848 -464646 -369258 -225588 -1z2x3c -1a2s3d4f -123456qwe -02061980 -02031982 -02011984 -zaqxswcde -wrench -washington -violetta -tuning -trainer -tootie -store -spurs1 -sporty -sowhat -sophi -smashing -sleeper -slave1 -sexysexy -seeking -sam123 -robotics -rjhjktdf -reckless -pulsar -project -placebo -paddle -oooo -nightmare -nanook -married -linda1 -lilian -lazarus -kuang -knockers -killkill -keng -katherin -Jordan -jellybea -jayson -iloveme -hunt -hothot -homerj -hhhhhhhh -helene -haggis -goat -ganesh -gandalf1 -fulham -force -dynasty -drakon -download -doomsday -dieter -devil666 -desmond -darklord -daemon -dabears -cramps -cougars -clowns -classics -citizen -cigar -chrysler -carlito -candace -bruno1 -browning -brodie -bolton -biao -barbados -aubrey -arlene -arcadia -amigo -abstr -9293709b13 -737373 -4444444 -4242 -369852 -20202020 -1qa2ws -1Pussy -1947 -1234560 -1112 -1000000 -02091983 -02061987 -01081989 -zephyr -yugioh -yjdsqgfhjkm -woofer -wanted -volcom -verizon -tripper -toaster -tipper -tigger1 -tartar -superb -stiffy -spock -soprano -snowboard -sexxxy -senator -scrabble -santafe -sally1 -sahara -romero -rhjrjlbk -reload -ramsey -rainbow6 -qazwsxedc123 -poopy -pharmacy -obelix -normal -nevermind -mordor -mclaren -mariposa -mari -manuela -mallory -magelan -lovebug -lips -kokoko -jakejake -insanity -iceberg -hughes -hookup -hockey1 -hamish -graphics -geoffrey -firewall -fandango -ernie -dottie -doofus -donovan -domain -digimon -darryl -darlene -dancing -county -chloe1 -chantal -burrito -bummer -bubba69 -brett -bounty -bigcat -bessie -basset -augustus -ashleigh -878787 -3434 -321321321 -12051988 -111qqq -1023 -1013 -05051987 -02101989 -02101987 -02071987 -02071980 -02041985 -titan -thong -sweetnes -stanislav -sssssss -snappy -shanti -shanna -shan -script -scorpio1 -RuleZ -rochelle -rebel1 -radiohea -q1q2q3 -puss -pumpkins -puffin -onetwo -oatmeal -nutmeg -ninja1 -nichole -mobydick -marine1 -mang -lover1 -longjohn -lindros -killjoy -kfhbcf -karen1 -jingle -jacques -iverson3 -istanbul -iiiiii -howdy -hover -hjccbz -highheel -happiness -guitar1 -ghosts -georg -geneva -gamecock -fraser -faithful -dundee -dell -creature -creation -corey -concorde -cleo -cdtnbr -carmex2 -budapest -bronze -brains -blue12 -battery -attila -arrow -anthrax -aloha -383838 -19711971 -1948 -134679852 -123qw -123000 -02091984 -02091981 -02091980 -02061983 -02041981 -01011900 -zhjckfd -zazaza -wingman -windmill -wifey -webhompas -watch -thisisit -tech -submit -stress -spongebo -silver1 -senators -scott1 -sausages -radical -qwer12 -ppppp -pixies -pineapple -piazza -patrice -officer -nygiants -nikitos -nigga -nextel -moses -moonbeam -mihail -MICHAEL -meagan -marcello -maksimka -loveless -lottie -lollypop -laurent -latina -kris -kleopatra -kkkk -kirsty -katarina -kamila -jets -iiii -icehouse -hooligan -gertrude -fullmoon -fuckinside -fishin -everett -erin -dynamite -dupont -dogcat -dogboy -diane -corolla -citadel -buttfuck -bulldog1 -broker -brittney -boozer -banger -aviation -almond -aaron1 -78945 -616161 -426hemi -333777 -22041987 -2008 -20022002 -153624 -1121 -111111q -05051985 -02081977 -02071988 -02051988 -02051987 -02041979 -zander -wwww -webmaste -webber -taylor1 -taxman -sucking -stylus -spoon -spiker -simmons -sergi -sairam -royal -ramrod -radiohead -popper -platypus -pippo -pepito -pavel -monkeybo -Michael1 -master12 -marty -kjkszpj -kidrock -judy -juanita -joshua1 -jacobs -idunno -icu812 -hubert -heritage -guyver -gunther -Good123654 -ghost1 -getout -gameboy -format -festival -evolution -epsilon -enrico -electro -dynamo -duckie -drive -dolphin1 -ctrhtn -cthtuf -cobain -club -chilly -charter -celeb -cccccccc -caught -cascade -carnage -bunker -boxers -boxer -bombay -bigboss -bigben -beerman -baggio -asdf12 -arrows -aptiva -a1a2a3 -a12345678 -626262 -26061987 -1616 -15051981 -08031986 -060606 -02061984 -02061982 -02051989 -02051984 -02031981 -woodland -whiteout -visa -vanguard -towers -tiny -tigger2 -temppass -super12 -stop -stevens -softail -sheriff -robot -reddwarf -pussy123 -praise -pistons -patric -partner -niceguy -morgan1 -model -mars -mariana -manolo -mankind -lumber -krusty -kittens -kirby -june -johann -jared -imation -henry1 -heat -gobears -forsaken -Football -fiction -ferguson -edison -earnhard -dwayne -dogger -diver -delight -dandan -dalshe -cross -cottage -coolcool -coach -camila -callum -busty -british -biology -beta -beardog -baldwin -alone -albany -airwolf -9876543 -987123 -7894561230 -786786 -535353 -21031987 -1949 -13041988 -1234qw -123456l -1215 -111000 -11051987 -10011986 -06061986 -02091985 -02021981 -02021979 -01031988 -vjcrdf -uranus -tiger123 -summer99 -state -starstar -squeeze -spikes -snowflak -slamdunk -sinned -shocker -season -santa -sanity -salome -saiyan -renata -redrose -queenie -puppet -popo -playboy1 -pecker -paulie -oliver1 -ohshit -norwich -news -namaste -muscles -mortal -michael2 -mephisto -mandy1 -magnet -longbow -llll -living -lithium -komodo -kkkkkkkk -kjrjvjnbd -killer12 -kellie -julie1 -jarvis -iloveyou2 -holidays -highway -havana -harvest -harrypotter -gorgeous -giraffe -garion -frost -fishman -erika -earth -dusty1 -dudedude -demo -deer -concord -colnago -clit -choice -chillin -bumper -blam -bitter -bdsm -basebal -barron -baker -arturo -annie1 -andersen -amerika -aladin -abbott -81fukkc -5678 -135791 -1002 -02101986 -02081983 -02041989 -02011989 -01011978 -zzzxxx -zxcvbnm123 -yyyyyy -yuan -yolanda -winners -welcom -volkswag -vera -ursula -ultra -toffee -toejam -theatre -switch -superma -Stone55 -solitude -sissy -sharp -scoobydoo -romans -roadster -punk -presiden -pool6123 -playstat -pipeline -pinball -peepee -paulina -ozzy -nutter -nights -niceass -mypassword -mydick -milan -medic -mazdarx7 -mason1 -marlon -mama123 -lemonade -krasotka -koroleva -karin -jennife -itsme -isaac -irishman -hookem -hewlett -hawaii50 -habibi -guitars -grande -glacier -gagging -gabriel1 -freefree -francesco -food -flyfish -fabric -edward1 -dolly -destin -delilah -defense -codered -cobras -climber -cindy1 -christma -chipmunk -chef -brigitte -bowwow -bigblock -bergkamp -bearcats -baba -altima -74108520 -45M2DO5BS -30051985 -258258 -24061986 -22021989 -21011989 -20061988 -1z2x3c4v -14061991 -13041987 -123456m -12021988 -11081989 -03041991 -02071981 -02031979 -02021976 -01061990 -01011960 -yvette -yankees2 -wireless -werder -wasted -visual -trust -tiffany1 -stratus -steffi -stasik -starligh -sigma -rubble -ROBERT -register -reflex -redfox -record -qwerty7 -premium -prayer -players -pallmall -nurses -nikki1 -nascar24 -mudvayne -moritz -moreno -moondog -monsters -micro -mickey1 -mckenzie -mazda626 -manila -madcat -louie -loud -krypton -kitchen -kisskiss -kate -jubilee -impact -Horny -hellboy -groups -goten -gonzalez -gilles -gidget -gene -gbhfvblf -freebird -federal -fantasia -dogbert -deeper -dayton -comanche -cocker -choochoo -chambers -borabora -bmw325 -blast -ballin -asdfgh01 -alissa -alessandro -airport -abrakadabra -7777777777 -635241 -494949 -420000 -23456789 -23041987 -19701970 -1951 -18011987 -172839 -1235 -123456789s -1125 -1102 -1031 -07071987 -02091989 -02071989 -02071983 -02021973 -02011981 -01121986 -01071986 -0101 -zodiac -yogibear -word -water1 -wasabi -wapbbs -wanderer -vintage -viktoriya -varvara -upyours -undertak -underground -undead -umpire -tropical -tiger2 -threesom -there -sunfire -sparky1 -snoopy1 -smart -slowhand -sheridan -sensei -savanna -rudy -redsox1 -ramirez -prowler -postman -porno1 -pocket -pelican -nfytxrf -nation -mykids -mygirl -moskva -mike123 -Master1 -marianna -maggie1 -maggi -live -landon -lamer -kissmyass -keenan -just4fun -julien -juicy -JORDAN -jimjim -hornets -hammond -hallie -glenn -ghjcnjgfhjkm -gasman -FOOTBALL -flanker -fishhead -firefire -fidelio -fatty -excalibur -enterme -emilia -ellie -eeee -diving -dindom -descent -daniele -dallas1 -customer -contest -compass -comfort -comedy -cocksuck -close -clay -chriss -chiara -cameron1 -calgary -cabron -bologna -berkeley -andyod22 -alexey -achtung -45678 -3636 -28041987 -25081988 -24011985 -20111986 -19651965 -1941 -19101987 -19061987 -1812 -14111986 -13031987 -123ewq -123456123 -12121990 -112112 -10071987 -10031988 -02101988 -02081980 -02021990 -01091987 -01041985 -01011995 -zebra -zanzibar -waffle -training -teenage -sweetness -sutton -sushi -suckers -spam -south -sneaky -sisters -shinobi -shibby -sexy1 -rockies -presley -president -pizza1 -piggy -password12 -olesya -nitro -motion -milk -medion -markiz -lovelife -longdong -lenny -larry1 -kirk -johndeer -jefferso -james123 -jackjack -ijrjkfl -hotone -heroes -gypsy -foxy -fishbone -fischer -fenway -eddie1 -eastern -easter -drummer1 -Dragon1 -Daniel -coventry -corndog -compton -chilli -chase1 -catwoman -booster -avenue -armada -987321 -818181 -606060 -5454 -28021992 -25800852 -22011988 -19971997 -1776 -17051988 -14021985 -13061986 -12121985 -11061985 -10101986 -10051987 -10011990 -09051945 -08121986 -04041991 -03041986 -02101983 -02101981 -02031989 -02031980 -01121988 -wwwwwww -virgil -troy -torpedo -toilet -tatarin -survivor -sundevil -stubby -straight -spotty -slater -skip -sheba1 -runaway -revolver -qwerty11 -qweasd123 -parol -paradigm -older -nudes -nonenone -moore -mildred -michaels -lowell -knock -klaste -junkie -jimbo1 -hotties -hollie -gryphon -gravity -grandpa -ghjuhfvvf -frogman -freesex -foreve -felix1 -fairlane -everlast -ethan -eggman -easton -denmark -deadly -cyborg -create -corinne -cisco -chick -chestnut -bruiser -broncos1 -bobdole -azazaz -antelope -anastasiya -456456456 -415263 -30041986 -29071983 -29051989 -29011985 -28021990 -28011987 -27061988 -25121987 -25031987 -24680 -22021986 -21031990 -20091991 -20031987 -196969 -19681968 -1946 -17061988 -16051989 -16051987 -1210 -11051990 -100500 -08051990 -05051989 -04041988 -02051980 -02051976 -02041980 -02031977 -02011983 -01061986 -01041988 -01011994 -0000007 -zxcasdqwe123 -washburn -vfitymrf -troll -tranny -tonight -thecure -studman -spikey -soccer12 -soccer10 -smirnoff -slick1 -skyhawk -skinner -shrimp -shakira -sekret -seagull -score -sasha_007 -rrrrrrrr -ross -rollins -reptile -razor -qwert12345 -pumpkin1 -porsche1 -playa -notused -noname123 -newcastle -never -nana -MUSTANG -minerva -megan1 -marseille -marjorie -mamamia -malachi -lilith -letmei -lane -lambda -krissy -kojak -kimball -keepout -karachi -kalina -justus -joel -joe123 -jerry1 -irinka -hurricane -honolulu -holycow -hitachi -highbury -hhhhh -hannah1 -hall -guess -glass -gilligan -giggles -flores -fabie -eeeeeeee -dungeon -drifter -dogface -dimas -dentist -death666 -costello -castor -bronson -brain -bolitas -boating -benben -baritone -bailey1 -badgers -austin1 -astra -asimov -asdqwe -armand -anthon -amorcit -797979 -4200 -31011987 -3030 -30031988 -3000gt -224466 -22071986 -21101986 -21051991 -20091988 -2009 -20051988 -19661966 -18091985 -18061990 -15101986 -15051990 -15011987 -13121985 -12qw12qw -1234123 -1204 -12031987 -12031985 -11121986 -1025 -1003 -08081988 -08031985 -03031986 -02101979 -02071979 -02071978 -02051985 -02051978 -02051973 -02041975 -02041974 -02031988 -02011982 -01031989 -01011974 -zoloto -zippo -wwwwwwww -w_pass -wildwood -wildbill -transit -superior -styles -stryker -string -stream -stefanie -slugger -skillet -sidekick -show -shawna -sf49ers -Salsero -rosario -remingto -redeye -redbaron -question -quasar -ppppppp -popova -physics -papers -palermo -options -mothers -moonligh -mischief -ministry -minemine -messiah -mentor -megane -mazda6 -marti -marble -leroy -laura1 -lantern -Kordell1 -koko -knuckles -khan -kerouac -kelvin -jorge -joebob -jewel -iforget -Hunter -house1 -horace -hilary -grand -gordo -glock -georgie -George -fuckhead -freefall -films -fantomas -extra -ellen -elcamino -doors -diaper -datsun -coldplay -clippers -chandra -carpente -carman -capricorn -calimero -boytoy -boiler -bluesman -bluebell -bitchy -bigpimp -bigbang -biatch -Baseball -audi -astral -armstron -angelika -angel123 -abcabc -999666 -868686 -3x7PxR -357357 -30041987 -27081990 -26031988 -258369 -25091987 -25041988 -24111989 -23021986 -22041988 -22031984 -21051988 -17011987 -16121987 -15021985 -142857 -14021986 -13021990 -12345qw -123456ru -1124 -10101990 -10041986 -07091990 -02051981 -01031985 -01021990 -****** -zildjian -yfnfkb -yeah -WP2003WP -vitamin -villa -valentine -trinitro -torino -tigge -thewho -thethe -tbone -swinging -sonia -sonata -smoke1 -sluggo -sleep -simba1 -shamus -sexxy -sevens -rober -rfvfcenhf -redhat -quentin -qazws -pufunga7782 -priest -pizdec -pigeon -pebble -palmtree -oxygen -nostromo -nikolai -mmmmmmm -mahler -lorena -lopez -lineage -korova -kokomo -kinky -kimmie -kieran -jsbach -johngalt -isabell -impreza -iloveyou1 -iiiii -huge -fuck123 -franc -foxylady -fishfish -fearless -evil -entry -enforcer -emilie -duffman -ducks -dominik -david123 -cutiepie -coolcat -cookie1 -conway -citroen -chinese -cheshire -cherries -chapman -changes -carver -capricor -book -blueball -blowfish -benoit -Beast1 -aramis -anchor -741963 -654654 -57chevy -5252 -357159 -345678 -31031988 -25091990 -25011990 -24111987 -23031990 -22061988 -21011991 -21011988 -1942 -19283746 -19031985 -19011989 -18091986 -17111985 -16051988 -15071987 -145236 -14081985 -132456 -13071984 -1231 -12081985 -1201 -11021985 -10071988 -09021988 -05061990 -02051972 -02041978 -02031983 -01091985 -01031984 -010191 -01012009 -yamahar1 -wormix -whistler -wertyu -warez -vjqgfhjkm -versace -universa -taco -sugar1 -strawber -stacie -sprinter -spencer1 -sonyfuck -smokey1 -slimshady -skibum -series -screamer -sales -roswell -roses -report -rampage -qwedsa -q11111 -program -Princess -petrova -patrol -papito -papillon -paco -oooooooo -mother1 -mick -Maverick -marcius2 -magneto -macman -luck -lalakers -lakeside -krolik -kings -kille -kernel -kent -junior1 -jules -jermaine -jaguars -honeybee -hola -highlander -helper -hejsan -hate -hardone -gustavo -grinch -gratis -goth -glamour -ghbywtccf -ghbdtn123 -elefant -earthlink -draven -dmitriy -dkflbr -dimples -cygnusx1 -cold -cococo -clyde -cleopatr -choke -chelse -cecile -casper1 -carnival -cardiff -buddy123 -bruce1 -bootys -bookie -birddog -bigbob -bestbuy -assasin -arkansas -anastasi -alberta -addict -acmilan -7896321 -30081984 -258963 -25101988 -23051985 -23041986 -23021989 -22121987 -22091988 -22071987 -22021988 -2006 -20052005 -19051987 -15041988 -15011985 -14021990 -14011986 -13051987 -13011988 -13011987 -12345s -12061988 -12041988 -12041986 -11111q -11071988 -11031988 -10081989 -08081986 -07071990 -07071977 -05071984 -04041983 -03021986 -02091988 -02081976 -02051977 -02031978 -01071987 -01041987 -01011976 -zack -zachary1 -yoyoma -wrestler -weston -wealth -wallet -vjkjrj -vendetta -twiggy -twelve -turnip -tribal -tommie -tkbpfdtnf -thecrow -test12 -terminat -telephone -synergy -style -spud -smackdow -slammer -sexgod -seabee -schalke -sanford -sandrine -salope -rusty2 -right -repair -referee -ratman -radar -qwert40 -qwe123qwe -prozac -portal -polish -Patrick -passes -otis -oreo -option -opendoor -nuclear -navy -nautilus -nancy1 -mustang6 -murzik -mopar -monty1 -Misfit99 -mental -medved -marseill -magpies -magellan -limited -Letmein1 -lemmein -leedsutd -larissa -kikiki -jumbo -jonny -jamess -jackass1 -install -hounddog -holes -hetfield -heidi1 -harlem -gymnast -gtnhjdbx -godlike -glow -gideon -ghhh47hj7649 -flip -flame -fkbyjxrf -fenris -excite -espresso -ernesto -dontknow -dogpound -dinner -diablo2 -dejavu -conan -complete -cole -chocha -chips -chevys -cayman -breanna -borders -blue32 -blanco -bismillah -biker -bennie -benito -azazel -ashle -arianna -argentin -antonia -alanis -advent -acura -858585 -4040 -333444 -30041985 -29071985 -29061990 -27071987 -27061985 -27041990 -26031990 -24031988 -23051990 -2211 -22011986 -21061986 -20121989 -20092009 -20091986 -20081991 -20041988 -20041986 -1qwerty -19671967 -1950 -19121989 -19061990 -18101987 -18051988 -18041986 -18021984 -17101986 -17061989 -17041991 -16021990 -15071988 -15071986 -14101987 -135798642 -13061987 -1234zxcv -12321 -1214 -12071989 -1129 -11121985 -11061991 -10121987 -101101 -10101985 -10031987 -100200 -09041987 -09031988 -06041988 -05071988 -03081989 -02071985 -02071975 -0123456 -01051989 -01041992 -01041990 -zarina -woodie -whiteboy -white1 -waterboy -volkov -vlad -virus -vikings1 -viewsoni -vbkfirf -trans -terefon -swedish -squeak -spanner -spanker -sixpack -seymour -sexxx -serpent -samira -roma -rogue -robocop -robins -real -Qwerty1 -qazxcv -q2w3e4 -punch -pinky1 -perry -peppe -penguin1 -Password123 -pain -optimist -onion -noway -nomad -nine -morton -moonshin -money12 -modern -mcdonald -mario1 -maple -loveya -love1 -loretta -lookout -loki -lllll -llamas -limewire -konstantin -k.lvbkf -keisha -jones1 -jonathon -johndoe -johncena -john123 -janelle -intercourse -hugo -hopkins -harddick -glasgow -gladiato -gambler -galant -gagged -fortress -factory -expert -emperor -eight -django -dinara -devo -daniels -crusty -cowgirl -clutch -clarissa -cevthrb -ccccccc -capetown -candy1 -camero -camaross -callisto -butters -bigpoppa -bigones -bigdawg -best -beater -asgard -angelus -amigos -amand -alexandre -9999999999 -8989 -875421 -30011985 -29051985 -2626 -26061985 -25111987 -25071990 -22081986 -22061989 -21061985 -20082008 -20021988 -1a2s3d -19981998 -16051985 -15111988 -15051985 -15021990 -147896 -14041988 -123567 -12345qwerty -12121988 -12051990 -12051986 -12041990 -11091989 -11051986 -11051984 -1008 -10061986 -0815 -06081987 -06021987 -04041990 -02081981 -02061977 -02041977 -02031975 -01121987 -01061988 -01031986 -01021989 -01021988 -wolfpac -wert -vienna -venture -vehpbr -vampir -university -tuna -trucking -trip -trees -transfer -tower -tophat -tomahawk -timosha -timeout -tenchi -tabasco -sunny1 -suckmydick -suburban -stratfor -steaua -spiral -simsim -shadow12 -screw -schmidt -rough -rockie -reilly -reggae -quebec -private1 -printing -pentagon -pearson -peachy -notebook -noname -nokian73 -myrtle -munch -moron -matthias -mariya -marijuan -mandrake -mamacita -malice -links -lekker -lback -larkin -ksusha -kkkkk -kestrel -kayleigh -inter -insight -hotgirls -hoops -hellokitty -hallo123 -gotmilk -googoo -funstuff -fredrick -firefigh -finland -fanny -eggplant -eating -dogwood -doggies -dfktynby -derparol -data -damon -cvthnm -cuervo -coming -clock -cleopatra -clarke -cheddar -cbr900rr -carroll -canucks -buste -bukkake -boyboy -bowman -bimbo -bighead -bball -barselona -aspen -asdqwe123 -around -aries -americ -almighty -adgjmp -addison -absolutely -aaasss -4ever -357951 -29061989 -28051987 -27081986 -25061985 -25011986 -24091986 -24061988 -24031990 -21081987 -21041992 -20031991 -2001112 -19061985 -18111987 -18021988 -17071989 -17031987 -16051990 -15021986 -14031988 -14021987 -14011989 -1220 -1205 -120120 -111999 -111777 -1115 -1114 -11011990 -1027 -10011983 -09021989 -07051990 -06051986 -05091988 -05081988 -04061986 -04041985 -03041980 -02101976 -02071976 -02061976 -02011975 -01031983 -zasada -wyoming -wendy1 -washingt -warrior1 -vickie -vader1 -uuuuuu -username -tupac -Trustno1 -tinkerbe -suckdick -streets -strap -storm1 -stinker -sterva -southpaw -solaris -sloppy -sexylady -sandie -roofer -rocknrol -rico -rfhnjirf -QWERTY -qqqqq1 -punker +speed +seventeen +senior +scottie +sam +ryan +rogers +rhonda progress -platon -Phoenix -Phoeni -peeper -pastor -paolo -page -obsidian -nirvana1 -nineinch -nbvjatq -navigator -native -money123 -modelsne -minimoni -millenium -max333 -maveric -matthe -marriage -marquis -markie -marines1 -marijuana -margie -little1 -lfybbk -klizma -kimkim -kfgjxrf -joshu -jktxrf -jennaj -irishka -irene -ilove -hunte -htubcnhfwbz -hottest -heinrich -happy2 -hanson -handball -greedy -goodie -golfer1 -gocubs -gerrard -gabber -fktyrf -facebook -eskimo -elway7 -dylan1 -dominion -domingo -dogbone -default -darkangel -cumslut -cumcum -cricket1 -coral -coors +polska +plastic +pinky +muhammad +medusa +maryland +married +lololo +login +lillian +leanne +knicks +jewels +hithere +giraffe +gillian +frozen +frogger +foxtrot +evergreen +emilio +duchess +dragoon +devil +deanna +daughter +daemon +command +claudio +clarinet +chucky +chuckles +chloe +carlton +beverly +beethoven +beach +babies +arlene +anakin +almighty +aaaaaaaaaa +9876543210 +1qaz1qaz +1313 +wilbur +waterfall +tttttt +tina +theking +suckit +sparta +sneakers +smelly +saratoga +root +reebok +raquel +quantum +qawsedrf +qawsed +motocross +maxmax +majestic +kingfish +kasper +japanese +integra +hhhhhh +help +harper +graphics +golf +flounder +erika +dundee +daphne +dance +corinne +coltrane chris123 -charon -challeng -canuck -call -calibra -buceta -bubba123 -bricks -bozo -blues1 -bluejays -berry -beech -awful -april1 -antonina +checkers +carbon +brandi +boxing +better +barbados +augustus +angelika +12345qwert +washburn +veritas +tottenham +tempest +survivor +strange +stanford +spanish +soulmate +snapper +shawn +robert1 +rasputin +rambo +rachael +queenie +pallmall +overkill +nimrod +mustard +mittens +medina +meatloaf +maureen +lowrider +katarina +ilovegod +heather1 +hamburg +hallo123 +grandpa +gogogo +giuseppe +georgie +fingers +europe +enrique +eastwood +duke +dominion +destroyer +dawson +chiquita +chipmunk +castillo +bugger +buffy +bobbie +berkeley +beast +antony +alexandria +9999 +2000 +121314 +1122334455 +1029384756 +zander +yasmin +world +trebor +toledo +thinking +tarheels +skiing +simona +sheldon +shanti +seminole +select +rookie +radiohead +priscilla +pornstar +platypus +peacock +nirvana1 +mephisto +marvel +mama +magnus +lancaster +knowledge +johnjohn +hubert +hackers +grant +gameover +fuckface +david123 +darklord +cutiepie +create +contact +company +carnival +candyman +cancel +camper +booker +blowfish +black1 +bigboss +bender +alien +active +abc1234 +zidane +wright +working +wedding +vortex +ursula +twisted +terry +ssssssss +squash +sponge +snowboard +smoking +shasta +shadows +seeker +sausage +sandwich +sailboat +rupert +romano +ripper +rebel +rancid +pudding +prophet +powder +philly +olivier +nutmeg +mandarin +knuckles +jimbob +jasmine1 +japan +helene +hardrock +greece +gold +forum +floppy +elwood +dominik +dimitri +daredevil +bristol +boomboom +benedict +babyface +anders +albatros +963852741 +565656 +323232 +262626 +whynot +whisky +valentino +trident +theboss +tanya +sprinter +soccer1 +shocker +shakira +scream +sammy1 +samara +salvation +rolltide +rodriguez +r2d2c3po +qwer +poetry +plasma +password12 +pancake +mustangs +moonshine +missouri +minimum +mikey +meridian +melina +meatball +marino +mango +mandy +malaysia +kinder +killbill +justin1 +jason1 +illinois +hottie +gringo +green1 +gonzalez +georgina +gargoyle +flores +evangelion +engine +emilie +disaster +depeche +daniel1 +coolman +compton +complete +coco +claymore +cheesecake +chainsaw +cat +cabbage +bluebell +blake +98765432 +yvette +wolfman +wishbone +warhammer +viewsonic +vampires +uranus +thunderbird +tammy +susanne +smashing +sales +sabbath +rrrrrr +rhiannon +reagan +rachelle +playtime +petunia +offspring +octopus +marius +marcia +marcella +maggot +lonestar +lawyer +jenifer +hooker +heritage +hehehe +hayabusa +harvard +freestyle +forward +forsaken +ferrari1 +fatman +emperor +elvira +dusty +double +darius +cypress +cruise +crash +china +charley +challenger +carole +beer +beanie +battery +backdoor +asshole1 +angelus +4321 +22222 +147896325 +11235813 +yosemite +yogibear +xxxxx +wolf +venus +user +talisman +taekwondo +syracuse +supersonic +scully +sasuke +redline +red +randolph +ramones +raistlin +preacher +peyton +peugeot +patty +party +papa +orchid +musica +millions +metallic +max +matador +marcos +mailman +madison1 +ludwig +lucy +losangeles +loretta +lazarus +kevin1 +isaac +indians +iloveme +hewlett +hernandez +hayley +gunners +girls +franky +flight +eternal +eeyore +dontknow +coolcool +charisma +cessna +bigbird +xanadu +werner +wednesday +village +topper +susana +starwars1 +start +sonic +sinister +sharky +scout +scotch +scanner +salomon +roman +program +polo +pistol +paulina +passpass +pancho +outside +open +mohammad +mcdonald +mayhem +laurent +lambda +kodiak +jacques +hilary +helen +goldeneye +geheim +frontier +francesca +flipflop +fisherman +famous +fallout +eraser +emilia +eggplant +diego +deejay +dannyboy +daniella +cosmic +conner +coleman +chrysler +catch22 +cameron1 +cambridge +buckshot +bounty +arkansas +archangel +america1 +12345679 +zorro +yomama +xxx +wutang +woohoo +walrus +vermont +twins +tom +tanker +sprint +skyler +shuttle +romantic +robotics +redalert +rebels +really +punkin +prayer +newpass +moocow +mine +mememe +megatron +marty +marker +mamapapa +mail +liquid +lilith +ladies +kristi +jojo +install +hyperion +honesty +hamburger +gundam +good +goliath +gladys +gadget +gabriel1 +fuckfuck +friendship +friendly +florida1 +first +expert +erica +eatshit +dreaming +dollars +doghouse +dog +disturbed +dianne +citizen +christin +celtics +candice +bubblegum +brigitte +banner +anubis +addicted +abcd123 +778899 +xxxxxxx +xander +valley +underworld +slacker +shane +shadow12 +rosie +presto +porkchop +pierce +passat +negative +mistress +melissa1 +massimo +living +letter +lance +jethro +jermaine +james007 +impact +hanson +great +garage +gabriella +francine +fletch +everest +dumbass +dookie +deskjet +delphine +cyclops +crystal1 +computers +common +chestnut +capital +booster +blood +blah +baseball1 +barber +auckland +attack +arturo +alfredo +aaa111 +321654987 +191919 +writer +wanderer +virtual +venice +vancouver +tomahawk +toffee +thanatos +tango +syncmaster +snow +snoopdog +skinny +sinbad +sassy +sanchez +roderick +ripple +princesa +porno +popopo +poodle +poncho +pentagon +paula +nathaniel +money123 +millenium +mildred +mighty +mechanic +liverpool1 +lesbian +kenshin +julien +joejoe +greg +francesco +fishes +europa +esmeralda +demons +darrell +dante +creature +cornwall +chadwick +celeron +carpediem +camila +calendar +breeze +bottom +blue123 +betty +barry +auburn +assass +ariel antares another -andrea1 -amore -alena -aileen -a1234 -996633 -556677 -5329 -5201314 -3006 -28051986 -28021985 -27031989 -26021987 -25101989 -25061986 -25041985 -25011985 -24061987 -23021985 -23011985 -223322 -22121986 -22121983 -22081983 -22071989 -22061987 -22061941 -22041986 -22021985 -21021985 -2007 -20031988 -1qaz -199999 -19101990 -19071988 -19071986 -18061985 -18051990 -17071985 -16111990 -16061986 -16011989 -15081991 -15051987 -14071987 -13031986 -123qwer -1235789 -123459 -1227 -1226 -12101988 -12081984 -12071987 -1200 -11121987 -11081987 -11071985 -11011991 -1101 -1004 -08071987 -08061987 -05061986 -04061991 -03111987 -03071987 -02091976 -02081979 -02041976 -02031973 -02021991 -02021980 -02021971 -zouzou -yaya -wxcvbn -wolfen -wives -wingnut -whatwhat -Welcome1 -wanking -VQsaBLPzLa -truth -tracer -trace -theforce -terrell -sylveste -susanna +airbus +abdullah +Michael +112358 +zodiac +xbox360 +wayne +wassup +video +vendetta +vector +tyrone +twenty +timmy +telecom +switch +supervisor stephane -stephan -spoons -spence -sixty -sheepdog -services -sawyer -sandr -saigon -rudolf -rodeo -roadrunner -rimmer -ricard -republic -redskin -Ranger -ranch -proton -post -pigpen -peggy -paris1 -paramedi -ou8123 -nevets -nazgul -mizzou -midnite -metroid -Matthew -masterbate -margarit -loser1 -lolol -lloyd -kronos -kiteboy -junk -joyce -jomama -joemama -ilikepie -hung -homework -hattrick -hardball -guido -goodgirl -globus -funky -friendster -flipflop -flicks -fender1 -falcon1 -f00tball -evolutio -dukeduke -disco -devon -derf -decker -davies -cucumber -cnfybckfd -clifton -chiquita -castillo -cars -capecod -cafc91 -brown1 -brand -bomb -boater -bledsoe -bigdicks -bbbbbbb -barley -barfly -ballet -azzer -azert -asians -angelic -ambers -alcohol -6996 -5424 -393939 -31121990 -30121987 -29121987 -29111989 -29081990 -29081985 -29051990 -27272727 -27091985 -27031987 -26031987 -26031984 -24051990 -23061990 -22061990 -22041985 -22031991 -22021990 -21111985 -21041985 -20021986 -19071990 -19051986 -19011987 -17171717 -17061986 -17041987 -16101987 -16031990 -159357a -15091987 -15081988 -15071985 -15011986 -14101988 -14071988 -14051990 -14021983 -132465 -13111990 -12121987 -12121982 -12061986 -12011989 -11111987 -11081990 -10111986 -10031991 -09090909 -08051987 -08041986 -05051990 -04081987 -04051988 -03061987 -03031993 -03031988 -02101980 -02101977 -02091977 -02091975 -02061979 -02051975 -01081990 -01061987 -01011971 -wiseguy -weed420 -tosser -toriamos -toolbox -toocool -tomas -thedon -tender -taekwondo -starwar -start1 -sprout -sonyericsson -slimshad -skateboard -shonuf -shoes -sheep -shag -ring -riccardo -rfntymrf -redcar -qwe321 -qqqwww -proview -prospect -persona -penetration -peaches1 -peace1 -olympus -oberon -nokia6233 -nightwish -munich -morales -mone -mohawk -merlin1 -Mercedes -mega -maxwell1 -mash4077 -marcelo -mann -mad -macbeth -LOVE -loren -longer -lobo -leeds -lakewood -kurt -krokodil -kolbasa -kerstin -jenifer -hott -hello12 -hairball -gthcbr -grin -grandam -gotribe -ghbrjk -ggggggg -FUCKYOU -fuck69 -footjob -flasher -females -fellow -explore -evangelion -egghead -dudeman -doubled -doris -dolemite -dirty1 -devin -delmar -delfin -David -daddyo -cromwell -cowboy1 -closer -cheeky -ceasar -cassandr -camden -cabernet -burns -bugs -budweiser -boxcar -boulder -biggun -beloved -belmont -beezer -beaker -Batman -bastards -bahamut -azertyui -awnyce -auggie -aolsucks -allegro -963963 -852852 -515000 -45454545 -31011990 -29011987 -28071986 -28021986 -27051987 -27011988 -26051988 -26041991 -26041986 -25011993 -24121986 -24061992 -24021991 -24011990 -23051986 -23021988 -23011990 -21121986 -21111990 -21071989 -20071986 -20051985 -20011989 -1943 -19111987 -19091988 -18041990 -18021986 -18011986 -17101987 -17091987 -17021985 -17011990 -16061985 -1598753 -15051986 -14881488 -14121989 -14081988 -14071986 -13111984 -122112 -12121989 -12101985 -12051985 -111213 -11071986 -1103 -11011987 -10293847 -101112 -10081985 -10061987 -10041983 -0911 -07091982 -07081986 -06061987 -06041987 -06031983 -04091986 -03071986 -03051987 -03051986 -03031990 -03011987 -02101978 -02091973 -02081974 -02071977 -02071971 -0192837465 -01051988 -01051986 -01011973 -????? -zxcv123 -zxasqw -yyyy -yessir -wordup -wizards -werty -watford -Victoria -vauxhall -vancouve -tuscl -trailer -touching -tokiohotel -suslik -supernov -steffen -spider1 -speakers -spartan1 -sofia -signal -sigmachi -shen -sheeba -sexo -sambo -salami -roger1 -rocknroll -rockin -road -reserve -rated -rainyday -q123456789 -purpl -puppydog -power123 -poiuytre -pointer -pimping -phialpha -penthous -pavement -outside -odyssey -nthvbyfnjh -norbert -nnnnnnnn -mutant -Mustang -mulligan -mississippi -mingus -Merlin -magic32 -lonesome -liliana -lighting -lara -ksenia -koolaid -kolokol -klondike -kkkkkkk -kiwi -kazantip -junio -jewish -jajaja -jaime -jaeger -irving -ironmaiden -iriska -homemade -herewego -helmut -hatred -harald -gonzales -goldfing -gohome -gerbil -genesis1 -fyfnjkbq -freee -forgetit -foolish -flamengo -finally -favorite6 -exchange -enternow -emilio -eeeeeee -dougie -dodgers1 -deniro -delaware -deaths -darkange -commande -comein -cement -catcher -cashmone -burn -buffet -breaker -brandy1 -bordeaux -books -bongo -blue99 -blaine -birgit -billabon -benessere -banan -awesome1 -asdffdsa -archange -annmarie -ambrosia -ambrose -alleycat -all4one -alchemy -aceace -aaaaaaaaaa -777999 -43214321 -369258147 -31121988 -31121987 -30061987 -30011986 -2fast4u -29041985 -28121984 -28061986 -28041992 -28031982 -27111985 -27021991 -26111985 -26101986 -26091986 -26031986 -25021988 -24111990 -24101986 -24071987 -24011987 -23051991 -23051987 -23031987 -222777 -22071983 -22051986 -21101989 -21071987 -21051986 -20081986 -20061986 -20031986 -20021985 -20011988 -19641964 -19111986 -19101986 -19021990 -18051987 -18031991 -18021987 -16111982 -16011987 -15111984 -15091988 -15061988 -15031988 -15021983 -14021989 -14011988 -14011987 -12348765 -12345qaz -1234566 -12111990 -12091988 -12051989 -12051987 -12031988 -12021985 -12011985 -11111986 -11091984 -1109 -11071989 -1016 -10071985 -10061984 -10041990 -10031989 -10011988 -06071983 -05021988 -03041987 -02091982 -02091971 -02061974 -02051990 -02051979 -02011990 -01051990 -010390 -01021985 -youtube -yasmin -woodstoc -wonderful -wildone -widget -whiplash -ukraine -tyson1 -twinkie -trouble1 -treetop -tigers1 -their -testing1 -tarpon -tantra -summer69 -stickman -stafford -spooge -spliff -speedway -somerset -smoothie -siobhan -shuttle -shodan -SHADOW +skylar +simba selina -segblue2 -sebring -scheisse -Samantha -rrrr -roll -riders -revolution -redbone -reason -rasmus -randy1 -rainbows -pumper -pornking -point -ploppy -pimpdadd -payday -pasadena -p0o9i8u7 -opennow -nittany -newark -navyseal -nautica -monic -mikael -metall -Marlboro -manfred -macleod -luna -luca -longhair -lokiloki -lkjhgfds -lefty -lakers1 -kittys -killa -kenobi -karine -kamasutra -juliana -joseph1 -jenjen -jello -interne -houdini -gsxr1000 -grass -gotham -goodday -gianni -getting -gannibal -gamma -flower2 -fishon -Fabie -evgeniy -drums -dingo -daylight -dabomb -cornwall -cocksucker -climax -catnip -carebear -camber -butkus -bootsy -blue42 -auto -austin31 -auditt -ariel -alice1 -algebra -advance -adrenalin -888999 -789654123 -777333 -5Wr2i7H8 -4567 -3ip76k2 -32167 -31031987 -30111987 -30071986 -30061983 -30051989 -30041991 -28071987 -28051990 -28051985 -27041985 -26071987 -26061986 -26051986 -25121985 -25051985 -24081988 -24041988 -24031987 -24021988 -23skidoo -23121986 -23091987 -23071985 -23061992 -22111985 -22091986 -22081991 -22071990 -22061985 -21081985 -21071992 -21021987 -20101988 -20061984 -20051989 -20041990 -1Dragon -19091990 -19031987 -18121984 -18081988 -18061991 -18041991 -18011988 -17061991 -17021987 -16031988 -16021987 -15091989 -15081990 -15071983 -15041987 -14091990 -14081990 -14041992 -14041987 -14031989 -13081985 -13021987 -123qwert -12345qwer -12345abc -123456t -123456789m -1212121212 -12081983 -12021991 -111112 -11101986 -11081988 -11061989 -11041991 -11011989 -1018 -1015 -10121986 -10121985 -10101989 -10041991 -09091986 -09081988 -09051986 -08071988 -08011986 -07101987 -07071985 -0660 -06061985 -06011988 -05031991 -05021987 -04061984 -04051985 -02101973 -02061981 -02061972 -02041973 -02011979 -01101987 -01051985 -01021987 -workout -wonderboy -winter1 -wetter -werdna -vvvv -voyager1 -vagabond -trustme -toonarmy -timtim -Tigger -thrasher -terra -swoosh -supra -stigmata -stayout -status -square -sperma -smackdown -sixty9 -sexybabe -sergbest -senna -scuba1 -scrapper -samoht -sammy123 -salem -rugger -royalty -rivera -ringo -restart -reginald -readers -raleigh -rainbow1 -rage -prosper -pitch -pictures -petunia -peterbil -perfect1 -patrici -pantera1 -pancake +rockets +revolver +reggae +railroad +qwerty12345 +placebo +paloma +pablo p4ssw0rd -outback -norris -normandy -nevermore -needles -nathan1 -nataly -narnia -musical -mooney -michal -maxdog -MASTER -madmad -m123456 -lumina -luckyone -luciano -linkin -lillie -leigh -kirkland -kahlua -junkmail -Joshua -josephin -Jordan23 -johnson1 -jocelyn -jeannie -javelin -inlove -honor -holein1 -harbor -grisha -gina -gatit -futurama -firenze -fireblad -fellatio -esquire -errors -emmett -elvisp -drum -driller -dragonfl -dragon69 -dingle +monaco +minnesota +marlon +mariners +manuela +leather +killers +insert +iloveyou2 +ibanez +holyshit +hollow +hallo +freeze +freeway +freak +elisabeth +donnie +demo +database +celica +cathy +calypso +bumblebee +bruins +bobafett +bernardo +barkley +ballet +astrid +amethyst +albatross +advanced +addison +987456 +272727 +zxcvb +whistler +wellington +weezer +weaver +warlord +wagner +volley +vernon +trisha +trapper +susanna +suicide +starter +sphinx +smitty +slamdunk +sisters +sheffield +scrabble +roadkill +retard +realmadrid +randall +rainbows +queens +profile +postal +polopolo +obsidian +northern +mortal +message +mathias +magic1 +magenta +looser +looney +legacy +learning +kashmir +independent +impossible +husband +hailey +elements +electron +diane +derek davinci -crackers -corwin -compaq1 -collie -christa -checker -cartoons -buttercup -bungle -budgie -boomer1 -body -blue1234 -biit -bigguns -barry1 -audio -atticus -atlas -Anthony -angus1 -Anai -alisa -alex12 -aikman -abacab -951357 -7894 -4711 -321678 -31101987 -31051985 -30121986 -30091989 -30031992 -30031986 -30011987 -29061988 -29061985 -29031988 -28061988 -27061983 -27031986 -27021990 -26101987 -26071989 -26071986 -25081986 -25061987 -25051987 -25041991 -24101989 -24071991 -23111987 -23091986 -23051983 -23031986 -2222222222 -22121989 -22071991 -22051991 -22011985 -21121985 -21031985 -20121988 -20121986 -20061990 -20051987 -1q2q3q -1944 -19091983 -19061992 -1905 -19021991 -18121987 -18121983 -18111986 -16121986 -16091987 -16071991 -16071987 -15111989 -15031990 -14041986 -13121983 -13101987 -13091984 -13071990 -1245 -12345m -1234568 -123456789qwe -1234567899 -1234561 -1228 -12211221 -12121991 -12121986 -12101990 -12101984 -12091991 -1209 -12081988 -12071990 -12071988 -115599 -11111a -11041990 -1028 -10081990 -10081983 -10071990 -10061989 -10011992 -09111987 -09081985 -08121987 -08111984 -08101986 -08051989 -07091988 -07081987 -07071988 -07071984 -07071982 -07051987 -06031992 -05111986 -05051991 -05031990 -05011987 -04111988 -04061987 -04041987 -040404 -02081973 -02061978 -02031991 -02031990 -02011976 -01071984 -01041980 -01021992 -zaqwsxcde -yyyyyyyy -worthy -woowoo -wind -William -warhamme -walton -vodka -venom +customer +corrado +concord +comfort +cinder +chopin +chantal +budweiser +brisbane +bogart +baritone +balloon +badman +asd +armageddon +andrey +amigos +amarillo +alonso +algebra +alexandr +aerosmith +adriano +123457 +12301230 +windmill +wheels +westham +visual +vintage +vanhalen +telefon +tardis +surprise +stefano +starfire +speakers +snatch +smoker +shazam +seymour +satan +sandro +salome +safari +sadie +river +radio +postman +poppy +palace +oregon +odessa +noname +ncc1701e +nation +mustafa +music1 +mimosa +method +lucille +luciano +lifetime +lambert +kittykat +keller +gideon +funny +fredrick +fidelity +fabulous +everyday +eastern +dixie +dentist +daytona +davids +darlene +craig +coolness +concorde +clancy +chapman +catwoman +casablanca +browns +boris +blackhawk +belle +barrett +babybaby +atomic +aladdin +aaa +147147 +will +vodafone +traveler +trader +tractor +tara +summer1 +stoner +stimpy +southside +sarah1 +santa +renault +rainbow1 +radical +princess1 +primus +potatoes +polly +pipeline +philippe +peter1 +payton +patton +pathfinder +openup +nofear +nigeria +monterey +maxime +marsha +madden +lipstick +lesley +lakeside +krystal +kendra +kelly1 +kelley +juice +joey +jakarta +italian +internet1 +insanity +hustler +hughes +hotshot +hihihi +harvest +gaston +fishbone +emma +elite +diehard +destroy +daisy1 +curious +critter +chihuahua +channel +bordeaux +boeing +biohazard +beatriz +beamer +bacchus +alfonso +21122112 +159159 +wookie +windsurf +windsor +wanted +walnut +vinnie velocity -treble -tralala -tigercat -tarakan -sunlight -streaming -starr -sonysony -smart1 -skylark -sites -shower -sheldon -seneca -sedona -scamper -sand -sabrina1 -romantic -rockwell -rabbits -q1234567 +vagabond +torres +topsecret +thegame +temp +stretch +stereo +seamus +scratch +saskia +sahara +rescue +reloaded +redred +raindrop +prudence +professional +praise +power1 +pilgrim +pharmacy +peaceful +patrice +nnnnnn +musical +multimedia +montgomery +midget +marseille +marisa +marietta +luke +lotus +letmein2 +ladybird +kaitlyn +jenny1 +janet +irish +internal +hyundai +hitachi +havana +gigabyte +gameboy +fourteen +feather +everett +ernesto +egghead +dynasty +dolphin1 +davis +damnit +chambers +castro +bushido +bunghole +buckeyes +buckeye +brodie +breaker +bluefish +bleach +beowulf +bedford +because +bartman +apocalypse +aphrodite +adonis +5555555 +4815162342 +23232323 +1988 +1980 +12369874 +111222333 +111 +zerocool +yyyyyy +ytrewq +wrestler +vicky +tracy +tortoise +sysadmin +sunshine1 +subzero +starship +sonia +sean +sawyer +redwood +redhot +reason +qwerty123456 +qwerty11 puzzle -protect -poker1 -plato -plastics -pinnacle -peppers -pathetic -patch +primrose +politics +pluto +paranoia pancakes -ottawa -ooooo -offshore -octopus -nounours -nokia1 -neville -ncc74656 -natasha1 -nastia -mynameis -motor -motocros -middle -met2002 -meow -meliss -medina -meadow -matty -masterp -manga -lucia -loose -linden -lhfrjy -letsdoit -leopold -lawson -larson -laddie -ladder -kristian -kittie -jughead -joecool -jimmys -iklo -honeys -hoffman -hiking -hello2 -heels -harrier -hansol -haley -granada -gofast -fyutkjxtr -frogs -francisc -four -fields -farm -faith1 -fabio -dreamcas -dragster -doggy1 -dirt -dicky +overload +opensesame +okokok +nikola +nevermore +moscow +melbourne +matthews +marriage +mallory +magdalena +macaroni +lespaul +lemons +laurel +kyle +kittens +kiss +kicker +justme +juanita +jonathon +jocelyn +jacqueline +jackjack +infinite +hope +heinrich +hansolo +hacked +greens +gratis +graduate +goodness +godspeed +feedback +domingo +dieter +cougars +corolla +cornelia +corleone +choochoo +chinese +challenge +chairman +canon +butthole +buddy123 +brennan +bouncer +bossman +bonsai +bonkers +barracuda +azsxdcfv +andrew1 +alisha +accounting +505050 +445566 +420420 +1478963 +102938 +woofer +warner +volcano +tyler1 +trucks +toby +slider +sleeping +serious +remington +quicksilver +pringles +premier +power123 +paradigm +nickolas +navigator +nautilus +muscle +moreno +milkshake +miles +menace +master123 +massage +marshal +killer1 +kathy +kate +jonas +jane +jammer +gravity +gerrard +geneva +ganesh +frog +formula +feathers +facebook +enrico +dragon12 +deluxe +damage +cruiser +condom +cinema +brittney +bones +bazooka +aviation +avalanche +anthrax +airport +admiral +666999 +19841984 +123qweasdzxc +10203040 +wolfie +wildwood +whatsup +thrasher +summit +stunner +staples +speedway +sonny +songbird +sinatra +sickness +shannon1 +senator +screamer +savior +sascha +samantha1 +riverside +riley +renata +redbull +rabbits +quentin +profit +princeton +powell +popper +poopie +pooper +peters +pepito +oscar1 +olympia +oldman +nopass +noelle +monster1 +milena +micron +mauricio +mattie +massive +marika +manhattan +manfred +love1234 +lithium +labtec +keegan +joyce +jojojo +jennifer1 +jeffery +jayson +janelle +intel +indonesia +iceberg +ibrahim +hungry +hell +hawk +hammond +filter +epsilon +email +elena +electra +doreen +dimples +devil666 +deacon +dandan +creator +cosmo +cooking +clipper +circle +chimera +caveman +bugsbunny +budlight +bowler +bottle +birdman +benfica +barton +android +ambrosia +adrianna +909090 +2222222 +2001 +zxcvbnm1 +zero +windows1 +wheeler +waffle +verona +ventura +toulouse +toto +topcat +tazmania +static +stacy +speedo +spears +spaghetti +slinky +slapshot +reptile +rebekah +pigeon +panties +monty +mitch +ministry +miami +mental +matteo +mathilde +magpie +lighting +lady +john123 +jenkins +huskers +houses +helsinki +heidi +hanuman +girlfriend +gateway1 +garnet +fussball +frisbee +frederik +flexible +finland +festival +federal +familia +eloise +dynamic +dwight +dungeon +doggy +dickens destiny1 -deputy -delpiero -dbnfkbr -dakota1 -daisydog -cyprus -cutie -cupoi -colonial -colin -clovis -cirrus -chewy -chessie -chelle -caster +daydream +coventry +constant +connection +charles1 +carpet +bicycle +becky +babyboy +area51 +angeline +alucard +a123456789 +99999 +1234321 +111111111 +zebra +woodland +wasser +vipers +trust +trains +theatre +tabasco +swinger +string +steffi +spectre +sooner +skinhead +signature +shutup +sandrine +sam123 +sacred +rufus +rockford +quartz +possum +pinball +nipper +nina +nichole +namaste +morton +merchant +ketchup +kenwood +jazz +hentai +hanna +haggis +greatest +grapes +fuckers +fritz +ford +everlast +eunice +espresso +encore +ellen +elizabet +eeeeee +drifter +dragon123 +dolly +dddddddd +darkman +community +chrome +chouchou +chiefs +charlton +champs +champagne +carlitos +camel +brad +boobs +bobbob +blueblue +beaner +beaches +balls +baldwin +awesome1 +athens +aspirine +anne +allstar +alcohol +963852 +77777 +3333 +1985 +12345abc +123098 +zaphod +weed +vvvvvv +vienna +thelma +theend +tekken +technology +stronger +stephan +starbuck +spotty +skeleton +second +scissors +rosario +rolling +rodman +rocks +reginald +redeemer +ralph +raleigh +polarbear +pheonix +pepsi1 +normandy +night +never +minime +mellow +media +maryann +mariam +manning +manman +luckydog +liliana +leon +laserjet +just4fun +johann +jarvis +impulse +idiot +hilton +heroes +haha +greenbay +granada +graffiti +giorgio +galileo +fiction +fantastic +durango +doughboy +dortmund +donuts +dodge +delphi +delilah +dazzle +daniele +dan +crunch +cheers +carrera +carnage +carmel +building +bruiser +bombay +blue22 +bennie +bbbbbbbb +bassman +banzai +armani +annabelle +annabell +alex123 +alchemist +absolut +aaliyah +223344 +2112 +zimbabwe +worship +wisconsin +winchester +weekend +tunafish +truman +tolkien +thisisit +sticks +stafford +sputnik +spalding +sometimes +solitude +sofia +snooker +sex +sancho +robotech +rich +reader +rainbow6 +qazwsx12 +pulsar +protect +pooppoop +pointer +oxygen +onlyme +officer +minister +man +lynn +lola +lilly +leonidas +lemon +kungfu +kirkland +jarrett +integral +incognito +ilovesex +ignatius +honda1 +helper +heavenly +gustav +goblue +gggggggg +ferdinand +female +faggot +exchange +droopy +dogman +dark +combat +carroll +busted +bulldog1 +bravo +blackdog +bearbear +bacon +alan +911911 +6666 +1990 +123454321 +zaqwsx +wings +winfield +westlife +turtles +tricia +trainer +thriller +tarheel +synergy +summertime +spartans +snapple +smiths +skidoo +shell +sausages +salvatore +salamander +romans +printing +premium +poster +photos +palmtree +opendoor +ocean +obiwan +normal +nestor +mypass +mybaby +mosquito +milkyway +mexican +mcdonalds +maynard +mason +magnet +lucky7 +laughter +klondike +kitchen +kingsley +kings +kaylee +josiah +joe +jesus123 +ivan +irving +invisible +humphrey +hillside +hattrick +hampton +hammerhead +grinch +function +forgotten +fighting +excellent +estelle +esteban +easton +delaware +darthvader +dale +costello +corey +colonel +cisco +chief +catalyst +carla +cardinals +caprice +bucket +bolton +bobmarley +blanca +bermuda +batman1 +babylove +assholes +annika +andersen +amerika +alexande +Daniel +989898 +369369 +19891989 +1234asdf +xyz123 +xxxx +whiplash +wasted +wasabi +wally +visitor +usa123 +traffic +toaster +tiffany1 +tiburon +teddy1 +steele +sooners +solutions +smart +smallville +slimshady +sixteen +sergei +sammy123 +saint +romero +rockwell +robinhood +ripley +reddevil +qwerty7 +piano +pelican +pastor +palermo +ophelia +odyssey +nuclear +nipple +nana +mouse1 +morgana +mommy +mimi +maxwell1 +margie +major +mailbox +madeleine +louisa +lolo +loaded +life +ledzep +latino +larisa +lansing +kahuna +jordan1 +harriet +grendel +grayson +gordon24 +glendale +giovanna +frodo +frisco +foxylady +fortress +ficken +favorite +endless +doughnut +domain +direct +delaney +daniels +cutter +cristal +coucou +comanche +clark +cheshire +cherries +cheddar +cheater +century +catarina +butch +brett +bob123 +bigger +bertrand +benoit +barefoot +aubrey +armada +arabella +alligator +alissa +advance +aaa123 +1qaz2wsx3edc +zxczxc +ziggy +vanguard +titan +swallow +super1 +stuttgart +stephen1 +square +skating +shampoo +rockon +rhapsody +renee +redwing +reckless +ramirez +puppet +pumpkin1 +problem +powerful +pooh +phone +pervert +partner +painting +othello +octavia +novell +nocturne +nickname +narnia +mynameis +mikemike +martin1 +maison +llllllll +limited +leighton +lacoste +koko +kkkkkkkk +kingfisher +juniper +jorge +jokers +johnston +jagger +jade +holidays +hohoho +highway +henderson +handyman +gregor +fuckoff1 +front242 +flamenco +escalade +doudou +doobie +dogdog +division +delfin +decker +custom +covenant +cornell +colors +circus +churchill +changes +chandra cannibal -candyass +bummer +boots +bobo +bobby1 +blanco +bird +bertie +bears +badminton +azsxdc +ashlee +annmarie +alexander1 +alcatraz +agatha +a1s2d3 +11112222 +wwwwwwww +wildcard +whitesox +vincent1 +unlock +tyson +tycoon +twiggy +trojans +thornton +thalia +temporary +survival +supernatural +sunny1 +sprocket +sony +sonata +somerset +smarty +skorpion +skinner +services +saxophone +sacrifice +rotten +romania +restless +renato +record +pumpkins +paprika +packer +operation +nosferatu +newpassword +moses +monkey123 +middle +michelle1 +michal +meathead +mankind +management +lucky123 +licorice +laser +language +kronos +kismet +julio +jean +jacob1 +jackass1 +irene +infiniti +icarus +horror +homers +groove +goose +goalie +generation +gary +gamecube +foolish +flanders +electro +edinburgh +duckie +disciple +diplomat +darryl +crescent +cowgirl +counterstrike +cocaine +cluster +clemson +chunky +chippy +cherie +catholic +caravan +capoeira +calculator +browning +biscuits +bikini +baker +ambrose +alexalex +P@ssw0rd +Jennifer +19861986 +123456abc +yousuck +winston1 +whitey +virus +virgil +violator +transam +train +torpedo +tinman +tangerine +super123 +straight +stalin +sporty +sorcerer +sidekick +shredder +schubert +savanna +sanjose +racecar +prestige +presley +peter123 +pasword +nonsense +news +naomi +mulligan +moneyman +misha +matchbox +mars +march +marcela +marble +marauder +losers +longhair +lisalisa +killme +kieran +kayleigh +kakashi +jayden +islander +india +homeboy +gunther +grasshopper +geraldine +genesis1 +generic +gardenia +gabriele +explore +everything +emanuel +edmonton +dwayne +downhill +digital1 +denali +defense +davide +dana +cromwell +corazon +chowchow +cats +catman +carebear +candy1 +burnout +boxer +bounce +bettyboop +benito +benben +beastie +beans +ass +ashley1 +363636 +1984 +161616 +wizards +walking +volcom +viktor +vanessa1 +twelve +terrapin +tennessee +tasha +swords +stockton +stitch +steph +spartacus +smoothie +shinobi +seahawks +russian +revelation +rebecca1 +rangers1 +qweqweqwe +qqqqqqq +puppydog +portal +popular +physics +pete +norbert +nipples +nimbus +nestle +milkman +midway +meghan +marigold +margot +malachi +louie +longbow +lion +krypton +krissy +info +hurley +homerun +hoffman +higgins +hansen +hacking +gregorio +gotcha +goldfinger +glamour +giggle +ghosts +gangbang +freaks +fowler +fischer +finance +dutchess +dirty +dean +dealer +daylight +dawn +constantine +colin +cobalt +clueless +cloud +clever +chilli +chaser +caution +catcat +capone +calamity +blaze +blanche +bigdick +beefcake +bayern +basil +banker +babe +aquarium +anathema +ambition +amanda1 +address +a12345678 +222333 +1986 +19821982 +wildlife +vince +undercover +truck +tribal +transit +today +timeout +snowbird +shaolin +shanna +serpent +secret1 +schneider +saffron +rosita +rain +qwert12345 +qwerqwer +prospect +porsche911 +pinhead +perkins +pendragon +north +nike +native +natalie1 +mutant +momo +mallard +lunatic +lol +lockdown +lkjhgfdsa +letsgo +lala +junebug +jose +jellyfish +jameson +italiano +irishman +inter +infamous +hydrogen +hooper +hippie +hellboy +hartford +hammers +guess +gryphon +goodyear +glacier +generals +garrison +galant +foxhound +entrance +eighteen +earth +drake +dimension +diamante +denis +daedalus +current +crack +colton +cocktail +champ +chameleon +celina +callum +caligula +borabora +bondage +bonanza +behemoth +becker +bass +bart +bangkok +bambino +balloons +bachelor +andrews +amelie +adeline +313131 +234567 +123698745 +xerxes +waterman +volvo +trenton +thomas1 +teenager +suckme +stumpy +stellar +spanking +south +soccer10 +sergeant +seashell +seahorse +scroll +scarecrow +ruben +royal +riffraff +rick +rapper +radar +prowler +privacy +pothead +possible +pittsburgh +pissoff +pinnacle +peachy +paulie +paper +optimus +oatmeal +nostromo +members +maximilian +marc +mantra +malone +malice +lulu +lord +letters +latitude +kevin123 +kellie +kamasutra +jehovah +jared +italy +invasion +hugo +houdini +hopkins +honey1 +hibiscus +heyhey +harman +hans +hallmark +granite +goodboy +glasses +glasgow +fuzzy +fuller +flyboy +firestorm +fernandez +envision +enjoy +engage +ellie +editor +ecuador +devon +desperado +dejavu +daddy1 +cody +cicero +charcoal +character +cardiff +canyon +candace +camels +caleb +bronze +bonjovi +blue1234 +bigguy +berger +aurelia +antelope +angus +alejandra +aircraft +abby +753159 +456852 +314159 +303030 +1978 +1969 +123456aa +123456123 +1234560 +west +viktoria +vectra +unlimited +tundra +transport +topher +stripper +stinker +stefania +spinner +spiders +snowwhite +smirnoff +silly +shearer +sexual +seraphim +sebastien +sample +ronaldo7 +rockman +rivers +reporter +redskin +razor +rayray +ramsey +ramses +raiders1 +plumber +peach +painkiller +numbers +nineteen +muppet +morena +monolith +moneys +moneymaker +mishka +messiah +memories +memorial +massacre +manila +lottie +leland +legends +lamborghini +kimber +josie +jimmie +jazzman +hussain +huskies +honduras +habibi +goofball +george1 +gareth +fullmoon +fraser +forever1 +fester +ethan +enter1 +engineering +elefante +eatme +duck +dragonballz +doorknob +dipstick +deadly +crusher +compact +commerce +cecile +carousel +callisto +calico +builder +brilliant +blubber +bettina +berenice +barbarian +banane +backup +augusta +asdfzxcv +ariane +angeles +alex1234 +alchemy +alberta +advent +Welcome1 +9999999 +343434 +336699 +332211 +1qa2ws3ed +19871987 +12345678a +123455 +zaq123 +wormwood +wood +weapon +watcher +volkswagen +tomas +tipper +tahiti +starstar +spiral +spidey +sonics +solaris +snuffy +shrimp +sheep +sheba +sexygirl +sephiroth +screen +schumacher +sasasa +samiam +salsa +rudolf +rosewood +roses +rochester +roadster +reload +rapunzel +putter +prisoner +prescott +pizza123 +phillies +phil +phantom1 +perfect1 +pasadena +papaya +orange1 +optimist +norway +nitram +nikolas +myrtle +monkeyboy +molson +mikael +metropolis +master12 +marquis +luna +locked +larson +lakota +kimberley +killjoy +karine +junkmail +jingle +jigsaw +jenna +inspiron +hillary +hhhhhhhh +hellohello +griffith +greenwood +golfball +gator +gambler +fucku +forester +fergus +euphoria +england1 +edwin +discus +denmark +dell +death666 +cornelius +coolcat +constance +conquest +confirm +colt45 +clitoris +chips +chelsey +cesar +cartoons +buzzard +butcher +buckaroo +buck +bologna +bluejays +ben +angelic +analog +Alexander +789123 +787878 +1991 +1977 +123465 +winners +weenie +waiting +volunteer +violence +undead +ultra +tree +titties +testpass +terrence +temporal +tech +teamwork +tadpole +stevens +sport +spencer1 +soprano +social +skate +silverado +shipping +serendipity +saigon +roosters +retired +reflex +referee +redeye +prophecy +popcorn1 +playmate +pistons +paragon +panorama +p0o9i8u7 +noway +nonono +motion +mordor +meadow +marcopolo +manolo +magneto +luis +looker +lioness +lighter +leticia +landmark +kill +khalid +johnson1 +jess +jacobs +iverson3 +instinct +infected +illuminati +iceland +hunter1 +horace +honeydew +golfing +gilles +gabby +foundation +force +forbidden +floyd +flame +fidelio +esperanza +dogs +document +dharma +deutsch +deadline +dead +dahlia +dadada +crocodile +credit +cowboys1 +coolguy +climbing +choice +chicks +chamber +castor +cassius camping -cable -bynthytn +buddies +bubbles1 +briana +bremen +bluestar +birmingham +beretta +bathroom +bastian +barker +baltimore +balboa +anamaria +amber1 +aloha +88888 +33333 +258456 +25802580 +24682468 +123451 +yoyo +wildman +whiteboy +webber +vader +trinitron +topdog +titleist +tiberius +testing123 +talent +superhero +stoned +skydive +silvana +sienna +sidewinder +shitty +salami +ruby +rosemarie +rosalie +retarded +requiem +qqqqq +primavera +players +peppermint +palomino +outsider +oooooooo +musician +monarch +misfit +michelin +maria1 +mafia +macbeth +m +lynette +lowell +kimmie +june +juggernaut +ironmaiden +hyacinth +hamish +grease +goaway +gerbil +gavin +gatorade +fuzzball +fujitsu +feline +falling +everyone +dottie +dictionary +development +delirium +daisy123 +cyber +cutie +critical +cradle +corner +cordelia +collection +chivas +chiara +cat123 +carl +capitals +caliente +burning +bunnies +bunker +brent +bobdylan +blackrose +birdhouse +bighead +beta +bassoon +author +asparagus +anton +allegro +albino +Michelle +Jessica +898989 +654123 +545454 +1a2s3d4f +1982 +19781978 +wiggles +weston +walleye +voltaire +vodka +valiant +thedoors +test1 +tender +submarine +stress +stonewall +special1 +southpaw +soledad +soccer12 +slasher +simmons +season +scamper +sauron +sandy1 +sanctuary +s +ruthless +rugby +rivera +reuben +redstar +recall +reaction +rasta +rapture +racerx +quebec +qazwsxed +prometheus +portable +poisson +pizzas +pimp +pilot +perry +pepper1 +password11 +passcode +oyster +otto +omar +olive +official +newbie +neverland +mullet +morales +monsoon +mojo +misery +mindless +micro +masamune +leopold +lenny +lennox +legendary +lalalala +laddie +kirsty +kiki +kerstin +joel +jimmy1 +incredible +icecube +horatio +holloway +helios +heartless +hazard +harley1 +hairball +gollum +girl +genevieve +game +format +fireworks +eskimo +entropy +drew +doogie +dirtbike +dinner +dinamo +dilligaf +defiant +daewoo +cunt +crossfire +colette +clippers +chicago1 +cheeky +cheech +cayman +caldwell +butters +butt +bernadette +apricot +allan +aggies +agent007 +addict +adams +abcabc +321123 +282828 +19831983 +19801980 +19751975 +123000 +1010 +zzzzz +yellow1 +word +widget +waterpolo +warthog +warrior1 +vulcan +vertical +venture +timeless +thomson +thegreat +superuser +steve1 +steel +sssss +squall +spelling +source +someday +solo +snoop +slippery +silicon +shine +salman +rusty1 +russel +rumble +rrrrrrrr +roxy +rovers +robot +robocop +ricochet +reefer +redemption +reborn +raspberry +protocol +producer +priest +photo +penguin1 +patterson +p455w0rd +olivetti +oliveira +oicu812 +neville +mona +mnbvcx +meteor +metalica +mentor +melisa +mclaren +max123 +matter +martins +mannheim +mandingo +magellan +machines +lovebird +link +linden +leonie +lara +killing +karma +jubilee +jonathan1 +jason123 +inflames +important +idunno +heretic +helloworld +headache +hancock +hal9000 +godbless +glenn +giggles +gemstone +funky +fucked +ffffffff +fatass +emily1 +duster +danilo +danica +cyclones +cristiano +crazy1 +color +colonial +collie +claudius +citadel +chinook +cheeks +carver +burrito +bulgaria +brunette +bradshaw +bowser +boobie +blazers +bitter +beth +bastards +basset +basement +baron +baboon +baba +azertyuiop +astro +arcadia +applesauce +angelique +alvin +alice1 +albany +admin1 +acapulco +abacus +Charlie +786786 +25252525 +1987 +123789456 +123456987 +12312312 +zachary1 +yourmom +yingyang +xtreme +workshop +work +what +vicious +ulysses +twinkie +trueblue +transformers +thierry +tarantula +sycamore +sunderland +stripes +stigmata +sticky +stargazer +staff +shopper +seneca +sabrina1 +rollin +riccardo +qazxswedc +playboy1 +peppers +password01 +override +ontario +nomore +nighthawk +nickel +napoli +music123 +motdepasse +mortgage +moment +mickeymouse +meandyou +maxim +mantis +macdaddy +lovebug +lorelei +listen +leicester +laura1 +knockers +kisskiss +keenan +katrin +jjjjjjjj +invader +hysteria +honest +hilltop +gonzo +godlike +god +gallery +frank1 +forgiven +factory +evanescence +eugenia +ernie +equinox +dutch +distance +destruction +denied +cyrus +cosworth +cortez +console +coke +coconuts +clifton +client +cash +carlo +carlisle +buster1 +burgess +breakfast +booty +blinky +blink +blaine +bitch1 +bengals +astros +aspen +asgard +asdfjkl; +antivirus +aikido +66666 +31415926 +21212121 +123321123 +100000 +yokohama +worker +unforgiven +triple +tommy123 +tictac +therapy +surrender +spikey +spiker +spike1 +smithy +sixers +shoes +shiner +sheriff +sheepdog +shawna +seinfeld +sayang +sabotage +ronaldinho +richter +redfish +reddragon +rampage +prissy +pressure +pinetree +peggy +pavement +oriental +offshore +nutter +nice +newzealand +netscape +modern +misfits +michaels +meow +memorex +mathieu +mash4077 +mallorca +madagascar +licker +lawson +landon +kokomo +koala +kestrel +junkyard +johncena +jewish +jakejake +invincible +intern +indira +hawthorn +hawaiian +hannah1 +halifax +greyhound +greene +glenda +futbol +fresh +frenchie +flyaway +fleming +fishing1 +finally +ferris +fastball +elisha +doggies +desktop +dental +delight +deathrow +ddddddd +cocker +chilly +chat +casey1 +carpenter +calimero +calgary +broker +breakout +bootsie +bonito +black123 +bismarck +bigtime +belmont +barnes +ball +baggins +arrow +alone +alkaline +adrenalin +abbott +987987 +3333333 +123qwerty +000111 +zxcv1234 +walton +vaughn +tryagain +trent +thatcher +templar +stratus +status +stampede +small +sinned +silver1 +signal +shakespeare +selene +scheisse +sayonara +santacruz +sanity +rover +roswell +reverse +redbird +poppop +pompom +pollux +pokerface +passions +papers +option +olympus +oliver1 +notorious +nothing1 +norris +nicole1 +necromancer +nameless +mysterio +mylife +muslim +monkey12 +mitsubishi +millwall +millennium +megabyte +mccarthy +malina +magister +magick +maggie1 +madhouse +lopez +liverpoo +leviathan +latina +laetitia +kurt +kernel +kayla +karachi +joshua1 +joaquin +jennings +janina +jaime +holstein +henrik +hellraiser +head +harder +granger +freefall +focus +flawless +finish +emergency +edmund +ebenezer +dougie +divinity +delpiero +cyborg +cream +comedy +clovis +chewie +chewbacca +chastity +charlott +carlotta +camden +bunny1 +bumble +buchanan +bradley1 +bombers +blacks +best +bella1 +bell +behappy +battlefield +aventura +astral +ashanti +asdffdsa +arctic +anchor +academy +525252 +456654 +1979 +19741974 +090909 +zildjian +zaqxsw +wyoming +wingman +welcome123 +wargames +vvvvvvvv +viper1 +unicorns +toilet +timberland +things +tenerife +tasmania +tania +symphony +sweet1 +superb +stolen +stan +sssssss +spoon +splendid +sonyvaio +snapshot +slick +sleeper +simon1 +shining +sherri +sensei +seagull +scott1 +schmidt +saunders +sarajevo +runaway +route66 +rockey +reverend +redfox +quattro +prototype +proton +pooter +polaroid +pixies +pixie +perfecto +passme +owen +nurse +nookie +nokia123 +nitro +nights +nebula +natasha1 +mystical +milan +melanie1 +material +mariner +mamamia +mamama +maddison +macross +lost +lloyd +landlord +kristal +kris +korean +kenzie +kaktus +juvenile +instant +hybrid +horny +hollie +hawkins +harry1 +gypsy +gunnar +goodwill +goldwing +gilberto +gandalf1 +fuckthis +froggie +frisky +flossy +flapjack +flamengo +finnegan +fabienne +error +erection +defence +danny1 +dammit +conway +content +concept +climber +clemente +christophe +christa +charon +cereal +caterpillar +caterina +capetown +cancan +bull +brains +bracken +bolero +biggles +berserk +bacardi +austria +austin316 +antonio1 +angelito +amigo +alvaro +accounts +abstract +Robert +19911991 +19761976 +1976 +020202 +01234567 +zxcvbnm123 +wilhelm +warwick +walmart +walkman +vincenzo +vesper +turnip +townsend +tonight +thought +theater +technical +tazman +stoney +soccer11 +smithers +smiling +slugger +slash +skyblue +shooting +shitshit +shadow123 +senators +schwarz +sairam +sacramento +royals +rowena +router +redbaron +raven1 +qwert1 +proview +programmer +prison +present +porn +poipoi +percival +painless +ou812 +oberon +oasis +northstar +newspaper +myfamily +mongolia +miroslav +marbles +macarena +lumberjack +lee +landrover +lakewood +klingon +kkkkkkk +killer12 +keisha +kareem +incoming +immanuel +images +hometown +homeless +hockey1 +hillbilly +helmet +hellothere +gunter +guillaume +goodnight +giulia +giordano +gina +genocide +gabber +funtime +fiona +fanatic +ezekiel +etoile +enforcer +eight +eduard +drizzt +dreamcast +doodles +dispatch +developer +crayon +corsair +copenhagen +codename +clowns +clockwork +class +clarke +chick +cccccccc +caramelo +callaway +calculus +buzz +bugatti +bronson +brian123 +boom +blessed1 +bismark +berry +benjamin1 +bartender +bambi +attorney +asteroid +arianna +ariadne +aramis +angeleyes +ananda +almond +alfalfa +alcatel +akira +academia +aa +a1b2c3d4e5 +784512 +1975 +1972 +12131415 +yamahar1 +wilder +whore +wealth +warehouse +violeta +versace +venom +tuning +tucson +tricolor +tracer +tim +thecure +terrance +summer99 +stocks +stirling +stamford +stairway +spooner +specialist +sorrow +soldiers +slater +singing +showme +shitface +scorpio1 +rotterdam +ross +rollins +ringo +right +records +real +rainer +quest +principe +pizzahut +pizza1 +pepperoni +patricio +passwerd +pacers +orient +orgasm +orchard +okinawa +oilers +nigga +nautica +nathan1 +nasty +mulberry +muffins +mistral +melrose +meister +meagan +maximo +manny +malcom +luscious +lifeline +legoland +leelee +leaves +kirby +kickflip +kennwort +kathrine +katelyn +junk +josefina +johnnie +johnathan +jimbo +jesus777 +hornets +hopeful +hollister +hellsing +gofish +gianni +getout +funfun +frogman +fragile +fishman +excelsior +easy +drummond +disneyland +deutschland +delldell +cupcakes +crybaby +cottage +corina +complex +claudine +ciaociao +christia +checkmate +checker +check +centurion +catcher +cashmere +carthage +bosco +bookmark +bobobo +boarder +bluejay +bartlett +b +armand +armagedon +animation +alphonse +alessandra +Benjamin +5201314 +51505150 +424242 +2004 +1992 +192837465 +yumyum +yasmine +xxxxxxxxxx +xxx123 +woodside +winona +willem +willard +werder +water1 +warcraft3 +vengeance +vaseline +trinity1 +toxicity +tommyboy +ticktock +thor +terence +teachers +submit +strategy +sting +stephens +spiffy +spanner +snowdrop +snappy +smeghead +shutdown +sexysexy +script +santafe +rider +riddle +rachel1 +prosper +princesse +pretender +popsicle +polish +pinkie +piggy +philadelphia +petersen +pearson +pasta +password3 +pandas +oscar123 +orioles +nova +niners +nelly +natali +moonstone +meggie +mckenna +masterkey +maryanne +manowar +magicman +kittie +kingking +kerry +justus +juan +jonjon +jeannie +jarrod +identity +icehouse +humble +hannover +greedy +goofy +glorious +gizmo1 +ginger1 +gfhjkm +gathering +gardner +furious +forgetit +fishtank +finalfantasy +fifteen +fetish +fernandes +epiphone +elevator +elegance +drumline +doodoo +devilman +delta1 +delivery +cross +cooter +compass +chuckie +chrissie +carnaval +carlito +caffeine byebye buzzer -burnout -burner -bumbum -bumble -briggs -brest -boyz -bowtie -bootsie -bmwbmw -blanche -blanca -bigbooty -baylor -base -azertyuiop -austria -asd222 -armando -ariane -amstel -amethyst -airman -afrika -adelina -acidburn -7734 -741741 -66613666 -44332211 -31071990 -31051993 -30051987 -30011990 -29091987 -29061986 -29011982 -2828 -28101986 -28081990 -28081986 -28011988 -27111989 -27031992 -27021992 -26081986 -25081985 -25031991 -25031983 -24121987 -24091991 -23111989 -23091989 -23091985 -23061989 -22091991 -22071985 -22071984 -22061984 -22051989 -22051987 -22031986 -22011992 -21061988 -21031984 -20071988 -20061983 -20041985 -1qazzaq1 -1qazxsw23edc -19991999 -19061991 -18101985 -18051989 -18031988 -18021992 -18011985 -17051990 -17051989 -17051987 -17021989 -16091988 -16081986 -16061988 -16061987 -15121987 -15091985 -15081986 -15061985 -15011983 -14101986 -1357911 -13071987 -13061985 -13021985 -123456qqq -123456789d -1234509876 -12131213 -12111991 -12111985 -12081990 -12081987 -12071991 -1207 -120689 -1120 -11071987 -11051988 -1104 -11031983 -10091984 -10071989 -10071986 -10061985 -10051990 -10041987 -10031993 -10031990 -09091988 -09051987 -09041986 -08081990 -08081989 -08021990 -07101984 -07071989 -07041987 -07031989 -07021991 -06061981 -06021986 -05121990 -05061988 -05031987 -04071988 -04071986 -04041986 -03101991 -03091983 -03051988 -03041983 -03031992 -02081970 -02061971 -02051970 -02041972 -02031974 -02021978 -0202 -02011977 -01121990 -01091992 -01081992 -01081985 -01011972 -007bond -zapper -vipergts -vfntvfnbrf -vfndtq -tujhrf -tripleh -track -THOMAS -thierry -thebear +bukowski +brownies +bond +blue12 +bearcats +badboys +architect +ankara +amalia +albion +akatsuki +987456321 +567890 +19941994 +135246 +111213 +000001 +you +woofwoof +virginie +untitled +ukraine +tuxedo +tttttttt +troy +tommy1 +tommie +timothy1 +ticket systems -supernova -stone1 -stephen1 -stang -stan -spot -sparkles -soul -snowbird -snicker -slonik -slayer1 -sixsix -singapor -shauna -scissors -savior -samm -rumble -rrrrr -robin1 -renato -redstar -raphael -q1w2e3r -pressure -poptart -playball -pizzaman -pinetree +sushi +summers +stickman +starlite +spawn +southwest +snoopy1 +smarties +sexyboy +seaside +sarita +sanfran +sailormoon +robins +report +pickup +penthouse +peanutbutter +oxymoron +options +onetime +oleander +ohmygod +ocelot +oceans +nightfall +nicky +newjersey +new +ncc1701a +musashi +mullen +muhammed +morphine +moritz +mohawk +mobydick +merlot +meltdown +medieval +martian +marlins +mahogany +magic123 +lucinda +lonnie +longshot +lockheed +lkjhgf +livewire +lister +lakeland +konrad +kokoko +kleenex +killian +kenworth +interpol +integrity +hunter12 +hibernia +hermann +helpdesk +havefun +harbor +gymnast +guatemala +gospel +godofwar +godiva +gidget +genuine +fruity +frost +fishhead +everybody +ethernet +erin +emmett +elemental +ecstasy +duracell +dogfood +dempsey +delicious +daniel123 +custard +cthulhu +crystals +cool123 +confidence +comet +comeon +colossus +cirrus +chappy +callofduty +burner +bulls +buffett +bowwow +besiktas +belladonna +backlash +asylum +asdf12 +asddsa +anime +alanis +airforce1 +academic +abnormal +Jordan +Andrew +5555555555 +19901990 +1989 +1973 +123qwe123 +123987 +1234566 +zeus +wrestle +wendell +watch +violetta +vineyard +truffle +tigger1 +three +thistle +therese +terrible +tamtam +tabatha +sverige +suburban +stocking +steven1 +starbucks +stanton +springfield +spider1 +snuffles +smalls +sideways +sharma +sensation +schwartz +scania +salasana +runescape +rubbish +rosalind +rocking +rockie +robots +ringer +rhubarb +radiation +q1w2e3r4t5y6 +pussy1 +purple1 +purchase +protection +practice +poiuytre +piramide phyllis -pathfind -papamama -panter -pandas -panda1 -pajero -pacino -orchard -olive -nightmar -nico -Mustang1 -mooses +patrol +panacea +ninjas +nashville +naked +muriel montrose -montecar -montag -melrose -masterbating -maserati -marshal -makaka +mondeo +molly123 +mercer +medion +maximus1 +maryam +martine +mammamia macmac -mackie -lockdown -liverpool1 -link -lemans -leinad -lagnaf -kingking +mac +lunchbox +lucky13 +lookout +lonesome +limerick +liberty1 +lexus +kitty1 +kissing killer123 -kaboom -jeter2 -jeremy1 -jeepster -jabber -itisme -italy -ilovegod -idefix -howell -hores -HIZIAD -hewitt -hellsing -Heather -gonzo1 -golden1 -GEORGE -generic -gatsby -fujitsu -frodo1 -frederik -forlife -fitter -feelgood -fallon -escalade -enters -emil -eleonora -earl -dummy -donner -dominiqu -dnsadm -dickens -deville -delldell -daughter +jill +jaybird +insight +imagination +ignition +homebrew +higher +hellos +helicopter +harry123 +guido +guadalupe +groucho +greenman +godsmack +glory +gilmore +gerardo +fucku2 +flossie +firefire +fergie +faisal +empress +electronic +economics +doug +doris +don +disco +dino +declan +dayton +danzig +daniel12 +damon +damned +cricket1 +correct +cookie1 contract contra -conquest -compact -christi -chill -chavez -chaos1 -chains -casio -carrots -building -buffalo1 -brennan -boubou -bonner -blubber -blacklab -behappy -barbar -bambi -babycake -aprilia -ANDREW -allgood -alive -adriano -808080 -7777777a -777666 -31121986 -31121985 -31051991 -31051987 -30121988 -30121985 -30101988 -30061988 -29041988 -27091991 -26121989 -26061989 -26031991 -25111991 -25031984 -25021986 -24121989 -24121988 -24101990 -24101984 -24071992 -24051989 -24041986 -23091991 -23061987 -23041988 -23021992 -23021983 -22111988 -22091990 -22091984 -22051988 -21111986 -21101988 -21101987 -21091989 -21051990 -21021989 -20101987 -20071984 -20051983 -20031990 -20031985 -20011983 -1passwor -19111985 -19081987 -19051983 -19041985 -18121990 -18121985 -18121812 -18091987 -17121985 -17111987 -17071987 -17071986 -17061987 -17041986 -17041985 -16121991 -16101986 -16041988 -16041985 -16031986 -16021988 -16011986 -15121983 -15101991 -15061984 -15011988 -14091987 -14061988 -14051983 -13101992 -13101988 -13101982 -13071989 -13071985 -13061991 -13051990 -13031989 -123456n -1234567890- -123450 -1216 -12101989 -1208 -12071984 -12061987 -12041991 -12031990 -12021984 -1117 -11091986 -11091985 -11081986 -1026 -10101988 -10101980 -10091986 -10091985 -10081987 -10051988 -10021987 -10021986 -09041985 -09031987 -08041985 -08031987 -07061988 -07041989 -07021980 -06011982 -05121988 -05061989 -05051986 -04031991 -03071985 -03061986 -03061985 -03031987 -03031984 -03011991 -02111987 -02061990 -02011971 -01091988 -01071990 -01061983 -01051980 -01022010 -000777 -000123 -young1 -yamato -winona -winner1 -whatthe -weiner -weekend -volleyba -volcano -virginie -videos -vegitto -uptown -tycoon -treefrog -trauma -town -toast -titts -these -therock1 -tetsuo -tennesse -tanya1 -success1 -stupid1 -stockton -stock -stellar -springs -spoiled -someday -skinhead -sick -shyshy -shojou -shampoo -sexman -sex69 -saskia -Sandra -s123456 -russel -rudeboy -rollin -ridge -ride -rfgecnf -qwqwqwqw -pushkin -puck -probes -pong -playmate -planes -piercing -phat -pearls -password9 -painting -nineball -navajo -napalm -mohammad -miller1 -matchbox -marie1 -mariam -mamas -malish -maison -logger -locks -lister -lfitymrf -legos -lander -laetitia -kenken -kane -johnny5 -jjjjjjj -jesper -jerk -jellybean -jeeper -jakarta -instant -ilikeit -icecube -hotass -hogtied -having -harman -hanuman -hair -hacking -gumby -gramma -GOLF -goldeneye -gladys -furball -fuckme2 -franks -fick -fduecn -farmboy -eunice -erection -entrance -elisabet -elements -eclipse1 -eatmenow -duane -dooley -dome -doktor -dimitri -dental -delaney -Dallas -cyrano -cubs -crappy -cloudy -clips -cliff -clemente -charlie2 -cassandra -cashmoney -camil -burning -buckley -booyah -boobear -bonanza -bobmarley -bleach -bedford -bathing -baracuda -antony -ananas -alinka -alcatraz -aisan -5000 -49ers -334455 -31051982 -30051988 -30051986 -29111988 -29051992 -29041989 -29031990 -28121989 -28071985 -28021983 -27111990 -27071988 -26071984 -26061991 -26021992 -26011990 -26011986 -25091991 -25091989 -25081989 -25071987 -25071985 -25071983 -25051988 -25051980 -25041987 -25021985 -24101991 -24101988 -24071990 -24061985 -24041985 -24041984 -23456 -23111986 -23101987 -23041991 -23031983 -22071992 -22071988 -21121989 -21111989 -21111983 -21101983 -21041991 -21041987 -21031986 -21021990 -21021988 -20081990 -20061991 -20061987 -20032003 -20031992 -1qw23er4 -1q1q1q1q -1Master -19121988 -19081986 -19071989 -19041986 -18111983 -18071990 -18071989 -18071986 -18031986 -17121987 -17091985 -17071990 -17051983 -16091990 -15081989 -15071990 -15051992 -15051989 -15031991 -15011990 -14031986 -13091988 -13091987 -13091986 -13081986 -13071982 -13051986 -13041989 -13021991 -1269 -123890 -1234rewq -12345r -1231234 -12111984 -12091986 -12081993 -12071992 -1206 -12021990 -111555 -11111991 -11091990 -11061987 -11061986 -11061984 -11041985 -11031986 -1030 -1029 -1014 -101091m -10041984 -10031980 -10011980 -09051984 -08071985 -07081984 -07041988 -06101989 -06061988 -06041984 -05091987 -05081992 -05081986 -05071985 -05041985 -04111991 -04071987 -04021990 -03091988 -03061988 -03041989 -03041984 -03031991 -02091978 -01071988 -01061992 -01041993 -01041983 -01031981 -0069 -zyjxrf -xian -wizard1 -winger -wilder -welkom -wearing -weare138 -vanessa1 -usmarine -unlock -thumb -this -tasha1 -talks -talbot -summers -sucked -storage -sqdwfe -socce -sniffing -smirnov -shovel -shopper -shady -semper -screwy -schatz -samanth -salman -rugby1 -rjhjkm -rita -rfhfylfi -retire -ratboy -rachelle -qwerasdfzxcv -purple1 -prince1 -pookey -picks -perkins -patches1 +conflict +comeback +coldplay +cocoa +coach +clock +clara +civic +cheeseburger +chachi +carmine +cantona +braveheart +bramble +boohoo +bongo +bingo1 +beyond +bert +believer +bedroom +beaumont +bangladesh +banger +athlon +arrowhead +anytime +angelita +amores +alternative +aileen +agent +Thomas +456321 +23456789 +2002 +1999 +1971 +135792468 +112211 +1122 +woodward +woodie +wolverin +whatever1 +werdna +wellness +webcam +vishnu +tripper +torrent +timberlake +terrorist +temptation +teapot +swingers +supergirl +style +starman +squeak +solstice +snake1 +smooch +skylark +sheryl +scratchy +salinas +ruth +roosevelt +rockport +return +reilly +redlight +quake +puppy123 +puddles +pretzel +post +pompey +poker +pocket +play +persona +perfection +penny1 +pavlov +paulette password99 -oyster -olenka -nympho -nikolas -neon -muslim -muhammad -morrowind -monk -missie -mierda -mercede -melina -maximo -matrix1 -Martin -mariner -mantle -mammoth -mallrats -madcow -macintos -macaroni -lunchbox -lucas1 -london1 -lilbit -leoleo -KILLER -kerry -kcchiefs -juniper -jonas -jazzy -istheman -implants -hyundai -hfytnrb -herring -grunt -grimace -granite -grace1 -gotenks -glasses -giggle -ghjcnbnenrf -garnet -gabriele -gabby -fosters -forever1 -fluff -Fktrcfylh -finder -experienced -dunlop -duffer -driven -dragonballz -draco -downer -douche -doom -discus -darina -daman -daisey -clement -chouchou -cheerleaers -Charles -charisma -celebrity -cardinals -captain1 +panther1 +paisley +overtime +outback +orbital +omega1 +ollie +nopassword +nikolai +neutron +nazareth +mudvayne +movement +mother1 +mmmmmmm +miracles +milo +mike1234 +mikado +maxell +matisse +maserati +marihuana +marbella +luciana +lily +lifestyle +leroy +lamont +kiwikiwi +jurassic +jules +jim +jacky +infernal +hereford +guiness +goodtime +goodlife +goodgirl +garlic +gamecock +galadriel +gabriell +friends1 +foofoo +flatron +firefighter +ferreira +fenerbahce +farley +fanny +ethiopia +elektra +edgar +dogface +dionysus +different +devin +debora +deadpool +crossroads +colgate +closer +clint +clapton +christos +chauncey +catalog +castaway +carling +carefree +byteme +burnside +brewer +boulder +borussia +border +boomerang +bohemian +blueboy +blackice +blackhole +billy1 +billion +bigmouth +benji +barley +baptiste +bahamas +augustin +atticus +asian +asdfg123 +arlington +ambassador +alistair +alias +agustin +agamemnon +advocate +adgjmptw +acoustic +Princess +7894561230 +6666666 +666 +235689 +1qwerty +19811981 +1981 +1968 +123456q +122333 +11221122 +0 +zimmerman +youandme +yorkshire +wallpaper +vinicius +version +veronique +vauxhall +utility +understand +tyler123 +tiptop +the +terminus +sweeney +susie +surround +suckmydick +stronghold +storage +spurs +spice +sonora +soccer13 +snicker +sneaky +smokin +slipknot1 +slim +shauna +shaun +shades +sexylady +sessions +scirocco +schiller +schedule +sasha1 +sapper +sanjay +ruthie +rosebud1 +repair +regional +rainman +radiance +quarter +quaker +punk +portia +popo +poiuy +pioneers +phantasy +peaches1 +p@ssw0rd +orpheus +one +obsession +nigel +neutrino +mountains +moore +model +mike123 +marta +marmalade +maribel +mariano +malaga +lourdes +llamas +linda1 +lavinia +larkin +kilroy +kendrick +jamesbond007 +irvine +image +hogwarts +helloo +heinlein +hatred +harlem +hard +haley +guitarra +guitar1 +grande +gillette +germania +fun +fruitcake +flowers1 +fighters +field +feeling +fastback +farrell +fabrizio +export +exercise +essence +envelope +element1 +eeeeeeee +e +dynamo +doraemon +divorce +dickie +diabetes +destination +death1 +davenport +danish +damascus +cutlass +cubbies +corpse +coronado +cook +cloud9 +christo +chevalier +cheese1 +cashflow +carola +cardigan +canary caca -c2h5oh -bubbles1 -brook +buddah +british +boyfriend +books +bogdan +blueprint +blackboy +bitchy +bitchass +beacon +bbbbb +bball +backpack +babycakes +austin1 +arschloch +arielle +aquila +aquamarine +anakonda +aimee +adrien +abcxyz +Victoria +911turbo +8888888 +4runner +258963 +1993 +19851985 +19721972 +1234567899 +yogurt +worldwide +woody1 +witches +wiseman +water123 +vivien +viscount +violette +venezuela +vegas +undertow +traveller +transformer +topaz +toni +tombstone +tits +think +tessie +tennis1 +teacher1 +tank +tacoma +sword +surgery +surfboard +success1 +stuff +stratocaster +stephani +stainless +spikes +siobhan +silva +shania +sergey +seaman +scorpions +rudolph +rosanna +romain +rolando +ritchie +redstone +ready +premiere +planning +piranha +piper +peacemaker +paramore +panter +packers1 +outcast +numberone +nitrogen +natascha +mutter +munich +moonwalk +midori +meme +maurizio +matty +marzipan +mandolin +mamamama +maintain +macgyver +ludacris +loredana +london1 +logout +lillie +lexington +landscape +lahore +ladder +kristie +kodak +kim +killkill +khalil +justice1 +judy +joachim +jazmin +jailbird +ilovemyself +iiiiii +harrier +google123 +goodnews +golden1 +glass +gene +gatekeeper +gandhi +freshman +frankfurt +frankenstein +flower1 +flavia +firestar +etienne +erik +eleonora +dumdum +dreamland +dragon11 +domenico +dog123 +django +discreet +detective +darian +dalila +crossbow +crispy +creative1 +cordoba +cola +cock +clown +cleaner +citroen +christi +choppers +cheesy +canela +buddie +bryce +breathe +brando +bowman +bollox +bloom +betrayed +bernice +bernhard +benton +basilisk +bahamut +augusto +asdqwe123 +asdqwe +asdasd123 +armadillo +aries +antigone +annabel +altima +alterego +allie +alhambra +aladin +aerobics +advantage +adelina +Superman +Dragon +888999 +224466 +20012001 +1million +1983 +143143 +123qaz +yyyyyyyy +yellowstone +www +workout +woodruff +woodrow +woodman +verena +vampire1 +trout +treetop +tickle +texas1 +terra +tequiero +sylvie +surf +sunnyboy +star69 +spot +spence +specialk +sorrento +socks +snyder +smokie +simsim +simba1 +short +shiva +sevilla +school1 +salazar +sabres +rolex +rhino +reliance +ratchet +rajesh +qqqq +q2w3e4r5 +proverbs +prime +policeman +point +playgirl +pitcher +petra +persian +pentium4 +pedigree +partners +overdrive +oswald +origami +orange12 +observer +nomad +nolimit +noah +nnnnnnnn +nicholas1 +newworld +needle +navarro +morrow +morley +moriarty +more +mommy1 +mmmmm +misty1 +missing +minotaur +mikaela +metro +mazda +maya +margo +manunited +malaka +lydia +lori +location +leo +leavemealone +larry1 +komodo +knockout +knickers +kerrie +keepout +katie1 +kassandra +kamila +july +joelle +jemima +jelly +jeffrey1 +jajaja +ismael +ignacio +iforget +hi +hellyeah +harald +griffey +greentea +goodgood +giselle +gisela +germany1 +gasoline +garret +fugazi +fuck123 +fox +flashman +five +firestarter +fatty +fatality +fallon +evan +emiliano +ellipsis +doom +dogwood +disorder +dianna +device +deadlock +davidoff +dasher +couscous +county +construction +congress +comics +cloudy +cleaning +clarkson +christoph +cheerleader +charlie2 +ceramics +catnip +casandra +carman +carlson +caramba +cancun +campus +cambodia +budman +bridges +brain +blackstar +bigmoney +bigbang +ballerina +backbone +aurelie +astra +aragon +anfield +ananas +amnesia +alexandru +alexa +alessio +airhead +90210 +7895123 +74108520 +24681012 +24242424 +1password +1995 +19881988 +123zxc +123456123456 +12 +yesyes +yamato +x +wraith +whatwhat +westcoast +watching +underwear +truth +treble +tortuga +tomatoes +tiramisu +tiberian +thurston +tanaka +tammie +taffy +sutton +sun +stream +steffen +spinning +slippers +slave +slapper +simon123 +shayne +shasha +serene +sequoia +scuba +sadie1 +romana +review +response +reindeer +ransom +rambler +raccoon +qwertyuio +quinton +prosperity +porsche1 +pinguin +phones +payday +patch +password1234 +panchito +onions +nuggets +nottingham +noreen +niagara +nessie +mythology +mummy +muller +montana1 +medium +mayfield +marquise +manifest +mammoth +magnetic +lumina +lovelace +loser1 +letmein123 +lesbians +leinad +kosmos +kids +kane +joystick +jonny +johndoe +iris +inspector +industry +ilovejesus +husker +hunters +hola +herring +henry1 +hardy +hannes +hambone +gulliver +ground +griffon +goldman +gogo +gianna +getlost +gaylord +ganymede +ganja +galactic +furniture +forums +flashback +flanker +firenze +felix1 +fedora +fast +eyeball +esoteric +emmitt +elvis1 +elias +dropdead +drinking +diving +dingle +digimon +devildog +cullen +courier +copeland +cobain +christop +christian1 +chess +cherish +cheerios +cheerio +chatting +chantelle +changeit +chang +chad +cerulean +carrots +carmelo +carmela +cabernet +buckley +brendon +boiler +blackheart +bizkit +bizarre +bionicle +bertram +barron +bandit1 +baltazar +babes +auto +as +archery +amoremio +alpha123 +alleycat +allah +accident +abraxas +Joshua +353535 +1994 +19771977 +1970 +1964 +147369 +123mudar +wrigley +warfare +viola +veteran +tulips +trickster +trailer +todd +toast +tingting +thething +testing1 +tallulah +talking +taiwan +symmetry +sweeper +summer69 +sugars +stubby +stroke +stonehenge +ssss +spoiled +spark +smartass +sliver +sissy +shortcake +shakur +shadow11 +sex123 +series +seaweed +sarina +salesman +rushmore +royalty +roxana +rodolfo +resource +replay +rebirth +rayman +racoon +privet +pride +pregnant +praxis +pleasant +playground +platoon +plankton +peoples +pendulum +peabody +paterson +password8 +partizan +outlook +ottawa +olympics +nursing +northwest +networks +nederland +nate +napalm +mystique +mouser +mosaic +monte +models +mischa +mini +mickey1 +metallica1 +mendoza +mckinley +mcgregor +maxpower +matias +mathematics +marita +love12 +longhorns +longer +lombard +livelife +leoleo +lamer +lafayette +kokakola +kleopatra +kimkim +khan +keywest +katherin +kaboom +justina +julianna +jezebel +jessika +jeannine +j +horseman +homeland +holiday1 +hidalgo +hennessy +healthy +hazel +gunman +guesswho +greywolf +grand +gilligan +gifted +gentle +gasman +gallardo +freewill +franks +francisca +francis1 +fordf150 +fleetwood +flamer +fantomas +exotic +evil +eightball +eddy +echo +ebony +dutchman +drummer1 +diamant +dementia +deaths +data +cygnus +cousin +copycat +coolest +concert +compaq1 +coming +clay +citron +chaotic +cellphone +cattle +carissa +cadence +budgie +breanna +breakdown +bread +boring +blitz +blessings +binder +bethel +berliner +bengal +barnaby +atlas +ashraf +arnaud +antonella +anthem +andrew123 +aleksandra +adrenaline +acmilan +achtung +abrakadabra +Shadow +QWERTY +George +20002000 +1996 +19951995 +1967 +007 +zoltan +yoshi +yoda +woodwork +women +winifred +welkom +welcome2 +waterboy +wakeup +vargas +troopers +trees +torture +theodora +taylor1 +styles +stick +starlet +sphere +sound +sonoma +sometime +smackdown +skillet +shayla +sharp +sandeep +sagittarius +sadness +russell1 +rocketman +roadking +rifleman +riders +refresh +raymond1 +ramon +racer +qwerty13 +priyanka +private1 +pop +pizzaman +phantasm +pathetic +parliament +park +p +oldschool +norwood +norwich +norfolk +nicotine +nefertiti +nadia +motherlode +mormon +moose1 +mollydog +modena +mocha +minstrel +minicooper +milwaukee +millionaire +milk +midnight1 +matthieu +maroon +markie +marisol +maria123 +logical +logic +live +lipton +lemming +lebron23 +lander +lakshmi +lakers24 +kitty123 +kindness +kent +karla +javelin +java +invest +insurance +independence +homer1 +hippo +hero +heller +hatter +hatfield +hangman +gymnastics +gonzalo +goat +glover +gigi +getmoney +general1 +fuckin +fubar +freelance +forsythe +fontaine +final +fiddle +feelgood +fart +experience +evidence +erickson +enter123 +energizer +enable +dupont +downfall +develop +delores +delgado +deadwood +dani +dandelion +damaris +cumshot +crusty +crazyman +corporate +corinna +commandos +clarice +citation +chinchilla +changed +champions +ceasar +calliope +byron +broccoli +brenna +boozer +bone +bleeding +bigben +berserker +bergkamp +belfast +backstreet +asmodeus +asia +asdfgh1 +artistic +antilles +anteater +anhyeuem +amy +alameda +aaaa1111 +a1a2a3 +Sunshine +Jonathan +789654 +585858 +414141 +321321321 +1qa2ws +19731973 +19691969 +112112 +000000000 +wrinkles +wowwow +wishes +winter1 +website +vanity +trumpet1 +trotter +triplets +towers +totoro +toolbox +tomboy +terran +telecaster +tandem +talbot +sunnyday +summer12 +students +stockholm +steward +start123 +starshine +spam +spain +sopranos +slipper +sleep +slappy +sigma +siberian +shetland +sheppard +shamus +senate +scrapper +schooner +salina +rush +rosa +rogue +robby +ritter +rhodes +restart +regent +rebellion +qqq111 +qazwsx1 +psyche +poochie +pigpen +pershing +pecker +password7 +parasite +pantera1 +palmetto +overture +odysseus +notredame +noisette +nibbles +narayana +nakamura +mushrooms +mongol +moderator +metalgear +mediator +mcintosh +mazda626 +mayflower +massey +marykate +manpower +malamute +macaco +lukas +louisiana +look +loki +little1 +libra +lena +kryptonite +keaton +kathmandu +justin12 +junkie +jumping +jumbo +joker1 +jewel +jeronimo +jeremy1 +jeremias +jamaican +imperium +hurricanes +humberto +hotmail1 +horton +hoosiers +holly1 +henning +helmut +harpoon +goldmine +futurama +fulcrum +erotic +elisabet +effect +eden +earthquake +dumpling +dragster +dragon13 +doubled +dominica +dominate +didier +dictator +desperate +denton +darnell +corwin +corbin +cookbook +confusion +concerto +cole +christel +charge +chaplin +caster +cashmoney +cartier +breast +branden +book +boating +blank +blacksmith +bilbo +biker +bigone +bigcock +beholder +beebee +baddog +babushka +autobahn +audia4 +attention +atmosphere +anywhere +anjali +ancient +analsex +amateur +alright +allyson +aftermath +afrika +acidburn +abhishek +aaron1 +789987 +789654123 +44444 +321456 +123123a +100100 +zippy +zapata +z1x2c3v4 +winslow +whiteout +wertyu +welder +vickie +vicki +typewriter +trauma +topolino +thousand +thorsten +thematrix +tetris +symbol +symantec +sugar1 +stanley1 +stacie +splatter +spiderman1 +sorry +sonysony +smegma +slaughter +skull +shady +setter +seth +sensitive +schaefer +saphire +samsara +robbins +reddwarf +puddin +providence +position +popopopo +policy +pikapika +piercing +performance +pebble +pearls +peanut1 +pasquale +paramedic +pakistani +paddy +neil +neighbor +motorcycle +mireille +mierda +marcie +mantle +manga +manatee +makoto +makeup +lyndon +lucia +lovesick +loverman +london12 +lockwood +lockout +loading +lllllll +lifeguard +kowalski +kerberos +kellyann +karaoke +julie1 +jughead +johnny1 +jimmy123 +jayhawks +jarred +jarhead +ipswich +invalid +innuendo +incorrect +ilovemom +iiiiiiii +hummingbird +houston1 +horrible +hooter +himalaya +hill +highlife +hetfield +heartbeat +guitarist +graphite +gorgon +goodies +godisgood +ghostrider +gerhard +gamble +furball +funnyman +frenzy +frenchy +foreman +flip +flasher +f00tball +estate +erotica +epiphany +elvis123 +dogshit +discount +dipshit +danny123 +danielle1 +cristi +creepy +copyright +consumer +conquer +concordia +conan +complicated +clyde +clothes +clementine +city +chouette +chosen +chip +chinchin +chinatown +chinaman +chicco +chesterfield +cervantes +celestial +caracas +calderon +caitlyn +c +bullhead +buffer +brussels +broadband +brian1 +brasilia +boy +boricua +bookie +bobby123 +bluedog +bellevue +bank +bang +bagpipes +baby123 +aurelius +aristotle +altitude +althea +aloysius +alabama1 +airwolf +affinity +abcdefg1 +Password123 +Hunter +969696 +292929 +20102010 +09876543 +030303 +zxasqw12 +winters +winnipeg +whistle +wannabe +ultraman +treefrog +totally +tongue +tigercat +terrier +taratara +tactical +system32 +swastika +suzette +starr +spades +sneaker +smokes +skipper1 +simple1 +simeon +shaker +session +searcher +salem +rules +rodger +riviera +reserved +release +reject +redbeard +rebeca +realtime +rasmus +qwqwqw +qwert1234 +qwer123 +qwe123qwe +pyramids +provider +projects +production +poptart +poontang +planeta +pippo +pippen +pinecone +photon +pericles +pereira +pennywise +peavey +passing +paradiso +parachute +parabola +pants +palestine +overflow +nico +motorbike +mom +merrill +merlin1 +meeting +mechanical +mazdarx7 +mavericks +matrix1 +marybeth +marriott +marko +mario1 +manzana +madeira +madalena +mack +loophole +lonsdale +lolly +lingerie +libertad +leigh +ledzeppelin +lavalamp +kuwait +klaus +kkkkk +julieta +joselito +joker123 +johan +jerrylee +jan +jamison +jamboree +interest +inlove +imissyou +imation +human +hugoboss +hoosier +holahola +heythere +hellen +hehehehe +hate +hangover +guerrero +grinder +greatone +grammy +gianluca +giacomo +gardener +gangsta1 +galina +funeral +frieda +frantic +fields +farside +exorcist +espana +elizabeth1 +dressage +donner +dominic1 +dominator +domination +dodgeram +diver +display +devine +daisey +dada +dabomb +d +cyprus +cummings +crosby +corrie +corndog +commodore +colby +clemens +christen +chevy1 +callahan +calcutta +burberry +bumper +bulletproof +breezy brady +bombshell +blackburn +bimbo +betsy +betrayal +bearcat +avenue +atkinson +athletic +army +arachnid +arabian +angela1 +amaranth +alyson +altair +almost +allsop +alisa +algernon +alastair +alanna +absinthe +98765 +543210 +258258 +2020 +2005 +1965 +01020304 +zoomzoom +zimmer +wysiwyg +wonderboy +wiseguy +whatthefuck +watchman +warhead +vanilla1 +update +tugboat +trouble1 +troll +trivial +tripod +transform +trampoline +tortilla +torino +thunderbolt +termite +superduper +steaua +starry +squeaky +squadron +smile123 +skylight +skates +shower +shield +serial +score +schatz +sanfrancisco +salamandra +romario +rising +ricardo1 +reunion +resistance +reliable +recorder +radius +qwertyqwerty +quasar +puffin +provence +porsche9 +plato +pietro +piedmont +pentagram +patches1 +password9 +passed +parsons +paige +paco +overdose +omicron +oktober +oksana +nuts +nightman +nightingale +name +mymother +morgen +monument +missy1 +miamia +medicina +majesty +madonna1 +longtime +lolololo +lokiloki +littleman +lebanon +laughing +kilgore +kerrigan +karin +jordon +jeopardy +janjan +jamie1 +jackie1 +irina +iomega +inspiration +ibelieve +iamgod +houghton +horsemen +hootie +hondas +hologram +hideaway +hawaii50 +happydays +handicap +hamsters +hack +guillermo +gucci +gohome +gerber +georgia1 +geezer +gamma +fungus +freddie1 +forklift +food +flubber +finished +feeder +fairway +elefant +dorothea +dinero +devotion +deathstar +davies +darkknight +corsica +conchita +cocacola1 +classy +classics +chowder +chopper1 +choose +cecil +candies +burn +bumbum +buffalo1 +bubba123 +bridgette +brenden +bloods +blingbling +bigblue +bigballs +bebe +bean +barnyard +baphomet +badlands +badgirl +asterisk +arcangel +aol123 +antoinette +annemarie +anette +aditya +Richard +Master +Christian +4444444 +415263 +333666 +20022002 +200000 +19971997 +1966 +1963 +1960 +1234567891 +100200 +zzzzzzz +zxcv +zebras +wizzard +wild +whoknows +weirdo +weed420 +wazzup +victoria1 +useless +uniform +ulrich +tulip +trousers +treehouse +tranquil +tower +toriamos +tenten +temppass +temp123 +teardrop +superboy +stories +states +srinivas +solange +snowman1 +slammer +skills +shuffle +shortcut +shockwave +shocking +shelton +shelter +senha123 +scranton +sandoval +sandie +roseanne +riddler +rewind +red12345 +recycle +punjabi +prospero +pronto +products +process +pokey +playing +pepe +patrik +paperclip +papamama +paolo +padres +outdoors +otter +osborne +organic +nightwish +nemesis1 +nanook +nagasaki +mousepad +morrissey +morgan1 +monkeyman +modeling +minute +microlab +mick +mariel +margaux +maranatha +manish +mamma +makeitso +maine +maelstrom +luck +lineage +limpbizkit +lightbulb +lettuce +lalakers +kiwi +kirk +katharina +kakaroto +kaitlin +juggalo +jjjjjjj +jimjim +jewell +jesse1 +jeannette +jay +jaeger +jack123 +investor +insecure +ice +humanoid +hotline +hotel +hotboy +hondacivic +holler +holiness +hiroshi +high +hewitt +helpless +hello2 +healing +halo +hallelujah +haircut +guilty +greenhouse +great1 +graphic +grace1 +gigolo +ggggggg +germaine +georges +garland +gamer +gallagher +freefree +francais +forbes +follow +flora +flicker +firestone +firebolt +filipino +federica +fathead +fantom +falstaff +extra +evening +eleven11 +electronics +economist +durham +dunlop +dummy +dominant +dogcat +dogbert +diabolic +diablo2 +descent +degree +deadbeat +crockett +crazycat +comrade +composer +colombo +collier +coleslaw +citrus +cincinnati +chloe1 +cheval +cherub +chatter +cesare +cayenne +cascades +cantor +camilo +brook +bretagne +breasts +breaking +boxers +bourbon +bluenose +bluegrass +block +bisexual +binky +billions +billbill +bigbrother +belgium +beckham7 +avengers +athletics +assembly +asasasas +apple2 +anal +amore +allstars +ali +alakazam +agosto +adamant +activate +abcde12345 +abbey +Pa55word +Computer +794613 +777 +369258147 +1q2w3e4r5 +1997 +192837 +125125 +123456qwerty +zzzz +zoom +zombies +zerozero +zapper +windowsxp +whopper +whales +wachtwoord +voyage +vitamin +vigilant +verygood +vandal +under +trustnoone +truffles +trash +toothpaste +tigris +tigerman +thirty +thinker +thankgod +test12 +terrell +telefono +sweetwater +swatch +summoner +suicidal +strummer +striper +stiletto +start1 +stadium +squishy +squire +squeaker +springs +sixtynine +sithlord +siegfried +showcase +shibby +shandy +serenade +sepultura +secret123 +scrooge +rudy +rotation +romulus +rockhard +reserve +reeves +raisin +raining +quintana +pussies +purity +player1 +pepsicola +passenger +paris1 +papito +pacifica +orwell +ortega +optical +omsairam +obelix +nonstop +nightshade +newhouse +nazgul +napster +nairobi +nacional +muenchen +movie +mousey +motorhead +motley +morrigan +montecarlo +minette +michael2 +metroid +memememe +maybe +maximize +marino13 +marciano +manual +macdonald +lovegod +loveable +long +logan1 +loco +linux +lethal +lampard +lakeview +kurtis +konstantin +kenneth1 +junior1 +jukebox +jamal +ilikepie +hyderabad +hotspur +historia +highschool +hiawatha +hermitage +hendrik +haggard +grunge +gromit +gretel +goodtimes +getsome +gerry +gatsby +funk +freeport +flathead +fishy +filippo +faulkner +falcon1 +explode +evelina +endymion +emirates +edition +dresden +dreamers +dragon69 +douche +dooley +district +dingbat +dildo +dietrich +demonic +deicide +dannie +cyrano +crayola +cranberry +colibri +cockroach +cliff +clemence +claudia1 +classified +chriss +chocolate1 +chemist +chelle +chateau +cellular +catherin +carmella +canucks +calibra +butterfly1 +burgundy +bugaboo +brutal +brother1 +breath +branch +bonzai +bolivia +blooming +blitzkrieg +blender +bladerunner +bigboobs +bible +beijing +beavers +beachbum +barclay +barbara1 +balder +badgers +backyard +backward +babybear +argonaut +appleton +amour +alonzo +allied +aliyah +alina +aguilera +adonai +abundance +Nicholas +Michael1 +Anthony +9999999999 +676767 +373737 +321 +258369 +2009 +1qazzaq1 +172839 +13243546 +12qw34er +123456ab +000007 +zelda +zealot +zaragoza +worlds +woodcock +wolfen +wisteria +wilma +westlake +wert +vitoria +victoire +untouchable +tyrant +trapdoor +torment +tom123 +tigereye +thetruth +testicle +teste +team +talon +tabby +superbowl +student1 +stripe +store +sprinkle +snakebite +smart1 +silencer +sheeba +sharpie +shakti +shade +servant +sector +secreto +secretary +scottish +sanderson +sanandreas +sage +rockies +robertson +riddick +richelle +richardson +retire +rene +religion +redmond +rastafari +rashid +quiksilver +queenbee +pugsley +psychology +pool +playhouse +planes +physical +philipp +pensacola +pedersen +peace1 +pat +password0 +paperboy +pandemonium +outkast +origin +optima +nikolaus +nickie +newyear +newuser +murderer +morten +montero +montague +mockingbird +mindy +milagros +mercutio +mercurio +mcknight +maxpayne +mature +marmar +marie1 +mari +marcin +mandragora +manager1 +mamacita +malika +magics +madhatter +lucretia +loveya +love4ever +lorenz +lol12345 +logger +leilani +lauren1 +laura123 +kusanagi +knoxville +kira +kemper +katmandu +katina +kamala +kaka +julianne +juju +joseluis +jiujitsu +jingles +jeanpaul +ivanhoe +inspire +infrared +industrial +ichigo +hustle +humbug +humanity +house1 +hotwheels +hot +honeypot +honeybun +hester +heroin +herkules +heartbreaker +hawkeyes +hattie +hank +gregory1 +gilgamesh +ghost1 +geometry +garner +gaming +g +friedman +freiheit +freezer +foghorn +flashy +firework +finley +federation +fear +family1 +exeter +executive +exclusive +excellence +esprit +emotional +elohim +elbereth +edith +dylan1 +dragon99 +draco +dominus +dollface +devilish +derby +democrat +darkmoon +cretin +creeper +creamy +crackpot +cracked +costarica +costanza +cortina +corky +core +consuelo +clarisse +clarion +citibank +cingular +chrystal +channing +casio +carvalho +carolin +buffy1 +brownie1 +bluebear +birgit +billyjoe +beyonce +benedikt +beaufort +batman12 +barnabas +baracuda +banks +banana1 +baggio +augustine +assault +armitage +angell +alex12 +alcapone +afterlife +adrianne +acacia +a1a2a3a4 +Internet +Football +1998 +12345q +zappa +zack +yourself +yorktown +yeahyeah +xyzzy +winning +wildflower +weiner +web +waffles +victor1 +vantage +valdemar +unlocked +unleashed +twinkles +trujillo +torrents +tootie +tonyhawk +tobacco +tiny +tanzania +takedown +takamine +suresh +supra +supercool +subwoofer +storms +stitches +steiner +steeler +standing +stalingrad +srilanka +spliff +spirits +sparhawk +slowpoke +sizzle +shoelace +shiraz +service1 +senorita +seashore +sandstorm +sachin +sable +roulette +rocky123 +reboot +rambo1 +ralphie +radiator +quinn +q1q1q1 +problems +powerhouse +powered +postmaster +platform +plague +picnic +penner +paulo +parallax +outlaws +ostrich +obvious +oakwood +noel +niklas +nepenthe +naples +moonmoon +merrick +megan1 +mason1 +marconi +mansion +malik +mackie +lovehate +lovable +livingston +lifesucks +lickme +leo123 +leandro +labyrinth +kookie +komputer +kikiki +kerouac +joy +jeep +jazzy +jackhammer +intrigue +interface +interact +insider +imogen +hummel +honeymoon +hikaru +helium +hejsan +hayward +hansel +grapefruit +government +gossip +godfrey +ggggg +geology +geography +garnett +galloway +fullback +fuckhead +finder +fellow +faith1 +fairview +fabio +example +ella +eliana +edwina +eating +down +dondon +divorced +disabled +deputy +defiance +deeznutz +deep +ddddd +daniel01 +dan123 +cristy +cristo +council +cookies1 +communication +cocksucker +chocho +cheating +chakra +catalin +casper1 +casimir +carlin +carcass +candles +bush +buckwheat +break +bozo +boob +boner +boat +boarding +blackdragon +bergen +batata +basic +baseline +bandicoot +baldrick +back +arcane +apollo11 +annamaria +angola +ambulance +alvarez +aluminum +ahmed +acer +abercrombie +852963 +777888 +727272 +6543210 +357159 +2468 +19931993 +19791979 +zach +yugioh +youyou +woodwind +woodpecker +woodbury +whoami +watchdog +vikings1 +videos +vicente +vedder +vanille +unhappy +turbo1 +tribunal +total +toreador +tigerwoods +thinkpad +thebeach +test12345 +terrific +teaching +tantra +syzygy +supper +supermario +sunfire +sundown +successful +stud +stringer +stop +star123 +sovereign +souvenir +sombrero +skip +sk8board +sincere +simons +siberia +shuriken +shotokan +shock +shinichi +shawnee +sevens +scouts +scooters +schroeder +schnitzel +sargent +sanford +rugrats +rosalinda +rob +riches +rhinos +regiment +redbone +reaver +ramrod +rainfall +qwerty78 +qweasd123 +qpalzm +q123456 +puertorico +ppppppp +pop123 +plokij +planner +piston +pistache +pianoman +payment +paddle +paddington +overseas +orville +orthodox +nietzsche +nettie +needles +nachos +motor +mooses +moonman +monorail +momdad +missie +miss +minemine +milhouse +mickie +mermaids +memento +melon +maverick1 +margarida +mansfield +malena +madrigal +london22 +linus +lima +leander +lasalle +krakatoa +korea +karen1 +junction +joyful +joseph1 +jolene +johnboy +jenjen +jello +jamess +intranet +impreza +imperator +hunter123 +humility +hubbard +hotsex +horney +holy +hermit +hedwig +harmless +harlan +graves +grass +graeme +grace123 +googoo +giuliana +gauntlet +ganesha +fugitive +fuckyou123 +frazier +flatland +fenris +feelings +fabregas +esquire +escobar +entertainment +emanuele +elodie +election +dumpster +douglas1 +cruzeiro +crowley +crafty +cracking +cooper1 +control1 +compute +code +cobra1 +chillin +cheaters +centrino +carrier +captain1 +canberra +calling +caliban +bricks +botswana +bobber +blockbuster +blahblahblah +blackfire +blackbelt +bestfriend +base +banjo +bailey1 +autocad +atreides +athlete +asuncion +astronomy +astroboy +assist +aqualung +annie1 +andrej +amnesiac +amiga +allgood +adorable +Patrick +Matthew +David +3edc4rfv +2003 +1z2x3c4v +19921992 +14141414 +12211221 +120120 +zinger +yankees2 +yahoo1 +wrench +worldcup +witch +winger +wholesale +wendy1 +vulture +vittorio +vishal +vera +uuuuuu +underwood +underwater +ulises +tupac +trial +track +tracie +trace +toxic +touchdown +tonton +theworld +thebeast +thaddeus +telemark +tango1 +sylvania +surveyor +suitcase +sucks +stroller +stripped +stratford +stallone +spock +speedster +sniffer +smoke420 +shop +septembe +scales +saviour +sasa +sandbox +sandberg +samira +saber +rowland +rousseau +robin1 +revenant +redford +rattler +raffles +purdue +protector +protected +product +prasad +poppet +pianos +pepsi123 +pembroke +password4 +password13 +parkside +paint +outbreak +ohyeah +ocarina +obsolete +nyquist +nutshell +nounours +nonenone +nine +nigger1 +nielsen +nichols +nada +multisync +mueller +mousse +momentum +microwave +michele1 +mehmet +marguerite +maldives +magdalen +longbeach +lockhart +lawless +lantern +land +krokodil +kraken +khaled +kensington +kenken +just4me +junker +illegal +igor +icetea +i +humboldt +homebase +hippos +hhhh +headshot +headless +hazelnut +harmon +hades +guru +gremlins +golfer1 +geordie +frankie1 +frank123 +fireman1 +fireblade +faceoff +fabiola +external +entering +ellis +elegant +electrical +east +eagles1 +dulcinea +duffer +drums +dropkick +draconis +dont4get +domestic +dododo +doc +dirk +dimple +diddle +delmar +delano +daydreamer +darkwing +curly +cummins +corporal +colour +cocorico +closed +cleo +chino +chimaera +cheyanne +chavez +centre +centaur +celebrate +cashew +carsten +caballero +bully +breakers +braxton +brainstorm +boys +boogers +bluesman +blackpool +bethesda +beluga +beatle +bavaria +basketba +ballin +aviator +ashish +around +aprilia +antichrist +andyandy +allison1 +aisling +agnes +adolfo +accent +abcdefghi +William +Garfield +Abcd1234 +797979 +656565 +646464 +2010 +1236987 +050505 +yummy +yoyoyoyo +yeahbaby +yahweh +wwwww +wilfred +whites +wetter +wetpussy +wanda +villa +vergessen +vaughan +variable +urchin +unicorn1 +ttttttt +trustme +trillium +trey +tralala +torrance +tool +tikitiki +tiger2 +thumper1 +thesaint +theforce +thecat +tessa +teiubesc +tables +sweet123 +survey +sunbird +sunbeam +suck +succubus +stockman +steve123 +stefani +spleen +speeding +sonya +solitaire +sokrates +slut +slingshot +slayers +skateboarding +silverfox +showboat +shifty +sherwin +sexy123 +sequence +schultz +satanas +sandra1 +samuel1 +sambo +rottweiler +roma +rita +rincewind +rimmer +rico +ribbon +reveal +redhat +rainmaker +racers +qwerty99 +punker +postcard +polkadot +photoshop +persimmon +perfume +passes +parole +paradis +pandabear +panda1 +outland +orlando1 +open123 +nymets +nutrition +nowhere +nora +no +niggers +nicolas1 +nicknick +nectar +navajo +naughty1 +mysterious +murdock +mortis +morocco +montoya +momomo +miller1 +micky +mercy +meaghan +maxi +mauser +marine1 +marielle +maneater +lucian +loves +lizzy +lions +lionlion +liberal +leningrad +leapfrog +larsson +langley +kristopher +korn +koolaid +kool +kirkwood +kilkenny +kidney +kalle +jordan12 +joe123 +jerry1 +jediknight +jazmine +jacket +jabberwocky +intercom +intense +ingram +informix +include +illini +ib6ub9 +hunger +howdy +hounddog +hoops +homicide +hijack +herschel +hermosa +henrietta +hellcat +hatteras +harakiri +halfmoon +gunslinger +guide +gretzky +greeny +goodwin +gomez +glider +fremont +four +forgive +flint +flavor +fivestar +firewood +expedition +executor +euclid +elcamino +egyptian +edmond +eclipse1 +duckling +drumming +drifting +dorado +door +donny +dodo +denny +debra +davida +daisydog +dagmar +cute +crisis +court +cortney +coolgirl +contrast +collector +club +close +ciccio +choclate +chilling +channels +cerise +catapult +careless +capitan +californ +cadbury +bullets +brunswick +brick +brendan1 +braindead +bored +blunts +bluedragon +bloodline +blind +binary +bimmer +beverley +becca +bbbbbbb +barrel +baptist +audio +audi +atalanta +astonvilla +assword +ashley12 +asdfghjkl1 +art +arizona1 +antihero +andrew12 +andrea1 +anabel +allright +akasha +airman +ab123456 +aaasss +43214321 +369852 +2525 +2222222222 +2121 +1a2s3d +1974 +1961 +18436572 +162534 +123567 +123456654321 +1234561 +1231234 +1000 +youssef +yeah +woodlands +windows7 +wilkinson +wibble +white1 +wellcome +walters +waldemar +vanish +valerian +true +tristan1 +trilogy +tricks +trekker +tornado1 +thunders +thomas123 +testament +tennyson +taxman +tarragon +tapestry +tajmahal +sunny123 +struggle +storm1 +starling +starchild +spoons +spaniel +sodapop +sobriety +snowfall +snickers1 +skyline1 +skyhawk +shirley1 +shalimar +sexyman +settings +sebastian1 +schnecke +satriani +sasha123 +sameer +sailfish +roserose +robson +rickey +restore +rejoice +reference +raiden +rafaela +qwerty22 +qwedsa +qqqqqqqqqq +puffer +proteus +print +princes +prashant +prancer +ppppp +powerman +powerade +playstation2 +plastics +planets +plane +pinkpink +pieman +patron +patate +parents +parallel +papercut +p4ssword +olympic +offline +nutella +numlock +norma +nicolai +navyseal +mufasa +monopoli +moises +mnemonic +millenia +mercenary +membrane +mayfair +manitoba +magyar +magda +maddox +madcat +lineage2 +limelight +leopards +leeann +lasers +kurdistan +kittys +kindred +kimball +kidrock +kassie +karoline +kali +johnpaul +jenson +jeanine +jasmina +jamila +jaguars +jackman +ismail +interior +interesting +insomniac +idiots +homepage +hello1234 +hello12 +heavymetal +headhunter +harvester +hartman +halcyon +guitars +greeting +gray +goodie +goodday +golfgolf +godson +go +glassman +gladstone +galatasaray +galahad +fruit +friction +foot +florin +filthy +filomena +fffff +felice +fabien +eulalia +ethereal +emotions +dudedude +drizzle +drive +douglass +doofus +dominika +doll +divers +diva +dipper +desperados +demetrio +demented +deliver +deepak +decision +datsun +darrel +cuthbert +culture +crunchy +crappy +cornel +consult +compound +comatose +clocks +civilwar +circuit +chessie +charleston +chariot +chan +castello +caspar +carioca +candie +cachorro +bushman +bulletin +brown1 +britain +brandnew +braden +boswell +bogus +bluegill +blue32 +bloodhound +blondy +bliss +blanket +blader +billing +beautiful1 +baywatch +bastardo +baraka +bagheera +babette +avanti +aurore +aspirin +asimov +arrows +arcade +april1 +annalisa +anatomy +allstate +allegra +algeria +alfaromeo +aldebaran +alberto1 +agenda +actress +accept +Samantha +Jackson +Elizabeth +963963 +78945612 +654654 +2fast4u +2cool4u +2006 +1957 +1598753 +159632 +1234568 +01010101 +0007 +zxzxzx +yellow12 +woman +wolverines +wolfhound +wildbill +whittier +werty +watkins +warrant +vittoria +virgilio +vegetable +vangogh +uptown +upgrade +unbreakable +umberto +trusting +troubles +triplex +trading +tonytony +times +tiamat +thebest1 +terriers +template +temper +telefoon +talented +table +superpower +supermen +sugarplum +steroid +starting +sprout +spartan117 +sowhat +sophie1 +snowball1 +smurf +slimjim +sixpence +simplicity +sigmund +sidewalk +shoshana +shivers +shammy +seville +setup +serrano +section +schools +sasquatch +samtron +rugrat +roxane +rowing +rotary +rodent +rocky2 +resist +repeat +renate +relax +read +rattlesnake +rainbow7 +rafferty +qwerty77 +qwerty00 +pussys +promotion +pokemon123 +pinocchio +philosophy +philippines +pheasant +petter +pentium3 +pawpaw +patrizia +parking +parade +overlook +overhead +operations +okokokok +ohshit +oddball +nwo4life +novembre +nostradamus +niggas +nexus +newlife1 +newdelhi +nervous +myspace1 +myfriend +munchies +mouses +mountaindew +moneybag +molecule +mistake +miki +midnite +mercury1 +melville +mcintyre +mattress +marylou +martino +marshmallow +marmite +maritime +mariachi +maple +makemoney +magali +maddy +luckie +lucien +loveyou2 +lovesong +lolalola +lindsey1 +lifeboat +lana +kitties +kimono +katie123 +kasandra +kara +kaplan +kalamazoo +jupiter1 +jump +julia123 +judge +jordan123 +jockey +jenni +jackrabbit +isabela +intelligent +innocence +india123 +iamthebest +hundred +hollis +heyman +henry123 +henrique +hellion +hardball +handbook +hacienda +guilherme +grenoble +gotmilk +goodmorning +goddamn +giuliano +genie +geisha +fudge +frostbite +fresno +freehand +fragment +foreskin +folder +fido +f +explosion +experiment +erwin +erasure +ensemble +elisa +eclectic +duffy +ducky +dotcom +dong +dogger +dogfight +dodgers1 +disease +diogenes +dillweed +dickinson +derick +demon666 +demetrius +daybreak +darrin +dapper +dagobert +curtain +culinary +cuervo +crossing +cronos +croatia +coolboy +controls +consulting +cobblers +coaster +climax +click +clare +cindy1 +chrono +chill +chatterbox +charlie123 +charissa +changer +celebrity +campos +cable +buster12 +bungle +bungalow +bullit +brock +broadcast +brianna1 +boxcar +bootleg +bodyguard +bella123 +belkin +belize +beaker +barnett +ballroom +azrael +artur +aria +arbiter +andrzej +andre123 +analysis +ana +amber123 +all4one +alegria +albania +afghanistan +addiction +abc321 +aa123456 +Phoenix +686868 +434343 +2wsx3edc +2bornot2b +225588 +147741 +12131213 +zxcasdqwe +yes +yannick +wyvern +wwwwwww +writing +witchcraft +wertwert +weight +warcraft1 +wallet +vivienne +vivaldi +virago +versus +vermilion +vega +usarmy +unity +ultrasound +tweeter +tuppence +tropicana +trafford +tototo +teddy123 +t +survive +summer123 +strife +streamer +strato +stifler +starburst +star1234 +stapler +ssssssssss +spotlight +specialized +sparrows +songoku +solomon1 +soloman +solid +sloppy +simply +sideshow +shimmer +sherpa +sherbert +sentry +seminoles +sebastia +seagate +scribble +sarasota +sarasara +sarah123 +sanguine +sandy123 +sand +samwise +samsung123 +saibaba +robert12 +rhythm +request +reflection +redhorse +rational +raptors +ramiro +rakesh +radioman +qwerty01 +punjab +protein +progressive +poophead +plutonium +phantoms +pepino +peddler +password00 +passage +paperino +panic +panache +page +ozzy +osprey +organize +optiplex +october1 +null +nokia1 +niki +neverdie +nantucket +munch +mothers +moron +morbid +mooney +moondog +monsieur +monkfish +monica1 +modem +mmmm +minimal +mineral +midland +melodie +megane +mauritius +master01 +marymary +marvelous +marnie +mark123 +marduk +mann +manifesto +mahesh +macleod +machete +macedonia +lumber +lullaby +luckyme +lucas123 +loyalty +lovejoy +logistic +locker +llama +lili +libido +leprechaun +lemmings +langston +krusty +kipling +killer11 +killah +karl +kappa +joyride +joking +jimenez +jeffry +jayhawk +jack1234 +itsme +ireland1 +invent +innovation +import +iiyama +ihateu +hungary +house123 +honeys +holla +hihihihi +hhhhh +hemlock +hellhole +healer +hardwood +grandad +govinda +ginny +gentry +generator +gazelle +gaspar +funhouse +fullhouse +fulham +freebie +franny +foxfire +flowerpower +fiorella +farewell +fantasma +fall +faithless +fairy +failsafe +explicit +esposito +enters +enchanted +elissa +duckduck +drilling +drawing +dragon10 +doremi +doors +doodlebug +donjuan +dickweed +dewey +denial +demon1 +dallas1 +crunchie +crawfish +craft +conker +condition +chessman +charter +chanelle +chamonix +celebration +candys +candy123 +brotherhood +briggs +brewers +brainiac +borneo +bomb +bluewater +blocked +birdland +binladen +billings +before +barlow +bareback +bacteria +authority +astronaut +asdfqwer +asd12345 +arrakis +arpeggio +appleseed +anthony2 +animator +analyst +amazonas +alpacino +ajax +airline +adelaida +adamadam +aaron123 +Einstein +Buster +Bailey +Ashley +90909090 +741963 +5150 +444555 +369258 +1962 +12qwas +1234zxcv +101101 +zebulon +youtube +yasmeen +yamamoto +wormhole +witness +windows98 +wiggle +whiteman +westgate +watchmen +walden +visa +virgo +vergeten +veracruz +vanquish +uuuuuuuu +urban +undefined +tugger +trucking +trooper1 +tramp +tosser +tormentor +tomate +timelord +timberwolf +thrust +tangent +taichi +synapse +supers +stupid1 +strings +strangle +stoneman +stokes +starless +spiritual +spinach +spagetti +soviet +sorensen +somethin +snuggle +snowhite +snooze +smiler +slovakia +sledge +skydiver +skunk +sinful +silvester +silicone +silencio +siamese +shevchenko +shayna +shaved +shanty +selector +scumbag +scramble +scott123 +schalke +scarab +saracen +salinger +rosette +revival +renoir +rendezvous +reminder +redheads +rage +qwerty321 +qwe +propaganda +pringle +presidente +prakash +points +pocahontas +pierrot +photography +phaedrus +permanent +peeper +paulchen +password5 +passion1 +paraguay +panda123 +palacios +pain +pacino +osbourne +orange123 +opus +onepiece +nolan +nitrous +nippon +ninja1 +mutation +murcielago +murakami +mongo +mitchel +mina +mike1 +mercator +matematica +mario123 +marin +marcy +manticore +mahler +lynnette +luigi +lucero +loyola +lookatme +lock +lllll +linda123 +lightnin +lifeless +libby +leopoldo +lenore +lenin +lawman +latisha +latin +kristin1 +knitting +kinetic +killerbee +killa +kawaii +katrina1 +kabuki +julia1 +journal +jabber +iridium +interactive +hussein +hunt +hotdogs +holding +hickory +hershey1 +hellhound +haunted +happening +hansol +hanover +gutter +gussie +gridlock +greatness +grape +grandam +goethe +gigantic +getaway +gemma +garvey +gaby +fred1234 +florent +flavio +flatline +firehouse +firehawk +filbert +fight +fellatio +faraway +face +excite +eugenio +eruption +erasmus +encounter +dragons1 +dragon88 +doktor +dogfish +dionne +delorean +decipher +dddd +davidb +darken +darkblue +dario +danika +crush +creed +creatine +craven +couple +counting +cornbread +coolidge +cookie12 +converge +contest +clubbing +clear +cigars +charmaine +charade +chair +chains +cement +cbr600rr +casual +carnegie +caribbean +capcom +canton +calabria +buttfuck +butterflies +broncos1 +brindle +bowie +bonfire +blueball +blister +blair +bigcat +biatch +beware +beemer +beautifu +bbbb +batter +bateman +barnacle +barman +barbarossa +banking +bach +babygurl +azazel +azalea +avocado +automatic +asturias +assasin +ashwin +armchair +archives +aperture +andree +amos +amandine +ally +alexei +agnieszka +aggie +ace +Liverpool +Killer +717171 +535353 +515151 +474747 +22446688 +20032003 +1qaz +1a2b3c4d5e +19641964 +12141214 +101112 +01230123 +zarina +yourname +yahooo +wxcvbn +woods +wilkins +whores +whitewolf +warszawa +warsaw +viviana +vista +visionary +viagra +vette +versailles +valera +twist +trophy +tribble +trapped +toothpick +tillie +tigress +therock1 +there +theory +testuser +temp1234 +taipan +swordfis +swiss +superdog +sunflowers +sunflowe +stevenson +sportsman +somewhere +solar +soccer22 +snoopdogg +slovenia +slide +slayer666 +sinfonia +silverfish +shells +sexybitch +sexbomb +seadog +scrotum +scribe +scimitar +sceptre +sassy1 +sandal +sally1 +rossi +rosebush +rodeo +reznor +resonance +resolution +reno +registration +redriver +redeemed +ranger1 +ramstein +ram +rahman +radish +radiant +qweasdzx +quick +qazzaq +q12345 +q +purpose +puppy1 +proper +prince1 +primetime +precision +plumbing +pirata +pimping +pickwick +pavel +password22 +parsifal +paramount +pajero +overcome +otis +onetwo +olga +octagon +nutcracker +ninjutsu +newport1 +newcomer +net +neon +narayan +nanana +motmot +mostafa +monkey01 +minority +minion +midwest +marques +mariette +manu +manitou +manage +maldini +malawi +mahmoud +mafalda +lover1 +loveland +lottery +localhost +llcoolj +like +leona +league +leadership +lagrange +kenton +kelli +kanada +kaitlynn +justin123 +joshua123 +john1234 +joan +jjjj +jedi +janette +jamjam +isis +irish1 +invictus +inventor +inspired +inform +icecold +iamcool +hurrican +hotness +honey123 +holbrook +hiroshima +heracles +hehe +hawthorne +hathaway +grey +governor +goody +goodrich +gizmo123 +garion +front +friday13 +fortytwo +foreplay +foolproof +flash1 +flakes +fishhook +fishfish +fishers +financial +fillmore +figure +figment +fiddler +ferrets +fake +evangeline +espinoza +enough +emerald1 +electricity +ekaterina +edgewood +duisburg +drummers +dowjones +dopey +dodge1 +dizzy +delbert +dantes +danmark +crow +corrina +convict +continental +cococo +clinic +cipher +chewy +charmer +cards +cameroon +bunnie +buddyboy +bruno1 +britta +britt +bracelet +booter +bonner +bolivar +bogeyman +board +bluerose +birdcage +billy123 +billgates +bikers +bigfish +benny1 +bennet +benjie +beepbeep +batman123 +barret +barney1 +austen +ashtray +asdfgh12 +armenia +archive +architecture +anyone +antonina +andi +anaheim +anabolic +amor +alma +allister +aliali +albacore +airedale +aguilar +again +activity +Patricia +Nicole +Justin +99887766 +987321 +963258 +808080 +757575 +741741 +333 +20202020 +19701970 +153624 +1357924680 +1231 +115599 +080808 +yessir +yardbird +xcountry +wine +wildrose +waves +watanabe +wareagle +wanderlust +waldo +wakefield +volker +verity +verify +velcro +validate +unix +union +twin +tripping +tripleh +trip +treetree +timbuktu +tilly +tight +tesoro +teaser +taytay +tarantino +syndicate +sylvan +sylvain +swifty +swift +swansea +sunburn +summer00 +sultana +stuntman +strokes +stroker +strata +stlouis +stetson +steelman +steamer +spartan1 +spaceship +snowshoe +smuggler +slowhand +skynet +simcity +shorter +shift +sharpshooter +shanice +shadow01 +sensor +senna +seasons +schuster +schumi +schalke04 +satelite +sarge +samir +saddam +russ +romeo1 +rockin +rightnow +resume +reset +regret +reese +reactor +r4e3w2q1 +quagmire +punch +price +prefect +prague +portsmouth +porridge +pollock +plummer +platon +pinkerton +perseus +period +percy +peerless +paxton +paganini +orchestra +optional +opera +oioioi +nowayout +nounou +nintendo64 +nickle +nicaragua +newstart +neworder +neumann +monty1 +monkey13 +momoney +mom123 +moimoi +mission1 +michelangelo +menthol +mega +mcmillan +may +maxx +mara +manana +machado +m123456 +lurker +lucky777 +lotion +loren +lombardo +lisette +lindberg +leah +launch +larkspur +laredo +landing +lancia +lambchop +lalaland +lachlan +kosova +kirakira +kamehameha +just +jurgen +juneau +juggler +juanito +joshua12 +jonah +jetaime +jesper +jellybeans +january1 +itachi +innovision +infinito +index +indeed +identify +hostile +hgfdsa +here +hellomoto +hellgate +heatwave +heater +hartley +harlequin +hardon +hall +grounded +greenish +grandmother +gorillaz +goldsmith +gloves +glen +gerhardt +generous +gauthier +gator1 +gardens +frontera +fridge +freezing +franz +fracture +fourth +forces +fool +firewater +fellowship +fastlane +explosive +environment +embassy +elmo +elmer +eeeeeee +dummies +duane +drunk +drum +draven +drafting +donnelly +dolomite +direction +devlin +deviant +deception +daytime +darien +darby +damocles +cyanide +cunningham +crossroad +critters +crickets +crabtree +cowboy1 +cortland +cooley +convert +constantin +connected +confidential +comrades +codered +clothing +cleric +classical +chuchu +chiller +checking +chase1 +charmed1 +cathleen +carter15 +carleton +caribou +car123 +capella +candela +camelia +caboose +butterscotch +butterball +burgers +bulldozer +browny +brenner +borland +bomberman +blueline +blue11 +blondes +blaise +bittersweet +bigblack +berries +belial +beehive +bauer +bastard1 +baobab +bagger +backspin +babababa +audition +auction +ass123 +asians +argentum +antonius +antiques +ann +animate +angelfish +americana +ambush +aluminium +alfa +alain +abigail1 +abc123abc +abbie +aassdd +Brandon +666777 +636363 +575757 +369963 +2hot4u +147963 +14531453 +000 +wwww +worthy +woodlawn +woodchuck +winwin +windward +wind +warranty +wander +visitors +vertex +vanderbilt +valdez +turquoise +triathlon +trespass +trashcan +traitor +trade +tori +topnotch +tokyo +titania +tigger12 +thongs +theron +theo +thedog +tatjana +switzerland +suzie +surgeon +supply +summer05 +summer01 +sturgeon +studioworks +strikers +state +spook +sparky1 +sounds +solidus +soft +snowy +smoke1 +skipjack +simulator +silverman +shipyard +shimano +shekinah +sexy69 +severin +scouting +satanic +sanpedro +sandrock +rubicon +rootroot +ronaldo9 +romina +roger1 +rocco +riptide +riley1 +reynaldo +renaissance +rembrandt +relentless +relative +recover +ray +randy1 +rancho +rainier +radagast +qwertzui +qwe321 +quiet +quack +puddle +presidio +presence +prentice +porcupine +poppy1 +polar +playback +playa +place +ping +pilsner +philippa +peterman +persia +perrin +peregrin +peaceman +papabear +pagoda +organist +optimum +ok +octavio +octavian +northside +nnnnnnn +nikhil +nightwing +niceday +next +nathanael +nascar24 +muscles +multipass +mostwanted +monteiro +monkeys1 +monk +monet +monday1 +molina +mirella +minnow +millhouse +mikhail +micaela +metaphor +mervin +merida +matilde +masterp +manifold +mangos +mandala +mancity +maltese +makelove +makayla +mahoney +lysander +love69 +louisville +london123 +logistics +lobsters +line +lifesaver +liana +levi +layla +lagoon +kylie +kristofer +kinky +kimmy +kilimanjaro +kellogg +karmen +kalvin +julie123 +jolly +johngalt +jamaica1 +jalapeno +jakob +jacobsen +islanders +isengard +idefix +icecream1 +hutchins +hotlips +horizons +holger +hitchcock +hemingway +heavens +heartland +haynes +hawkwind +hasan +harding +happyboy +happy2 +halima +habitat +gwendolyn +gutentag +grunt +grenade +graveyard +gracious +godislove +glenwood +girlie +ghbdtn +gggg +frogs +frogfrog +freelancer +franck +fraction +foxy +forgetful +foreigner +folklore +flaming +firetruck +fever +fender1 +fantasy1 +fahrenheit +express1 +exposure +everton1 +ericson +eragon +enfield +endurance +employee +embrace +elysium +elektro +economic +dunhill +ducksoup +dragonslayer +doggystyle +diskette +devious +destin +despair +descartes +delacruz +davina +dashboard +damnation +daisies +custer +crissy +creepers +copperhead +colony +cognac +cobras +clements +cheerful +characters +chantel +certified +cecily +cathedral +catering +career +caracol +capucine +capacity +calvary +cabinet +bypass +bugs +buffet +budget +bridgett +breakaway +brat +boyscout +bourne +bogota +blue42 +bloomer +bloodlust +bling +blackstone +bird33 +bingo123 +bibi +belgrade +beginner +bavarian +band +baloo +bagels +backfire +astaroth +asswipe +asphalt +asdfg12345 +arsehole +argent +ararat +anselmo +annelise +andrew01 +anabelle +amherst +albright +airlines +adminadmin +adelante +adam12 +acrobat +account1 +abdulla +Maverick +Maggie +London +Dennis +998877 +85208520 +555 +357951 +2323 +2007 +1q2w3e4 +145236 +14121412 +134679852 +132435 +123456789abc +zzzzzzzzzz +zouzou +zazaza +yakuza +yahoo123 +wretched +winthrop +wildone +whirlwind +westwind +wendel +weinberg +weewee +wade +vacuum +upyours +tumbleweed +trashman +toronto1 +tissue +timtim +tigger2 +threesome +thomas01 +thibault +thesims +thekid +test11 +teller +tata +tartar +taco +system1 +syndrome +swinging +sweetiepie +sweetest +suspect +superwoman +sunita +sunburst +streak +strauss +sperma +sperling +spectral +soul +song +soldier1 +solace +smasher +sky +sixpack +simplex +silmaril +shoulder +shortie +shahrukh +settlers +semper +seduction +searching +scotsman +scofield +schumann +schule +scholar +satisfaction +santamaria +sandals +safeway +rudeboy +rossignol +ronny +rodrigues +rockrock +rockland +robyn +retriever +resurrection +restaurant +regine +redwine +redcar +rebelde +race +r +qwerty69 +qaywsx +prozac +promises +priscila +priority +principal +poop123 +pookie1 +polina +playoffs +persephone +peregrine +pebbles1 +pearl1 +patter +pasha +owner +owned +overseer +orleans +orion1 +order +orbit +opposite +oldsmobile +okay +octavius +oconnor +obscure +nikko +nikenike +nightcrawler +nehemiah +navy +nasser +nassau +mystery1 +myriam +mylene +moving +morticia +morrowind +moonraker +monkey11 +mogul +modest +mobster +mithrandir +misty123 +mingus +milenium +microphone +michael3 +miamor +mendez +matt123 +matrix123 +math +markos +marcio +maisie +mailer +lollollol +loader +lizbeth +lincoln1 +lilwayne +leanna +lawton +lausanne +lasher +lake +kokokoko +kobold +kisser +kilowatt +killall +kidding +kick +k +juggle +judson +joanie +jjjjj +jessy +jelena +jacob123 +issues +ishmael +isadora +interval +insect +ignorant +huntsman +hubble +hothot +host +hooligans +homo +homesick +holycow +hobgoblin +highlands +highbury +hhhhhhh +herrera +hellbent +hawks +hands +handle +hallie +halibut +hackman +guerilla +graywolf +grandson +goonies +gmoney +gizzmo +gertie +georgetown +gentleman +gecko +gargamel +gangsters +gameplay +galway +fractal +foryou +fortis +flowerpot +firefly1 +fighter1 +fielding +fermat +felony +favour +faramir +familiar +falconer +factor +ezequiel +ester +endgame +emotion +eeeee +edward1 +dynamics +dougal +dominican +dingo +dickson +demolition +demetria +demeter +dede +deathnote +david2 +daryl +darkroom +curtains +currency +crocodil +creativity +crawling +cranky +cory +commercial +cold +cigarette +ciao +christy1 +chivalry +charlie7 +chapter +chance1 +celestine +cecelia +ccccc +catriona +cassiopeia +carolann +carlie +card +cantona7 +cannonball +canfield +camber +buttocks +buller +brinkley +bribri +brianne +boromir +bordello +bonny +blissful +blast +blackwell +blackbox +billiard +bigbooty +bergman +belvedere +bauhaus +bastille +bashful +barbershop +background +avril +australian +atreyu +astalavista +assassins +ashes +asdfg1 +as123456 +artofwar +artichoke +aptiva +antique +annalena +animated +angle +alvarado +alternate +alive +alicante +alex2000 +aleksandr +alabaster +aerospace +accurate +aabbcc +852852 +2008 +2 +17171717 +159159159 +141516 +123456as +00112233 +00001111 From a14916d10676e9d139cd94a1da9cfbf96524bed2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 11:03:29 -0700 Subject: [PATCH 287/569] Minor dep upgrades --- package-lock.json | 26 +++++++++++++------------- package.json | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae264090..be373f20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -773,9 +773,9 @@ } }, "fs-extra": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.0.tgz", - "integrity": "sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -1627,7 +1627,7 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { @@ -2098,9 +2098,9 @@ } }, "sqlite3": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.3.tgz", - "integrity": "sha512-nuWqc26oiJZXyY5MEz+rQbiki1BTibnXsy8Kqo7QD/ut6eksOWi6uWwFMbdnFNME7CZyplWdDXj2fbdQVaEfuA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.4.tgz", + "integrity": "sha512-CO8vZMyUXBPC+E3iXOCc7Tz2pAdq5BWfLcQmOokCOZW5S5sZ/paijiPOCdvzpdP83RroWHYa5xYlVqCxSqpnQg==", "requires": { "nan": "~2.10.0", "node-pre-gyp": "^0.10.3", @@ -2219,14 +2219,14 @@ } }, "tar": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", - "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.3", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", "yallist": "^3.0.2" diff --git a/package.json b/package.json index 0d4e4a9f..73fd9ebc 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "buffers": "github:NuSkooler/node-buffers", "bunyan": "^1.8.12", "exiftool": "^0.0.3", - "fs-extra": "^7.0.0", + "fs-extra": "^7.0.1", "glob": "^7.1.2", "graceful-fs": "^4.1.15", "hashids": "^1.1.1", @@ -44,7 +44,7 @@ "rlogin": "^1.0.0", "sane": "^4.0.2", "sanitize-filename": "^1.6.1", - "sqlite3": "^4.0.3", + "sqlite3": "^4.0.4", "sqlite3-trans": "^1.2.0", "ssh2": "^0.6.1", "temptmp": "^1.0.0", From 23aa3d0bd35b761e409d7ba968119437b7f5df9c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 18:11:00 -0700 Subject: [PATCH 288/569] Full source build for ARM/pi --- misc/install.sh | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/misc/install.sh b/misc/install.sh index 42cbbd0d..f4084928 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -9,6 +9,16 @@ ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` WAIT_BEFORE_INSTALL=10 +is_arch_arm() +{ + local ARCH=`arch` + if [[ $ARCH == "arm"* ]]; then + return 1 + else + return 0 + fi +} + enigma_header() { clear cat << EndOfMessage @@ -31,18 +41,19 @@ EndOfMessage sleep ${WAIT_BEFORE_INSTALL} } +fatal_error() { + printf "${TIME_FORMAT} \e[41mERROR:\033[0m %b\n" "$*" >&2; + exit 1 +} + enigma_install_needs() { - command -v $1 >/dev/null 2>&1 || { log_error "ENiGMA½ requires $1 but it's not installed. Please install it and restart the installer."; exit 1; } + command -v $1 >/dev/null 2>&1 || { fatal_error "ENiGMA½ requires $1 but it's not installed. Please install it and restart the installer." } } log() { printf "${TIME_FORMAT} %b\n" "$*"; } -log_error() { - printf "${TIME_FORMAT} \e[41mERROR:\033[0m %b\n" "$*" >&2; -} - enigma_install_init() { log "Checking git installation" enigma_install_needs git @@ -73,28 +84,35 @@ download_enigma_source() { if [ -d "$INSTALL_DIR/.git" ]; then log "ENiGMA½ is already installed in $INSTALL_DIR, trying to update using git" command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch 2> /dev/null || { - log_error "Failed to update ENiGMA½, run 'git fetch' in $INSTALL_DIR yourself." - exit 1 + fatal_error "Failed to update ENiGMA½, run 'git fetch' in $INSTALL_DIR yourself." } else log "Downloading ENiGMA½ from git to '$INSTALL_DIR'" mkdir -p "$INSTALL_DIR" command git clone ${ENIGMA_SOURCE} "$INSTALL_DIR" || { - log_error "Failed to clone ENiGMA½ repo. Please report this!" - exit 1 + fatal_error "Failed to clone ENiGMA½ repo. Please report this!" } fi } +extra_npm_install_args() +{ + if is_arch_arm; then + echo "--build-from-source" + else + echo "" + fi +} + install_node_packages() { log "Installing required Node packages" cd ${ENIGMA_INSTALL_DIR} - git checkout ${ENIGMA_BRANCH} && npm install + local EXTRA_NPM_ARGS=$(extra_npm_install_args) + git checkout ${ENIGMA_BRANCH} && npm install ${EXTRA_NPM_ARGS} if [ $? -eq 0 ]; then log "npm package installation complete" else - log_error "Failed to install ENiGMA½ npm packages. Please report this!" - exit 1 + fatal_error "Failed to install ENiGMA½ npm packages. Please report this!" fi } From 6c50b640dbcd92bd55e8ede361dc102d81e060cf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 18:19:38 -0700 Subject: [PATCH 289/569] Fixes --- misc/install.sh | 54 +++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/misc/install.sh b/misc/install.sh index f4084928..0b100edb 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -9,16 +9,6 @@ ENIGMA_SOURCE=${ENIGMA_SOURCE:=https://github.com/NuSkooler/enigma-bbs.git} TIME_FORMAT=`date "+%Y-%m-%d %H:%M:%S"` WAIT_BEFORE_INSTALL=10 -is_arch_arm() -{ - local ARCH=`arch` - if [[ $ARCH == "arm"* ]]; then - return 1 - else - return 0 - fi -} - enigma_header() { clear cat << EndOfMessage @@ -47,7 +37,7 @@ fatal_error() { } enigma_install_needs() { - command -v $1 >/dev/null 2>&1 || { fatal_error "ENiGMA½ requires $1 but it's not installed. Please install it and restart the installer." } + command -v $1 >/dev/null 2>&1 || fatal_error "ENiGMA½ requires $1 but it's not installed. Please install it and restart the installer." } log() { @@ -78,25 +68,31 @@ configure_nvm() { } download_enigma_source() { - local INSTALL_DIR - INSTALL_DIR=${ENIGMA_INSTALL_DIR} + local INSTALL_DIR + INSTALL_DIR=${ENIGMA_INSTALL_DIR} - if [ -d "$INSTALL_DIR/.git" ]; then - log "ENiGMA½ is already installed in $INSTALL_DIR, trying to update using git" - command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch 2> /dev/null || { - fatal_error "Failed to update ENiGMA½, run 'git fetch' in $INSTALL_DIR yourself." - } - else - log "Downloading ENiGMA½ from git to '$INSTALL_DIR'" - mkdir -p "$INSTALL_DIR" - command git clone ${ENIGMA_SOURCE} "$INSTALL_DIR" || { - fatal_error "Failed to clone ENiGMA½ repo. Please report this!" - } - fi + if [ -d "$INSTALL_DIR/.git" ]; then + log "ENiGMA½ is already installed in $INSTALL_DIR, trying to update using git" + command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch 2> /dev/null || + fatal_error "Failed to update ENiGMA½, run 'git fetch' in $INSTALL_DIR yourself." + else + log "Downloading ENiGMA½ from git to '$INSTALL_DIR'" + mkdir -p "$INSTALL_DIR" + command git clone ${ENIGMA_SOURCE} "$INSTALL_DIR" || + fatal_error "Failed to clone ENiGMA½ repo. Please report this!" + fi } -extra_npm_install_args() -{ +is_arch_arm() { + local ARCH=`arch` + if [[ $ARCH == "arm"* ]]; then + return 1 + else + return 0 + fi +} + +extra_npm_install_args() { if is_arch_arm; then echo "--build-from-source" else @@ -110,9 +106,9 @@ install_node_packages() { local EXTRA_NPM_ARGS=$(extra_npm_install_args) git checkout ${ENIGMA_BRANCH} && npm install ${EXTRA_NPM_ARGS} if [ $? -eq 0 ]; then - log "npm package installation complete" + log "npm package installation complete" else - fatal_error "Failed to install ENiGMA½ npm packages. Please report this!" + fatal_error "Failed to install ENiGMA½ npm packages. Please report this!" fi } From 713329b5d3a79a2cf966e62914e60a697c5142d0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 18:46:43 -0700 Subject: [PATCH 290/569] More fixes --- misc/install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/install.sh b/misc/install.sh index 0b100edb..b471124e 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -86,14 +86,14 @@ download_enigma_source() { is_arch_arm() { local ARCH=`arch` if [[ $ARCH == "arm"* ]]; then - return 1 + true else - return 0 + false fi } extra_npm_install_args() { - if is_arch_arm; then + if is_arch_arm ; then echo "--build-from-source" else echo "" From 0058d544241c0b5cfe8f89e89231edb898664ec8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 20:00:25 -0700 Subject: [PATCH 291/569] More updates on config template stuff --- core/oputil/oputil_config.js | 2 + misc/config_template.in.hjson | 152 ++++++++++++++++++++++++++++------ 2 files changed, 130 insertions(+), 24 deletions(-) diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 66b11fa6..de4731f7 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -32,6 +32,8 @@ function getAnswers(questions, cb) { const ConfigIncludeKeys = [ 'theme', + 'loginServers', + 'contentServers', ]; const QUESTIONS = { diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 79bc2bbf..901bd0c2 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -57,6 +57,100 @@ // Theme applied before a user has logged in. "*" indicates random. preLogin: XXXXX + + // + // dateFormat, timeFormat, and dateTimeFormat blocks configure + // moment.js (https://momentjs.com/docs/#/displaying/) style formats + // for dates and times. Short and long versions are available. + // Note that themes may override these settings. + // + } + + // + // Login servers represent available servers (or protocols) in which + // users are permitted to access your system. + // + loginServers: { + // Remember kids, Telnet is insecure! + telnet: { + } + + // ...but SSH *is* secure! + ssh: { + // + // To enable SSH: + // 1) Generate a Private Key (PK): + // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 + // 2) Set "privateKeyPass" below + // + enabled: XXXXX + + // set this to your PK's password, generated in step #1 above + privateKeyPass: SuperSecretPasswordChangeMe! + + // + // It's possible to lock down various algorithms available to + // SSH, but be aware this may limit the clients that can connect! + // + algorithms: {} + } + + webSocket: { + // + // Setting "proxied" to true allows non-secure (ws://) WebSockets + // to be considered secure when the X-Fowarded-Proto HTTP header + // is set to "https". This is helpful when ENiGMA is running behind + // another web server doing SSL/TLS termination. + // + proxied: false + + // Non-secure WebSockets, or ws:// + ws: {} + + // Secure WebSockets, or wss:// + wss: {} + } + } + + // + // Content Servers expose content from the system + // + contentServers: { + // + // The Web Content Server can expose content over HTTP (http://) and + // HTTPS (https://) for (but not limited to) the following purposes: + // * Static content + // * Web downloads from the file base + // * Password reset forms (sent to users in PW reset emails; see + // "email" block below) + // + web: { + // Set to your public FQDN + domain: XXXXX + } + } + + // + // Currently, ENiGMA½ can use external email to mail + // users for password resets. Additional functionality will + // be added in the future. + // + email: { + // + // Set the following keys to configure: + // * "defaultFrom" to the reply address + // * "transport" to a configuration block that meets the + // requirements of Nodemailer (https://nodemailer.com/) + // + // Example: + // transport: { + // service: Zoho + // auth: { + // user: myuser@myhost.com + // pass: supersecretpassword + // } + // } + // } // Message conferences and areas are within this block @@ -77,30 +171,6 @@ } } - // Archive files and related - archives: { - // - // External utilities used for import & upload processing archives such - // as .zip, .rar, .arj, etc. - // - // You'll want to have archivers configured for the many old-school archive - // formats that a BBS may encounter! - // - // See config.js for additional configuration - // - archivers: { - // - // Each key in the "archivers" configuration block represents a specific - // external archive utility. ENiGMA½ has sane configuration by default - // for many archivers, but the tools themselves are likely not yet installed - // on your system! - // - // Please consult the documentation on information as to where to find and - // install these utilities! - // - } - } - users: { // // ENiGMA½ utilizes user groups similar to Windows and *nix. Built in groups @@ -108,5 +178,39 @@ // groups to the system as well by adding a 'groups' key in this section: // groups: [ "leet", "lamerz" ] // + + // Set default group(s) new users should automatically be assigned to + // defaultGroups : [ "lamerz" ] + + // Should new users require +op activation? + requireActivation: false, + } + + // Archive files and related + archives: { + archivers: { + // + // Each key in the "archivers" configuration block represents a specific + // external archive utility. ENiGMA½ has sane configuration by default + // for many archivers, but the tools themselves are likely not yet installed + // on your system! + // + // You'll want to have archivers configured for the many old-school archive + // formats that a BBS may encounter! Please consult the documentation on + // information as to where to find and install these utilities! + // + } + } + + fileTransferProtocols: { + // + // Each key in the "fileTransferProtocols" configuration block defines + // an external file transfer utility for legacy protocols such as + // X, Y, and Z-Modem. + // + // You will want to ensure your system has these external utilities + // installed and/or define new or additional protocols. Please + // see the documentation for more information! + // } } \ No newline at end of file From f00dfea84dba3214c39ebc07e3425c52b8fad712 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 20:26:11 -0700 Subject: [PATCH 292/569] Log that it can take a while! --- misc/install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/misc/install.sh b/misc/install.sh index b471124e..aa2b3d9b 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -101,7 +101,9 @@ extra_npm_install_args() { } install_node_packages() { - log "Installing required Node packages" + log "Installing required Node packages..." + log "Note that on some systems such as RPi, this can take a VERY long time. Be patient!" + cd ${ENIGMA_INSTALL_DIR} local EXTRA_NPM_ARGS=$(extra_npm_install_args) git checkout ${ENIGMA_BRANCH} && npm install ${EXTRA_NPM_ARGS} From 99893b0bd1e9ef72fec8becd125898a4c0a229a9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 20:26:23 -0700 Subject: [PATCH 293/569] Yet more updates with config new --- core/oputil/oputil_config.js | 1 + misc/config_template.in.hjson | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index de4731f7..eade918b 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -34,6 +34,7 @@ const ConfigIncludeKeys = [ 'theme', 'loginServers', 'contentServers', + 'fileBase.areaStoragePrefix', ]; const QUESTIONS = { diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 901bd0c2..b20b21a9 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -171,6 +171,53 @@ } } + // + // ENiGMA½ comes with a very powerful File Base, but may be a bit strange + // until you get used to it. Please see the documentation! + // + fileBase: { + // + // Storage tags with relative paths (that is, paths that do not start + // with a "/") are relative to the following path: + // + areaStoragePrefix: XXXXX + + // + // Storage tags create a tag -> directory (relative or full path) + // that can be used in areas. + // + storageTags: { + // + // Example storage tag: "super_l33t_warez": + // super_l33t_warez: "/path/to/super/l33t/warez" + // + } + + areas: { + // + // Example area with the areaTag of "an_example_area": + // an_example_area: { + // name: "Example File Area" + // desc: "It's just an example, yo!" + // storageTags: [ + // "super_l33t_warez" + // ] + // } + // + // File Base Areas are read-only (ie: download only) by default. + // To make a uploadable area, set ACS as you like. For example, + // to allow all users to upload to an area: + // + // an_example_area: { + // // ... + // acs: { + // write: GM[users] + // } + // } + } + } + + // General user configuration users: { // // ENiGMA½ utilizes user groups similar to Windows and *nix. Built in groups From 82cfdc978fd161d2755b7608e17b88b378a0e5b6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 22:04:59 -0700 Subject: [PATCH 294/569] Put ports at top of blocks --- misc/config_template.in.hjson | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index b20b21a9..c8c9bebb 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -73,10 +73,14 @@ loginServers: { // Remember kids, Telnet is insecure! telnet: { + // It's best to use non-privileged ports and NAT/foward to them + port: XXXXX } // ...but SSH *is* secure! ssh: { + port: XXXXX + // // To enable SSH: // 1) Generate a Private Key (PK): @@ -105,10 +109,24 @@ proxied: false // Non-secure WebSockets, or ws:// - ws: {} + ws: { + port: XXXXX + } // Secure WebSockets, or wss:// - wss: {} + wss: { + port: XXXXX + enabled: XXXXX + + // + // Certificate and Key in PEM format. + // Note that web browsers will not trust self-signed certs. Look + // into Let's Encrypt and perhaps running ENiGMA behind another + // web server such as Caddy. + // + certPem: XXXXX + keyPem: XXXXX + } } } From c24695e9988e6eabaef3838609125a8731f8ef38 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 10 Nov 2018 23:59:26 -0700 Subject: [PATCH 295/569] + oputil.js config cat * Many updates to config gen y --- WHATSNEW.md | 3 + core/config.js | 13 + core/oputil/oputil_config.js | 47 ++- core/oputil/oputil_help.js | 5 + misc/config_template.in.hjson | 523 ++++++++++++++++++---------------- 5 files changed, 337 insertions(+), 254 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index de8b6383..90b9e374 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -19,6 +19,9 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Any module may now register for a system startup intiialization via the `initializeModules(initInfo, cb)` export. * User event log is now functional. Various events a user performs will be persisted to the `system.db` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`! * New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md) +* `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected. +* `oputil.js config new` will now generate a much more complete configuration file with comments, examples, etc. `oputil.js config cat` dumps your current config to stdout. + ## 0.0.8-alpha diff --git a/core/config.js b/core/config.js index bc6be381..f6adaa35 100644 --- a/core/config.js +++ b/core/config.js @@ -346,6 +346,19 @@ function getDefaultConfig() { certPem : paths.join(__dirname, './../config/https_cert.pem'), keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), } + }, + + gopher : { + enabled : false, + port : 8070, + publicHostname : 'another-fine-enigma-bbs.org', + publicPort : 8080, // adjust if behind NAT/etc. + bannerFile : 'gopher_banner.asc', + + // + // Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ] + // to export message confs/areas + // } }, diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index eade918b..b25441d8 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -4,12 +4,14 @@ // ENiGMA½ const resolvePath = require('../../core/misc_util.js').resolvePath; -const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; -const ExitCodes = require('./oputil_common.js').ExitCodes; -const argv = require('./oputil_common.js').argv; -const getConfigPath = require('./oputil_common.js').getConfigPath; +const { + printUsageAndSetExitCode, + getConfigPath, + argv, + ExitCodes, + initConfigAndDatabases +} = require('./oputil_common.js'); const getHelpFor = require('./oputil_help.js').getHelpFor; -const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; const Errors = require('../../core/enig_error.js').Errors; // deps @@ -21,6 +23,8 @@ const hjson = require('hjson'); const paths = require('path'); const _ = require('lodash'); +const packageJson = require('../../package.json'); + exports.handleConfigCommand = handleConfigCommand; @@ -37,6 +41,15 @@ const ConfigIncludeKeys = [ 'fileBase.areaStoragePrefix', ]; +const HJSONStringifyComonOpts = { + emitRootBraces : true, + bracesSameLine : true, + space : 4, + keepWsc : true, + quotes : 'min', + eol : '\n', +}; + const QUESTIONS = { Intro : [ { @@ -214,7 +227,10 @@ function askNewConfigQuestions(cb) { } function writeConfig(config, path) { - config = hjson.stringify(config, { bracesSameLine : true, space : '\t', keepWsc : true, quotes : 'strings' } ); + config = hjson.stringify(config, HJSONStringifyComonOpts) + .replace(/%ENIG_VERSION%/g, packageJson.version) + .replace(/%HJSON_VERSION%/g, hjson.version) + ; try { fs.writeFileSync(path, config, 'utf8'); @@ -522,6 +538,24 @@ function getImportEntries(importType, importData) { return importEntries; } +function catCurrentConfig() { + try { + const config = hjson.rt.parse(fs.readFileSync(getConfigPath(), 'utf8')); + const hjsonOpts = Object.assign({}, HJSONStringifyComonOpts, { + colors : false === argv.colors ? false : true, + keepWsc : false === argv.comments ? false : true, + }); + + console.log(hjson.stringify(config, hjsonOpts)); + } catch(e) { + if('ENOENT' == e.code) { + console.error(`File not found: ${getConfigPath()}`); + } else { + console.error(e); + } + } +} + function handleConfigCommand() { if(true === argv.help) { return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); @@ -532,6 +566,7 @@ function handleConfigCommand() { switch(action) { case 'new' : return buildNewConfig(); case 'import-areas' : return importAreas(); + case 'cat' : return catCurrentConfig(); default : return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); } diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 4981f922..7c96d171 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -39,12 +39,17 @@ actions: actions: new generate a new/initial configuration import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH + cat cat current configuration to stdout import-areas args: --conf CONF_TAG specify conference tag in which to import areas --network NETWORK specify network name/key to associate FTN areas --uplinks UL1,UL2,... specify one or more comma separated uplinks --type TYPE specifies area import type. valid options are "bbs" and "na" + +cat args: + --no-color disable color + --no-comments strip any comments `, FileBase : `usage: oputil.js fb [] diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index c8c9bebb..b05fa057 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -1,281 +1,308 @@ { - /* - ./\/\.' ENiGMA½ System Configuration -/--/-------- - -- - + /* + ./\/\.' ENiGMA½ System Configuration -/--/-------- - -- - - _____________________ _____ ____________________ __________\_ / - \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! - // __|___// | \// |// | \// | | \// \ /___ /_____ - /____ _____| __________ ___|__| ____| \ / _____ \ - ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ - /__ _\ - <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + _____________________ _____ ____________________ __________\_ / + \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! + // __|___// | \// |// | \// | | \// \ /___ /_____ + /____ _____| __________ ___|__| ____| \ / _____ \ + ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - ------------------------------------------------------------------------------- + *-----------------------------------------------------------------------------* + Generated by ENiGMA½ v%ENIG_VERSION% / hjson v%HJSON_VERSION% + *-----------------------------------------------------------------------------* - General Information - ------------------------------- - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec - JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - - See http://hjson.org/ for more information and syntax. - Various editors and IDEs have plugins for the HJSON format which can be - very useful. + ------------------------------- -- - - + General Information + ------------------------------- - - + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - Available Configuration - ------------------------------- - ENiGMA½ is highly configurable! By default, this file contains common - configuration elements, examples, etc. To see a full list of settings - available to this file, don't be afraid to open up core/config.js and - look around. Do not make changes there however, you may override any - of the configuration from within this file! + See http://hjson.org/ for more information and syntax. - See the documentation for more information, and don't be shy to ask - for help! - */ + Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so + on have syntax highlighting for the HJSON format which are highly recommended. - general: { - // Your BBS Name! - boardName: XXXXX - } - logging: { - // - // By default, the system will rotate logs. - // Remember you can pipe logs through bunyan to pretty-print: - // > tail -F enigma/logs/enigma-bbs.log | enigma/node_modules/bunyan/bin/bunyan - // - rotatingFile: { - // If you're having trouble, try setting this to "trace" - level: XXXXX - } - } + ------------------------------- -- - - + Configuration + ------------------------------- - - + ENiGMA½ is *highly* configurable, and thus can be overwhelming at first! - theme: { - // Default theme applied to new users. "*" indicates random. - default: XXXXX + By default, this file contains common configuration elements, examples, etc. + To see a more complete view of settings available to the system, don't be + afraid to open up core/config.js and look around. Do not make changes there + however! All system configuration can be extended and defaults overridden + via this file! - // Theme applied before a user has logged in. "*" indicates random. - preLogin: XXXXX + Please see RTFM ...er, uh... see the documentation for more information, and + don't be shy to ask for help: - // - // dateFormat, timeFormat, and dateTimeFormat blocks configure - // moment.js (https://momentjs.com/docs/#/displaying/) style formats - // for dates and times. Short and long versions are available. - // Note that themes may override these settings. - // - } + BBS : Xibalba @ xibalba.l33t.codes + FTN : BBS Discussion on fsxNet + IRC : #enigma-bbs / FreeNode + Email : bryan@leet.codes + */ - // - // Login servers represent available servers (or protocols) in which - // users are permitted to access your system. - // - loginServers: { - // Remember kids, Telnet is insecure! - telnet: { - // It's best to use non-privileged ports and NAT/foward to them - port: XXXXX - } + general: { + // Your BBS Name! + boardName: XXXXX + } - // ...but SSH *is* secure! - ssh: { - port: XXXXX + logging: { + // + // By default, the system will rotate logs. + // Remember you can pipe logs through bunyan to pretty-print: + // > tail -F enigma/logs/enigma-bbs.log | enigma/node_modules/bunyan/bin/bunyan + // + rotatingFile: { + // If you're having trouble, try setting this to "trace" + level: XXXXX + } + } - // - // To enable SSH: - // 1) Generate a Private Key (PK): - // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 - // 2) Set "privateKeyPass" below - // - enabled: XXXXX + theme: { + // Default theme applied to new users. "*" indicates random. + default: XXXXX + // Theme applied before a user has logged in. "*" indicates random. + preLogin: XXXXX - // set this to your PK's password, generated in step #1 above - privateKeyPass: SuperSecretPasswordChangeMe! + // + // dateFormat, timeFormat, and dateTimeFormat blocks configure + // moment.js (https://momentjs.com/docs/#/displaying/) style formats + // for dates and times. Short and long versions are available. + // Note that themes may override these settings. + // + } - // - // It's possible to lock down various algorithms available to - // SSH, but be aware this may limit the clients that can connect! - // - algorithms: {} - } + // + // Login servers represent available servers (or protocols) in which + // users are permitted to access your system. + // + loginServers: { + // Remember kids, Telnet is insecure! + telnet: { + // It's best to use non-privileged ports and NAT/foward to them + port: XXXXX + } - webSocket: { - // - // Setting "proxied" to true allows non-secure (ws://) WebSockets - // to be considered secure when the X-Fowarded-Proto HTTP header - // is set to "https". This is helpful when ENiGMA is running behind - // another web server doing SSL/TLS termination. - // - proxied: false + // ...but SSH *is* secure! + ssh: { + port: XXXXX - // Non-secure WebSockets, or ws:// - ws: { - port: XXXXX - } + // + // To enable SSH: + // 1) Generate a Private Key (PK): + // > openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 + // 2) Set "privateKeyPass" below + // + enabled: XXXXX - // Secure WebSockets, or wss:// - wss: { - port: XXXXX - enabled: XXXXX + // set this to your PK's password, generated in step #1 above + privateKeyPass: SuperSecretPasswordChangeMe! - // - // Certificate and Key in PEM format. - // Note that web browsers will not trust self-signed certs. Look - // into Let's Encrypt and perhaps running ENiGMA behind another - // web server such as Caddy. - // - certPem: XXXXX - keyPem: XXXXX - } - } - } + // + // It's possible to lock down various algorithms available to + // SSH, but be aware this may limit the clients that can connect! + // + algorithms: {} + } - // - // Content Servers expose content from the system - // - contentServers: { - // - // The Web Content Server can expose content over HTTP (http://) and - // HTTPS (https://) for (but not limited to) the following purposes: - // * Static content - // * Web downloads from the file base - // * Password reset forms (sent to users in PW reset emails; see - // "email" block below) - // - web: { - // Set to your public FQDN - domain: XXXXX - } - } + webSocket: { + // + // Setting "proxied" to true allows non-secure (ws://) WebSockets + // to be considered secure when the X-Fowarded-Proto HTTP header + // is set to "https". This is helpful when ENiGMA is running behind + // another web server doing SSL/TLS termination. + // + proxied: false - // - // Currently, ENiGMA½ can use external email to mail - // users for password resets. Additional functionality will - // be added in the future. - // - email: { - // - // Set the following keys to configure: - // * "defaultFrom" to the reply address - // * "transport" to a configuration block that meets the - // requirements of Nodemailer (https://nodemailer.com/) - // - // Example: - // transport: { - // service: Zoho - // auth: { - // user: myuser@myhost.com - // pass: supersecretpassword - // } - // } - // - } + // Non-secure WebSockets, or ws:// + ws: { + port: XXXXX + } - // Message conferences and areas are within this block - messageConferences: { - // An entry here prepresents a conference taka aka confTag - another_sample_conf: { - name: "Another Sample Conference" - desc: "Another conf sample. Change me!" - areas: { - // Similar to confTags, this is a areaTag - another_sample_area: { - name: "Another Sample Area" - desc: "Another area example. Change me!" - // The 'sort' key can override natural sort order and can live at the conference and area levels - sort: 2 - } - } - } - } + // Secure WebSockets, or wss:// + wss: { + port: XXXXX + enabled: XXXXX - // - // ENiGMA½ comes with a very powerful File Base, but may be a bit strange - // until you get used to it. Please see the documentation! - // - fileBase: { - // - // Storage tags with relative paths (that is, paths that do not start - // with a "/") are relative to the following path: - // - areaStoragePrefix: XXXXX + // + // Certificate and Key in PEM format. + // Note that web browsers will not trust self-signed certs. Look + // into Let's Encrypt and perhaps running ENiGMA behind another + // web server such as Caddy. + // + certPem: XXXXX + keyPem: XXXXX + } + } + } - // - // Storage tags create a tag -> directory (relative or full path) - // that can be used in areas. - // - storageTags: { - // - // Example storage tag: "super_l33t_warez": - // super_l33t_warez: "/path/to/super/l33t/warez" - // - } + // + // Content Servers expose content from the system + // + contentServers: { + // + // The Web Content Server can expose content over HTTP (http://) and + // HTTPS (https://) for (but not limited to) the following purposes: + // * Static content + // * Web downloads from the file base + // * Password reset forms (sent to users in PW reset emails; see + // "email" block below) + // + web: { + // Set to your public FQDN + domain: XXXXX + } - areas: { - // - // Example area with the areaTag of "an_example_area": - // an_example_area: { - // name: "Example File Area" - // desc: "It's just an example, yo!" - // storageTags: [ - // "super_l33t_warez" - // ] - // } - // - // File Base Areas are read-only (ie: download only) by default. - // To make a uploadable area, set ACS as you like. For example, - // to allow all users to upload to an area: - // - // an_example_area: { - // // ... - // acs: { - // write: GM[users] - // } - // } - } - } + // Ladies and gentlemen, a Gopher server! + gopher: { + port: XXXXX + enabled: false - // General user configuration - users: { - // - // ENiGMA½ utilizes user groups similar to Windows and *nix. Built in groups - // include "users" (for regular users) and "sysops" for +ops. You can add other - // groups to the system as well by adding a 'groups' key in this section: - // groups: [ "leet", "lamerz" ] - // + // + // The Gopher Content Server can export message base + // conferences and areas via the "messageConferences" key. + // + // Example: + // messageConferences: { + // some_conf: [ "area_tag1", "area_tag2" ] + // } + } + } - // Set default group(s) new users should automatically be assigned to - // defaultGroups : [ "lamerz" ] + // + // Currently, ENiGMA½ can use external email to mail + // users for password resets. Additional functionality will + // be added in the future. + // + email: { + // + // Set the following keys to configure: + // * "defaultFrom" to the reply address + // * "transport" to a configuration block that meets the + // requirements of Nodemailer (https://nodemailer.com/) + // + // Example: + // transport: { + // service: Zoho + // auth: { + // user: myuser@myhost.com + // pass: supersecretpassword + // } + // } + // + } - // Should new users require +op activation? - requireActivation: false, - } + // Message conferences and areas are within this block + messageConferences: { + // An entry here prepresents a conference taka aka confTag + another_sample_conf: { + name: "Another Sample Conference" + desc: "Another conf sample. Change me!" + areas: { + // Similar to confTags, this is a areaTag + another_sample_area: { + name: "Another Sample Area" + desc: "Another area example. Change me!" + // The 'sort' key can override natural sort order and can live at the conference and area levels + sort: 2 + } + } + } + } - // Archive files and related - archives: { - archivers: { - // - // Each key in the "archivers" configuration block represents a specific - // external archive utility. ENiGMA½ has sane configuration by default - // for many archivers, but the tools themselves are likely not yet installed - // on your system! - // - // You'll want to have archivers configured for the many old-school archive - // formats that a BBS may encounter! Please consult the documentation on - // information as to where to find and install these utilities! - // - } - } + // + // ENiGMA½ comes with a very powerful File Base, but may be a bit strange + // until you get used to it. Please see the documentation! + // + fileBase: { + // + // Storage tags with relative paths (that is, paths that do not start + // with a "/") are relative to the following path: + // + areaStoragePrefix: XXXXX - fileTransferProtocols: { - // - // Each key in the "fileTransferProtocols" configuration block defines - // an external file transfer utility for legacy protocols such as - // X, Y, and Z-Modem. - // - // You will want to ensure your system has these external utilities - // installed and/or define new or additional protocols. Please - // see the documentation for more information! - // - } + // + // Storage tags create a tag -> directory (relative or full path) + // that can be used in areas. + // + storageTags: { + // + // Example storage tag: "super_l33t_warez": + // super_l33t_warez: "/path/to/super/l33t/warez" + // + } + + areas: { + // + // Example area with the areaTag of "an_example_area": + // an_example_area: { + // name: "Example File Area" + // desc: "It's just an example, yo!" + // storageTags: [ + // "super_l33t_warez" + // ] + // } + // + // File Base Areas are read-only (ie: download only) by default. + // To make a uploadable area, set ACS as you like. For example, + // to allow all users to upload to an area: + // + // an_example_area: { + // // ... + // acs: { + // write: GM[users] + // } + // } + } + } + + // General user configuration + users: { + // + // ENiGMA½ utilizes user groups similar to Windows and *nix. Built in groups + // include "users" (for regular users) and "sysops" for +ops. You can add other + // groups to the system as well by adding a 'groups' key in this section: + // groups: [ "leet", "lamerz" ] + // + + // Set default group(s) new users should automatically be assigned to + // defaultGroups : [ "lamerz" ] + + // Should new users require +op activation? + requireActivation: false, + } + + // Archive files and related + archives: { + archivers: { + // + // Each key in the "archivers" configuration block represents a specific + // external archive utility. ENiGMA½ has sane configuration by default + // for many archivers, but the tools themselves are likely not yet installed + // on your system! + // + // You'll want to have archivers configured for the many old-school archive + // formats that a BBS may encounter! Please consult the documentation on + // information as to where to find and install these utilities! + // + } + } + + fileTransferProtocols: { + // + // Each key in the "fileTransferProtocols" configuration block defines + // an external file transfer utility for legacy protocols such as + // X, Y, and Z-Modem. + // + // You will want to ensure your system has these external utilities + // installed and/or define new or additional protocols. Please + // see the documentation for more information! + // + } } \ No newline at end of file From f592da26451a76c8da1dc8dc93689edc4c962c8b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 11 Nov 2018 00:19:01 -0700 Subject: [PATCH 296/569] oputil.js config new now preps 'menuFile' and 'promptFile' --- core/oputil/oputil_config.js | 39 +++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index b25441d8..95bb4155 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -15,13 +15,14 @@ const getHelpFor = require('./oputil_help.js').getHelpFor; const Errors = require('../../core/enig_error.js').Errors; // deps -const async = require('async'); -const inq = require('inquirer'); -const mkdirsSync = require('fs-extra').mkdirsSync; -const fs = require('graceful-fs'); -const hjson = require('hjson'); -const paths = require('path'); -const _ = require('lodash'); +const async = require('async'); +const inq = require('inquirer'); +const mkdirsSync = require('fs-extra').mkdirsSync; +const fs = require('graceful-fs'); +const hjson = require('hjson'); +const paths = require('path'); +const _ = require('lodash'); +const sanatizeFilename = require('sanitize-filename'); const packageJson = require('../../package.json'); @@ -240,12 +241,36 @@ function writeConfig(config, path) { } } +const copyFileSyncSilent = (to, from, flags) => { + try { + fs.copyFileSync(to, from, flags); + } catch(e) {} +}; + function buildNewConfig() { askNewConfigQuestions( (err, configPath, config) => { if(err) { return; } + const bn = sanatizeFilename(config.general.boardName).replace(/ /g, '_').toLowerCase(); + const menuFile = `${bn}.hjson`; + copyFileSyncSilent( + paths.join(__dirname, '../../config/menu.hjson'), + paths.join(__dirname, '../../config/', menuFile), + fs.constants.COPYFILE_EXCL + ); + + const promptFile = `${bn}_prompt.hjson`; + copyFileSyncSilent( + paths.join(__dirname, '../../config/prompt.hjson'), + paths.join(__dirname, '../../config/', promptFile), + fs.constants.COPYFILE_EXCL + ) + + config.general.menuFile = menuFile; + config.general.promptFile = promptFile; + if(writeConfig(config, configPath)) { console.info('Configuration generated'); } else { From e8e9df767f0be02fec4c2fbb2fd5156e769db513 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 11 Nov 2018 01:00:42 -0700 Subject: [PATCH 297/569] * Move idle config to user * More 'config new' enhancements --- core/client.js | 4 +- core/config.js | 10 ++--- core/oputil/oputil_config.js | 4 ++ misc/config_template.in.hjson | 75 +++++++++++++++++++++++++++++++---- 4 files changed, 77 insertions(+), 16 deletions(-) diff --git a/core/client.js b/core/client.js index ec10947f..1caaa85d 100644 --- a/core/client.js +++ b/core/client.js @@ -441,8 +441,8 @@ Client.prototype.startIdleMonitor = function() { const nowMs = Date.now(); const idleLogoutSeconds = this.user.isAuthenticated() ? - Config().misc.idleLogoutSeconds : - Config().misc.preAuthIdleLogoutSeconds; + Config().users.idleLogoutSeconds : + Config().users.preAuthIdleLogoutSeconds; if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { this.emit('idle timeout'); diff --git a/core/config.js b/core/config.js index f6adaa35..b8692a7c 100644 --- a/core/config.js +++ b/core/config.js @@ -178,6 +178,9 @@ function getDefaultConfig() { 'sysop', 'admin', 'administrator', 'root', 'all', 'areamgr', 'filemgr', 'filefix', 'areafix', 'allfix' ], + + preAuthIdleLogoutSeconds : 60 * 3, // 3m + idleLogoutSeconds : 60 * 6, // 6m }, theme : { @@ -906,14 +909,7 @@ function getDefaultConfig() { } }, - misc : { - preAuthIdleLogoutSeconds : 60 * 3, // 3m - idleLogoutSeconds : 60 * 6, // 6m - }, - logging : { - level : 'debug', - rotatingFile : { // set to 'disabled' or false to disable type : 'rotating-file', fileName : 'enigma-bbs.log', diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 95bb4155..2e8b0add 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -37,9 +37,13 @@ function getAnswers(questions, cb) { const ConfigIncludeKeys = [ 'theme', + 'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds', + 'users.newUserNames', + 'paths.logs', 'loginServers', 'contentServers', 'fileBase.areaStoragePrefix', + 'logging.rotatingFile', ]; const HJSONStringifyComonOpts = { diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index b05fa057..731f1965 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -52,11 +52,26 @@ boardName: XXXXX } + paths: { + // + // Other paths can also be configured as well, + // but generally unnecessary + // + logs: XXXXX + } + logging: { // - // By default, the system will rotate logs. - // Remember you can pipe logs through bunyan to pretty-print: - // > tail -F enigma/logs/enigma-bbs.log | enigma/node_modules/bunyan/bin/bunyan + // Each block here represents a Bunyan style config. + // See https://github.com/trentm/node-bunyan#streams + // + // Remember you can pipe logs through Bunyan to pretty-print: + // tail -F ./logs/enigma-bbs.log | bunyan + // + // (npm install -g bunyan to get the binary) + // + // We default to a rotating-file stream: + // https://github.com/trentm/node-bunyan#stream-type-rotating-file // rotatingFile: { // If you're having trouble, try setting this to "trace" @@ -156,7 +171,38 @@ // web: { // Set to your public FQDN - domain: XXXXX + domain: another-fine-enigma-bbs.org + + // Standard issue "www" folder. Place static content here + staticRoot: XXXXX + + // + // This block configures password reset emails. Template files + // support the following variables: + // * %BOARDNAME% : Name of BBS + // * %USERNAME% : Username of whom to reset password + // * %TOKEN% : Reset token + // * %RESET_URL% : In case of email, the link to follow + // for reset. In case of landing page, URL to POST submit reset form. + // + resetPassword: { + + } + + http: { + port: XXXXX + } + + https: { + port: XXXXX + enabled: XXXXX + + // + // Note that web browsers will not trust self-signed certs. Look + // into Let's Encrypt and perhaps running ENiGMA behind another + // web server such as Caddy. + // + } } // Ladies and gentlemen, a Gopher server! @@ -268,14 +314,29 @@ // ENiGMA½ utilizes user groups similar to Windows and *nix. Built in groups // include "users" (for regular users) and "sysops" for +ops. You can add other // groups to the system as well by adding a 'groups' key in this section: - // groups: [ "leet", "lamerz" ] + // groups: [ + // "leet", "lamerz" + // ] + // // - // Set default group(s) new users should automatically be assigned to - // defaultGroups : [ "lamerz" ] + // defaultGroups : [ + // "lamerz" + // ] + // // Should new users require +op activation? requireActivation: false, + + // How long pre-authenticated users (have not logged in) can idle + preAuthIdleLogoutSeconds: XXXXX + + // How long authenticated users (logged in) can idle + idleLogoutSeconds: XXXXX + + // Usernames reserved for applying to your system + newUserNames: [] + } // Archive files and related From 322274a115b0ff8058427dc7fa31e3ce4f4c00c6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 11 Nov 2018 01:55:38 -0700 Subject: [PATCH 298/569] System keeps login history events forever by default -- override in config --- core/config.js | 6 ++++++ core/user_login.js | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/config.js b/core/config.js index b8692a7c..c1fffc45 100644 --- a/core/config.js +++ b/core/config.js @@ -923,6 +923,12 @@ function getDefaultConfig() { debug : { assertsEnabled : false, + }, + + statLog : { + systemEvents : { + loginHistoryMax: -1 // forever + } } }; } diff --git a/core/user_login.js b/core/user_login.js index 2030152d..e64d20a9 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -7,6 +7,7 @@ const clientConnections = require('./client_connections.js').clientConnections; const StatLog = require('./stat_log.js'); const logger = require('./logger.js'); const Events = require('./events.js'); +const Config = require('./config.js').get; // deps const async = require('async'); @@ -86,12 +87,12 @@ function userLogin(client, username, password, cb) { return StatLog.incrementUserStat(user, 'login_count', 1, callback); }, function recordLoginHistory(callback) { - const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers + const loginHistoryMax = Config().statLog.systemEvents.loginHistoryMax; const historyItem = JSON.stringify({ userId : user.userId, sessionId : user.sessionId, }); - return StatLog.appendSystemLogEntry('user_login_history', historyItem, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); + return StatLog.appendSystemLogEntry('user_login_history', historyItem, loginHistoryMax, StatLog.KeepType.Max, callback); } ], err => { From 00a0e131b45bf2c5d4eeed5b5f6c3093a73a1238 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 11 Nov 2018 01:58:49 -0700 Subject: [PATCH 299/569] Statlog to config --- core/user_login.js | 9 ++++++++- misc/config_template.in.hjson | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/user_login.js b/core/user_login.js index e64d20a9..a3b2089b 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -92,7 +92,14 @@ function userLogin(client, username, password, cb) { userId : user.userId, sessionId : user.sessionId, }); - return StatLog.appendSystemLogEntry('user_login_history', historyItem, loginHistoryMax, StatLog.KeepType.Max, callback); + + return StatLog.appendSystemLogEntry( + 'user_login_history', + historyItem, + loginHistoryMax, + StatLog.KeepType.Max, + callback + ); } ], err => { diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 731f1965..e24b4e92 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -366,4 +366,11 @@ // see the documentation for more information! // } + + statLog: { + systemEvents: { + // Max login history event records kept. -1 = unlimited + loginHistoryMax: -1 + } + } } \ No newline at end of file From 2b97ee4b8a4ca031b3ec06ffae78c4c23eaa9264 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Nov 2018 11:30:35 -0700 Subject: [PATCH 300/569] Point to 0.0.9-alpha for install.sh --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0b70f71b..c2056a33 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * [MCI support](docs/art/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles * 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 - * [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 * Full [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support * Renegade style pipe color codes * [SQLite](http://sqlite.org/) storage of users, message areas, and so on @@ -55,14 +55,15 @@ ENiGMA has been tested with many terminals. However, the following are suggested * [MagiTerm](https://magickabbs.com/index.php/magiterm/) ## Boards -* WQH: :skull: [Xibalba](https://l33t.codes/xibalba-bbs) :skull: (**telnet://xibalba.l33t.codes:44510** or via SSH secure on port 44511) +* WQH: :skull: [Xibalba](https://l33t.codes/xibalba-bbs) :skull: (**ssh://xibalba.l33t.codes:44511** or **telnet://xibalba.l33t.codes:44510**) * [fORCE9](http://bbs.force9.org/): (**telnet://bbs.force9.org**) * [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**) +* [PlaneT Afr0](https://planetafr0.org/): (**ssh://planetafr0.org:8889**) ## Installation ``` -curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash +curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh | bash ``` Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/installation/installation-methods.html) in the docs for more information. From 14095d8f036eebdf73f8da1a83b78fc17a4dee90 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Nov 2018 11:30:52 -0700 Subject: [PATCH 301/569] Updates RE message networks --- docs/messageareas/message-networks.md | 105 +++++++++++++++++++------- misc/config_template.in.hjson | 11 +++ 2 files changed, 87 insertions(+), 29 deletions(-) diff --git a/docs/messageareas/message-networks.md b/docs/messageareas/message-networks.md index a334ab94..fec31a08 100644 --- a/docs/messageareas/message-networks.md +++ b/docs/messageareas/message-networks.md @@ -2,29 +2,33 @@ layout: page title: Message Networks --- -Configuring message networks in ENiGMA½ requires three specific pieces of config - the network and your -assigned address on it, the message areas (echos) of the network you wish to map to ENiGMA½ message areas, -then the schedule and routes to send mail packets on the network. +ENiGMA½ considers all non-ENiGMA½, non-local messages (and their networks, such as FTN "external". That is, messages are only imported and exported from/to such a networks. Configuring such external message networks in ENiGMA½ requires three sections in your `config.hjson`. + +1. `messageNetworks..networks`: declares available networks. +2. `messageNetworks..areas`: establishes local area mappings and per-area specifics. +3. `scannerTossers.`: general configuration for the scanner/tosser (import/export). This is also where we configure per-node settings. ## FTN Networks - -FTN networks are configured under the `messageNetworks::ftn` section of `config.hjson`. +FidoNet and FidoNet style (FTN) networks as well as a FTN/BSO scanner/tosser (`ftn_bso` module) are configured via the `messageNetworks.ftn` and `scannerTossers.ftn_bso` blocks in `config.hjson`. -The `networks` section contains a sub section for each network you wish you join your board to. -Each entry's key name is referenced elsewhere in `config.hjson` for FTN oriented configurations. +:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external utility such as Binkd is required for this -### Example Configuration +### Networks +The `networks` block a per-network configuration where each entry's key may be referenced elswhere in `config.hjson`. +Example: the following example declares two networks: `agoranet` and `fsxnet`: ```hjson { messageNetworks: { ftn: { networks: { - agoranet: { - localAddress: "46:3/102" + araknet: { + defaultZone: 10 + localAddress: "10:101/9" } - fsxnet: { - localAddress: "21:4/333" + fsxnet: { + defaultZone: 21 + localAddress: "21:1/121" } } } @@ -32,36 +36,79 @@ Each entry's key name is referenced elsewhere in `config.hjson` for FTN oriented } ``` -## Message Areas +### Areas +The `areas` section describes a mapping of local **area tags** configured in your `messageConferences` (see [Configuring a Message Area](configuring-a-message-area.md)) to a message network (described above), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages. -The `areas` section describes a mapping of local **area tags** configured in your `messageConferences` (see -[Configuring a Message Area](configuring-a-message-area.md)) to a message network (described -above), a FTN specific area tag, and remote uplink address(s). - -This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages. - -When ENiGMA½ imports messages, they will be placed in the local area that matches key under `areas`. +When ENiGMA½ imports messages, they will be placed in the local area that matches key under `areas` while exported messages will be sent to the relevant `network`. | Config Item | Required | Description | |-------------|----------|----------------------------------------------------------| -| `network` | :+1: | Associated network from the `networks` section above | -| `tag` | :+1: | FTN area tag | -| `uplinks` | :+1: | An array of FTN address uplink(s) for this network | - -### Example Configuration +| `network` | :+1: | Associated network from the `networks` section above | +| `tag` | :+1: | FTN area tag (ie: `FSX_GEN`) | +| `uplinks` | :+1: | An array of FTN address uplink(s) for this network | +Example: ```hjson { messageNetworks: { ftn: { areas: { - agoranet_bbs: { // tag found within messageConferences - network: agoranet - tag: AGN_BBS - uplinks: "46:1/100" + fsx_general: // *local* tag found within messageConferences + network: fsxnet // that we are mapping to this network + tag: FSX_GEN // ...and this remote FTN-specific tag + uplinks: [ "21:1/100" ] // a single string also allowed here } } } } } ``` + +### FTN/BSO Scanner Tosser + +| Config Item | Required | Description | +|-------------|----------|----------------------------------------------------------| +| `schedule` | :+1: | Sets `import` and `export` schedules. [Later style text parsing](https://bunkat.github.io/later/parsers.html#text) supported. `import` also can utilize a `@watch:` syntax while `export` additionally supports `@immediate`. | +| `packetMsgEncoding` | :-1: | Override default `utf8` encoding. +| `defaultNetwork` | :-1: | Explicitly set default network (by tag in `messageNetworks.ftn.networks`). If not set, the first found is used. | +| `nodes` | :+1: | Per-node settings. Entries (keys) here support wildcards for a portion of the FTN-style address (e.g.: `21:1/*`). `archiveType` may be set to a FTN supported archive extention that the system supports (TODO); if unset, only .PKT files are produced. `encoding` may be set to override `packetMsgEncoding` on a per-node basis. If the node requires a packet password, set `packetPassword` | + +Example: +```hjson +scannerTossers: { + ftn_bso: { + schedule: { + // Check every 30m, or whenever the "toss!.now" file is touched (ie: by Binkd) + import: every 30 minutes or @watch:/enigma-bbs/mail/ftn_in/toss!.now + + // Export immediately, but also check every 15m to be sure + export: every 15 minutes or @immediate + } + + // Override default FTN/BSO packet encoding. Defaults to 'utf8' + packetMsgEncoding: utf8 + + defaultNetwork: fsxnet + + nodes: { + "21:1/100" : { // May also contain wildcards, ie: "21:1/*" + archiveType: ZIP // By-ext archive type: ZIP, ARJ, ..., optional. + encoding: utf8 // Encoding for exported messages + packetPassword: MUHPA55 // FTN .PKT password, optional + + tic: { + // See TIC docs + } + } + } + + netMail: { + // See NetMail docs + } + + ticAreas: { + // See TIC docs + } + } +} +``` \ No newline at end of file diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index e24b4e92..296e7826 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -262,6 +262,17 @@ } } + // Configuration block for scanner/tosser modules + scannerTossers: { + // The most popular being FTN/BSO style networks + ftn_bso: { + // + // When you're ready to hook up to FTN networks, please + // see the documentation on message networks. + // + } + } + // // ENiGMA½ comes with a very powerful File Base, but may be a bit strange // until you get used to it. Please see the documentation! From 2b36693240f48967b24b4896988ca1528f51ecdd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Nov 2018 22:03:28 -0700 Subject: [PATCH 302/569] WIP: User Interrupt Queue * All queueing of messages/etc. * Queueing across nodes * Start on interruption points for displaying queued items * Start on a multi-node messaging system using such a queue --- art/themes/luciano_blocktronics/theme.hjson | 15 +++ core/ansi_term.js | 2 +- core/client.js | 16 +-- core/client_connections.js | 19 +-- core/menu_module.js | 83 ++++++++++--- core/node_msg.js | 126 ++++++++++++++++++++ core/user_interrupt_queue.js | 58 +++++++++ 7 files changed, 290 insertions(+), 29 deletions(-) create mode 100644 core/node_msg.js create mode 100644 core/user_interrupt_queue.js diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index aa720934..f359539f 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -353,6 +353,21 @@ } } + nodeMessage: { + 0: { + mci: { + SM1: { + width: 22 + itemFormat: "|00|07{text} |08(|07{userName}|08)" + focusItemFormat: "|00|15{text} |07(|15{userName}|07)" + } + ET2: { + width: 70 + } + } + } + } + messageAreaViewPost: { 0: { diff --git a/core/ansi_term.js b/core/ansi_term.js index 44c82464..f00fd011 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -56,7 +56,7 @@ exports.getSyncTERMFontFromAlias = getSyncTERMFontFromAlias; exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; exports.setCursorStyle = setCursorStyle; exports.setEmulatedBaudRate = setEmulatedBaudRate; -exports.vtxHyperlink = vtxHyperlink; +exports.vtxHyperlink = vtxHyperlink; // // See also diff --git a/core/client.js b/core/client.js index 1caaa85d..6f4aec79 100644 --- a/core/client.js +++ b/core/client.js @@ -32,13 +32,14 @@ ----/snip/---------------------- */ // ENiGMA½ -const term = require('./client_term.js'); -const ansi = require('./ansi_term.js'); -const User = require('./user.js'); -const Config = require('./config.js').get; -const MenuStack = require('./menu_stack.js'); -const ACS = require('./acs.js'); -const Events = require('./events.js'); +const term = require('./client_term.js'); +const ansi = require('./ansi_term.js'); +const User = require('./user.js'); +const Config = require('./config.js').get; +const MenuStack = require('./menu_stack.js'); +const ACS = require('./acs.js'); +const Events = require('./events.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); // deps const stream = require('stream'); @@ -84,6 +85,7 @@ function Client(/*input, output*/) { this.menuStack = new MenuStack(this); this.acs = new ACS(this); this.mciCache = {}; + this.interruptQueue = new UserInterruptQueue(this); this.clearMciCache = function() { this.mciCache = {}; diff --git a/core/client_connections.js b/core/client_connections.js index 93bb9465..f0bca4d7 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -15,11 +15,16 @@ exports.getActiveNodeList = getActiveNodeList; exports.addNewClient = addNewClient; exports.removeClient = removeClient; exports.getConnectionByUserId = getConnectionByUserId; +exports.getConnectionByNodeId = getConnectionByNodeId; const clientConnections = []; -exports.clientConnections = clientConnections; +exports.clientConnections = clientConnections; -function getActiveConnections() { return clientConnections; } +function getActiveConnections(authUsersOnly = false) { + return clientConnections.filter(conn => { + return ((authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly); + }); +} function getActiveNodeList(authUsersOnly) { @@ -29,11 +34,7 @@ function getActiveNodeList(authUsersOnly) { const now = moment(); - const activeConnections = getActiveConnections().filter(ac => { - return ((authUsersOnly && ac.user.isAuthenticated()) || !authUsersOnly); - }); - - return _.map(activeConnections, ac => { + return _.map(getActiveConnections(authUsersOnly), ac => { const entry = { node : ac.node, authenticated : ac.user.isAuthenticated(), @@ -118,3 +119,7 @@ function removeClient(client) { function getConnectionByUserId(userId) { return getActiveConnections().find( ac => userId === ac.user.userId ); } + +function getConnectionByNodeId(nodeId) { + return getActiveConnections().find( ac => nodeId == ac.node ); +} diff --git a/core/menu_module.js b/core/menu_module.js index ee1502cb..ce8eb556 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -25,15 +25,13 @@ exports.MenuModule = class MenuModule extends PluginModule { this.menuName = options.menuName; this.menuConfig = options.menuConfig; this.client = options.client; - //this.menuConfig.options = options.menuConfig.options || {}; this.menuMethods = {}; // methods called from @method's this.menuConfig.config = this.menuConfig.config || {}; - this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); - - //this.cls = _.isBoolean(this.menuConfig.options.cls) ? this.menuConfig.options.cls : Config().menus.cls; - this.viewControllers = {}; + + // *initial* interruptable state for this menu + this.disableInterruption(); } enter() { @@ -44,6 +42,14 @@ exports.MenuModule = class MenuModule extends PluginModule { this.detachViewControllers(); } + toggleInterruptionAndDisplayQueued(cb) { + this.enableInterruption(); + this.displayQueuedInterruptions( () => { + this.disableInterruption(); + return cb(null); + }); + } + initSequence() { const self = this; const mciData = {}; @@ -51,8 +57,11 @@ exports.MenuModule = class MenuModule extends PluginModule { async.series( [ + function beforeArtInterrupt(callback) { + return self.toggleInterruptionAndDisplayQueued(callback); + }, function beforeDisplayArt(callback) { - self.beforeArt(callback); + return self.beforeArt(callback); }, function displayMenuArt(callback) { if(!_.isString(self.menuConfig.art)) { @@ -160,6 +169,48 @@ exports.MenuModule = class MenuModule extends PluginModule { // nothing in base } + neverInterruptable() { + return this.menuConfig.config.interruptable === 'never'; + } + + enableInterruption() { + if(!this.neverInterruptable()) { + this.interruptable = true; + } + } + + disableInterruption() { + if(!this.neverInterruptable()) { + this.interruptable = false; + } + } + + displayQueuedInterruptions(cb) { + if(true !== this.interruptable) { + return cb(null); + } + + async.whilst( + () => this.client.interruptQueue.hasItems(), + next => { + this.client.interruptQueue.display( (err, interruptItem) => { + if(err) { + return next(err); + } + + if(interruptItem.pause) { + return this.pausePrompt(next); + } + + return next(null); + }); + }, + err => { + return cb(err); + } + ) + } + getSaveState() { // nothing in base } @@ -178,11 +229,15 @@ exports.MenuModule = class MenuModule extends PluginModule { return this.prevMenu(cb); // no next, go to prev } - return this.client.menuStack.next(cb); + this.displayQueuedInterruptions( () => { + return this.client.menuStack.next(cb); + }); } prevMenu(cb) { - return this.client.menuStack.prev(cb); + this.displayQueuedInterruptions( () => { + return this.client.menuStack.prev(cb); + }); } gotoMenu(name, options, cb) { @@ -234,13 +289,13 @@ exports.MenuModule = class MenuModule extends PluginModule { } autoNextMenu(cb) { - const self = this; - - function gotoNextMenu() { - if(self.haveNext()) { - return menuUtil.handleNext(self.client, self.menuConfig.next, {}, cb); + const gotoNextMenu = () => { + if(this.haveNext()) { + this.displayQueuedInterruptions( () => { + return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb); + }); } else { - return self.prevMenu(cb); + return this.prevMenu(cb); } } diff --git a/core/node_msg.js b/core/node_msg.js new file mode 100644 index 00000000..2c6f23ed --- /dev/null +++ b/core/node_msg.js @@ -0,0 +1,126 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const { Errors } = require('./enig_error.js'); +const { + getActiveNodeList, + getConnectionByNodeId, +} = require('./client_connections.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); + +// deps +const series = require('async/series'); +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'Node Message', + desc : 'Multi-node messaging', + author : 'NuSkooler', +}; + +const FormIds = { + sendMessage : 0, +}; + +const MciViewIds = { + sendMessage : { + nodeSelect : 1, + message : 2, + preview : 3, + + customRangeStart : 10, + } +} + +exports.getModule = class NodeMessageModule extends MenuModule { + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + + this.menuMethods = { + sendMessage : (formData, extraArgs, cb) => { + const nodeId = formData.value.node; + const message = formData.value.message; + + const interruptItem = { + contents : message, + } + + if(0 === nodeId) { + // ALL nodes + UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client); + } else { + UserInterruptQueue.queueGlobal(interruptItem, [ getConnectionByNodeId(nodeId) ]); + } + + return this.prevMenu(cb); + }, + } + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + series( + [ + (next) => { + return this.prepViewController('sendMessage', FormIds.sendMessage, mciData.menu, next); + }, + (next) => { + const nodeSelectView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.nodeSelect); + if(!nodeSelectView) { + return next(Errors.MissingMci(`Missing node selection MCI ${MciViewIds.sendMessage.nodeSelect}`)); + } + + this.prepareNodeList(); + + nodeSelectView.on('index update', idx => { + this.nodeListSelectionIndexUpdate(idx); + }); + + nodeSelectView.setItems(this.nodeList); + nodeSelectView.redraw(); + this.nodeListSelectionIndexUpdate(0); + return next(null); + } + ], + err => { + return cb(err); + } + ); + }); + } + + prepareNodeList() { + // standard node list with {text} field added for compliance + this.nodeList = [{ + text : '-ALL-', + // dummy fields: + node : 0, + authenticated : false, + userId : 0, + action : 'N/A', + userName : 'Everyone', + realName : 'All Users', + location : 'N/A', + affils : 'N/A', + timeOn : 'N/A', + }].concat(getActiveNodeList(true) + .map(node => Object.assign(node, { text : node.node.toString() } )) + ).filter(node => node.node !== this.client.node); // remove our client's node + this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node + } + + nodeListSelectionIndexUpdate(idx) { + const node = this.nodeList[idx]; + if(!node) { + return; + } + this.updateCustomViewTextsWithFilter('sendMessage', MciViewIds.sendMessage.customRangeStart, node); + } +} \ No newline at end of file diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js new file mode 100644 index 00000000..a29fd4df --- /dev/null +++ b/core/user_interrupt_queue.js @@ -0,0 +1,58 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Art = require('./art.js'); +const { + getActiveConnections +} = require('./client_connections.js'); +const ANSI = require('./ansi_term.js'); + +// deps +const _ = require('lodash'); + +module.exports = class UserInterruptQueue +{ + constructor(client) { + this.client = client; + this.queue = []; + } + + static queueGlobal(interruptItem, connections) { + connections.forEach(conn => { + conn.interruptQueue.queueItem(interruptItem); + }); + } + + // common shortcut: queue global, all active clients minus |client| + static queueGlobalOtherActive(interruptItem, client) { + const otherConnections = getActiveConnections(true).filter(ac => ac.node !== client.node); + return UserInterruptQueue.queueGlobal(interruptItem, otherConnections ); + } + + queueItem(interruptItem) { + interruptItem.pause = _.get(interruptItem, 'pause', true); + this.queue.push(interruptItem); + } + + hasItems() { + return this.queue.length > 0; + } + + display(cb) { + const interruptItem = this.queue.pop(); + if(!interruptItem) { + return cb(null); + } + + if(interruptItem.cls) { + this.client.term.rawWrite(ANSI.clearScreen()); + } else { + this.client.term.rawWrite('\r\n\r\n'); + } + + Art.display(this.client, interruptItem.contents, err => { + return cb(err, interruptItem); + }); + } +}; \ No newline at end of file From 516116f83efddf5ed751a2a8b1d7144e72ff259c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Nov 2018 22:05:21 -0700 Subject: [PATCH 303/569] Spinner Menu now supports itemFormat and focusItemFormat --- core/spinner_menu_view.js | 52 ++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index 7c38b195..f4517807 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -1,9 +1,11 @@ /* jslint node: true */ 'use strict'; -const MenuView = require('./menu_view.js').MenuView; -const ansi = require('./ansi_term.js'); -const strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); +const { pipeToAnsi } = require('./color_codes.js'); +const formatString = require('./string_format'); const util = require('util'); const assert = require('assert'); @@ -36,40 +38,47 @@ function SpinnerMenuView(options) { this.emit('index update', this.focusedItemIndex); }; - this.drawItem = function() { - var item = self.items[this.focusedItemIndex]; + this.drawItem = function(index) { + const item = this.items[index]; if(!item) { return; } - this.client.term.write(ansi.goto(this.position.row, this.position.col)); - this.client.term.write(self.hasFocus ? self.getFocusSGR() : self.getSGR()); + const cached = this.getRenderCacheItem(index, this.hasFocus); + if(cached) { + return this.client.term.write(`${ansi.goto(this.position.row, this.position.col)}${cached}`); + } - var text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + let text; + let sgr; + if(this.complexItems) { + text = pipeToAnsi(formatString(this.hasFocus && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (this.hasFocus ? this.getFocusSGR() : self.getSGR()); + } else { + text = strUtil.stylizeString(item.text, this.hasFocus ? self.focusTextStyle : self.textStyle); + sgr = this.hasFocus ? this.getFocusSGR() : this.getSGR(); + } - self.client.term.write( - strUtil.pad(text, this.dimens.width + 1, this.fillChar, this.justify)); - }; + text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; + this.client.term.write(`${ansi.goto(this.position.row, this.position.col)}${text}`); + this.setRenderCacheItem(index, text, this.hasFocus); + } } util.inherits(SpinnerMenuView, MenuView); SpinnerMenuView.prototype.redraw = function() { SpinnerMenuView.super_.prototype.redraw.call(this); - - //this.cachePositions(); this.drawItem(this.focusedItemIndex); }; SpinnerMenuView.prototype.setFocus = function(focused) { SpinnerMenuView.super_.prototype.setFocus.call(this, focused); - this.redraw(); }; SpinnerMenuView.prototype.setFocusItemIndex = function(index) { SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - this.updateSelection(); // will redraw }; @@ -103,16 +112,3 @@ SpinnerMenuView.prototype.getData = function() { const item = this.getItem(this.focusedItemIndex); return _.isString(item.data) ? item.data : this.focusedItemIndex; }; - -SpinnerMenuView.prototype.setItems = function(items) { - SpinnerMenuView.super_.prototype.setItems.call(this, items); - - var longest = 0; - for(var i = 0; i < this.items.length; ++i) { - if(longest < this.items[i].text.length) { - longest = this.items[i].text.length; - } - } - - this.dimens.width = longest; -}; \ No newline at end of file From 74b03fe846199cac9c296baff2db474d72f02f78 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Nov 2018 22:05:36 -0700 Subject: [PATCH 304/569] Fix exception when no SSH stuff is configured --- core/servers/login/ssh.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index f9186cf9..72f91a2a 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -241,6 +241,10 @@ exports.getModule = class SSHServerModule extends LoginServerModule { createServer() { const config = Config(); + if(true != config.loginServers.ssh.enabled) { + return; + } + const serverConf = { hostKeys : [ { @@ -269,6 +273,10 @@ exports.getModule = class SSHServerModule extends LoginServerModule { listen() { const config = Config(); + if(true != config.loginServers.ssh.enabled) { + return true; // no server, but not an error + } + const port = parseInt(config.loginServers.ssh.port); if(isNaN(port)) { Log.error( { server : ModuleInfo.name, port : config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); From 308f09b2919ec99c670b66de92529907e852a273 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 12 Nov 2018 22:05:49 -0700 Subject: [PATCH 305/569] Proper callback when missing MCI --- core/msg_conf_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index 3c9d7152..7ba75376 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -73,7 +73,7 @@ exports.getModule = class MessageConfListModule extends MenuModule { (next) => { const confListView = this.viewControllers.confList.getView(MciViewIds.confList); if(!confListView) { - return cb(Errors.MissingMci(`Missing conf list MCI ${MciViewIds.confList}`)); + return next(Errors.MissingMci(`Missing conf list MCI ${MciViewIds.confList}`)); } confListView.on('index update', idx => { From c1f7eb05cae1860b4da9da1f94cb089b616f4be1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Nov 2018 19:40:48 -0700 Subject: [PATCH 306/569] Better sanatization of menu/prompt.hjson files based on board name --- core/oputil/oputil_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 2e8b0add..dd6fc783 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -257,7 +257,7 @@ function buildNewConfig() { return; } - const bn = sanatizeFilename(config.general.boardName).replace(/ /g, '_').toLowerCase(); + const bn = sanatizeFilename(config.general.boardName).replace(/[^a-z0-9_\-]/ig, '_').toLowerCase(); const menuFile = `${bn}.hjson`; copyFileSyncSilent( paths.join(__dirname, '../../config/menu.hjson'), From d28b5ce3b2a0cbe0d9cf5dc4a627f4431a097cb5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Nov 2018 19:44:27 -0700 Subject: [PATCH 307/569] Even better file name generation for new configs --- core/oputil/oputil_config.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index dd6fc783..40644dee 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -257,15 +257,18 @@ function buildNewConfig() { return; } - const bn = sanatizeFilename(config.general.boardName).replace(/[^a-z0-9_\-]/ig, '_').toLowerCase(); - const menuFile = `${bn}.hjson`; + const bn = sanatizeFilename(config.general.boardName) + .replace(/[^a-z0-9_\-]/ig, '_') + .replace(/_+/g, '_') + .toLowerCase(); + const menuFile = `${bn}-menu.hjson`; copyFileSyncSilent( paths.join(__dirname, '../../config/menu.hjson'), paths.join(__dirname, '../../config/', menuFile), fs.constants.COPYFILE_EXCL ); - const promptFile = `${bn}_prompt.hjson`; + const promptFile = `${bn}-prompt.hjson`; copyFileSyncSilent( paths.join(__dirname, '../../config/prompt.hjson'), paths.join(__dirname, '../../config/', promptFile), From 5b1412aa319f4e6908a8cb4dd97a869df0f59b22 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Nov 2018 19:59:56 -0700 Subject: [PATCH 308/569] * Clean up door examples in menu.hjson * Remove specific doors in default door menu; +op will need to add them as they configure them --- art/themes/luciano_blocktronics/DOORMNU.ANS | Bin 3953 -> 3859 bytes config/menu.hjson | 76 ++++++++++++-------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/art/themes/luciano_blocktronics/DOORMNU.ANS b/art/themes/luciano_blocktronics/DOORMNU.ANS index bcd28f390209baf3ae6ea09321286f828d64cccd..9621ec1d74d03dfffa28a31135e3ab86377de1d9 100644 GIT binary patch delta 271 zcmew;H(74OQ6?7YXcL3YKbRyLosG?N6{MpLt&I&pw6R&Pf~#MqyRV}{acMz8eo;xW zLT+MSr9ydPWwAnj8c>Nru5`4iGgu2yF_3RPIgq8uzyc{7hpaD?55XXQVWd?JBc7B0ENxnjIeqLH;dTCLrLUDd>s-ko>$cYBl Rn@!lnIhj`QP4?wi0RRF-QDp!C delta 365 zcmbO%_fc-cQ6^&r1?gx*Yh#04AZ=`xtB{kQoR}jWZIBBTHg^WgndK%W73XB;WkZC` zH=ksZWOM*(GS4*x3R#1+6=dcXD3m7_6$4E%2kA8g>CH6(X@QtrQk0mI3R0n9WME(< z9X(li3D3PwN?ptC@My7?NbI42Vu|K#_4DgfNpXLbMp diff --git a/config/menu.hjson b/config/menu.hjson index 3d651412..1c48ef63 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -1564,43 +1564,48 @@ value: { command: "Q" } action: @systemMethod:prevMenu } + // + // The system supports many ways of launching doors including + // modules for DoorParty!, BBSLink, etc. + // + // Below are some examples. See the documentation for more info. + // { - value: { command: "PW" } - action: @menu:doorPimpWars + value: { command: "ABRACADABRA" } + action: @menu:doorAbracadabraExample } { - value: { command: "TW" } - action: @menu:doorTradeWars2002BBSLink - } - { - value: { command: "DL" } - action: @menu:doorDarkLands + value: { command: "TWBBLINK" } + action: @menu:doorTradeWars2002BBSLinkExample } { value: { command: "DP" } - action: @menu:doorParty + action: @menu:doorPartyExample } { value: { command: "CN" } - action: @menu:combatNet + action: @menu:doorCombatNetExample } { - value: { command: "AGENT" } - action: @menu:telnetBridgeAgency + value: { command: "EXODUS" } + action: @menu:doorExodusCataclysm } ] } // - // Example using the abracadabra module for a retro DOS door + // Local Door Example via abracadabra module // - doorPimpWars: { - desc: Playing PimpWars + // This example assumes launch_door.sh (which is passed args) + // launches the door. + // + doorAbracadabraExample: { + desc: Abracadabra Example module: abracadabra config: { - name: PimpWars + name: Example Door dropFileType: DORINFO - cmd: /home/enigma/DOS/scripts/pimpwars.sh + cmd: /home/enigma/DOS/scripts/launch_door.sh args: [ "{node}", "{dropFile}", @@ -1613,11 +1618,11 @@ } // - // TradeWars 2000 example via BBSLink + // BBSLink Example (TradeWars 2000) // - // You will need to register with BBSLink to obtain sysCode, authCode and schemeCode + // Register @ https://bbslink.net/ // - doorTradeWars2002BBSLink: { + doorTradeWars2002BBSLinkExample: { desc: Playing TW 2002 (BBSLink) module: bbs_link config: { @@ -1628,8 +1633,12 @@ } } - // DoorParty! support. You'll need to register to obtain credentials - doorParty: { + // + // DoorParty! Example + // + // Register @ http://throwbackbbs.com/ + // + doorPartyExample: { desc: Using DoorParty! module: door_party config: { @@ -1639,8 +1648,12 @@ } } - // CombatNet support. You'll need to register at http://combatnet.us/ to obtain credentials - combatNet: { + // + // CombatNet Example + // + // Register @ http://combatnet.us/ + // + doorCombatNetExample: { desc: Using CombatNet module: combatnet config: { @@ -1649,11 +1662,18 @@ } } - telnetBridgeAgency: { - desc: Connected to HappyLand BBS - module: telnet_bridge + // + // Exodus Example (cataclysm) + // Register @ https://oddnetwork.org/exodus/ + // + doorExodusCataclysm: { + desc: Cataclysm + module: exodus config: { - host: agency.bbs.geek.nz + rejectUnauthorized: false + board: XXX + key: XXXXXXXX + door: cataclysm } } From d8f07083108935a7ffb47919266d247b2b71df65 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Nov 2018 20:03:54 -0700 Subject: [PATCH 309/569] getActiveNodeList -> getActiveConnectionList (be consistent) --- core/client_connections.js | 4 ++-- core/node_msg.js | 4 ++-- core/whos_online.js | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index f0bca4d7..6b8faf65 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -11,7 +11,7 @@ const moment = require('moment'); const hashids = require('hashids'); exports.getActiveConnections = getActiveConnections; -exports.getActiveNodeList = getActiveNodeList; +exports.getActiveConnectionList = getActiveConnectionList; exports.addNewClient = addNewClient; exports.removeClient = removeClient; exports.getConnectionByUserId = getConnectionByUserId; @@ -26,7 +26,7 @@ function getActiveConnections(authUsersOnly = false) { }); } -function getActiveNodeList(authUsersOnly) { +function getActiveConnectionList(authUsersOnly) { if(!_.isBoolean(authUsersOnly)) { authUsersOnly = true; diff --git a/core/node_msg.js b/core/node_msg.js index 2c6f23ed..276dae2f 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -5,7 +5,7 @@ const { MenuModule } = require('./menu_module.js'); const { Errors } = require('./enig_error.js'); const { - getActiveNodeList, + getActiveConnectionList, getConnectionByNodeId, } = require('./client_connections.js'); const UserInterruptQueue = require('./user_interrupt_queue.js'); @@ -110,7 +110,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { location : 'N/A', affils : 'N/A', timeOn : 'N/A', - }].concat(getActiveNodeList(true) + }].concat(getActiveConnectionList(true) .map(node => Object.assign(node, { text : node.node.toString() } )) ).filter(node => node.node !== this.client.node); // remove our client's node this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node diff --git a/core/whos_online.js b/core/whos_online.js index 0ea2321a..5910bd29 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -2,9 +2,9 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const { getActiveNodeList } = require('./client_connections.js'); -const { Errors } = require('./enig_error.js'); +const { MenuModule } = require('./menu_module.js'); +const { getActiveConnectionList } = require('./client_connections.js'); +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); @@ -43,7 +43,7 @@ exports.getModule = class WhosOnlineModule extends MenuModule { return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.onlineList}`)); } - const onlineList = getActiveNodeList(true).slice(0, onlineListView.height).map( + const onlineList = getActiveConnectionList(true).slice(0, onlineListView.height).map( oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) }) ); From 330e1efa781544d96bec3552f9b478d25747f51c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Nov 2018 21:11:33 -0700 Subject: [PATCH 310/569] Updates to user interruptions & node-to-node message module * Can now have header/footer art on node-to-node messages * 'text' and more advanced 'contents' fields * format via 'messageFormat' --- core/node_msg.js | 86 ++++++++++++++++++++++++++++++------ core/user_interrupt_queue.js | 15 +++++-- 2 files changed, 85 insertions(+), 16 deletions(-) diff --git a/core/node_msg.js b/core/node_msg.js index 276dae2f..fde5f35e 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -9,10 +9,14 @@ const { getConnectionByNodeId, } = require('./client_connections.js'); const UserInterruptQueue = require('./user_interrupt_queue.js'); +const { getThemeArt } = require('./theme.js'); +const { pipeToAnsi } = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); // deps -const series = require('async/series'); -const _ = require('lodash'); +const series = require('async/series'); +const _ = require('lodash'); +const async = require('async'); exports.moduleInfo = { name : 'Node Message', @@ -44,18 +48,16 @@ exports.getModule = class NodeMessageModule extends MenuModule { const nodeId = formData.value.node; const message = formData.value.message; - const interruptItem = { - contents : message, - } + this.createInterruptItem(message, (err, interruptItem) => { + if(0 === nodeId) { + // ALL nodes + UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client); + } else { + UserInterruptQueue.queueGlobal(interruptItem, [ getConnectionByNodeId(nodeId) ]); + } - if(0 === nodeId) { - // ALL nodes - UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client); - } else { - UserInterruptQueue.queueGlobal(interruptItem, [ getConnectionByNodeId(nodeId) ]); - } - - return this.prevMenu(cb); + return this.prevMenu(cb); + }); }, } } @@ -96,6 +98,64 @@ exports.getModule = class NodeMessageModule extends MenuModule { }); } + createInterruptItem(message, cb) { + const textFormatObj = { + fromUserName : this.client.user.username, + fromRealName : this.client.user.properties.real_name, + fromNodeId : this.client.node, + message : message, + }; + + const messageFormat = + this.config.messageFormat || + 'Message from {fromUserName} on node {fromNodeId}:\r\n{message}'; + + const item = { + text : stringFormat(messageFormat, textFormatObj), + pause : true, + }; + + const getArt = (name, callback) => { + const spec = _.get(this.config, `art.${name}`); + if(!spec) { + return callback(null); + } + const getArtOpts = { + name : spec, + client : this.client, + random : false, + }; + getThemeArt(getArtOpts, (err, artInfo) => { + // ignore errors + return callback(artInfo ? artInfo.data : null); + }); + }; + + async.waterfall( + [ + (callback) => { + getArt('header', headerArt => { + return callback(null, headerArt); + }); + }, + (headerArt, callback) => { + getArt('footer', footerArt => { + return callback(null, headerArt, footerArt); + }); + }, + (headerArt, footerArt, callback) => { + if(headerArt || footerArt) { + item.contents = `${headerArt || ''}\r\n${pipeToAnsi(item.text)}\r\n${footerArt || ''}`; + } + return callback(null); + } + ], + err => { + return cb(err, item); + } + ); + } + prepareNodeList() { // standard node list with {text} field added for compliance this.nodeList = [{ diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index a29fd4df..be6767d5 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -7,6 +7,7 @@ const { getActiveConnections } = require('./client_connections.js'); const ANSI = require('./ansi_term.js'); +const { pipeToAnsi } = require('./color_codes.js'); // deps const _ = require('lodash'); @@ -31,6 +32,9 @@ module.exports = class UserInterruptQueue } queueItem(interruptItem) { + if(!_.isString(interruptItem.contents) && !_.isString(interruptItem.text)) { + return; + } interruptItem.pause = _.get(interruptItem, 'pause', true); this.queue.push(interruptItem); } @@ -51,8 +55,13 @@ module.exports = class UserInterruptQueue this.client.term.rawWrite('\r\n\r\n'); } - Art.display(this.client, interruptItem.contents, err => { - return cb(err, interruptItem); - }); + if(interruptItem.contents) { + Art.display(this.client, interruptItem.contents, err => { + this.client.term.rawWrite('\r\n\r\n'); + return cb(err, interruptItem); + }); + } else { + return this.client.term.write(pipeToAnsi(`${interruptItem.text}\r\n\r\n`, this.client), cb); + } } }; \ No newline at end of file From f59b4b883f3edcaeee504f909ffea15f76999866 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Nov 2018 21:25:46 -0700 Subject: [PATCH 311/569] Update basic info in menu.hjson, fix email addr --- config/menu.hjson | 45 ++++++++++++++++++++++++++--------- misc/config_template.in.hjson | 2 +- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/config/menu.hjson b/config/menu.hjson index 1c48ef63..8f1aca3b 100644 --- a/config/menu.hjson +++ b/config/menu.hjson @@ -10,20 +10,43 @@ /__ _\ <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - ------------------------------------------------------------------------------- + *-----------------------------------------------------------------------------* - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec - JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. - - See http://hjson.org/ for more information and syntax. + General Information + ------------------------------- - - + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. + See http://hjson.org/ for more information and syntax. - If you haven't yet, copy the conents of this file to something like - sick_board.hjson. Point to it via config.hjson using the - 'general.menuFile' key: - - general: { menuFile: "sick_board.hjson" } - + Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so + on have syntax highlighting for the HJSON format which are highly recommended. + + ------------------------------- -- - - + Menu Configuration + ------------------------------- - - + ENiGMA½ makes no assumptions about specific menu types (main, doors, etc.), + but instead allows full customization of all menus throughout the system. + Some menus such as a main menu are considered "standard" while others are + backed by a specific module. SysOps can tweak various settings about these + modules (look & feel, keyboard interation, and so on) or even fully replace + the module with something else. + + This file starts out as an example setup. Look at the examples, change + settings, menu ordering/flow, add/remove menus, implement ACS control, + etc.! + + Remember you can *live edit* this file. That is, make a change and save + while you're logged into the system and it will take effect on the next + menu change or screen refresh. + + Please see RTFM ...er, uh... see the documentation for more information, and + don't be shy to ask for help: + + BBS : Xibalba @ xibalba.l33t.codes + FTN : BBS Discussion on fsxNet + IRC : #enigma-bbs / FreeNode + Email : bryan@l33t.codes */ menus: { // diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 296e7826..3acb0823 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -44,7 +44,7 @@ BBS : Xibalba @ xibalba.l33t.codes FTN : BBS Discussion on fsxNet IRC : #enigma-bbs / FreeNode - Email : bryan@leet.codes + Email : bryan@l33t.codes */ general: { From 96d30af5d2cf0480f798adaf201467a8b18dbb4a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 13 Nov 2018 21:32:22 -0700 Subject: [PATCH 312/569] POSSIBLY BREAKING: config/menu.hjson and config/prompt.hjson are now moved to misc/menu_template.in.hjson and config/prompt_template.in.hjson. These are now simply used as template files and copied to config/ with proper naming when issuing 'oputil.js config new' --- core/config.js | 4 ++-- core/oputil/oputil_config.js | 4 ++-- config/menu.hjson => misc/menu_template.in.hjson | 0 config/prompt.hjson => misc/prompt_template.in.hjson | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename config/menu.hjson => misc/menu_template.in.hjson (100%) rename config/prompt.hjson => misc/prompt_template.in.hjson (100%) diff --git a/core/config.js b/core/config.js index c1fffc45..97091be5 100644 --- a/core/config.js +++ b/core/config.js @@ -138,8 +138,8 @@ function getDefaultConfig() { closedSystem : false, // is the system closed to new users? loginAttempts : 3, - menuFile : 'menu.hjson', // Override to use something else, e.g. demo.hjson. Can be a full path (defaults to ./config) - promptFile : 'prompt.hjson', // Override to use soemthing else, e.g. myprompt.hjson. Can be a full path (defaults to ./config) + menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path + promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path }, users : { diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 40644dee..83ac5232 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -263,14 +263,14 @@ function buildNewConfig() { .toLowerCase(); const menuFile = `${bn}-menu.hjson`; copyFileSyncSilent( - paths.join(__dirname, '../../config/menu.hjson'), + paths.join(__dirname, '../../misc/menu_template.in.hjson'), paths.join(__dirname, '../../config/', menuFile), fs.constants.COPYFILE_EXCL ); const promptFile = `${bn}-prompt.hjson`; copyFileSyncSilent( - paths.join(__dirname, '../../config/prompt.hjson'), + paths.join(__dirname, '../../misc/prompt_template.in.hjson'), paths.join(__dirname, '../../config/', promptFile), fs.constants.COPYFILE_EXCL ) diff --git a/config/menu.hjson b/misc/menu_template.in.hjson similarity index 100% rename from config/menu.hjson rename to misc/menu_template.in.hjson diff --git a/config/prompt.hjson b/misc/prompt_template.in.hjson similarity index 100% rename from config/prompt.hjson rename to misc/prompt_template.in.hjson From 23c9e89849d349aba06d85196cf60bf6afd1ba42 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 19:16:54 -0700 Subject: [PATCH 313/569] Add default keys to menu --- art/themes/luciano_blocktronics/MSGMNU.ANS | Bin 3510 -> 3660 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MSGMNU.ANS b/art/themes/luciano_blocktronics/MSGMNU.ANS index ce6f4815645341d1f226f2de457a4156a048fe62..3585799b98f1e143184bf8efa54e5da16bfccba8 100644 GIT binary patch delta 294 zcmdlceMV-32MeR=%_l3fDKZ%uPhQL7!Duqslr=^rOF=r?z}na> zH#s9QFFjQuIX^EgwJ0?&IaNB^*gRJ{+Q`Ux@;O#NO9cg>O2b?mps+!%K3LKYs#T$& zD78#KFSVisi+0n=iENpZC$dU285>Q0$ChjiH7FX%pjb?U5{pt3VJ@(o9Lnw|Xk_6G z6fnyLIs@!5v&rjNVI?UMglC*q9m}$nF-?A@e LWqde!BCjd{cdAv1 delta 151 zcmX>jvrT$~2Mec(sk3ynu~F{*$^9&$?8fG~($OY{let+v7|kYovbar-W(}QugiV^s z(0KASc6mmH$yZqYChuY6nEaAOLDmAO*DyC-I@-Y6*eq8ey(BGP5oj975JQv6(QKKM vT{)z<6re&zrjsACC9_CJn;J|mXZK?=GoAdC-3>@>Zsu6b%IG~=olg}26uT=H From 23af00e7ec0145c0a89c1afae8916adea81cacd2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 19:52:55 -0700 Subject: [PATCH 314/569] Add timestamp --- core/node_msg.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/node_msg.js b/core/node_msg.js index fde5f35e..f98db5f1 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -17,6 +17,7 @@ const stringFormat = require('./string_format.js'); const series = require('async/series'); const _ = require('lodash'); const async = require('async'); +const moment = require('moment'); exports.moduleInfo = { name : 'Node Message', @@ -104,6 +105,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { fromRealName : this.client.user.properties.real_name, fromNodeId : this.client.node, message : message, + timestamp : moment(), }; const messageFormat = From 941e7d0a03b50aa9df96c6584a48d5704c4d79e0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 19:53:14 -0700 Subject: [PATCH 315/569] Fix ./main.js --version --- core/bbs.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/bbs.js b/core/bbs.js index f3d59200..b4f88296 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -45,6 +45,10 @@ function printHelpAndExit() { process.exit(); } +function printVersionAndExit() { + console.info(require('../package.json').version); +} + function main() { async.waterfall( [ @@ -52,7 +56,11 @@ function main() { const argv = require('minimist')(process.argv.slice(2)); if(argv.help) { - printHelpAndExit(); + return printHelpAndExit(); + } + + if(argv.version) { + return printVersionAndExit(); } const configOverridePath = argv.config; From b3930d1999d04808ca9743b2109f0c86458fc7f1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 20:51:11 -0700 Subject: [PATCH 316/569] WIP on node-to-node msg + Preview * Fix node IDs + Add new MenuModule method for validating MCI codes --- core/menu_module.js | 16 ++++++++++++++ core/node_msg.js | 49 ++++++++++++++++++++++++++++++----------- core/view_controller.js | 4 ++++ 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index ce8eb556..2dd40b9e 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -549,4 +549,20 @@ exports.MenuModule = class MenuModule extends PluginModule { }); } } + + validateMCIByViewIds(formName, viewIds, cb) { + if(!Array.isArray(viewIds)) { + viewIds = [ viewIds ]; + } + const form = _.get(this, [ 'viewControllers', formName ] ); + if(!form) { + return cb(Errors.DoesNotExist(`Form does not exist: ${formName}`)); + } + for(let i = 0; i < viewIds.length; ++i) { + if(!form.hasView(viewIds[i])) { + return cb(Errors.MissingMci(`Missing MCI ${viewIds[i]}`)); + } + } + return cb(null); + } }; diff --git a/core/node_msg.js b/core/node_msg.js index f98db5f1..412e14ab 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -46,15 +46,18 @@ exports.getModule = class NodeMessageModule extends MenuModule { this.menuMethods = { sendMessage : (formData, extraArgs, cb) => { - const nodeId = formData.value.node; + const nodeId = this.nodeList[formData.value.node].node; // index from from -> node! const message = formData.value.message; this.createInterruptItem(message, (err, interruptItem) => { - if(0 === nodeId) { + if(-1 === nodeId) { // ALL nodes UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client); } else { - UserInterruptQueue.queueGlobal(interruptItem, [ getConnectionByNodeId(nodeId) ]); + const conn = getConnectionByNodeId(nodeId); + if(conn) { + UserInterruptQueue.queueGlobal(interruptItem, [ conn ]); + } } return this.prevMenu(cb); @@ -71,15 +74,18 @@ exports.getModule = class NodeMessageModule extends MenuModule { series( [ - (next) => { - return this.prepViewController('sendMessage', FormIds.sendMessage, mciData.menu, next); + (callback) => { + return this.prepViewController('sendMessage', FormIds.sendMessage, mciData.menu, callback); }, - (next) => { + (callback) => { + return this.validateMCIByViewIds( + 'sendMessage', + [ MciViewIds.sendMessage.nodeSelect, MciViewIds.sendMessage.message ], + callback + ); + }, + (callback) => { const nodeSelectView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.nodeSelect); - if(!nodeSelectView) { - return next(Errors.MissingMci(`Missing node selection MCI ${MciViewIds.sendMessage.nodeSelect}`)); - } - this.prepareNodeList(); nodeSelectView.on('index update', idx => { @@ -89,7 +95,24 @@ exports.getModule = class NodeMessageModule extends MenuModule { nodeSelectView.setItems(this.nodeList); nodeSelectView.redraw(); this.nodeListSelectionIndexUpdate(0); - return next(null); + return callback(null); + }, + (callback) => { + const previewView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.preview); + if(!previewView) { + return callback(null); // preview is optional + } + + const messageView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.message); + let timerId; + messageView.on('key press', () => { + clearTimeout(timerId); + const focused = this.viewControllers.sendMessage.getFocusedView(); + if(focused === messageView) { + previewView.setText(messageView.getData()); + focused.setFocus(true); + } + }, 500); } ], err => { @@ -163,7 +186,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { this.nodeList = [{ text : '-ALL-', // dummy fields: - node : 0, + node : -1, authenticated : false, userId : 0, action : 'N/A', @@ -173,7 +196,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { affils : 'N/A', timeOn : 'N/A', }].concat(getActiveConnectionList(true) - .map(node => Object.assign(node, { text : node.node.toString() } )) + .map(node => Object.assign(node, { text : -1 == node.node ? '-ALL-' : node.node.toString() } )) ).filter(node => node.node !== this.client.node); // remove our client's node this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node } diff --git a/core/view_controller.js b/core/view_controller.js index cd0dd3b0..de6f1f05 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -431,6 +431,10 @@ ViewController.prototype.getView = function(id) { return this.views[id]; }; +ViewController.prototype.hasView = function(id) { + return this.getView(id) ? true : false; +} + ViewController.prototype.getViewsByMciCode = function(mciCode) { if(!Array.isArray(mciCode)) { mciCode = [ mciCode ]; From ea4fb090e25ff2c1c6543d450e3a2d1aa1ddcf66 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 21:00:21 -0700 Subject: [PATCH 317/569] Better error report from spawn() fail in extractTo() --- core/archive_util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/archive_util.js b/core/archive_util.js index 6a58f935..b10f364f 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -272,7 +272,9 @@ module.exports = class ArchiveUtil { try { proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath)); } catch(e) { - return cb(e); + return cb(Errors.ExternalProcess( + `Error spawning archiver process "${archiver[action].cmd}" with args "${args.join(' ')}": ${e.message}`) + ); } return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb); From b48f3f3d420c130102602ef0fd41c8c3f6c39ba6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 21:05:48 -0700 Subject: [PATCH 318/569] Better reject/retain notes --- docs/messageareas/message-networks.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/messageareas/message-networks.md b/docs/messageareas/message-networks.md index fec31a08..1c251712 100644 --- a/docs/messageareas/message-networks.md +++ b/docs/messageareas/message-networks.md @@ -72,6 +72,7 @@ Example: | `packetMsgEncoding` | :-1: | Override default `utf8` encoding. | `defaultNetwork` | :-1: | Explicitly set default network (by tag in `messageNetworks.ftn.networks`). If not set, the first found is used. | | `nodes` | :+1: | Per-node settings. Entries (keys) here support wildcards for a portion of the FTN-style address (e.g.: `21:1/*`). `archiveType` may be set to a FTN supported archive extention that the system supports (TODO); if unset, only .PKT files are produced. `encoding` may be set to override `packetMsgEncoding` on a per-node basis. If the node requires a packet password, set `packetPassword` | +| `paths` | :-1: | An optional configuration block that can set a `retain` path and/or a `reject` path. These will be used for archiving processed packets. | Example: ```hjson @@ -85,6 +86,12 @@ scannerTossers: { export: every 15 minutes or @immediate } + // optional + paths: { + reject: /path/to/store/bad/packets/ + retain: /path/to/store/good/packets/ + } + // Override default FTN/BSO packet encoding. Defaults to 'utf8' packetMsgEncoding: utf8 From e4cb93a17c224c0aa9fc2ed5e8238560204e76f1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 21:24:15 -0700 Subject: [PATCH 319/569] Use better logging for archiver spawn() --- core/archive_util.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/archive_util.js b/core/archive_util.js index b10f364f..8549cd12 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -222,7 +222,9 @@ module.exports = class ArchiveUtil { try { proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts()); } catch(e) { - return cb(e); + return cb(Errors.ExternalProcess( + `Error spawning archiver process "${archiver.compress.cmd}" with args "${args.join(' ')}": ${e.message}`) + ); } return this.spawnHandler(proc, 'Compression', cb); @@ -297,7 +299,9 @@ module.exports = class ArchiveUtil { try { proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); } catch(e) { - return cb(e); + return cb(Errors.ExternalProcess( + `Error spawning archiver process "${archiver.list.cmd}" with args "${args.join(' ')}": ${e.message}`) + ); } let output = ''; From 2c6ba680dc0acec2a62ee38fd4262d0274a1d598 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 21:45:20 -0700 Subject: [PATCH 320/569] Fix up FTN/BSO vs Message Network docs a bit more --- docs/messageareas/bso-import-export.md | 85 ++++++++++++++++++++------ docs/messageareas/message-networks.md | 13 +--- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md index 49e533d8..106fd65a 100644 --- a/docs/messageareas/bso-import-export.md +++ b/docs/messageareas/bso-import-export.md @@ -2,24 +2,24 @@ layout: page title: BSO Import / Export --- -The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and -scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` -under `scannerTossers::ftn_bso`. +The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`. -| Config Item | Required | Description | -|-------------------------|----------|---------------------------------------------------------------------------------| -| `defaultZone` | :+1: | Sets the default BSO outbound zone -| `defaultNetwork` | :-1: | Sets the default network name from `messageNetworks.ftn.networks`. **Required if more than one network is defined**. -| `paths` | :-1: | Override default paths set by the system. This section may contain `outbound`, `inbound`, and `secInbound`. -| `packetTargetByteSize` | :-1: | Overrides the system *target* packet (.pkt) size of 512000 bytes (512k) -| `bundleTargetByteSize` | :-1: | Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M) -| `schedule` | :+1: | See Scheduling -| `nodes` | :+1: | See Nodes +:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external utility such as Binkd is required for this! + +Let's look at some of the basic configuration: + +| Config Item | Required | Description | +|-------------|----------|----------------------------------------------------------| +| `schedule` | :+1: | Sets `import` and `export` schedules. [Later style text parsing](https://bunkat.github.io/later/parsers.html#text) supported. `import` also can utilize a `@watch:` syntax while `export` additionally supports `@immediate`. | +| `packetMsgEncoding` | :-1: | Override default `utf8` encoding. +| `defaultNetwork` | :-1: | Explicitly set default network (by tag in `messageNetworks.ftn.networks`). If not set, the first found is used. | +| `nodes` | :+1: | Per-node settings. Entries (keys) here support wildcards for a portion of the FTN-style address (e.g.: `21:1/*`). `archiveType` may be set to a FTN supported archive extention that the system supports (TODO); if unset, only .PKT files are produced. `encoding` may be set to override `packetMsgEncoding` on a per-node basis. If the node requires a packet password, set `packetPassword` | +| `paths` | :-1: | An optional configuration block that can set a `retain` path and/or a `reject` path. These will be used for archiving processed packets. You may additionally override the default `outbound`, `inbound`, and `secInbound` (secure inbound) *base* paths for packet processing. | +| `packetTargetByteSize` | :-1: | Overrides the system *target* packet (.pkt) size of 512000 bytes (512k) | +| `bundleTargetByteSize` | :-1: | Overrides the system *target* ArcMail bundle size of 2048000 bytes (2M) | ## Scheduling -Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. -Each entry is allowed a "free form" text and/or special indicators for immediate export or watch -file triggers. +Schedules can be defined for importing and exporting via `import` and `export` under `schedule`. Each entry is allowed a "free form" text and/or special indicators for immediate export or watch file triggers. * `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`. * `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`. @@ -27,7 +27,7 @@ file triggers. See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information. -### Example Configuration +### Example Schedule Configuration ```hjson { @@ -45,8 +45,7 @@ See [Later text parsing documentation](http://bunkat.github.io/later/parsers.htm ## Nodes The `nodes` section defines how to export messages for one or more uplinks. -A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may -contain wildcard(s) for net/zone/node/point/domain. +A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain. | Config Item | Required | Description | |------------------|----------|---------------------------------------------------------------------------------| @@ -61,7 +60,7 @@ contain wildcard(s) for net/zone/node/point/domain. scannerTossers: { ftn_bso: { nodes: { - "46:*": { + "21:*": { packetType: 2+ packetPassword: mypass encoding: cp437 @@ -71,4 +70,52 @@ contain wildcard(s) for net/zone/node/point/domain. } } } +``` + +## A More Complete Example +Below is a more complete example showing the sections described above. + +```hjson +scannerTossers: { + ftn_bso: { + schedule: { + // Check every 30m, or whenever the "toss!.now" file is touched (ie: by Binkd) + import: every 30 minutes or @watch:/enigma-bbs/mail/ftn_in/toss!.now + + // Export immediately, but also check every 15m to be sure + export: every 15 minutes or @immediate + } + + // optional + paths: { + reject: /path/to/store/bad/packets/ + retain: /path/to/store/good/packets/ + } + + // Override default FTN/BSO packet encoding. Defaults to 'utf8' + packetMsgEncoding: utf8 + + defaultNetwork: fsxnet + + nodes: { + "21:1/100" : { // May also contain wildcards, ie: "21:1/*" + archiveType: ZIP // By-ext archive type: ZIP, ARJ, ..., optional. + encoding: utf8 // Encoding for exported messages + packetPassword: MUHPA55 // FTN .PKT password, optional + + tic: { + // See TIC docs + } + } + } + + netMail: { + // See NetMail docs + } + + ticAreas: { + // See TIC docs + } + } +} ``` \ No newline at end of file diff --git a/docs/messageareas/message-networks.md b/docs/messageareas/message-networks.md index 1c251712..10904cab 100644 --- a/docs/messageareas/message-networks.md +++ b/docs/messageareas/message-networks.md @@ -9,9 +9,9 @@ ENiGMA½ considers all non-ENiGMA½, non-local messages (and their networks, suc 3. `scannerTossers.`: general configuration for the scanner/tosser (import/export). This is also where we configure per-node settings. ## FTN Networks -FidoNet and FidoNet style (FTN) networks as well as a FTN/BSO scanner/tosser (`ftn_bso` module) are configured via the `messageNetworks.ftn` and `scannerTossers.ftn_bso` blocks in `config.hjson`. +FidoNet and FidoNet style (FTN) networks as well as a [FTN/BSO scanner/tosser](bso-import-export.md) (`ftn_bso` module) are configured via the `messageNetworks.ftn` and `scannerTossers.ftn_bso` blocks in `config.hjson`. -:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external utility such as Binkd is required for this +:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external utility such as Binkd is required for this! ### Networks The `networks` block a per-network configuration where each entry's key may be referenced elswhere in `config.hjson`. @@ -65,14 +65,7 @@ Example: ``` ### FTN/BSO Scanner Tosser - -| Config Item | Required | Description | -|-------------|----------|----------------------------------------------------------| -| `schedule` | :+1: | Sets `import` and `export` schedules. [Later style text parsing](https://bunkat.github.io/later/parsers.html#text) supported. `import` also can utilize a `@watch:` syntax while `export` additionally supports `@immediate`. | -| `packetMsgEncoding` | :-1: | Override default `utf8` encoding. -| `defaultNetwork` | :-1: | Explicitly set default network (by tag in `messageNetworks.ftn.networks`). If not set, the first found is used. | -| `nodes` | :+1: | Per-node settings. Entries (keys) here support wildcards for a portion of the FTN-style address (e.g.: `21:1/*`). `archiveType` may be set to a FTN supported archive extention that the system supports (TODO); if unset, only .PKT files are produced. `encoding` may be set to override `packetMsgEncoding` on a per-node basis. If the node requires a packet password, set `packetPassword` | -| `paths` | :-1: | An optional configuration block that can set a `retain` path and/or a `reject` path. These will be used for archiving processed packets. | +Please see the [FTN/BSO Scanner/Tosser](bso-import-export.md) documentation for information on this area. Example: ```hjson From cc9c1439273346f09909ed15f2219e9f8e76fa1d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 22:04:29 -0700 Subject: [PATCH 321/569] Cleanup & prep for real-time interrupt --- core/menu_module.js | 22 +++++++++++++--------- core/user_interrupt_queue.js | 6 +++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 2dd40b9e..1e68823c 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -42,14 +42,6 @@ exports.MenuModule = class MenuModule extends PluginModule { this.detachViewControllers(); } - toggleInterruptionAndDisplayQueued(cb) { - this.enableInterruption(); - this.displayQueuedInterruptions( () => { - this.disableInterruption(); - return cb(null); - }); - } - initSequence() { const self = this; const mciData = {}; @@ -185,6 +177,14 @@ exports.MenuModule = class MenuModule extends PluginModule { } } + toggleInterruptionAndDisplayQueued(cb) { + this.enableInterruption(); + this.displayQueuedInterruptions( () => { + this.disableInterruption(); + return cb(null); + }); + } + displayQueuedInterruptions(cb) { if(true !== this.interruptable) { return cb(null); @@ -193,7 +193,7 @@ exports.MenuModule = class MenuModule extends PluginModule { async.whilst( () => this.client.interruptQueue.hasItems(), next => { - this.client.interruptQueue.display( (err, interruptItem) => { + this.client.interruptQueue.displayNext( (err, interruptItem) => { if(err) { return next(err); } @@ -211,6 +211,10 @@ exports.MenuModule = class MenuModule extends PluginModule { ) } + attemptInterruptNow(interruptItem, cb) { + return cb(null, false); + } + getSaveState() { // nothing in base } diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index be6767d5..1a62e063 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -43,12 +43,16 @@ module.exports = class UserInterruptQueue return this.queue.length > 0; } - display(cb) { + displayNext(cb) { const interruptItem = this.queue.pop(); if(!interruptItem) { return cb(null); } + return interruptItem ? this.displayWithItem(interruptItem, cb) : cb(null); + } + + displayWithItem(interruptItem, cb) { if(interruptItem.cls) { this.client.term.rawWrite(ANSI.clearScreen()); } else { From c5100d741a8349b0fdd85ccbc4505e11ebfd2110 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 22:05:05 -0700 Subject: [PATCH 322/569] Del dupe config example --- docs/messageareas/message-networks.md | 46 --------------------------- 1 file changed, 46 deletions(-) diff --git a/docs/messageareas/message-networks.md b/docs/messageareas/message-networks.md index 10904cab..84b7859e 100644 --- a/docs/messageareas/message-networks.md +++ b/docs/messageareas/message-networks.md @@ -66,49 +66,3 @@ Example: ### FTN/BSO Scanner Tosser Please see the [FTN/BSO Scanner/Tosser](bso-import-export.md) documentation for information on this area. - -Example: -```hjson -scannerTossers: { - ftn_bso: { - schedule: { - // Check every 30m, or whenever the "toss!.now" file is touched (ie: by Binkd) - import: every 30 minutes or @watch:/enigma-bbs/mail/ftn_in/toss!.now - - // Export immediately, but also check every 15m to be sure - export: every 15 minutes or @immediate - } - - // optional - paths: { - reject: /path/to/store/bad/packets/ - retain: /path/to/store/good/packets/ - } - - // Override default FTN/BSO packet encoding. Defaults to 'utf8' - packetMsgEncoding: utf8 - - defaultNetwork: fsxnet - - nodes: { - "21:1/100" : { // May also contain wildcards, ie: "21:1/*" - archiveType: ZIP // By-ext archive type: ZIP, ARJ, ..., optional. - encoding: utf8 // Encoding for exported messages - packetPassword: MUHPA55 // FTN .PKT password, optional - - tic: { - // See TIC docs - } - } - } - - netMail: { - // See NetMail docs - } - - ticAreas: { - // See TIC docs - } - } -} -``` \ No newline at end of file From 4d5b8c0dd94fd9b4d5cc33cdcf0c0d0153fe44ed Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 22:10:03 -0700 Subject: [PATCH 323/569] Not official links, but better than nothing for now --- docs/messageareas/bso-import-export.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md index 106fd65a..af8784d1 100644 --- a/docs/messageareas/bso-import-export.md +++ b/docs/messageareas/bso-import-export.md @@ -4,7 +4,7 @@ title: BSO Import / Export --- The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`. -:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external utility such as Binkd is required for this! +:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external [mailer](http://www.filegate.net/bbsmailers.htm) such as [Binkd](https://github.com/pgul/binkd) is required for this! Let's look at some of the basic configuration: From 7f67e9adc7243208f8c151415681b26a4144fd98 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 22:14:08 -0700 Subject: [PATCH 324/569] Add link to Binkd blog --- docs/messageareas/bso-import-export.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md index af8784d1..ac645c63 100644 --- a/docs/messageareas/bso-import-export.md +++ b/docs/messageareas/bso-import-export.md @@ -118,4 +118,7 @@ scannerTossers: { } } } -``` \ No newline at end of file +``` + +## Additional Resources +* [Blog entry on setting up ENiGMA + Binkd on CentOS7](http2://l33t.codes/enigma-12-binkd-on-centos-7/). Note that this references an **older version**, so be wary of the `config.hjson` refernces! From f4e710b37903cf7c1fbab861bf9d80545b4da0ce Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 22:15:19 -0700 Subject: [PATCH 325/569] Typo fix --- docs/messageareas/bso-import-export.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md index ac645c63..88b36a25 100644 --- a/docs/messageareas/bso-import-export.md +++ b/docs/messageareas/bso-import-export.md @@ -121,4 +121,4 @@ scannerTossers: { ``` ## Additional Resources -* [Blog entry on setting up ENiGMA + Binkd on CentOS7](http2://l33t.codes/enigma-12-binkd-on-centos-7/). Note that this references an **older version**, so be wary of the `config.hjson` refernces! +* [Blog entry on setting up ENiGMA + Binkd on CentOS7](https://l33t.codes/enigma-12-binkd-on-centos-7/). Note that this references an **older version**, so be wary of the `config.hjson` refernces! From 080d1727c2b12ea218ef75f96d9307b993f551d2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 14 Nov 2018 22:47:20 -0700 Subject: [PATCH 326/569] WIP on real-time interruptions (ie: incoming message) Still need work on *when* they are allowed with good defaults, etc. --- core/menu_module.js | 32 ++++++++++++++++++-------------- core/user_interrupt_queue.js | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 1e68823c..bd75c94c 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -192,19 +192,7 @@ exports.MenuModule = class MenuModule extends PluginModule { async.whilst( () => this.client.interruptQueue.hasItems(), - next => { - this.client.interruptQueue.displayNext( (err, interruptItem) => { - if(err) { - return next(err); - } - - if(interruptItem.pause) { - return this.pausePrompt(next); - } - - return next(null); - }); - }, + next => this.client.interruptQueue.displayNext(next), err => { return cb(err); } @@ -212,7 +200,21 @@ exports.MenuModule = class MenuModule extends PluginModule { } attemptInterruptNow(interruptItem, cb) { - return cb(null, false); + if(true !== this.interruptable) { + return cb(null, false); // don't eat up the item; queue for later + } + + // + // Default impl: clear screen -> standard display -> reload menu + // + this.client.interruptQueue.displayWithItem(Object.assign({}, interruptItem, { cls : true }), err => { + if(err) { + return cb(err, false); + } + this.reload(err => { + return cb(err, err ? false : true); + }); + }); } getSaveState() { @@ -311,6 +313,8 @@ exports.MenuModule = class MenuModule extends PluginModule { } else { return gotoNextMenu(); } + } else { + this.enableInterruption(); } } diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index 1a62e063..e48fc2ea 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -35,8 +35,17 @@ module.exports = class UserInterruptQueue if(!_.isString(interruptItem.contents) && !_.isString(interruptItem.text)) { return; } + + // pause defaulted on interruptItem.pause = _.get(interruptItem, 'pause', true); - this.queue.push(interruptItem); + + this.client.currentMenuModule.attemptInterruptNow(interruptItem, (err, ateIt) => { + if(err) { + // :TODO: Log me + } else if(true !== ateIt) { + this.queue.push(interruptItem); + } + }); } hasItems() { @@ -61,8 +70,13 @@ module.exports = class UserInterruptQueue if(interruptItem.contents) { Art.display(this.client, interruptItem.contents, err => { - this.client.term.rawWrite('\r\n\r\n'); - return cb(err, interruptItem); + if(err) { + return cb(err); + } + //this.client.term.rawWrite('\r\n\r\n'); // :TODO: Prob optional based on contents vs text + this.client.currentMenuModule.pausePrompt( () => { + return cb(null); + }); }); } else { return this.client.term.write(pipeToAnsi(`${interruptItem.text}\r\n\r\n`, this.client), cb); From 3b0a872eaf61205b5b1a23ff126da359cc59718b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 15 Nov 2018 16:42:40 -0700 Subject: [PATCH 327/569] Basic updates. WIP --- docs/art/themes.md | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/art/themes.md b/docs/art/themes.md index 9ef482b6..7fb02ef2 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -2,9 +2,29 @@ layout: page title: Themes --- -# Creating Your Own -:warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included -`luciano_blocktronics' theme. Create your own and make changes to that instead: +## Themes +ENiGMA½ comes with an advanced theming system allowing system operators to highly customize the look and feel of their boards. A given installation can have as many themes as you like for your users to choose from. + +## General Information +Themes live in `art/themes/`. Each theme (and thus it's *theme ID*) is a directory within the `themes` directory. The theme itself is simply a collection of art files, and a `theme.hjson` file that further defines layout, colors & formatting, etc. ENiGMA½ comes with a default theme by [Luciano Ayres](http://blocktronics.org/tag/luciano-ayres/) of [Blocktronics](http://blocktronics.org/) called Mystery Skull. This theme is in `art/themes/luciano_blocktronics`, and thus it's *theme ID* is `luciano_blocktronics`. + +## Theme Sections +Themes are have some important sections to be aware of: + +* `info`: This section describes the theme. You may set the `enabled` field to `false` to disable it (Users assigned to this theme fall back to the default set in your `config.hjson`). +* `customization`: The beef! + +### Theme Section: Customization +The `customization` block in `theme.hjson` is itself broken up into major parts: +* `defaults`: Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. +* `menus`: The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. +* `prompts`: Similar to `menus`, this file themes prompts found in `prompts.hjson`. + +TODO: More information about theming! + + +## Creating Your Own +:warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included `luciano_blocktronics' theme. Create your own and make changes to that instead: 1. Copy `/art/themes/luciano_blocktronics` to `art/themes/your_board_theme` 2. Update the `info` block at the top of the theme.hjson file: @@ -17,13 +37,10 @@ title: Themes } ``` -3. Specify it in the `defaults` section of `config.hjson`. The name supplied should match the name of the -directory you created in step 1: - +3. If desired, you may make this the default system theme in `config.hjson` via `theme.default`. `theme.preLogin` may be set if you want this theme used for pre-authenticated users. Both of these values also accept `*` if you want the system to radomly pick. ``` hjson - defaults: { - theme: your_board_theme - } + theme: { + default: your_board_theme + preLogin: * + } ``` - -# General Theme Info \ No newline at end of file From f9429dd2e666034324e3f50c6ec8c8ccb64cdc8e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 15 Nov 2018 20:32:08 -0700 Subject: [PATCH 328/569] Move where passwordChar lives --- art/themes/luciano_blocktronics/theme.hjson | 4 +--- core/theme.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index aa720934..d23904af 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -9,9 +9,7 @@ customization: { defaults: { - general: { - passwordChar: * - } + passwordChar: * dateTimeFormat: { short: MMM Do h:mm a diff --git a/core/theme.js b/core/theme.js index 2f88027e..6414da62 100644 --- a/core/theme.js +++ b/core/theme.js @@ -38,7 +38,7 @@ function refreshThemeHelpers(theme) { getPasswordChar : function() { let pwChar = _.get( theme, - 'customization.defaults.general.passwordChar', + 'customization.defaults.passwordChar', Config().theme.passwordChar ); From 3313421341b8407e5d092fdb485027f30f44b14c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 15 Nov 2018 20:32:19 -0700 Subject: [PATCH 329/569] Yet more docs on themes... more to come! --- docs/art/themes.md | 101 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/docs/art/themes.md b/docs/art/themes.md index 7fb02ef2..c3ac38cf 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -8,19 +8,102 @@ ENiGMA½ comes with an advanced theming system allowing system operators to high ## General Information Themes live in `art/themes/`. Each theme (and thus it's *theme ID*) is a directory within the `themes` directory. The theme itself is simply a collection of art files, and a `theme.hjson` file that further defines layout, colors & formatting, etc. ENiGMA½ comes with a default theme by [Luciano Ayres](http://blocktronics.org/tag/luciano-ayres/) of [Blocktronics](http://blocktronics.org/) called Mystery Skull. This theme is in `art/themes/luciano_blocktronics`, and thus it's *theme ID* is `luciano_blocktronics`. +## Art +Of course one of the most basic elements of BBS theming is art. ENiGMA½ uses a fallback system for art selection by default (you may override this in a `menu.hjson` entry if desired). When a menu entry calls for a piece of art, the following search is made: + +1. If a direct or relative path is supplied, look there first. +2. In the users current theme directory. +3. In the system default theme directory. +4. In the `art/general` directory. + +TL;DR: In general, to theme a piece of art, create a version of it in your themes directory. + +:information: Remember that by default, the system will allow for randomly selecting art (in one of the directories mentioned above) by numbering it: `FOO1.ANS`, `FOO2.ANS`, etc.! + ## Theme Sections -Themes are have some important sections to be aware of: +Themes are some important sections to be aware of: -* `info`: This section describes the theme. You may set the `enabled` field to `false` to disable it (Users assigned to this theme fall back to the default set in your `config.hjson`). -* `customization`: The beef! +| Config Item | Description | +|-------------|----------------------------------------------------------| +| `info` | This section describes the theme. | +| `customization` | The beef! | -### Theme Section: Customization -The `customization` block in `theme.hjson` is itself broken up into major parts: -* `defaults`: Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. -* `menus`: The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. -* `prompts`: Similar to `menus`, this file themes prompts found in `prompts.hjson`. +### Info Block +The `info` configuration block describes the theme itself. -TODO: More information about theming! +| Item | Required | Description | +|-------------|----------|----------------------------------------------------------| +| `name` | :+1: | Name of the theme. Be creative! | +| `author` | :+1: | Author of the theme/artwork. | +| `group` | :-1: | Group/affils of author. | +| `enabled` | :-1: | Boolean of enabled state. If set to `false`, this theme will not be available to your users. If a user currently has this theme selected, the system default will be selected for them at next login. | + +### Customization Block +The `customization` block in is itself broken up into major parts: + +| Item | Required | Description | +|-------------|----------|----------------------------------------------------------| +| `defaults` | :-1: | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. | +| `menus` | :-1: | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. | +| `prompts` | :-1: | Similar to `menus`, this file themes prompts found in `prompts.hjson`. | + +#### Defaults +| Item | Description | +|-------------|---------------------------------------------------| +| `passwordChar` | Character to display in password fields | +| `dateFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for dates. | +| `timeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for times. | +| `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. | + +Example: +```hjson +defaults: { + dateTimeFormat: { + short: MMM Do h:mm a + } +} +``` + +#### Menus Block +Each *key* in the `menus` block matches up with a key found in your `menu.hjson`. For example, consider a `matrix` menu defined in `menu.hjson`. In addition to perhaps providing a `MATRIX.ANS` in your themes directory, you can also theme other parts of the menu via a `matrix` entry in `theme.hjson`. + +Major areas to override/theme: +* `config`: Override and/or provide additional theme information over that found in the `menu.hjson`'s entry. Common entries here are for further overriding date/time formats, and custom range info formats (`InfoFormat`). +* `mci`: Set `height`, `width`, override `text`, `textStyle`/`focusTextStyle`, `itemFormat`/`focusItemFormat`, etc. + +Two main formats for `mci` are allowed: +* Verbose where a form ID(s) are supplied. +* Shorthand if only a single/first form is needed. + +Example: Verbose `mci` with form IDs: +```hjson +newUserFeedbackToSysOp: { + 0: { + mci: { + TL1: { width: 19, textOverflow: "..." } + ET2: { width: 19, textOverflow: "..." } + ET3: { width: 19, textOverflow: "..." } + } + } + 1: { + mci: { + MT1: { height: 14 } + } + } +} +``` + +Example: Shorthand `mci` format: +```hjson +matrix: { + mci: { + VM1: { + itemFormat: "|03{text}" + focusItemFormat: "|11{text!styleFirstLower}" + } + } +} +``` ## Creating Your Own From fbf07cda0cfab1a6ca1e4662ef68e969875ef1cd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 15 Nov 2018 21:38:19 -0700 Subject: [PATCH 330/569] Doc updates --- UPGRADE.md | 1 + docs/messageareas/bso-import-export.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 8cb98e13..359abec1 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -61,6 +61,7 @@ webSocket: { * The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup. * If you have art configured for message conference or area selection via the `art` configuration value, you will need to include a `show_art` menu reference. Defaulted to `changeMessageConfPreArt` for conferences and `changeMessageAreaPreArt` for areas & included in the example `menu.hjson`. * Config `defaults` section was theme related and as such, has been renamed to `theme`. `defaults.theme` is now `theme.default`, and `preLoginTheme` is now `theme.preLogin`. See `config.js` if this isn't clear as mud. +* Similar to the last item, `defaults.general.passwordChar` in `theme.hjson` is now just `defaults.passwordChar`. # 0.0.7-alpha to 0.0.8-alpha diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md index 88b36a25..ad189bbc 100644 --- a/docs/messageareas/bso-import-export.md +++ b/docs/messageareas/bso-import-export.md @@ -23,7 +23,7 @@ Schedules can be defined for importing and exporting via `import` and `export` u * `@immediate`: A message will be immediately exported if this trigger is defined in a schedule. Only used for `export`. * `@watch:/path/to/file`: This trigger watches the path specified for changes and will trigger an import or export when such events occur. Only used for `import`. - * Free form text can be things like `at 5:00 pm` or `every 2 hours`. + * Free form [Later style](https://bunkat.github.io/later/parsers.html#text) text — can be things like `at 5:00 pm` or `every 2 hours`. See [Later text parsing documentation](http://bunkat.github.io/later/parsers.html#text) for more information. @@ -45,14 +45,14 @@ See [Later text parsing documentation](http://bunkat.github.io/later/parsers.htm ## Nodes The `nodes` section defines how to export messages for one or more uplinks. -A node entry starts with a FTN style address (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain. +A node entry starts with a [FTN address](http://ftsc.org/docs/old/fsp-1028.001) (up to 5D) **as a key** in `config.hjson`. This key may contain wildcard(s) for net/zone/node/point/domain. | Config Item | Required | Description | |------------------|----------|---------------------------------------------------------------------------------| -| `packetType` | :-1: | `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability | -| `packetPassword` | :-1: | Password for the packet | -| `encoding` | :-1: | Encoding to use for message bodies; Defaults to `utf-8` | -| `archiveType` | :-1: | Specifies the archive type for ArcMail bundles. Must be a valid archiver name such as `zip` (See archiver configuration) | +| `packetType` | :-1: | `2`, `2.2`, or `2+`. Defaults to `2+` for modern mailer compatiability. | +| `packetPassword` | :-1: | Optional password for the packet | +| `encoding` | :-1: | Encoding to use for message bodies; Defaults to `utf-8`. | +| `archiveType` | :-1: | Specifies the archive type (by extension) for ArcMail bundles. This should be `zip` for most setups. Other valid examples include `arc`, `arj`, `lhz`, `pak`, `sqz`, or `zoo`. See docs on archiver configuration for more information. | **Example**: ```hjson @@ -60,9 +60,9 @@ A node entry starts with a FTN style address (up to 5D) **as a key** in `config. scannerTossers: { ftn_bso: { nodes: { - "21:*": { + "21:*": { // wildcard address packetType: 2+ - packetPassword: mypass + packetPassword: D@TP4SS encoding: cp437 archiveType: zip } From f4b03826b07e0380747779e7af79bd43cd9f498d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 15 Nov 2018 22:21:45 -0700 Subject: [PATCH 331/569] Fix --version --- core/oputil/oputil_main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/oputil/oputil_main.js b/core/oputil/oputil_main.js index d9492c0d..aafc8ef1 100644 --- a/core/oputil/oputil_main.js +++ b/core/oputil/oputil_main.js @@ -17,7 +17,7 @@ module.exports = function() { process.exitCode = ExitCodes.SUCCESS; if(true === argv.version) { - return console.info(require('../package.json').version); + return console.info(require('../../package.json').version); } if(0 === argv._.length || From f1f749499ff4a8c679e1b2b98a40cf3d3c25de59 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 16 Nov 2018 18:30:44 -0700 Subject: [PATCH 332/569] More doc updates --- docs/art/mci.md | 5 +++-- docs/art/themes.md | 24 +++++++++++++++++------- docs/modding/msg-area-list.md | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/art/mci.md b/docs/art/mci.md index f5876105..36ca4aa6 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -71,6 +71,7 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `SU` | Total uploads, system wide | | `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) | + Some additional special case codes also exist: | Code | Description | |--------|--------------| @@ -78,7 +79,7 @@ Some additional special case codes also exist: | `CB##` | Moves the cursor position back _##_ characters | | `CU##` | Moves the cursor position up _##_ characters | | `CD##` | Moves the cursor position down _##_ characters | -| `XY` | A special code that may be utilized for placement identification when creating menus or to extend an otherwise empty space in an art file down the screen. +| `XY` | A special code that may be utilized for placement identification when creating menus or to extend an otherwise empty space in an art file down the screen. | ## Views @@ -104,7 +105,7 @@ see additional information. ## Properties & Theming -Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. +Predefined MCI codes and other Views can have properties set via `menu.hjson` and further *themed* via `theme.hjson`. See [Themes](themes.md) for more information on this subject. ### Common Properties diff --git a/docs/art/themes.md b/docs/art/themes.md index c3ac38cf..f0093897 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -41,11 +41,11 @@ The `info` configuration block describes the theme itself. ### Customization Block The `customization` block in is itself broken up into major parts: -| Item | Required | Description | +| Item | Description | |-------------|----------|----------------------------------------------------------| -| `defaults` | :-1: | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. | -| `menus` | :-1: | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. | -| `prompts` | :-1: | Similar to `menus`, this file themes prompts found in `prompts.hjson`. | +| `defaults` | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. | +| `menus` | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. | +| `prompts` | Similar to `menus`, this file themes prompts found in `prompts.hjson`. | #### Defaults | Item | Description | @@ -68,10 +68,10 @@ defaults: { Each *key* in the `menus` block matches up with a key found in your `menu.hjson`. For example, consider a `matrix` menu defined in `menu.hjson`. In addition to perhaps providing a `MATRIX.ANS` in your themes directory, you can also theme other parts of the menu via a `matrix` entry in `theme.hjson`. Major areas to override/theme: -* `config`: Override and/or provide additional theme information over that found in the `menu.hjson`'s entry. Common entries here are for further overriding date/time formats, and custom range info formats (`InfoFormat`). -* `mci`: Set `height`, `width`, override `text`, `textStyle`/`focusTextStyle`, `itemFormat`/`focusItemFormat`, etc. +* `config`: Override and/or provide additional theme information over that found in the `menu.hjson`'s entry. Common entries here are for further overriding date/time formats, and custom range info formats (`InfoFormat`). See Entry Formatting in [MCI Codes](mci.md) and Custom Range Info Formatting below. +* `mci`: Set per-MCI code properties such as `height`, `width`, text styles, etc. See [MCI Codes](mci.md) for a more information. -Two main formats for `mci` are allowed: +Two formats for `mci` blocks are allowed: * Verbose where a form ID(s) are supplied. * Shorthand if only a single/first form is needed. @@ -105,6 +105,16 @@ matrix: { } ``` +##### Custom Range Info Formatting +Many modules support "custom range" MCI items. These are MCI codes that are left to the user to define using a format object specific to the module. For example, consider the `msg_area_list` module: This module sets MCI codes 10+ (`%TL10`, `%TL11`, etc.) as "custom range". When theming you can place these MCI codes in your artwork then define the format in `theme.hjson`: + +```hjson +messageAreaChangeCurrentArea: { + config: { + areaListInfoFormat10: "|15{name}|07: |03{desc}" + } +} +``` ## Creating Your Own :warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included `luciano_blocktronics' theme. Create your own and make changes to that instead: diff --git a/docs/modding/msg-area-list.md b/docs/modding/msg-area-list.md index e3f5cde1..499b7fa6 100644 --- a/docs/modding/msg-area-list.md +++ b/docs/modding/msg-area-list.md @@ -14,4 +14,4 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): The following additional MCIs are updated as the user changes selections in the main list: * MCI 2 (ie: `%TL2` or `%M%2`) is updated with the area description. -* MCI 10+ (ie `%TL10`...) are custom ranges updated with the same information available above in `itemFormat`. +* MCI 10+ (ie `%TL10`...) are custom ranges updated with the same information available above in `itemFormat`. Use `areaListItemFormat##`. From d83201248ac328252b2f5b57a41e89870f9c8997 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 16 Nov 2018 18:42:04 -0700 Subject: [PATCH 333/569] Fix tables hopefully --- docs/art/mci.md | 2 +- docs/art/themes.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/art/mci.md b/docs/art/mci.md index 36ca4aa6..bc6bab5a 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -71,8 +71,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `SU` | Total uploads, system wide | | `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) | - Some additional special case codes also exist: + | Code | Description | |--------|--------------| | `CF##` | Moves the cursor position forward _##_ characters | diff --git a/docs/art/themes.md b/docs/art/themes.md index f0093897..713e2d72 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -42,7 +42,7 @@ The `info` configuration block describes the theme itself. The `customization` block in is itself broken up into major parts: | Item | Description | -|-------------|----------|----------------------------------------------------------| +|-------------|---------------------------------------------------| | `defaults` | Default values to use when this theme is active. These values override system defaults, but can still be overridden themselves in specific areas of your theme. | | `menus` | The bulk of what you theme in the system will be here. Any menu (that is, anything you find in `menu.hjson`) can be tweaked. | | `prompts` | Similar to `menus`, this file themes prompts found in `prompts.hjson`. | From cf64be3550725150f9c2bf105ba0ec72b28062b6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 01:21:11 -0700 Subject: [PATCH 334/569] docs --- docs/art/themes.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/art/themes.md b/docs/art/themes.md index 713e2d72..9bc0eb9e 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -18,7 +18,7 @@ Of course one of the most basic elements of BBS theming is art. ENiGMA½ uses a TL;DR: In general, to theme a piece of art, create a version of it in your themes directory. -:information: Remember that by default, the system will allow for randomly selecting art (in one of the directories mentioned above) by numbering it: `FOO1.ANS`, `FOO2.ANS`, etc.! +:information_source: Remember that by default, the system will allow for randomly selecting art (in one of the directories mentioned above) by numbering it: `FOO1.ANS`, `FOO2.ANS`, etc.! ## Theme Sections Themes are some important sections to be aware of: @@ -50,7 +50,7 @@ The `customization` block in is itself broken up into major parts: #### Defaults | Item | Description | |-------------|---------------------------------------------------| -| `passwordChar` | Character to display in password fields | +| `passwordChar` | Character to display in password fields. Defaults to `*` | | `dateFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for dates. | | `timeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for times. | | `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. | @@ -117,16 +117,16 @@ messageAreaChangeCurrentArea: { ``` ## Creating Your Own -:warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included `luciano_blocktronics' theme. Create your own and make changes to that instead: +:warning: ***IMPORTANT!*** It is recommended you don't make any customisations to the included `luciano_blocktronics' theme. Instead, create your own and make changes to that instead: 1. Copy `/art/themes/luciano_blocktronics` to `art/themes/your_board_theme` 2. Update the `info` block at the top of the theme.hjson file: ``` hjson info: { - name: Awesome Theme - author: Cool Artist - group: Sick Group - enabled: true + name: Awesome Theme + author: Cool Artist + group: Sick Group + enabled: true // default } ``` From 0891fffc67da2a7ddb97f694f6132184280d7174 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 01:52:17 -0700 Subject: [PATCH 335/569] Docs --- docs/art/general.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/art/general.md b/docs/art/general.md index f08994e8..fa6d623a 100644 --- a/docs/art/general.md +++ b/docs/art/general.md @@ -2,4 +2,5 @@ layout: page title: General --- -General art lives in the `art/general` directory. 'General' art is ANSI you want to stay consistent across themes, such as a welcome ANSI or a rotation of logoff ANSIs. +## General Art +General art lives in the `art/general` directory. This is art (ANSI, ASCII, ...) you want to stay consistent across themes, such as a login croller or a set of logoff screens to pick randomly. From b4b20e4972d6c11aafe80d52363a6986bc106cdc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 11:44:27 -0700 Subject: [PATCH 336/569] Lots of doc updates around art --- docs/art/general.md | 111 ++++++++++++++++++++++++++++++- docs/art/themes.md | 9 +-- docs/configuration/menu-hjson.md | 4 +- 3 files changed, 111 insertions(+), 13 deletions(-) diff --git a/docs/art/general.md b/docs/art/general.md index fa6d623a..a0620de3 100644 --- a/docs/art/general.md +++ b/docs/art/general.md @@ -1,6 +1,111 @@ --- layout: page -title: General +title: General Art Information --- -## General Art -General art lives in the `art/general` directory. This is art (ANSI, ASCII, ...) you want to stay consistent across themes, such as a login croller or a set of logoff screens to pick randomly. +## General Art Information +One of the most basic elements of BBS customization is through it's artwork. ENiGMA½ supports a variety of ways to select, display, and manage art. + +As a general rule, art files live in one of two places: + +1. The `art/general` directory. This is where you place command non-themed art files. +2. Within a theme such as `art/themes/super_fancy_theme`. + +### Menu Entries +While art can be displayed programmatically such as from a custom module, the most basic and common form is via `menu.hjson` entries. This usually falls into one of two forms: a "standard" entry where a single `art` spec is utilized or a entry for a custom module where multiple pieces are declared and used. The second style usually takes the form of a `config.art` block with two or more entries. + +A menu entry has a few elements that control how art is choosen and displayed. First, the `art` *spec* tells teh system how to look for the art asset. Second, the `config` block can further control aspecs of lookup and display: + +| Item | Description| +|------|------------| +| `font` | Sets the [SyncTERM](http://syncterm.bbsdev.net/) style font to use when displaying this art. If unset, the system will use the art's embedded [SAUCE](http://www.acid.org/info/sauce/sauce.htm) record if present or simply use the current font. See Fonts below. | +| `pause` | If set to `true`, pause after displaying. | +| `baudRate` | Set a [SyncTERM](http://syncterm.bbsdev.net/) style emulated baud rate when displaying this art. In other words, slow down the display. | +| `cls` | Clear the screen before display if set to `true`. | +| `random` | Set to `false` to explicitly disable random lookup. | +| `types` | An optional array of types (aka file extensions) to consider for lookup. For example : `[ '.ans', '.asc' ]` | +| `readSauce` | May be set to `false` if you need to explictly disable SAUCE support. | + +#### Art Spec +It was mentioned that the `art` member is a *spec*. The value of a `art` member controls how the system looks for an asset. The following forms are supported: + +* `FOO`: The system will look for `FOO.ANS`, `FOO.ASC`, `FOO.TXT`, etc. using the default search path. Unless otherwise specified if `FOO1.ANS`, `FOO2.ANS`, and so on exist, a random selection will be made. +* `FOO.ANS`: By specifying an extension, only that type will be searched for. +* `rel/path/to/BAR.ANS`: Only match a path (relative to the system's `art` directory). +* `/path/to/BAZ.ANS`: Exact path only. + +ENiGMA½ uses a fallback system for art selection. When a menu entry calls for a piece of art, the following search is made: + +1. If a direct or relative path is supplied, look there first. +2. In the users current theme directory. +3. In the system default theme directory. +4. In the `art/general` directory. + +#### SyncTERM Style Fonts +ENiGMA½ can set a [SyncTERM](http://syncterm.bbsdev.net/) style font for art display. This is supported by many popular BBS terminals besides just SyncTERM and is common for displaying Amiga style fonts for example. The system will use the `font` specifier or look for a font declared in an artworks SAUCE record (unless `readSauce` is `false`). + +The most common fonts are probably as follows: + +* `cp437` +* `c64_upper` +* `c64_lower` +* `c128_upper` +* `c128_lower` +* `atari` +* `pot_noodle` +* `mo_soul` +* `microknight_plus` +* `topaz_plus` +* `microknight` +* `topaz` + +Other fonts fonts also available: +* `cp1251` +* `koi8_r` +* `iso8859_2` +* `iso8859_4` +* `cp866` +* `iso8859_9` +* `haik8` +* `iso8859_8` +* `koi8_u` +* `iso8859_15` +* `iso8859_4` +* `koi8_r_b` +* `iso8859_4` +* `iso8859_5` +* `ARMSCII_8` +* `iso8859_15` +* `cp850` +* `cp850` +* `cp885` +* `cp1251` +* `iso8859_7` +* `koi8-r_c` +* `iso8859_4` +* `iso8859_1` +* `cp866` +* `cp437` +* `cp866` +* `cp885` +* `cp866_u` +* `iso8859_1` +* `cp1131` + +See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. + +#### SyncTERM Style Baud Rates +The `baudRate` member can set a [SyncTERM](http://syncterm.bbsdev.net/) style emulated baud rate. May be `300`, `600`, `1200`, `2400`, `4800`, `9600`, `19200`, `38400`, `57600`, `76800`, or `115200`. A value of `ulimited`, `off`, or `0` resets (disables) the rate. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. + +## Common Example +```hjson +fullLogoffSequenceRandomBoardAd: { + art: OTHRBBS + desc: Logging Off + next: logoff + config: { + baudRate: 57600 + pause: true + cls: true + } +} +``` \ No newline at end of file diff --git a/docs/art/themes.md b/docs/art/themes.md index 9bc0eb9e..577a95fe 100644 --- a/docs/art/themes.md +++ b/docs/art/themes.md @@ -9,14 +9,7 @@ ENiGMA½ comes with an advanced theming system allowing system operators to high Themes live in `art/themes/`. Each theme (and thus it's *theme ID*) is a directory within the `themes` directory. The theme itself is simply a collection of art files, and a `theme.hjson` file that further defines layout, colors & formatting, etc. ENiGMA½ comes with a default theme by [Luciano Ayres](http://blocktronics.org/tag/luciano-ayres/) of [Blocktronics](http://blocktronics.org/) called Mystery Skull. This theme is in `art/themes/luciano_blocktronics`, and thus it's *theme ID* is `luciano_blocktronics`. ## Art -Of course one of the most basic elements of BBS theming is art. ENiGMA½ uses a fallback system for art selection by default (you may override this in a `menu.hjson` entry if desired). When a menu entry calls for a piece of art, the following search is made: - -1. If a direct or relative path is supplied, look there first. -2. In the users current theme directory. -3. In the system default theme directory. -4. In the `art/general` directory. - -TL;DR: In general, to theme a piece of art, create a version of it in your themes directory. +For information on art files, see [General Art Information](general.md). TL;DR: In general, to theme a piece of art, create a version of it in your themes directory. :information_source: Remember that by default, the system will allow for randomly selecting art (in one of the directories mentioned above) by numbering it: `FOO1.ANS`, `FOO2.ANS`, etc.! diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 7d1bbed4..336c9878 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -33,8 +33,8 @@ Menu entries live under the `menus` section of `menu.hjson`. The *key* for a men * `cls`: If `true` the screen will be cleared before showing this menu. * `pause`: If `true` a pause will occur after showing this menu. Useful for simple menus such as displaying art or status screens. * `nextTimeout`: Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. - * `baudRate`: Sets the SyncTERM style emulated baud rate. May be `300`, `600`, `1200`, `2400`, `4800`, `9600`, `19200`, `38400`, `57600`, `76800`, or `115200`. A value of `ulimited`, `off`, or `0` resets (disables) the rate. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. - * `font`: Sets the SyncTERM style font. May be one of the following: `cp437`, `cp1251`, `koi8_r`, `iso8859_2`, `iso8859_4`, `cp866`, `iso8859_9`, `haik8`, `iso8859_8`, `koi8_u`, `iso8859_15`, `iso8859_4`, `koi8_r_b`, `iso8859_4`, `iso8859_5`, `ARMSCII_8`, `iso8859_15`, `cp850`, `cp850`, `cp885`, `cp1251`, `iso8859_7`, `koi8-r_c`, `iso8859_4`, `iso8859_1`, `cp866`, `cp437`, `cp866`, `cp885`, `cp866_u`, `iso8859_1`, `cp1131`, `c64_upper`, `c64_lower`, `c128_upper`, `c128_lower`, `atari`, `pot_noodle`, `mo_soul`, `microknight_plus`, `topaz_plus`, `microknight`, `topaz`. See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. + * `baudRate`: See baud rate information in [General Art Information](docs/art/general.md). + * `font`: See font listing in [General Art Information](docs/art/general.md). ## Forms TODO From 8702e309aed34b4ca72f173b634244da1233c255 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 13:14:51 -0700 Subject: [PATCH 337/569] Fix bug in default config / user config merging: Some arrays should be replaced while others should be merged --- core/config.js | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/core/config.js b/core/config.js index 97091be5..3799d538 100644 --- a/core/config.js +++ b/core/config.js @@ -42,17 +42,53 @@ function hasMessageConferenceAndArea(config) { return result; } +const ArrayReplaceKeyPaths = [ + 'loginServers.ssh.algorithms.kex', + 'loginServers.ssh.algorithms.cipher', + 'loginServers.ssh.algorithms.hmac', + 'loginServers.ssh.algorithms.compress', +]; + +const ArrayReplaceKeys = [ + 'args', + 'sendArgs', 'recvArgs', 'recvArgsNonBatch', +]; + function mergeValidateAndFinalize(config, cb) { + const defaultConfig = getDefaultConfig(); + + const arrayReplaceKeyPathsMutable = _.clone(ArrayReplaceKeyPaths); + const shouldReplaceArray = (arr, key) => { + if(ArrayReplaceKeys.includes(key)) { + return true; + } + for(let i = 0; i < arrayReplaceKeyPathsMutable.length; ++i) { + const o = _.get(defaultConfig, arrayReplaceKeyPathsMutable[i]); + if(_.isEqual(o, arr)) { + arrayReplaceKeyPathsMutable.splice(i, 1); + return true; + } + } + return false; + }; + async.waterfall( [ function mergeWithDefaultConfig(callback) { const mergedConfig = _.mergeWith( - getDefaultConfig(), - config, (conf1, conf2) => { - // Arrays should always concat - if(_.isArray(conf1)) { - // :TODO: look for collisions & override dupes - return conf1.concat(conf2); + defaultConfig, + config, + (defConfig, userConfig, key) => { + if(Array.isArray(defConfig) && Array.isArray(userConfig)) { + // + // Arrays are special: Some we merge, while others + // we simply replace. + // + if(shouldReplaceArray(defConfig, key)) { + return userConfig; + } else { + return _.uniq(defConfig.concat(userConfig)); + } } } ); From ac0f54dc9ba9f1b9eb035b7a40d9f5be3b4babc2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 13:24:16 -0700 Subject: [PATCH 338/569] Better MCI handling --- core/color_codes.js | 3 ++- core/predefined_mci.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index 2032f057..e07b805e 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -110,7 +110,8 @@ function renegadeToAnsi(s, client) { result += s.substr(lastIndex, m.index - lastIndex) + attr; } else if(m[4] || m[1]) { // |AA MCI code or |Cx## movement where ## is in m[1] - const val = getPredefinedMCIValue(client, m[4] || m[1], m[2]) || (m[0]); // value itself or literal + let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]); + val = _.isString(val) ? val : m[0]; // value itself or literal result += s.substr(lastIndex, m.index - lastIndex) + val; } else if(m[5]) { // || -- literal '|', that is. diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 9888ba5e..5a04ff07 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -95,7 +95,7 @@ const PREDEFINED_MCI_GENERATORS = { ST : function serverName(client) { return client.session.serverName; }, FN : function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); - return activeFilter ? activeFilter.name : ''; + return activeFilter ? activeFilter.name : '(Unknown)'; }, DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes From 36e1456b47510d97109a9205dea4b625f5e79c48 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 18:03:54 -0700 Subject: [PATCH 339/569] More doc updates of course --- docs/configuration/menu-hjson.md | 127 +++++++++++++++++-------------- 1 file changed, 68 insertions(+), 59 deletions(-) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 336c9878..34114573 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -2,19 +2,10 @@ layout: page title: menu.hjson --- -:warning: ***IMPORTANT!*** Before making any customisations, create your own copy of `/config/menu.hjson`, and specify it in the `general` section of `config.hjson`: +## Menu HJSON +The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/yourboardname-menu.hjson` or similar. This file determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. -````hjson -general: { - menuFile: yourboardname.hjson -} -```` -This document and others will refer to `menu.hjson`. This should be seen as an alias to `yourboardname.hjson` - -## The Basics -Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. - -Entries in `menu.hjson` are objects or _sections_ defining a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: +Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: * Classical Main, Messages, and File menus * Art file display @@ -23,21 +14,38 @@ Entries in `menu.hjson` are objects or _sections_ defining a menu. A menu in thi Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system. ## Common Menu Entry Members -* `desc`: A friendly description that can be found in places such as "Who's Online" or the `%MD` MCI code. -* `art`: An art file specification. -* `next`: Specifies the next menu to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. -* `prompt`: Specifies a prompt, by name, to use along with this menu. -* `form`: Defines one or more forms available on this menu. -* `submit`: Defines a submit handler when using `prompt`. -* `config`: May contain any of the following standard configuration members in addition to per-module defined types (see appropriate module for more information): - * `cls`: If `true` the screen will be cleared before showing this menu. - * `pause`: If `true` a pause will occur after showing this menu. Useful for simple menus such as displaying art or status screens. - * `nextTimeout`: Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. - * `baudRate`: See baud rate information in [General Art Information](docs/art/general.md). - * `font`: See font listing in [General Art Information](docs/art/general.md). +Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. See documentation for the module in question for particulars. + +| Item | Description | +|--------|--------------| +| `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | +| `art`: | An art file *spec*. See [General Art Information](docs/art/general.md). | +| `next`: Specifies the next menu entry to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | +| `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. | +| `submit` | Defines a submit handler when using `prompt`. +| `form` | An object defining one or more *forms* available on this menu. | +| `module` | Sets the module name to use for this menu. | +| `config` | An object containing additional configuration. See **Config Block** below. | + +### Config Block +The `config` block for a menu entry can contain common members as well as a per-module (when `module` is used) settings. + +| Item | Description | +|------|-------------| +| `cls` | If `true` the screen will be cleared before showing this menu. | +| `pause` | If `true` a pause will occur after showing this menu. Useful for simple menus such as displaying art or status screens. | +| `nextTimeout` | Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. | +| `baudRate` | See baud rate information in [General Art Information](docs/art/general.md). | +| `font` | Sets a SyncTERM style font to use when displaying this menus `art`. See font listing in [General Art Information](docs/art/general.md). | + + ## Forms -TODO +ENiGMA½ uses a concept of *forms* in menus. A form is a collection of associated *views*. Consider a New User Application using the `nua` module: The default implementation utilizes a single form with multiple EditTextView views, a submit button, etc. Forms are identified by number starting with `0`. A given menu may have mutiple forms (often associated with different states or screens within the menu). + +Menus may also support more than one layout type by using a *MCI key*. A MCI key is a alpha-numerically sorted key made from 1:n MCI codes. This lets the system choose the appropriate set of form(s) based on theme or random art. An example of this may be a matrix menu: Perhaps one style of your matrix uses a vertical light bar (`VM` key) while another uses a horizontal (`HM` key). The system can discover the correct form to use by matching MCI codes found in the art to that of the available forms defined in `menu.hjson`. + +For more information on views and associated MCI codes, see [MCI Codes](docs/art/mci.md). ## Submit Handlers TODO @@ -49,17 +57,14 @@ Let's look a couple basic menu entries: telnetConnected: { art: CONNECT next: matrix - options: { nextTimeout: 1500 } + config: { nextTimeout: 1500 } } ``` -The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). - -An art pattern of `CONNECT` is set telling the system to look for `CONNECT.*` where `` represents a optional integer in art files to cause randomness, e.g. `CONNECT1.ANS`, `CONNECT2.ANS`, and so on. If desired, you can also be explicit by supplying a full filename with an extention such as `CONNECT.ANS`. - -The entry `next` sets up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`. - -Finally, an `options` object may contain various common options for menus. In this case, `nextTimeout` tells the system to proceed to the `next` entry automatically after 1500ms. +The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). The entry sets up a few things: +* A `art` spec of `CONNECT`. (See [General Art Information](docs/art/general.md)). +* A `next` entry up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`. +* An `config` block containing a single `nextTimeout` field telling the system to proceed to the `next` (`matrix`) entry automatically after 1500ms. Now let's look at `matrix`, the `next` entry from `telnetConnected`: @@ -68,34 +73,38 @@ matrix: { art: matrix desc: Login Matrix form: { - 0: { - VM: { - mci: { - VM1: { - submit: true - focus: true - items: [ "login", "apply", "log off" ] - argName: matrixSubmit + 0: { + // + // Here we have a MCI key of "VM". In this case we could + // omit this level since no other keys are present. + // + VM: { + mci: { + VM1: { + submit: true + focus: true + items: [ "login", "apply", "log off" ] + argName: matrixSubmit + } + } + submit: { + *: [ + { + value: { matrixSubmit: 0 } + action: @menu:login + } + { + value: { matrixSubmit: 1 }, + action: @menu:newUserApplication + } + { + value: { matrixSubmit: 2 }, + action: @menu:logoff + } + ] + } } } - submit: { - *: [ - { - value: { matrixSubmit: 0 } - action: @menu:login - } - { - value: { matrixSubmit: 1 }, - action: @menu:newUserApplication - } - { - value: { matrixSubmit: 2 }, - action: @menu:logoff - } - ] - } - } - } } } ``` From 7f55dae27f7d1ad2c73d8c5c16073d55a6645aa7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 18:04:45 -0700 Subject: [PATCH 340/569] Fix typos --- docs/configuration/menu-hjson.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 34114573..3fb6a7dd 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -19,8 +19,8 @@ Below is a table of **common** menu entry members. These members apply to most e | Item | Description | |--------|--------------| | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | -| `art`: | An art file *spec*. See [General Art Information](docs/art/general.md). | -| `next`: Specifies the next menu entry to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | +| `art` | An art file *spec*. See [General Art Information](docs/art/general.md). | +| `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | | `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. | | `submit` | Defines a submit handler when using `prompt`. | `form` | An object defining one or more *forms* available on this menu. | From 0f35b0c58e2aaa34af1efff3ceb04e52b0d9934b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 18:05:42 -0700 Subject: [PATCH 341/569] Fix paths? --- docs/configuration/menu-hjson.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 3fb6a7dd..4f932696 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -35,8 +35,8 @@ The `config` block for a menu entry can contain common members as well as a per- | `cls` | If `true` the screen will be cleared before showing this menu. | | `pause` | If `true` a pause will occur after showing this menu. Useful for simple menus such as displaying art or status screens. | | `nextTimeout` | Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. | -| `baudRate` | See baud rate information in [General Art Information](docs/art/general.md). | -| `font` | Sets a SyncTERM style font to use when displaying this menus `art`. See font listing in [General Art Information](docs/art/general.md). | +| `baudRate` | See baud rate information in [General Art Information](/docs/art/general.md). | +| `font` | Sets a SyncTERM style font to use when displaying this menus `art`. See font listing in [General Art Information](/docs/art/general.md). | @@ -45,7 +45,7 @@ ENiGMA½ uses a concept of *forms* in menus. A form is a collection of associate Menus may also support more than one layout type by using a *MCI key*. A MCI key is a alpha-numerically sorted key made from 1:n MCI codes. This lets the system choose the appropriate set of form(s) based on theme or random art. An example of this may be a matrix menu: Perhaps one style of your matrix uses a vertical light bar (`VM` key) while another uses a horizontal (`HM` key). The system can discover the correct form to use by matching MCI codes found in the art to that of the available forms defined in `menu.hjson`. -For more information on views and associated MCI codes, see [MCI Codes](docs/art/mci.md). +For more information on views and associated MCI codes, see [MCI Codes](/docs/art/mci.md). ## Submit Handlers TODO @@ -62,7 +62,7 @@ telnetConnected: { ``` The above entry `telnetConnected` is set as the Telnet server's first menu entry (set by `firstMenu` in the Telnet server's config). The entry sets up a few things: -* A `art` spec of `CONNECT`. (See [General Art Information](docs/art/general.md)). +* A `art` spec of `CONNECT`. (See [General Art Information](/docs/art/general.md)). * A `next` entry up the next menu, by name, in the stack (`matrix`) that we'll go to after `telnetConnected`. * An `config` block containing a single `nextTimeout` field telling the system to proceed to the `next` (`matrix`) entry automatically after 1500ms. From b3ec97cc5cc635a73c78138bc83b56783e70297f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 18:56:09 -0700 Subject: [PATCH 342/569] Art asset ACS conditional cleanup: Make more general purpose --- core/fse.js | 4 ++-- core/menu_module.js | 7 ++++++- core/theme.js | 5 +++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/fse.js b/core/fse.js index fcf27ad4..98065b58 100644 --- a/core/fse.js +++ b/core/fse.js @@ -542,7 +542,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul theme.displayThemedAsset( art[n], self.client, - { font : self.menuConfig.font, acsCondMember : 'art' }, + { font : self.menuConfig.font }, function displayed(err) { next(err); } @@ -622,7 +622,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul theme.displayThemedAsset( art[n], self.client, - { font : self.menuConfig.font, acsCondMember : 'art' }, + { font : self.menuConfig.font }, function displayed(err, artData) { if(artData) { mciData[n] = artData; diff --git a/core/menu_module.js b/core/menu_module.js index ee1502cb..0ce2a2a6 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -49,13 +49,18 @@ exports.MenuModule = class MenuModule extends PluginModule { const mciData = {}; let pausePosition; + const hasArt = () => { + return _.isString(self.menuConfig.art) || + (Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs')); + }; + async.series( [ function beforeDisplayArt(callback) { self.beforeArt(callback); }, function displayMenuArt(callback) { - if(!_.isString(self.menuConfig.art)) { + if(!hasArt()) { return callback(null); } diff --git a/core/theme.js b/core/theme.js index 6414da62..052eeac7 100644 --- a/core/theme.js +++ b/core/theme.js @@ -682,8 +682,9 @@ function displayThemedAsset(assetSpec, client, options, cb) { options = {}; } - if(Array.isArray(assetSpec) && _.isString(options.acsCondMember)) { - assetSpec = client.acs.getConditionalValue(assetSpec, options.acsCondMember); + if(Array.isArray(assetSpec)) { + const acsCondMember = options.acsCondMember || 'art'; + assetSpec = client.acs.getConditionalValue(assetSpec, acsCondMember); } const artAsset = asset.getArtAsset(assetSpec); From bf43766355f142a9e465b27e975e41122482e845 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 18:56:36 -0700 Subject: [PATCH 343/569] More doc updates! --- docs/configuration/acs.md | 3 ++- docs/configuration/menu-hjson.md | 44 +++++++++++++++++++++++--------- docs/filebase/acs.md | 4 +-- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index eeab8e9c..dde55709 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -61,6 +61,7 @@ The following touch points exist in the system. Many more are planned: * Message conferences and areas * File base areas -* Menus within `menu.hjson`. See [menu.hjson](menu-hjson.md). +* Menus within `menu.hjson`. See [Menu HJSON](menu-hjson.md). + See the specific areas documentation for information on available ACS checks. diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 4f932696..87cee85e 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -19,7 +19,7 @@ Below is a table of **common** menu entry members. These members apply to most e | Item | Description | |--------|--------------| | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | -| `art` | An art file *spec*. See [General Art Information](docs/art/general.md). | +| `art` | An art file *spec*. See [General Art Information](/docs/art/general.md). | | `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | | `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. | | `submit` | Defines a submit handler when using `prompt`. @@ -70,7 +70,7 @@ Now let's look at `matrix`, the `next` entry from `telnetConnected`: ```hjson matrix: { - art: matrix + art: MATRIX desc: Login Matrix form: { 0: { @@ -104,21 +104,22 @@ matrix: { ] } } + + // + // If we wanted, we could declare a "HM" MCI key block here. + // This would allow a horizontal matrix style when the matrix art + // loaded contained a %HM code. + // } } } ``` -In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form -by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` -(*VerticalMenuView*) MCI entry. `VM1` is then setup to `submit` and start focused via `focus: true` -as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` for this -action as `matrixSubmit`. +In the above entry, you'll notice `form`. This defines a form(s) object. In this case, a single form by ID of `0`. The system is then told to use a block only when the resulting art provides a `VM` (*VerticalMenuView*) MCI entry. Some other bits about the form: -The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). - Upon submit, the first match will be executed. For example, if the user selects "login", the first entry - with a value of `{ matrixSubmit: 0 }` will match causing `action` of `@menu:login` to be executed (go - to `login` menu). +* `VM1` is then setup to `submit` and start focused via `focus: true` as well as have some menu entries ("login", "apply", ...) defined. We provide an `argName` of `matrixSubmit` for this element view. +* The `submit` object tells the system to attempt to apply provided match entries from any view ID (`*`). +* Upon submit, the first match will be executed. For example, if the user selects "login", the first entry with a value of `{ matrixSubmit: 0 }` will match (due to 0 being the first index in the list and `matrixSubmit` being the arg name in question) causing `action` of `@menu:login` to be executed (go to `login` menu). ## ACS Checks Menu modules can check user ACS in order to restrict areas and perform flow control. See [ACS](acs.md) for available ACS syntax. @@ -150,4 +151,23 @@ login: { } ] } -``` \ No newline at end of file +``` + +### Art Asset Selection +Another area in which you can apply ACS in a menu is art asset specs. + +```hjson +someMenu: { + desc: Neato Dorito + art: [ + { + acs: GM[couriers] + art: COURIERINFO + } + { + // show ie: EVERYONEELSE.ANS to everyone else + art: EVERYONEELSE + } + ] +} +``` diff --git a/docs/filebase/acs.md b/docs/filebase/acs.md index 50527928..63884c71 100644 --- a/docs/filebase/acs.md +++ b/docs/filebase/acs.md @@ -2,8 +2,8 @@ layout: page title: ACS --- - -If no `acs` block is supplied in a file area definition, the following defaults apply to an area: +## File Base ACS +[ACS Codes](/docs/configuration/acs.md) may be used to control acess to File Base areas by specifying an `acs` string in a file area's definition. If no `acs` is supplied in a file area definition, the following defaults apply to an area: * `read` (list, download, etc.): `GM[users]` * `write` (upload): `GM[sysops]` From 2a95849f7de4ecf54978dff26ab3a45d01022d91 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 21:04:00 -0700 Subject: [PATCH 344/569] Add default keys for show art by message/file conf/areas --- core/show_art.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/show_art.js b/core/show_art.js index 30b6e56e..a480fa05 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -68,7 +68,7 @@ exports.getModule = class ShowArtModule extends MenuModule { } showByExtraArgs(cb) { - this.getArtKeyValue( (err, artSpec) => { + this.getArtKeyValue(this.config.key, (err, artSpec) => { if(err) { return cb(err); } @@ -89,7 +89,7 @@ exports.getModule = class ShowArtModule extends MenuModule { } showByFileBaseArea(cb) { - this.getArtKeyValue( (err, key) => { + this.getArtKeyValue('areaTag', (err, key) => { if(err) { return cb(err); } @@ -98,7 +98,7 @@ exports.getModule = class ShowArtModule extends MenuModule { } showByMessageConf(cb) { - this.getArtKeyValue( (err, key) => { + this.getArtKeyValue('confTag', (err, key) => { if(err) { return cb(err); } @@ -107,7 +107,7 @@ exports.getModule = class ShowArtModule extends MenuModule { } showByMessageArea(cb) { - this.getArtKeyValue( (err, key) => { + this.getArtKeyValue('areaTag', (err, key) => { if(err) { return cb(err); } @@ -133,8 +133,8 @@ exports.getModule = class ShowArtModule extends MenuModule { return this.displaySingleArtWithOptions(artSpec, options, cb); } - getArtKeyValue(cb) { - const key = this.config.key; + getArtKeyValue(defaultKey, cb) { + const key = this.config.key || defaultKey; if(!_.isString(key)) { return cb(Errors.MissingConfig('Config option "key" is required for method "extraArgs"')); } From 780e2231d349c0b1b412219c1137814a32b99815 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 21:04:16 -0700 Subject: [PATCH 345/569] Add menuFlags docs --- docs/configuration/menu-hjson.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 87cee85e..1956252c 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -37,7 +37,16 @@ The `config` block for a menu entry can contain common members as well as a per- | `nextTimeout` | Sets the number of **milliseconds** before the system will automatically advanced to the `next` menu. | | `baudRate` | See baud rate information in [General Art Information](/docs/art/general.md). | | `font` | Sets a SyncTERM style font to use when displaying this menus `art`. See font listing in [General Art Information](/docs/art/general.md). | +| `menuFlags` | An array of menu flag(s) controlling menu behavior. See **Menu Flags** below. +#### Menu Flags +The `menuFlags` field of a `config` block can change default behavior of a particular menu. + +| Flag | Description | +|------|-------------| +| `noHistory` | Prevents the menu from remaining in the menu stack / history. When this flag is set, when the **next** menu falls back, this menu will be skipped and the previous menu again displayed instead. Example: menuA -> menuB(noHistory) -> menuC: Exiting menuC returns the user to menuA. | +| `popParent` | When *this* menu is exited, fall back beyond the parent as well. Often used in combination with `noHistory`. | +| `forwardArgs` | If set, when the next menu is entered, forward any `extraArgs` arguments to *this* menu on to it. | ## Forms From 6a01c05ec9f75a8aece6e964c92d22b470b69214 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 17 Nov 2018 21:04:44 -0700 Subject: [PATCH 346/569] Show art docs --- docs/_includes/nav.md | 1 + docs/modding/show-art.md | 70 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 docs/modding/show-art.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index c8cb67c9..4c2fc0f6 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -73,6 +73,7 @@ - [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %}) - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) - [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %}) + - [Show Art]({{ site.baseurl }}{% link modding/show-art.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/show-art.md b/docs/modding/show-art.md new file mode 100644 index 00000000..c00d7009 --- /dev/null +++ b/docs/modding/show-art.md @@ -0,0 +1,70 @@ +--- +layout: page +title: User List +--- +## The Show Art Module +The built in `show_art` module add some advanced ways in which you can configure your system to display art assets beyond what a standard menu entry can provide. For example, based on user selection of a file or message base area. + +## Configuration +### Config Block +Available `config` block entries: +* `method`: Set the method in which to show art. See **Methods** below. +* `optional`: Is this art required or optional? If non-optional and we cannot show art based on `method`, it is an error. +* `key`: Used for some `method`s. See **Methods** + +### Methods +#### Extra Args +When `method` is `extraArgs`, the module selects an *art spec* from a value found within `extraArgs` that were passed to `show_art` by `key`. Consider the following: + +Given an `menu.hjson` entry: +```hjson +showWithExtraArgs: { + module: show_art + config: { + method: extraArgs + key: fooBaz + } +} +``` +If the `showWithExtraArgs` menu was entered and passed `extraArgs` as the following: +```json +{ + fizzBang : true, + fooBaz : "LOLART" +} +``` + +...then the system would use the *art spec* of `LOLART`. + +#### Area & Conferences +Handy for inserting into File Base, Message Conferences, or Mesage Area selections selections. When `method` is `fileBaseArea`, `messageConf`, or `messageArea` the selected conf/area's associated *art spec* is utilized. Example: + +Given a file base entry in `config.hjson`: +```hjson +areas: { + all_ur_base: { + name: All Your Base + desc: chown -r us ./base + art: ALLBASE + } +} +``` + +A menu entry may look like this: +```hjson +showFileBaseAreaArt: { + module: show_art + config: { + method: fileBaseArea + cls: true + pause: true + menuFlags: [ "popParent", "noHistory" ] + } +} +``` + +...if the user choose the "All Your Base" area, the *art spec* of `ALLBASE` would be selected and displayed. + +The only difference for `messageConf` or `messageArea` methods are where the art is defined (which is always next to the conf or area declaration in `config.hjson`). + +While `key` can be overridden, the system uses `areaTag` for message/file area selections, and `confTag` for conference selections by default. From 05804206ee0a729aa4d54c0219ec90cecdf4f93d Mon Sep 17 00:00:00 2001 From: SemperFu Date: Sun, 18 Nov 2018 01:40:08 -0500 Subject: [PATCH 347/569] Windows logging hint --- misc/config_template.in.hjson | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 3acb0823..48a4a8c4 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -66,7 +66,8 @@ // See https://github.com/trentm/node-bunyan#streams // // Remember you can pipe logs through Bunyan to pretty-print: - // tail -F ./logs/enigma-bbs.log | bunyan + // Linux : tail -F ./logs/enigma-bbs.log | bunyan + // Windows : "Get-Content .\enigma-bbs.log | bunyan.cmd" // // (npm install -g bunyan to get the binary) // @@ -384,4 +385,4 @@ loginHistoryMax: -1 } } -} \ No newline at end of file +} From f567ef645218e922308ca13ace107e16027906a1 Mon Sep 17 00:00:00 2001 From: SemperFu Date: Sun, 18 Nov 2018 01:41:06 -0500 Subject: [PATCH 348/569] Update config_template.in.hjson --- misc/config_template.in.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 48a4a8c4..9facd9e3 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -67,7 +67,7 @@ // // Remember you can pipe logs through Bunyan to pretty-print: // Linux : tail -F ./logs/enigma-bbs.log | bunyan - // Windows : "Get-Content .\enigma-bbs.log | bunyan.cmd" + // Windows : Get-Content .\enigma-bbs.log | bunyan.cmd // // (npm install -g bunyan to get the binary) // From e316b5fe8083e11392c0d64ef63ca0256aeedfa9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 18 Nov 2018 01:56:40 -0700 Subject: [PATCH 349/569] Add File Base Download Manager docs --- art/themes/luciano_blocktronics/theme.hjson | 7 ++----- core/file_base_download_manager.js | 6 +----- docs/_includes/nav.md | 1 + docs/modding/file-base-download-manager.md | 22 +++++++++++++++++++++ 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 docs/modding/file-base-download-manager.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index d23904af..acc3c845 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -783,16 +783,13 @@ } fileBaseDownloadManager: { - config: { - queueListFormat: "|00|03{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}" - focusQueueListFormat: "|00|19|15{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}" - } - 0: { mci: { VM1: { height: 11 width: 69 + itemFormat: "|00|03{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}" + focusItemFormat: "|00|19|15{fileName:<61.60} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}" } HM2: { width: 50 diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index f590ebd3..50abd6da 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -150,11 +150,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { return cb(Errors.DoesNotExist('Queue view does not exist')); } - const queueListFormat = this.menuConfig.config.queueListFormat || '{fileName} {byteSize}'; - const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; - - queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); - queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); + queueView.setItems(this.dlQueue.items); queueView.on('index update', idx => { const fileEntry = this.dlQueue.items[idx]; diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 4c2fc0f6..d0a4e7eb 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -74,6 +74,7 @@ - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) - [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %}) - [Show Art]({{ site.baseurl }}{% link modding/show-art.md %}) + - [Download Manager]({{ site.baseurl }}{% link modding/file_base_download_manager.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/file-base-download-manager.md b/docs/modding/file-base-download-manager.md new file mode 100644 index 00000000..c454de8a --- /dev/null +++ b/docs/modding/file-base-download-manager.md @@ -0,0 +1,22 @@ +--- +layout: page +title: File Transfer Protocol Select +--- +## File Base Download Manager Module +The `file_base_download_manager` module provides a download queue manager for "legacy" (X/Y/Z-Modem, etc.) downloads. + +## Configuration +### Configuration Block +Available `config` block entries: +* `webDlExpireTimeFormat`: Sets the moment.js style format for web download expiration date/time. +* `fileTransferProtocolSelection`: Overrides the default `fileTransferProtocolSelection` target for a protocol selection menu. +* `emptyQueueMenu`: Overrides the default `fileBaseDownloadManagerEmptyQueue` target for menu to show when the users D/L queue is empty. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) and MCI 10+ custom fields: +* `fileId`: File ID. +* `areaTag`: Area tag. +* `fileName`: Entry filename. +* `path`: Full file path. +* `byteSize`: Size in bytes of file. + From b34294fbefa83bee10b562c5a4437a40ab64f871 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 18 Nov 2018 12:02:57 -0700 Subject: [PATCH 350/569] * Docs + itemFormat/focusItem format for web download manager * Fix some typos --- art/themes/luciano_blocktronics/theme.hjson | 4 +-- core/file_base_web_download_manager.js | 6 +---- docs/_includes/nav.md | 3 ++- docs/modding/file-base-download-manager.md | 7 ++--- .../modding/file-base-web-download-manager.md | 26 +++++++++++++++++++ 5 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 docs/modding/file-base-web-download-manager.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index acc3c845..2316eac9 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -801,8 +801,6 @@ fileBaseWebDownloadManager: { config: { - queueListFormat: "|00|03{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}" - focusQueueListFormat: "|00|19|15{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}" queueManagerInfoFormat10: "|03batch|08: |03{webBatchDlLink}" queueManagerInfoFormat11: "|03exp |08: |03{webBatchDlExpire}" } @@ -811,6 +809,8 @@ mci: { VM1: { height: 8 + itemFormat: "|00|03{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} |11{byteSize!sizeAbbr}" + focusItemFormat: "|00|19|15{webDlLink:<36.35} {fileName:<26.25} {byteSize!sizeWithoutAbbr:>7.6} {byteSize!sizeAbbr}" } HM2: { width: 50 diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index 02bffa3e..62ee02eb 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -121,11 +121,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { return cb(Errors.DoesNotExist('Queue view does not exist')); } - const queueListFormat = this.menuConfig.config.queueListFormat || '{webDlLink}'; - const focusQueueListFormat = this.menuConfig.config.focusQueueListFormat || queueListFormat; - - queueView.setItems(this.dlQueue.items.map( queueItem => stringFormat(queueListFormat, queueItem) ) ); - queueView.setFocusItems(this.dlQueue.items.map( queueItem => stringFormat(focusQueueListFormat, queueItem) ) ); + queueView.setItems(this.dlQueue.items); queueView.on('index update', idx => { const fileEntry = this.dlQueue.items[idx]; diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index d0a4e7eb..a42520bb 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -74,7 +74,8 @@ - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) - [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %}) - [Show Art]({{ site.baseurl }}{% link modding/show-art.md %}) - - [Download Manager]({{ site.baseurl }}{% link modding/file_base_download_manager.md %}) + - [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %}) + - [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/file-base-download-manager.md b/docs/modding/file-base-download-manager.md index c454de8a..22d634a0 100644 --- a/docs/modding/file-base-download-manager.md +++ b/docs/modding/file-base-download-manager.md @@ -1,9 +1,9 @@ --- layout: page -title: File Transfer Protocol Select +title: File Base Download Manager --- ## File Base Download Manager Module -The `file_base_download_manager` module provides a download queue manager for "legacy" (X/Y/Z-Modem, etc.) downloads. +The `file_base_download_manager` module provides a download queue manager for "legacy" (X/Y/Z-Modem, etc.) downloads. Web (HTTP/HTTPS) download functionality can be optionally available when the web content server is enabled. ## Configuration ### Configuration Block @@ -19,4 +19,5 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) and MCI 10+ * `fileName`: Entry filename. * `path`: Full file path. * `byteSize`: Size in bytes of file. - +* `webDlLink`: Web download link including VTX style ANSI ESC sequences. +* `webDlExpire`: Expiration date/time for this link. Formatted using `webDlExpireTimeFormat`. \ No newline at end of file diff --git a/docs/modding/file-base-web-download-manager.md b/docs/modding/file-base-web-download-manager.md new file mode 100644 index 00000000..09b8f22c --- /dev/null +++ b/docs/modding/file-base-web-download-manager.md @@ -0,0 +1,26 @@ +--- +layout: page +title: File Base Web Download Manager +--- +## File Base Web Download Manager Module +The `file_base_web_download_manager` module provides a download queue manager for web (HTTP/HTTPS) based downloads. This module relies on having the web server enabled at a minimum. + +Web downloads can be a convienent way for users to download larger (100+ MiB) files where legacy protocols often have trouble. Additionally, batch downloads can be streamed to users in a single zip archive. + +## Configuration +### Configuration Block +Available `config` block entries: +* `webDlExpireTimeFormat`: Sets the moment.js style format for web download expiration date/time. +* `emptyQueueMenu`: Overrides the default `fileBaseDownloadManagerEmptyQueue` target for menu to show when the users D/L queue is empty. + +### Theming +The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) and custom range MCI 10+ custom fields: +* `fileId`: File ID. +* `areaTag`: Area tag. +* `fileName`: Entry filename. +* `path`: Full file path. +* `byteSize`: Size in bytes of file. +* `webDlLinkRaw`: Web download link. +* `webDlLink`: Web download link including VTX style ANSI ESC sequences. +* `webDlExpire`: Expiration date/time for this link. Formatted using `webDlExpireTimeFormat`. + From 7cd0b3b393666120f49391f92287b029c59dc8f1 Mon Sep 17 00:00:00 2001 From: SemperFu Date: Sun, 18 Nov 2018 14:06:46 -0500 Subject: [PATCH 351/569] Update config_template.in.hjson Changed to PowerShell. Added -F equivalent (-wait) and Tail --- misc/config_template.in.hjson | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 9facd9e3..cde8284b 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -66,8 +66,8 @@ // See https://github.com/trentm/node-bunyan#streams // // Remember you can pipe logs through Bunyan to pretty-print: - // Linux : tail -F ./logs/enigma-bbs.log | bunyan - // Windows : Get-Content .\enigma-bbs.log | bunyan.cmd + // Linux : tail -F ./logs/enigma-bbs.log | bunyan + // Powershell : Get-Content .\enigma-bbs.log -Wait -Tail 15 | bunyan.cmd // // (npm install -g bunyan to get the binary) // From 7410862f5707350d45ec783ef7f62e694c220532 Mon Sep 17 00:00:00 2001 From: SemperFu Date: Sun, 18 Nov 2018 14:10:11 -0500 Subject: [PATCH 352/569] Update config_template.in.hjson Cap S --- misc/config_template.in.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index cde8284b..0c0e40c1 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -67,7 +67,7 @@ // // Remember you can pipe logs through Bunyan to pretty-print: // Linux : tail -F ./logs/enigma-bbs.log | bunyan - // Powershell : Get-Content .\enigma-bbs.log -Wait -Tail 15 | bunyan.cmd + // PowerShell : Get-Content .\enigma-bbs.log -Wait -Tail 15 | bunyan.cmd // // (npm install -g bunyan to get the binary) // From 5f4e129369c25016f659526ff0ac473c1410b6a0 Mon Sep 17 00:00:00 2001 From: SemperFu Date: Sun, 18 Nov 2018 14:17:42 -0500 Subject: [PATCH 353/569] Remove -Wait -Wait doesn't work. It sits there waiting for completion before forwarding. --- misc/config_template.in.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 0c0e40c1..ba3e8937 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -67,7 +67,7 @@ // // Remember you can pipe logs through Bunyan to pretty-print: // Linux : tail -F ./logs/enigma-bbs.log | bunyan - // PowerShell : Get-Content .\enigma-bbs.log -Wait -Tail 15 | bunyan.cmd + // PowerShell : Get-Content .\enigma-bbs.log -Tail 15 | bunyan.cmd // // (npm install -g bunyan to get the binary) // From 370f8039db0abf1a8445baca339c9544c71a9876 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 18 Nov 2018 14:19:34 -0700 Subject: [PATCH 354/569] * Use itemFormat/focusItemFormat for set newscan message/file conf/areas * Docs for set_newscan_date module --- .../luciano_blocktronics/SETMNSDATE.ANS | Bin 512 -> 526 bytes art/themes/luciano_blocktronics/theme.hjson | 10 ++++++ core/set_newscan_date.js | 11 +++---- docs/_includes/nav.md | 1 + docs/modding/file-base-download-manager.md | 2 +- .../modding/file-base-web-download-manager.md | 2 +- docs/modding/set-newscan-date.md | 29 ++++++++++++++++++ misc/menu_template.in.hjson | 1 - 8 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 docs/modding/set-newscan-date.md diff --git a/art/themes/luciano_blocktronics/SETMNSDATE.ANS b/art/themes/luciano_blocktronics/SETMNSDATE.ANS index 4d3b43a314582ba6d98d9721010a2432cb717c88..61cbb3daa0239aa4e58983984875677507959ae2 100644 GIT binary patch delta 33 ocmZo*>0_A?EMV#^9c^fBY?Lb}FCA@=tFSS&m637kGOMpXbjr3O0y diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 2316eac9..31a67054 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -254,6 +254,16 @@ } } + messageAreaSetNewScanDate: { + mci: { + SM2: { + width: 54 + itemFormat: "|00|07{conf.name} |08- |07{area.name}" + focusItemFormat: "|00|15{conf.name} |07- |15{area.name}" + } + } + } + mailMenuCreateMessage: { 0: { mci: { diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js index 27a27c21..c86b8e26 100644 --- a/core/set_newscan_date.js +++ b/core/set_newscan_date.js @@ -153,11 +153,13 @@ exports.getModule = class SetNewScanDate extends MenuModule { selections.push({ conf : { confTag : conf.confTag, + text : conf.conf.name, // standard name : conf.conf.name, desc : conf.conf.desc, }, area : { areaTag : area.areaTag, + text : area.area.name, // standard name : area.area.name, desc : area.area.desc, } @@ -168,11 +170,13 @@ exports.getModule = class SetNewScanDate extends MenuModule { selections.unshift({ conf : { confTag : '', + text : 'All conferences', name : 'All conferences', desc : 'All conferences', }, area : { areaTag : '', + text : 'All areas', name : 'All areas', desc : 'All areas', } @@ -236,14 +240,9 @@ exports.getModule = class SetNewScanDate extends MenuModule { scanDateView.setText(today.format(scanDateFormat)); if('message' === self.target) { - const messageSelectionsFormat = self.menuConfig.config.messageSelectionsFormat || '{conf.name} - {area.name}'; - const messageSelectionFocusFormat = self.menuConfig.config.messageSelectionFocusFormat || messageSelectionsFormat; - const targetSelectionView = vc.getView(MciViewIds.main.targetSelection); - targetSelectionView.setItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); - targetSelectionView.setFocusItems(self.targetSelections.map(targetSelection => stringFormat(messageSelectionFocusFormat, targetSelection))); - + targetSelectionView.setItems(self.targetSelections); targetSelectionView.setFocusItemIndex(0); } diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index a42520bb..fe587900 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -76,6 +76,7 @@ - [Show Art]({{ site.baseurl }}{% link modding/show-art.md %}) - [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %}) - [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %}) + - [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/file-base-download-manager.md b/docs/modding/file-base-download-manager.md index 22d634a0..023ae478 100644 --- a/docs/modding/file-base-download-manager.md +++ b/docs/modding/file-base-download-manager.md @@ -19,5 +19,5 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) and MCI 10+ * `fileName`: Entry filename. * `path`: Full file path. * `byteSize`: Size in bytes of file. -* `webDlLink`: Web download link including VTX style ANSI ESC sequences. +* `webDlLink`: Web download link including [VTX style ANSI ESC sequences](https://raw.githubusercontent.com/codewar65/VTX_ClientServer/master/vtx.txt). * `webDlExpire`: Expiration date/time for this link. Formatted using `webDlExpireTimeFormat`. \ No newline at end of file diff --git a/docs/modding/file-base-web-download-manager.md b/docs/modding/file-base-web-download-manager.md index 09b8f22c..1dadca00 100644 --- a/docs/modding/file-base-web-download-manager.md +++ b/docs/modding/file-base-web-download-manager.md @@ -21,6 +21,6 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`) and custom r * `path`: Full file path. * `byteSize`: Size in bytes of file. * `webDlLinkRaw`: Web download link. -* `webDlLink`: Web download link including VTX style ANSI ESC sequences. +* `webDlLink`: Web download link including [VTX style ANSI ESC sequences](https://raw.githubusercontent.com/codewar65/VTX_ClientServer/master/vtx.txt). * `webDlExpire`: Expiration date/time for this link. Formatted using `webDlExpireTimeFormat`. diff --git a/docs/modding/set-newscan-date.md b/docs/modding/set-newscan-date.md new file mode 100644 index 00000000..5649edac --- /dev/null +++ b/docs/modding/set-newscan-date.md @@ -0,0 +1,29 @@ +--- +layout: page +title: Set Newscan Date Module +--- +## Set Newscan Date Module +The `set_newscan_date` module allows setting newscan dates (aka pointers) for message conferences and areas as well as within the file base. Users can select specific conferences/areas or all (where applicable). + +## Configuration +### Configuration Block +Available `config` block entries are as follows: +* `target`: Choose from `message` for message conferences & areas, or `file` for file base areas. +* `scanDateFormat`: Format for scan date. This format must align with the **output** of the MaskEditView (`%ME1`) MCI utilized for input. Defaults to `YYYYMMDD` (which matches mask of `####/##/##`). + +### Theming +#### Message Conference & Areas +When `target` is `message`, the following `itemFormat` object is provided to MCI 2 (ie: `%SM2`): +* `conf`: An object containing: + * `confTag`: Conference tag. + * `name`: Conference name. Also available in `{text}`. + * `desc`: Conference description. +* `area`: An object containing: + * `areaTag`: Area tag. + * `name`: Area name. Also available in `{text}`. + * `desc`: Area description. + +When dealing with the file base, ENiGMA½ does not currently have the ability to set newscan dates for specific areas. No `%SM2` is used in this case. + +### Submit Actions +Submit action should map to `@method:scanDateSubmit` and provide `scanDate` in form data. For message conf/areas (`target` of `message`), `targetSelection` should be also be provided in form data: An index to the selected conf/area. diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 8f1aca3b..e7653081 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1921,7 +1921,6 @@ SM2: { argName: targetSelection submit: false - justify: right } } submit: { From 53a23c51634c828da5f1d14b66e219e184d220f4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Nov 2018 19:08:01 -0700 Subject: [PATCH 355/569] Notes on binkd --- docs/messageareas/bso-import-export.md | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/messageareas/bso-import-export.md b/docs/messageareas/bso-import-export.md index ad189bbc..aab72429 100644 --- a/docs/messageareas/bso-import-export.md +++ b/docs/messageareas/bso-import-export.md @@ -120,5 +120,34 @@ scannerTossers: { } ``` +## Binkd +Since Binkd is a very common mailer, a few tips on integrating it with ENiGMA½: + +### Scheduling Polls +Binkd does not have it's own scheduler. Instead, you'll need to set up an Event Scheduler entry or perhaps a cron job: + +First, create a script that runs through all of your uplinks. For example: +```bash +#!/bin/bash +UPLINKS=("21:1/100@fsxnet" "80:774/1@retronet" "10:101/0@araknet") +for uplink in "${UPLINKS[@]}" +do + /usr/local/sbin/binkd -p -P $uplink /home/enigma/xibalba/misc/binkd_xibalba.conf +done +``` + +Now, create an Event Scheuler entry in your `config.hjson`. As an example: +```hjson +eventScheduler: { + events: { + pollWithBink: { + // execute the script above very 1 hours + schedule: every 1 hours + action: @execute:/path/to/poll_bink.sh + } + } +} +``` + ## Additional Resources * [Blog entry on setting up ENiGMA + Binkd on CentOS7](https://l33t.codes/enigma-12-binkd-on-centos-7/). Note that this references an **older version**, so be wary of the `config.hjson` refernces! From 16474c885fe551596be826d540150fc5df408662 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Nov 2018 20:00:29 -0700 Subject: [PATCH 356/569] Initial Event Scheduler docs --- docs/configuration/event-scheduler.md | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/configuration/event-scheduler.md diff --git a/docs/configuration/event-scheduler.md b/docs/configuration/event-scheduler.md new file mode 100644 index 00000000..77b56f15 --- /dev/null +++ b/docs/configuration/event-scheduler.md @@ -0,0 +1,79 @@ +--- +layout: page +title: Event Scheduler +--- +## Event Scheduler +The ENiGMA½ scheduler allows system operators to configure arbitrary events that can can fire based on date and/or time, or by watching for changes in a file. Events can kick off internal handlers, custom modules, or binaries & scripts. + +## Scheduling Events +To create a scheduled event, create a new configuration block in `config.hjson` under `eventScheduler.events`. + +Events can have the following members: + +| Item | Required | Description | +|------|----------|-------------| +| `schedule` | :+1: | A [Later style](https://bunkat.github.io/later/parsers.html#text) parsable schedule string such as `at 4:00 am`, or `every 24 hours`. Can also be (or contain) an `@watch` clause. See **Schedules** below for details. | +| `action` | :+1: | Action to perform when the schedule is triggered. May be an `@method` or `@execute` spec. See **Actions** below. | +| `args` | :-1: | An array of arguments to pass along to the method or binary specified in `action`. | + +### Schedules +As mentioned above, `schedule` may contain a [Later style](https://bunkat.github.io/later/parsers.html#text) parsable schedule string and/or an `@watch` clause. + +`schedule` examples: +* `every 2 hours` +* `on the last day of the week` +* `after 12th hour` + +An `@watch` clause monitors a specified file for changes and takes the following form: `@watch:` where `` is a fully qualified path. + +:information_source: If you would like to have a schedule **and** watch a file for changes, place the `@watch` clause second and seperated with the word `or`. For example: `every 24 hours or @watch:/path/to/somefile.txt`. + +### Actions +Events can kick off actions by calling a method (function) provided by the system or custom module in addition to executing arbritary binaries or scripts. + +#### Methods +An action with a `@method` can take the following forms: + +* `@method:/full/path/to/module.js:methodName`: Executes `methodName` at `/full/path/to/module.js`. +* `@method:rel/path/to/module.js:methodName`: Executes `methodName` using the *relative* path `rel/path/to/module.js`. Paths for `@method` are relative to the ENiGMA½ installation directory. + +Methods are passed any supplied `args` in the order they are provided. + +##### Method Signature +To create your own method, simply `export` a method with the following signature: `(args, callback)`. Methods are executed asynchronously. + +Example: +```javascript +// my_custom_mod.js +exports.myCustomMethod = (args, cb) => { + console.log(`Hello, ${args[0]}!`); + return cb(null); +} +``` + +#### Executables +When using the `@execute` action, a binary or script can be executed. A full path or just the binary name is acceptable. If using the form without a path, the binary much be in ENiGMA½'s `PATH`. + +Examples: +* `@execute:/usr/bin/foo` +* `@execute:foo` + +Just like with methods, any supplied `args` will be passed along. + +## Example Entries + +Post a message to supplied networks every Monday night using the message post mod (see modding): +```hjson +eventScheduler: { + events: { + enigmaAdToNetworks: { + schedule: at 10:35 pm on Mon + action: @method:mods/message_post_evt/message_post_evt.js:messagePostEvent + args: [ + "fsx_bot" + "/home/enigma-bbs/ad.asc" + ] + } + } +} +``` \ No newline at end of file From ec1f437dd92ea8f37551676bb73cf2f750d45e59 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Nov 2018 20:16:30 -0700 Subject: [PATCH 357/569] Fix DEP0005 during file base scan --- core/file_base_area.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 78450501..a684f575 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -705,7 +705,7 @@ function scanFile(filePath, options, iterator, cb) { // up to many seconds in time for larger files. // const chunkSize = 1024 * 64; - const buffer = new Buffer(chunkSize); + const buffer = Buffer.allocUnsafe(chunkSize); fs.open(filePath, 'r', (err, fd) => { if(err) { From df82868ddf7c5ce789c6b9798dc0b9f6d83aa4b2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Nov 2018 20:40:02 -0700 Subject: [PATCH 358/569] Minor dependency updates --- package-lock.json | 69 +++++++++++++++++++++++++---------------------- package.json | 8 +++--- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index be373f20..a3bc6f81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,11 +94,6 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -494,16 +489,15 @@ } }, "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", "requires": { - "globby": "^5.0.0", + "globby": "^6.1.0", "is-path-cwd": "^1.0.0", "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", "rimraf": "^2.2.8" } }, @@ -878,16 +872,22 @@ } }, "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "version": "6.1.0", + "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "requires": { "array-union": "^1.0.1", - "arrify": "^1.0.0", "glob": "^7.0.3", "object-assign": "^4.0.1", "pify": "^2.0.0", "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } } }, "graceful-fs": { @@ -954,9 +954,9 @@ "integrity": "sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w==" }, "hjson": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/hjson/-/hjson-3.1.1.tgz", - "integrity": "sha512-1oGkOq4sssz7HFZ8Is9HuTR47r8gSC46qAzQxVlAkj0lNKpS+W5Lv2eci+c5+fFqL+Idtj5EvprFreUwH29a8A==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hjson/-/hjson-3.1.2.tgz", + "integrity": "sha512-2ILrho8eRl2Bniy61mDFiXRAloYqH2T6OwWkoF/8y55DPFgG2RcqQGNXIfBLp432dnAbLOpBJ4pJs63W3X27EA==" }, "http-signature": { "version": "1.2.0", @@ -1649,6 +1649,11 @@ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -1675,9 +1680,9 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, "pinkie": { "version": "2.0.4", @@ -2233,11 +2238,11 @@ } }, "temptmp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.0.0.tgz", - "integrity": "sha1-M7Djbh8nMXyKKBIO6Wufj+tw2UM=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.1.0.tgz", + "integrity": "sha512-gHelQlePUzxRmodWL1uJ9LiwI+a7a3rkFGS9azTf4noPZgGOlx0dOPV9tZs5+QwGc4Nm8BfFxL9cfvV42GNxPQ==", "requires": { - "del": "^2.2.2" + "del": "^3.0.0" } }, "through": { @@ -2493,9 +2498,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz", - "integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", + "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", "requires": { "async-limiter": "~1.0.0" } @@ -2514,9 +2519,9 @@ "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" }, "yazl": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.4.3.tgz", - "integrity": "sha1-7CblzIfVYBud+EMtvdPNLlFzoHE=", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.0.tgz", + "integrity": "sha512-rgptqKwX/f1/7bIRF1FHb4HGsP5k11QyxBpDl1etUDfNpTa7CNjDOYNPFnIaEzZ9dRq0c47IEJS+sy+T39JCLw==", "requires": { "buffer-crc32": "~0.2.3" } diff --git a/package.json b/package.json index 73fd9ebc..f6425f01 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "glob": "^7.1.2", "graceful-fs": "^4.1.15", "hashids": "^1.1.1", - "hjson": "^3.1.1", + "hjson": "^3.1.2", "iconv-lite": "^0.4.23", "inquirer": "^6.0.0", "later": "1.2.0", @@ -47,12 +47,12 @@ "sqlite3": "^4.0.4", "sqlite3-trans": "^1.2.0", "ssh2": "^0.6.1", - "temptmp": "^1.0.0", + "temptmp": "^1.1.0", "uuid": "^3.2.1", "uuid-parse": "^1.0.0", - "ws": "^6.1.0", + "ws": "^6.1.2", "xxhash": "^0.2.4", - "yazl": "^2.4.2" + "yazl": "^2.5.0" }, "devDependencies": {}, "engines": { From 5ea6f5e1f89d8d0f966b4bdb7be480650d2c69e8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Nov 2018 20:40:20 -0700 Subject: [PATCH 359/569] Sample event scheduler entry --- misc/config_template.in.hjson | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index ba3e8937..8a474779 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -331,7 +331,7 @@ // ] // // - // Set default group(s) new users should automatically be assigned to + // Set default group(s) new users should automatically be assigned to: // defaultGroups : [ // "lamerz" // ] @@ -379,6 +379,25 @@ // } + // + // Use the Event Scheduler to set up arbitrary scheduled events + // using Later style syntax and/or @watch files. + // See docs/event-scheduler.md for more information. + // + eventScheduler: { + events: { + // Example: + // + // sampleEvent: { + // schedule: every 2 hours + // action: @execute:/path/to/some/script.sh + // args: [ + // "--foo", "--bar" + // ] + // } + } + } + statLog: { systemEvents: { // Max login history event records kept. -1 = unlimited From 284151a0c94e22d55040d8ce5494550fd9845359 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 19 Nov 2018 21:16:37 -0700 Subject: [PATCH 360/569] Many doc updates RE: HJSON/general config --- docs/_includes/nav.md | 8 +++--- docs/configuration/config-hjson.md | 4 ++- docs/configuration/creating-config.md | 23 ++++------------ docs/configuration/hjson.md | 39 +++++++++++++++++++++++++++ docs/configuration/menu-hjson.md | 6 ++--- docs/configuration/prompt-hjson.md | 4 ++- 6 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 docs/configuration/hjson.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index fe587900..728e5444 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -14,15 +14,17 @@ - [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %}) - [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %}) - [Editing hjson]({{ site.baseurl }}{% link configuration/editing-hjson.md %}) - - [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - - [menu.hjson]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) - - [prompt.hjson]({{ site.baseurl }}{% link configuration/prompt-hjson.md %}) + - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %}) + - [HJSON General]({{ site.baseurl }}{% link configuration/hjson.md %}) + - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) + - [Prompts]({{ site.baseurl }}{% link configuration/prompt-hjson.md %}) - [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %}) - [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %}) - [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %}) - [Email]({{ site.baseurl }}{% link configuration/email.md %}) - [Colour Codes]({{ site.baseurl }}{% link configuration/colour-codes.md %}) - [Access Condition System (ACS)]({{ site.baseurl }}{% link configuration/acs.md %}) + - [Event Scheduler]({{ site.baseurl }}{% link configuration/event-scheduler.md %}) - Scheduled jobs - File Base diff --git a/docs/configuration/config-hjson.md b/docs/configuration/config-hjson.md index 37d07beb..4ebfd030 100644 --- a/docs/configuration/config-hjson.md +++ b/docs/configuration/config-hjson.md @@ -1,10 +1,12 @@ --- layout: page -title: config.hjson +title: System Configuration --- ## System Configuration The main system configuration file, `config.hjson` both overrides defaults and provides additional configuration such as message areas. The default path is `/enigma-bbs-install-path/config/config.hjson` though you can override the `config.hjson` location with the `--config` parameter when invoking `main.js`. Values found in `core/config.js` may be overridden by simply providing the object members you wish replace. +See also [HJSON General Information](hjson.md) for more information on the HJSON format. + ### Creating a Configuration Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory: ``` diff --git a/docs/configuration/creating-config.md b/docs/configuration/creating-config.md index c8495f1c..5d845d5e 100644 --- a/docs/configuration/creating-config.md +++ b/docs/configuration/creating-config.md @@ -2,26 +2,13 @@ layout: page title: Creating Initial Config Files --- -Configuration files in ENiGMA½ are simple UTF-8 encoded [HJSON](http://hjson.org/) files. HJSON is just -like JSON but simplified and much more resilient to human error. +Configuration files in ENiGMA½ are simple UTF-8 encoded [HJSON](http://hjson.org/) files. HJSON is just like JSON but simplified and much more resilient to human error. -## config.hjson -Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your -enigma-bbs root directory: -``` +## Initial Configuration +Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory: +```bash ./oputil.js config new ``` -You will be asked a series of questions to create an initial configuration. +You will be asked a series of questions to create an initial configuration, which will be saved to `/enigma-bbs-install-path/config/config.hjson`. This will also produce `config/-menu.hjson` and `config/-prompt.hjson` files (where `` is replaced by the name you provided in the steps above). See [Menu HJSON](menu-hjson.md) and [Prompt HJSON](prompt-hjson.md) for more information. -## menu.hjson and prompt.hjson - -Create your own copy of `/config/menu.hjson` and `/config/prompt.hjson`, and specify it in the -`general` section of `config.hjson`: - -````hjson -general: { - menuFile: my-menu.hjson - promptFile: my-prompt.hjson -} -```` \ No newline at end of file diff --git a/docs/configuration/hjson.md b/docs/configuration/hjson.md new file mode 100644 index 00000000..a4eda583 --- /dev/null +++ b/docs/configuration/hjson.md @@ -0,0 +1,39 @@ +--- +layout: page +title: HJSON General Information +--- +## JSON for Humans! +HJSON is the configuration file format used by ENiGMA½ for [System Configuration](config-hjson.md), [Menus](menu-hjson.md), [Prompts](prompt-hjson.md), etc. [HJSON](https://hjson.org/) is is [JSON](https://json.org/) for humans! + +For those completely unfamiliar, JSON stands for JavaScript Object Notation. But don't let that scare you! JSON is simply a text file format with a bit of structure ― kind of like a fancier INI file. HJSON on the other hand as mentioned previously, is JSON for humans. That is, it has the following features and more: + +* More resilliant to syntax errors such as missing a comma +* Strings generally do not need to be quoted. Multi-line strings are also supported! +* Comments are supported (JSON doesn't allow this!): `#`, `//` and `/* ... */` style comments are allowed. +* Keys never need to be quoted +* ...much more! See [the official HJSON website](https://hjson.org/). + +## Terminology +Through the documentation, some terms regarding HJSON and configuration files will be used: + +* `config.hjson`: Refers to `/path/to/enigma-bbs/config/config.hjson`. See [System Configuration](config-hjson.md). +* `menu.hjson`: Refers to `/path/to/enigma-bbs/config/-menu.hjson`. See [Menus](menu-hjson.md). +* `prompt.hjson`: Refers to `/path/to/enigma-bbs/config/-prompt.hjson`. See [Prompts](prompt-hjson.md). +* Configuration *key*: Elements in HJSON are name-value pairs where the name is the *key*. For example, provided `foo: bar`, `foo` is the key. +* Configuration *section* or *block* (also commonly called an "Object" in code): This is referring to a section in a HJSON file that starts with a *key*. For example: +```hjson +someSection: { + foo: bar +} +``` +Note that `someSection` is the configuration *section* (or *block*) and `foo: bar` is within it. + +## Editing HJSON +HJSON is a text file format, and ENiGMA½ configuration files **should always be saved as UTF-8**. + +It is **highly** recommended to use a text editor that has HJSON support. A few (but not all!) examples include: +* Sublime Text 3 via the `sublime-hjson` package. +* Visual Studio code via the `vscode-hjson` plugin. +* Notepad++ via the `npp-hjson` plugin. + +See https://hjson.org/users.html for more information. diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 1956252c..d5e9b66d 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -1,9 +1,9 @@ --- layout: page -title: menu.hjson +title: Menus --- -## Menu HJSON -The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/yourboardname-menu.hjson` or similar. This file determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. +## Menus +The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/yourboardname-menu.hjson` or similar. This file determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information. Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: diff --git a/docs/configuration/prompt-hjson.md b/docs/configuration/prompt-hjson.md index 7b7a3ab5..993e5b8e 100644 --- a/docs/configuration/prompt-hjson.md +++ b/docs/configuration/prompt-hjson.md @@ -3,4 +3,6 @@ layout: page title: prompt.hjson --- :zap: This page is to describe general information the `prompt.hjson` file. It -needs fleshing out, please submit a PR if you'd like to help! \ No newline at end of file +needs fleshing out, please submit a PR if you'd like to help! + +See [HJSON General Information](hjson.md) for more information. From ce9a3c29b4baf31ad8b52300e4d5b980b1356e31 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 20 Nov 2018 18:45:01 -0700 Subject: [PATCH 361/569] More notes on HJSON --- docs/configuration/hjson.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/configuration/hjson.md b/docs/configuration/hjson.md index a4eda583..509c2980 100644 --- a/docs/configuration/hjson.md +++ b/docs/configuration/hjson.md @@ -7,7 +7,7 @@ HJSON is the configuration file format used by ENiGMA½ for [System Configuratio For those completely unfamiliar, JSON stands for JavaScript Object Notation. But don't let that scare you! JSON is simply a text file format with a bit of structure ― kind of like a fancier INI file. HJSON on the other hand as mentioned previously, is JSON for humans. That is, it has the following features and more: -* More resilliant to syntax errors such as missing a comma +* More resilient to syntax errors such as missing a comma * Strings generally do not need to be quoted. Multi-line strings are also supported! * Comments are supported (JSON doesn't allow this!): `#`, `//` and `/* ... */` style comments are allowed. * Keys never need to be quoted @@ -36,4 +36,23 @@ It is **highly** recommended to use a text editor that has HJSON support. A few * Visual Studio code via the `vscode-hjson` plugin. * Notepad++ via the `npp-hjson` plugin. -See https://hjson.org/users.html for more information. +See https://hjson.org/users.html for more more editors & plugins. + +### Hot-Reload A.K.A. Live Editing +ENiGMA½'s configuration, menu, and theme files can edited while your BBS is running. When a file is saved, it is hot-reloaded into the running system. If users are currently connected and you change a menu for example, the next reload of that menu will show the changes. + +### CaSe SeNsiTiVE +Configuration keys are **case sensitive**. That means if a configuration key is `boardName` for example, `boardname`, or `BOARDNAME` **will not work**. + +## Tips & Tricks +### JSON Compatibility +Remember that standard JSON is fully compatible with HJSON. If you are more comfortable with JSON (or have an editor that works with JSON that you prefer) simply convert your config file(s) to JSON and use that instead! + +HJSON can be converted to JSON with the `hjson` CLI: +```bash +cd /path/to/enigma-bbs +cp ./config/config.hjson ./config/config.hjson.backup +./node_modules/hjson/bin/hjson ./config/config.hjson.backup -j > ./config/config.hjson +``` + +You can always convert back to HJSON by omitting `-j` in the command above. From 3fd526da6fdf994980d1f50d36fbc51f9cf2594d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 20 Nov 2018 21:01:39 -0700 Subject: [PATCH 362/569] Fix some spelling --- core/oputil/oputil_help.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 7c96d171..273bd787 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -26,9 +26,9 @@ commands: actions: pw USERNAME PASSWORD set password to PASSWORD for USERNAME - rm USERNAME permanantely removes USERNAME user from system + rm USERNAME permanently removes USERNAME user from system activate USERNAME sets USERNAME's status to active - deactivate USERNAME sets USERNAME's status to deactive + deactivate USERNAME sets USERNAME's status to inactive disable USERNAME sets USERNAME's status to disabled group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP `, @@ -57,7 +57,7 @@ cat args: actions: scan AREA_TAG[@STORAGE_TAG] scan specified area may also contain optional GLOB as last parameter, - for examle: scan some_area *.zip + for example: scan some_area *.zip info CRITERIA display information about areas and/or files where CRITERIA is one of the following: From c1c4f80a18fd948596e244c12bcc59222442f57a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 20 Nov 2018 21:02:01 -0700 Subject: [PATCH 363/569] Note on oputil config cat --- docs/configuration/hjson.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration/hjson.md b/docs/configuration/hjson.md index 509c2980..f29aaddb 100644 --- a/docs/configuration/hjson.md +++ b/docs/configuration/hjson.md @@ -56,3 +56,6 @@ cp ./config/config.hjson ./config/config.hjson.backup ``` You can always convert back to HJSON by omitting `-j` in the command above. + +### oputil +You can easily dump out your current configuration in a pretty-printed style using oputil: ```./oputil.js config cat``` From 37914b8f717ef51138d9a6674a0191014aba9a71 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 20 Nov 2018 21:02:19 -0700 Subject: [PATCH 364/569] Fix spelling --- docs/configuration/menu-hjson.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index d5e9b66d..4d8dec9b 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -20,7 +20,7 @@ Below is a table of **common** menu entry members. These members apply to most e |--------|--------------| | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | | `art` | An art file *spec*. See [General Art Information](/docs/art/general.md). | -| `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilites dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | +| `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. | | `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. | | `submit` | Defines a submit handler when using `prompt`. | `form` | An object defining one or more *forms* available on this menu. | From 3af5b6f509ba84036b01a54286f7fbf147d33a03 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 20 Nov 2018 21:02:30 -0700 Subject: [PATCH 365/569] * Word wrap messages when output via Gopher server * Fix Gopher startup banner default --- core/servers/content/gopher.js | 38 +++++++++++++++++++++++++++------- misc/config_template.in.hjson | 3 +++ misc/gopher_banner.asc | 9 ++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 misc/gopher_banner.asc diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 240236d1..37cd9adb 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -17,6 +17,7 @@ const { } = require('../../message_area.js'); const { sortAreasOrConfs } = require('../../conf_area_util.js'); const AnsiPrep = require('../../ansi_prep.js'); +const { wordWrapText } = require('../../word_wrap.js'); // deps const net = require('net'); @@ -27,9 +28,10 @@ const moment = require('moment'); const ModuleInfo = exports.moduleInfo = { name : 'Gopher', - desc : 'Gopher Server', + desc : 'A RFC-1436-ish Gopher Server', author : 'NuSkooler', packageName : 'codes.l33t.enigma.gopher.server', + notes : 'https://tools.ietf.org/html/rfc1436', }; const Message = require('../../message.js'); @@ -158,7 +160,7 @@ exports.getModule = class GopherModule extends ServerModule { defaultGenerator(selectorMatch, cb) { this.log.debug( { selector : selectorMatch[0] }, 'Serving default content'); - let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'startup_banner.asc'); + let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'gopher_banner.asc'); bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); fs.readFile(bannerFile, 'utf8', (err, banner) => { if(err) { @@ -182,21 +184,43 @@ exports.getModule = class GopherModule extends ServerModule { } prepareMessageBody(body, cb) { + // + // From RFC-1436: + // "User display strings are intended to be displayed on a line on a + // typical screen for a user's viewing pleasure. While many screens can + // accommodate 80 character lines, some space is needed to display a tag + // of some sort to tell the user what sort of item this is. Because of + // this, the user display string should be kept under 70 characters in + // length. Clients may truncate to a length convenient to them." + // + // Messages on BBSes however, have generally been <= 79 characters. If we + // start wrapping earlier, things will generally be OK except: + // * When we're doing with FTN-style quoted lines + // * When dealing with ANSI/ASCII art + // + // Anyway, the spec says "should" and not MUST or even SHOULD! ...so, to + // to follow the KISS principle: Wrap at 79. + // + const WordWrapColumn = 79; if(isAnsi(body)) { AnsiPrep( body, { - cols : 79, // Gopher std. wants 70, but we'll have to deal with it. - forceLineTerm : true, // ensure each line is term'd - asciiMode : true, // export to ASCII - fillLines : false, // don't fill up to |cols| + cols : WordWrapColumn, // See notes above + forceLineTerm : true, // Ensure each line is term'd + asciiMode : true, // Export to ASCII + fillLines : false, // Don't fill up to |cols| }, (err, prepped) => { return cb(prepped || body); } ); } else { - return cb(cleanControlCodes(body, { all : true } )); + const prepped = splitTextAtTerms(cleanControlCodes(body, { all : true } ) ) + .map(l => (wordWrapText(l, { width : WordWrapColumn } ).wrapped || []).join('\n')) + .join('\n'); + + return cb(prepped); } } diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 8a474779..ab1b37b8 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -211,6 +211,9 @@ port: XXXXX enabled: false + // bannerFile path in misc/ by default. Full paths allowed. + bannerFile: XXXXX + // // The Gopher Content Server can export message base // conferences and areas via the "messageConferences" key. diff --git a/misc/gopher_banner.asc b/misc/gopher_banner.asc new file mode 100644 index 00000000..b758e066 --- /dev/null +++ b/misc/gopher_banner.asc @@ -0,0 +1,9 @@ +_____________________ _____ ____________________ __________\_ / +\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! +// __|___// | \// |// | \// | | \// \ /___ /_____ +/____ _____| __________ ___|__| ____| \ / _____ \ +---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + +------------------------------------------------------------------------------- From ebc70907d4c3b6ebb0b940a721dede7c9694d986 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 21 Nov 2018 17:55:31 -0700 Subject: [PATCH 366/569] Newest messages first when listing msgs from Gopher --- core/message_area.js | 20 +++++++++++++------- core/servers/content/gopher.js | 11 +++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/core/message_area.js b/core/message_area.js index e5227ef0..98647cfd 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -353,13 +353,19 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { }); } -function getMessageListForArea(client, areaTag, cb) { - const filter = { - areaTag, - resultType : 'messageList', - sort : 'messageId', - order : 'ascending', - }; +function getMessageListForArea(client, areaTag, filter, cb) +{ + if(!cb && _.isFunction(filter)) { + cb = filter; + filter = { + areaTag, + resultType : 'messageList', + sort : 'messageId', + order : 'ascending' + }; + } else { + Object.assign(filter, { areaTag } ); + } if(Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = client.user.userId; diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 37cd9adb..da09acd9 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -249,7 +249,7 @@ exports.getModule = class GopherModule extends ServerModule { return message.load( { uuid : msgUuid }, err => { if(err) { - this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existant message UUID!'); + this.log.debug( { uuid : msgUuid }, 'Attempted access to non-existent message UUID!'); return this.notFoundGenerator(selectorMatch, cb); } @@ -292,10 +292,17 @@ ${msgBody} return this.notFoundGenerator(selectorMatch, cb); } - return getMessageListForArea(null, areaTag, (err, msgList) => { + const filter = { + resultType : 'messageList', + sort : 'messageId', + order : 'descending', // we want newest messages first for Gopher + }; + + return getMessageListForArea(null, areaTag, filter, (err, msgList) => { const response = [ this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`), + this.makeItem(ItemTypes.InfoMessage, '(newest first)'), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), ...msgList.map(msg => this.makeItem( ItemTypes.TextFile, From 9fd819d608be567acfc69ae06f7b1e9e3c49ee7e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 21 Nov 2018 19:43:50 -0700 Subject: [PATCH 367/569] Resolve TODO RE using EnigError for bad login attempts --- core/enig_error.js | 4 +++- core/servers/login/ssh.js | 5 +++-- core/system_menu_method.js | 15 +++++++++------ core/user.js | 7 ++++--- core/user_login.js | 14 ++++++++------ 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/core/enig_error.js b/core/enig_error.js index 3f189dd7..01e62c40 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -34,8 +34,9 @@ exports.Errors = { ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), - MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode), + MissingParam : (reason, reasonCode) => new EnigError('Missing paramter(s)', -32008, reason, reasonCode), MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), + BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode), }; exports.ErrorReasons = { @@ -44,4 +45,5 @@ exports.ErrorReasons = { NoPreviousMenu : 'NOPREV', NoConditionMatch : 'NOCONDMATCH', NotEnabled : 'NOTENABLED', + AlreadyLoggedIn : 'ALREADYLOGGEDIN', }; \ No newline at end of file diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 72f91a2a..5f0ff05f 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -10,6 +10,7 @@ const userLogin = require('../../user_login.js').userLogin; const enigVersion = require('../../../package.json').version; const theme = require('../../theme.js'); const stringFormat = require('../../string_format.js'); +const { ErrorReasons } = require('../../enig_error.js'); // deps const ssh2 = require('ssh2'); @@ -70,7 +71,7 @@ function SSHClient(clientConn) { userLogin(self, ctx.username, ctx.password, function authResult(err) { if(err) { - if(err.existingConn) { + if(ErrorReasons.AlreadyLoggedIn === err.reasonCode) { return alreadyLoggedIn(username); } @@ -96,7 +97,7 @@ function SSHClient(clientConn) { userLogin(self, username, (answers[0] || ''), err => { if(err) { - if(err.existingConn) { + if(ErrorReasons.AlreadyLoggedIn === err.reasonCode) { return alreadyLoggedIn(username); } diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 9218e34a..8485a90f 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -2,10 +2,11 @@ 'use strict'; // ENiGMA½ -const removeClient = require('./client_connections.js').removeClient; +const { removeClient } = require('./client_connections.js'); const ansiNormal = require('./ansi_term.js').normal; -const userLogin = require('./user_login.js').userLogin; +const { userLogin } = require('./user_login.js'); const messageArea = require('./message_area.js'); +const { ErrorReasons } = require('./enig_error.js'); // deps const _ = require('lodash'); @@ -26,12 +27,14 @@ function login(callingMenu, formData, extraArgs, cb) { userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { if(err) { // login failure - if(err.existingConn && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { + if(ErrorReasons.AlreadyLoggedIn === err.reasonCode && + _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) + { return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); - } else { - // Other error - return callingMenu.prevMenu(cb); } + + // Other error + return callingMenu.prevMenu(cb); } // success! diff --git a/core/user.js b/core/user.js index 6c2b964d..27f4775a 100644 --- a/core/user.js +++ b/core/user.js @@ -45,9 +45,10 @@ module.exports = class User { static get AccountStatus() { return { - disabled : 0, - inactive : 1, - active : 2, + disabled : 0, // +op disabled + inactive : 1, // inactive, aka requires +op approval/activation + active : 2, // standard, active + locked : 3, // locked out (too many bad login attempts, etc.) }; } diff --git a/core/user_login.js b/core/user_login.js index a3b2089b..c4f1bcf1 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -8,6 +8,10 @@ const StatLog = require('./stat_log.js'); const logger = require('./logger.js'); const Events = require('./events.js'); const Config = require('./config.js').get; +const { + Errors, + ErrorReasons +} = require('./enig_error.js'); // deps const async = require('async'); @@ -48,12 +52,10 @@ function userLogin(client, username, password, cb) { 'Already logged in' ); - const existingConnError = new Error('Already logged in as supplied user'); - existingConnError.existingConn = true; - - // :TODO: We should use EnigError & pass existing connection as second param - - return cb(existingConnError); + return cb(Errors.BadLogin( + `User ${user.username} already logged in.`, + ErrorReasons.AlreadyLoggedIn + )); } // update client logger with addition of username From 472968e81d08da6100f6158968821c1430af19de Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 21 Nov 2018 19:50:03 -0700 Subject: [PATCH 368/569] Cleaner code --- core/user_login.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/core/user_login.js b/core/user_login.js index c4f1bcf1..d08788f3 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -32,14 +32,10 @@ function userLogin(client, username, password, cb) { // // Ensure this user is not already logged in. - // Loop through active connections -- which includes the current -- - // and check for matching user ID. If the count is > 1, disallow. // - let existingClientConnection; - clientConnections.forEach(function connEntry(cc) { - if(cc.user !== user && cc.user.userId === user.userId) { - existingClientConnection = cc; - } + const existingClientConnection = clientConnections.find(cc => { + return user !== cc.user && // not current connection + user.userId === cc.user.userId; // ...but same user }); if(existingClientConnection) { From 0721a7201c3d88fea8b4f0530f4078ff8c813771 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 21 Nov 2018 19:51:03 -0700 Subject: [PATCH 369/569] Default to logging user off if a 2nd login attempt is made while they're on --- misc/menu_template.in.hjson | 1 + 1 file changed, 1 insertion(+) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index e7653081..7a0fbcfa 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -185,6 +185,7 @@ cls: true nextTimeout: 2000 } + next: logoff } forgotPassword: { From ec1ce3062ec7a51702e90353119482ac230420a4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 21 Nov 2018 21:24:11 -0700 Subject: [PATCH 370/569] Catch bad spawn --- core/event_scheduler.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 22c2d58d..e6592d1b 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -5,6 +5,7 @@ const PluginModule = require('./plugin_module.js').PluginModule; const Config = require('./config.js').get; const Log = require('./logger.js').log; +const { Errors } = require('./enig_error.js'); const _ = require('lodash'); const later = require('later'); @@ -138,7 +139,14 @@ class ScheduledEvent { env : process.env, }; - const proc = pty.spawn(this.action.what, this.action.args, opts); + let proc; + try { + proc = pty.spawn(this.action.what, this.action.args, opts); + } catch(e) { + return cb(Errors.ExternalProcess( + `Error spawning @execute process "${this.action.what}" with args "${this.action.args.join(' ')}": ${e.message}`) + ); + } proc.once('exit', exitCode => { if(exitCode) { From 20e9d9ad7e0ab9ef37a47bc2001f1c935cd74305 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 21 Nov 2018 22:21:24 -0700 Subject: [PATCH 371/569] Actually log spawn() failure in Event Scheduler --- core/event_scheduler.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/core/event_scheduler.js b/core/event_scheduler.js index e6592d1b..95de5a97 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -117,7 +117,7 @@ class ScheduledEvent { methodModule[this.action.what](this.action.args, err => { if(err) { Log.debug( - { error : err.toString(), eventName : this.name, action : this.action }, + { error : err.message, eventName : this.name, action : this.action }, 'Error performing scheduled event action'); } @@ -125,7 +125,7 @@ class ScheduledEvent { }); } catch(e) { Log.warn( - { error : e.toString(), eventName : this.name, action : this.action }, + { error : e.message, eventName : this.name, action : this.action }, 'Failed to perform scheduled event action'); return cb(e); @@ -143,9 +143,17 @@ class ScheduledEvent { try { proc = pty.spawn(this.action.what, this.action.args, opts); } catch(e) { - return cb(Errors.ExternalProcess( - `Error spawning @execute process "${this.action.what}" with args "${this.action.args.join(' ')}": ${e.message}`) + Log.warn( + { + error : 'Failed to spawn @execute process', + reason : e.message, + eventName : this.name, + action : this.action, + what : this.action.what, + args : this.action.args + } ); + return cb(e); } proc.once('exit', exitCode => { From 1f9b963e769c2749297d5012981d9f0b843b4f0e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 22 Nov 2018 10:10:53 -0700 Subject: [PATCH 372/569] Fix typo --- core/scanner_tossers/ftn_bso.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index e2260d93..3292588d 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1746,7 +1746,7 @@ function FTNMessageScanTossModule() { } return callback(null, localInfo); // continue even if we couldn't find an old match }); - } else if(fileIds.legnth > 1) { + } else if(fileIds.length > 1) { return callback(Errors.General(`More than one existing entry for TIC in ${localInfo.areaTag} ([${fileIds.join(', ')}])`)); } else { return callback(null, localInfo); From f18b02365231068dd9790d05be9972674b413f58 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 22 Nov 2018 10:11:04 -0700 Subject: [PATCH 373/569] Notes on escaping --- docs/configuration/hjson.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/configuration/hjson.md b/docs/configuration/hjson.md index f29aaddb..cc8fa26d 100644 --- a/docs/configuration/hjson.md +++ b/docs/configuration/hjson.md @@ -44,6 +44,14 @@ ENiGMA½'s configuration, menu, and theme files can edited while your BBS is run ### CaSe SeNsiTiVE Configuration keys are **case sensitive**. That means if a configuration key is `boardName` for example, `boardname`, or `BOARDNAME` **will not work**. +### Escaping +Some values need escaped. This is especially important to remember on Windows machines where file paths contain backslashes (`\`). To specify a path to `C:\foo\bar\baz.exe` for example, an entry may look like this in your configuration file: +```hjson +something: { + path: "C:\\foo\\bar\\baz.exe" // note the extra \'s! +} +``` + ## Tips & Tricks ### JSON Compatibility Remember that standard JSON is fully compatible with HJSON. If you are more comfortable with JSON (or have an editor that works with JSON that you prefer) simply convert your config file(s) to JSON and use that instead! From df2bf4477e5ff839bec0bcec3ab68b7e8b8f4dd9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 22 Nov 2018 23:07:37 -0700 Subject: [PATCH 374/569] SECURITY UPDATE * Handle failed login attempts via Telnet * New lockout features for >= N failed attempts * New auto-unlock over email feature * New auto-unlock after N minutes feature * Code cleanup in users * Add user_property.js - start using consts for user properties. Clean up over time. * Update email docs --- .../luciano_blocktronics/ACCOUNTINACTIVE.ANS | Bin 0 -> 207 bytes .../luciano_blocktronics/ACCOUNTLOCKED.ANS | Bin 0 -> 197 bytes core/config.js | 8 +- core/enig_error.js | 6 +- core/oputil/oputil_config.js | 2 +- core/oputil/oputil_help.js | 1 + core/oputil/oputil_user.js | 32 +-- core/servers/login/ssh.js | 59 +++-- core/stat_log.js | 1 + core/system_menu_method.js | 10 +- core/tic_file_info.js | 4 +- core/user.js | 247 ++++++++++++++---- core/user_login.js | 18 +- core/user_property.js | 25 ++ core/web_password_reset.js | 9 +- docs/configuration/email.md | 32 ++- misc/config_template.in.hjson | 17 ++ misc/menu_template.in.hjson | 30 +++ 18 files changed, 401 insertions(+), 100 deletions(-) create mode 100644 art/themes/luciano_blocktronics/ACCOUNTINACTIVE.ANS create mode 100644 art/themes/luciano_blocktronics/ACCOUNTLOCKED.ANS create mode 100644 core/user_property.js diff --git a/art/themes/luciano_blocktronics/ACCOUNTINACTIVE.ANS b/art/themes/luciano_blocktronics/ACCOUNTINACTIVE.ANS new file mode 100644 index 0000000000000000000000000000000000000000..d0d2cd0e4f93f105a20f26af99667fea5639d6a6 GIT binary patch literal 207 zcmb1+Hn27^ur@Z&1boKT*u_({L;J^3j#%8&W$;tVpc_j*&#R@>#+?@R6 z?9>#Xj6tp= N + autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes. + }, + unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts }, theme : { diff --git a/core/enig_error.js b/core/enig_error.js index 01e62c40..78798a4a 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -46,4 +46,8 @@ exports.ErrorReasons = { NoConditionMatch : 'NOCONDMATCH', NotEnabled : 'NOTENABLED', AlreadyLoggedIn : 'ALREADYLOGGEDIN', -}; \ No newline at end of file + TooMany : 'TOOMANY', + Disabled : 'DISABLED', + Inactive : 'INACTIVE', + Locked : 'LOCKED', +}; diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 83ac5232..52c388f7 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -38,7 +38,7 @@ function getAnswers(questions, cb) { const ConfigIncludeKeys = [ 'theme', 'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds', - 'users.newUserNames', + 'users.newUserNames', 'users.failedLogin', 'users.unlockAtEmailPwReset', 'paths.logs', 'loginServers', 'contentServers', diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 273bd787..4d7931e0 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -30,6 +30,7 @@ actions: activate USERNAME sets USERNAME's status to active deactivate USERNAME sets USERNAME's status to inactive disable USERNAME sets USERNAME's status to disabled + lock USERNAME sets USERNAME's status to locked group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP `, diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 60d3888d..d1f21408 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -55,6 +55,14 @@ function setAccountStatus(user, status) { } const AccountStatus = require('../../core/user.js').AccountStatus; + + status = { + activate : AccountStatus.active, + deactivate : AccountStatus.inactive, + disable : AccountStatus.disabled, + lock : AccountStatus.locked, + }[status]; + const statusDesc = _.invert(AccountStatus)[status]; user.persistProperty('account_status', status, err => { if(err) { @@ -147,21 +155,6 @@ function modUserGroups(user) { } } -function activateUser(user) { - const AccountStatus = require('../../core/user.js').AccountStatus; - return setAccountStatus(user, AccountStatus.active); -} - -function deactivateUser(user) { - const AccountStatus = require('../../core/user.js').AccountStatus; - return setAccountStatus(user, AccountStatus.inactive); -} - -function disableUser(user) { - const AccountStatus = require('../../core/user.js').AccountStatus; - return setAccountStatus(user, AccountStatus.disabled); -} - function handleUserCommand() { function errUsage() { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); @@ -195,11 +188,12 @@ function handleUserCommand() { del : removeUser, delete : removeUser, - activate : activateUser, - deactivate : deactivateUser, - disable : disableUser, + activate : setAccountStatus, + deactivate : setAccountStatus, + disable : setAccountStatus, + lock : setAccountStatus, group : modUserGroups, - }[action] || errUsage)(user); + }[action] || errUsage)(user, action); }); } \ No newline at end of file diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 5f0ff05f..f626cb79 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -10,7 +10,10 @@ const userLogin = require('../../user_login.js').userLogin; const enigVersion = require('../../../package.json').version; const theme = require('../../theme.js'); const stringFormat = require('../../string_format.js'); -const { ErrorReasons } = require('../../enig_error.js'); +const { + Errors, + ErrorReasons +} = require('../../enig_error.js'); // deps const ssh2 = require('ssh2'); @@ -37,8 +40,6 @@ function SSHClient(clientConn) { const self = this; - let loginAttempts = 0; - clientConn.on('authentication', function authAttempt(ctx) { const username = ctx.username || ''; const password = ctx.password || ''; @@ -53,26 +54,56 @@ function SSHClient(clientConn) { return clientConn.end(); } - function alreadyLoggedIn(username) { - ctx.prompt(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); + function promptAndTerm(msg) { + if('keyboard-interactive' === ctx.method) { + ctx.prompt(msg); + } return terminateConnection(); } + function accountAlreadyLoggedIn(username) { + return promptAndTerm(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); + } + + function accountDisabled(username) { + return promptAndTerm(`${username} is disabled.\n(Press any key to continue)`); + } + + function accountInactive(username) { + return promptAndTerm(`${username} is waiting for +op activation.\n(Press any key to continue)`); + } + + function accountLocked(username) { + return promptAndTerm(`${username} is locked.\n(Press any key to continue)`); + } + + function isSpecialHandleError(err) { + return [ ErrorReasons.AlreadyLoggedIn, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked ].includes(err.reasonCode); + } + + function handleSpecialError(err, username) { + switch(err.reasonCode) { + case ErrorReasons.AlreadyLoggedIn : return accountAlreadyLoggedIn(username); + case ErrorReasons.Inactive : return accountInactive(username); + case ErrorReasons.Disabled : return accountDisabled(username); + case ErrorReasons.Locked : return accountLocked(username); + default : return terminateConnection(); + } + } + // // If the system is open and |isNewUser| is true, the login - // sequence is hijacked in order to start the applicaiton process. + // sequence is hijacked in order to start the application process. // if(false === config.general.closedSystem && self.isNewUser) { return ctx.accept(); } if(username.length > 0 && password.length > 0) { - loginAttempts += 1; - userLogin(self, ctx.username, ctx.password, function authResult(err) { if(err) { - if(ErrorReasons.AlreadyLoggedIn === err.reasonCode) { - return alreadyLoggedIn(username); + if(isSpecialHandleError(err)) { + return handleSpecialError(err, username); } return ctx.reject(SSHClient.ValidAuthMethods); @@ -93,15 +124,13 @@ function SSHClient(clientConn) { const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; ctx.prompt(interactivePrompt, function retryPrompt(answers) { - loginAttempts += 1; - userLogin(self, username, (answers[0] || ''), err => { if(err) { - if(ErrorReasons.AlreadyLoggedIn === err.reasonCode) { - return alreadyLoggedIn(username); + if(isSpecialHandleError(err)) { + return handleSpecialError(err, username); } - if(loginAttempts >= config.general.loginAttempts) { + if(Errors.BadLogin().code === err.code) { return terminateConnection(); } diff --git a/core/stat_log.js b/core/stat_log.js index 9e655999..4ee4c53b 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -68,6 +68,7 @@ class StatLog { }; } + // :TODO: fix spelling :) setNonPeristentSystemStat(statName, statValue) { this.systemStats[statName] = statValue; } diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 8485a90f..7c135f22 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -26,13 +26,21 @@ function login(callingMenu, formData, extraArgs, cb) { userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { if(err) { - // login failure + // already logged in with this user? if(ErrorReasons.AlreadyLoggedIn === err.reasonCode && _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) { return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); } + const ReasonsMenus = [ + ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked + ]; + if(ReasonsMenus.includes(err.reasonCode)) { + const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]); + return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb); + } + // Other error return callingMenu.prevMenu(cb); } diff --git a/core/tic_file_info.js b/core/tic_file_info.js index 5db24cc2..fd3c7572 100644 --- a/core/tic_file_info.js +++ b/core/tic_file_info.js @@ -187,10 +187,10 @@ module.exports = class TicFileInfo { // send the file to be distributed and the accompanying TIC file. // Some File processors (Allfix) only insert a line with this // keyword when the file and the associated TIC file are to be - // file routed through a third sysem instead of being processed + // file routed through a third system instead of being processed // by a file processor on that system. Others always insert it. // Note that the To keyword may cause problems when the TIC file - // is proecessed by software that does not recognise it and + // is processed by software that does not recognize it and // passes the line "as is" to other systems. // // Example: To 292/854 diff --git a/core/user.js b/core/user.js index 27f4775a..a712829c 100644 --- a/core/user.js +++ b/core/user.js @@ -1,11 +1,18 @@ /* jslint node: true */ 'use strict'; +// ENiGMA½ const userDb = require('./database.js').dbs.user; const Config = require('./config.js').get; const userGroup = require('./user_group.js'); -const Errors = require('./enig_error.js').Errors; +const { + Errors, + ErrorReasons +} = require('./enig_error.js'); const Events = require('./events.js'); +const UserProps = require('./user_property.js'); +const Log = require('./logger.js').log; +const StatLog = require('./stat_log.js'); // deps const crypto = require('crypto'); @@ -39,7 +46,7 @@ module.exports = class User { static get StandardPropertyGroups() { return { - password : [ 'pw_pbkdf2_salt', 'pw_pbkdf2_dk' ], + password : [ UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk ], }; } @@ -52,6 +59,18 @@ module.exports = class User { }; } + static isSamePasswordSlowCompare(passBuf1, passBuf2) { + if(passBuf1.length !== passBuf2.length) { + return false; + } + + let c = 0; + for(let i = 0; i < passBuf1.length; i++) { + c |= passBuf1[i] ^ passBuf2[i]; + } + return 0 === c; + } + isAuthenticated() { return true === this.authenticated; } @@ -61,16 +80,21 @@ module.exports = class User { return false; } - return this.hasValidPassword(); + return this.hasValidPasswordProperties(); } - hasValidPassword() { - if(!this.properties || !this.properties.pw_pbkdf2_salt || !this.properties.pw_pbkdf2_dk) { + hasValidPasswordProperties() { + const salt = this.getProperty(UserProps.PassPbkdf2Salt); + const dk = this.getProperty(UserProps.PassPbkdf2Dk); + + if(!salt || !dk || + (salt.length !== User.PBKDF2.saltLen * 2) || + (dk.length !== User.PBKDF2.keyLen * 2)) + { return false; } - return ((this.properties.pw_pbkdf2_salt.length === User.PBKDF2.saltLen * 2) && - (this.properties.pw_pbkdf2_dk.length === User.PBKDF2.keyLen * 2)); + return true; } isRoot() { @@ -102,24 +126,77 @@ module.exports = class User { return 10; // :TODO: Is this what we want? } + processFailedLogin(userId, cb) { + async.waterfall( + [ + (callback) => { + return User.getUser(userId, callback); + }, + (tempUser, callback) => { + return StatLog.incrementUserStat( + tempUser, + UserProps.FailedLoginAttempts, + 1, + (err, failedAttempts) => { + return callback(null, tempUser, failedAttempts); + } + ); + }, + (tempUser, failedAttempts, callback) => { + const lockAccount = _.get(Config(), 'users.failedLogin.lockAccount'); + if(lockAccount > 0 && failedAttempts >= lockAccount) { + const props = { + [ UserProps.AccountStatus ] : User.AccountStatus.locked, + [ UserProps.AccountLockedTs ] : StatLog.now, + }; + if(!_.has(tempUser.properties, UserProps.AccountLockedPrevStatus)) { + props[UserProps.AccountLockedPrevStatus] = tempUser.getProperty(UserProps.AccountStatus); + } + return tempUser.persistProperties(props, callback); + } + + return cb(null); + } + ], + err => { + return cb(err); + } + ); + } + + unlockAccount(cb) { + const prevStatus = this.getProperty(UserProps.AccountLockedPrevStatus); + if(!prevStatus) { + return cb(null); // nothing to do + } + + this.persistProperty(UserProps.AccountStatus, prevStatus, err => { + if(err) { + return cb(err); + } + + return this.removeProperties( [ UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs ], cb); + }); + } + authenticate(username, password, cb) { const self = this; - const cachedInfo = {}; + const tempAuthInfo = {}; async.waterfall( [ function fetchUserId(callback) { // get user ID User.getUserIdAndName(username, (err, uid, un) => { - cachedInfo.userId = uid; - cachedInfo.username = un; + tempAuthInfo.userId = uid; + tempAuthInfo.username = un; return callback(err); }); }, function getRequiredAuthProperties(callback) { // fetch properties required for authentication - User.loadProperties(cachedInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { + User.loadProperties(tempAuthInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { return callback(err, props); }); }, @@ -136,30 +213,53 @@ module.exports = class User { const passDkBuf = Buffer.from(passDk, 'hex'); const propsDkBuf = Buffer.from(propsDk, 'hex'); - if(passDkBuf.length !== propsDkBuf.length) { - return callback(Errors.AccessDenied('Invalid password')); - } - - let c = 0; - for(let i = 0; i < passDkBuf.length; i++) { - c |= passDkBuf[i] ^ propsDkBuf[i]; - } - - return callback(0 === c ? null : Errors.AccessDenied('Invalid password')); + return callback(User.isSamePasswordSlowCompare(passDkBuf, propsDkBuf) ? + null : + Errors.AccessDenied('Invalid password') + ); }, function initProps(callback) { - User.loadProperties(cachedInfo.userId, (err, allProps) => { + User.loadProperties(tempAuthInfo.userId, (err, allProps) => { if(!err) { - cachedInfo.properties = allProps; + tempAuthInfo.properties = allProps; } return callback(err); }); }, + function checkAccountStatus(callback) { + const accountStatus = parseInt(tempAuthInfo.properties[UserProps.AccountStatus], 10); + if(User.AccountStatus.disabled === accountStatus) { + return callback(Errors.AccessDenied('Account disabled', ErrorReasons.Disabled)); + } + if(User.AccountStatus.inactive === accountStatus) { + return callback(Errors.AccessDenied('Account inactive', ErrorReasons.Inactive)); + } + + if(User.AccountStatus.locked === accountStatus) { + const autoUnlockMinutes = _.get(Config(), 'users.failedLogin.autoUnlockMinutes'); + const lockedTs = moment(tempAuthInfo.properties[UserProps.AccountLockedTs]); + if(autoUnlockMinutes && lockedTs.isValid()) { + const minutesSinceLocked = moment().diff(lockedTs, 'minutes'); + if(minutesSinceLocked >= autoUnlockMinutes) { + // allow the login - we will clear any lock there + return callback(null); + } + } + return callback(Errors.AccessDenied('Account is locked', ErrorReasons.Locked)); + } + + // anything else besides active is still not allowed + if(User.AccountStatus.active !== accountStatus) { + return callback(Errors.AccessDenied('Account is not active')); + } + + return callback(null); + }, function initGroups(callback) { - userGroup.getGroupsForUser(cachedInfo.userId, (err, groups) => { + userGroup.getGroupsForUser(tempAuthInfo.userId, (err, groups) => { if(!err) { - cachedInfo.groups = groups; + tempAuthInfo.groups = groups; } return callback(err); @@ -167,15 +267,44 @@ module.exports = class User { } ], err => { - if(!err) { - self.userId = cachedInfo.userId; - self.username = cachedInfo.username; - self.properties = cachedInfo.properties; - self.groups = cachedInfo.groups; + if(err) { + // + // If we failed login due to something besides an inactive or disabled account, + // we need to update failure status and possibly lock the account. + // + // If locked already, update the lock timestamp -- ie, extend the lockout period. + // + if(![ErrorReasons.Disabled, ErrorReasons.Inactive].includes(err.reasonCode) && tempAuthInfo.userId) { + self.processFailedLogin(tempAuthInfo.userId, persistErr => { + if(persistErr) { + Log.warn( { error : persistErr.message }, 'Failed to persist failed login information'); + } + return cb(err); // pass along original error + }); + } else { + return cb(err); + } + } else { + // everything checks out - load up info + self.userId = tempAuthInfo.userId; + self.username = tempAuthInfo.username; + self.properties = tempAuthInfo.properties; + self.groups = tempAuthInfo.groups; self.authenticated = true; - } - return cb(err); + self.removeProperty(UserProps.FailedLoginAttempts); + + // + // We need to *revert* any locked status back to + // the user's previous status & clean up props. + // + self.unlockAccount(unlockErr => { + if(unlockErr) { + Log.warn( { error : unlockErr.message }, 'Failed to unlock account'); + } + return cb(null); + }); + } } ); } @@ -191,7 +320,7 @@ module.exports = class User { const self = this; // :TODO: set various defaults, e.g. default activation status, etc. - self.properties.account_status = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; + self.properties[UserProps.AccountStatus] = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; async.waterfall( [ @@ -212,7 +341,7 @@ module.exports = class User { // Do not require activation for userId 1 (root/admin) if(User.RootUserID === self.userId) { - self.properties.account_status = User.AccountStatus.active; + self.properties[UserProps.AccountStatus] = User.AccountStatus.active; } return callback(null, trans); @@ -225,8 +354,8 @@ module.exports = class User { return callback(err); } - self.properties.pw_pbkdf2_salt = info.salt; - self.properties.pw_pbkdf2_dk = info.dk; + self.properties[UserProps.PassPbkdf2Salt] = info.salt; + self.properties[UserProps.PassPbkdf2Dk] = info.dk; return callback(null, trans); }); }, @@ -290,20 +419,32 @@ module.exports = class User { ); } + static persistPropertyByUserId(userId, propName, propValue, cb) { + userDb.run( + `REPLACE INTO user_property (user_id, prop_name, prop_value) + VALUES (?, ?, ?);`, + [ userId, propName, propValue ], + err => { + if(cb) { + return cb(err, propValue); + } + } + ); + } + + getProperty(propName) { + return this.properties[propName]; + } + + getPropertyAsNumber(propName) { + return parseInt(this.getProperty(propName), 10); + } + persistProperty(propName, propValue, cb) { // update live props this.properties[propName] = propValue; - userDb.run( - `REPLACE INTO user_property (user_id, prop_name, prop_value) - VALUES (?, ?, ?);`, - [ this.userId, propName, propValue ], - err => { - if(cb) { - return cb(err); - } - } - ); + return User.persistPropertyByUserId(this.userId, propName, propValue, cb); } removeProperty(propName, cb) { @@ -322,6 +463,15 @@ module.exports = class User { ); } + removeProperties(propNames, cb) { + async.each(propNames, (name, next) => { + return this.removeProperty(name, next); + }, + err => { + return cb(err); + }); + } + persistProperties(properties, transOrDb, cb) { if(!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; @@ -372,8 +522,9 @@ module.exports = class User { } getAge() { - if(_.has(this.properties, 'birthdate')) { - return moment().diff(this.properties.birthdate, 'years'); + const birthdate = this.getProperty(UserProps.Birthdate); + if(birthdate) { + return moment().diff(birthdate, 'years'); } } diff --git a/core/user_login.js b/core/user_login.js index d08788f3..be2a99a1 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -15,20 +15,30 @@ const { // deps const async = require('async'); +const _ = require('lodash'); exports.userLogin = userLogin; function userLogin(client, username, password, cb) { - client.user.authenticate(username, password, function authenticated(err) { + client.user.authenticate(username, password, err => { + const config = Config(); + if(err) { client.log.info( { username : username, error : err.message }, 'Failed login attempt'); - // :TODO: if username exists, record failed login attempt to properties - // :TODO: check Config max failed logon attempts/etc. - set err.maxAttempts = true + client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1; + const disconnect = config.users.failedLogin.disconnect; + if(disconnect > 0 && client.user.sessionFailedLoginAttempts >= disconnect) { + return cb(Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany)); + } return cb(err); } - const user = client.user; + + const user = client.user; + + // Good login; reset any failed attempts + delete user.sessionFailedLoginAttempts; // // Ensure this user is not already logged in. diff --git a/core/user_property.js b/core/user_property.js new file mode 100644 index 00000000..04f8f0c9 --- /dev/null +++ b/core/user_property.js @@ -0,0 +1,25 @@ +/* jslint node: true */ +'use strict'; + +// +// Common user properties used throughout the system. +// +// This IS NOT a full list. For example, custom modules +// can utilize their own properties as well! +// +module.exports = { + PassPbkdf2Salt : 'pw_pbkdf2_salt', + PassPbkdf2Dk : 'pw_pbkdf2_dk', + + AccountStatus : 'account_status', + + Birthdate : 'birthdate', + + FailedLoginAttempts : 'failed_login_attempts', + AccountLockedTs : 'account_locked_timestamp', + AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status + + EmailPwResetToken : 'email_password_reset_token', + EmailPwResetTokenTs : 'email_password_reset_token_ts', +}; + diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 6ca916da..06fd7838 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -10,6 +10,7 @@ const User = require('./user.js'); const userDb = require('./database.js').dbs.user; const getISOTimestampString = require('./database.js').getISOTimestampString; const Log = require('./logger.js').log; +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -17,6 +18,7 @@ const crypto = require('crypto'); const fs = require('graceful-fs'); const url = require('url'); const querystring = require('querystring'); +const _ = require('lodash'); const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = `%USERNAME%: @@ -283,8 +285,11 @@ class WebPasswordReset { } // delete assoc properties - no need to wait for completion - user.removeProperty('email_password_reset_token'); - user.removeProperty('email_password_reset_token_ts'); + user.removeProperties([ UserProps.EmailPwResetToken, UserProps.EmailPwResetTokenTs ]); + + if(true === _.get(config, 'users.unlockAtEmailPwReset')) { + user.unlockAccount( () => { /* dummy */ } ); + } resp.writeHead(200); return resp.end('Password changed successfully'); diff --git a/docs/configuration/email.md b/docs/configuration/email.md index 5bc4d4c8..eb13ef71 100644 --- a/docs/configuration/email.md +++ b/docs/configuration/email.md @@ -2,16 +2,18 @@ layout: page title: Email --- -ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid SMTP -config in your [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}) +## Email Support +ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible. -## SMTP Services +Additional email support will come in the near future. -If you don't have an SMTP server to send from, [Sendgrid](https://sendgrid.com/) provide a reliable free -service. +## Services -## Example SMTP Configuration +If you don't have an SMTP server to send from, [Sendgrid](https://sendgrid.com/) and [Zoho](https://www.zoho.com/mail/) both provide reliable and free services. +## Example Configurations + +Example 1 - SMTP: ```hjson email: { defaultFrom: sysop@bbs.awesome.com @@ -27,3 +29,21 @@ email: { } } ``` + +Example 2 - Zoho +```hjson +email: { + defaultFrom: sysop@bbs.awesome.com + + transport: { + service: Zoho + auth: { + user: noreply@bbs.awesome.com + pass: yuspymypass + } + } +} +``` + +## Lockout Reset +If email is available on your system and you allow email-driven password resets, you may elect to allow unlocking accounts at the time of a password reset. This is controlled by the `users.unlockAtEmailPwReset` configuration option. If an account is locked due to too many failed login attempts, a user may reset their password to remedy the situation themselves. diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index ab1b37b8..489a7cb3 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -352,6 +352,23 @@ // Usernames reserved for applying to your system newUserNames: [] + // Handling of failed logins + failedLogin : { + // disconnect after N failed attempts. 0=disabled. + disconnect : XXXXX + + // Lock the user out after N failed attempts. 0=disabled. + lockAccount : XXXXX + + // + // If locked out, how long until the user can login again? + // Set to 0 to disable auto-unlock + // + autoUnlockMinutes : XXXXX + }, + + // Allow email driven password resets to unlock accounts? + unlockAtEmailPwReset : XXXXX } // Archive files and related diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 7a0fbcfa..6e1d889d 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -145,6 +145,9 @@ next: fullLoginSequenceLoginArt config: { tooNodeMenu: loginAttemptTooNode + inactive: loginAttemptAccountInactive + disabled: loginAttemptAccountDisabled + locked: loginAttemptAccountLocked } form: { 0: { @@ -188,6 +191,33 @@ next: logoff } + loginAttemptAccountLocked: { + art: ACCOUNTLOCKED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountDisabled: { + art: ACCOUNTDISABLED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountInactive: { + art: ACCOUNTINACTIVE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + forgotPassword: { desc: Forgot password prompt: forgotPasswordPrompt From f0e7b46a2f548893e44f166226c5a606ebb1f5c9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 11:05:51 -0700 Subject: [PATCH 375/569] Moarrrrrr doc updates --- docs/_includes/nav.md | 2 -- docs/admin/oputil.md | 14 +++++----- docs/configuration/config-hjson.md | 15 +++++++++-- docs/configuration/sysop-setup.md | 3 +-- docs/installation/manual.md | 35 ++++++++++++------------- docs/installation/os-hardware.md | 15 ++++++----- docs/installation/windows.md | 6 +++-- docs/oputil/index.md | 17 ------------ docs/troubleshooting/monitoring-logs.md | 17 ++++++++---- 9 files changed, 64 insertions(+), 60 deletions(-) delete mode 100644 docs/oputil/index.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 728e5444..84785dbe 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -82,8 +82,6 @@ - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) - - - [Oputil]({{ site.baseurl }}{% link oputil/index.md %}) - Troubleshooting - [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 8fbf0a46..9db1439c 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -27,7 +27,7 @@ Commands break up operations by groups: | Command | Description | |-----------|---------------| | `user` | User management | -| `config` | System configuration and maintentance | +| `config` | System configuration and maintenance | | `fb` | File base configuration and management | | `mb` | Message base configuration and management | @@ -45,11 +45,12 @@ usage: optutil.js user [] actions: pw USERNAME PASSWORD set password to PASSWORD for USERNAME - rm USERNAME permanantely removes USERNAME user from system + rm USERNAME permanently removes USERNAME user from system activate USERNAME sets USERNAME's status to active - deactivate USERNAME sets USERNAME's status to deactive + deactivate USERNAME sets USERNAME's status to inactive disable USERNAME sets USERNAME's status to disabled - group USERNAME [+|-]GROUP adds (+) or removes (-) USERNAME from GROUP + lock USERNAME sets USERNAME's status to locked + group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP ``` | Action | Description | Examples | Aliases | @@ -59,6 +60,7 @@ actions: | `activate` | Activates user | `./oputil.js user activate joeuser` | N/A | | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | | `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | +| `lock` | Locks the user account (prevents logins) | `./oputil.js user lock joeuser` | N/A | | `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser -derp` | N/A | ## Configuration @@ -82,7 +84,7 @@ import-areas args: | Action | Description | Examples | |-----------|-------------------|---------------------------------------| | `new` | Generates a new/initial configuration | `./oputil.js config new` (follow the prompts) | -| `import-areas` | Imports areas using a Fidonet style *.NA or AREAS.BBS formatted file | `./oputil.js config import-areas /some/path/l33tnet.na` | +| `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file | `./oputil.js config import-areas /some/path/l33tnet.na` | When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args". @@ -138,7 +140,7 @@ general information: The `scan` action can (re)scan a file area for new entries as well as update (`--update`) existing entry records (description, etc.). When scanning, a valid area tag must be specified. Optionally, storage tag may also be supplied in order to scan a specific filesystem location using the `@the_storage_tag` syntax. If a [GLOB](http://man7.org/linux/man-pages/man7/glob.7.html) is supplied as the last argument, only file entries with filenames matching will be processed. ##### Examples -Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extentions: +Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extensions: ``` $ ./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip` ``` diff --git a/docs/configuration/config-hjson.md b/docs/configuration/config-hjson.md index 4ebfd030..7aae987f 100644 --- a/docs/configuration/config-hjson.md +++ b/docs/configuration/config-hjson.md @@ -8,7 +8,7 @@ The main system configuration file, `config.hjson` both overrides defaults and p See also [HJSON General Information](hjson.md) for more information on the HJSON format. ### Creating a Configuration -Your initial configuration skeleton can be created using the `oputil.js` command line utility. From your enigma-bbs root directory: +Your initial configuration skeleton should be created using the `oputil.js` command line utility. From your enigma-bbs root directory: ``` ./oputil.js config new ``` @@ -30,10 +30,21 @@ general: { } ``` -(Note the very slightly different syntax. **You can use standard JSON if you wish**) +(Note the very slightly [HJSON](hjson.md) different syntax. **You can use standard JSON if you wish!**) While not everything that is available in your `config.hjson` file can be found defaulted in `core/config.js`, a lot is. [Poke around and see what you can find](https://github.com/NuSkooler/enigma-bbs/blob/master/core/config.js)! +### Configuration Sections +Below is a list of various configuration sections. There are many more, but this should get you started: + +* [ACS](acs.md) +* [Archivers](archivers.md): Set up external archive utilities for handling things like ZIP, ARJ, RAR, and so on. +* [Email](email.md): System email support. +* [Event Scheduler](event-scheduler.md): Set up events as you see fit! +* [File Base](/docs/filebase/index.md) +* [File Transfer Protocols](file-transfer-protocols.md): Oldschool file transfer protocols such as X/Y/Z-Modem! +* [Message Areas](/docs/messageareas/configuring-a-message-area.md), [Networks](/docs/messageareas/message-networks.md), [NetMail](/docs/messageareas/netmail.md), etc. + ### A Sample Configuration Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked. diff --git a/docs/configuration/sysop-setup.md b/docs/configuration/sysop-setup.md index b8c4beb6..502f412e 100644 --- a/docs/configuration/sysop-setup.md +++ b/docs/configuration/sysop-setup.md @@ -2,5 +2,4 @@ layout: page title: SysOp Setup --- -SySop privileges will be granted to the first user to log into a fresh ENiGMA½ installation. - +SySop privileges will be granted to the first user to log into a fresh ENiGMA½ installation. +ops belong to the `sysop` user group by default. \ No newline at end of file diff --git a/docs/installation/manual.md b/docs/installation/manual.md index 01c5ca47..a24cd3eb 100644 --- a/docs/installation/manual.md +++ b/docs/installation/manual.md @@ -19,16 +19,21 @@ are OK) for Windows users. Note that you **should only need the Visual C++ compo * [git](https://git-scm.com/downloads) to check out the ENiGMA source code. ## Node.js -If you're new to Node.js and/or do not care about Node itself and just want to get ENiGMA½ running -these steps should get you going on most \*nix type environments: +### With NVM +Node Version Manager (NVM) is an excellent way to install and manage Node.js versions on most UNIX-like environments. [Get the latest version here](https://github.com/creationix/nvm). The install should look something like this: ```bash -curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash -nvm install 6 -nvm use 6 +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash ``` -If the above completed without errors, you should now have `nvm`, `node`, and `npm` installed and in your environment. +Next, install Node.js with NVM: +```bash +nvm install 10 +nvm use 10 +nvm alias default 10 +``` + +If the above steps completed without errors, you should now have `nvm`, `node`, and `npm` installed and in your environment. For Windows nvm-like systems exist ([nvm-windows](https://github.com/coreybutler/nvm-windows), ...) or [just download the installer](https://nodejs.org/en/download/). @@ -41,31 +46,25 @@ git clone https://github.com/NuSkooler/enigma-bbs.git ## Install Node Packages ```bash cd enigma-bbs -npm install +npm install # yarn also works ``` ## Other Recommended Packages +ENiGMA BBS makes use of a few packages for archive and legacy protocol support. They're not pre-requisites for running ENiGMA, but without them you'll miss certain functionality. Once installed, they should be made available on your system path. -ENiGMA BBS makes use of a few packages for unarchiving and modem support. They're not pre-requisites for -running ENiGMA, but without them you'll miss certain functionality. Once installed, they should be made -available on your system path. - -| Package | Description | Debian/Ubuntu Package (APT/DEP) | Red Hat Package (YUM/RPM) | Windows Package | +| Package | Description | Debian/Ubuntu Package (APT/DEP) | Red Hat Package (YUM/RPM) | Windows Package | |------------|-----------------------------------|--------------------------------------------|---------------------------------------------------|------------------------------------------------------------------| | arj | Unpacking arj archives | `arj` | n/a, binaries [here](http://arj.sourceforge.net/) | [ARJ](http://arj.sourceforge.net/) | | 7zip | Unpacking zip, rar, archives | `p7zip-full` | `p7zip-full` | [7-zip](http://www.7-zip.org/) | | lha | Unpacking lha archives | `lhasa` | n/a, source [here](http://www2m.biglobe.ne.jp/~dolphin/lha/lha.htm) | Unknown | | Rar | Unpacking rar archives | `unrar` | n/a, binaries [here](https://www.rarlab.com/download.htm) | Unknown | -| lrzsz | sz/rz: X/Y/Z modem support | `lrzsz` | `lrzsz` | Unknown | -| sexyz | SexyZ modem support | [sexyz](https://l33t.codes/outgoing/sexyz) | [sexyz](https://l33t.codes/outgoing/sexyz) | Available with [Synchronet](http://wiki.synchro.net/install:win) | +| lrzsz | sz/rz: X/Y/Z protocol support | `lrzsz` | `lrzsz` | Unknown | +| sexyz | SexyZ protocol support | [sexyz](https://l33t.codes/outgoing/sexyz) | [sexyz](https://l33t.codes/outgoing/sexyz) | Available with [Synchronet](http://wiki.synchro.net/install:win) | | exiftool | [ExifTool](https://www.sno.phy.queensu.ca/~phil/exiftool/) | libimage-exiftool-perl | perl-Image-ExifTool | Unknown | xdms | Unpack/view Amiga DMS | [xdms](http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html) | xdms | Unknown ## Config Files - -You'll need a basic configuration to get started. The main system configuration is handled via -`config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compiliant JSON is also OK). -See [Configuration](../configuration/) for more information. +You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compiliant JSON is also OK). See [Configuration](../configuration/) for more information. Use `oputil.js` to generate your **initial** configuration: diff --git a/docs/installation/os-hardware.md b/docs/installation/os-hardware.md index a49283b4..6332ee82 100644 --- a/docs/installation/os-hardware.md +++ b/docs/installation/os-hardware.md @@ -2,10 +2,13 @@ layout: page title: OS & Hardware Specific Information --- -There are multiple ways of installing ENiGMA BBS, depending on your level of experience and desire to do -things manually versus have it automated for you. +There are multiple ways of installing ENiGMA BBS, depending on your level of experience and desire to do things manually versus have it automated for you. -| Method | Notes | -|----------------------------------------|---------------------------------------------------------------------------------------------| -| [Raspberry Pi](rpi) | All Raspberry Pi models work great with ENiGMA½! | -| [Windows](windows) | Compatible with all Windows Operating Systems | +In general, please see [Installation Methods](installation-methods.md) and [Install Script](install-script.md). + +Below are some special cases: + +| Method | Notes | +|--------|-------| +| [Raspberry Pi](rpi.md) | All Raspberry Pi models work great with ENiGMA½! | +| [Windows](windows.md) | Compatible with all Windows Operating Systems | diff --git a/docs/installation/windows.md b/docs/installation/windows.md index 4eaed906..a68afe97 100644 --- a/docs/installation/windows.md +++ b/docs/installation/windows.md @@ -36,11 +36,13 @@ ENiGMA½ will run on both 32bit and 64bit Windows. If you want to run 16bit door *Add 7zip to your path so `7z` can be called from the console 1. Right click `This PC` and Select `Properties` - 2. Go to the `Advanced` Tab and click on `Enviromental Varibles` - 3. Select `Path` under `System Varibles` and click `Edit` + 2. Go to the `Advanced` Tab and click on `Environment Variables` + 3. Select `Path` under `System Variables` and click `Edit` 4. Click `New` and paste the path to 7zip 5. Close your console window and reopen. You can type `7z` to make sure it's working. +(Please see [Archivers](/docs/archivers.md) for additional archive utilities!) + 3. Install [Git](https://git-scm.com/downloads) and optionally [TortoiseGit](https://tortoisegit.org/download/). 4. Clone ENiGMA½ - browse to the directory you want and run diff --git a/docs/oputil/index.md b/docs/oputil/index.md deleted file mode 100644 index b23a7362..00000000 --- a/docs/oputil/index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -layout: page -title: oputil ---- - -oputil is the ENiGMA½ command line utility for maintaining users, file areas and message areas, as well as -generating your initial ENiGMA½ config. - -## File areas -The `oputil.js` +op utilty `fb` command has tools for managing file bases. For example, to import existing -files found within **all** storage locations tied to an area and set tags `tag1` and `tag2` to each import: - -```bash -oputil.js fb scan some_area --tags tag1,tag2 -``` - -See `oputil.js fb --help` for additional information. \ No newline at end of file diff --git a/docs/troubleshooting/monitoring-logs.md b/docs/troubleshooting/monitoring-logs.md index 7c04cb40..dd665f5c 100644 --- a/docs/troubleshooting/monitoring-logs.md +++ b/docs/troubleshooting/monitoring-logs.md @@ -2,14 +2,21 @@ layout: page title: Monitoring Logs --- -ENiGMA½ does not produce much to stdout. Logs are produced by Bunyan which outputs each entry as a -JSON object. +ENiGMA½ does not produce much to stdout. Logs are produced by Bunyan which outputs each entry as a JSON object. Start by installing bunyan and making it available on your path: - npm install bunyan -g +```bash +npm install bunyan -g +``` + +or with Yarn: +```bash +yarn global add bunyan +``` To tail logs in a colorized and pretty format, issue the following command: - - tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan +```bash +tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan +``` From a4823c0c4a8bc9c93f58f4cc99bd433ac94798f0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 11:44:46 -0700 Subject: [PATCH 376/569] Logging around accoung lock/unlocking --- core/user.js | 5 +++++ core/user_login.js | 5 ++--- core/web_password_reset.js | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/user.js b/core/user.js index a712829c..609a9e43 100644 --- a/core/user.js +++ b/core/user.js @@ -152,6 +152,7 @@ module.exports = class User { if(!_.has(tempUser.properties, UserProps.AccountLockedPrevStatus)) { props[UserProps.AccountLockedPrevStatus] = tempUser.getProperty(UserProps.AccountStatus); } + Log.info( { userId, failedAttempts }, '(Re)setting account to locked due to failed logins'); return tempUser.persistProperties(props, callback); } @@ -243,6 +244,10 @@ module.exports = class User { const minutesSinceLocked = moment().diff(lockedTs, 'minutes'); if(minutesSinceLocked >= autoUnlockMinutes) { // allow the login - we will clear any lock there + Log.info( + { username, userId : tempAuthInfo.userId, lockedAt : lockedTs.format() }, + 'Locked account will now be unlocked due to auto-unlock minutes policy' + ); return callback(null); } } diff --git a/core/user_login.js b/core/user_login.js index be2a99a1..fa9ae676 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -24,14 +24,13 @@ function userLogin(client, username, password, cb) { const config = Config(); if(err) { - client.log.info( { username : username, error : err.message }, 'Failed login attempt'); - client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1; const disconnect = config.users.failedLogin.disconnect; if(disconnect > 0 && client.user.sessionFailedLoginAttempts >= disconnect) { - return cb(Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany)); + err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany); } + client.log.info( { username : username, error : err.message }, 'Failed login attempt'); return cb(err); } diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 06fd7838..ceefe9c5 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -288,6 +288,10 @@ class WebPasswordReset { user.removeProperties([ UserProps.EmailPwResetToken, UserProps.EmailPwResetTokenTs ]); if(true === _.get(config, 'users.unlockAtEmailPwReset')) { + Log.info( + { username : user.username, userId : user.userId }, + 'Remove any lock on account due to password reset policy' + ); user.unlockAccount( () => { /* dummy */ } ); } From f45e785da12aa482d3d0a18d650ed742419f5dc9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 12:02:41 -0700 Subject: [PATCH 377/569] oputil.js user activate will now unlock accounts --- core/oputil/oputil_user.js | 49 ++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index d1f21408..18519b06 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -8,25 +8,13 @@ const argv = require('./oputil_common.js').argv; const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; const getHelpFor = require('./oputil_help.js').getHelpFor; const Errors = require('../enig_error.js').Errors; +const UserProps = require('../user_property.js'); const async = require('async'); const _ = require('lodash'); exports.handleUserCommand = handleUserCommand; -function getUser(userName, cb) { - const User = require('../../core/user.js'); - User.getUserIdAndName(userName, (err, userId) => { - if(err) { - process.exitCode = ExitCodes.BAD_ARGS; - return cb(err); - } - const u = new User(); - u.userId = userId; - return cb(null, u); - }); -} - function initAndGetUser(userName, cb) { async.waterfall( [ @@ -34,12 +22,12 @@ function initAndGetUser(userName, cb) { initConfigAndDatabases(callback); }, function getUserObject(callback) { - getUser(userName, (err, user) => { + const User = require('../../core/user.js'); + User.getUserIdAndName(userName, (err, userId) => { if(err) { - process.exitCode = ExitCodes.BAD_ARGS; return callback(err); } - return callback(null, user); + return User.getUser(userId, callback); }); } ], @@ -64,14 +52,29 @@ function setAccountStatus(user, status) { }[status]; const statusDesc = _.invert(AccountStatus)[status]; - user.persistProperty('account_status', status, err => { - if(err) { - process.exitCode = ExitCodes.ERROR; - console.error(err.message); - } else { - console.info(`User status set to ${statusDesc}`); + + async.series( + [ + (callback) => { + return user.persistProperty(UserProps.AccountStatus, status, callback); + }, + (callback) => { + if(AccountStatus.active !== status) { + return callback(null); + } + + return user.unlockAccount(callback); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } else { + console.info(`User status set to ${statusDesc}`); + } } - }); + ); } function setUserPassword(user) { From 4bd340a48032ee8d33dfceb4f9ed7a24241bf369 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 13:51:24 -0700 Subject: [PATCH 378/569] Add notes to WHATSNEW --- WHATSNEW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 90b9e374..191f1c7b 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -21,7 +21,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md) * `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected. * `oputil.js config new` will now generate a much more complete configuration file with comments, examples, etc. `oputil.js config cat` dumps your current config to stdout. - +* Handling of failed login attempts is now fully in. Disconnect clients, lock out accounts, ability to auto or unlock at (email-driven) password reset, etc. See `users.failedLogin` in `config.hjson`. ## 0.0.8-alpha From b82c6400140b515b166c9af84698e63101b33f54 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 14:47:18 -0700 Subject: [PATCH 379/569] Work on using UserProps, fix up ISO timestamps, etc. --- core/database.js | 8 +++++++- core/file_base_area.js | 5 +++-- core/file_base_filter.js | 12 +++++++----- core/message_area.js | 9 +++++---- core/misc_util.js | 4 +++- core/nua.js | 35 ++++++++++++++++++++--------------- core/stat_log.js | 8 ++++++-- core/user_config.js | 24 +++++++++++++++--------- core/user_property.js | 36 +++++++++++++++++++++++++++--------- core/web_password_reset.js | 4 ++-- 10 files changed, 95 insertions(+), 50 deletions(-) diff --git a/core/database.js b/core/database.js index 8bae6a87..017aa949 100644 --- a/core/database.js +++ b/core/database.js @@ -67,7 +67,13 @@ function loadDatabaseForMod(modInfo, cb) { function getISOTimestampString(ts) { ts = ts || moment(); - return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + if(!moment.isMoment(ts)) { + if(_.isString(ts)) { + ts = ts.replace(/\//g, '-'); + } + ts = moment(ts); + } + return ts.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); } function sanatizeString(s) { diff --git a/core/file_base_area.js b/core/file_base_area.js index a684f575..760eb87c 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -14,6 +14,7 @@ const resolveMimeType = require('./mime_util.js').resolveMimeType; const stringFormat = require('./string_format.js'); const wordWrapText = require('./word_wrap.js').wordWrapText; const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const _ = require('lodash'); @@ -136,11 +137,11 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) { }, function changeArea(area, callback) { if(true === options.persist) { - client.user.persistProperty('file_area_tag', areaTag, err => { + client.user.persistProperty(UserProps.FileAreaTag, areaTag, err => { return callback(err, area); }); } else { - client.user.properties['file_area_tag'] = areaTag; + client.user.properties[UserProps.FileAreaTag] = areaTag; return callback(null, area); } } diff --git a/core/file_base_filter.js b/core/file_base_filter.js index 82a75986..0ff19b94 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -1,9 +1,11 @@ /* jslint node: true */ 'use strict'; +const UserProps = require('./user_property.js'); + // deps -const _ = require('lodash'); -const uuidV4 = require('uuid/v4'); +const _ = require('lodash'); +const uuidV4 = require('uuid/v4'); module.exports = class FileBaseFilters { constructor(client) { @@ -90,7 +92,7 @@ module.exports = class FileBaseFilters { } persist(cb) { - return this.client.user.persistProperty('file_base_filters', JSON.stringify(this.filters), cb); + return this.client.user.persistProperty(UserProps.FileBaseFilters, JSON.stringify(this.filters), cb); } cleanTags(tags) { @@ -102,7 +104,7 @@ module.exports = class FileBaseFilters { if(activeFilter) { this.activeFilter = activeFilter; - this.client.user.persistProperty('file_base_filter_active_uuid', filterUuid); + this.client.user.persistProperty(UserProps.FileBaseFilterActiveUuid, filterUuid); return true; } @@ -150,6 +152,6 @@ module.exports = class FileBaseFilters { return; } - return user.persistProperty('user_file_base_last_viewed', fileId, cb); + return user.persistProperty(UserProps.FileBaseLastViewedId, fileId, cb); } }; diff --git a/core/message_area.js b/core/message_area.js index 98647cfd..ea6b4a87 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -8,6 +8,7 @@ const Message = require('./message.js'); const Log = require('./logger.js').log; const msgNetRecord = require('./msg_network.js').recordMessage; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -222,8 +223,8 @@ function changeMessageConference(client, confTag, cb) { }, function changeConferenceAndArea(conf, areaInfo, callback) { const newProps = { - message_conf_tag : confTag, - message_area_tag : areaInfo.areaTag, + [ UserProps.MessageConfTag ] : confTag, + [ UserProps.MessageAreaTag ] : areaInfo.areaTag, }; client.user.persistProperties(newProps, err => { callback(err, conf, areaInfo); @@ -262,11 +263,11 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { }, function changeArea(area, callback) { if(true === options.persist) { - client.user.persistProperty('message_area_tag', areaTag, function persisted(err) { + client.user.persistProperty(UserProps.MessageAreaTag, areaTag, function persisted(err) { return callback(err, area); }); } else { - client.user.properties['message_area_tag'] = areaTag; + client.user.properties[UserProps.MessageAreaTag] = areaTag; return callback(null, area); } } diff --git a/core/misc_util.js b/core/misc_util.js index 6e477821..62a3967d 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -4,6 +4,8 @@ const paths = require('path'); const os = require('os'); +const moment = require('moment'); + const packageJson = require('../package.json'); exports.isProduction = isProduction; @@ -57,4 +59,4 @@ function valueAsArray(value) { return []; } return Array.isArray(value) ? value : [ value ]; -} \ No newline at end of file +} diff --git a/core/nua.js b/core/nua.js index 2d838fcb..42c63895 100644 --- a/core/nua.js +++ b/core/nua.js @@ -8,9 +8,14 @@ const theme = require('./theme.js'); const login = require('./system_menu_method.js').login; const Config = require('./config.js').get; const messageArea = require('./message_area.js'); +const { + getISOTimestampString +} = require('./database.js'); +const UserProps = require('./user_property.js'); // deps const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { name : 'NUA', @@ -80,20 +85,20 @@ exports.getModule = class NewUserAppModule extends MenuModule { areaTag = areaTag || ''; newUser.properties = { - real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), // :TODO: Use moment & explicit ISO string format - sex : formData.value.sex, - location : formData.value.location, - affiliation : formData.value.affils, - email_address : formData.value.email, - web_address : formData.value.web, - account_created : new Date().toISOString(), // :TODO: Use moment & explicit ISO string format + [ UserProps.RealName ] : formData.value.realName, + [ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate), + [ UserProps.Sex ] : formData.value.sex, + [ UserProps.Location ] : formData.value.location, + [ UserProps.Affiliations ] : formData.value.affils, + [ UserProps.EmailAddress ] : formData.value.email, + [ UserProps.WebAddress ] : formData.value.web, + [ UserProps.AccountCreated ] : getISOTimestampString(), - message_conf_tag : confTag, - message_area_tag : areaTag, + [ UserProps.MessageConfTag ] : confTag, + [ UserProps.MessageAreaTag ] : areaTag, - term_height : self.client.term.termHeight, - term_width : self.client.term.termWidth, + [ UserProps.TermHeight ] : self.client.term.termHeight, + [ UserProps.TermWidth ] : self.client.term.termWidth, // :TODO: Other defaults // :TODO: should probably have a place to create defaults/etc. @@ -101,9 +106,9 @@ exports.getModule = class NewUserAppModule extends MenuModule { const defaultTheme = _.get(config, 'theme.default'); if('*' === defaultTheme) { - newUser.properties.theme_id = theme.getRandomTheme(); + newUser.properties[UserProps.ThemeId] = theme.getRandomTheme(); } else { - newUser.properties.theme_id = defaultTheme; + newUser.properties[UserProps.ThemeId] = defaultTheme; } // :TODO: User.create() should validate email uniqueness! @@ -133,7 +138,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { }; } - if(User.AccountStatus.inactive === self.client.user.properties.account_status) { + if(User.AccountStatus.inactive === self.client.user.properties[UserProps.AccountStatus]) { return self.gotoMenu(extraArgs.inactive, cb); } else { // diff --git a/core/stat_log.js b/core/stat_log.js index 4ee4c53b..173222b9 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -2,10 +2,12 @@ 'use strict'; const sysDb = require('./database.js').dbs.system; +const { + getISOTimestampString +} = require('./database.js'); // deps const _ = require('lodash'); -const moment = require('moment'); /* System Event Log & Stats @@ -149,7 +151,9 @@ class StatLog { } // the time "now" in the ISO format we use and love :) - get now() { return moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } + get now() { + return getISOTimestampString(); + } appendSystemLogEntry(logName, logValue, keep, keepType, cb) { sysDb.run( diff --git a/core/user_config.js b/core/user_config.js index f9aad6c4..ef72e1ad 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -1,11 +1,17 @@ /* jslint node: true */ 'use strict'; +// ENiGMA½ const MenuModule = require('./menu_module.js').MenuModule; const ViewController = require('./view_controller.js').ViewController; const theme = require('./theme.js'); const sysValidate = require('./system_view_validate.js'); +const UserProps = require('./user_property.js'); +const { + getISOTimestampString +} = require('./database.js'); +// deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); @@ -101,15 +107,15 @@ exports.getModule = class UserConfigModule extends MenuModule { assert(formData.value.password === formData.value.passwordConfirm); const newProperties = { - real_name : formData.value.realName, - birthdate : new Date(Date.parse(formData.value.birthdate)).toISOString(), - sex : formData.value.sex, - location : formData.value.location, - affiliation : formData.value.affils, - email_address : formData.value.email, - web_address : formData.value.web, - term_height : formData.value.termHeight.toString(), - theme_id : self.availThemeInfo[formData.value.theme].themeId, + [ UserProps.RealName ] : formData.value.realName, + [ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate), + [ UserProps.Sex ] : formData.value.sex, + [ UserProps.Location ] : formData.value.location, + [ UserProps.Affiliations ] : formData.value.affils, + [ UserProps.EmailAddress ] : formData.value.email, + [ UserProps.WebAddress ] : formData.value.web, + [ UserProps.TermHeight ] : formData.value.termHeight.toString(), + [ UserProps.ThemeId ] : self.availThemeInfo[formData.value.theme].themeId, }; // runtime set theme diff --git a/core/user_property.js b/core/user_property.js index 04f8f0c9..bbb36928 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -8,18 +8,36 @@ // can utilize their own properties as well! // module.exports = { - PassPbkdf2Salt : 'pw_pbkdf2_salt', - PassPbkdf2Dk : 'pw_pbkdf2_dk', + PassPbkdf2Salt : 'pw_pbkdf2_salt', + PassPbkdf2Dk : 'pw_pbkdf2_dk', - AccountStatus : 'account_status', + AccountStatus : 'account_status', - Birthdate : 'birthdate', + RealName : 'real_name', + Sex : 'sex', + Birthdate : 'birthdate', + Location : 'location', + Affiliations : 'affiliation', + EmailAddress : 'email_address', + WebAddress : 'web_address', + TermHeight : 'term_height', + TermWidth : 'term_width', + ThemeId : 'theme_id', + AccountCreated : 'account_created', - FailedLoginAttempts : 'failed_login_attempts', - AccountLockedTs : 'account_locked_timestamp', - AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status + FailedLoginAttempts : 'failed_login_attempts', + AccountLockedTs : 'account_locked_timestamp', + AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status - EmailPwResetToken : 'email_password_reset_token', - EmailPwResetTokenTs : 'email_password_reset_token_ts', + EmailPwResetToken : 'email_password_reset_token', + EmailPwResetTokenTs : 'email_password_reset_token_ts', + + FileAreaTag : 'file_area_tag', + FileBaseFilters : 'file_base_filters', + FileBaseFilterActiveUuid : 'file_base_filter_active_uuid', + FileBaseLastViewedId : 'user_file_base_last_viewed', + + MessageConfTag : 'message_conf_tag', + MessageAreaTag : 'message_area_tag', }; diff --git a/core/web_password_reset.js b/core/web_password_reset.js index ceefe9c5..c76a232c 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -79,8 +79,8 @@ class WebPasswordReset { token = token.toString('hex'); const newProperties = { - email_password_reset_token : token, - email_password_reset_token_ts : getISOTimestampString(), + [ UserProps.EmailPwResetToken ] : token, + [ UserProps.EmailPwResetTokenTs ] : getISOTimestampString(), }; // we simply place the reset token in the user's properties From 4050affedf4e775f3a12c8ca514ece8eae84beda Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 17:41:16 -0700 Subject: [PATCH 380/569] More conversion to UserProps --- core/client_connections.js | 23 +++++++++++---------- core/download_queue.js | 5 +++-- core/dropfile.js | 30 ++++++++++++++------------- core/file_area_filter_edit.js | 3 ++- core/file_base_filter.js | 6 +++--- core/fse.js | 3 ++- core/login_server_module.js | 7 ++++--- core/message_area.js | 4 ++-- core/mod_mixins.js | 12 ++++++----- core/msg_area_list.js | 3 ++- core/msg_area_post_fse.js | 7 +++++-- core/msg_list.js | 3 ++- core/predefined_mci.js | 38 ++++++++++++++++++++--------------- core/set_newscan_date.js | 6 +++--- core/system_menu_method.js | 13 ++++++------ core/theme.js | 6 ++++-- core/user_config.js | 22 ++++++++++---------- core/user_login.js | 11 +++++----- core/user_property.js | 8 ++++++-- core/web_password_reset.js | 8 ++++---- 20 files changed, 123 insertions(+), 95 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index 93bb9465..334fa69e 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -2,13 +2,14 @@ 'use strict'; // ENiGMA½ -const logger = require('./logger.js'); -const Events = require('./events.js'); +const logger = require('./logger.js'); +const Events = require('./events.js'); +const UserProps = require('./user_property.js'); // deps -const _ = require('lodash'); -const moment = require('moment'); -const hashids = require('hashids'); +const _ = require('lodash'); +const moment = require('moment'); +const hashids = require('hashids'); exports.getActiveConnections = getActiveConnections; exports.getActiveNodeList = getActiveNodeList; @@ -17,7 +18,7 @@ exports.removeClient = removeClient; exports.getConnectionByUserId = getConnectionByUserId; const clientConnections = []; -exports.clientConnections = clientConnections; +exports.clientConnections = clientConnections; function getActiveConnections() { return clientConnections; } @@ -46,11 +47,11 @@ function getActiveNodeList(authUsersOnly) { // if(ac.user.isAuthenticated()) { entry.userName = ac.user.username; - entry.realName = ac.user.properties.real_name; - entry.location = ac.user.properties.location; - entry.affils = entry.affiliation = ac.user.properties.affiliation; + entry.realName = ac.user.properties[UserProps.RealName]; + entry.location = ac.user.properties[UserProps.Location]; + entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations]; - const diff = now.diff(moment(ac.user.properties.last_login_timestamp), 'minutes'); + const diff = now.diff(moment(ac.user.properties[UserProps.LastLoginTs]), 'minutes'); entry.timeOn = moment.duration(diff, 'minutes'); } return entry; @@ -61,7 +62,7 @@ function addNewClient(client, clientSock) { const id = client.session.id = clientConnections.push(client) - 1; const remoteAddress = client.remoteAddress = clientSock.remoteAddress; - // create a uniqe identifier one-time ID for this session + // create a unique identifier one-time ID for this session client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); // Create a client specific logger diff --git a/core/download_queue.js b/core/download_queue.js index 0d1e3847..28ca3aac 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -2,6 +2,7 @@ 'use strict'; const FileEntry = require('./file_entry.js'); +const UserProps = require('./user_property.js'); // deps const { partition } = require('lodash'); @@ -11,8 +12,8 @@ module.exports = class DownloadQueue { this.client = client; if(!Array.isArray(this.client.user.downloadQueue)) { - if(this.client.user.properties.dl_queue) { - this.loadFromProperty(this.client.user.properties.dl_queue); + if(this.client.user.properties[UserProps.DownloadQueue]) { + this.loadFromProperty(this.client.user.properties[UserProps.DownloadQueue]); } else { this.client.user.downloadQueue = []; } diff --git a/core/dropfile.js b/core/dropfile.js index 345a031a..4c6a9c0b 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -4,6 +4,7 @@ // ENiGMA½ const Config = require('./config.js').get; const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const fs = require('graceful-fs'); @@ -168,7 +169,7 @@ module.exports = class DropFile { '115200', Config().general.boardName, this.client.user.userId.toString(), - this.client.user.properties.real_name || this.client.user.username, + this.client.user.properties[UserProps.RealName] || this.client.user.username, this.client.user.username, this.client.user.getLegacySecurityLevel().toString(), '546', // :TODO: Minutes left! @@ -189,21 +190,22 @@ module.exports = class DropFile { const opUserName = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; const userName = /[^\s]*/.exec(this.client.user.username)[0]; const secLevel = this.client.user.getLegacySecurityLevel().toString(); + const location = this.client.user.properties[UserProps.Location]; return iconv.encode( [ - Config().general.boardName, // "The name of the system." - opUserName, // "The sysop's name up to the first space." - opUserName, // "The sysop's name following the first space." - 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." - '57600', // "The current port (DTE) rate." - '0', // "The number "0"" - userName, // "The current user's name, up to the first space." - userName, // "The current user's name, following the first space." - this.client.user.properties.location || '', // "Where the user lives, or a blank line if unknown." - '1', // "The number "0" if TTY, or "1" if ANSI." - secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." - '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." - '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." + Config().general.boardName, // "The name of the system." + opUserName, // "The sysop's name up to the first space." + opUserName, // "The sysop's name following the first space." + 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." + '57600', // "The current port (DTE) rate." + '0', // "The number "0"" + userName, // "The current user's name, up to the first space." + userName, // "The current user's name, following the first space." + location || '', // "Where the user lives, or a blank line if unknown." + '1', // "The number "0" if TTY, or "1" if ANSI." + secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." + '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." + '-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." ].join('\r\n') + '\r\n', 'cp437'); } diff --git a/core/file_area_filter_edit.js b/core/file_area_filter_edit.js index 06c0fd6e..e20d766b 100644 --- a/core/file_area_filter_edit.js +++ b/core/file_area_filter_edit.js @@ -7,6 +7,7 @@ const ViewController = require('./view_controller.js').ViewContro const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; const FileBaseFilters = require('./file_base_filter.js'); const stringFormat = require('./string_format.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -111,7 +112,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { // // If the item was also the active filter, we need to make a new one active // - if(filterUuid === this.client.user.properties.file_base_filter_active_uuid) { + if(filterUuid === this.client.user.properties[UserProps.FileBaseFilterActiveUuid]) { const newActive = this.filtersArray[this.currentFilterIndex]; if(newActive) { filters.setActive(newActive.uuid); diff --git a/core/file_base_filter.js b/core/file_base_filter.js index 0ff19b94..d72b3eea 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -66,7 +66,7 @@ module.exports = class FileBaseFilters { } load() { - let filtersProperty = this.client.user.properties.file_base_filters; + let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters]; let defaulted; if(!filtersProperty) { filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters()); @@ -131,11 +131,11 @@ module.exports = class FileBaseFilters { } static getActiveFilter(client) { - return new FileBaseFilters(client).get(client.user.properties.file_base_filter_active_uuid); + return new FileBaseFilters(client).get(client.user.properties[UserProps.FileBaseFilterActiveUuid]); } static getFileBaseLastViewedFileIdByUser(user) { - return parseInt((user.properties.user_file_base_last_viewed || 0)); + return parseInt((user.properties[UserProps.FileBaseLastViewedId] || 0)); } static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { diff --git a/core/fse.js b/core/fse.js index 98065b58..3529935d 100644 --- a/core/fse.js +++ b/core/fse.js @@ -24,6 +24,7 @@ const { const Config = require('./config.js').get; const { getAddressedToInfo } = require('./mail_util.js'); const Events = require('./events.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -738,7 +739,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const fromView = self.viewControllers.header.getView(MciViewIds.header.from); const area = getMessageAreaByTag(self.messageAreaTag); if(area && area.realNames) { - fromView.setText(self.client.user.properties.real_name || self.client.user.username); + fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username); } else { fromView.setText(self.client.user.username); } diff --git a/core/login_server_module.js b/core/login_server_module.js index d1a3552f..e5fccb39 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -6,6 +6,7 @@ const conf = require('./config.js'); const logger = require('./logger.js'); const ServerModule = require('./server_module.js').ServerModule; const clientConns = require('./client_connections.js'); +const UserProps = require('./user_property.js'); // deps const _ = require('lodash'); @@ -25,12 +26,12 @@ module.exports = class LoginServerModule extends ServerModule { // const preLoginTheme = _.get(conf.config, 'theme.preLogin'); if('*' === preLoginTheme) { - client.user.properties.theme_id = theme.getRandomTheme() || ''; + client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || ''; } else { - client.user.properties.theme_id = preLoginTheme; + client.user.properties[UserProps.ThemeId] = preLoginTheme; } - theme.setClientTheme(client, client.user.properties.theme_id); + theme.setClientTheme(client, client.user.properties[UserProps.ThemeId]); return cb(null); // note: currently useless to use cb here - but this may change...again... } diff --git a/core/message_area.js b/core/message_area.js index ea6b4a87..a465b235 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -304,8 +304,8 @@ function tempChangeMessageConfAndArea(client, areaTag) { return false; } - client.user.properties.message_conf_tag = confTag; - client.user.properties.message_area_tag = areaTag; + client.user.properties[UserProps.MessageConfTag] = confTag; + client.user.properties[UserProps.MessageAreaTag] = areaTag; return true; } diff --git a/core/mod_mixins.js b/core/mod_mixins.js index f3d9d5ad..22e49407 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -2,8 +2,10 @@ 'use strict'; const messageArea = require('../core/message_area.js'); -const { get } = require('lodash'); +const UserProps = require('./user_property.js'); +// deps +const { get } = require('lodash'); exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { @@ -15,8 +17,8 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { if(recordPrevious) { this.prevMessageConfAndArea = { - confTag : this.client.user.properties.message_conf_tag, - areaTag : this.client.user.properties.message_area_tag, + confTag : this.client.user.properties[UserProps.MessageConfTag], + areaTag : this.client.user.properties[UserProps.MessageAreaTag], }; } @@ -27,8 +29,8 @@ exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { tempMessageConfAndAreaRestore() { if(this.prevMessageConfAndArea) { - this.client.user.properties.message_conf_tag = this.prevMessageConfAndArea.confTag; - this.client.user.properties.message_area_tag = this.prevMessageConfAndArea.areaTag; + this.client.user.properties[UserProps.MessageConfTag] = this.prevMessageConfAndArea.confTag; + this.client.user.properties[UserProps.MessageAreaTag] = this.prevMessageConfAndArea.areaTag; } } }; diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 97a2e16e..1d47f76c 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -5,6 +5,7 @@ const { MenuModule } = require('./menu_module.js'); const messageArea = require('./message_area.js'); const { Errors } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -110,7 +111,7 @@ exports.getModule = class MessageAreaListModule extends MenuModule { initList() { let index = 1; this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( - this.client.user.properties.message_conf_tag, + this.client.user.properties[UserProps.MessageConfTag], { client : this.client } ).map(area => { return { diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 8e4d9f3f..123ce13c 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -3,6 +3,7 @@ const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; const persistMessage = require('./message_area.js').persistMessage; +const UserProps = require('./user_property.js'); const _ = require('lodash'); const async = require('async'); @@ -58,8 +59,10 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { } enter() { - if(_.isString(this.client.user.properties.message_area_tag) && !_.isString(this.messageAreaTag)) { - this.messageAreaTag = this.client.user.properties.message_area_tag; + if(_.isString(this.client.user.properties[UserProps.MessageAreaTag]) && + !_.isString(this.messageAreaTag)) + { + this.messageAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; } super.enter(); diff --git a/core/msg_list.js b/core/msg_list.js index 2796db88..f73dae8a 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -8,6 +8,7 @@ const messageArea = require('./message_area.js'); const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const Errors = require('./enig_error.js').Errors; const Message = require('./message.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -167,7 +168,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( if(this.config.messageAreaTag) { this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); } else { - this.config.messageAreaTag = this.client.user.properties.message_area_tag; + this.config.messageAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; } } } diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 5a04ff07..1622bd03 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -2,17 +2,18 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const Log = require('./logger.js').log; +const Config = require('./config.js').get; +const Log = require('./logger.js').log; const { getMessageAreaByTag, getMessageConferenceByTag -} = require('./message_area.js'); -const clientConnections = require('./client_connections.js'); -const StatLog = require('./stat_log.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const { formatByteSize } = require('./string_util.js'); -const ANSI = require('./ansi_term.js'); +} = require('./message_area.js'); +const clientConnections = require('./client_connections.js'); +const StatLog = require('./stat_log.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const { formatByteSize } = require('./string_util.js'); +const ANSI = require('./ansi_term.js'); +const UserProps = require('./user_property.js'); // deps const packageJson = require('../package.json'); @@ -83,7 +84,9 @@ const PREDEFINED_MCI_GENERATORS = { UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, LO : function location(client) { return userStatAsString(client, 'location', ''); }, UA : function age(client) { return client.user.getAge().toString(); }, - BD : function birthdate(client) { return moment(client.user.properties.birthdate).format(client.currentTheme.helpers.getDateFormat()); }, // iNiQUiTY + BD : function birthdate(client) { // iNiQUiTY + return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat()); + }, US : function sex(client) { return userStatAsString(client, 'sex', ''); }, UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, @@ -114,7 +117,9 @@ const PREDEFINED_MCI_GENERATORS = { 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[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat()); + }, PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, @@ -123,19 +128,19 @@ const PREDEFINED_MCI_GENERATORS = { }, MA : function messageAreaName(client) { - const area = getMessageAreaByTag(client.user.properties.message_area_tag); + const area = getMessageAreaByTag(client.user.properties[UserProps.MessageAreaTag]); return area ? area.name : ''; }, MC : function messageConfName(client) { - const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); + const conf = getMessageConferenceByTag(client.user.properties[UserProps.MessageConfTag]); return conf ? conf.name : ''; }, ML : function messageAreaDescription(client) { - const area = getMessageAreaByTag(client.user.properties.message_area_tag); + const area = getMessageAreaByTag(client.user.properties[UserProps.MessageAreaTag]); return area ? area.desc : ''; }, CM : function messageConfDescription(client) { - const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); + const conf = getMessageConferenceByTag(client.user.properties[UserProps.MessageConfTag]); return conf ? conf.desc : ''; }, @@ -169,8 +174,9 @@ const PREDEFINED_MCI_GENERATORS = { // Clean up CPU strings a bit for better display // return os.cpus()[0].model - .replace(/\(R\)|\(TM\)|processor|CPU/g, '') - .replace(/\s+(?= )/g, ''); + .replace(/\(R\)|\(TM\)|processor|CPU/ig, '') + .replace(/\s+(?= )/g, '') + .trim(); }, // :TODO: MCI for core count, e.g. os.cpus().length diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js index c86b8e26..7713f647 100644 --- a/core/set_newscan_date.js +++ b/core/set_newscan_date.js @@ -14,7 +14,7 @@ const { updateMessageAreaLastReadId, getMessageIdNewerThanTimestampByArea } = require('./message_area.js'); -const stringFormat = require('./string_format.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -183,8 +183,8 @@ exports.getModule = class SetNewScanDate extends MenuModule { }); // Find current conf/area & move it directly under "All" - const currConfTag = this.client.user.properties.message_conf_tag; - const currAreaTag = this.client.user.properties.message_area_tag; + const currConfTag = this.client.user.properties[UserProps.MessageConfTag]; + const currAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; if(currConfTag && currAreaTag) { const confAreaIndex = selections.findIndex( confArea => { return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag; diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 7c135f22..5e7b651b 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -7,6 +7,7 @@ const ansiNormal = require('./ansi_term.js').normal; const { userLogin } = require('./user_login.js'); const messageArea = require('./message_area.js'); const { ErrorReasons } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); // deps const _ = require('lodash'); @@ -105,7 +106,7 @@ function reloadMenu(menu, cb) { function prevConf(callingMenu, formData, extraArgs, cb) { const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); - const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag) || confs.length; + const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties[UserProps.MessageConfTag]) || confs.length; messageArea.changeMessageConference(callingMenu.client, confs[currIndex - 1].confTag, err => { if(err) { @@ -118,7 +119,7 @@ function prevConf(callingMenu, formData, extraArgs, cb) { function nextConf(callingMenu, formData, extraArgs, cb) { const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); - let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties.message_conf_tag); + let currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties[UserProps.MessageConfTag]); if(currIndex === confs.length - 1) { currIndex = -1; @@ -134,8 +135,8 @@ function nextConf(callingMenu, formData, extraArgs, cb) { } function prevArea(callingMenu, formData, extraArgs, cb) { - const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); - const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag) || areas.length; + const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties[UserProps.MessageConfTag]); + const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]) || areas.length; messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => { if(err) { @@ -147,8 +148,8 @@ function prevArea(callingMenu, formData, extraArgs, cb) { } function nextArea(callingMenu, formData, extraArgs, cb) { - const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties.message_conf_tag); - let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties.message_area_tag); + const areas = messageArea.getSortedAvailMessageAreasByConfTag(callingMenu.client.user.properties[UserProps.MessageConfTag]); + let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]); if(currIndex === areas.length - 1) { currIndex = -1; diff --git a/core/theme.js b/core/theme.js index 052eeac7..6dfee685 100644 --- a/core/theme.js +++ b/core/theme.js @@ -13,7 +13,9 @@ const Errors = require('./enig_error.js').Errors; const ErrorReasons = require('./enig_error.js').ErrorReasons; const Events = require('./events.js'); const AnsiPrep = require('./ansi_prep.js'); +const UserProps = require('./user_property.js'); +// deps const fs = require('graceful-fs'); const paths = require('path'); const async = require('async'); @@ -427,8 +429,8 @@ function getThemeArt(options, cb) { // random // const config = Config(); - if(!options.themeId && _.has(options, 'client.user.properties.theme_id')) { - options.themeId = options.client.user.properties.theme_id; + if(!options.themeId && _.has(options, [ 'client', 'user', 'properties', UserProps.ThemeId ])) { + options.themeId = options.client.user.properties[UserProps.ThemeId]; } else { options.themeId = config.theme.default; } diff --git a/core/user_config.js b/core/user_config.js index ef72e1ad..d2748c4b 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -55,7 +55,7 @@ exports.getModule = class UserConfigModule extends MenuModule { // // If nothing changed, we know it's OK // - if(self.client.user.properties.email_address.toLowerCase() === data.toLowerCase()) { + if(self.client.user.properties[UserProps.EmailAddress].toLowerCase() === data.toLowerCase()) { return cb(null); } @@ -182,22 +182,22 @@ exports.getModule = class UserConfigModule extends MenuModule { }), 'name'); currentThemeIdIndex = Math.max(0, _.findIndex(self.availThemeInfo, function cmp(ti) { - return ti.themeId === self.client.user.properties.theme_id; + return ti.themeId === self.client.user.properties[UserProps.ThemeId]; })); callback(null); }, function populateViews(callback) { - var user = self.client.user; + const user = self.client.user; - self.setViewText('menu', MciCodeIds.RealName, user.properties.real_name); - self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties.birthdate).format('YYYYMMDD')); - self.setViewText('menu', MciCodeIds.Sex, user.properties.sex); - self.setViewText('menu', MciCodeIds.Loc, user.properties.location); - self.setViewText('menu', MciCodeIds.Affils, user.properties.affiliation); - self.setViewText('menu', MciCodeIds.Email, user.properties.email_address); - self.setViewText('menu', MciCodeIds.Web, user.properties.web_address); - self.setViewText('menu', MciCodeIds.TermHeight, user.properties.term_height.toString()); + self.setViewText('menu', MciCodeIds.RealName, user.properties[UserProps.RealName]); + self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties[UserProps.Birthdate]).format('YYYYMMDD')); + self.setViewText('menu', MciCodeIds.Sex, user.properties[UserProps.Sex]); + self.setViewText('menu', MciCodeIds.Loc, user.properties[UserProps.Location]); + self.setViewText('menu', MciCodeIds.Affils, user.properties[UserProps.Affiliations]); + self.setViewText('menu', MciCodeIds.Email, user.properties[UserProps.EmailAddress]); + self.setViewText('menu', MciCodeIds.Web, user.properties[UserProps.WebAddress]); + self.setViewText('menu', MciCodeIds.TermHeight, user.properties[UserProps.TermHeight].toString()); var themeView = self.getView(MciCodeIds.Theme); diff --git a/core/user_login.js b/core/user_login.js index fa9ae676..a46be5b1 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -12,6 +12,7 @@ const { Errors, ErrorReasons } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -74,24 +75,24 @@ function userLogin(client, username, password, cb) { client.log.info('Successful login'); // User's unique session identifier is the same as the connection itself - user.sessionId = client.session.uniqueId; // convienence + user.sessionId = client.session.uniqueId; // convenience Events.emit(Events.getSystemEvents().UserLogin, { user } ); async.parallel( [ function setTheme(callback) { - setClientTheme(client, user.properties.theme_id); + setClientTheme(client, user.properties[UserProps.ThemeId]); return callback(null); }, function updateSystemLoginCount(callback) { - return StatLog.incrementSystemStat('login_count', 1, callback); + return StatLog.incrementSystemStat('login_count', 1, callback); // :TODO: create system_property.js }, function recordLastLogin(callback) { - return StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); + return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback); }, function updateUserLoginCount(callback) { - return StatLog.incrementUserStat(user, 'login_count', 1, callback); + return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback); }, function recordLoginHistory(callback) { const loginHistoryMax = Config().statLog.systemEvents.loginHistoryMax; diff --git a/core/user_property.js b/core/user_property.js index bbb36928..3c2f4f12 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -11,7 +11,7 @@ module.exports = { PassPbkdf2Salt : 'pw_pbkdf2_salt', PassPbkdf2Dk : 'pw_pbkdf2_dk', - AccountStatus : 'account_status', + AccountStatus : 'account_status', // See User.AccountStatus enum RealName : 'real_name', Sex : 'sex', @@ -24,10 +24,14 @@ module.exports = { TermWidth : 'term_width', ThemeId : 'theme_id', AccountCreated : 'account_created', + LastLoginTs : 'last_login_timestamp', + LoginCount : 'login_count', + + DownloadQueue : 'dl_queue', // download_queue.js FailedLoginAttempts : 'failed_login_attempts', AccountLockedTs : 'account_locked_timestamp', - AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status + AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status before lock out EmailPwResetToken : 'email_password_reset_token', EmailPwResetTokenTs : 'email_password_reset_token_ts', diff --git a/core/web_password_reset.js b/core/web_password_reset.js index c76a232c..90c5f57c 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -59,7 +59,7 @@ class WebPasswordReset { } User.getUser(userId, (err, user) => { - if(err || !user.properties.email_address) { + if(err || !user.properties[UserProps.EmailAddress]) { return callback(Errors.DoesNotExist('No email address associated with this user')); } @@ -105,13 +105,13 @@ class WebPasswordReset { function buildAndSendEmail(user, textTemplate, htmlTemplate, callback) { const sendMail = require('./email.js').sendMail; - const resetUrl = webServer.instance.buildUrl(`/reset_password?token=${user.properties.email_password_reset_token}`); + const resetUrl = webServer.instance.buildUrl(`/reset_password?token=${user.properties[UserProps.EmailPwResetToken]}`); function replaceTokens(s) { return s .replace(/%BOARDNAME%/g, Config().general.boardName) .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, user.properties.email_password_reset_token) + .replace(/%TOKEN%/g, user.properties[UserProps.EmailPwResetToken]) .replace(/%RESET_URL%/g, resetUrl) ; } @@ -122,7 +122,7 @@ class WebPasswordReset { } const message = { - to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`, + to : `${user.properties[UserProps.RealName]||user.username} <${user.properties[UserProps.EmailAddress]}>`, // from will be filled in subject : 'Forgot Password', text : textTemplate, From d11aca571e003b0f6d7ee1588bd28f21f667f306 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 22:02:36 -0700 Subject: [PATCH 381/569] Yet more UserProps usage --- core/acs_parser.js | 30 ++++++++++++++++-------------- core/dropfile.js | 12 +++++++----- core/fse.js | 2 +- core/predefined_mci.js | 32 ++++++++++++++++---------------- core/user_property.js | 6 ++++++ misc/acs_parser.pegjs | 30 ++++++++++++++++-------------- 6 files changed, 62 insertions(+), 50 deletions(-) diff --git a/core/acs_parser.js b/core/acs_parser.js index 3025f654..6e32cb06 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -847,6 +847,8 @@ function peg$parse(input, options) { const client = options.client; const user = options.client.user; + const UserProps = require('./user_property.js'); + const moment = require('moment'); function checkAccess(acsCode, value) { @@ -863,7 +865,7 @@ function peg$parse(input, options) { value = [ value ]; } - const userAccountStatus = parseInt(user.properties.account_status, 10); + const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus); return value.map(n => parseInt(n, 10)).includes(userAccountStatus); }, EC : function isEncoding() { @@ -888,15 +890,15 @@ function peg$parse(input, options) { return value.map(n => parseInt(n, 10)).includes(client.node); }, NP : function numberOfPosts() { - const postCount = parseInt(user.properties.post_count, 10) || 0; + const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; return !isNaN(value) && postCount >= value; }, NC : function numberOfCalls() { - const loginCount = parseInt(user.properties.login_count, 10); + const loginCount = user.getPropertyAsNumber(UserProps.LoginCount); return !isNaN(value) && loginCount >= value; }, AA : function accountAge() { - const accountCreated = moment(user.properties.account_created); + const accountCreated = moment(user.getProperty(UserProps.AccountCreated)); const now = moment(); const daysOld = accountCreated.diff(moment(), 'days'); return !isNaN(value) && @@ -905,36 +907,36 @@ function peg$parse(input, options) { daysOld >= value; }, BU : function bytesUploaded() { - const bytesUp = parseInt(user.properties.ul_total_bytes, 10) || 0; + const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; return !isNaN(value) && bytesUp >= value; }, UP : function uploads() { - const uls = parseInt(user.properties.ul_total_count, 10) || 0; + const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; return !isNaN(value) && uls >= value; }, BD : function bytesDownloaded() { - const bytesDown = parseInt(user.properties.dl_total_bytes, 10) || 0; + const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; return !isNaN(value) && bytesDown >= value; }, DL : function downloads() { - const dls = parseInt(user.properties.dl_total_count, 10) || 0; + const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; return !isNaN(value) && dls >= value; }, NR : function uploadDownloadRatioGreaterThan() { - const ulCount = parseInt(user.properties.ul_total_count, 10) || 0; - const dlCount = parseInt(user.properties.dl_total_count, 10) || 0; + const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; + const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; const ratio = ~~((ulCount / dlCount) * 100); return !isNaN(value) && ratio >= value; }, KR : function uploadDownloadByteRatioGreaterThan() { - const ulBytes = parseInt(user.properties.ul_total_bytes, 10) || 0; - const dlBytes = parseInt(user.properties.dl_total_bytes, 10) || 0; + const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; + const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; const ratio = ~~((ulBytes / dlBytes) * 100); return !isNaN(value) && ratio >= value; }, PC : function postCallRatio() { - const postCount = parseInt(user.properties.post_count, 10) || 0; - const loginCount = parseInt(user.properties.login_count, 10); + const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; + const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0; const ratio = ~~((postCount / loginCount) * 100); return !isNaN(value) && ratio >= value; }, diff --git a/core/dropfile.js b/core/dropfile.js index 4c6a9c0b..a23c5c1c 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -85,6 +85,8 @@ module.exports = class DropFile { const prop = this.client.user.properties; const now = moment(); const secLevel = this.client.user.getLegacySecurityLevel().toString(); + const fullName = prop[UserProps.RealName] || this.client.user.username; + const bd = moment(prop[UserProp.Birthdate).format('MM/DD/YY'); // :TODO: fix time remaining // :TODO: fix default protocol -- user prop: transfer_protocol @@ -98,13 +100,13 @@ module.exports = class DropFile { 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" 'Y', // "Page Bell - Y=On N=Off (Default to Y)" 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" - prop.real_name || this.client.user.username, // "User Full Name" - prop.location || 'Anywhere', // "Calling From" + fullName, // "User Full Name" + prop[UserProps.Location]|| 'Anywhere', // "Calling From" '123-456-7890', // "Home Phone" '123-456-7890', // "Work/Data Phone" 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) secLevel, // "Security Level" - prop.login_count.toString(), // "Total Times On" + prop[UserProps.LoginCount].toString(), // "Total Times On" now.format('MM/DD/YY'), // "Last Date Called" '15360', // "Seconds Remaining THIS call (for those that particular)" '256', // "Minutes Remaining THIS call" @@ -121,7 +123,7 @@ module.exports = class DropFile { '0', // "Total Downloads" '0', // "Daily Download "K" Total" '999999', // "Daily Download Max. "K" Limit" - moment(prop.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" + bd, // "Caller's Birthdate" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" 'X:\\GEN\\', // "Path to the GEN directory" StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" @@ -142,7 +144,7 @@ module.exports = class DropFile { '0', // "Files d/led so far today" '0', // "Total "K" Bytes Uploaded" '0', // "Total "K" Bytes Downloaded" - prop.user_comment || 'None', // "User Comment" + prop[UserProps.UserComment] || 'None', // "User Comment" '0', // "Total Doors Opened" '0', // "Total Messages Left" diff --git a/core/fse.js b/core/fse.js index 3529935d..9ecf3bdd 100644 --- a/core/fse.js +++ b/core/fse.js @@ -480,7 +480,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } Events.emit(Events.getSystemEvents().UserPostMessage, { user : this.client.user, areaTag : this.message.areaTag }); - return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); + return StatLog.incrementUserStat(this.client.user, UserProps.MessagePostCount, 1, cb); } redrawFooter(options, cb) { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 1622bd03..c83f8353 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -81,18 +81,18 @@ const PREDEFINED_MCI_GENERATORS = { UN : function userName(client) { return client.user.username; }, UI : function userId(client) { return client.user.userId.toString(); }, UG : function groups(client) { return _.values(client.user.groups).join(', '); }, - UR : function realName(client) { return userStatAsString(client, 'real_name', ''); }, - LO : function location(client) { return userStatAsString(client, 'location', ''); }, + UR : function realName(client) { return userStatAsString(client, UserProps.RealName, ''); }, + LO : function location(client) { return userStatAsString(client, UserProps.Location, ''); }, UA : function age(client) { return client.user.getAge().toString(); }, BD : function birthdate(client) { // iNiQUiTY return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat()); }, - US : function sex(client) { return userStatAsString(client, 'sex', ''); }, - UE : function emailAddres(client) { return userStatAsString(client, 'email_address', ''); }, - UW : function webAddress(client) { return userStatAsString(client, 'web_address', ''); }, - UF : function affils(client) { return userStatAsString(client, 'affiliation', ''); }, - UT : function themeId(client) { return userStatAsString(client, 'theme_id', ''); }, - UC : function loginCount(client) { return userStatAsString(client, 'login_count', 0); }, + US : function sex(client) { return userStatAsString(client, UserProps.Sex, ''); }, + UE : function emailAddres(client) { return userStatAsString(client, UserProps.EmailAddress, ''); }, + UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); }, + UF : function affils(client) { return userStatAsString(client, UserProps.Affiliations, ''); }, + UT : function themeId(client) { return userStatAsString(client, UserProps.ThemeId, ''); }, + UC : function loginCount(client) { return userStatAsString(client, UserProps.LoginCount, 0); }, ND : function connectedNode(client) { return client.node.toString(); }, IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version ST : function serverName(client) { return client.session.serverName; }, @@ -100,28 +100,28 @@ const PREDEFINED_MCI_GENERATORS = { const activeFilter = FileBaseFilters.getActiveFilter(client); return activeFilter ? activeFilter.name : '(Unknown)'; }, - DN : function userNumDownloads(client) { return userStatAsString(client, 'dl_total_count', 0); }, // Obv/2 + DN : function userNumDownloads(client) { return userStatAsString(client, UserProps.FileDlTotalCount, 0); }, // Obv/2 DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes - const byteSize = StatLog.getUserStatNum(client.user, 'dl_total_bytes'); + const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileDlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, - UP : function userNumUploads(client) { return userStatAsString(client, 'ul_total_count', 0); }, // Obv/2 + UP : function userNumUploads(client) { return userStatAsString(client, UserProps.FileUlTotalCount, 0); }, // Obv/2 UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes - const byteSize = StatLog.getUserStatNum(client.user, 'ul_total_bytes'); + const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileUlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, NR : function userUpDownRatio(client) { // Obv/2 - return getUserRatio(client, 'ul_total_count', 'dl_total_count'); + return getUserRatio(client, UserProps.FileUlTotalCount, UserProps.FileDlTotalCount); }, KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio - return getUserRatio(client, 'ul_total_bytes', 'dl_total_bytes'); + return getUserRatio(client, UserProps.FileUlTotalBytes, UserProps.FileDlTotalBytes); }, MS : function accountCreatedclient(client) { return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat()); }, - PS : function userPostCount(client) { return userStatAsString(client, 'post_count', 0); }, - PC : function userPostCallRatio(client) { return getUserRatio(client, 'post_count', 'login_count'); }, + PS : function userPostCount(client) { return userStatAsString(client, UserProps.MessagePostCount, 0); }, + PC : function userPostCallRatio(client) { return getUserRatio(client, UserProps.MessagePostCount, UserProps.LoginCount); }, MD : function currentMenuDescription(client) { return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; diff --git a/core/user_property.js b/core/user_property.js index 3c2f4f12..7f2bf6c5 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -26,6 +26,7 @@ module.exports = { AccountCreated : 'account_created', LastLoginTs : 'last_login_timestamp', LoginCount : 'login_count', + UserComment : 'user_comment', // NYI DownloadQueue : 'dl_queue', // download_queue.js @@ -40,8 +41,13 @@ module.exports = { FileBaseFilters : 'file_base_filters', FileBaseFilterActiveUuid : 'file_base_filter_active_uuid', FileBaseLastViewedId : 'user_file_base_last_viewed', + FileDlTotalCount : 'dl_total_count', + FileUlTotalCount : 'ul_total_count', + FileDlTotalBytes : 'dl_total_bytes', + FileUlTotalBytes : 'ul_total_bytes', MessageConfTag : 'message_conf_tag', MessageAreaTag : 'message_area_tag', + MessagePostCount : 'post_count', }; diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs index 4c9bc37b..ed6089ba 100644 --- a/misc/acs_parser.pegjs +++ b/misc/acs_parser.pegjs @@ -3,6 +3,8 @@ const client = options.client; const user = options.client.user; + const UserProps = require('./user_property.js'); + const moment = require('moment'); function checkAccess(acsCode, value) { @@ -19,7 +21,7 @@ value = [ value ]; } - const userAccountStatus = parseInt(user.properties.account_status, 10); + const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus); return value.map(n => parseInt(n, 10)).includes(userAccountStatus); }, EC : function isEncoding() { @@ -44,15 +46,15 @@ return value.map(n => parseInt(n, 10)).includes(client.node); }, NP : function numberOfPosts() { - const postCount = parseInt(user.properties.post_count, 10) || 0; + const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; return !isNaN(value) && postCount >= value; }, NC : function numberOfCalls() { - const loginCount = parseInt(user.properties.login_count, 10); + const loginCount = user.getPropertyAsNumber(UserProps.LoginCount); return !isNaN(value) && loginCount >= value; }, AA : function accountAge() { - const accountCreated = moment(user.properties.account_created); + const accountCreated = moment(user.getProperty(UserProps.AccountCreated)); const now = moment(); const daysOld = accountCreated.diff(moment(), 'days'); return !isNaN(value) && @@ -61,36 +63,36 @@ daysOld >= value; }, BU : function bytesUploaded() { - const bytesUp = parseInt(user.properties.ul_total_bytes, 10) || 0; + const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; return !isNaN(value) && bytesUp >= value; }, UP : function uploads() { - const uls = parseInt(user.properties.ul_total_count, 10) || 0; + const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; return !isNaN(value) && uls >= value; }, BD : function bytesDownloaded() { - const bytesDown = parseInt(user.properties.dl_total_bytes, 10) || 0; + const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; return !isNaN(value) && bytesDown >= value; }, DL : function downloads() { - const dls = parseInt(user.properties.dl_total_count, 10) || 0; + const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; return !isNaN(value) && dls >= value; }, NR : function uploadDownloadRatioGreaterThan() { - const ulCount = parseInt(user.properties.ul_total_count, 10) || 0; - const dlCount = parseInt(user.properties.dl_total_count, 10) || 0; + const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; + const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; const ratio = ~~((ulCount / dlCount) * 100); return !isNaN(value) && ratio >= value; }, KR : function uploadDownloadByteRatioGreaterThan() { - const ulBytes = parseInt(user.properties.ul_total_bytes, 10) || 0; - const dlBytes = parseInt(user.properties.dl_total_bytes, 10) || 0; + const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; + const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; const ratio = ~~((ulBytes / dlBytes) * 100); return !isNaN(value) && ratio >= value; }, PC : function postCallRatio() { - const postCount = parseInt(user.properties.post_count, 10) || 0; - const loginCount = parseInt(user.properties.login_count, 10); + const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; + const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0; const ratio = ~~((postCount / loginCount) * 100); return !isNaN(value) && ratio >= value; }, From f80e07fcf959d5510dafa2c2450a321f9018482c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Nov 2018 22:18:15 -0700 Subject: [PATCH 382/569] ...and more UserProps --- core/bbs.js | 12 ++++++++---- core/last_callers.js | 9 +++++---- core/user.js | 10 +++++----- core/user_list.js | 3 ++- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index b4f88296..76b7138b 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -10,6 +10,7 @@ const conf = require('./config.js'); const logger = require('./logger.js'); const database = require('./database.js'); const resolvePath = require('./misc_util.js').resolvePath; +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -229,18 +230,21 @@ function initialize(cb) { }, function getOpProps(opUserName, next) { const propLoadOpts = { - names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], + names : [ + UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, + UserProps.Location, UserProps.Affiliations, + ], }; User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { - return next(err, opUserName, opProps); + return next(err, opUserName, opProps, propLoadOpts); }); } ], - (err, opUserName, opProps) => { + (err, opUserName, opProps, propLoadOpts) => { const StatLog = require('./stat_log.js'); if(err) { - [ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => { + propLoadOpts.concat('username').forEach(v => { StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); }); } else { diff --git a/core/last_callers.js b/core/last_callers.js index 08eb7b71..6b25caf6 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -7,6 +7,7 @@ const StatLog = require('./stat_log.js'); const User = require('./user.js'); const sysDb = require('./database.js').dbs.system; const { Errors } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); // deps const moment = require('moment'); @@ -165,7 +166,7 @@ exports.getModule = class LastCallersModule extends MenuModule { loadUserForHistoryItems(loginHistory, cb) { const getPropOpts = { - names : [ 'real_name', 'location', 'affiliation' ] + names : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations ] }; const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k); @@ -185,9 +186,9 @@ exports.getModule = class LastCallersModule extends MenuModule { item.userName = item.text = userName; User.loadProperties(item.userId, getPropOpts, (err, props) => { - item.location = (props && props.location) || ''; - item.affiliation = item.affils = (props && props.affiliation) || ''; - item.realName = (props && props.real_name) || ''; + item.location = (props && props[UserProps.Location]) || ''; + item.affiliation = item.affils = (props && props[UserProps.Affiliations]) || ''; + item.realName = (props && props[UserProps.RealName]) || ''; if(!indicatorSumsSql) { return next(null, item); diff --git a/core/user.js b/core/user.js index 609a9e43..307dbacc 100644 --- a/core/user.js +++ b/core/user.js @@ -203,8 +203,8 @@ module.exports = class User { }, function getDkWithSalt(props, callback) { // get DK from stored salt and password provided - User.generatePasswordDerivedKey(password, props.pw_pbkdf2_salt, (err, dk) => { - return callback(err, dk, props.pw_pbkdf2_dk); + User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => { + return callback(err, dk, props[UserProps.PassPbkdf2Dk]); }); }, function validateAuth(passDk, propsDk, callback) { @@ -516,8 +516,8 @@ module.exports = class User { } const newProperties = { - pw_pbkdf2_salt : info.salt, - pw_pbkdf2_dk : info.dk, + [ UserProps.PassPbkdf2Salt ] : info.salt, + [ UserProps.PassPbkdf2Dk ] : info.dk, }; this.persistProperties(newProperties, err => { @@ -596,7 +596,7 @@ module.exports = class User { WHERE id = ( SELECT user_id FROM user_property - WHERE prop_name='real_name' AND prop_value LIKE ? + WHERE prop_name='${UserProps.RealName}' AND prop_value LIKE ? );`, [ realName ], (err, row) => { diff --git a/core/user_list.js b/core/user_list.js index 6f005432..3b342fab 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -5,6 +5,7 @@ const { MenuModule } = require('./menu_module.js'); const { getUserList } = require('./user.js'); const { Errors } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); // deps const moment = require('moment'); @@ -44,7 +45,7 @@ exports.getModule = class UserListModule extends MenuModule { } const fetchOpts = { - properties : [ 'real_name', 'location', 'affiliation', 'last_login_timestamp' ], + properties : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations, UserProps.LastLoginTs ], propsCamelCase : true, // e.g. real_name -> realName }; getUserList(fetchOpts, (err, userList) => { From 2fb3ce83a34019ee01ca7eaa88f01598d4eef665 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 24 Nov 2018 09:33:20 -0700 Subject: [PATCH 383/569] Spelling... --- core/menu_module.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 2ad30b8a..87f96dc0 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -30,7 +30,7 @@ exports.MenuModule = class MenuModule extends PluginModule { this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); this.viewControllers = {}; - // *initial* interruptable state for this menu + // *initial* Interruptible state for this menu this.disableInterruption(); } @@ -167,18 +167,18 @@ exports.MenuModule = class MenuModule extends PluginModule { } neverInterruptable() { - return this.menuConfig.config.interruptable === 'never'; + return this.menuConfig.config.Interruptible === 'never'; } enableInterruption() { if(!this.neverInterruptable()) { - this.interruptable = true; + this.Interruptible = true; } } disableInterruption() { if(!this.neverInterruptable()) { - this.interruptable = false; + this.Interruptible = false; } } @@ -191,7 +191,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } displayQueuedInterruptions(cb) { - if(true !== this.interruptable) { + if(true !== this.Interruptible) { return cb(null); } @@ -205,7 +205,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } attemptInterruptNow(interruptItem, cb) { - if(true !== this.interruptable) { + if(true !== this.Interruptible) { return cb(null, false); // don't eat up the item; queue for later } From 1dafa2854ba02ae3c1a9d7bd858cb16fa9a0e2d4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 24 Nov 2018 09:39:53 -0700 Subject: [PATCH 384/569] Fix initial load introduced last nigth :( --- core/bbs.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 76b7138b..05168e72 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -223,28 +223,29 @@ function initialize(cb) { // const User = require('./user.js'); + const propLoadOpts = { + names : [ + UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, + UserProps.Location, UserProps.Affiliations, + ], + }; + async.waterfall( [ function getOpUserName(next) { return User.getUserName(1, next); }, function getOpProps(opUserName, next) { - const propLoadOpts = { - names : [ - UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, - UserProps.Location, UserProps.Affiliations, - ], - }; User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { - return next(err, opUserName, opProps, propLoadOpts); + return next(err, opUserName, opProps); }); - } + }, ], - (err, opUserName, opProps, propLoadOpts) => { + (err, opUserName, opProps) => { const StatLog = require('./stat_log.js'); if(err) { - propLoadOpts.concat('username').forEach(v => { + propLoadOpts.names.concat('username').forEach(v => { StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); }); } else { From a5f3a65faa5dc41a5db46affb55df245d6788099 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 24 Nov 2018 10:33:33 -0700 Subject: [PATCH 385/569] Doc update --- docs/configuration/config-hjson.md | 87 +----------------------------- 1 file changed, 1 insertion(+), 86 deletions(-) diff --git a/docs/configuration/config-hjson.md b/docs/configuration/config-hjson.md index 7aae987f..38392943 100644 --- a/docs/configuration/config-hjson.md +++ b/docs/configuration/config-hjson.md @@ -44,90 +44,5 @@ Below is a list of various configuration sections. There are many more, but this * [File Base](/docs/filebase/index.md) * [File Transfer Protocols](file-transfer-protocols.md): Oldschool file transfer protocols such as X/Y/Z-Modem! * [Message Areas](/docs/messageareas/configuring-a-message-area.md), [Networks](/docs/messageareas/message-networks.md), [NetMail](/docs/messageareas/netmail.md), etc. +* ...and a **lot** more! Explore the docs! If you can't find something, please contact us! - -### A Sample Configuration -Below is a **sample** `config.hjson` illustrating various (but certainly not all!) elements that can be configured / tweaked. - -**This is for illustration purposes! Do not cut & paste this configuration!** - - -```hjson -{ - general: { - boardName: A Sample BBS - menuFile: "your_bbs.hjson" // copy of menu.hjson file (and adapt to your needs) - } - - theme: { - default: "super-fancy-theme" // default-assigned theme (for new users) - } - - messageConferences: { - local_general: { - name: Local - desc: Local Discussions - default: true - - areas: { - local_enigma_dev: { - name: ENiGMA 1/2 Development - desc: Discussion related to development and features of ENiGMA 1/2! - default: true - } - } - } - - agoranet: { - name: Agoranet - desc: This network is for blatant exploitation of the greatest BBS scene art group ever.. ACiD. - - areas: { - agoranet_bbs: { - name: BBS Discussion - desc: Discussion related to BBSs - } - } - } - } - - messageNetworks: { - ftn: { - areas: { - agoranet_bbs: { /* hey kids, this matches above! */ - - // oh oh oh, and this one pairs up with a network below - network: agoranet - tag: AGN_BBS - uplinks: "46:1/100" - } - } - - networks: { - agoranet: { - localAddress: "46:3/102" - } - } - } - } - - scannerTossers: { - ftn_bso: { - schedule: { - import: every 1 hours or @watch:/home/enigma/bink/watchfile.txt - export: every 1 hours or @immediate - } - - defaultZone: 46 - defaultNetwork: agoranet - - nodes: { - "46:*": { - archiveType: ZIP - encoding: utf8 - } - } - } - } -} -``` From 2c4fdfdd5f427ecddf47dd6329dfcc997c05e3c8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 24 Nov 2018 20:02:19 -0700 Subject: [PATCH 386/569] * Moved oputil.js config import-areas to 'oputil.js mb import-areas' * 'oputil.js mb import-areas' now *optionally* binds areas to FTN networks. Otherwise only areas are imported --- core/bbs.js | 2 +- core/oputil/oputil_common.js | 35 +++ core/oputil/oputil_config.js | 339 +---------------------------- core/oputil/oputil_help.js | 18 +- core/oputil/oputil_message_base.js | 324 ++++++++++++++++++++++++++- docs/admin/oputil.md | 42 +++- 6 files changed, 407 insertions(+), 353 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 05168e72..ede2b82e 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -216,7 +216,7 @@ function initialize(cb) { }, function loadSysOpInformation(callback) { // - // Copy over some +op information from the user DB -> system propertys. + // Copy over some +op information from the user DB -> system properties. // * Makes this accessible for MCI codes, easy non-blocking access, etc. // * We do this every time as the op is free to change this information just // like any other user diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index 90328b07..e1d6c962 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -7,6 +7,11 @@ const db = require('../../core/database.js'); const _ = require('lodash'); const async = require('async'); +const inq = require('inquirer'); +const fs = require('fs'); +const hjson = require('hjson'); + +const packageJson = require('../../package.json'); exports.printUsageAndSetExitCode = printUsageAndSetExitCode; exports.getDefaultConfigPath = getDefaultConfigPath; @@ -14,6 +19,17 @@ exports.getConfigPath = getConfigPath; exports.initConfigAndDatabases = initConfigAndDatabases; exports.getAreaAndStorage = getAreaAndStorage; exports.looksLikePattern = looksLikePattern; +exports.getAnswers = getAnswers; +exports.writeConfig = writeConfig; + +const HJSONStringifyCommonOpts = exports.HJSONStringifyCommonOpts = { + emitRootBraces : true, + bracesSameLine : true, + space : 4, + keepWsc : true, + quotes : 'min', + eol : '\n', +}; const exitCodes = exports.ExitCodes = { SUCCESS : 0, @@ -100,4 +116,23 @@ function looksLikePattern(tag) { } return /[*?[\]!()+|^]/.test(tag); +} + +function getAnswers(questions, cb) { + inq.prompt(questions).then( answers => { + return cb(answers); + }); +} + +function writeConfig(config, path) { + config = hjson.stringify(config, HJSONStringifyCommonOpts) + .replace(/%ENIG_VERSION%/g, packageJson.version) + .replace(/%HJSON_VERSION%/g, hjson.version); + + try { + fs.writeFileSync(path, config, 'utf8'); + return true; + } catch(e) { + return false; + } } \ No newline at end of file diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index 52c388f7..8a51fdb5 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -9,10 +9,11 @@ const { getConfigPath, argv, ExitCodes, - initConfigAndDatabases + getAnswers, + writeConfig, + HJSONStringifyCommonOpts, } = require('./oputil_common.js'); const getHelpFor = require('./oputil_help.js').getHelpFor; -const Errors = require('../../core/enig_error.js').Errors; // deps const async = require('async'); @@ -24,17 +25,8 @@ const paths = require('path'); const _ = require('lodash'); const sanatizeFilename = require('sanitize-filename'); -const packageJson = require('../../package.json'); - exports.handleConfigCommand = handleConfigCommand; - -function getAnswers(questions, cb) { - inq.prompt(questions).then( answers => { - return cb(answers); - }); -} - const ConfigIncludeKeys = [ 'theme', 'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds', @@ -46,15 +38,6 @@ const ConfigIncludeKeys = [ 'logging.rotatingFile', ]; -const HJSONStringifyComonOpts = { - emitRootBraces : true, - bracesSameLine : true, - space : 4, - keepWsc : true, - quotes : 'min', - eol : '\n', -}; - const QUESTIONS = { Intro : [ { @@ -231,34 +214,21 @@ function askNewConfigQuestions(cb) { ); } -function writeConfig(config, path) { - config = hjson.stringify(config, HJSONStringifyComonOpts) - .replace(/%ENIG_VERSION%/g, packageJson.version) - .replace(/%HJSON_VERSION%/g, hjson.version) - ; - - try { - fs.writeFileSync(path, config, 'utf8'); - return true; - } catch(e) { - return false; - } -} - const copyFileSyncSilent = (to, from, flags) => { try { fs.copyFileSync(to, from, flags); - } catch(e) {} + } catch(e) { + /* absorb! */ + } }; function buildNewConfig() { askNewConfigQuestions( (err, configPath, config) => { - if(err) { - return; + if(err) { return; } const bn = sanatizeFilename(config.general.boardName) - .replace(/[^a-z0-9_\-]/ig, '_') + .replace(/[^a-z0-9_-]/ig, '_') .replace(/_+/g, '_') .toLowerCase(); const menuFile = `${bn}-menu.hjson`; @@ -273,7 +243,7 @@ function buildNewConfig() { paths.join(__dirname, '../../misc/prompt_template.in.hjson'), paths.join(__dirname, '../../config/', promptFile), fs.constants.COPYFILE_EXCL - ) + ); config.general.menuFile = menuFile; config.general.promptFile = promptFile; @@ -286,294 +256,10 @@ function buildNewConfig() { }); } -function validateUplinks(uplinks) { - const ftnAddress = require('../../core/ftn_address.js'); - const valid = uplinks.every(ul => { - const addr = ftnAddress.fromString(ul); - return addr; - }); - return valid; -} - -function getMsgAreaImportType(path) { - if(argv.type) { - return argv.type.toLowerCase(); - } - - const ext = paths.extname(path).toLowerCase().substr(1); - return ext; // .bbs|.na|... -} - -function importAreas() { - const importPath = argv._[argv._.length - 1]; - if(argv._.length < 3 || !importPath || 0 === importPath.length) { - return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); - } - - const importType = getMsgAreaImportType(importPath); - if('na' !== importType && 'bbs' !== importType) { - return console.error(`"${importType}" is not a recognized import file type`); - } - - // optional data - we'll prompt if for anything not found - let confTag = argv.conf; - let networkName = argv.network; - let uplinks = argv.uplinks; - if(uplinks) { - uplinks = uplinks.split(/[\s,]+/); - } - - let importEntries; - - async.waterfall( - [ - function readImportFile(callback) { - fs.readFile(importPath, 'utf8', (err, importData) => { - if(err) { - return callback(err); - } - - importEntries = getImportEntries(importType, importData); - if(0 === importEntries.length) { - return callback(Errors.Invalid('Invalid or empty import file')); - } - - // We should have enough to validate uplinks - if('bbs' === importType) { - for(let i = 0; i < importEntries.length; ++i) { - if(!validateUplinks(importEntries[i].uplinks)) { - return callback(Errors.Invalid('Invalid uplink(s)')); - } - } - } else { - if(!validateUplinks(uplinks)) { - return callback(Errors.Invalid('Invalid uplink(s)')); - } - } - - return callback(null); - }); - }, - function init(callback) { - return initConfigAndDatabases(callback); - }, - function validateAndCollectInput(callback) { - const msgArea = require('../../core/message_area.js'); - const sysConfig = require('../../core/config.js').get(); - - let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } ); - if(!msgConfs) { - return callback(Errors.DoesNotExist('No conferences exist in your configuration')); - } - - msgConfs = msgConfs.map(mc => { - return { - name : mc.conf.name, - value : mc.confTag, - }; - }); - - if(confTag && !msgConfs.find(mc => { - return confTag === mc.value; - })) - { - return callback(Errors.DoesNotExist(`Conference "${confTag}" does not exist`)); - } - - let existingNetworkNames = []; - if(_.has(sysConfig, 'messageNetworks.ftn.networks')) { - existingNetworkNames = Object.keys(sysConfig.messageNetworks.ftn.networks); - } - - if(0 === existingNetworkNames.length) { - return callback(Errors.DoesNotExist('No FTN style networks exist in your configuration')); - } - - if(networkName && !existingNetworkNames.find(net => networkName === net)) { - return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`)); - } - - getAnswers([ - { - name : 'confTag', - message : 'Message conference:', - type : 'list', - choices : msgConfs, - pageSize : 10, - when : !confTag, - }, - { - name : 'networkName', - message : 'Network name:', - type : 'list', - choices : existingNetworkNames, - when : !networkName, - }, - { - name : 'uplinks', - message : 'Uplink(s) (comma separated):', - type : 'input', - validate : (input) => { - const inputUplinks = input.split(/[\s,]+/); - return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)'; - }, - when : !uplinks && 'bbs' !== importType, - } - ], - answers => { - confTag = confTag || answers.confTag; - networkName = networkName || answers.networkName; - uplinks = uplinks || answers.uplinks; - - importEntries.forEach(ie => { - ie.areaTag = ie.ftnTag.toLowerCase(); - }); - - return callback(null); - }); - }, - function confirmWithUser(callback) { - const sysConfig = require('../../core/config.js').get(); - - console.info(`Importing the following for "${confTag}" - (${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`); - importEntries.forEach(ie => { - console.info(` ${ie.ftnTag} - ${ie.name}`); - }); - - console.info(''); - console.info('Importing will NOT create required FTN network configurations.'); - console.info('If you have not yet done this, you will need to complete additional steps after importing.'); - console.info('See docs/msg_networks.md for details.'); - console.info(''); - - getAnswers([ - { - name : 'proceed', - message : 'Proceed?', - type : 'confirm', - } - ], - answers => { - return callback(answers.proceed ? null : Errors.General('User canceled')); - }); - - }, - function loadConfigHjson(callback) { - const configPath = getConfigPath(); - fs.readFile(configPath, 'utf8', (err, confData) => { - if(err) { - return callback(err); - } - - let config; - try { - config = hjson.parse(confData, { keepWsc : true } ); - } catch(e) { - return callback(e); - } - return callback(null, config); - - }); - }, - function performImport(config, callback) { - const confAreas = { messageConferences : {} }; - confAreas.messageConferences[confTag] = { areas : {} }; - - const msgNetworks = { messageNetworks : { ftn : { areas : {} } } }; - - importEntries.forEach(ie => { - const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area - - confAreas.messageConferences[confTag].areas[ie.areaTag] = { - name : ie.name, - desc : ie.name, - }; - - msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = { - network : networkName, - tag : ie.ftnTag, - uplinks : specificUplinks - }; - }); - - - const newConfig = _.defaultsDeep(config, confAreas, msgNetworks); - const configPath = getConfigPath(); - - if(!writeConfig(newConfig, configPath)) { - return callback(Errors.UnexpectedState('Failed writing configuration')); - } - - return callback(null); - } - ], - err => { - if(err) { - console.error(err.reason ? err.reason : err.message); - } else { - const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"'; - console.info('Configuration generated.'); - console.info(`You may wish to validate changes made to ${getConfigPath()}`); - console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`); - console.info(''); - } - } - ); - -} - -function getImportEntries(importType, importData) { - let importEntries = []; - - if('na' === importType) { - // - // parse out - // TAG DESC - // - const re = /^([^\s]+)\s+([^\r\n]+)/gm; - let m; - - while( (m = re.exec(importData) )) { - importEntries.push({ - ftnTag : m[1], - name : m[2], - }); - } - } else if ('bbs' === importType) { - // - // Various formats for AREAS.BBS seem to exist. We want to support as much as possible. - // - // SBBS http://www.synchro.net/docs/sbbsecho.html#AREAS.BBS - // CODE TAG UPLINKS - // - // VADV https://www.vadvbbs.com/products/vadv/support/docs/docs_vfido.php#AREAS.BBS - // TAG UPLINKS - // - // Misc - // PATH|OTHER TAG UPLINKS - // - // Assume the second item is TAG and 1:n UPLINKS (space and/or comma sep) after (at the end) - // - const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm; - let m; - while ( (m = re.exec(importData) )) { - const tag = m[1]; - - importEntries.push({ - ftnTag : tag, - name : `Area: ${tag}`, - uplinks : m[2].split(/[\s,]+/), - }); - } - } - - return importEntries; -} - function catCurrentConfig() { try { const config = hjson.rt.parse(fs.readFileSync(getConfigPath(), 'utf8')); - const hjsonOpts = Object.assign({}, HJSONStringifyComonOpts, { + const hjsonOpts = Object.assign({}, HJSONStringifyCommonOpts, { colors : false === argv.colors ? false : true, keepWsc : false === argv.comments ? false : true, }); @@ -596,9 +282,8 @@ function handleConfigCommand() { const action = argv._[1]; switch(action) { - case 'new' : return buildNewConfig(); - case 'import-areas' : return importAreas(); - case 'cat' : return catCurrentConfig(); + case 'new' : return buildNewConfig(); + case 'cat' : return catCurrentConfig(); default : return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); } diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 4d7931e0..f6ac84ba 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -39,15 +39,8 @@ actions: actions: new generate a new/initial configuration - import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH cat cat current configuration to stdout -import-areas args: - --conf CONF_TAG specify conference tag in which to import areas - --network NETWORK specify network name/key to associate FTN areas - --uplinks UL1,UL2,... specify one or more comma separated uplinks - --type TYPE specifies area import type. valid options are "bbs" and "na" - cat args: --no-color disable color --no-comments strip any comments @@ -99,12 +92,19 @@ general information: FILE_ID a file identifier. see file.sqlite3 `, MessageBase : - `usage: oputil.js mb [] +`usage: oputil.js mb [] - actions: +actions: areafix CMD1 CMD2 ... ADDR sends an AreaFix NetMail to ADDR with the supplied command(s) one or more commands may be supplied. commands that are multi part such as "%COMPRESS ZIP" should be quoted. + import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH + +import-areas args: + --conf CONF_TAG conference tag in which to import areas + --network NETWORK network name/key to associate FTN areas + --uplinks UL1,UL2,... one or more comma separated uplinks + --type TYPE area import type. valid options are "bbs" and "na" ` }; diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index 60a99a2a..d3653caf 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -2,16 +2,25 @@ /* eslint-disable no-console */ 'use strict'; -const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; -const ExitCodes = require('./oputil_common.js').ExitCodes; -const argv = require('./oputil_common.js').argv; -const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; +const { + printUsageAndSetExitCode, + getConfigPath, + ExitCodes, + argv, + initConfigAndDatabases, + getAnswers, + writeConfig, +} = require('./oputil_common.js'); const getHelpFor = require('./oputil_help.js').getHelpFor; const Address = require('../ftn_address.js'); const Errors = require('../enig_error.js').Errors; // deps const async = require('async'); +const paths = require('path'); +const fs = require('fs'); +const hjson = require('hjson'); +const _ = require('lodash'); exports.handleMessageBaseCommand = handleMessageBaseCommand; @@ -121,6 +130,310 @@ function areaFix() { ); } +function validateUplinks(uplinks) { + const ftnAddress = require('../../core/ftn_address.js'); + const valid = uplinks.every(ul => { + const addr = ftnAddress.fromString(ul); + return addr; + }); + return valid; +} + +function getMsgAreaImportType(path) { + if(argv.type) { + return argv.type.toLowerCase(); + } + + return paths.extname(path).substr(1).toLowerCase(); // bbs|na|... +} + +function importAreas() { + const importPath = argv._[argv._.length - 1]; + if(argv._.length < 3 || !importPath || 0 === importPath.length) { + return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); + } + + const importType = getMsgAreaImportType(importPath); + if('na' !== importType && 'bbs' !== importType) { + return console.error(`"${importType}" is not a recognized import file type`); + } + + // optional data - we'll prompt if for anything not found + let confTag = argv.conf; + let networkName = argv.network; + let uplinks = argv.uplinks; + if(uplinks) { + uplinks = uplinks.split(/[\s,]+/); + } + + let importEntries; + + async.waterfall( + [ + function readImportFile(callback) { + fs.readFile(importPath, 'utf8', (err, importData) => { + if(err) { + return callback(err); + } + + importEntries = getImportEntries(importType, importData); + if(0 === importEntries.length) { + return callback(Errors.Invalid('Invalid or empty import file')); + } + + // We should have enough to validate uplinks + if('bbs' === importType) { + for(let i = 0; i < importEntries.length; ++i) { + if(!validateUplinks(importEntries[i].uplinks)) { + return callback(Errors.Invalid('Invalid uplink(s)')); + } + } + } else { + if(!validateUplinks(uplinks || [])) { + return callback(Errors.Invalid('Invalid uplink(s)')); + } + } + + return callback(null); + }); + }, + function init(callback) { + return initConfigAndDatabases(callback); + }, + function validateAndCollectInput(callback) { + const msgArea = require('../../core/message_area.js'); + const sysConfig = require('../../core/config.js').get(); + + let msgConfs = msgArea.getSortedAvailMessageConferences(null, { noClient : true } ); + if(!msgConfs) { + return callback(Errors.DoesNotExist('No conferences exist in your configuration')); + } + + msgConfs = msgConfs.map(mc => { + return { + name : mc.conf.name, + value : mc.confTag, + }; + }); + + if(confTag && !msgConfs.find(mc => { + return confTag === mc.value; + })) + { + return callback(Errors.DoesNotExist(`Conference "${confTag}" does not exist`)); + } + + const existingNetworkNames = Object.keys(_.get(sysConfig, 'messageNetworks.ftn.networks', {})); + + if(networkName && !existingNetworkNames.find(net => networkName === net)) { + return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`)); + } + + // can't use --uplinks without a network + if(!networkName && 0 === existingNetworkNames.length && uplinks) { + return callback(Errors.Invalid('Cannot use --uplinks without an FTN network to import to')); + } + + getAnswers([ + { + name : 'confTag', + message : 'Message conference:', + type : 'list', + choices : msgConfs, + pageSize : 10, + when : !confTag, + }, + { + name : 'networkName', + message : 'FTN network name:', + type : 'list', + choices : [ '-None-' ].concat(existingNetworkNames), + pageSize : 10, + when : !networkName && existingNetworkNames.length > 0, + filter : (choice) => { + return '-None-' === choice ? undefined : choice; + } + }, + ], + answers => { + confTag = confTag || answers.confTag; + networkName = networkName || answers.networkName; + uplinks = uplinks || answers.uplinks; + + importEntries.forEach(ie => { + ie.areaTag = ie.ftnTag.toLowerCase(); + }); + + return callback(null); + }); + }, + function collectUplinks(callback) { + if(!networkName || uplinks || 'bbs' === importType) { + return callback(null); + } + + getAnswers([ + { + name : 'uplinks', + message : 'Uplink(s) (comma separated):', + type : 'input', + validate : (input) => { + const inputUplinks = input.split(/[\s,]+/); + return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)'; + }, + } + ], + answers => { + uplinks = answers.uplinks; + return callback(null); + }); + }, + function confirmWithUser(callback) { + const sysConfig = require('../../core/config.js').get(); + + console.info(`Importing the following for "${confTag}"`); + console.info(`(${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`); + console.info(''); + importEntries.forEach(ie => { + console.info(` ${ie.ftnTag} - ${ie.name}`); + }); + + if(networkName) { + console.info(''); + console.info(`For FTN network: ${networkName}`); + console.info(`Uplinks: ${uplinks}`); + console.info(''); + console.info('Importing will NOT create required FTN network configurations.'); + console.info('If you have not yet done this, you will need to complete additional steps after importing.'); + console.info('See Message Networks docs for details.'); + console.info(''); + } + + getAnswers([ + { + name : 'proceed', + message : 'Proceed?', + type : 'confirm', + } + ], + answers => { + return callback(answers.proceed ? null : Errors.General('User canceled')); + }); + + }, + function loadConfigHjson(callback) { + const configPath = getConfigPath(); + fs.readFile(configPath, 'utf8', (err, confData) => { + if(err) { + return callback(err); + } + + let config; + try { + config = hjson.parse(confData, { keepWsc : true } ); + } catch(e) { + return callback(e); + } + return callback(null, config); + + }); + }, + function performImport(config, callback) { + const confAreas = { messageConferences : {} }; + confAreas.messageConferences[confTag] = { areas : {} }; + + const msgNetworks = { messageNetworks : { ftn : { areas : {} } } }; + + importEntries.forEach(ie => { + const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area + + confAreas.messageConferences[confTag].areas[ie.areaTag] = { + name : ie.name, + desc : ie.name, + }; + + if(networkName) { + msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = { + network : networkName, + tag : ie.ftnTag, + uplinks : specificUplinks + }; + } + }); + + + const newConfig = _.defaultsDeep(config, confAreas, msgNetworks); + const configPath = getConfigPath(); + + if(!writeConfig(newConfig, configPath)) { + return callback(Errors.UnexpectedState('Failed writing configuration')); + } + + return callback(null); + } + ], + err => { + if(err) { + console.error(err.reason ? err.reason : err.message); + } else { + const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"'; + console.info('Configuration generated.'); + console.info(`You may wish to validate changes made to ${getConfigPath()}`); + console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`); + console.info(''); + } + } + ); +} + +function getImportEntries(importType, importData) { + let importEntries = []; + + if('na' === importType) { + // + // parse out + // TAG DESC + // + const re = /^([^\s]+)\s+([^\r\n]+)/gm; + let m; + + while( (m = re.exec(importData) )) { + importEntries.push({ + ftnTag : m[1].trim(), + name : m[2].trim(), + }); + } + } else if ('bbs' === importType) { + // + // Various formats for AREAS.BBS seem to exist. We want to support as much as possible. + // + // SBBS http://www.synchro.net/docs/sbbsecho.html#AREAS.BBS + // CODE TAG UPLINKS + // + // VADV https://www.vadvbbs.com/products/vadv/support/docs/docs_vfido.php#AREAS.BBS + // TAG UPLINKS + // + // Misc + // PATH|OTHER TAG UPLINKS + // + // Assume the second item is TAG and 1:n UPLINKS (space and/or comma sep) after (at the end) + // + const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm; + let m; + while ( (m = re.exec(importData) )) { + const tag = m[1].trim(); + + importEntries.push({ + ftnTag : tag, + name : `Area: ${tag}`, + uplinks : m[2].trim().split(/[\s,]+/), + }); + } + } + + return importEntries; +} + function handleMessageBaseCommand() { function errUsage() { @@ -137,6 +450,7 @@ function handleMessageBaseCommand() { const action = argv._[1]; return({ - areafix : areaFix, + areafix : areaFix, + 'import-areas' : importAreas, }[action] || errUsage)(); } \ No newline at end of file diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 9db1439c..1e081afb 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -70,23 +70,18 @@ The `config` command allows sysops to perform various system configuration and m usage: optutil.js config [] actions: - new generate a new/initial configuration - import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH + new generate a new/initial configuration + cat cat current configuration to stdout -import-areas args: - --conf CONF_TAG specify conference tag in which to import areas - --network NETWORK specify network name/key to associate FTN areas - --uplinks UL1,UL2,... specify one or more comma separated uplinks - --type TYPE specifies area import type. valid options are "bbs" and "na" +cat args: + --no-color disable color + --no-comments strip any comments ``` - | Action | Description | Examples | |-----------|-------------------|---------------------------------------| | `new` | Generates a new/initial configuration | `./oputil.js config new` (follow the prompts) | -| `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file | `./oputil.js config import-areas /some/path/l33tnet.na` | - -When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args". +| `cat` | Pretty prints current `config.hjson` configuration to stdout. | `./oputil.js config cat` | ## File Base Management The `fb` command provides a powerful file base management interface. @@ -189,3 +184,28 @@ file_crc32: fc6655d file_md5: 3455f74bbbf9539e69bd38f45e039a4e file_sha1: 558fab3b49a8ac302486e023a3c2a86bd4e4b948 ``` + +## Message Base Management +The `mb` command provides various Message Base related tools: + +``` +usage: oputil.js mb [] + +actions: + areafix CMD1 CMD2 ... ADDR sends an AreaFix NetMail to ADDR with the supplied command(s) + one or more commands may be supplied. commands that are multi + part such as "%COMPRESS ZIP" should be quoted. + import-areas PATH import areas using fidonet *.NA or AREAS.BBS file from PATH + +import-areas args: + --conf CONF_TAG conference tag in which to import areas + --network NETWORK network name/key to associate FTN areas + --uplinks UL1,UL2,... one or more comma separated uplinks + --type TYPE area import type. valid options are "bbs" and "na" +``` + +| Action | Description | Examples | +|-----------|-------------------|---------------------------------------| +| `import-areas` | Imports areas using a FidoNet style *.NA or AREAS.BBS formatted file. Optionally maps areas to FTN networks. | `./oputil.js config import-areas /some/path/l33tnet.na` | + +When using the `import-areas` action, you will be prompted for any missing additional arguments described in "import-areas args". From a520b26b893e0362891f3ce3f92947993946243d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 24 Nov 2018 20:12:45 -0700 Subject: [PATCH 387/569] Doc updates on message conf/areas --- .../configuring-a-message-area.md | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/messageareas/configuring-a-message-area.md b/docs/messageareas/configuring-a-message-area.md index 5ba396ea..8e8b5a33 100644 --- a/docs/messageareas/configuring-a-message-area.md +++ b/docs/messageareas/configuring-a-message-area.md @@ -2,29 +2,28 @@ layout: page title: Configuring a Message Area --- +## Message Conferences **Message Conferences** and **Areas** allow for grouping of message base topics. -## Message Conferences -Message Conferences are the top level container for 1:n Message Areas via the `messageConferences` section -in `config.hjson`. Common message conferences may include a local conference and one or more conferences -each dedicated to a particular message network such as FsxNet, AgoraNet, etc. +## Conferences +Message Conferences are the top level container for *1:n* Message *Areas* via the `messageConferences` block in `config.hjson`. A common setup may include a local conference and one or more conferences each dedicated to a particular message network such as fsxNet, ArakNet, etc. -Each conference is represented by a entry under `messageConferences`. **The areas key is the conferences tag**. +Each conference is represented by a entry under `messageConferences`. Each entries top level key is it's *conference tag*. | Config Item | Required | Description | |-------------|----------|---------------------------------------------------------------------------------| -| `name` | :+1: | Friendly conference name | -| `desc` | :+1: | Friendly conference description | -| `sort` | :-1: | If supplied, provides a key used for sorting | +| `name` | :+1: | Friendly conference name | +| `desc` | :+1: | Friendly conference description. | +| `sort` | :-1: | Set to a number to override the default alpha-numeric sort order based on the `name` field. | | `default` | :-1: | Specify `true` to make this the default conference (e.g. assigned to new users) | -| `areas` | :+1: | Container of 1:n areas described below | +| `areas` | :+1: | Container of 1:n areas described below | ### Example ```hjson { messageConferences: { - local: { + local: { // conference tag name: Local desc: Local discussion sort: 1 @@ -35,16 +34,14 @@ Each conference is represented by a entry under `messageConferences`. **The area ``` ## Message Areas -Message Areas are topic specific containers for messages that live within a particular conference. # -**The area's key is its area tag**. For example, "General Discussion" may live under a Local conference -while an AgoraNet conference may contain "BBS Discussion". +Message Areas are topic specific containers for messages that live within a particular conference. The top level key for an area sets it's *area tag*. For example, "General Discussion" may live under a Local conference while an fsxNet conference may contain "BBS Discussion". | Config Item | Required | Description | |-------------|----------|---------------------------------------------------------------------------------| -| `name` | :+1: | Friendly area name | -| `desc` | :+1: | Friendly area discription | -| `sort` | :-1: | If supplied, provides a key used for sorting | -| `default` | :-1: | Specify `true` to make this the default area (e.g. assigned to new users) | +| `name` | :+1: | Friendly area name. | +| `desc` | :+1: | Friendly area description. | +| `sort` | :-1: | Set to a number to override the default alpha-numeric sort order based on the `name` field. | +| `default` | :-1: | Specify `true` to make this the default area (e.g. assigned to new users) | ### Example @@ -62,4 +59,7 @@ messageConferences: { } } } -``` \ No newline at end of file +``` + +## Importing +FidoNet style `.na` files as well as legacy `AREAS.BBS` files in common formats can be imported using `oputil.js mb import-areas`. See [The oputil CLI](/docs/admin/oputil.md) for more information and usage. From 4ee7d7a5409b28ac5fe0c0b959b6f5f635bd36e5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Nov 2018 10:26:42 -0700 Subject: [PATCH 388/569] Arrange a bit and add global newscan --- art/themes/luciano_blocktronics/MMENU.ANS | Bin 3492 -> 3527 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index 4a058fc910136e6839ec43a91e6b84fb2c5ac686..75251877665dfef7961dfa9376f8ed1b294279ad 100644 GIT binary patch delta 146 zcmZ1?eO!9ORSq#jGiT{&!(3G-KaCts1?gymTuy+nG$RSq#D183=I!(3G-KaCts1?gymTv+&LZ Js$9gY3IMYXB5nWx From e464f95924204863c0bfb4617583d7db16d03af8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Nov 2018 10:35:05 -0700 Subject: [PATCH 389/569] UT MCI now displays theme name, UD displays ID --- core/predefined_mci.js | 5 ++++- docs/art/mci.md | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/predefined_mci.js b/core/predefined_mci.js index c83f8353..cda9f5b4 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -91,7 +91,10 @@ const PREDEFINED_MCI_GENERATORS = { UE : function emailAddres(client) { return userStatAsString(client, UserProps.EmailAddress, ''); }, UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); }, UF : function affils(client) { return userStatAsString(client, UserProps.Affiliations, ''); }, - UT : function themeId(client) { return userStatAsString(client, UserProps.ThemeId, ''); }, + UT : function themeName(client) { + return _.get(client, 'currentTheme.info.name', userStatAsString(client, UserProps.ThemeId, '')); + }, + UD : function themeId(client) { return userStatAsString(client, UserProps.ThemeId, ''); }, UC : function loginCount(client) { return userStatAsString(client, UserProps.LoginCount, 0); }, ND : function connectedNode(client) { return client.node.toString(); }, IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version diff --git a/docs/art/mci.md b/docs/art/mci.md index bc6bab5a..04e177c2 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -35,7 +35,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `UE` | Current user's email address | | `UW` | Current user's web address | | `UF` | Current user's affiliations | -| `UT` | Current user's *theme ID* (e.g. "luciano_blocktronics") | +| `UT` | Current user's theme name | +| `UD` | Current user's *theme ID* (e.g. "luciano_blocktronics") | | `UC` | Current user's login/call count | | `ND` | Current user's connected node number | | `IP` | Current user's IP address | From ec97b3e8d405ea62b1f862be9b6b9c88da015ebc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Nov 2018 19:05:16 -0700 Subject: [PATCH 390/569] More user/system stat constants & usage --- core/bbs.js | 7 ++++--- core/config.js | 2 +- core/database.js | 8 -------- core/file_area_web.js | 11 +++++++---- core/file_base_area.js | 3 ++- core/file_base_area_select.js | 3 ++- core/file_transfer.js | 20 ++++++++++++-------- core/last_callers.js | 3 ++- core/predefined_mci.js | 29 +++++++++++++++-------------- core/rumorz.js | 21 +++++++++++++-------- core/stat_log.js | 3 +-- core/system_log.js | 11 +++++++++++ core/system_property.js | 25 +++++++++++++++++++++++++ core/user_login.js | 6 ++++-- 14 files changed, 99 insertions(+), 53 deletions(-) create mode 100644 core/system_log.js create mode 100644 core/system_property.js diff --git a/core/bbs.js b/core/bbs.js index ede2b82e..000b19a8 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -11,6 +11,7 @@ const logger = require('./logger.js'); const database = require('./database.js'); const resolvePath = require('./misc_util.js').resolvePath; const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps const async = require('async'); @@ -246,13 +247,13 @@ function initialize(cb) { if(err) { propLoadOpts.names.concat('username').forEach(v => { - StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); + StatLog.setNonPersistentSystemStat(`sysop_${v}`, 'N/A'); }); } else { opProps.username = opUserName; _.each(opProps, (v, k) => { - StatLog.setNonPeristentSystemStat(`sysop_${k}`, v); + StatLog.setNonPersistentSystemStat(`sysop_${k}`, v); }); } @@ -265,7 +266,7 @@ function initialize(cb) { getAreaStats( (err, stats) => { if(!err) { const StatLog = require('./stat_log.js'); - StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats); } return callback(null); diff --git a/core/config.js b/core/config.js index 701c8494..a90ff50d 100644 --- a/core/config.js +++ b/core/config.js @@ -969,7 +969,7 @@ function getDefaultConfig() { statLog : { systemEvents : { - loginHistoryMax: -1 // forever + loginHistoryMax: 5000, // set to -1 for forever } } }; diff --git a/core/database.js b/core/database.js index 017aa949..90f15692 100644 --- a/core/database.js +++ b/core/database.js @@ -189,14 +189,6 @@ const DB_INIT_TABLE = { );` ); - dbs.user.run( - `CREATE TABLE IF NOT EXISTS user_login_history ( - user_id INTEGER NOT NULL, - user_name VARCHAR NOT NULL, - timestamp DATETIME NOT NULL - );` - ); - return cb(null); }, diff --git a/core/file_area_web.js b/core/file_area_web.js index a1d12989..b36c8b72 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -15,6 +15,8 @@ const Log = require('./logger.js').log; const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; const Events = require('./events.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_menu_method.js'); // deps const hashids = require('hashids'); @@ -470,10 +472,11 @@ class FileAreaWebAccess { }); }, function updateStats(user, callback) { - StatLog.incrementUserStat(user, 'dl_total_count', 1); - StatLog.incrementUserStat(user, 'dl_total_bytes', dlBytes); - StatLog.incrementSystemStat('dl_total_count', 1); - StatLog.incrementSystemStat('dl_total_bytes', dlBytes); + StatLog.incrementUserStat(user, UserProps.FileDlTotalCount, 1); + StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes); + + StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1); + StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes); return callback(null, user); }, diff --git a/core/file_base_area.js b/core/file_base_area.js index 760eb87c..f699a543 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -15,6 +15,7 @@ const stringFormat = require('./string_format.js'); const wordWrapText = require('./word_wrap.js').wordWrapText; const StatLog = require('./stat_log.js'); const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps const _ = require('lodash'); @@ -1012,7 +1013,7 @@ function getAreaStats(cb) { function updateAreaStatsScheduledEvent(args, cb) { getAreaStats( (err, stats) => { if(!err) { - StatLog.setNonPeristentSystemStat('file_base_area_stats', stats); + StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats); } return cb(err); diff --git a/core/file_base_area_select.js b/core/file_base_area_select.js index 1b77eb21..a8f74322 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -5,6 +5,7 @@ const MenuModule = require('./menu_module.js').MenuModule; const { getSortedAvailableFileAreas } = require('./file_base_area.js'); const StatLog = require('./stat_log.js'); +const SysProps = require('./system_property.js'); // deps const async = require('async'); @@ -52,7 +53,7 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { async.waterfall( [ function mergeAreaStats(callback) { - const areaStats = StatLog.getSystemStat('file_base_area_stats') || { areas : {} }; + const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || { areas : {} }; // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override const availAreas = getSortedAvailableFileAreas(self.client); diff --git a/core/file_transfer.js b/core/file_transfer.js index 340ca6b9..83dedc13 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -11,6 +11,8 @@ const StatLog = require('./stat_log.js'); const FileEntry = require('./file_entry.js'); const Log = require('./logger.js').log; const Events = require('./events.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps const async = require('async'); @@ -479,10 +481,11 @@ exports.getModule = class TransferFileModule extends MenuModule { }); }, () => { // All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks - StatLog.incrementUserStat(this.client.user, 'dl_total_count', downloadCount); - StatLog.incrementUserStat(this.client.user, 'dl_total_bytes', downloadBytes); - StatLog.incrementSystemStat('dl_total_count', downloadCount); - StatLog.incrementSystemStat('dl_total_bytes', downloadBytes); + StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalCount, downloadCount); + StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalBytes, downloadBytes); + + StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount); + StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes); fileIds.forEach(fileId => { FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); @@ -509,10 +512,11 @@ exports.getModule = class TransferFileModule extends MenuModule { return next(null); }); }, () => { - StatLog.incrementUserStat(this.client.user, 'ul_total_count', uploadCount); - StatLog.incrementUserStat(this.client.user, 'ul_total_bytes', uploadBytes); - StatLog.incrementSystemStat('ul_total_count', uploadCount); - StatLog.incrementSystemStat('ul_total_bytes', uploadBytes); + StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalCount, uploadCount); + StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalBytes, uploadBytes); + + StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount); + StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes); return cb(null); }); diff --git a/core/last_callers.js b/core/last_callers.js index 6b25caf6..c80ce336 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -8,6 +8,7 @@ const User = require('./user.js'); const sysDb = require('./database.js').dbs.system; const { Errors } = require('./enig_error.js'); const UserProps = require('./user_property.js'); +const SysLogKeys = require('./system_log.js'); // deps const moment = require('moment'); @@ -91,7 +92,7 @@ exports.getModule = class LastCallersModule extends MenuModule { } StatLog.getSystemLogEntries( - 'user_login_history', + SysLogKeys.UserLoginHistory, StatLog.Order.TimestampDesc, 200, // max items to fetch - we need more than max displayed for filtering/etc. (err, loginHistory) => { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index cda9f5b4..5b92e0e3 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -14,6 +14,7 @@ const FileBaseFilters = require('./file_base_filter.js'); const { formatByteSize } = require('./string_util.js'); const ANSI = require('./ansi_term.js'); const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps const packageJson = require('../package.json'); @@ -34,7 +35,7 @@ function setNextRandomRumor(cb) { entry = entry[0]; } const randRumor = entry && entry.log_value ? entry.log_value : ''; - StatLog.setNonPeristentSystemStat('random_rumor', randRumor); + StatLog.setNonPersistentSystemStat(SysProps.NextRandomRumor, randRumor); if(cb) { return cb(null); } @@ -67,12 +68,12 @@ const PREDEFINED_MCI_GENERATORS = { VN : function version() { return packageJson.version; }, // +op info - SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, - SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, - SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, - SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, - SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, - SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, + SN : function opUserName() { return StatLog.getSystemStat(SysProps.SysOpUsername); }, + SR : function opRealName() { return StatLog.getSystemStat(SysProps.SysOpRealName); }, + SL : function opLocation() { return StatLog.getSystemStat(SysProps.SysOpLocation); }, + SA : function opAffils() { return StatLog.getSystemStat(SysProps.SysOpAffiliations); }, + SS : function opSex() { return StatLog.getSystemStat(SysProps.SysOpSex); }, + SE : function opEmail() { return StatLog.getSystemStat(SysProps.SysOpEmailAddress); }, // :TODO: op age, web, ????? // @@ -189,7 +190,7 @@ const PREDEFINED_MCI_GENERATORS = { AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, - TC : function totalCalls() { return StatLog.getSystemStat('login_count').toLocaleString(); }, + TC : function totalCalls() { return StatLog.getSystemStat(SysProps.LoginCount).toLocaleString(); }, RR : function randomRumor() { // start the process of picking another random one @@ -203,22 +204,22 @@ const PREDEFINED_MCI_GENERATORS = { // // :TODO: DD - Today's # of downloads (iNiQUiTY) // - SD : function systemNumDownloads() { return sysStatAsString('dl_total_count', 0); }, + SD : function systemNumDownloads() { return sysStatAsString(SysProps.FileDlTotalCount, 0); }, SO : function systemByteDownload() { - const byteSize = StatLog.getSystemStatNum('dl_total_bytes'); + const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, - SU : function systemNumUploads() { return sysStatAsString('ul_total_count', 0); }, + SU : function systemNumUploads() { return sysStatAsString(SysProps.FileUlTotalCount, 0); }, SP : function systemByteUpload() { - const byteSize = StatLog.getSystemStatNum('ul_total_bytes'); + const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, TF : function totalFilesOnSystem() { - const areaStats = StatLog.getSystemStat('file_base_area_stats'); + const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); return _.get(areaStats, 'totalFiles', 0).toLocaleString(); }, TB : function totalBytesOnSystem() { - const areaStats = StatLog.getSystemStat('file_base_area_stats'); + const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); return formatByteSize(totalBytes, true); // true=withAbbr }, diff --git a/core/rumorz.js b/core/rumorz.js index 31aea104..51e58d53 100644 --- a/core/rumorz.js +++ b/core/rumorz.js @@ -8,6 +8,7 @@ const theme = require('./theme.js'); const resetScreen = require('./ansi_term.js').resetScreen; const StatLog = require('./stat_log.js'); const renderStringLength = require('./string_util.js').renderStringLength; +const SystemLogKeys = require('./system_log.js'); // deps const async = require('async'); @@ -20,8 +21,6 @@ exports.moduleInfo = { packageName : 'codes.l33t.enigma.rumorz', }; -const STATLOG_KEY_RUMORZ = 'system_rumorz'; - const FormIds = { View : 0, Add : 1, @@ -52,10 +51,16 @@ exports.getModule = class RumorzModule extends MenuModule { if(_.isString(formData.value.rumor) && renderStringLength(formData.value.rumor) > 0) { const rumor = formData.value.rumor.trim(); // remove any trailing ws - StatLog.appendSystemLogEntry(STATLOG_KEY_RUMORZ, rumor, StatLog.KeepDays.Forever, StatLog.KeepType.Forever, () => { - this.clearAddForm(); - return this.displayViewScreen(true, cb); // true=cls - }); + StatLog.appendSystemLogEntry( + SystemLogKeys.UserAddedRumorz, + rumor, + StatLog.KeepDays.Forever, + StatLog.KeepType.Forever, + () => { + this.clearAddForm(); + return this.displayViewScreen(true, cb); // true=cls + } + ); } else { // empty message - treat as if cancel was hit return this.displayViewScreen(true, cb); // true=cls @@ -149,7 +154,7 @@ exports.getModule = class RumorzModule extends MenuModule { function fetchEntries(callback) { const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries); - StatLog.getSystemLogEntries(STATLOG_KEY_RUMORZ, StatLog.Order.Timestamp, (err, entries) => { + StatLog.getSystemLogEntries(SystemLogKeys.UserAddedRumorz, StatLog.Order.Timestamp, (err, entries) => { return callback(err, entriesView, entries); }); }, @@ -158,7 +163,7 @@ exports.getModule = class RumorzModule extends MenuModule { return { text : e.log_value, // standard rumor : e.log_value, - } + }; })); entriesView.redraw(); diff --git a/core/stat_log.js b/core/stat_log.js index 173222b9..80840ddb 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -70,8 +70,7 @@ class StatLog { }; } - // :TODO: fix spelling :) - setNonPeristentSystemStat(statName, statValue) { + setNonPersistentSystemStat(statName, statValue) { this.systemStats[statName] = statValue; } diff --git a/core/system_log.js b/core/system_log.js new file mode 100644 index 00000000..e753c68b --- /dev/null +++ b/core/system_log.js @@ -0,0 +1,11 @@ +/* jslint node: true */ +'use strict'; + +// +// Common SYSTEM/global log keys +// +module.exports = { + UserAddedRumorz : 'system_rumorz', + UserLoginHistory : 'user_login_history', +}; + diff --git a/core/system_property.js b/core/system_property.js new file mode 100644 index 00000000..f2743088 --- /dev/null +++ b/core/system_property.js @@ -0,0 +1,25 @@ +/* jslint node: true */ +'use strict'; + +// +// Common SYSTEM/global properties/stats used throughout the system. +// +// This IS NOT a full list. Custom modules & the like can create +// their own! +// +module.exports = { + LoginCount : 'login_count', + + FileBaseAreaStats : 'file_base_area_stats', // object - see file_base_area.js::getAreaStats + FileUlTotalCount : 'ul_total_count', + FileUlTotalBytes : 'ul_total_bytes', + + SysOpUsername : 'sysop_username', + SysOpRealName : 'sysop_real_name', + SysOpLocation : 'sysop_location', + SysOpAffiliations : 'sysop_affiliation', + SysOpSex : 'sysop_sex', + SysOpEmailAddress : 'sysop_email_address', + + NextRandomRumor : 'random_rumor', +}; diff --git a/core/user_login.js b/core/user_login.js index a46be5b1..b02066a1 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -13,6 +13,8 @@ const { ErrorReasons } = require('./enig_error.js'); const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); +const SystemLogKeys = require('./system_log.js'); // deps const async = require('async'); @@ -86,7 +88,7 @@ function userLogin(client, username, password, cb) { return callback(null); }, function updateSystemLoginCount(callback) { - return StatLog.incrementSystemStat('login_count', 1, callback); // :TODO: create system_property.js + return StatLog.incrementSystemStat(SysProps.LoginCount, 1, callback); }, function recordLastLogin(callback) { return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback); @@ -102,7 +104,7 @@ function userLogin(client, username, password, cb) { }); return StatLog.appendSystemLogEntry( - 'user_login_history', + SystemLogKeys.UserLoginHistory, historyItem, loginHistoryMax, StatLog.KeepType.Max, From f471fd0ebecbd38b7c0c523c8dc37517ffd8d2e0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Nov 2018 20:13:48 -0700 Subject: [PATCH 391/569] Finally implement "Total Calls Today" MCI: TT + Add findSystemLogEntries() to StatLog amongst others --- art/themes/luciano_blocktronics/SYSSTAT.ANS | Bin 2860 -> 2877 bytes art/themes/luciano_blocktronics/theme.hjson | 1 + core/bbs.js | 17 +++ core/database.js | 2 +- core/predefined_mci.js | 7 +- core/stat_log.js | 130 ++++++++++++++------ core/system_property.js | 3 + core/user_login.js | 1 + docs/art/mci.md | 3 +- 9 files changed, 121 insertions(+), 43 deletions(-) diff --git a/art/themes/luciano_blocktronics/SYSSTAT.ANS b/art/themes/luciano_blocktronics/SYSSTAT.ANS index 97beb53d8a39949e2982fb876b065d599cc6d492..19a3d39e0b11353a7c8b7415742ea3870a3c6fcb 100644 GIT binary patch delta 42 xcmZ1@wpVOJJ*SX#v_Y { + if(!err) { + StatLog.setNonPersistentSystemStat(SysProps.LoginsToday, callsToday); + } + return callback(null); + }); + }, function initMCI(callback) { return require('./predefined_mci.js').init(callback); }, diff --git a/core/database.js b/core/database.js index 90f15692..55fdb1c7 100644 --- a/core/database.js +++ b/core/database.js @@ -73,7 +73,7 @@ function getISOTimestampString(ts) { } ts = moment(ts); } - return ts.utc().format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); + return ts.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); } function sanatizeString(s) { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 5b92e0e3..39310559 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -15,6 +15,7 @@ const { formatByteSize } = require('./string_util.js'); const ANSI = require('./ansi_term.js'); const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); +const SysLogKeys = require('./system_log.js'); // deps const packageJson = require('../package.json'); @@ -30,7 +31,7 @@ function init(cb) { } function setNextRandomRumor(cb) { - StatLog.getSystemLogEntries('system_rumorz', StatLog.Order.Random, 1, (err, entry) => { + StatLog.getSystemLogEntries(SysLogKeys.UserAddedRumorz, StatLog.Order.Random, 1, (err, entry) => { if(entry) { entry = entry[0]; } @@ -191,6 +192,9 @@ const PREDEFINED_MCI_GENERATORS = { AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, TC : function totalCalls() { return StatLog.getSystemStat(SysProps.LoginCount).toLocaleString(); }, + TT : function totalCallsToday() { + return StatLog.getSystemStat(SysProps.LoginsToday).toLocaleString(); + }, RR : function randomRumor() { // start the process of picking another random one @@ -227,7 +231,6 @@ const PREDEFINED_MCI_GENERATORS = { // :TODO: PT - Messages posted *today* (Obv/2) // -> Include FTN/etc. // :TODO: NT - New users today (Obv/2) - // :TODO: CT - Calls *today* (Obv/2) // :TODO: FT - Files uploaded/added *today* (Obv/2) // :TODO: DD - Files downloaded *today* (iNiQUiTY) // :TODO: TP - total message/posts on the system (Obv/2) diff --git a/core/stat_log.js b/core/stat_log.js index 80840ddb..805198bd 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -5,9 +5,11 @@ const sysDb = require('./database.js').dbs.system; const { getISOTimestampString } = require('./database.js'); +const Errors = require('./enig_error.js'); // deps const _ = require('lodash'); +const moment = require('moment'); /* System Event Log & Stats @@ -74,6 +76,19 @@ class StatLog { this.systemStats[statName] = statValue; } + incrementNonPersistentSystemStat(statName, incrementBy) { + incrementBy = incrementBy || 1; + + let newValue = parseInt(this.systemStats[statName]); + if(!isNaN(newValue)) { + newValue += incrementBy; + } else { + newValue = incrementBy; + } + this.setNonPersistentSystemStat(statName, newValue); + return newValue; + } + setSystemStat(statName, statValue, cb) { // live stats this.systemStats[statName] = statValue; @@ -99,19 +114,7 @@ class StatLog { } incrementSystemStat(statName, incrementBy, cb) { - incrementBy = incrementBy || 1; - - let newValue = parseInt(this.systemStats[statName]); - if(newValue) { - if(!_.isNumber(newValue)) { - return cb(new Error(`Value for ${statName} is not a number!`)); - } - - newValue += incrementBy; - } else { - newValue = incrementBy; - } - + const newValue = this.incrementNonPersistentSystemStat(statName, incrementBy); return this.setSystemStat(statName, newValue, cb); } @@ -216,26 +219,78 @@ class StatLog { ); } - getSystemLogEntries(logName, order, limit, cb) { - let sql = - `SELECT timestamp, log_value - FROM system_event_log - WHERE log_name = ?`; + /* + Find System Log entries by |filter|: - switch(order) { - case 'timestamp' : - case 'timestamp_asc' : - sql += ' ORDER BY timestamp ASC'; - break; - - case 'timestamp_desc' : - sql += ' ORDER BY timestamp DESC'; - break; - - case 'random' : - sql += ' ORDER BY RANDOM()'; + filter.logName (required) + filter.resultType = (obj) | count + where obj contains timestamp and log_value + filter.limit + filter.date - exact date to filter against + filter.order = (timestamp) | timestamp_asc | timestamp_desc | random + */ + findSystemLogEntries(filter, cb) { + filter = filter || {}; + if(!_.isString(filter.logName)) { + return cb(Errors.MissingParam('filter.logName is required')); } + filter.resultType = filter.resultType || 'obj'; + filter.order = filter.order || 'timestamp'; + + let sql; + if('count' === filter.resultType) { + sql = + `SELECT COUNT() AS count + FROM system_event_log`; + } else { + sql = + `SELECT timestamp, log_value + FROM system_event_log`; + } + + sql += ' WHERE log_name = ?'; + + if(filter.date) { + filter.date = moment(filter.date); + sql += ` AND DATE(timestamp) = DATE("${filter.date.format('YYYY-MM-DD')}")`; + } + + if('count' !== filter.resultType) { + switch(filter.order) { + case 'timestamp' : + case 'timestamp_asc' : + sql += ' ORDER BY timestamp ASC'; + break; + + case 'timestamp_desc' : + sql += ' ORDER BY timestamp DESC'; + break; + + case 'random' : + sql += ' ORDER BY RANDOM()'; + break; + } + } + + if(_.isNumber(filter.limit) && 0 !== filter.limit) { + sql += ` LIMIT ${filter.limit}`; + } + + sql += ';'; + + if('count' === filter.resultType) { + sysDb.get(sql, [ filter.logName ], (err, row) => { + return cb(err, row ? row.count : 0); + }); + } else { + sysDb.all(sql, [ filter.logName ], (err, rows) => { + return cb(err, rows); + }); + } + } + + getSystemLogEntries(logName, order, limit, cb) { if(!cb && _.isFunction(limit)) { cb = limit; limit = 0; @@ -243,15 +298,12 @@ class StatLog { limit = limit || 0; } - if(0 !== limit) { - sql += ` LIMIT ${limit}`; - } - - sql += ';'; - - sysDb.all(sql, [ logName ], (err, rows) => { - return cb(err, rows); - }); + const filter = { + logName, + order, + limit, + }; + return this.findSystemLogEntries(filter, cb); } appendUserLogEntry(user, logName, logValue, keepDays, cb) { diff --git a/core/system_property.js b/core/system_property.js index f2743088..52830a06 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -9,17 +9,20 @@ // module.exports = { LoginCount : 'login_count', + LoginsToday : 'logins_today', // non-persistent FileBaseAreaStats : 'file_base_area_stats', // object - see file_base_area.js::getAreaStats FileUlTotalCount : 'ul_total_count', FileUlTotalBytes : 'ul_total_bytes', + // begin +op non-persistent... SysOpUsername : 'sysop_username', SysOpRealName : 'sysop_real_name', SysOpLocation : 'sysop_location', SysOpAffiliations : 'sysop_affiliation', SysOpSex : 'sysop_sex', SysOpEmailAddress : 'sysop_email_address', + // end +op non-persistent NextRandomRumor : 'random_rumor', }; diff --git a/core/user_login.js b/core/user_login.js index b02066a1..2959e3a7 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -88,6 +88,7 @@ function userLogin(client, username, password, cb) { return callback(null); }, function updateSystemLoginCount(callback) { + StatLog.incrementNonPersistentSystemStat(SysProps.LoginsToday, 1); return StatLog.incrementSystemStat(SysProps.LoginCount, 1, callback); }, function recordLastLogin(callback) { diff --git a/docs/art/mci.md b/docs/art/mci.md index 04e177c2..4f351dad 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -65,7 +65,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `SC` | System CPU model | | `NV` | System underlying Node.js version | | `AN` | Current active node count | -| `TC` | Total login/calls to system | +| `TC` | Total login/calls to the system *ever* | +| `TT` | Total login/calls to the system *today* | | `RR` | Displays a random rumor | | `SD` | Total downloads, system wide | | `SO` | Total downloaded amount, system wide (formatted to appropriate bytes/megs/etc.) | From e606ec6f63f42f409bc8229f9c33c0e3414d2467 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Nov 2018 20:29:36 -0700 Subject: [PATCH 392/569] Fix ISO timestamps hopefully --- core/database.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database.js b/core/database.js index 55fdb1c7..7193adee 100644 --- a/core/database.js +++ b/core/database.js @@ -73,7 +73,7 @@ function getISOTimestampString(ts) { } ts = moment(ts); } - return ts.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); + return ts.format('YYYY-MM-DDTHH:mm:ss.SSS'); } function sanatizeString(s) { From 4b5f26b31bdcf7d84aa463613b526e411e15ae44 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Nov 2018 20:31:25 -0700 Subject: [PATCH 393/569] Record offeset again... dur --- core/database.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database.js b/core/database.js index 7193adee..e4a812d4 100644 --- a/core/database.js +++ b/core/database.js @@ -73,7 +73,7 @@ function getISOTimestampString(ts) { } ts = moment(ts); } - return ts.format('YYYY-MM-DDTHH:mm:ss.SSS'); + return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } function sanatizeString(s) { From fb13381bb5daeb3c55d24addfd221a37ffae9762 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 27 Nov 2018 19:45:36 -0700 Subject: [PATCH 394/569] Default back to 'forever' for login history. It's small data... --- core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index a90ff50d..3f4aeb61 100644 --- a/core/config.js +++ b/core/config.js @@ -969,7 +969,7 @@ function getDefaultConfig() { statLog : { systemEvents : { - loginHistoryMax: 5000, // set to -1 for forever + loginHistoryMax: -1, // set to -1 for forever } } }; From 6cce013187df1fcaff85ce862a21275e377f52cc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 27 Nov 2018 21:21:00 -0700 Subject: [PATCH 395/569] + MCI: PT - total messages posted today (non-private) + MCI: TP - total messages/posts on system (non-private, includes imports, only counts *current*, not all of time) * Move some stats to startup() calls * Fix some DATE() comparisons in SQL to use 'localtime' as our timestamps include TZ * Update luciano_blocktronics SYSSTAT to show more info --- art/themes/luciano_blocktronics/SYSSTAT.ANS | Bin 2877 -> 3066 bytes art/themes/luciano_blocktronics/theme.hjson | 7 ++-- core/bbs.js | 14 ++------ core/file_base_area.js | 20 ++++++++++- core/fse.js | 6 +++- core/message.js | 8 ++++- core/message_area.js | 36 ++++++++++++++++++++ core/msg_area_post_fse.js | 2 +- core/predefined_mci.js | 16 ++++++--- core/scanner_tossers/ftn_bso.js | 10 ++++-- core/stat_log.js | 2 +- core/system_property.js | 3 ++ 12 files changed, 98 insertions(+), 26 deletions(-) diff --git a/art/themes/luciano_blocktronics/SYSSTAT.ANS b/art/themes/luciano_blocktronics/SYSSTAT.ANS index 19a3d39e0b11353a7c8b7415742ea3870a3c6fcb..a76e3bd654c45d46566905990ca88b74c0b5e868 100644 GIT binary patch delta 493 zcmdlh_Dg(1qn~uNv3ag^w3#!IVU(-j3KTHR&6JKdum*D7eH}sE)ljZNS!z*nW_})* zf&y3@h%(Gob@DR=DgYW{XgRUhLLVXpF%HaD4f8YN0`q}N41g9H<|+UIM33dfi{X+G zz+stxHGILX*G745;XQ?`em;ps5>u|VofRuuq zJDHDDO~@D&PGB?c-?^`FN8vu#lo7{?#YteDgfF0f*t?> delta 261 zcmew*zE^BQBM+B?f^@X8d9HM{k?F+q7RY?V$!3hUjoHuJR6|=Gf zb*Vb}8BSi#K84@dELSzm$4ENb0BDk-<>V-icu}xYAkW&^Aa^6s9FUapWC2cBRxoFB zBd5sZX`F_W_jAZie#U7vIf+wI2dr2D2q4N$oq<}+a+3>6q2?$kSb?QfgPlz#@8vX| zY{H?!WNbKDic5t>I@%QIMlLn6lFZyxn0lyk=Mb~Wd0cuxH5Sg3`M8fUUFDj5javl( DX{|*^ diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index a5100e6b..330075ff 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -127,15 +127,16 @@ mainMenuSystemStats: { mci: { BN1: { width: 17 } - VL2: { width: 17 } + VN2: { width: 17 } OS3: { width: 33 } SC4: { width: 33 } - DT5: { width: 33 } - CT6: { width: 33 } AN7: { width: 6 } ND8: { width: 6 } TC9: { width: 6 } TT11: { width: 6 } + PT12: { width: 6 } + TP13: { width: 6 } + NV14: { width: 17 } } } diff --git a/core/bbs.js b/core/bbs.js index c154f285..4d371fb3 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -263,17 +263,6 @@ function initialize(cb) { } ); }, - function initFileAreaStats(callback) { - const getAreaStats = require('./file_base_area.js').getAreaStats; - getAreaStats( (err, stats) => { - if(!err) { - const StatLog = require('./stat_log.js'); - StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats); - } - - return callback(null); - }); - }, function initCallsToday(callback) { const StatLog = require('./stat_log.js'); const filter = { @@ -289,6 +278,9 @@ function initialize(cb) { return callback(null); }); }, + function initMessageStats(callback) { + return require('./message_area.js').startup(callback); + }, function initMCI(callback) { return require('./predefined_mci.js').init(callback); }, diff --git a/core/file_base_area.js b/core/file_base_area.js index f699a543..293ab265 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -57,7 +57,25 @@ const WellKnownAreaTags = exports.WellKnownAreaTags = { }; function startup(cb) { - return cleanUpTempSessionItems(cb); + async.series( + [ + (callback) => { + return cleanUpTempSessionItems(callback); + }, + (callback) => { + getAreaStats( (err, stats) => { + if(!err) { + StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats); + } + + return callback(null); + }); + } + ], + err => { + return cb(err); + } + ); } function isInternalArea(areaTag) { diff --git a/core/fse.js b/core/fse.js index 9ecf3bdd..d294f10b 100644 --- a/core/fse.js +++ b/core/fse.js @@ -25,6 +25,7 @@ const Config = require('./config.js').get; const { getAddressedToInfo } = require('./mail_util.js'); const Events = require('./events.js'); const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps const async = require('async'); @@ -470,7 +471,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul ); } - updateUserStats(cb) { + updateUserAndSystemStats(cb) { if(Message.isPrivateAreaTag(this.message.areaTag)) { Events.emit(Events.getSystemEvents().UserSendMail, { user : this.client.user }); if(cb) { @@ -480,6 +481,9 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } Events.emit(Events.getSystemEvents().UserPostMessage, { user : this.client.user, areaTag : this.message.areaTag }); + + StatLog.incrementNonPersistentSystemStat(SysProps.MessageTotalCount, 1); + StatLog.incrementNonPersistentSystemStat(SysProps.MessagesToday, 1); return StatLog.incrementUserStat(this.client.user, UserProps.MessagePostCount, 1, cb); } diff --git a/core/message.js b/core/message.js index 768cd116..df0804d5 100644 --- a/core/message.js +++ b/core/message.js @@ -239,7 +239,10 @@ module.exports = class Message { filter.toUserName filter.fromUserName filter.replyToMesageId - filter.newerThanTimestamp + + filter.newerThanTimestamp - may not be used with |date| + filter.date - moment object - may not be used with |newerThanTimestamp| + filter.newerThanMessageId filter.areaTag - note if you want by conf, send in all areas for a conf *filter.metaTuples - {category, name, value} @@ -356,7 +359,10 @@ module.exports = class Message { }); if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { + // :TODO: should be using "localtime" here? appendWhereClause(`DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); + } else if(moment.isMoment(filter.date)) { + appendWhereClause(`DATE(m.modified_timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`); } if(_.isNumber(filter.newerThanMessageId)) { diff --git a/core/message_area.js b/core/message_area.js index a465b235..7f4993ec 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -9,12 +9,17 @@ const Log = require('./logger.js').log; const msgNetRecord = require('./msg_network.js').recordMessage; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; const UserProps = require('./user_property.js'); +const StatLog = require('./stat_log.js'); +const SysProps = require('./system_property.js'); // deps const async = require('async'); const _ = require('lodash'); const assert = require('assert'); +const moment = require('moment'); +exports.startup = startup; +exports.shutdown = shutdown; exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; @@ -35,6 +40,37 @@ exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; exports.persistMessage = persistMessage; exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; +function startup(cb) { + // by default, private messages are NOT included + async.series( + [ + (callback) => { + Message.findMessages( { resultType : 'count' }, (err, count) => { + if(count) { + StatLog.setNonPersistentSystemStat(SysProps.MessageTotalCount, count); + } + return callback(err); + }); + }, + (callback) => { + Message.findMessages( { resultType : 'count', date : moment() }, (err, count) => { + if(count) { + StatLog.setNonPersistentSystemStat(SysProps.MessagesToday, count); + } + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); +} + +function shutdown(cb) { + return cb(null); +} + function getAvailableMessageConferences(client, options) { options = options || { includeSystemInternal : false }; diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 123ce13c..613cee04 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -38,7 +38,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { return persistMessage(msg, callback); }, function updateStats(callback) { - self.updateUserStats(callback); + self.updateUserAndSystemStats(callback); } ], function complete(err) { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 39310559..76b34fd5 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -155,13 +155,14 @@ const PREDEFINED_MCI_GENERATORS = { // // Date/Time // - // :TODO: change to CD for 'Current Date' DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, // // OS/System Info // + // https://github.com/nodejs/node-v0.x-archive/issues/25769 + // OS : function operatingSystem() { return { linux : 'Linux', @@ -169,6 +170,9 @@ const PREDEFINED_MCI_GENERATORS = { win32 : 'Windows', sunos : 'SunOS', freebsd : 'FreeBSD', + android : 'Android', + openbsd : 'OpenBSD', + aix : 'IBM AIX', }[os.platform()] || os.type(); }, @@ -227,14 +231,16 @@ const PREDEFINED_MCI_GENERATORS = { const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); return formatByteSize(totalBytes, true); // true=withAbbr }, + PT : function messagesPostedToday() { // Obv/2 + return sysStatAsString(SysProps.MessagesToday, 0); + }, + TP : function totalMessagesOnSystem() { // Obv/2 + return sysStatAsString(SysProps.MessageTotalCount, 0); + }, - // :TODO: PT - Messages posted *today* (Obv/2) - // -> Include FTN/etc. // :TODO: NT - New users today (Obv/2) // :TODO: FT - Files uploaded/added *today* (Obv/2) // :TODO: DD - Files downloaded *today* (iNiQUiTY) - // :TODO: TP - total message/posts on the system (Obv/2) - // -> Include FTN/etc. // :TODO: LC - name of last caller to system (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 3292588d..36f0d769 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -21,6 +21,8 @@ const copyFileWithCollisionHandling = require('../file_util.js').copyFileWit const getAreaStorageDirectoryByTag = require('../file_base_area.js').getAreaStorageDirectoryByTag; const isValidStorageTag = require('../file_base_area.js').isValidStorageTag; const User = require('../user.js'); +const StatLog = require('../stat_log.js'); +const SysProps = require('../system_property.js'); // deps const moment = require('moment'); @@ -1261,12 +1263,12 @@ function FTNMessageScanTossModule() { } } - // we do this after such that error cases can be preseved above + // we do this after such that error cases can be preserved above if(lookupName !== message.toUserName) { message.toUserName = localUserName; } - // set the meta information - used elsehwere for retrieval + // set the meta information - used elsewhere for retrieval message.meta.System[Message.SystemMetaNames.LocalToUserID] = localToUserId; return callback(null); }); @@ -1277,6 +1279,10 @@ function FTNMessageScanTossModule() { // save to disc message.persist(err => { + if(!message.isPrivate()) { + StatLog.incrementNonPersistentSystemStat(SysProps.MessageTotalCount, 1); + StatLog.incrementNonPersistentSystemStat(SysProps.MessagesToday, 1); + } return callback(err); }); } diff --git a/core/stat_log.js b/core/stat_log.js index 805198bd..b97ea417 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -253,7 +253,7 @@ class StatLog { if(filter.date) { filter.date = moment(filter.date); - sql += ` AND DATE(timestamp) = DATE("${filter.date.format('YYYY-MM-DD')}")`; + sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`; } if('count' !== filter.resultType) { diff --git a/core/system_property.js b/core/system_property.js index 52830a06..28250f20 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -15,6 +15,9 @@ module.exports = { FileUlTotalCount : 'ul_total_count', FileUlTotalBytes : 'ul_total_bytes', + MessageTotalCount : 'message_post_total_count', // total non-private messages on the system; non-persistent + MessagesToday : 'message_post_today', // non-private messages posted/imported today; non-persistent + // begin +op non-persistent... SysOpUsername : 'sysop_username', SysOpRealName : 'sysop_real_name', From 098c24e48a22ce11e62e0c23d533a5cf123e239b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 27 Nov 2018 21:29:35 -0700 Subject: [PATCH 396/569] Fix D/L stats --- core/system_property.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/system_property.js b/core/system_property.js index 28250f20..ca3cf7cd 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -14,6 +14,8 @@ module.exports = { FileBaseAreaStats : 'file_base_area_stats', // object - see file_base_area.js::getAreaStats FileUlTotalCount : 'ul_total_count', FileUlTotalBytes : 'ul_total_bytes', + FileDlTotalCount : 'dl_total_count', + FileDlTotalBytes : 'dl_total_bytes', MessageTotalCount : 'message_post_total_count', // total non-private messages on the system; non-persistent MessagesToday : 'message_post_today', // non-private messages posted/imported today; non-persistent From 6d2c0959766ef5e26a3c30c83a908e4402b9b43c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 27 Nov 2018 22:01:14 -0700 Subject: [PATCH 397/569] Fix cb --- core/user.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/user.js b/core/user.js index 307dbacc..0121133a 100644 --- a/core/user.js +++ b/core/user.js @@ -473,7 +473,9 @@ module.exports = class User { return this.removeProperty(name, next); }, err => { - return cb(err); + if(cb) { + return cb(err); + } }); } From 965c7b9ed1fc3ef8578ddf42333251141319c003 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 27 Nov 2018 22:59:26 -0700 Subject: [PATCH 398/569] Update MCI docs --- docs/art/mci.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/art/mci.md b/docs/art/mci.md index 4f351dad..e365f393 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -72,6 +72,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `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.) | +| `TP` | Total messages posted/imported to the system *currently* | +| `PT` | Total messages posted/imported to the system *today* | Some additional special case codes also exist: From e1862e6916af70a478c734549ebc4504af9a045a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 27 Nov 2018 23:22:09 -0700 Subject: [PATCH 399/569] Improve ACS & uploads docs around FB --- docs/filebase/acs.md | 16 +++++++++++----- docs/filebase/uploads.md | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/filebase/acs.md b/docs/filebase/acs.md index 63884c71..618c1843 100644 --- a/docs/filebase/acs.md +++ b/docs/filebase/acs.md @@ -3,9 +3,10 @@ layout: page title: ACS --- ## File Base ACS -[ACS Codes](/docs/configuration/acs.md) may be used to control acess to File Base areas by specifying an `acs` string in a file area's definition. If no `acs` is supplied in a file area definition, the following defaults apply to an area: -* `read` (list, download, etc.): `GM[users]` -* `write` (upload): `GM[sysops]` +[ACS Codes](/docs/configuration/acs.md) may be used to control access to File Base areas by specifying an `acs` string in a file area's definition. If no `acs` is supplied in a file area definition, the following defaults apply to an area: +* `read` : `GM[users]`: List/view the area and it's contents. +* `write` : `GM[sysops]`: Upload. +* `download` : `GM[users]`: Download. To override read and/or write ACS, supply a valid `acs` member. @@ -18,8 +19,13 @@ areas: { desc: Oldschool PC/DOS storageTags: [ "retro_pc", "retro_pc_bbs" ] acs: { - write: GM[users] + // only users of the "l33t" group or those who have + // uploaded 10+ files can download from here... + download: GM[l33t]|UP10 } } } -``` \ No newline at end of file +``` + +## See Also +[Access Condition System (ACS)](/docs/configuration/acs.md) diff --git a/docs/filebase/uploads.md b/docs/filebase/uploads.md index 4b1e9c38..8e1e2530 100644 --- a/docs/filebase/uploads.md +++ b/docs/filebase/uploads.md @@ -2,7 +2,24 @@ layout: page title: Uploads --- +## Uploads +The default ACS for file areas areas in ENiGMA½ is to allow read (viewing of the area), and downloads for users while only permitting SysOps to write (upload). See [File Base ACS](acs.md) for more information. -Note that `storageTags` may contain *1:n* storage tag references. +To allow uploads to a particular area, change the ACS level for `write`. For example: +```hjson +uploads: { + name: Uploads + desc: User Uploads + storageTags: [ + "uploads" + ] + acs: { + write: GM[users] + } +} +```` + +:information_source: Remember that uploads in a particular area are stored **using the first storage tag defined in that area.** + +:information_source: Any ACS checks are allowed. See [ACS](/docs/acs.md) -**Uploads in a particular area are stored in the first storage tag defined in an area.** From 555326771a32305926d1a7222b152f9feed43a1f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 27 Nov 2018 23:36:45 -0700 Subject: [PATCH 400/569] About File Areas updates --- docs/filebase/index.md | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/filebase/index.md b/docs/filebase/index.md index c225338e..6557d7cc 100644 --- a/docs/filebase/index.md +++ b/docs/filebase/index.md @@ -2,18 +2,21 @@ layout: page title: About File Areas --- -## A Different Approach -ENiGMA½ has strayed away from the old familure setup here and instead takes a more modern approach: -* [Gazelle](https://whatcd.github.io/Gazelle/) inspired system for searching & browsing files -* No File Conferences (just areas!) -* File Areas are still around but should generally be used less. Instead, files can have one or more tags. Think things like `dos.retro`, `pc.warez`, `games`, etc. -* Temporary web (http:// or https://) download links in additional to standard X/Y/Z protocol support -* Users can star rate files & search/filter by ratings -* Concept of user defined filters +## About File Areas -## Other bells and whistles -* A given area can span one to many physical storage locations -* Upload processor can extract and use `FILE_ID.DIZ`/`DESC.SDI`, for standard descriptions as well as `README.TXT`, `*.NFO`, and so on for longer descriptions -* Upload processor also attempts release year estimation by scanning prementioned description file(s) -* Fast indexed Full Text Search (FTS) -* Duplicates validated by SHA-256 \ No newline at end of file +### A Different Approach +ENiGMA½ has strayed away from the old familiar setup here and instead takes a more modern approach: +* [Gazelle](https://whatcd.github.io/Gazelle/) inspired system for searching & browsing files. +* No conferences (just areas!) +* File areas are still around but should *generally* be used less. Instead, files can have one or more tags. Think things like `dos.retro`, `pc.warez`, `games`, etc. + +### Other bells and whistles +* Temporary web (http:// or https://) download links in additional to standard X/Y/Z protocol support. Batch downloads of many files can be downloaded as a single ZIP archive. +* Users can star rate files & search/filter by ratings +* Concept of user defined filters/searches including the ability to save them. For example, "Latest Artscene Releases". +* A given area can span one to many physical storage locations. +* Upload processor can extract and use `FILE_ID.DIZ`/`DESC.SDI`, for standard descriptions as well as `README.TXT`, `*.NFO`, and so on for longer descriptions. The processor also attempts release year estimation by scanning aforementioned description file(s). +* Fast indexed Full Text Search (FTS) across descriptions and filenames. +* Duplicates are checked for by SHA-256. +* Support for many archive and file formats. External utilities can easily be added to the configuration to extend for additional formats. +* Much, much more! From 6335483a6739834b8518c6691aff10f23279c259 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 28 Nov 2018 18:55:48 -0700 Subject: [PATCH 401/569] About File Areas updates...again --- docs/filebase/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/filebase/index.md b/docs/filebase/index.md index 6557d7cc..d5dac134 100644 --- a/docs/filebase/index.md +++ b/docs/filebase/index.md @@ -12,11 +12,11 @@ ENiGMA½ has strayed away from the old familiar setup here and instead takes a m ### Other bells and whistles * Temporary web (http:// or https://) download links in additional to standard X/Y/Z protocol support. Batch downloads of many files can be downloaded as a single ZIP archive. -* Users can star rate files & search/filter by ratings -* Concept of user defined filters/searches including the ability to save them. For example, "Latest Artscene Releases". +* Users can rate files & search/filter by ratings. +* Users can also create and save their own filters for later use such as "Latest Artscene Releases" or "C64 SIDs". * A given area can span one to many physical storage locations. * Upload processor can extract and use `FILE_ID.DIZ`/`DESC.SDI`, for standard descriptions as well as `README.TXT`, `*.NFO`, and so on for longer descriptions. The processor also attempts release year estimation by scanning aforementioned description file(s). -* Fast indexed Full Text Search (FTS) across descriptions and filenames. -* Duplicates are checked for by SHA-256. +* Fast indexed [Full Text Search (FTS)](https://sqlite.org/fts3.html) across descriptions and filenames. +* Duplicates are checked for by cryptographically secure [SHA-256](https://en.wikipedia.org/wiki/SHA-2) hashes. * Support for many archive and file formats. External utilities can easily be added to the configuration to extend for additional formats. * Much, much more! From fe44f2c4d6bbfa619210bf6cd371ba90d6e5019e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 30 Nov 2018 23:20:44 -0700 Subject: [PATCH 402/569] User interrupts & node module ready to rock. ...maybe with bugs? --- art/themes/luciano_blocktronics/MMENU.ANS | Bin 3527 -> 3574 bytes art/themes/luciano_blocktronics/theme.hjson | 14 ++-- core/menu_module.js | 71 +++++++++----------- core/node_msg.js | 32 +++++---- core/user_interrupt_queue.js | 33 ++++++--- docs/_includes/nav.md | 1 + docs/misc/user-interrupt.md | 17 +++++ docs/modding/node-msg.md | 41 +++++++++++ misc/menu_template.in.hjson | 57 ++++++++++++++++ 9 files changed, 198 insertions(+), 68 deletions(-) create mode 100644 docs/misc/user-interrupt.md create mode 100644 docs/modding/node-msg.md diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index 75251877665dfef7961dfa9376f8ed1b294279ad..ad029e33f765113432e247acfd1b6a04a6e67e4d 100644 GIT binary patch delta 118 zcmX>u{Y`p97$>WAw2^`H8` { - this.disableInterruption(); - return cb(null); - }); - } - displayQueuedInterruptions(cb) { - if(true !== this.Interruptible) { + if(MenuModule.InterruptTypes.Never === this.interrupt) { return cb(null); } + let opts = { cls : true }; // clear screen for first message + async.whilst( () => this.client.interruptQueue.hasItems(), - next => this.client.interruptQueue.displayNext(next), + next => { + this.client.interruptQueue.displayNext(opts, err => { + opts = {}; + return next(err); + }); + }, err => { return cb(err); } - ) + ); } attemptInterruptNow(interruptItem, cb) { - if(true !== this.Interruptible) { + if(MenuModule.InterruptTypes.Realtime !== this.interrupt) { return cb(null, false); // don't eat up the item; queue for later } // // Default impl: clear screen -> standard display -> reload menu // - this.client.interruptQueue.displayWithItem(Object.assign({}, interruptItem, { cls : true }), err => { - if(err) { - return cb(err, false); - } - this.reload(err => { - return cb(err, err ? false : true); + this.client.interruptQueue.displayWithItem( + Object.assign({}, interruptItem, { cls : true }), + err => { + if(err) { + return cb(err, false); + } + this.reload(err => { + return cb(err, err ? false : true); + }); }); - }); } getSaveState() { @@ -308,7 +299,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } else { return this.prevMenu(cb); } - } + }; if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { if(this.hasNextTimeout()) { @@ -318,8 +309,6 @@ exports.MenuModule = class MenuModule extends PluginModule { } else { return gotoNextMenu(); } - } else { - this.enableInterruption(); } } diff --git a/core/node_msg.js b/core/node_msg.js index 412e14ab..72c54430 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -2,16 +2,16 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const { Errors } = require('./enig_error.js'); +const { MenuModule } = require('./menu_module.js'); const { getActiveConnectionList, getConnectionByNodeId, -} = require('./client_connections.js'); -const UserInterruptQueue = require('./user_interrupt_queue.js'); -const { getThemeArt } = require('./theme.js'); -const { pipeToAnsi } = require('./color_codes.js'); -const stringFormat = require('./string_format.js'); +} = require('./client_connections.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); +const { getThemeArt } = require('./theme.js'); +const { pipeToAnsi } = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); +const { renderStringLength } = require('./string_util.js'); // deps const series = require('async/series'); @@ -47,23 +47,27 @@ exports.getModule = class NodeMessageModule extends MenuModule { this.menuMethods = { sendMessage : (formData, extraArgs, cb) => { const nodeId = this.nodeList[formData.value.node].node; // index from from -> node! - const message = formData.value.message; + const message = _.get(formData.value, 'message', '').trim(); + + if(0 === renderStringLength(message)) { + return this.prevMenu(cb); + } this.createInterruptItem(message, (err, interruptItem) => { if(-1 === nodeId) { // ALL nodes - UserInterruptQueue.queueGlobalOtherActive(interruptItem, this.client); + UserInterruptQueue.queue(interruptItem, { omit : this.client }); } else { const conn = getConnectionByNodeId(nodeId); if(conn) { - UserInterruptQueue.queueGlobal(interruptItem, [ conn ]); + UserInterruptQueue.queue(interruptItem, { clients : conn } ); } } return this.prevMenu(cb); }); }, - } + }; } mciReady(mciData, cb) { @@ -123,12 +127,14 @@ exports.getModule = class NodeMessageModule extends MenuModule { } createInterruptItem(message, cb) { + const dateTimeFormat = this.config.dateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); + const textFormatObj = { fromUserName : this.client.user.username, fromRealName : this.client.user.properties.real_name, fromNodeId : this.client.node, message : message, - timestamp : moment(), + timestamp : moment().format(dateTimeFormat), }; const messageFormat = @@ -208,4 +214,4 @@ exports.getModule = class NodeMessageModule extends MenuModule { } this.updateCustomViewTextsWithFilter('sendMessage', MciViewIds.sendMessage.customRangeStart, node); } -} \ No newline at end of file +}; diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index e48fc2ea..2e72bbd1 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -19,18 +19,26 @@ module.exports = class UserInterruptQueue this.queue = []; } - static queueGlobal(interruptItem, connections) { - connections.forEach(conn => { - conn.interruptQueue.queueItem(interruptItem); + static queue(interruptItem, opts) { + opts = opts || {}; + if(!opts.clients) { + let omitNodes = []; + if(Array.isArray(opts.omit)) { + omitNodes = opts.omit; + } else if(opts.omit) { + omitNodes = [ opts.omit ]; + } + omitNodes = omitNodes.map(n => _.isNumber(n) ? n : n.node); + opts.clients = getActiveConnections(true).filter(ac => !omitNodes.includes(ac.node)); + } + if(!Array.isArray(opts.clients)) { + opts.clients = [ opts.clients ]; + } + opts.clients.forEach(c => { + c.interruptQueue.queueItem(interruptItem); }); } - // common shortcut: queue global, all active clients minus |client| - static queueGlobalOtherActive(interruptItem, client) { - const otherConnections = getActiveConnections(true).filter(ac => ac.node !== client.node); - return UserInterruptQueue.queueGlobal(interruptItem, otherConnections ); - } - queueItem(interruptItem) { if(!_.isString(interruptItem.contents) && !_.isString(interruptItem.text)) { return; @@ -52,12 +60,17 @@ module.exports = class UserInterruptQueue return this.queue.length > 0; } - displayNext(cb) { + displayNext(options, cb) { + if(!cb && _.isFunction(options)) { + cb = options; + options = {}; + } const interruptItem = this.queue.pop(); if(!interruptItem) { return cb(null); } + Object.assign(interruptItem, options); return interruptItem ? this.displayWithItem(interruptItem, cb) : cb(null); } diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 84785dbe..b6303960 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -79,6 +79,7 @@ - [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %}) - [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %}) - [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %}) + - [Node to Node Messaging]({{ site.baseurl }}{% link modding/node-msg.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/misc/user-interrupt.md b/docs/misc/user-interrupt.md new file mode 100644 index 00000000..fe20fdd9 --- /dev/null +++ b/docs/misc/user-interrupt.md @@ -0,0 +1,17 @@ +--- +layout: page +title: User Interruptions +--- +## User Interruptions +ENiGMA½ provides functionality to "interrupt" a user for various purposes such as a [node-to-node message](/docs/modding/node-msg.md). User interruptions can be queued and displayed at the next opportune time such as when switching to a new menu, or realtime if appropriate. + +## Standard Menu Behavior +Standard menus control interruption by the `interrupt` config block option, which may be set to one of the following values: +* `never`: Never interrupt the user when on this menu. +* `queued`: Queue interrupts for the next opportune time. Any queued message(s) will then be shown. This is the default. +* `realtime`: If possible, display messages in realtime. That is, show them right away. Standard menus that do not override default behavior will show the message then reload. + + +## See Also +See [user_interrupt_queue.js](/core/user_interrupt_queue.js) as well as usage within [menu_module.js](/core/menu_module.js). + diff --git a/docs/modding/node-msg.md b/docs/modding/node-msg.md new file mode 100644 index 00000000..5377e68a --- /dev/null +++ b/docs/modding/node-msg.md @@ -0,0 +1,41 @@ +--- +layout: page +title: Node to Node Messaging +--- +## The Node to Node Messaging Module +The node to node messaging (`node_msg`) module allows users to send messages to one or more users on different nodes. Messages delivered to nodes follow standard [User Interruption](/docs/misc/user-interrupt.md) rules. + +## Configuration +### Config Block +Available `config` block entries: +* `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` format. +* `messageFormat`: Format string for sent messages. Defaults to `Message from {fromUserName} on node {fromNodeId}:\r\n{message}`. The following format object members are available: + * `fromUserName`: Username who sent the message. + * `fromRealName`: Real name of user who sent the message. + * `fromNodeId`: Node ID where the message was sent from. + * `message`: User entered message. May contain pipe color codes. + * `timestamp`: A timestamp formatted using `dateTimeFormat` above. +* `art`: Block containing: + * `header`: Art spec for header to display with message. + * `footer`: Art spec for footer to display with message. + +## Theming +### MCI Codes +1. Node selection. Must be a View that allows lists such as `SpinnerMenuView` (`%SM1`), `HorizontalMenuView` (`%HM1`), etc. +2. Message entry (`%ET2`). +3. Message preview (`%TL3`). A rendered (that is, pipe codes resolved) preview of the text in `%ET2`. + +10+: Custom using `itemFormat`. See below. + +### Item Format +The following `itemFormat` object is provided for MCI 1 and 10+ for the currently selected item/node: +* `text`: Node ID or "-ALL-" (All nodes). +* `node`: Node ID or `-1` in the case of all nodes. +* `userId`: User ID. +* `action`: User's action. +* `userName`: Username. +* `realName`: Real name. +* `location`: User's location. +* `affils`: Affiliations. +* `timeOn`: How long the user has been online (approx). + diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 6e1d889d..27ad3003 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -991,8 +991,13 @@ prompt: menuCommand config: { font: cp437 + interrupt: realtime } submit: [ + { + value: { command: "MSG" } + action: @menu:nodeMessage + } { value: { command: "G" } action: @menu:fullLogoffSequence @@ -1064,6 +1069,46 @@ ] } + nodeMessage: { + desc: Node Messaging + module: node_msg + art: NODEMSG + config: { + cls: true + art: { + header: NODEMSGHDR + footer: NODEMSGFTR + } + } + form: { + 0: { + mci: { + SM1: { + argName: node + } + ET2: { + argName: message + submit: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + submit: { + *: [ + { + value: { message: null } + action: @method:sendMessage + } + ] + } + } + } + } + mainMenuLastCallers: { desc: Last Callers module: last_callers @@ -1609,6 +1654,9 @@ desc: Doors Menu art: DOORMNU prompt: menuCommand + config: { + interrupt: realtime + } submit: [ { value: { command: "G" } @@ -1738,6 +1786,9 @@ art: MSGMNU desc: Message Area prompt: messageMenuCommand + config: { + interrupt: realtime + } submit: [ { value: { command: "P" } @@ -2464,6 +2515,9 @@ art: MAILMNU desc: Mail Menu prompt: menuCommand + config: { + interrupt: realtime + } submit: [ { value: { command: "C" } @@ -2666,6 +2720,9 @@ desc: File Base art: FMENU prompt: fileMenuCommand + config: { + interrupt: realtime + } submit: [ { value: { menuOption: "L" } From eec06e70042562eb166ff6b31f345a39903dd791 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 1 Dec 2018 16:59:47 -0700 Subject: [PATCH 403/569] Fix oops! --- core/dropfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/dropfile.js b/core/dropfile.js index a23c5c1c..6595aa1a 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -86,7 +86,7 @@ module.exports = class DropFile { const now = moment(); const secLevel = this.client.user.getLegacySecurityLevel().toString(); const fullName = prop[UserProps.RealName] || this.client.user.username; - const bd = moment(prop[UserProp.Birthdate).format('MM/DD/YY'); + const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY'); // :TODO: fix time remaining // :TODO: fix default protocol -- user prop: transfer_protocol From 36d55a409e61f4fbf5391f0296fc6ace1482d1c3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 1 Dec 2018 17:00:07 -0700 Subject: [PATCH 404/569] Add send node msg event --- core/module_util.js | 2 +- core/node_msg.js | 5 ++++- core/stat_log.js | 2 +- core/system_events.js | 1 + docs/modding/last-callers.md | 3 ++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/module_util.js b/core/module_util.js index ae342d4b..273ddaf0 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -149,7 +149,7 @@ function initializeModules(cb) { return nextModule(null); } } catch(e) { - Log.warn( { error : e }, 'Exception during "moduleInitialize"'); + Log.warn( { error : e.message, fullModulePath }, 'Exception during "moduleInitialize"'); return nextModule(null); } }, diff --git a/core/node_msg.js b/core/node_msg.js index 72c54430..bf22e24a 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -12,6 +12,7 @@ const { getThemeArt } = require('./theme.js'); const { pipeToAnsi } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); const { renderStringLength } = require('./string_util.js'); +const Events = require('./events.js'); // deps const series = require('async/series'); @@ -37,7 +38,7 @@ const MciViewIds = { customRangeStart : 10, } -} +}; exports.getModule = class NodeMessageModule extends MenuModule { constructor(options) { @@ -64,6 +65,8 @@ exports.getModule = class NodeMessageModule extends MenuModule { } } + Events.emit(Events.getSystemEvents().UserSendNodeMsg, { user : this.client.user } ); + return this.prevMenu(cb); }); }, diff --git a/core/stat_log.js b/core/stat_log.js index b97ea417..6cf6198b 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -355,7 +355,7 @@ class StatLog { systemEvents.NewUser, systemEvents.UserUpload, systemEvents.UserDownload, systemEvents.UserPostMessage, systemEvents.UserSendMail, - systemEvents.UserRunDoor, + systemEvents.UserRunDoor, systemEvents.UserSendNodeMsg, ]; Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => { diff --git a/core/system_events.js b/core/system_events.js index c8345160..0f8118a2 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -20,4 +20,5 @@ module.exports = { UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { areaTag } UserSendMail : 'codes.l33t.enigma.system.user_send_mail', UserRunDoor : 'codes.l33t.enigma.system.user_run_door', + UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', }; diff --git a/docs/modding/last-callers.md b/docs/modding/last-callers.md index f754a912..830244d7 100644 --- a/docs/modding/last-callers.md +++ b/docs/modding/last-callers.md @@ -20,6 +20,7 @@ Available `config` block entries: * `userPostMsg` * `userSendMail` * `userRunDoor` + * `userSendNodeMsg` * `actionIndicatorDefault`: Default indicator when an action is not set. Defaults to "-". Remember that entries such as `actionIndicators` and `actionIndicatorDefault` may contain pipe color codes! @@ -32,6 +33,6 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `ts`: Timestamp in `dateTimeFormat` format. * `location`: User's location. * `affiliation` or `affils`: Users affiliations. -* `actions`: A string built by concatenating action indicators for a users logged in session. For example, given a indincator of `userDownload` mapped to "D", the string may be "-D----". The format was made popular on Amiga style boards. +* `actions`: A string built by concatenating action indicators for a users logged in session. For example, given a indicator of `userDownload` mapped to "D", the string may be "-D----". The format was made popular on Amiga style boards. From 5dea13e652f6d7ceebc90600d37cbcee5f27a0f1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Dec 2018 19:30:50 -0700 Subject: [PATCH 405/569] + validateConfigFields() for 'config' block validation --- core/menu_module.js | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/core/menu_module.js b/core/menu_module.js index 9bac7b03..bc631feb 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -567,4 +567,55 @@ exports.MenuModule = class MenuModule extends PluginModule { } return cb(null); } + + validateConfigFields(fields, cb) { + // + // fields is expected to be { key : type || validator(key, config) } + // where |type| is 'string', 'array', object', 'number' + // + if(!_.isObject(fields)) { + return cb(Errors.Invalid('Invalid validator!')); + } + + const config = this.config || this.menuConfig.config; + let firstBadKey; + let badReason; + const good = _.every(fields, (type, key) => { + if(_.isFunction(type)) { + if(!type(key, config)) { + firstBadKey = key; + badReason = 'Validate failure'; + return false; + } + return true; + } + + const c = config[key]; + let typeOk; + if(_.isUndefined(c)) { + typeOk = false; + badReason = `Missing "${key}", expected ${type}`; + } else { + switch(type) { + case 'string' : typeOk = _.isString(c); break; + case 'object' : typeOk = _.isObject(c); break; + case 'array' : typeOk = Array.isArray(c); break; + case 'number' : typeOk = !isNaN(parseInt(c)); break; + default : + typeOk = false; + badReason = `Don't know how to validate ${type}`; + break; + } + } + if(!typeOk) { + firstBadKey = key; + if(!badReason) { + badReason = `Expected ${type}`; + } + } + return typeOk; + }); + + return cb(good ? null : Errors.Invalid(`Invalid or missing config option "${firstBadKey}" (${badReason})`)); + } }; From 8f9f4227c134b3938f8434210088f1f417e440d2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Dec 2018 19:31:27 -0700 Subject: [PATCH 406/569] Fix typo --- misc/menu_template.in.hjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 27ad3003..82a3893f 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1677,7 +1677,7 @@ action: @menu:doorAbracadabraExample } { - value: { command: "TWBBLINK" } + value: { command: "TWBBSLINK" } action: @menu:doorTradeWars2002BBSLinkExample } { From 0c23339a2d95acdbe7b89e3c7cc5da9687b04510 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Dec 2018 19:33:07 -0700 Subject: [PATCH 407/569] Code cleanup: Use EnigError's vs standard Error. WIP... --- core/abracadabra.js | 5 +++-- core/art.js | 15 ++++++++------- core/bbs_link.js | 34 ++++++++++++++++++---------------- core/combatnet.js | 37 ++++++++++++++++++++----------------- core/connect.js | 19 +++++++++---------- core/door_party.js | 29 +++++++++++++++-------------- core/event_scheduler.js | 2 +- core/fnv1a.js | 6 ++++-- core/listening_server.js | 4 ++-- core/module_util.js | 14 ++++++++------ 10 files changed, 88 insertions(+), 77 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 1ddec925..32c4466a 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -7,6 +7,7 @@ const Door = require('./door.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); const Events = require('./events.js'); +const { Errors } = require('./enig_error.js'); const async = require('async'); const assert = require('assert'); @@ -98,7 +99,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { if(_.isString(self.config.tooManyArt)) { theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { self.pausePrompt( () => { - callback(new Error('Too many active instances')); + return callback(Errors.AccessDenied('Too many active instances')); }); }); } else { @@ -106,7 +107,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { // :TODO: Use MenuModule.pausePrompt() self.pausePrompt( () => { - callback(new Error('Too many active instances')); + return callback(Errors.AccessDenied('Too many active instances')); }); } } else { diff --git a/core/art.js b/core/art.js index 315950e1..d709d366 100644 --- a/core/art.js +++ b/core/art.js @@ -2,11 +2,12 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const miscUtil = require('./misc_util.js'); -const ansi = require('./ansi_term.js'); -const aep = require('./ansi_escape_parser.js'); -const sauce = require('./sauce.js'); +const Config = require('./config.js').get; +const miscUtil = require('./misc_util.js'); +const ansi = require('./ansi_term.js'); +const aep = require('./ansi_escape_parser.js'); +const sauce = require('./sauce.js'); +const { Errors } = require('./enig_error.js'); // deps const fs = require('graceful-fs'); @@ -209,7 +210,7 @@ function getArt(name, options, cb) { return getArtFromPath(readPath, options, cb); } - return cb(new Error(`No matching art for supplied criteria: ${name}`)); + return cb(Errors.DoesNotExist(`No matching art for supplied criteria: ${name}`)); }); } @@ -236,7 +237,7 @@ function display(client, art, options, cb) { } if(!art || !art.length) { - return cb(new Error('Empty art')); + return cb(Errors.Invalid('No art supplied!')); } options.mciReplaceChar = options.mciReplaceChar || ' '; diff --git a/core/bbs_link.js b/core/bbs_link.js index cbb7c036..71fa04c1 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -1,11 +1,12 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; -const resetScreen = require('./ansi_term.js').resetScreen; +const { MenuModule } = require('./menu_module.js'); +const { resetScreen } = require('./ansi_term.js'); +const { Errors } = require('./enig_error.js'); +// deps const async = require('async'); -const _ = require('lodash'); const http = require('http'); const net = require('net'); const crypto = require('crypto'); @@ -60,15 +61,17 @@ exports.getModule = class BBSLinkModule extends MenuModule { async.series( [ function validateConfig(callback) { - if(_.isString(self.config.sysCode) && - _.isString(self.config.authCode) && - _.isString(self.config.schemeCode) && - _.isString(self.config.door)) - { - callback(null); - } else { - callback(new Error('Configuration is missing option(s)')); - } + return self.validateConfigFields( + { + host : 'string', + sysCode : 'string', + authCode : 'string', + schemeCode : 'string', + door : 'string', + port : 'number', + }, + callback + ); }, function acquireToken(callback) { // @@ -112,10 +115,9 @@ exports.getModule = class BBSLinkModule extends MenuModule { var status = body.trim(); if('complete' === status) { - callback(null); - } else { - callback(new Error('Bad authentication status: ' + status)); + return callback(null); } + return callback(Errors.AccessDenied(`Bad authentication status: ${status}`)); }); }, function createTelnetBridge(callback) { @@ -158,7 +160,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { bridgeConnection.on('end', function connectionEnd() { restorePipe(); - callback(clientTerminated ? new Error('Client connection terminated') : null); + return callback(clientTerminated ? Errors.General('Client connection terminated') : null); }); bridgeConnection.on('error', function error(err) { diff --git a/core/combatnet.js b/core/combatnet.js index 1b53a153..abb9a889 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -2,12 +2,12 @@ 'use strict'; // enigma-bbs -const MenuModule = require('../core/menu_module.js').MenuModule; -const resetScreen = require('../core/ansi_term.js').resetScreen; +const { MenuModule } = require('../core/menu_module.js'); +const { resetScreen } = require('../core/ansi_term.js'); +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); -const _ = require('lodash'); const RLogin = require('rlogin'); exports.moduleInfo = { @@ -32,13 +32,15 @@ exports.getModule = class CombatNetModule extends MenuModule { async.series( [ function validateConfig(callback) { - if(!_.isString(self.config.password)) { - return callback(new Error('Config requires "password"!')); - } - if(!_.isString(self.config.bbsTag)) { - return callback(new Error('Config requires "bbsTag"!')); - } - return callback(null); + return self.validateConfigFields( + { + host : 'string', + password : 'string', + bbsTag : 'string', + rloginPort : 'number', + }, + callback + ); }, function establishRloginConnection(callback) { self.client.term.write(resetScreen()); @@ -51,12 +53,13 @@ exports.getModule = class CombatNetModule extends MenuModule { }; const rlogin = new RLogin( - { 'clientUsername' : self.config.password, - 'serverUsername' : `${self.config.bbsTag}${self.client.user.username}`, - 'host' : self.config.host, - 'port' : self.config.rloginPort, - 'terminalType' : self.client.term.termClient, - 'terminalSpeed' : 57600 + { + clientUsername : self.config.password, + serverUsername : `${self.config.bbsTag}${self.client.user.username}`, + host : self.config.host, + port : self.config.rloginPort, + terminalType : self.client.term.termClient, + terminalSpeed : 57600 } ); @@ -88,7 +91,7 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.term.output.on('data', sendToRloginBuffer); } else { - return callback(new Error('Failed to establish establish CombatNet connection')); + return callback(Errors.General('Failed to establish establish CombatNet connection')); } } ); diff --git a/core/connect.js b/core/connect.js index 44277813..0b83f7f0 100644 --- a/core/connect.js +++ b/core/connect.js @@ -2,8 +2,9 @@ 'use strict'; // ENiGMA½ -const ansi = require('./ansi_term.js'); -const Events = require('./events.js'); +const ansi = require('./ansi_term.js'); +const Events = require('./events.js'); +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); @@ -15,7 +16,7 @@ function ansiDiscoverHomePosition(client, cb) { // We want to find the home position. ANSI-BBS and most terminals // utilize 1,1 as home. However, some terminals such as ConnectBot // think of home as 0,0. If this is the case, we need to offset - // our positioning to accomodate for such. + // our positioning to accommodate for such. // const done = function(err) { client.removeListener('cursor position report', cprListener); @@ -32,7 +33,7 @@ function ansiDiscoverHomePosition(client, cb) { // if(h > 1 || w > 1) { client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); - return done(new Error('Home position CPR expected to be 0,0, or 1,1')); + return done(Errors.UnexpectedState('Home position CPR expected to be 0,0, or 1,1')); } if(0 === h & 0 === w) { @@ -49,7 +50,7 @@ function ansiDiscoverHomePosition(client, cb) { client.once('cursor position report', cprListener); const giveUpTimer = setTimeout( () => { - return done(new Error('Giving up on home position CPR')); + return done(Errors.General('Giving up on home position CPR')); }, 3000); // 3s client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos @@ -78,14 +79,14 @@ function ansiQueryTermSizeIfNeeded(client, cb) { const w = pos[1]; // - // Netrunner for example gives us 1x1 here. Not really useful. Ignore + // NetRunner for example gives us 1x1 here. Not really useful. Ignore // values that seem obviously bad. // if(h < 10 || w < 10) { client.log.warn( { height : h, width : w }, 'Ignoring ANSI CPR screen size query response due to very small values'); - return done(new Error('Term size <= 10 considered invalid')); + return done(Errors.Invalid('Term size <= 10 considered invalid')); } client.term.termHeight = h; @@ -107,7 +108,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) { // give up after 2s const giveUpTimer = setTimeout( () => { - return done(new Error('No term size established by CPR within timeout')); + return done(Errors.General('No term size established by CPR within timeout')); }, 2000); // Start the process: Query for CPR @@ -116,8 +117,6 @@ function ansiQueryTermSizeIfNeeded(client, cb) { function prepareTerminal(term) { term.rawWrite(ansi.normal()); - //term.rawWrite(ansi.disableVT100LineWrapping()); - // :TODO: set xterm stuff -- see x84/others } function displayBanner(term) { diff --git a/core/door_party.js b/core/door_party.js index 63247f69..f6bc7be9 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -2,12 +2,12 @@ 'use strict'; // enigma-bbs -const MenuModule = require('../core/menu_module.js').MenuModule; -const resetScreen = require('../core/ansi_term.js').resetScreen; +const { MenuModule } = require('./menu_module.js'); +const { resetScreen } = require('./ansi_term.js'); +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); -const _ = require('lodash'); const SSHClient = require('ssh2').Client; exports.moduleInfo = { @@ -34,16 +34,17 @@ exports.getModule = class DoorPartyModule extends MenuModule { async.series( [ function validateConfig(callback) { - if(!_.isString(self.config.username)) { - return callback(new Error('Config requires "username"!')); - } - if(!_.isString(self.config.password)) { - return callback(new Error('Config requires "password"!')); - } - if(!_.isString(self.config.bbsTag)) { - return callback(new Error('Config requires "bbsTag"!')); - } - return callback(null); + return self.validateConfigFields( + { + host : 'string', + username : 'string', + password : 'string', + bbsTag : 'string', + sshPort : 'number', + rloginPort : 'number', + }, + callback + ); }, function establishSecureConnection(callback) { self.client.term.write(resetScreen()); @@ -71,7 +72,7 @@ exports.getModule = class DoorPartyModule extends MenuModule { // establish tunnel for rlogin sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { if(err) { - return callback(new Error('Failed to establish tunnel')); + return callback(Errors.General('Failed to establish tunnel')); } // diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 95de5a97..4b7da062 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -162,7 +162,7 @@ class ScheduledEvent { { eventName : this.name, action : this.action, exitCode : exitCode }, 'Bad exit code while performing scheduled event action'); } - return cb(exitCode ? new Error(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); + return cb(exitCode ? Errors.ExternalProcess(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); }); } } diff --git a/core/fnv1a.js b/core/fnv1a.js index 9acc8f27..b85e4241 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -1,7 +1,9 @@ /* jslint node: true */ 'use strict'; -let _ = require('lodash'); +const { Errors } = require('./enig_error.js'); + +const _ = require('lodash'); // FNV-1a based on work here: https://github.com/wiedi/node-fnv module.exports = class FNV1a { @@ -23,7 +25,7 @@ module.exports = class FNV1a { } if(!Buffer.isBuffer(data)) { - throw new Error('data must be String or Buffer!'); + throw Errors.Invalid('data must be String or Buffer!'); } for(let b of data) { diff --git a/core/listening_server.js b/core/listening_server.js index 00cf0a86..f28bea7f 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -3,6 +3,7 @@ // ENiGMA½ const logger = require('./logger.js'); +const { ErrorReasons } = require('./enig_error.js'); // deps const async = require('async'); @@ -30,9 +31,8 @@ function startListening(cb) { async.each( [ 'login', 'content' ], (category, next) => { moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { - // :TODO: use enig error here! if(err) { - if('EENIGMODDISABLED' === err.code) { + if(ErrorReasons.Disabled === err.reasonCode) { logger.log.debug(err.message); } else { logger.log.info( { err : err }, 'Failed loading module'); diff --git a/core/module_util.js b/core/module_util.js index 273ddaf0..0e8e5976 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -4,6 +4,10 @@ // ENiGMA½ const Config = require('./config.js').get; const Log = require('./logger.js').log; +const { + Errors, + ErrorReasons +} = require('./enig_error.js'); // deps const fs = require('graceful-fs'); @@ -28,9 +32,7 @@ function loadModuleEx(options, cb) { const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; if(_.isObject(modConfig) && false === modConfig.enabled) { - const err = new Error(`Module "${options.name}" is disabled`); - err.code = 'EENIGMODDISABLED'; - return cb(err); + return cb(Errors.AccessDenied(`Module "${options.name}" is disabled`, ErrorReasons.Disabled)); } // @@ -56,11 +58,11 @@ function loadModuleEx(options, cb) { } if(!_.isObject(mod.moduleInfo)) { - return cb(new Error('Module is missing "moduleInfo" section')); + return cb(Errors.Invalid(`No exported "moduleInfo" block for module ${modPath}!`)); } if(!_.isFunction(mod.getModule)) { - return cb(new Error('Invalid or missing "getModule" method for module!')); + return cb(Errors.Invalid(`No exported "getModule" method for module ${modPath}!`)); } return cb(null, mod); @@ -70,7 +72,7 @@ function loadModule(name, category, cb) { const path = Config().paths[category]; if(!_.isString(path)) { - return cb(new Error(`Not sure where to look for "${name}" of category "${category}"`)); + return cb(Errors.DoesNotExist(`Not sure where to look for module "${name}" of category "${category}"`)); } loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { From 7deb202623e34c8965c8b01e545ff473680aa158 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 2 Dec 2018 20:59:40 -0700 Subject: [PATCH 408/569] Slight cleanup --- docs/configuration/file-transfer-protocols.md | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/configuration/file-transfer-protocols.md b/docs/configuration/file-transfer-protocols.md index fd21a938..2f7d48ac 100644 --- a/docs/configuration/file-transfer-protocols.md +++ b/docs/configuration/file-transfer-protocols.md @@ -2,10 +2,10 @@ layout: page title: File Transfer Protocols --- -ENiGMA½ currently relies on external executables for "legacy" file transfer protocols such as X, Y, and ZModem. The `fileTransferProtocols` section of `config.hjson` is used to override defaults, add new handlers, etc. Remember that ENiGMA½ also support modern web (HTTP/HTTPS) downloads! +ENiGMA½ currently relies on external executable binaries for "legacy" file transfer protocols such as X, Y, and ZModem. Remember that ENiGMA½ also support modern web (HTTP/HTTPS) downloads! ## File Transfer Protocols -File transfer protocols are managed via the `fileTransferProtocols` configuration block of `config.hjson`. Each entry defines an **external** protocol that can be used for uploads (recv), downloads (send), or both. Depending on the protocol and handler, batch receiving of files (uploads) may also be available. +File transfer protocols are managed via the `fileTransferProtocols` configuration block of `config.hjson`. Each entry defines an **external** protocol handler that can be used for uploads (recv), downloads (send), or both. Depending on the protocol and handler, batch receiving of files (uploads) may also be available. ### Predefined File Transfer Protocols The following file transfer protocols are pre-configured in ENiGMA½ as of this writing. System operators may override or extend this list. PRs are welcome for pre-configured additions! @@ -32,18 +32,21 @@ For protocols of type `external` the following members may be defined: * `recvArgsNonBatch`: Required if using `recvCmd` and supporting non-batch (single file) uploads; A placeholder of `{fileName}` may be supplied to indicate to the protocol what the uploaded file should be named (this will be collected from the user before the upload starts). * `escapeTelnet`: Optional; If set to `true`, escape all internal Telnet related codes such as IAC's. This option is required for external protocol handlers such as `sz` and `rz` that do not escape themselves. +### Adding Your Own +Take a look a the example below as well as [core/config.js](/core/config.js). + #### Example File Transfer Protocol Configuration ``` zmodem8kSexyz : { - name : 'ZModem 8k (SEXYZ)', - type : 'external', - sort : 1, - external : { - sendCmd : 'sexyz', - sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], - recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], - } + name : 'ZModem 8k (SEXYZ)', + type : 'external', + sort : 1, + external : { + sendCmd : 'sexyz', + sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], + recvCmd : 'sexyz', + recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], + recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], + } } -``` \ No newline at end of file +``` From 1c911fe086bbc1e00907a869e173299eebcf4a74 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Dec 2018 20:01:24 -0700 Subject: [PATCH 409/569] Minor local door doc updates --- docs/modding/local-doors.md | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index 719a992b..9322e970 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -2,10 +2,13 @@ layout: page title: Local Doors --- -## The abracadabra Module -The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and process I/O through stdio or a temporary TCP server. +## Local Doors +ENiGMA½ has many ways to add doors to your system. In addition to the many built in door server modules, local doors are of course also supported using the ! The `abracadabra` module! -## Configuration +## The abracadabra Module +The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and perform I/O through standard I/O (stdio) or a temporary TCP server. + +### Configuration The `abracadabra` `config` block can contain the following members: * `name`: Used as a key for tracking number of clients using a particular door. * `dropFileType`: Specifies the type of drop file to generate (See **Argument Variables** below). @@ -14,24 +17,24 @@ The `abracadabra` `config` block can contain the following members: * `cwd`: Set the Current Working Directory for `cmd`. Defaults to the directory of `cmd`. * `nodeMax`: Max number of nodes that can access this door at once. Uses `name` as a mapping key * `tooManyArt`: Art file spec to display if too many instances are already in use -* `io`: Where to process I/O. Can be `stdio` or `socket` +* `io`: Where to process I/O. Can be `stdio` or `socket`. When using `stdio`, I/O is input/output from stdin/stdout. When using `socket` a temporary socket server is spawned that can be connected to. The server listens on localhost on `{srvPort}` (see below under Argument Variables). * `encoding`: Specify the door's encoding. Defaults to `cp437`. Linux binaries for example, often produce `utf8`. -### Drop File Types +#### Drop File Types Drop file types specified by `dropFileType`: * `DOOR`: [DOOR.SYS](http://goldfndr.home.mindspring.com/dropfile/doorsys.htm) -* `DOOR32`: [DOOR32.SYS](http://wiki.bbses.info/index.php/DOOR32.SYS) +* `DOOR32`: [DOOR32.SYS](https://raw.githubusercontent.com/NuSkooler/ansi-bbs/master/docs/dropfile_formats/door32_sys.txt) * `DORINFO`: [DORINFOx.DEF](http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm) -### Argument Variables -The following variables may be used in `{args}` entries: +#### Argument Variables +The following variables may be used in `args` entries: * `{node}`: Current node number. * `{dropFile}`: Drop _filename_ only. * `{dropFilePath}`: Full path to generated drop file. * `{userId}`: Current user ID. -* `{userName}`: _Sanatized_ username. Safe for filenames, etc. +* `{userName}`: _Sanitized_ username. Safe for filenames, etc. * `{userNameRaw}`: _Raw_ username. May not be safe for filenames! -* `{srvPort}`: Tempoary server port when `io` is set to `socket`. +* `{srvPort}`: Temporary server port when `io` is set to `socket`. * `{cwd}`: Current Working Directory. Example: @@ -41,7 +44,7 @@ args: [ ] ``` -## DOSEMU with abracadabra +### DOSEMU with abracadabra [DOSEMU](http://www.dosemu.org/) can provide a good solution for running legacy DOS doors when running on Linux systems. For this, we will create a virtual serial port (COM1) that communicates via stdio. As an example, here are the steps for setting up Pimp Wars: @@ -97,12 +100,12 @@ doorPimpWars: { ``` -## QEMU with abracadabra +### QEMU with abracadabra [QEMU](http://wiki.qemu.org/Main_Page) provides a robust, cross platform solution for launching doors under many platforms (likely anwywhere Node.js is supported and ENiGMA½ can run). Note however that there is an important and major caveat: **Multiple instances of a particular door/OS image should not be run at once!** Being more flexible means being a bit more complex. Let's look at an example for running L.O.R.D. under a UNIX like system such as Linux or FreeBSD. Basically we'll be creating a bootstrap shell script that generates a temporary node specific `go.bat` to launch our door. This will be called from `autoexec.bat` within our QEMU FreeDOS partition. -### Step 1: Create a FreeDOS image +#### Step 1: Create a FreeDOS image [FreeDOS](http://www.freedos.org/) is a free mostly MS-DOS compatible DOS package that works well for running 16bit doors. Follow the [QEMU/FreeDOS](https://en.wikibooks.org/wiki/QEMU/FreeDOS) guide for creating an `freedos_c.img`. This will contain FreeDOS itself and installed BBS doors. After this is complete, copy LORD to C:\DOORS\LORD within FreeDOS. An easy way to tranfer files from host to DOS is to use QEMU's vfat as a drive. For example: @@ -115,7 +118,7 @@ With the above you can now copy files from D: to C: within FreeDOS and add the f CALL E:\GO.BAT ``` -### Step 2: Create a bootstrap script +#### Step 2: Create a bootstrap script Our bootstrap script will prepare `GO.BAT` and launch FreeDOS. Below is an example: @@ -147,7 +150,7 @@ Note the `qemu-system-i386` line. We're telling QEMU to launch and use localtime For doors that do not *require* a FOSSIL driver, it is recommended to not load or use one unless you are having issues. -#### Step 3: Create a menu entry +##### Step 3: Create a menu entry Finally we can create a `menu.hjson` entry using the `abracadabra` module: ```hjson doorLORD: { @@ -169,7 +172,10 @@ doorLORD: { } ``` -## Resources +## Shared Socket Descriptors +As of this writing `DOOR32.SYS` style socket descriptor sharing is **not** supported. Workarounds include using the Telnet Bridge (`telnet_bridge` module) to hook up to local Telnet-accessible door servers such as [NET2BBS](http://pcmicro.com/netfoss/guide/net2bbs.html). + +## Additional Resources ### DOSBox * Custom DOSBox builds http://home.arcor.de/h-a-l-9000/ From 8744226c15d9fb3454b6427020ed8d2e4487690d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Dec 2018 20:06:01 -0700 Subject: [PATCH 410/569] Dosbox-x --- docs/modding/local-doors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index 9322e970..b5c56f9c 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -178,7 +178,7 @@ As of this writing `DOOR32.SYS` style socket descriptor sharing is **not** suppo ## Additional Resources ### DOSBox -* Custom DOSBox builds http://home.arcor.de/h-a-l-9000/ +* [DOSBox-X](https://github.com/joncampbell123/dosbox-x) ### Door Downloads & Support Sites #### General From 929ec7a4ab1c627551e44f8ca8fcce49b4bd12ce Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Dec 2018 20:17:27 -0700 Subject: [PATCH 411/569] WIP doc on updating installations --- docs/_includes/nav.md | 1 + docs/admin/updating.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docs/admin/updating.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index b6303960..0c0cd945 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -83,6 +83,7 @@ - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) + - [Updating]({{ site.baseurl }}{% link admin/updating.md %}) - Troubleshooting - [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) diff --git a/docs/admin/updating.md b/docs/admin/updating.md new file mode 100644 index 00000000..71a3d794 --- /dev/null +++ b/docs/admin/updating.md @@ -0,0 +1,16 @@ +--- +layout: page +title: Updating +--- +## Updating your Installation +Updating ENiGMA½ can be a bit of a learning curve compared to other systems. Especially when running off of a development branch (such as `0.0.9-alpha` being the recommended branch as of this writing), you'll want frequent updates. + +## Steps +In general the steps are as follows: +1. `cd /path/to/enigma-bbs` +2. `git pull` +3. Merge updates to `config/menu_template.hjson` to your `config/yourbbsname-menu.hjson` file. +4. If there are updates to the `art/themes/luciano_blocktronics/theme.hjson` file and you have a custom theme, you way wan to look at them as well. + +Visual diff tools such as [DiffMerge](https://www.sourcegear.com/diffmerge/downloads.php) (free, works on all major platforms) can be very helpful here. + From 66ea15e3aa81ddbed775399d55e744eb4f0caadb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Dec 2018 20:18:07 -0700 Subject: [PATCH 412/569] Note on updating modules --- docs/admin/updating.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/admin/updating.md b/docs/admin/updating.md index 71a3d794..a6199ba5 100644 --- a/docs/admin/updating.md +++ b/docs/admin/updating.md @@ -9,8 +9,9 @@ Updating ENiGMA½ can be a bit of a learning curve compared to other systems. Es In general the steps are as follows: 1. `cd /path/to/enigma-bbs` 2. `git pull` -3. Merge updates to `config/menu_template.hjson` to your `config/yourbbsname-menu.hjson` file. -4. If there are updates to the `art/themes/luciano_blocktronics/theme.hjson` file and you have a custom theme, you way wan to look at them as well. +3. `npm update` or `yarn` to refresh any new or updated modules. +4. Merge updates to `config/menu_template.hjson` to your `config/yourbbsname-menu.hjson` file. +5. If there are updates to the `art/themes/luciano_blocktronics/theme.hjson` file and you have a custom theme, you way wan to look at them as well. Visual diff tools such as [DiffMerge](https://www.sourcegear.com/diffmerge/downloads.php) (free, works on all major platforms) can be very helpful here. From 104f6aeda962f346177fb1028a2b7da635eadc0a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Dec 2018 21:33:55 -0700 Subject: [PATCH 413/569] Dur --- docs/admin/updating.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/admin/updating.md b/docs/admin/updating.md index a6199ba5..310b6313 100644 --- a/docs/admin/updating.md +++ b/docs/admin/updating.md @@ -11,7 +11,10 @@ In general the steps are as follows: 2. `git pull` 3. `npm update` or `yarn` to refresh any new or updated modules. 4. Merge updates to `config/menu_template.hjson` to your `config/yourbbsname-menu.hjson` file. -5. If there are updates to the `art/themes/luciano_blocktronics/theme.hjson` file and you have a custom theme, you way wan to look at them as well. +5. If there are updates to the `art/themes/luciano_blocktronics/theme.hjson` file and you have a custom theme, you may want to look at them as well. Visual diff tools such as [DiffMerge](https://www.sourcegear.com/diffmerge/downloads.php) (free, works on all major platforms) can be very helpful here. +Remember to also keep an eye on [WHATSNEW](/WHATSNEW.md) and [UPGARDE](/UPGRADE.md)! + + From 9c835af0001dd17af9fc58034713080761bcf7ef Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 3 Dec 2018 23:51:43 -0700 Subject: [PATCH 414/569] Initial support for FILEGATE.ZXX / RAID style import of file areas --- core/oputil/oputil_file_base.js | 205 +++++++++++++++++++++++++++-- core/oputil/oputil_help.js | 4 + core/oputil/oputil_message_base.js | 2 +- 3 files changed, 201 insertions(+), 10 deletions(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 9dfcdeca..9d847968 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -9,7 +9,10 @@ const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDataba const getHelpFor = require('./oputil_help.js').getHelpFor; const { getAreaAndStorage, - looksLikePattern + looksLikePattern, + getConfigPath, + getAnswers, + writeConfig } = require('./oputil_common.js'); const Errors = require('../enig_error.js').Errors; @@ -20,6 +23,9 @@ const _ = require('lodash'); const moment = require('moment'); const inq = require('inquirer'); const glob = require('glob'); +const sanatizeFilename = require('sanitize-filename'); +const hjson = require('hjson'); +const { mkdirs } = require('fs-extra'); exports.handleFileBaseCommand = handleFileBaseCommand; @@ -692,6 +698,185 @@ function removeFiles() { ); } +function getFileBaseImportType(path) { + if(argv.type) { + return argv.type.toLowerCase(); + } + + return paths.extname(path).substr(1).toLowerCase(); // zxx, ... +} + +function importFileAreas() { + // + // FILEGATE.ZXX "RAID" format currently the only supported format. + // + // See http://www.filegate.net/info/filegate.zxx + // + const importPath = argv._[argv._.length - 1]; + if(argv._.length < 3 || !importPath || 0 === importPath.length) { + return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); + } + + const importType = getFileBaseImportType(importPath); + if('zxx' !== importType) { + return console.error(`"${importType}" is not a recognized import file type`); + } + + const createDirs = argv['create-dirs']; + // :TODO: --base-dir (override config base/relative dir; use full paths) + + async.waterfall( + [ + (callback) => { + fs.readFile(importPath, 'utf8', (err, importData) => { + if(err) { + return callback(err); + } + + const importInfo = { + storageTags : {}, + areas : {}, + count : 0, + }; + + const re = /Area\s+([^\s]+)\s+[0-9]\s+(?:!|\*&)\s+([^\r\n]+)/gm; + let m; + while((m = re.exec(importData))) { + const dir = m[1].trim(); + const name = m[2].trim(); + const safeName = sanatizeFilename(name); + + const stPrefix = _.snakeCase(sanatizeFilename(safeName)); + const storageTag = `${stPrefix}__${_.snakeCase(sanatizeFilename(dir))}`; + const areaTag = _.snakeCase(safeName); + + if(!dir || !name || !storageTag || !areaTag) { + console.info(`Skipping entry: ${m[0]}`); + continue; + } + + importInfo.storageTags[storageTag] = dir; + importInfo.areas[areaTag] = { + name : name, + desc : name, + storageTags : [ storageTag ], + }; + ++importInfo.count; + } + + if(0 === importInfo.count) { + return callback(new Error('Nothing to import')); + } + + return callback(null, importInfo); + }); + }, + (importInfo, callback) => { + return initConfigAndDatabases(err => { + return callback(err, importInfo); + }); + }, + (importInfo, callback) => { + console.info(`Read to import the following ${importInfo.count} areas:`); + console.info(''); + _.each(importInfo.areas, (area, areaTag) => { + console.info(`${area.name} (${areaTag}):`); + const dir = importInfo.storageTags[area.storageTags[0]]; + console.info(` storage: ${area.storageTags[0]} => ${dir}`); + }); + + getAnswers([ + { + name : 'proceed', + message : 'Proceed?', + type : 'confirm', + } + ], + answers => { + if(answers.proceed) { + return callback(null, importInfo); + } + return callback(Errors.General('User canceled')); + }); + }, + (importInfo, callback) => { + fs.readFile(getConfigPath(), 'utf8', (err, configData) => { + if(err) { + return callback(err); + } + let config; + try { + config = hjson.rt.parse(configData); + } catch(e) { + return callback(e); + } + return callback(null, importInfo, config); + }); + }, + (importInfo, config, callback) => { + const newStorageTagDirs = []; + _.each(importInfo.areas, (area, areaTag) => { + const existingArea = _.get(config, [ 'fileBase', 'areas', areaTag ]); + if(existingArea) { + return console.info(`Skipping ${area.name}. Area tag "${areaTag}" already exists.`); + } + + const storageTag = area.storageTags[0]; + const existingStorageTag = _.get(config, [ 'fileBase', 'storageTags', storageTag ]); + if(existingStorageTag) { + return console.info(`Skipping ${area.name} (${areaTag}). Storage tag "${storageTag}" already exists`); + } + + const dir = importInfo.storageTags[storageTag]; + newStorageTagDirs.push(dir); + + config.fileBase.storageTags[storageTag] = dir; + config.fileBase.areas[areaTag] = area; + }); + + return callback(null, newStorageTagDirs, config); + }, + (newStorageTagDirs, config, callback) => { + if(!createDirs) { + return callback(null, config); + } + + // + // Create all directories + // + const prefixDir = config.fileBase.areaStoragePrefix; + async.eachSeries(newStorageTagDirs, (dir, nextDir) => { + const isAbs = paths.isAbsolute(dir); + if(!isAbs) { + dir = paths.join(prefixDir, dir); + } + mkdirs(dir, err => { + if(!err) { + console.log(`Created ${dir}`); + } + return nextDir(err); + }); + }, + err => { + return callback(err, config); + }); + }, + (config, callback) => { + const written = writeConfig(config, getConfigPath()); + return callback(written ? null : new Error('Failed to write config!')); + } + ], + err => { + if(err) { + return console.error(err.reason ? err.reason : err.message); + } + + console.info('Import complete.'); + console.info(`You may wish to validate changes made to ${getConfigPath()}`); + } + ); +} + function handleFileBaseCommand() { function errUsage() { @@ -708,15 +893,17 @@ function handleFileBaseCommand() { const action = argv._[1]; return ({ - info : displayFileAreaInfo, - scan : scanFileAreas, + info : displayFileAreaInfo, + scan : scanFileAreas, - mv : moveFiles, - move : moveFiles, + mv : moveFiles, + move : moveFiles, - rm : removeFiles, - remove : removeFiles, - del : removeFiles, - delete : removeFiles, + rm : removeFiles, + remove : removeFiles, + del : removeFiles, + delete : removeFiles, + + 'import-areas' : importFileAreas, }[action] || errUsage)(); } \ No newline at end of file diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index f6ac84ba..04e79f34 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -64,6 +64,7 @@ actions: rm SRC [SRC...] remove entry(s) from the system matching SRC SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + import-areas FILEGATE.ZXX import file base areas using FileGate RAID type format scan args: --tags TAG1,TAG2,... specify tag(s) to assign to discovered entries @@ -80,6 +81,9 @@ info args: remove args: --phys-file also remove underlying physical file + +import-areas args: + --create-dirs create backing storage directories `, FileOpsInfo : ` diff --git a/core/oputil/oputil_message_base.js b/core/oputil/oputil_message_base.js index d3653caf..0f1b5cfb 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -377,7 +377,7 @@ function importAreas() { console.error(err.reason ? err.reason : err.message); } else { const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"'; - console.info('Configuration generated.'); + console.info('Import complete.'); console.info(`You may wish to validate changes made to ${getConfigPath()}`); console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`); console.info(''); From 1520d46763397172410e269d59e0cd7281a04494 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 4 Dec 2018 16:56:05 -0700 Subject: [PATCH 415/569] File base doc updates --- docs/filebase/first-file-area.md | 58 +++++++++++++++++++------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index 7d7a133d..c494bbe8 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -2,34 +2,38 @@ layout: page title: Configuring a File Base --- +## Configuring a File Base +ENiGMA½ offers a powerful and flexible file base. Configuration of file the file base and areas is handled via the `fileBase` section of `config.hjson`. + ## ENiGMA½ File Base Key Concepts -Like many things in ENiGMA½, configuration of file base(s) is handled via `config.hjson` — specifically -in the `fileBase` section. First, there are a couple of concepts you should understand: +First, there are some core concepts you should understand: +* Storage Tags +* Areas (and Area Tags) +### Storage Tags +*Storage Tags* define paths to physical (file) storage locations that are referenced in a file *Area* entry. Each entry may be either a fully qualified path or a relative path. Relative paths are relative to the value set by the `areaStoragePrefix` key (defaults to `/path/to/enigma-bbs/file_base`). -### Storage tags - -**Storage Tags** define paths to physical (file) storage locations that are referenced in a -file *Area* entry. Each entry may be either a fully qualified path or a relative path. Relative paths -are relative to the value set by the `areaStoragePrefix` key (defaults to ` Date: Tue, 4 Dec 2018 16:57:11 -0700 Subject: [PATCH 416/569] Minor typo --- docs/filebase/first-file-area.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index c494bbe8..810ec925 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -32,7 +32,7 @@ File base *Areas* are configured using the `fileBase::areas` configuration block |--------|---------------|------------------| | `name` | :+1: | Friendly area name. | | `desc` | :-1: | Friendly area description. | -| `storageTags` : | :+1: | An array of storage tags for physical storage backing of the files in this area. If uploads are enabled for this area, **first ** storage tag location is utilized!** | +| `storageTags` | :+1: | An array of storage tags for physical storage backing of the files in this area. If uploads are enabled for this area, **first ** storage tag location is utilized!** | | `sort` | :-1: | If present, provides the sort key for ordering. `name` is used otherwise. | Example areas section: From 351807deb1d1abb8ccd0863d538be07aaa68dc26 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 4 Dec 2018 16:58:21 -0700 Subject: [PATCH 417/569] Another minor typo --- docs/filebase/first-file-area.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index 810ec925..22c71aa4 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -32,7 +32,7 @@ File base *Areas* are configured using the `fileBase::areas` configuration block |--------|---------------|------------------| | `name` | :+1: | Friendly area name. | | `desc` | :-1: | Friendly area description. | -| `storageTags` | :+1: | An array of storage tags for physical storage backing of the files in this area. If uploads are enabled for this area, **first ** storage tag location is utilized!** | +| `storageTags` | :+1: | An array of storage tags for physical storage backing of the files in this area. If uploads are enabled for this area, **first** storage tag location is utilized! | | `sort` | :-1: | If present, provides the sort key for ordering. `name` is used otherwise. | Example areas section: From 6f15622f2c67ba95afb6e207320ce7a70cbbea57 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 4 Dec 2018 16:59:56 -0700 Subject: [PATCH 418/569] Yet another... --- docs/filebase/first-file-area.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index 22c71aa4..59e1e56b 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -49,7 +49,7 @@ areas: { The above example defines an area called "Retro PC" which is referenced via the *area tag* of `retro_pc`. Two storage tags are used: `retro_pc_dos`, and `retro_pc_bbs`. These storage tags can be seen in the Storage Tags example above. ## Example Configuration -This combines the two concepts described above. When viewing the file areas from ENiGMA½ a user will only see the "Retro PC" area, but the files in the area are stored in the two locations defined in the `storageTags` section. We also show a uploads area. Uploads are allowed do to the [ACS](acs.md) block. See [Uploads](uploads.md) for more information. +This combines the two concepts described above. When viewing the file areas from ENiGMA½ a user will only see the "Retro PC" area, but the files in the area are stored in the two locations defined in the `storageTags` section. We also show a uploads area. Uploads are allowed due to the [ACS](acs.md) block. See [Uploads](uploads.md) for more information. ```hjson fileBase: { From 32986fa60b36235a92431899a7af40fe448bfb99 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 4 Dec 2018 18:24:43 -0700 Subject: [PATCH 419/569] Case sensitive note --- docs/filebase/first-file-area.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index 59e1e56b..9879cd86 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -25,6 +25,8 @@ storageTags: { :information_source: On their own, storage tags don't do anything — they are simply pointers to storage locations on your system. +:information_source: Remember that paths are case sensitive on most non-Windows systems! + ### Areas File base *Areas* are configured using the `fileBase::areas` configuration block in `config.hjson`. Valid members for an area are as follows: From dbc60e8746425ab9fe9437d76ea4fa0825045508 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 4 Dec 2018 19:39:00 -0700 Subject: [PATCH 420/569] Notes on oputil fb import-areas --- docs/admin/oputil.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 1e081afb..d7e69b0f 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -92,7 +92,7 @@ usage: oputil.js fb [] actions: scan AREA_TAG[@STORAGE_TAG] scan specified area may also contain optional GLOB as last parameter, - for examle: scan some_area *.zip + for example: scan some_area *.zip info CRITERIA display information about areas and/or files where CRITERIA is one of the following: @@ -105,6 +105,7 @@ actions: rm SRC [SRC...] remove entry(s) from the system matching SRC SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + import-areas FILEGATE.ZXX import file base areas using FileGate RAID type format scan args: --tags TAG1,TAG2,... specify tag(s) to assign to discovered entries @@ -122,6 +123,9 @@ info args: remove args: --phys-file also remove underlying physical file +import-areas args: + --create-dirs create backing storage directories + general information: AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag example: retro@bbs @@ -136,12 +140,12 @@ The `scan` action can (re)scan a file area for new entries as well as update (`- ##### Examples Performing a quick scan of a specific area's storage location ("retro_warez", "retro_warez_games) matching only *.zip extensions: -``` +```bash $ ./oputil.js fb scan --quick retro_warez@retro_warez_games *.zip` ``` Update all entries in the "artscene" area supplying the file tags "artscene", and "textmode". -``` +```bash $ ./oputil.js fb scan --update --quick --tags artscene,textmode artscene` ``` @@ -155,8 +159,8 @@ The `info` action can retrieve information about an area or file entry(s). ##### Examples Information about a particular area: -``` -$ ./oputil.js fb info retro_pc +```bash +./oputil.js fb info retro_pc areaTag: retro_pc name: Retro PC desc: Oldschool / retro PC @@ -167,8 +171,8 @@ storageTag: retro_pc_tdc_1993 => /file_base/dos/tdc/1993 ``` Perhaps we want to fetch some information about a file in which we know piece of the filename: -``` -$ ./oputil.js fb info "impulse*" +```bash +./oputil.js fb info "impulse*" file_id: 143 sha_256: 547299301254ccd73eba4c0ec9cd6ab8c5929fbb655e72c4cc842f11332792d4 area_tag: impulse_project @@ -185,6 +189,16 @@ file_md5: 3455f74bbbf9539e69bd38f45e039a4e file_sha1: 558fab3b49a8ac302486e023a3c2a86bd4e4b948 ``` +### Importing FileGate RAID Style Areas +Given a FileGate "RAID" style `FILEGATE.ZXX` file, one can import areas. + +#### Example +```bash +./oputil.js fb import-areas FILEGATE.ZXX --create-dirs +``` + +The above command will process FILEGATE.ZXX creating areas and backing directories. Directories created are relative to the `fileBase.areaStoragePrefix` `config.hjson` setting. + ## Message Base Management The `mb` command provides various Message Base related tools: From 60369ea378be82cd76ba954577b9bd34dec919a3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 4 Dec 2018 20:42:56 -0700 Subject: [PATCH 421/569] * Note on FILEBONE.NA support * Notes on importing in file area docs --- core/oputil/oputil_file_base.js | 4 +++- core/oputil/oputil_help.js | 1 + docs/admin/oputil.md | 1 + docs/filebase/first-file-area.md | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 9d847968..2077b385 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -711,6 +711,8 @@ function importFileAreas() { // FILEGATE.ZXX "RAID" format currently the only supported format. // // See http://www.filegate.net/info/filegate.zxx + // ...same format as FILEBONE.NA: + // http://wiki.mysticbbs.com/doku.php?id=mutil_import_filebone_na // const importPath = argv._[argv._.length - 1]; if(argv._.length < 3 || !importPath || 0 === importPath.length) { @@ -718,7 +720,7 @@ function importFileAreas() { } const importType = getFileBaseImportType(importPath); - if('zxx' !== importType) { + if(!['zxx', 'na'].includes(importType)) { return console.error(`"${importType}" is not a recognized import file type`); } diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 04e79f34..8b946edd 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -83,6 +83,7 @@ remove args: --phys-file also remove underlying physical file import-areas args: + --type TYPE sets import areas type. valid options are "zxx" or "na" --create-dirs create backing storage directories `, FileOpsInfo : diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index d7e69b0f..c733070f 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -124,6 +124,7 @@ remove args: --phys-file also remove underlying physical file import-areas args: + --type TYPE sets import areas type. valid options are "zxx" or "na" --create-dirs create backing storage directories general information: diff --git a/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index 9879cd86..ab76fe24 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/filebase/first-file-area.md @@ -82,3 +82,5 @@ fileBase: { } ``` +## Importing Areas +Areas can also be imported using [oputil](/docs/admin/oputil.md) using proper FileGate "RAID" aka `FILEBONE.NA` style files. After importing areas, you may wish to tweak configuration such as better `desc` fields, ACS, or sorting. From 0e2593bd1c6e7123d4f33170b7d1703e223baf63 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 4 Dec 2018 20:47:29 -0700 Subject: [PATCH 422/569] More notes on importing file areas --- docs/admin/oputil.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index c733070f..285d96fa 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -191,13 +191,20 @@ file_sha1: 558fab3b49a8ac302486e023a3c2a86bd4e4b948 ``` ### Importing FileGate RAID Style Areas -Given a FileGate "RAID" style `FILEGATE.ZXX` file, one can import areas. +Given a FileGate "RAID" style `FILEGATE.ZXX` file, one can import areas. This format also often comes in FTN-style info packs in the form of a `.NA` file i.e.: `FILEBONE.NA`. #### Example ```bash ./oputil.js fb import-areas FILEGATE.ZXX --create-dirs ``` +-or- + +```bash +# fsxNet info packs contain a FSX_FILE.NA file +./oputil.js fb import-areas FSX_FILE.NA --create-dirs --type NA +``` + The above command will process FILEGATE.ZXX creating areas and backing directories. Directories created are relative to the `fileBase.areaStoragePrefix` `config.hjson` setting. ## Message Base Management From ecb0cd89412c5ffecb5711450e7e91a8cf1e491f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Dec 2018 00:43:20 -0700 Subject: [PATCH 423/569] Minor door updates --- core/abracadabra.js | 4 +++- core/door.js | 24 +++++++++++++++++------- core/dropfile.js | 4 +++- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 32c4466a..e448315b 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -9,9 +9,11 @@ const ansi = require('./ansi_term.js'); const Events = require('./events.js'); const { Errors } = require('./enig_error.js'); +// deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); +const paths = require('path'); const activeDoorNodeInstances = {}; @@ -153,7 +155,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { const exeInfo = { cmd : this.config.cmd, - cwd : this.config.cwd, // null/undefined = parent_of(cmd) + cwd : this.config.cwd || paths.dirname(this.config.cmd), args : this.config.args, io : this.config.io || 'stdio', encoding : this.config.encoding || 'cp437', diff --git a/core/door.js b/core/door.js index d6326cbe..eb01d6fa 100644 --- a/core/door.js +++ b/core/door.js @@ -71,13 +71,23 @@ module.exports = class Door { const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); - const door = pty.spawn(exeInfo.cmd, args, { - cols : this.client.term.termWidth, - rows : this.client.term.termHeight, - cwd : cwd, - env : exeInfo.env, - encoding : null, // we want to handle all encoding ourself - }); + this.client.log.debug( + { cmd : exeInfo.cmd, args, io : this.io }, + 'Executing door' + ); + + let door; + try { + door = pty.spawn(exeInfo.cmd, args, { + cols : this.client.term.termWidth, + rows : this.client.term.termHeight, + cwd : cwd, + env : exeInfo.env, + encoding : null, // we want to handle all encoding ourself + }); + } catch(e) { + return cb(e); + } if('stdio' === this.io) { this.client.log.debug('Using stdio for door I/O'); diff --git a/core/dropfile.js b/core/dropfile.js index 6595aa1a..eb3d9136 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -16,6 +16,7 @@ const { mkdirs } = require('fs-extra'); // // Resources +// * https://github.com/NuSkooler/ansi-bbs/tree/master/docs/dropfile_formats // * http://goldfndr.home.mindspring.com/dropfile/ // * https://en.wikipedia.org/wiki/Talk%3ADropfile // * http://thoughtproject.com/libraries/bbs/Sysop/Doors/DropFiles/index.htm @@ -36,7 +37,7 @@ module.exports = class DropFile { get fileName() { return { DOOR : 'DOOR.SYS', // GAP BBS, many others - DOOR32 : 'DOOR32.SYS', // EleBBS / Mystic, Syncronet, Maximus, Telegard, AdeptXBBS, ... + DOOR32 : 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec) CALLINFO : 'CALLINFO.BBS', // Citadel? DORINFO : this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... CHAIN : 'CHAIN.TXT', // WWIV @@ -155,6 +156,7 @@ module.exports = class DropFile { // // Resources: // * http://wiki.bbses.info/index.php/DOOR32.SYS + // * https://github.com/NuSkooler/ansi-bbs/blob/master/docs/dropfile_formats/door32_sys.txt // // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! const Door32CommTypes = { From 2474e82829184289b0c9ea0da12d1511199612b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Dec 2018 23:41:42 -0700 Subject: [PATCH 424/569] Change default dropfile path to just ./enigma-bbs/drop/ so we can have a shorter name for Win16 binaries --- core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index 3f4aeb61..345f4c09 100644 --- a/core/config.js +++ b/core/config.js @@ -261,7 +261,7 @@ function getDefaultConfig() { logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such db : paths.join(__dirname, './../db/'), modsDb : paths.join(__dirname, './../db/mods/'), - dropFiles : paths.join(__dirname, './../dropfiles/'), // + "/node/ + dropFiles : paths.join(__dirname, './../drop/'), // + "/node/ misc : paths.join(__dirname, './../misc/'), }, From fd59a8512b708e756859869c3756d22670df2941 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 8 Dec 2018 23:42:14 -0700 Subject: [PATCH 425/569] Lots of local door doc cleanup + notes on bivrost! / shared file descriptor usage --- docs/modding/local-doors.md | 171 +++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 62 deletions(-) diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index b5c56f9c..06b80ac1 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -10,37 +10,48 @@ The `abracadabra` module provides a generic and flexible solution for many door ### Configuration The `abracadabra` `config` block can contain the following members: -* `name`: Used as a key for tracking number of clients using a particular door. -* `dropFileType`: Specifies the type of drop file to generate (See **Argument Variables** below). -* `cmd`: Path to executable to launch. -* `args`: Array of argument(s) to pass to `cmd`. See below for information on variables that can be used here. -* `cwd`: Set the Current Working Directory for `cmd`. Defaults to the directory of `cmd`. -* `nodeMax`: Max number of nodes that can access this door at once. Uses `name` as a mapping key -* `tooManyArt`: Art file spec to display if too many instances are already in use -* `io`: Where to process I/O. Can be `stdio` or `socket`. When using `stdio`, I/O is input/output from stdin/stdout. When using `socket` a temporary socket server is spawned that can be connected to. The server listens on localhost on `{srvPort}` (see below under Argument Variables). -* `encoding`: Specify the door's encoding. Defaults to `cp437`. Linux binaries for example, often produce `utf8`. -#### Drop File Types -Drop file types specified by `dropFileType`: -* `DOOR`: [DOOR.SYS](http://goldfndr.home.mindspring.com/dropfile/doorsys.htm) -* `DOOR32`: [DOOR32.SYS](https://raw.githubusercontent.com/NuSkooler/ansi-bbs/master/docs/dropfile_formats/door32_sys.txt) -* `DORINFO`: [DORINFOx.DEF](http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm) +| Item | Required | Description | +|------|----------|-------------| +| `name` | :+1: | Used as a key for tracking number of clients using a particular door. | +| `dropFileType` | :+1: | Specifies the type of dropfile to generate (See **Dropfile Types** below). | +| `cmd` | :+1: | Path to executable to launch. | +| `args` | :-1: | Array of argument(s) to pass to `cmd`. See **Argument Variables** below for information on variables that can be used here. +| `cwd` | :-1: | Sets the Current Working Directory (CWD) for `cmd`. Defaults to the directory of `cmd`. | +| `nodeMax` | :-1: | Max number of nodes that can access this door at once. Uses `name` as a tracking key. | +| `tooManyArt` | :-1: | Art spec to display if too many instances are already in use. | +| `io` | :-1: | How to process input/output (I/O). Can be `stdio` or `socket`. When using `stdio`, I/O is handled via standard stdin/stdout. When using `socket` a temporary socket server is spawned that can be connected back to. The server listens on localhost on `{srvPort}` (See **Argument Variables** below for more information). Default value is `stdio`. | +| `encoding` | :-1: | Sets the **door's** encoding. Defaults to `cp437`. Linux binaries often produce `utf8`. | + +#### Dropfile Types +Dropfile types specified by `dropFileType`: + +| Value | Description | +|-------|-------------| +| `DOOR` | [DOOR.SYS](http://goldfndr.home.mindspring.com/dropfile/doorsys.htm) +| `DOOR32` | [DOOR32.SYS](https://raw.githubusercontent.com/NuSkooler/ansi-bbs/master/docs/dropfile_formats/door32_sys.txt) +| `DORINFO` | [DORINFOx.DEF](http://goldfndr.home.mindspring.com/dropfile/dorinfo.htm) #### Argument Variables The following variables may be used in `args` entries: -* `{node}`: Current node number. -* `{dropFile}`: Drop _filename_ only. -* `{dropFilePath}`: Full path to generated drop file. -* `{userId}`: Current user ID. -* `{userName}`: _Sanitized_ username. Safe for filenames, etc. -* `{userNameRaw}`: _Raw_ username. May not be safe for filenames! -* `{srvPort}`: Temporary server port when `io` is set to `socket`. -* `{cwd}`: Current Working Directory. -Example: +| Variable | Description | Example | +|----------|-------------|---------| +| `{node}` | Current node number. | `1` | +| `{dropFile}` | Dropfile _filename_ only. | `DOOR.SYS` | +| `{dropFilePath}` | Full path to generated dropfile. The system places dropfiles in the path set by `paths.dropFiles` in `config.hjson`. | `C:\enigma-bbs\drop\node1\DOOR.SYS` | +| `{userId}` | Current user ID. | `420` | +| `{userName}` | _Sanitized_ username. Safe for filenames, etc. | `izard` | +| `{userNameRaw}` | _Raw_ username. May not be safe for filenames! | `\/\/izard` | +| `{srvPort}` | Temporary server port when `io` is set to `socket`. | `1234` | +| `{cwd}` | Current Working Directory. | `/home/enigma-bbs/doors/foo/` | + +Example `args` member using some variables described above: ```hjson args: [ - "-D", "{dropFile}", "-N", "{node}" + "-D", "{dropFilePath}", + "-N", "{node}" + "-U", "{userId}" ] ``` @@ -63,7 +74,7 @@ $_com1 = "virtual" The line `$_com1 = "virtual"` tells DOSEMU to use `stdio` as a virtual serial port on COM1. -Next, we create a virtual **X** drive for Pimp Wars to live such as `/enigma-bbs/DOS/X/PW` and map it with a custom `autoexec.bat` file within DOSEMU: +Next, we create a virtual **X** drive for Pimp Wars to live such as `/enigma-bbs/DOS/X/PW` and map it with a custom `AUTOEXEC.BAT` file within DOSEMU: ``` @echo off path d:\bin;d:\gnu;d:\dosemu @@ -80,30 +91,70 @@ Note that we also have the [BNU](http://www.pcmicro.com/bnu/) FOSSIL driver inst Finally, let's create a `menu.hjson` entry to launch the game: ```hjson doorPimpWars: { - desc: Playing PimpWars - module: abracadabra - config: { - name: PimpWars - dropFileType: DORINFO - cmd: /usr/bin/dosemu - args: [ - "-quiet", + desc: Playing PimpWars + module: abracadabra + config: { + name: PimpWars + dropFileType: DORINFO + cmd: /usr/bin/dosemu + args: [ + "-quiet", "-f", "/path/to/dosemu.conf", "X:\\PW\\START.BAT {dropFile} {node}" - ], - nodeMax: 1 - tooManyArt: DOORMANY + ], + nodeMax: 1 + tooManyArt: DOORMANY io: stdio - } + } } - ``` -### QEMU with abracadabra -[QEMU](http://wiki.qemu.org/Main_Page) provides a robust, cross platform solution for launching doors under many platforms (likely anwywhere Node.js is supported and ENiGMA½ can run). Note however that there is an important and major caveat: **Multiple instances of a particular door/OS image should not be run at once!** Being more flexible means being a bit more complex. Let's look at an example for running L.O.R.D. under a UNIX like system such as Linux or FreeBSD. +### Shared Socket Descriptors +Due to Node.js limitations, ENiGMA½ does not _directly_ support `DOOR32.SYS` style socket descriptor sharing (other `DOOR32.SYS` features are fully supported). However, a separate binary called [bivrost!](https://github.com/NuSkooler/bivrost) can be used. bivrost! is available for Windows and Linux x86/i686 and x86_64/AMD64. Other platforms where [Rust](https://www.rust-lang.org/) builds are likely to work as well. -Basically we'll be creating a bootstrap shell script that generates a temporary node specific `go.bat` to launch our door. This will be called from `autoexec.bat` within our QEMU FreeDOS partition. +#### Example configuration +Below is an example `menu.hjson` entry using bivrost! to launch a door: + +```hjson +doorWithBivrost: { + desc: Bivrost Example + module: abracadabra + config: { + name: BivrostExample + dropFileType: DOOR32 + cmd: "C:\\enigma-bbs\\utils\\bivrost.exe" + args: [ + "--port", "{srvPort}", // bivrost! will connect this port on localhost + "--dropfile", "{dropFilePath}", // ...and read this DOOR32.SYS produced by ENiGMA½ + "--out", "C:\\doors\\jezebel", // ...and produce a NEW DOOR32.SYS here. + + // + // Note that the final params bivrost! will use to + // launch the door are grouped here. The {fd} variable could + // also be supplied here if needed. + // + "C:\\door\\door.exe C:\\door\\door32.sys" + ], + nodeMax: 1 + tooManyArt: DOORMANY + io: socket + } +} +``` + +Please see the [bivrost!](https://github.com/NuSkooler/bivrost) documentation for more information. + +#### Phenom Productions Releases +Pre-built binaries of bivrost! have been released under [Phenom Productions](https://www.phenomprod.com/) and can be found on various boards. + +#### Alternative Workarounds +Alternative workarounds include Telnet Bridge (`telnet_bridge` module) to hook up Telnet-accessible (including local) door servers -- It may also be possible bridge via [NET2BBS](http://pcmicro.com/netfoss/guide/net2bbs.html). + +### QEMU with abracadabra +[QEMU](http://wiki.qemu.org/Main_Page) provides a robust, cross platform solution for launching doors under many platforms (likely anywhere Node.js is supported and ENiGMA½ can run). Note however that there is an important and major caveat: **Multiple instances of a particular door/OS image should not be run at once!** Being more flexible means being a bit more complex. Let's look at an example for running L.O.R.D. under a UNIX like system such as Linux or FreeBSD. + +Basically we'll be creating a bootstrap shell script that generates a temporary node specific `GO.BAT` to launch our door. This will be called from `AUTOEXEC.BAT` within our QEMU FreeDOS partition. #### Step 1: Create a FreeDOS image [FreeDOS](http://www.freedos.org/) is a free mostly MS-DOS compatible DOS package that works well for running 16bit doors. Follow the [QEMU/FreeDOS](https://en.wikibooks.org/wiki/QEMU/FreeDOS) guide for creating an `freedos_c.img`. This will contain FreeDOS itself and installed BBS doors. @@ -114,7 +165,7 @@ qemu-system-i386 -localtime /home/enigma/dos/images/freedos_c.img -hdb fat:/path ``` With the above you can now copy files from D: to C: within FreeDOS and add the following to it's `autoexec.bat`: -```batch +```bat CALL E:\GO.BAT ``` @@ -146,7 +197,7 @@ unix2dos /home/enigma/dos/go/node$NODE/GO.BAT qemu-system-i386 -localtime /home/enigma/dos/images/freedos_c.img -chardev socket,port=$SRVPORT,nowait,host=localhost,id=s0 -device isa-serial,chardev=s0 -hdb fat:/home/enigma/xibalba/dropfiles/node$NODE -hdc fat:/home/enigma/dos/go/node$NODE -nographic ``` -Note the `qemu-system-i386` line. We're telling QEMU to launch and use localtime for the clock, create a character device that connects to our temporary server port on localhost and map that to a serial device. The `-hdb` entry will represent the D: drive where our drop file is generated, while `-hdc` is the path that `GO.BAT` is generated in (`E:\GO.BAT`). Finally we specify `-nographic` to run headless. +Note the `qemu-system-i386` line. We're telling QEMU to launch and use localtime for the clock, create a character device that connects to our temporary server port on localhost and map that to a serial device. The `-hdb` entry will represent the D: drive where our dropfile is generated, while `-hdc` is the path that `GO.BAT` is generated in (`E:\GO.BAT`). Finally we specify `-nographic` to run headless. For doors that do not *require* a FOSSIL driver, it is recommended to not load or use one unless you are having issues. @@ -154,29 +205,25 @@ For doors that do not *require* a FOSSIL driver, it is recommended to not load o Finally we can create a `menu.hjson` entry using the `abracadabra` module: ```hjson doorLORD: { - desc: Playing L.O.R.D. - module: abracadabra - config: { - name: LORD - dropFileType: DOOR - cmd: /home/enigma/dos/scripts/lord.sh - args: [ - "{node}", - "{dropFile}", - "{srvPort}", - ], - nodeMax: 1 - tooManyArt: DOORMANY - io: socket - } + desc: Playing L.O.R.D. + module: abracadabra + config: { + name: LORD + dropFileType: DOOR + cmd: /home/enigma/dos/scripts/lord.sh + args: [ + "{node}", + "{dropFile}", + "{srvPort}", + ], + nodeMax: 1 + tooManyArt: DOORMANY + io: socket + } } ``` -## Shared Socket Descriptors -As of this writing `DOOR32.SYS` style socket descriptor sharing is **not** supported. Workarounds include using the Telnet Bridge (`telnet_bridge` module) to hook up to local Telnet-accessible door servers such as [NET2BBS](http://pcmicro.com/netfoss/guide/net2bbs.html). - ## Additional Resources - ### DOSBox * [DOSBox-X](https://github.com/joncampbell123/dosbox-x) From a8604ece5479ec253dc6a41121122864162d3a79 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 00:17:03 -0700 Subject: [PATCH 426/569] Ensure 'userName' has _something_ if sanatized all the way out --- core/door.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/door.js b/core/door.js index eb01d6fa..120bd6f3 100644 --- a/core/door.js +++ b/core/door.js @@ -64,7 +64,7 @@ module.exports = class Door { node : exeInfo.node.toString(), srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', userId : this.client.user.userId.toString(), - userName : sanatizeFilename(this.client.user.username), + userName : sanatizeFilename(this.client.user.username) || `user${this.client.user.userId.toString()}`, userNameRaw : this.client.user.username, cwd : cwd, }; From 8652b35b46873ff12081f84eec15f4933d02c1d3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 01:01:55 -0700 Subject: [PATCH 427/569] Code cleanup & resolve some minor TODO's in dropfile gen --- core/door.js | 3 +-- core/dropfile.js | 33 ++++++++++++++++++--------------- core/user.js | 16 +++++++++++----- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/core/door.js b/core/door.js index 120bd6f3..b07c89bc 100644 --- a/core/door.js +++ b/core/door.js @@ -9,7 +9,6 @@ const pty = require('node-pty'); const decode = require('iconv-lite').decode; const createServer = require('net').createServer; const paths = require('path'); -const sanatizeFilename = require('sanitize-filename'); module.exports = class Door { constructor(client) { @@ -64,7 +63,7 @@ module.exports = class Door { node : exeInfo.node.toString(), srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', userId : this.client.user.userId.toString(), - userName : sanatizeFilename(this.client.user.username) || `user${this.client.user.userId.toString()}`, + userName : this.client.user.getSanitizedName(), userNameRaw : this.client.user.username, cwd : cwd, }; diff --git a/core/dropfile.js b/core/dropfile.js index eb3d9136..0c66d38a 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -5,6 +5,7 @@ const Config = require('./config.js').get; const StatLog = require('./stat_log.js'); const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps const fs = require('graceful-fs'); @@ -86,9 +87,14 @@ module.exports = class DropFile { const prop = this.client.user.properties; const now = moment(); const secLevel = this.client.user.getLegacySecurityLevel().toString(); - const fullName = prop[UserProps.RealName] || this.client.user.username; + const fullName = this.client.user.getSanitizedName('real'); const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY'); + const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024); + const downK = Math.floor((parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024); + + const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format('hh:mm'); + // :TODO: fix time remaining // :TODO: fix default protocol -- user prop: transfer_protocol return iconv.encode( [ @@ -127,8 +133,8 @@ module.exports = class DropFile { bd, // "Caller's Birthdate" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" 'X:\\GEN\\', // "Path to the GEN directory" - StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" - this.client.user.username, // "Alias name" + StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)" + this.client.user.getSanitizedName(), // "Alias name" '00:05', // "Event time (hh:mm)" (note: wat?) 'Y', // "If its an error correcting connection (Y/N)" 'Y', // "ANSI supported & caller using NG mode (Y/N)" @@ -137,18 +143,15 @@ module.exports = class DropFile { // :TODO: fix minutes here also: '256', // "Time Credits In Minutes (positive/negative)" '07/07/90', // "Last New Files Scan Date (mm/dd/yy)" - // :TODO: fix last vs now times: - now.format('hh:mm'), // "Time of This Call" - now.format('hh:mm'), // "Time of Last Call (hh:mm)" + timeOfCall, // "Time of This Call" + timeOfCall, // "Time of Last Call (hh:mm)" '9999', // "Maximum daily files available" - // :TODO: fix these stats: - '0', // "Files d/led so far today" - '0', // "Total "K" Bytes Uploaded" - '0', // "Total "K" Bytes Downloaded" + '0', // "Files d/led so far today" + upK.toString(), // "Total "K" Bytes Uploaded" + downK.toString(), // "Total "K" Bytes Downloaded" prop[UserProps.UserComment] || 'None', // "User Comment" '0', // "Total Doors Opened" '0', // "Total Messages Left" - ].join('\r\n') + '\r\n', 'cp437'); } @@ -173,8 +176,8 @@ module.exports = class DropFile { '115200', Config().general.boardName, this.client.user.userId.toString(), - this.client.user.properties[UserProps.RealName] || this.client.user.username, - this.client.user.username, + this.client.user.getSanitizedName('real'), + this.client.user.getSanitizedName(), this.client.user.getLegacySecurityLevel().toString(), '546', // :TODO: Minutes left! '1', // ANSI @@ -191,8 +194,8 @@ module.exports = class DropFile { // // Note that usernames are just used for first/last names here // - const opUserName = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; - const userName = /[^\s]*/.exec(this.client.user.username)[0]; + const opUserName = /[^\s]*/.exec(StatLog.getSystemStat(SysProps.SysOpUsername))[0]; + const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0]; const secLevel = this.client.user.getLegacySecurityLevel().toString(); const location = this.client.user.properties[UserProps.Location]; diff --git a/core/user.js b/core/user.js index 0121133a..ba89b387 100644 --- a/core/user.js +++ b/core/user.js @@ -15,11 +15,12 @@ const Log = require('./logger.js').log; const StatLog = require('./stat_log.js'); // deps -const crypto = require('crypto'); -const assert = require('assert'); -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +const crypto = require('crypto'); +const assert = require('assert'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); +const sanatizeFilename = require('sanitize-filename'); exports.isRootUserId = function(id) { return 1 === id; }; @@ -114,6 +115,11 @@ module.exports = class User { return isMember; } + getSanitizedName(type='username') { + const name = 'real' === type ? this.getProperty(UserProps.RealName) : this.username; + return sanatizeFilename(name) || `user${this.userId.toString()}`; + } + getLegacySecurityLevel() { if(this.isRoot() || this.isGroupMember('sysops')) { return 100; From 4bceb74cc405ee53ca81381ff950000bdcf4de29 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 01:22:33 -0700 Subject: [PATCH 428/569] ACS doc improvements --- docs/configuration/acs.md | 22 +++++++++---- .../configuring-a-message-area.md | 32 ++++++++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index dde55709..1ed83bb5 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -4,7 +4,7 @@ title: Access Condition System (ACS) --- ## Access Condition System (ACS) -ENiGMA½ uses an Access Condition System (ACS) that is both familure to oldschool BBS operators and has it's own style. With ACS, SysOp's are able to control access to various areas of the system based on various conditions such as group membership, connection type, etc. Various touch points in the system are configured to allow for `acs` checks. In some cases ACS is a simple boolean check while others (via ACS blocks) allow to define what conditions must be true for certain _rights_ such as `read` and `write` (though others exist as well). +ENiGMA½ uses an Access Condition System (ACS) that is both familiar to oldschool BBS operators and has it's own style. With ACS, SysOp's are able to control access to various areas of the system based on various conditions such as group membership, connection type, etc. Various touch points in the system are configured to allow for `acs` checks. In some cases ACS is a simple boolean check while others (via ACS blocks) allow to define what conditions must be true for certain _rights_ such as `read` and `write` (though others exist as well). ## ACS Codes The following are ACS codes available as of this writing: @@ -48,20 +48,30 @@ The following logical operators are supported: ENiGMA½ also supports groupings using `(` and `)`. Lastly, some ACS codes allow for lists of acceptable values using `[` and `]` — for example, `GM[users,sysops]`. -### Examples +### Example ACS Strings * `NC2`: User must have called two more more times for the check to return true (to pass) * `ID1`: User must be ID 1 (the +op) * `GM[elite,power]`: User must be a member of the `elite` or `power` user group (they could be both) * `ID1|GM[co-op]`: User must be ID 1 (SysOp!) or belong to the `co-op` group * `!TH24`: Terminal height must NOT be 24 +## ACS Blocks +Some areas of the system require more than a single ACS string. In these situations an *ACS block* is used to allow for finer grain control. As an example, consider the following file area `acs` block: +```hjson +acs: { + read: GM[users] + write: GM[sysops,co-ops] + download: GM[elite-users] +} +``` + +All `users` can read (see) the area, `sysops` and `co-ops` can write (upload), and only members of the `elite-users` group can download. ## ACS Touch Points The following touch points exist in the system. Many more are planned: -* Message conferences and areas -* File base areas -* Menus within `menu.hjson`. See [Menu HJSON](menu-hjson.md). - +* [Message conferences and areas](/docs/messageareas/configuring-a-message-area.md) +* [File base areas](/docs/filebase/first-file-area.md) and [Uploads](/docs/filebase/uploads.md) +* Menus within [Menu HJSON (menu.hjson)](menu-hjson.md) See the specific areas documentation for information on available ACS checks. diff --git a/docs/messageareas/configuring-a-message-area.md b/docs/messageareas/configuring-a-message-area.md index 8e8b5a33..63af2826 100644 --- a/docs/messageareas/configuring-a-message-area.md +++ b/docs/messageareas/configuring-a-message-area.md @@ -10,13 +10,18 @@ Message Conferences are the top level container for *1:n* Message *Areas* via th Each conference is represented by a entry under `messageConferences`. Each entries top level key is it's *conference tag*. -| Config Item | Required | Description | -|-------------|----------|---------------------------------------------------------------------------------| -| `name` | :+1: | Friendly conference name | -| `desc` | :+1: | Friendly conference description. | -| `sort` | :-1: | Set to a number to override the default alpha-numeric sort order based on the `name` field. | -| `default` | :-1: | Specify `true` to make this the default conference (e.g. assigned to new users) | -| `areas` | :+1: | Container of 1:n areas described below | +| Config Item | Required | Description | +|-------------|----------|-------------| +| `name` | :+1: | Friendly conference name | +| `desc` | :+1: | Friendly conference description. | +| `sort` | :-1: | Set to a number to override the default alpha-numeric sort order based on the `name` field. | +| `default` | :-1: | Specify `true` to make this the default conference (e.g. assigned to new users) | +| `areas` | :+1: | Container of 1:n areas described below | +| `acs` | :-1: | A standard [ACS](/docs/configuration/acs.md) block. See **ACS** below. | + +### ACS +An optional standard [ACS](/docs/configuration/acs.md) block can be supplied with the following rules: +* `read`: ACS require to read (see) this conference. Defaults to `GM[users]`. ### Example @@ -28,6 +33,9 @@ Each conference is represented by a entry under `messageConferences`. Each entri desc: Local discussion sort: 1 default: true + acs: { + read: GM[users] // default + } } } } @@ -42,6 +50,11 @@ Message Areas are topic specific containers for messages that live within a part | `desc` | :+1: | Friendly area description. | | `sort` | :-1: | Set to a number to override the default alpha-numeric sort order based on the `name` field. | | `default` | :-1: | Specify `true` to make this the default area (e.g. assigned to new users) | +| `acs` | :-1: | A standard [ACS](/docs/configuration/acs.md) block. See **ACS** below. | + +### ACS +An optional standard [ACS](/docs/configuration/acs.md) block can be supplied with the following rules: +* `read`: ACS require to read (see) this conference. Defaults to `GM[users]`. ### Example @@ -54,7 +67,10 @@ messageConferences: { name: ENiGMA 1/2 Development desc: ENiGMA 1/2 discussion! sort: 1 - default: true + default: true + acs: { + read: GM[users] // default + } } } } From 167916e8dd47070e4c79051ea3a1f194ce6afb85 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 02:20:50 -0700 Subject: [PATCH 429/569] Fix bug in findByFullPath() --- core/file_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_entry.js b/core/file_entry.js index bb7a4e12..9ba1c692 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -401,7 +401,7 @@ module.exports = class FileEntry { // Checkums may have changed and are not validated here. static findByFullPath(fullPath, cb) { // first, basic by-filename lookup. - FileEntry.findByFileNameWildcard(paths.basename(fuillPath), (err, entries) => { + FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => { if(err) { return cb(err); } From 704c242aa442f220a3a95deb49e95cdb98ae0c97 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 02:32:20 -0700 Subject: [PATCH 430/569] Fix bug in newScanMessageArea() --- core/new_scan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/new_scan.js b/core/new_scan.js index 55cbedc3..ac741ab3 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -113,7 +113,7 @@ exports.getModule = class NewScanModule extends MenuModule { // :TODO: it would be nice to cache this - must be done by conf! const omitMessageAreaTags = valueAsArray(_.get(this, 'menuConfig.config.omitMessageAreaTags', [])); const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).filter(area => { - return area => !omitMessageAreaTags.includes(area.areaTag); + return !omitMessageAreaTags.includes(area.areaTag); }); const currentArea = sortedAreas[this.currentScanAux.area]; From 844286ea1ce1d4ded8728eda085c5007821da476 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 02:32:41 -0700 Subject: [PATCH 431/569] Use constants --- core/menu_stack.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/core/menu_stack.js b/core/menu_stack.js index 3e9c455b..080d0efa 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -3,7 +3,10 @@ // ENiGMA½ const loadMenu = require('./menu_util.js').loadMenu; -const Errors = require('./enig_error.js').Errors; +const { + Errors, + ErrorReasons +} = require('./enig_error.js'); // deps const _ = require('lodash'); @@ -43,26 +46,23 @@ module.exports = class MenuStack { get currentModule() { const top = this.top(); - if(top) { - return top.instance; - } + assert(top, 'Empty menu stack!'); + return top.instance; } next(cb) { const currentModuleInfo = this.top(); - assert(currentModuleInfo, 'Empty menu stack!'); - - const menuConfig = currentModuleInfo.instance.menuConfig; - const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); + const menuConfig = currentModuleInfo.instance.menuConfig; + const nextMenu = this.client.acs.getConditionalValue(menuConfig.next, 'next'); if(!nextMenu) { return cb(Array.isArray(menuConfig.next) ? - Errors.MenuStack('No matching condition for "next"', 'NOCONDMATCH') : - Errors.MenuStack('Invalid or missing "next" member in menu config', 'BADNEXT') + Errors.MenuStack('No matching condition for "next"', ErrorReasons.NoConditionMatch) : + Errors.MenuStack('Invalid or missing "next" member in menu config', ErrorReasons.InvalidNextMenu) ); } if(nextMenu === currentModuleInfo.name) { - return cb(Errors.MenuStack('Menu config "next" specifies current menu', 'ALREADYTHERE')); + return cb(Errors.MenuStack('Menu config "next" specifies current menu', ErrorReasons.AlreadyThere)); } this.goto(nextMenu, { }, cb); @@ -86,7 +86,7 @@ module.exports = class MenuStack { return this.goto(previousModuleInfo.name, opts, cb); } - return cb(Errors.MenuStack('No previous menu available', 'NOPREV')); + return cb(Errors.MenuStack('No previous menu available', ErrorReasons.NoPreviousMenu)); } goto(name, options, cb) { @@ -102,7 +102,7 @@ module.exports = class MenuStack { if(currentModuleInfo && name === currentModuleInfo.name) { if(cb) { - cb(Errors.MenuStack('Already at supplied menu', 'ALREADYTHERE')); + cb(Errors.MenuStack('Already at supplied menu', ErrorReasons.AlreadyThere)); } return; } From a70d865d74c024db282a4cb88f2518fde4a2cfc1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 02:33:48 -0700 Subject: [PATCH 432/569] Code tidy --- core/client.js | 10 +++++++--- core/file_base_download_manager.js | 1 - core/file_base_web_download_manager.js | 1 - core/misc_util.js | 3 +-- core/nua.js | 1 - 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/client.js b/core/client.js index 6f4aec79..60a04af7 100644 --- a/core/client.js +++ b/core/client.js @@ -54,6 +54,7 @@ exports.Client = Client; // Resources & Standards: // * http://www.ansi-bbs.org/ansi-bbs-core-server.html // +/* eslint-disable no-control-regex */ const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; @@ -63,6 +64,7 @@ const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?: '(?:M([@ #!a`])(.)(.))', // mouse stuff '(?:1;)?(\\d+)?([a-zA-Z@])' ].join('|') + ')'); +/* eslint-enable no-control-regex */ const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); const RE_ESC_CODE_ANYWHERE = new RegExp( [ @@ -70,7 +72,7 @@ const RE_ESC_CODE_ANYWHERE = new RegExp( [ RE_META_KEYCODE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source, RE_DEV_ATTR_RESPONSE_ANYWHERE.source, - /\u001b./.source + /\u001b./.source // eslint-disable-line no-control-regex ].join('|')); @@ -158,15 +160,17 @@ function Client(/*input, output*/) { return termClient; }; + /* eslint-disable no-control-regex */ this.isMouseInput = function(data) { - return /\x1b\[M/.test(data) || // eslint-disable-line no-control-regex - /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || // eslint-disable-line no-control-regex + return /\x1b\[M/.test(data) || + /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || /\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || /\u001b\[(O|I)/.test(data); }; + /* eslint-enable no-control-regex */ this.getKeyComponentsFromCode = function(code) { return { diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index 50abd6da..8487697f 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -8,7 +8,6 @@ const DownloadQueue = require('./download_queue.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); const Errors = require('./enig_error.js').Errors; -const stringFormat = require('./string_format.js'); const FileAreaWeb = require('./file_area_web.js'); // deps diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index 62ee02eb..cf509cd9 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -8,7 +8,6 @@ const DownloadQueue = require('./download_queue.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); const Errors = require('./enig_error.js').Errors; -const stringFormat = require('./string_format.js'); const FileAreaWeb = require('./file_area_web.js'); const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; const Config = require('./config.js').get; diff --git a/core/misc_util.js b/core/misc_util.js index 62a3967d..78c76719 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -1,10 +1,9 @@ /* jslint node: true */ 'use strict'; +// deps const paths = require('path'); - const os = require('os'); -const moment = require('moment'); const packageJson = require('../package.json'); diff --git a/core/nua.js b/core/nua.js index 42c63895..2cb4c26b 100644 --- a/core/nua.js +++ b/core/nua.js @@ -15,7 +15,6 @@ const UserProps = require('./user_property.js'); // deps const _ = require('lodash'); -const moment = require('moment'); exports.moduleInfo = { name : 'NUA', From a036f6c6bf5e730662ca415a96a4b18fe8538da6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 02:35:34 -0700 Subject: [PATCH 433/569] Minor code tidy --- core/spinner_menu_view.js | 2 +- core/view_controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/spinner_menu_view.js b/core/spinner_menu_view.js index f4517807..9547c9b8 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -62,7 +62,7 @@ function SpinnerMenuView(options) { text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; this.client.term.write(`${ansi.goto(this.position.row, this.position.col)}${text}`); this.setRenderCacheItem(index, text, this.hasFocus); - } + }; } util.inherits(SpinnerMenuView, MenuView); diff --git a/core/view_controller.js b/core/view_controller.js index de6f1f05..84f33756 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -433,7 +433,7 @@ ViewController.prototype.getView = function(id) { ViewController.prototype.hasView = function(id) { return this.getView(id) ? true : false; -} +}; ViewController.prototype.getViewsByMciCode = function(mciCode) { if(!Array.isArray(mciCode)) { From 008cc00742d92162bc8e3d8fdcba282f8ffd659e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 14:47:37 -0700 Subject: [PATCH 434/569] Some cleanup/updates --- README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c2056a33..230878ed 100644 --- a/README.md +++ b/README.md @@ -12,29 +12,26 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * [MCI support](docs/art/mci.md) for lightbars, toggles, input areas, and so on plus many other other bells and whistles * 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 - * [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. * Full [SAUCE](http://www.acid.org/info/sauce/sauce.htm) support - * Renegade style pipe color codes - * [SQLite](http://sqlite.org/) storage of users, message areas, and so on - * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption + * Renegade style [pipe color codes](/docs/configuration/colour-codes.md). + * [SQLite](http://sqlite.org/) storage of users, message areas, etc. + * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption. * [Door support](docs/modding/door-servers.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) support! - * [Bunyan](https://github.com/trentm/node-bunyan) logging + * [Bunyan](https://github.com/trentm/node-bunyan) logging! * [Message networks](docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be set to read-only viewable using a built in Gopher server! * [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/servers/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! - * ANSI support in the Full Screen Editor (FSE), file descriptions, and so on + * ANSI support in the Full Screen Editor (FSE), file descriptions, etc. ## Documentation -[Browse the docs online](https://nuskooler.github.io/enigma-bbs/) +[Browse the docs online](https://nuskooler.github.io/enigma-bbs/). Be sure to checkout the [/docs/](/docs/) folder as well for the latest and greatest documentation. ## In the Works -* More ACS support coverage -* SysOp dashboard (ye ol' WFC) -* Native DOS emulation -* A lot more! Feel free to request features via [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) +Many more features are in the pipeline. Checkout the [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) and feel free to request features (or contribute!) features. ## Known Issues -As of now this is considered **alpha** code! Please **expect bugs** :bug: -- and when you find them, log issues and/or submit pull requests. Feature requests, suggestions, and so on are always welcome! I am also **looking for semi dedicated testers, artists, etc**! +As of now this is considered **alpha** code! Please **expect bugs** :bug: -- and when you find them, log issues and/or submit pull requests. With that said, the code is actually quite stable and is used by a number of boards. See [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) for more information. @@ -62,11 +59,12 @@ ENiGMA has been tested with many terminals. However, the following are suggested ## Installation +On *nix type systems: ``` curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh | bash ``` -Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/installation/installation-methods.html) in the docs for more information. +Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/installation/installation-methods.html) for Windows, Docker, and so on... ## Special Thanks * [Dave Stephens aka RiPuk](https://github.com/davestephens) for the awesome [ENiGMA website](https://enigma-bbs.github.io/) and [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc. From ebc129b17bb4743765f5ed44a633a9a3e8eb5654 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 14:47:51 -0700 Subject: [PATCH 435/569] Update to reflect realitiy --- art/themes/luciano_blocktronics/MSGVHLP.ANS | Bin 1098 -> 1023 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/MSGVHLP.ANS b/art/themes/luciano_blocktronics/MSGVHLP.ANS index 0320614d7a30c83170ba7829d117b6ac86d94bb9..6c79cbe2199177aa04d7b065fb64d76bfed9468b 100644 GIT binary patch delta 252 zcmX@b@t<8lI@-Y6#K79vJeNy4+SuHAqP&ETv6-`Uw4t?$xwWxD?tPGO?w$L0?%$S< zHZjP(4OC~4D;;fQ0ai4z;3+SNYYyfC)o%`G6l9z%!t6A8DU+0_5m4CJ45Zu;#4-o6 z3@s>6*OEUA)!HR%tOeT9UD{+=&WELxAq~;V%KEWKrIf0phfsrwQL1?lri!1=r CjYi1; delta 282 zcmey*eu_h0I@-Y6#K79vJeNy4+R)NjI@;JE7s#>zvCMNP3P|vQMGZlM=0L%T`f|K* z5s>7i;EM}Qx$SDi%TXSU=*J` zlSxVoqAUwZSypLofkH{X0#rHFJR`%&Czy05M=&`~PGA;M&q&QF0J;|FJh)Xr7Tl_W z{NfUYywvi^RZQ}Xg_9GRg;@$qGfO7dGRLUhVrF1qWDH;sWB>zUPbXi6Fn31?4^9FA DGXO{g From 8549fa7ac43eaee0eb1d2532f3fde94ed8466998 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 9 Dec 2018 20:06:34 -0700 Subject: [PATCH 436/569] Typos/etc. --- WHATSNEW.md | 4 ++-- docs/modding/local-doors.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 191f1c7b..f78bf6c3 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -15,8 +15,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * The old concept of `autoScale` has been removed. See https://github.com/NuSkooler/enigma-bbs/issues/166 * Ability to delete from personal mailbox (finally!) * Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson -* `{userName}` (sanatized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door. -* Any module may now register for a system startup intiialization via the `initializeModules(initInfo, cb)` export. +* `{userName}` (sanitized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door. +* Any module may now register for a system startup initialization via the `initializeModules(initInfo, cb)` export. * User event log is now functional. Various events a user performs will be persisted to the `system.db` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`! * New MCI codes including general purpose movement codes. See [MCI codes](docs/art/mci.md) * `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected. diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index 06b80ac1..4ab8037b 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -41,7 +41,7 @@ The following variables may be used in `args` entries: | `{dropFile}` | Dropfile _filename_ only. | `DOOR.SYS` | | `{dropFilePath}` | Full path to generated dropfile. The system places dropfiles in the path set by `paths.dropFiles` in `config.hjson`. | `C:\enigma-bbs\drop\node1\DOOR.SYS` | | `{userId}` | Current user ID. | `420` | -| `{userName}` | _Sanitized_ username. Safe for filenames, etc. | `izard` | +| `{userName}` | [Sanitized](https://www.npmjs.com/package/sanitize-filename) username. Safe for filenames, etc. If the full username is sanitized away, this will resolve to something like "user_1234". | `izard` | | `{userNameRaw}` | _Raw_ username. May not be safe for filenames! | `\/\/izard` | | `{srvPort}` | Temporary server port when `io` is set to `socket`. | `1234` | | `{cwd}` | Current Working Directory. | `/home/enigma-bbs/doors/foo/` | From cde329b4390caf985325e1eb66175e7d6aad4b3c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 10 Dec 2018 21:54:59 -0700 Subject: [PATCH 437/569] Spelling --- core/file_base_area.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/file_base_area.js b/core/file_base_area.js index 293ab265..95040791 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -239,7 +239,7 @@ function getExistingFileEntriesBySha256(sha256, cb) { ); } -// :TODO: This is bascially sliceAtEOF() from art.js .... DRY! +// :TODO: This is basically sliceAtEOF() from art.js .... DRY! function sliceAtSauceMarker(data) { let eof = data.length; const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) From 23d70773e2a37ceee95d3b644a1f069212d158f4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 10 Dec 2018 22:29:28 -0700 Subject: [PATCH 438/569] Some fixes / clarifications --- docs/installation/install-script.md | 4 ++-- docs/installation/manual.md | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/installation/install-script.md b/docs/installation/install-script.md index d0ce2a89..6481360e 100644 --- a/docs/installation/install-script.md +++ b/docs/installation/install-script.md @@ -6,10 +6,10 @@ Under most Linux/UNIX like environments (Linux, BSD, OS X, ...) new users can s `install.sh` script to get everything up and running. Cut + paste the following into your terminal: ``` -curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash +curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh | bash ``` -It is recommended you review the [installation script](https://github.com/NuSkooler/enigma-bbs/blob/master/misc/install.sh) +It is recommended you review the [installation script](https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh) on GitHub before running it. The script will install nvm, Node.js 6 and grab the latest ENiGMA BBS from GitHub. It will also guide you diff --git a/docs/installation/manual.md b/docs/installation/manual.md index a24cd3eb..0a8a8f1d 100644 --- a/docs/installation/manual.md +++ b/docs/installation/manual.md @@ -20,11 +20,12 @@ are OK) for Windows users. Note that you **should only need the Visual C++ compo ## Node.js ### With NVM -Node Version Manager (NVM) is an excellent way to install and manage Node.js versions on most UNIX-like environments. [Get the latest version here](https://github.com/creationix/nvm). The install should look something like this: +Node Version Manager (NVM) is an excellent way to install and manage Node.js versions on most UNIX-like environments. [Get the latest version here](https://github.com/creationix/nvm). The nvm install may look _something_ like this: ```bash curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash ``` +:information_source: Do not cut+paste the above command! Visit the [NVM](https://github.com/creationix/nvm) page and run the latest version! Next, install Node.js with NVM: ```bash From 772022f0d00642458dc2c93409eecb2dcca8354f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Dec 2018 22:21:57 -0700 Subject: [PATCH 439/569] + NNTP Content Server * Read-only to public conf/areas only for now * Missing some protocol support * Could use better encoding practices and ANSI prep --- core/config.js | 27 ++ core/exodus.js | 2 +- core/message.js | 14 +- core/servers/content/nntp.js | 769 ++++++++++++++++++++++++++++++++++ misc/config_template.in.hjson | 33 ++ package.json | 4 +- 6 files changed, 840 insertions(+), 9 deletions(-) create mode 100644 core/servers/content/nntp.js diff --git a/core/config.js b/core/config.js index 345f4c09..f75be3de 100644 --- a/core/config.js +++ b/core/config.js @@ -404,6 +404,33 @@ function getDefaultConfig() { // Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ] // to export message confs/areas // + }, + + nntp : { + // internal caching of groups, message lists, etc. + cache : { + maxItems : 200, + maxAge : 1000 * 30, // 30s + }, + + // + // Set publicMessageConferences{} to a map of confTag -> [ areaTag1, areaTag2, ... ] + // in order to export *public* conf/areas that are available to anonymous + // NNTP users. Other conf/areas: Standard ACS rules apply. + // + publicMessageConferences: {}, + + nntp : { + enabled : false, + port : 8119, + }, + + nntps : { + enabled : false, + port : 8563, + certPem : paths.join(__dirname, './../config/nntps_cert.pem'), + keyPem : paths.join(__dirname, './../config/nntps_key.pem'), + } } }, diff --git a/core/exodus.js b/core/exodus.js index e25b4973..0d439392 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -17,7 +17,7 @@ const crypto = require('crypto'); const moment = require('moment'); const https = require('https'); const querystring = require('querystring'); -const fs = require('fs'); +const fs = require('fs-extra'); const SSHClient = require('ssh2').Client; /* diff --git a/core/message.js b/core/message.js index df0804d5..34c590bb 100644 --- a/core/message.js +++ b/core/message.js @@ -238,7 +238,7 @@ module.exports = class Message { filter.ids - use with resultType='uuid' filter.toUserName filter.fromUserName - filter.replyToMesageId + filter.replyToMessageId filter.newerThanTimestamp - may not be used with |date| filter.date - moment object - may not be used with |newerThanTimestamp| @@ -253,7 +253,7 @@ module.exports = class Message { filter.order = ascending | (descending) filter.limit - filter.resultType = (id) | uuid | count + filter.resultType = (id) | uuid | count | messageList filter.extraFields = [] filter.privateTagUserId = - if set, only private messages belonging to are processed @@ -529,22 +529,22 @@ module.exports = class Message { }); } - // :TODO: this should only take a UUID... - load(options, cb) { - assert(_.isString(options.uuid)); + load(loadWith, cb) { + assert(_.isString(loadWith.uuid) || _.isNumber(loadWith.messageId)); const self = this; async.series( [ function loadMessage(callback) { + const whereField = loadWith.uuid ? 'message_uuid' : 'message_id'; msgDb.get( `SELECT message_id, area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp, view_count FROM message - WHERE message_uuid=? + WHERE ${whereField} = ? LIMIT 1;`, - [ options.uuid ], + [ loadWith.uuid || loadWith.messageId ], (err, msgRow) => { if(err) { return callback(err); diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js new file mode 100644 index 00000000..cf2a43b2 --- /dev/null +++ b/core/servers/content/nntp.js @@ -0,0 +1,769 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { + getMessageAreaByTag, + getMessageConferenceByTag, + getMessageListForArea, +} = require('../../message_area.js'); +const User = require('../../user.js'); +const Errors = require('../../enig_error.js').Errors; +const Message = require('../../message.js'); +const FTNAddress = require('../../ftn_address.js'); +const { + isAnsi, + cleanControlCodes, + splitTextAtTerms, +} = require('../../string_util.js'); +const AnsiPrep = require('../../ansi_prep.js'); + +// deps +const NNTPServerBase = require('nntp-server'); +const _ = require('lodash'); +const fs = require('fs-extra'); +const asyncReduce = require('async/reduce'); +const asyncMap = require('async/map'); +const asyncSeries = require('async/series'); +const LRU = require('lru-cache'); +const iconv = require('iconv-lite'); + +// +// Network News Transfer Protocol (NNTP) +// +// RFCS +// - https://www.w3.org/Protocols/rfc977/rfc977 +// - https://tools.ietf.org/html/rfc3977 +// - https://tools.ietf.org/html/rfc2980 +// - https://tools.ietf.org/html/rfc5536 + +// +exports.moduleInfo = { + name : 'NNTP', + desc : 'Network News Transfer Protocol (NNTP) Server', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.nntp.server', +}; + +/* + General TODO + - ACS checks need worked out. Currently ACS relies on |client|. We need a client + spec that can be created even without a login server. Some checks and simply + return false/fail. +*/ + + +class NNTPServer extends NNTPServerBase { + constructor(options, serverName) { + super(options); + + this.log = Log.child( { server : serverName } ); + + const config = Config(); + this.groupCache = new LRU({ + max : _.get(config, 'contentServers.nntp.cache.maxItems', 200), + maxAge : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s + }); + } + + _needAuth(session, command) { + return super._needAuth(session, command); + } + + _authenticate(session) { + const username = session.authinfo_user; + const password = session.authinfo_pass; + + this.log.trace( { username }, 'Authentication request'); + + return new Promise( resolve => { + const user = new User(); + user.authenticate(username, password, err => { + if(err) { + this.log.debug( { username, reason : err.message }, 'Authentication failure'); + return resolve(false); + } + + session.authUser = user; + + this.log.debug( { username }, 'User authenticated successfully'); + return resolve(true); + }); + }); + } + + getMessageListIndexByMessageID(id, session) { + return id - _.get(session.groupInfo.messageList, [ 0, 'messageId' ]); + } + + isGroupSelected(session) { + return Array.isArray(_.get(session, 'groupInfo.messageList')); + } + + getJAMStyleFrom(message, fromName) { + // + // Try to to create a (JamNTTPd) JAM style "From" field: + // + // - If we're dealing with a FTN address, create an email-like format + // but do not include ':' or '/' characters as it may cause clients + // to puke. FTN addresses are formatted how JamNTTPd does it for + // some sort of compliance. We also extend up to 5D addressing. + // - If we have an email address, then it's ready to go. + // + const remoteFrom = _.get(message.meta, [ 'System', Message.SystemMetaNames.RemoteFromUser ]); + let jamStyleFrom; + if(remoteFrom) { + const flavor = _.get(message.meta, [ 'System', Message.SystemMetaNames.ExternalFlavor ]); + switch(flavor) { + case [ Message.AddressFlavor.FTN ] : + { + let ftnAddr = FTNAddress.fromString(remoteFrom); + if(ftnAddr && ftnAddr.isValid()) { + // In general, addresses are in point, node, net, zone, domain order + if(ftnAddr.domain) { // 5D + // point.node.net.zone@domain or node.net.zone@domain + jamStyleFrom = `${ftnAddr.node}.${ftnAddr.net}.${ftnAddr.zone}@${ftnAddr.domain}`; + if(ftnAddr.point) { + jamStyleFrom = `${ftnAddr.point}.` + jamStyleFrom; + } + } else { + if(ftnAddr.point) { + jamStyleFrom = `${ftnAddr.point}@${ftnAddr.node}.${ftnAddr.net}.${ftnAddr.zone}`; + } else { + jamStyleFrom = `0@${ftnAddr.node}.${ftnAddr.net}.${ftnAddr.zone}`; + } + } + } + } + break; + + case [ Message.AddressFlavor.Email ] : + jamStyleFrom = `${fromName} <${remoteFrom}>`; + break; + } + } + + if(!jamStyleFrom) { + jamStyleFrom = fromName; + } + + return jamStyleFrom; + } + + populateNNTPHeaders(session, message, cb) { + // + // Build compliant headers + // + // Resources: + // - https://tools.ietf.org/html/rfc5536#section-3.1 + // - https://github.com/ftnapps/jamnntpd/blob/master/src/nntpserv.c#L962 + // + const toName = this.getMessageTo(message); + const fromName = this.getMessageFrom(message); + + message.nntpHeaders = { + From : this.getJAMStyleFrom(message, fromName), + 'X-Comment-To' : toName, + Newsgroups : session.group.name, + Subject : message.subject, + Date : this.getMessageDate(message), + 'Message-ID' : this.getMessageIdentifier(message), + Path : 'ENiGMA1/2!not-for-mail', + 'Content-Type' : 'text/plain; charset=utf-8', + }; + + const externalFlavor = _.get(message.meta.System, [ Message.SystemMetaNames.ExternalFlavor ]); + if(externalFlavor) { + message.nntpHeaders['X-ENiG-MessageFlavor'] = externalFlavor; + } + + // Any FTN properties -> X-FTN-* + _.each(message.meta.FtnProperty, (v, k) => { + const suffix = { + [ Message.FtnPropertyNames.FtnTearLine ] : 'Tearline', + [ Message.FtnPropertyNames.FtnOrigin ] : 'Origin', + [ Message.FtnPropertyNames.FtnArea ] : 'AREA', + [ Message.FtnPropertyNames.FtnSeenBy ] : 'SEEN-BY', + }[k]; + + if(suffix) { + // some special treatment. + if('Tearline' === suffix) { + v = v.replace(/^--- /, ''); + } else if('Origin' === suffix) { + v = v.replace(/^[ ]{1,2}\* Origin: /, ''); + } + if(Array.isArray(v)) { // ie: SEEN-BY[] -> one big list + v = v.join(' '); + } + message.nntpHeaders[`X-FTN-${suffix}`] = v.trim(); + } + }); + + // Other FTN kludges + _.each(message.meta.FtnKludge, (v, k) => { + if(Array.isArray(v)) { + v = v.join(' '); // same as above + } + message.nntpHeaders[`X-FTN-${k.toUpperCase()}`] = v.toString().trim(); + }); + + // + // Set X-FTN-To and X-FTN-From: + // - If remote to/from : joeuser + // - Without remote : joeuser + // + const remoteFrom = _.get(message.meta, [ 'System', Message.SystemMetaNames.RemoteFromUser ]); + message.nntpHeaders['X-FTN-From'] = remoteFrom ? `${fromName} <${remoteFrom}>` : fromName; + const remoteTo = _.get(message.meta [ 'System', Message.SystemMetaNames.RemoteToUser ]); + message.nntpHeaders['X-FTN-To'] = remoteTo ? `${toName} <${remoteTo}>` : toName; + + if(!message.replyToMsgId) { + return cb(null); + } + + // replyToMessageId -> Message-ID formatted ID + const filter = { + resultType : 'uuid', + ids : [ parseInt(message.replyToMsgId) ], + limit : 1, + }; + Message.findMessages(filter, (err, uuids) => { + if(!err && Array.isArray(uuids)) { + message.nntpHeaders.References = this.makeMessageIdentifier(message.replyToMsgId, uuids[0]); + } + return cb(null); + }); + } + + getMessageUUIDFromMessageID(session, messageId) { + let messageUuid; + + // Direct ID request + if((_.isString(messageId) && '<' !== messageId.charAt(0)) || _.isNumber(messageId)) { + // group must be in session + if(!this.isGroupSelected(session)) { + return null; + } + + messageId = parseInt(messageId); + if(isNaN(messageId)) { + return null; + } + + // + // Adjust to offset in message list & get UUID + // This works since we create "pseudo IDs" to return to NNTP + // by using firstRealID + index. A find on |index| member would + // also work, but would be O(n). + // + const mlIndex = this.getMessageListIndexByMessageID(messageId, session); + messageUuid = _.get(session.groupInfo.messageList, [ mlIndex, 'messageUuid']); + } else { + // request + [ , messageUuid ] = this.getMessageIdentifierParts(messageId); + } + + if(!_.isString(messageUuid)) { + return null; + } + + return messageUuid; + } + + _getArticle(session, messageId) { + return new Promise( resolve => { + this.log.trace( { messageId }, 'Get article request'); + + const messageUuid = this.getMessageUUIDFromMessageID(session, messageId); + if(!messageUuid) { + this.log.debug( { messageId }, 'Unable to retrieve message UUID for article request'); + return resolve(null); + } + + const message = new Message(); + asyncSeries( + [ + (callback) => { + return message.load( { uuid : messageUuid }, callback); + }, + (callback) => { + // :TODO: Must validate access! See Gopher, etc. !!!!! + // :TODO: we can only do this if a style was sent in, not a direct ID ?????? + if(session.groupInfo.areaTag !== message.areaTag) { + return resolve(null); + } + + if(!this.hasConfAndAreaReadAccess(session, session.groupInfo.confTag, session.groupInfo.areaTag)) { + this.log.info(`Access denied to message ${messageUuid}`); + return resolve(null); + } + + return callback(null); + }, + (callback) => { + return this.populateNNTPHeaders(session, message, callback); + }, + (callback) => { + return this.prepareMessageBody(message, callback); + } + ], + err => { + if(err) { + this.log.error( { error : err.message, messageId }, 'Failed to load article'); + return resolve(null); + } + return resolve(message); + } + ); + }); + } + + _getRange(session, first, last, options) { + return new Promise(resolve => { + // + // Build an array of message objects that can later + // be used with the various _build* methods. + // + // Messages must belong to the range of *pseudo IDs* + // aka |index|. + // + // :TODO: Handle |options| + if(!this.isGroupSelected(session)) { + return resolve(null); + } + + const uuids = session.groupInfo.messageList.filter(m => { + if(m.areaTag !== session.groupInfo.areaTag) { + return false; + } + if(m.index < first || m.index > last) { + return false; + } + return true; + }).map(m => { + return { uuid : m.messageUuid, index : m.index } + }); + + asyncMap(uuids, (msgInfo, nextMessageUuid) => { + const message = new Message(); + message.load( { uuid : msgInfo.uuid }, err => { + if(err) { + return nextMessageUuid(err); + } + + message.index = msgInfo.index; + + this.populateNNTPHeaders(session, message, () => { + this.prepareMessageBody(message, () => { + return nextMessageUuid(null, message); + }); + }); + }); + }, + (err, messages) => { + return resolve(err ? null : messages); + }); + }); + } + + _selectGroup (session, groupName) { + this.log.trace( { groupName }, 'Select group request'); + + return new Promise( resolve => { + this.getGroup(session, groupName, (err, group) => { + if(err) { + return resolve(false); + } + + session.group = Object.assign( + {}, // start clean + { + description : group.friendlyDesc || group.friendlyName, + current_article : group.nntp.total ? group.nntp.min_index : 0, + }, + group.nntp + ); + + session.groupInfo = group; // full set of info + + return resolve(true); + }); + }); + } + + _getGroups(session, time, wildmat) { + this.log.trace( { time, wildmat }, 'Get groups request'); + + // :TODO: handle time - probably use as caching mechanism - must consider user/auth/rights + // :TODO: handle |time| if possible. + return new Promise( (resolve, reject) => { + const config = Config(); + + // :TODO: merge confs avail to authenticated user + const publicConfs = _.get(config, 'contentServers.nntp.publicMessageConferences', {}); + + asyncReduce(Object.keys(publicConfs), [], (groups, confTag, nextConfTag) => { + const areaTags = publicConfs[confTag]; + // :TODO: merge area tags available to authenticated user + asyncMap(areaTags, (areaTag, nextAreaTag) => { + const groupName = this.getGroupName(confTag, areaTag); + + // filter on |wildmat| if supplied. We will remove + // empty areas below in the final results. + if(wildmat && !wildmat.test(groupName)) { + return nextAreaTag(null, null); + } + + this.getGroup(session, groupName, (err, group) => { + if(err) { + return nextAreaTag(null, null); // try others + } + return nextAreaTag(null, group.nntp); + }); + }, + (err, areas) => { + if(err) { + return nextConfTag(err); + } + + areas = areas.filter(a => a && Object.keys(a).length > 0); // remove empty + groups.push(...areas); + + return nextConfTag(null, groups); + }); + }, + (err, groups) => { + if(err) { + return reject(err); + } + return resolve(groups); + }); + }); + } + + isConfAndAreaPubliclyExposed(confTag, areaTag) { + const publicAreaTags = _.get(Config(), [ 'contentServers', 'nntp', 'publicMessageConferences', confTag ] ); + return Array.isArray(publicAreaTags) && publicAreaTags.includes(areaTag); + } + + hasConfAndAreaReadAccess(session, confTag, areaTag) { + if(Message.isPrivateAreaTag(areaTag)) { + return false; + } + + if(this.isConfAndAreaPubliclyExposed(confTag, areaTag)) { + return true; + } + + // further checks require an authenticated user & ACS + if(!session || !session.authUser) { + return false; + } + + const conf = getMessageConferenceByTag(confTag); + if(!conf) { + return false; + } + // :TODO: validate ACS + + const area = getMessageAreaByTag(areaTag, confTag); + if(!area) { + return false; + } + // :TODO: validate ACS + + return false; + } + + getGroup(session, groupName, cb) { + let group = this.groupCache.get(groupName); + if(group) { + return cb(null, group); + } + + const [ confTag, areaTag ] = groupName.split('.'); + if(!confTag || !areaTag) { + return cb(Errors.UnexpectedState(`Invalid NNTP group name: ${groupName}`)); + } + + if(!this.hasConfAndAreaReadAccess(session, confTag, areaTag)) { + return cb(Errors.AccessDenied(`No access to conference ${confTag} and/or area ${areaTag}`)); + } + + const area = getMessageAreaByTag(areaTag, confTag); + if(!area) { + return cb(Errors.DoesNotExist(`No area for areaTag "${areaTag}" / confTag "${confTag}"`)); + } + + getMessageListForArea(null, areaTag, (err, messageList) => { + if(err) { + return cb(err); + } + + if(0 === messageList.length) { + // + // Handle empty group + // See https://tools.ietf.org/html/rfc3977#section-6.1.1.2 + // + return cb(null, { + messageList : [], + confTag, + areaTag, + friendlyName : area.name, + friendlyDesc : area.desc, + nntp : { + name : groupName, + min_index : 0, + max_index : 0, + total : 0, + } + }); + } + + const firstMsg = messageList[0]; + + // node-nntp wants "index" + let index = firstMsg.messageId; + messageList.forEach(m => { + m.index = index; + ++index; + }); + + group = { + messageList, + confTag, + areaTag, + friendlyName : area.name, + friendlyDesc : area.desc, + nntp : { + name : groupName, + min_index : firstMsg.messageId, + max_index : firstMsg.messageId + messageList.length - 1, + total : messageList.length, + }, + }; + + this.groupCache.set(groupName, group); + + return cb(null, group); + }); + } + + _buildHead(session, message) { + return _.map(message.nntpHeaders, (v, k) => `${k}: ${v}`).join('\r\n'); + } + + _buildBody(session, message) { + return message.preparedBody; + } + + _buildHeaderField(session, message, field) { + const body = message.preparedBody || message.message; + const value = { + ':bytes' : Buffer.byteLength(body).toString(), + ':lines' : splitTextAtTerms(body).length.toString(), + }[field] || _.find(message.nntpHeaders, (v, k) => { + return k.toLowerCase() === field; + }); + + if(!value) { + this.log.debug(`No value for requested header field "${field}"`); + } + + return value; + } + + _getOverviewFmt(session) { + return super._getOverviewFmt(session); + } + + _getNewNews(session, time, wildmat) { + throw new Error('method `nntp._getNewNews` is not implemented'); + } + + getMessageDate(message) { + // https://tools.ietf.org/html/rfc5536#section-3.1.1 -> https://tools.ietf.org/html/rfc5322#section-3.3 + return message.modTimestamp.format('ddd, D MMM YYYY HH:mm:ss ZZ'); + } + + makeMessageIdentifier(messageId, messageUuid) { + // + // Spec : RFC-5536 Section 3.1.3 @ https://tools.ietf.org/html/rfc5536#section-3.1.3 + // Example : <2456.0f6587f7-5512-4d03-8740-4d592190145a@enigma-bbs> + // + return `<${messageId}.${messageUuid}@enigma-bbs>`; + } + + getMessageIdentifier(message) { + return this.makeMessageIdentifier(message.messageId, message.messageUuid); + } + + getMessageIdentifierParts(messageId) { + const m = messageId.match(/<([0-9]+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@enigma-bbs>/); + if(m) { + return [ m[1], m[2] ]; + } + return []; + } + + getMessageTo(message) { + // :TODO: same as From -- check config + return message.toUserName; + } + + getMessageFrom(message) { + // :TODO: NNTP config > conf > area config for real names + return message.fromUserName; + } + + prepareMessageBody(message, cb) { + if(isAnsi(message.message)) { + AnsiPrep( + message.message, + { + termWidth : 1000, // unrealistically long; don't want to wrap, really. + cols : 1000, // ...see above. + rows : 'auto', + asciiMode : true, // Export to ASCII + fillLines : false, // Don't fill up columns + }, + (err, prepped) => { + message.preparedBody = prepped || message.message; + return cb(null); + } + ); + } else { + message.preparedBody = cleanControlCodes(message.message, { all : true }); + return cb(null); + } + } + + getGroupName(confTag, areaTag) { + // + // Example: + // input : fsxNet (confTag) fsx_bbs (areaTag) + // output: fsx_net.fsx_bbs + // + // Note also that periods are replaced in conf and area + // tags such that we *only* have a period separator + // between the two for a group name! + // + return `${_.snakeCase(confTag).replace(/\./g, '_')}.${_.snakeCase(areaTag).replace(/\./g, '_')}`; + } +} + +exports.getModule = class NNTPServerModule extends ServerModule { + constructor() { + super(); + } + + isEnabled() { + return this.enableNntp || this.enableNttps; + } + + get enableNntp() { + return _.get(Config(), 'contentServers.nntp.nntp.enabled', false); + } + + get enableNttps() { + return _.get(Config(), 'contentServers.nntp.nntps.enabled', false); + } + + isConfigured() { + const config = Config(); + + // + // Any conf/areas exposed? + // + const publicConfs = _.get(config, 'contentServers.nntp.publicMessageConferences', {}); + const areasExposed = _.some(publicConfs, areas => { + return Array.isArray(areas) && areas.length > 0; + }); + + if(!areasExposed) { + return false; + } + + const nntp = _.get(config, 'contentServers.nntp.nntp'); + if(nntp && this.enableNntp) { + if(isNaN(nntp.port)) { + return false; + } + } + + const nntps = _.get(config, 'contentServers.nntp.nntps'); + if(nntps && this.enableNttps) { + if(isNaN(nntps.port)) { + return false; + } + + if(!_.isString(nntps.certPem) || !_.isString(nntps.keyPem)) { + return false; + } + } + + return true; + } + + createServer() { + if(!this.isEnabled() || !this.isConfigured()) { + return; + } + + const config = Config(); + + const commonOptions = { + //requireAuth : true, // :TODO: re-enable! + // :TODO: override |session| - use our own debug to Bunyan, etc. + }; + + if(this.enableNntp) { + this.nntpServer = new NNTPServer( + // :TODO: according to docs: if connection is non-tls, but behind proxy (assuming TLS termination?!!) then set this to true + Object.assign( { secure : false }, commonOptions), + 'NNTP' + ); + } + + if(this.enableNttps) { + this.nntpsServer = new NNTPServer( + Object.assign( + { + secure : true, + tls : { + cert : fs.readFileSync(config.contentServers.nntp.nntps.certPem), + key : fs.readFileSync(config.contentServers.nntp.nntps.keyPem), + } + }, + commonOptions + ), + 'NTTPS' + ); + } + } + + listen() { + const config = Config(); + [ 'nntp', 'nntps' ].forEach( service => { + const server = this[`${service}Server`]; + if(server) { + const port = config.contentServers.nntp[service].port; + server.listen(this.listenURI(port, service)) + .catch(e => { + Log.warn( { error : e.message, port }, `${service.toUpperCase()} failed to listen`); + }); + } + }); + + // :TODO: listen() needs to be async. I always should have been... + return true; + } + + listenURI(port, service = 'nntp') { + return `${service}://localhost:${port}`; + } +}; diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 489a7cb3..504cd02d 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -222,6 +222,39 @@ // messageConferences: { // some_conf: [ "area_tag1", "area_tag2" ] // } + // + } + + // You may also wish to enable NNTP services + nntp: { + // + // Set publicMessageConferences{} to configure + // publicly exposed conferences & areas. + // + // Example: + // publicMessageConferences: { + // some_conf: [ "area_tag1", "area_tag2" ] + // } + // + publicMessageConferences: {} + + // non-secure + nntp: { + enabled: false + port: XXXXX + } + + // secure (TLS) + nntps: { + enabled: false + port: XXXXX + + // + // You will need a SSL/TLS certificate and key + // + certPem: XXXXX + keyPem: XXXXX + } } } diff --git a/package.json b/package.json index f6425f01..89f99a00 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,9 @@ "uuid-parse": "^1.0.0", "ws": "^6.1.2", "xxhash": "^0.2.4", - "yazl": "^2.5.0" + "yazl": "^2.5.0", + "nntp-server": "^1.0.3", + "lru-cache" : "^5.1.1" }, "devDependencies": {}, "engines": { From 874aee5baa7f7e4b85992b2b10052123a8c093d5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Dec 2018 23:03:10 -0700 Subject: [PATCH 440/569] Change listen addr --- core/servers/content/nntp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index cf2a43b2..6420f1c0 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -764,6 +764,6 @@ exports.getModule = class NNTPServerModule extends ServerModule { } listenURI(port, service = 'nntp') { - return `${service}://localhost:${port}`; + return `${service}://0.0.0.0:${port}`; } }; From b903b2ee82e030f5709069767e5609365a5eb870 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Dec 2018 23:08:53 -0700 Subject: [PATCH 441/569] Better logging --- core/servers/content/nntp.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 6420f1c0..380f9f21 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -313,9 +313,11 @@ class NNTPServer extends NNTPServerBase { ], err => { if(err) { - this.log.error( { error : err.message, messageId }, 'Failed to load article'); + this.log.error( { error : err.message, messageUuid }, 'Failed to load article'); return resolve(null); } + + this.log.info( { messageUuid, messageId, areaTag : message.areaTag }, 'Serving article'); return resolve(message); } ); From a3ba57b0b831333d4ac42fcb96c021aa0c59816a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 14 Dec 2018 23:21:33 -0700 Subject: [PATCH 442/569] Fix schedule issue --- core/scanner_tossers/ftn_bso.js | 8 ++++---- core/servers/login/websocket.js | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 36f0d769..47184ea9 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -2177,8 +2177,8 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { Log.debug( { schedule : this.moduleConfig.schedule.export, - schedOK : -1 === exportSchedule.sched.error, - next : moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), + schedOK : -1 === _.get(exportSchedule, 'sched.error'), + next : exportSchedule.sched ? moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', immediate : exportSchedule.immediate ? true : false, }, 'Export schedule loaded' @@ -2206,8 +2206,8 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { Log.debug( { schedule : this.moduleConfig.schedule.import, - schedOK : -1 === importSchedule.sched.error, - next : moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a'), + schedOK : -1 === _.get(importSchedule, 'sched.error'), + next : importSchedule.sched ? moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', watchFile : _.isString(importSchedule.watchFile) ? importSchedule.watchFile : 'None', }, 'Import schedule loaded' diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index 3fc16343..43245e74 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -203,7 +203,11 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { ws.isConnectionAlive = false; // pong will reset this Log.trace('Ping to remote WebSocket client'); - return ws.ping('', false); // false=don't mask + try { + ws.ping('', false); // false=don't mask + } catch(e) { // don't barf on closing state + /* nothing */ + } }); } }); From dba2fc18f6cb4632c9fa65e70ab211ec8afdacba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 01:55:38 -0700 Subject: [PATCH 443/569] Strip MCI/Pipe codes --- core/servers/content/gopher.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index da09acd9..47fc3c6e 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -2,22 +2,23 @@ 'use strict'; // ENiGMA½ -const Log = require('../../logger.js').log; -const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').get; +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; const { splitTextAtTerms, isAnsi, cleanControlCodes -} = require('../../string_util.js'); +} = require('../../string_util.js'); const { getMessageConferenceByTag, getMessageAreaByTag, getMessageListForArea, -} = require('../../message_area.js'); -const { sortAreasOrConfs } = require('../../conf_area_util.js'); -const AnsiPrep = require('../../ansi_prep.js'); -const { wordWrapText } = require('../../word_wrap.js'); +} = require('../../message_area.js'); +const { sortAreasOrConfs } = require('../../conf_area_util.js'); +const AnsiPrep = require('../../ansi_prep.js'); +const { wordWrapText } = require('../../word_wrap.js'); +const { stripMciColorCodes } = require('../../color_codes.js'); // deps const net = require('net'); @@ -216,9 +217,13 @@ exports.getModule = class GopherModule extends ServerModule { } ); } else { - const prepped = splitTextAtTerms(cleanControlCodes(body, { all : true } ) ) - .map(l => (wordWrapText(l, { width : WordWrapColumn } ).wrapped || []).join('\n')) - .join('\n'); + const cleaned = stripMciColorCodes( + cleanControlCodes(body, { all : true } ) + ); + const prepped = + splitTextAtTerms(cleaned) + .map(l => (wordWrapText(l, { width : WordWrapColumn } ).wrapped || []).join('\n')) + .join('\n'); return cb(prepped); } From 4b2771012b08680cb7adf9c843ab3074540e66cf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 02:06:15 -0700 Subject: [PATCH 444/569] Show desc if set --- core/servers/content/gopher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 47fc3c6e..f5ae9cfa 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -341,7 +341,7 @@ ${msgBody} this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - ...areas.map(area => this.makeItem(ItemTypes.SubMenu, area.name, `/msgarea/${confTag}/${area.areaTag}`)) + ...areas.map(area => this.makeItem(ItemTypes.SubMenu, `${area.name} ${area.desc ? '- ' + area.desc : ''}`, `/msgarea/${confTag}/${area.areaTag}`)) ].join(''); return cb(response); @@ -362,7 +362,7 @@ ${msgBody} this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), this.makeItem(ItemTypes.InfoMessage, ''), - ...confs.map(conf => this.makeItem(ItemTypes.SubMenu, conf.name, `/msgarea/${conf.confTag}`)) + ...confs.map(conf => this.makeItem(ItemTypes.SubMenu, `${conf.name} ${conf.desc ? '- ' + conf.desc : ''}`, `/msgarea/${conf.confTag}`)) ].join(''); return cb(response); From 615edac7cd2396a928054a6a313d3c2ce3ea77fc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 02:39:36 -0700 Subject: [PATCH 445/569] docs/_includes/nav.md Tidy up + add link to updating --- docs/installation/installation-methods.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/installation/installation-methods.md b/docs/installation/installation-methods.md index 99a1db6c..bb55dcce 100644 --- a/docs/installation/installation-methods.md +++ b/docs/installation/installation-methods.md @@ -2,12 +2,15 @@ layout: page title: Installation Methods --- +## Installation Methods There are multiple ways of installing ENiGMA BBS, depending on your level of experience and desire to do things manually versus have it automated for you. -| Method | Operating System Compatibility | Notes | -|----------------------------------------|------------------------------------------------|---------------------------------------------------------------------------------------------| -| [Installation Script](install-script) | Linux, BSD, OSX | Quick and easy installation under most Linux/UNIX like environments (Linux, BSD, OS X, ...) | -| [Docker Images](docker) | Linux, BSD, OSX, Windows | Easy upgrades, compatible with all operating systems, no dependencies to install | -| [Manual Installation](manual) | Linux, Windows (probably others but untested! | If you like doing things manually, or are running Windows | +| Method | Operating System Compatibility | Notes | +|--------|--------------------------------|-------| +| [Installation Script](install-script) | Linux, BSD, OSX | Quick and easy installation under most Linux/UNIX like environments (Linux, BSD, OS X, ...) | +| [Docker Images](docker) | Linux, BSD, OSX, Windows | Easy upgrades, compatible with all operating systems, no dependencies to install | +| [Manual](manual) | Linux, Windows (probably others but untested! | If you like doing things manually, or are running Windows | +## Keeping Up To Date +After installing, you'll want to [keep your system updated](/docs/admin/updating.md). \ No newline at end of file From b89096fd990f09d35edefd4ff77e2b81083e67fa Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 02:39:57 -0700 Subject: [PATCH 446/569] publicPort typo --- core/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config.js b/core/config.js index f75be3de..90e641d6 100644 --- a/core/config.js +++ b/core/config.js @@ -397,7 +397,7 @@ function getDefaultConfig() { enabled : false, port : 8070, publicHostname : 'another-fine-enigma-bbs.org', - publicPort : 8080, // adjust if behind NAT/etc. + publicPort : 8070, // adjust if behind NAT/etc. bannerFile : 'gopher_banner.asc', // From faf076f3e3779a0d2e3cc3b61b6c4b7eb536197e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 02:40:13 -0700 Subject: [PATCH 447/569] Fix case --- docs/_includes/nav.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 0c0cd945..326ebaad 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -13,7 +13,7 @@ - Configuration - [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %}) - [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %}) - - [Editing hjson]({{ site.baseurl }}{% link configuration/editing-hjson.md %}) + - [Editing HJSON]({{ site.baseurl }}{% link configuration/editing-hjson.md %}) - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - [HJSON General]({{ site.baseurl }}{% link configuration/hjson.md %}) - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) From cf6e3d3ba8aa4c9ad9d1e5fcd370f705b7d1e6ea Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 02:40:36 -0700 Subject: [PATCH 448/569] Better logging --- core/servers/content/nntp.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 380f9f21..9963a566 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -276,7 +276,7 @@ class NNTPServer extends NNTPServerBase { _getArticle(session, messageId) { return new Promise( resolve => { - this.log.trace( { messageId }, 'Get article request'); + this.log.trace( { messageId }, 'Get article'); const messageUuid = this.getMessageUUIDFromMessageID(session, messageId); if(!messageUuid) { @@ -291,14 +291,18 @@ class NNTPServer extends NNTPServerBase { return message.load( { uuid : messageUuid }, callback); }, (callback) => { - // :TODO: Must validate access! See Gopher, etc. !!!!! - // :TODO: we can only do this if a style was sent in, not a direct ID ?????? + if(!_.has(session, 'groupInfo.areaTag')) { + // :TODO: if this is needed, how to validate properly? + this.log.warn( { messageUuid, messageId }, 'Get article request without group selection'); + return resolve(null); + } + if(session.groupInfo.areaTag !== message.areaTag) { return resolve(null); } if(!this.hasConfAndAreaReadAccess(session, session.groupInfo.confTag, session.groupInfo.areaTag)) { - this.log.info(`Access denied to message ${messageUuid}`); + this.log.info( { messageUuid, messageId}, 'Access denied for message'); return resolve(null); } @@ -519,6 +523,7 @@ class NNTPServer extends NNTPServerBase { friendlyDesc : area.desc, nntp : { name : groupName, + description : area.desc, min_index : 0, max_index : 0, total : 0, @@ -566,14 +571,15 @@ class NNTPServer extends NNTPServerBase { _buildHeaderField(session, message, field) { const body = message.preparedBody || message.message; const value = { - ':bytes' : Buffer.byteLength(body).toString(), - ':lines' : splitTextAtTerms(body).length.toString(), - }[field] || _.find(message.nntpHeaders, (v, k) => { - return k.toLowerCase() === field; - }); + ':bytes' : Buffer.byteLength(body).toString(), + ':lines' : splitTextAtTerms(body).length.toString(), + }[field] + || _.find(message.nntpHeaders, (v, k) => { + return k.toLowerCase() === field; + }); if(!value) { - this.log.debug(`No value for requested header field "${field}"`); + this.log.trace(`No value for requested header field "${field}"`); } return value; From fd5b50fc08a141e40c3365d8403cade4858d1cbe Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 21:41:58 -0700 Subject: [PATCH 449/569] Various doc updates --- docs/installation/install-script.md | 12 +++--- docs/messageareas/message-networks.md | 57 ++++++++++++++++++++----- docs/troubleshooting/monitoring-logs.md | 30 +++++++++++++ 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/docs/installation/install-script.md b/docs/installation/install-script.md index 6481360e..61930c93 100644 --- a/docs/installation/install-script.md +++ b/docs/installation/install-script.md @@ -2,15 +2,17 @@ layout: page title: Install Script --- -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. Cut + paste the following into your terminal: +## Install Script +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. Cut + paste the following into your terminal: ``` curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh | bash ``` -It is recommended you review the [installation script](https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh) +You may review the [installation script](https://raw.githubusercontent.com/NuSkooler/enigma-bbs/0.0.9-alpha/misc/install.sh) on GitHub before running it. -The script will install nvm, Node.js 6 and grab the latest ENiGMA BBS from GitHub. It will also guide you -through creating a basic configuration file, and recommend some packages to install. +The script will install nvm, Node.js 6 and grab the latest ENiGMA BBS from GitHub. It will also guide you through creating a basic configuration file, and recommend some packages to install. + +After installing, see [Updating](/docs/admin/updating.md). + diff --git a/docs/messageareas/message-networks.md b/docs/messageareas/message-networks.md index 84b7859e..5838da53 100644 --- a/docs/messageareas/message-networks.md +++ b/docs/messageareas/message-networks.md @@ -2,41 +2,44 @@ layout: page title: Message Networks --- +## Message Networks ENiGMA½ considers all non-ENiGMA½, non-local messages (and their networks, such as FTN "external". That is, messages are only imported and exported from/to such a networks. Configuring such external message networks in ENiGMA½ requires three sections in your `config.hjson`. 1. `messageNetworks..networks`: declares available networks. 2. `messageNetworks..areas`: establishes local area mappings and per-area specifics. 3. `scannerTossers.`: general configuration for the scanner/tosser (import/export). This is also where we configure per-node settings. -## FTN Networks +### FTN Networks FidoNet and FidoNet style (FTN) networks as well as a [FTN/BSO scanner/tosser](bso-import-export.md) (`ftn_bso` module) are configured via the `messageNetworks.ftn` and `scannerTossers.ftn_bso` blocks in `config.hjson`. -:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts** to perfrom packet transport! An external utility such as Binkd is required for this! +:information_source: ENiGMA½'s `ftn_bso` module is **not a mailer** and makes **no attempts** to perform packet transport! An external utility such as Binkd is required for this! -### Networks -The `networks` block a per-network configuration where each entry's key may be referenced elswhere in `config.hjson`. +#### Networks +The `networks` block a per-network configuration where each entry's key may be referenced elsewhere in `config.hjson`. -Example: the following example declares two networks: `agoranet` and `fsxnet`: +Example: the following example declares two networks: `araknet` and `fsxnet`: ```hjson { messageNetworks: { ftn: { networks: { - araknet: { - defaultZone: 10 - localAddress: "10:101/9" - } + // it is recommended to use lowercase network tags fsxnet: { defaultZone: 21 localAddress: "21:1/121" } + + araknet: { + defaultZone: 10 + localAddress: "10:101/9" + } } } } } ``` -### Areas +#### Areas The `areas` section describes a mapping of local **area tags** configured in your `messageConferences` (see [Configuring a Message Area](configuring-a-message-area.md)) to a message network (described above), a FTN specific area tag, and remote uplink address(s). This section can be thought of similar to the *AREAS.BBS* file used by other BBS packages. When ENiGMA½ imports messages, they will be placed in the local area that matches key under `areas` while exported messages will be sent to the relevant `network`. @@ -53,6 +56,7 @@ Example: messageNetworks: { ftn: { areas: { + // it is recommended to use lowercase area tags fsx_general: // *local* tag found within messageConferences network: fsxnet // that we are mapping to this network tag: FSX_GEN // ...and this remote FTN-specific tag @@ -64,5 +68,38 @@ Example: } ``` +:information_source: You can import `AREAS.BBS` or FTN style `.NA` files using [oputil](/docs/admin/oputil.md)! + +### A More Complete Example +Below is a more complete *example* illustrating some of the concepts above: + +```hjson +{ + messageNetworks: { + ftn: { + networks: { + fsxnet: { + defaultZone: 21 + localAddress: "21:1/121" + } + } + + areas: { + fsx_general: { + network: fsxnet + + // ie as found in your info packs .NA file + tag: FSX_GEN + + uplinks: [ "21:1/100" ] + } + } + } + } +} +``` + +:information_source: Remember for a complete FTN experience, you'll probably also want to configure [FTN/BSO scanner/tosser](bso-import-export.md) settings. + ### FTN/BSO Scanner Tosser Please see the [FTN/BSO Scanner/Tosser](bso-import-export.md) documentation for information on this area. diff --git a/docs/troubleshooting/monitoring-logs.md b/docs/troubleshooting/monitoring-logs.md index dd665f5c..28a3773a 100644 --- a/docs/troubleshooting/monitoring-logs.md +++ b/docs/troubleshooting/monitoring-logs.md @@ -2,6 +2,7 @@ layout: page title: Monitoring Logs --- +## Monitoring Logs ENiGMA½ does not produce much to stdout. Logs are produced by Bunyan which outputs each entry as a JSON object. Start by installing bunyan and making it available on your path: @@ -20,3 +21,32 @@ To tail logs in a colorized and pretty format, issue the following command: tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan ``` +See `bunyan --help` for more information on what you can do! + +### Example +Logs _without_ Bunyan: +```bash +tail -F /path/to/enigma-bbs/logs/enigma-bbs.log +{"name":"ENiGMA½ BBS","hostname":"nu-dev","pid":25002,"level":30,"eventName":"updateFileAreaStats","action":{"type":"method","location":"core/file_base_area.js","what":"updateAreaStatsScheduledEvent","args":[]},"reason":"Schedule","msg":"Executing scheduled event action...","time":"2018-12-15T16:00:00.001Z","v":0} +{"name":"ENiGMA½ BBS","hostname":"nu-dev","pid":25002,"level":30,"module":"FTN BSO","msg":"Performing scheduled message import/toss...","time":"2018-12-15T16:00:00.002Z","v":0} +{"name":"ENiGMA½ BBS","hostname":"nu-dev","pid":25002,"level":30,"module":"FTN BSO","msg":"Performing scheduled message import/toss...","time":"2018-12-15T16:30:00.008Z","v":0} +``` + +Oof! + +Logs _with_ Bunyan: +```bash +tail -F /path/to/enigma-bbs/logs/enigma-bbs.log | bunyan +[2018-12-15T16:00:00.001Z] INFO: ENiGMA½ BBS/25002 on nu-dev: Executing scheduled event action... (eventName=updateFileAreaStats, reason=Schedule) + action: { + "type": "method", + "location": "core/file_base_area.js", + "what": "updateAreaStatsScheduledEvent", + "args": [] + } +[2018-12-15T16:00:00.002Z] INFO: ENiGMA½ BBS/25002 on nu-dev: Performing scheduled message import/toss... (module="FTN BSO") +[2018-12-15T16:30:00.008Z] INFO: ENiGMA½ BBS/25002 on nu-dev: Performing scheduled message import/toss... (module="FTN BSO") +``` + +Much better! + From 3eaf4dd0d80a973e489878ffd22cca8061f23ef7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 23:42:19 -0700 Subject: [PATCH 450/569] + oputil.js user rm USERNAME * Fix some of my horrid spelling... --- core/database.js | 4 +- core/file_entry.js | 6 +- core/message.js | 6 +- core/oputil/oputil_user.js | 116 +++++++++++++++++++++++++++++++++++-- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/core/database.js b/core/database.js index e4a812d4..040cc1de 100644 --- a/core/database.js +++ b/core/database.js @@ -20,7 +20,7 @@ exports.getTransactionDatabase = getTransactionDatabase; exports.getModDatabasePath = getModDatabasePath; exports.loadDatabaseForMod = loadDatabaseForMod; exports.getISOTimestampString = getISOTimestampString; -exports.sanatizeString = sanatizeString; +exports.sanitizeString = sanitizeString; exports.initializeDatabases = initializeDatabases; exports.dbs = dbs; @@ -76,7 +76,7 @@ function getISOTimestampString(ts) { return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } -function sanatizeString(s) { +function sanitizeString(s) { return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex switch (c) { case '\0' : return '\\0'; diff --git a/core/file_entry.js b/core/file_entry.js index 9ba1c692..4f5e47d1 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -5,7 +5,7 @@ const fileDb = require('./database.js').dbs.file; const Errors = require('./enig_error.js').Errors; const { getISOTimestampString, - sanatizeString + sanitizeString } = require('./database.js'); const Config = require('./config.js').get; @@ -565,14 +565,14 @@ module.exports = class FileEntry { `f.file_id IN ( SELECT rowid FROM file_fts - WHERE file_fts MATCH ":${sanatizeString(filter.terms)}" + WHERE file_fts MATCH ":${sanitizeString(filter.terms)}" )` ); } if(filter.tags && filter.tags.length > 0) { // build list of quoted tags; filter.tags comes in as a space and/or comma separated values - const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanatizeString(tag)}"` ).join(','); + const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanitizeString(tag)}"` ).join(','); appendWhereClause( `f.file_id IN ( diff --git a/core/message.js b/core/message.js index 34c590bb..7a30fb02 100644 --- a/core/message.js +++ b/core/message.js @@ -8,7 +8,7 @@ const createNamedUUID = require('./uuid_util.js').createNamedUUID; const Errors = require('./enig_error.js').Errors; const ANSI = require('./ansi_term.js'); const { - sanatizeString, + sanitizeString, getISOTimestampString } = require('./database.js'); const { @@ -354,7 +354,7 @@ module.exports = class Message { [ 'toUserName', 'fromUserName' ].forEach(field => { if(_.isString(filter[field]) && filter[field].length > 0) { - appendWhereClause(`m.${_.snakeCase(field)} LIKE "${sanatizeString(filter[field])}"`); + appendWhereClause(`m.${_.snakeCase(field)} LIKE "${sanitizeString(filter[field])}"`); } }); @@ -375,7 +375,7 @@ module.exports = class Message { `m.message_id IN ( SELECT rowid FROM message_fts - WHERE message_fts MATCH ":${sanatizeString(filter.terms)}" + WHERE message_fts MATCH ":${sanitizeString(filter.terms)}" )` ); } diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 18519b06..3f169607 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -2,10 +2,13 @@ /* eslint-disable no-console */ 'use strict'; -const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; -const ExitCodes = require('./oputil_common.js').ExitCodes; -const argv = require('./oputil_common.js').argv; -const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; +const { + printUsageAndSetExitCode, + getAnswers, + ExitCodes, + argv, + initConfigAndDatabases +} = require('./oputil_common.js'); const getHelpFor = require('./oputil_help.js').getHelpFor; const Errors = require('../enig_error.js').Errors; const UserProps = require('../user_property.js'); @@ -111,8 +114,109 @@ function setUserPassword(user) { ); } -function removeUser() { - console.error('NOT YET IMPLEMENTED'); +function removeUserRecordsFromDbAndTable(dbName, tableName, userId, col, cb) { + const db = require('../../core/database.js').dbs[dbName]; + db.run( + `DELETE FROM ${tableName} + WHERE ${col} = ?;`, + [ userId ], + err => { + return cb(err); + } + ); +} + +function removeUser(user) { + async.series( + [ + (callback) => { + if(false === argv.prompt) { + return callback(null); + } + + console.info('About to permanently delete the following user:'); + console.info(`Username : ${user.username}`); + console.info(`Real name: ${user.properties[UserProps.RealName] || 'N/A'}`); + console.info(`User ID : ${user.userId}`); + console.info('WARNING: This cannot be undone!'); + getAnswers([ + { + name : 'proceed', + message : `Proceed in deleting ${user.username}?`, + type : 'confirm', + } + ], + answers => { + if(answers.proceed) { + return callback(null); + } + return callback(Errors.General('User canceled')); + }); + }, + (callback) => { + // op has confirmed they are wanting ready to proceed (or passed --no-prompt) + const DeleteFrom = { + message : [ 'user_message_area_last_read' ], + system : [ 'user_event_log', ], + user : [ 'user_group_member', 'user' ], + }; + + async.eachSeries(Object.keys(DeleteFrom), (dbName, nextDbName) => { + const tables = DeleteFrom[dbName]; + async.eachSeries(tables, (tableName, nextTableName) => { + const col = ('user' === dbName && 'user' === tableName) ? 'id' : 'user_id'; + removeUserRecordsFromDbAndTable(dbName, tableName, user.userId, col, err => { + return nextTableName(err); + }); + }, + err => { + return nextDbName(err); + }); + }, + err => { + return callback(err); + }); + }, + (callback) => { + // + // Clean up *private* messages *to* this user + // + const Message = require('../../core/message.js'); + const MsgDb = require('../../core/database.js').dbs.message; + + const filter = { + resultType : 'id', + privateTagUserId : user.userId, + }; + Message.findMessages(filter, (err, ids) => { + if(err) { + return callback(err); + } + + async.eachSeries(ids, (messageId, nextMessageId) => { + MsgDb.run( + `DELETE FROM message + WHERE message_id = ?;`, + [ messageId ], + err => { + return nextMessageId(err); + } + ); + }, + err => { + return callback(err); + }); + }); + } + ], + err => { + if(err) { + return console.error(err.reason ? err.reason : err.message); + } + + console.info('User has been deleted.'); + } + ); } function modUserGroups(user) { From 6d45d74a47b8b3a34b058d6fdb7eb61f3ae401ef Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 15 Dec 2018 23:52:59 -0700 Subject: [PATCH 451/569] Little better NNTP config --- core/servers/content/nntp.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 9963a566..98dbdca7 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -29,7 +29,6 @@ const asyncReduce = require('async/reduce'); const asyncMap = require('async/map'); const asyncSeries = require('async/series'); const LRU = require('lru-cache'); -const iconv = require('iconv-lite'); // // Network News Transfer Protocol (NNTP) @@ -589,8 +588,9 @@ class NNTPServer extends NNTPServerBase { return super._getOverviewFmt(session); } - _getNewNews(session, time, wildmat) { - throw new Error('method `nntp._getNewNews` is not implemented'); + _getNewNews(/*session, time, wildmat*/) { + // Currently seems pointless to implement. No semi-modern clients seem to use it anyway. + throw new Errors.Invalid('NEWNEWS is not enabled on this server'); } getMessageDate(message) { @@ -633,11 +633,11 @@ class NNTPServer extends NNTPServerBase { AnsiPrep( message.message, { - termWidth : 1000, // unrealistically long; don't want to wrap, really. - cols : 1000, // ...see above. rows : 'auto', - asciiMode : true, // Export to ASCII - fillLines : false, // Don't fill up columns + cols : 79, + forceLineTerm : true, + asciiMode : true, + fillLines : false, }, (err, prepped) => { message.preparedBody = prepped || message.message; From 422a925daaadd62d178b305bd8d6c9552564c872 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 16 Dec 2018 00:26:28 -0700 Subject: [PATCH 452/569] + oputil.js user info * Fix up some help messaging * Don't allow del of +op --- core/oputil/oputil_help.js | 17 ++++++++----- core/oputil/oputil_user.js | 52 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 8b946edd..36002245 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -25,13 +25,16 @@ commands: `usage: optutil.js user [] actions: - pw USERNAME PASSWORD set password to PASSWORD for USERNAME - rm USERNAME permanently removes USERNAME user from system - activate USERNAME sets USERNAME's status to active - deactivate USERNAME sets USERNAME's status to inactive - disable USERNAME sets USERNAME's status to disabled - lock USERNAME sets USERNAME's status to locked - group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP + info USERNAME display information about a user + pw USERNAME PASSWORD set a user's password + aliases: password, passwd + rm USERNAME permanently removes user from system + aliases: remove, delete, del + activate USERNAME set status to active + deactivate USERNAME set status to inactive + disable USERNAME set status to disabled + lock USERNAME set status to locked + group USERNAME [+|-]GROUP adds (+) or removes (-) user from a group `, Config : diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 3f169607..a52facfb 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -15,6 +15,7 @@ const UserProps = require('../user_property.js'); const async = require('async'); const _ = require('lodash'); +const moment = require('moment'); exports.handleUserCommand = handleUserCommand; @@ -129,6 +130,13 @@ function removeUserRecordsFromDbAndTable(dbName, tableName, userId, col, cb) { function removeUser(user) { async.series( [ + (callback) => { + if(user.isRoot()) { + return callback(Errors.Invalid('Cannot delete root/SysOp user!')); + } + + return callback(null); + }, (callback) => { if(false === argv.prompt) { return callback(null); @@ -262,6 +270,44 @@ function modUserGroups(user) { } } +function showUserInfo(user) { + + const User = require('../../core/user.js'); + + const statusDesc = () => { + const status = user.properties[UserProps.AccountStatus]; + return _.invert(User.AccountStatus)[status] || 'unknown'; + }; + + const created = () => { + const ac = user.properties[UserProps.AccountCreated]; + return ac ? moment(ac).format() : 'N/A'; + }; + + const lastLogin = () => { + const ll = user.properties[UserProps.LastLoginTs]; + return ll ? moment(ll).format() : 'N/A'; + }; + + const propOrNA = p => { + return user.properties[p] || 'N/A'; + }; + + console.info(`User information: +Username : ${user.username}${user.isRoot() ? ' (root/SysOp)' : ''} +Real name : ${propOrNA(UserProps.RealName)} +ID : ${user.userId} +Status : ${statusDesc()} +Groups : ${user.groups.join(', ')} +Created : ${created()} +Last login : ${lastLogin()} +Login count : ${propOrNA(UserProps.LoginCount)} +Email : ${propOrNA(UserProps.EmailAddress)} +Location : ${propOrNA(UserProps.Location)} +Affiliations : ${propOrNA(UserProps.Affiliations)} +`); +} + function handleUserCommand() { function errUsage() { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); @@ -272,7 +318,7 @@ function handleUserCommand() { } const action = argv._[1]; - const usernameIdx = [ 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1; + const usernameIdx = [ 'pw', 'pass', 'passwd', 'password', 'group' ].includes(action) ? argv._.length - 2 : argv._.length - 1; const userName = argv._[usernameIdx]; if(!userName) { @@ -286,7 +332,7 @@ function handleUserCommand() { } return ({ - pass : setUserPassword, + pw : setUserPassword, passwd : setUserPassword, password : setUserPassword, @@ -301,6 +347,8 @@ function handleUserCommand() { lock : setAccountStatus, group : modUserGroups, + + info : showUserInfo, }[action] || errUsage)(user, action); }); } \ No newline at end of file From 320ac1fc36cef3ffe64d2fcf8c8e7074f848be79 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 16 Dec 2018 00:38:49 -0700 Subject: [PATCH 453/569] Doc update --- docs/admin/oputil.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 285d96fa..881514ce 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -44,18 +44,22 @@ The `user` command covers various user operations. usage: optutil.js user [] actions: - pw USERNAME PASSWORD set password to PASSWORD for USERNAME - rm USERNAME permanently removes USERNAME user from system - activate USERNAME sets USERNAME's status to active - deactivate USERNAME sets USERNAME's status to inactive - disable USERNAME sets USERNAME's status to disabled - lock USERNAME sets USERNAME's status to locked - group USERNAME [+|-]GROUP adds (+) or removes (-) user from GROUP + info USERNAME display information about a user + pw USERNAME PASSWORD set a user's password + aliases: password, passwd + rm USERNAME permanently removes user from system + aliases: remove, delete, del + activate USERNAME set status to active + deactivate USERNAME set status to inactive + disable USERNAME set status to disabled + lock USERNAME set status to locked + group USERNAME [+|-]GROUP adds (+) or removes (-) user from a group ``` | Action | Description | Examples | Aliases | |-----------|-------------------|---------------------------------------|-----------| -| `pw` | Set password | `./oputil.js user pw joeuser s3cr37` | `pass`, `passwd`, `password` | +| `info` | Display user information| `./oputil.js user info joeuser` | N/A | +| `pw` | Set password | `./oputil.js user pw joeuser s3cr37` | `passwd`, `password` | | `rm` | Removes user | `./oputil.js user del joeuser` | `remove`, `del`, `delete` | | `activate` | Activates user | `./oputil.js user activate joeuser` | N/A | | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | From 12a1809a886d258c8baa2014c7813630cecf26b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 16 Dec 2018 00:40:14 -0700 Subject: [PATCH 454/569] lol fail --- core/oputil/oputil_help.js | 6 +++--- docs/admin/oputil.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 36002245..8e2bc2c2 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -8,7 +8,7 @@ exports.getHelpFor = getHelpFor; const usageHelp = exports.USAGE_HELP = { General : -`usage: optutil.js [--version] [--help] +`usage: oputil.js [--version] [--help] [] global args: @@ -22,7 +22,7 @@ commands: mb message base management `, User : -`usage: optutil.js user [] +`usage: oputil.js user [] actions: info USERNAME display information about a user @@ -38,7 +38,7 @@ actions: `, Config : -`usage: optutil.js config [] +`usage: oputil.js config [] actions: new generate a new/initial configuration diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 881514ce..dd989a5a 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -8,7 +8,7 @@ ENiGMA½ comes with `oputil.js` henceforth known as `oputil`, a command line int Let's look the main help output as per this writing: ``` -usage: optutil.js [--version] [--help] +usage: oputil.js [--version] [--help] [] global args: @@ -41,7 +41,7 @@ Type `./oputil.js --help` for additional help on a particular command. The `user` command covers various user operations. ``` -usage: optutil.js user [] +usage: oputil.js user [] actions: info USERNAME display information about a user @@ -71,7 +71,7 @@ actions: The `config` command allows sysops to perform various system configuration and maintenance tasks. ``` -usage: optutil.js config [] +usage: oputil.js config [] actions: new generate a new/initial configuration From e0f6847581c30fb2e1a922b3e9172aae82eb4396 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 11:09:42 -0700 Subject: [PATCH 455/569] + NNTP docs + Gopher docs * Minor doc cleanup --- WHATSNEW.md | 2 ++ docs/_includes/nav.md | 2 ++ docs/modding/file-area-list.md | 2 +- docs/servers/gopher.md | 40 +++++++++++++++++++++ docs/servers/nntp.md | 66 ++++++++++++++++++++++++++++++++++ docs/servers/ssh.md | 47 ++++++++++++------------ docs/servers/telnet.md | 30 +++++++++------- docs/servers/websocket.md | 3 ++ 8 files changed, 156 insertions(+), 36 deletions(-) create mode 100644 docs/servers/gopher.md create mode 100644 docs/servers/nntp.md diff --git a/WHATSNEW.md b/WHATSNEW.md index f78bf6c3..d7c26361 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -22,6 +22,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * `install.sh` will now attempt to use NPM's `--build-from-source` option when ARM is detected. * `oputil.js config new` will now generate a much more complete configuration file with comments, examples, etc. `oputil.js config cat` dumps your current config to stdout. * Handling of failed login attempts is now fully in. Disconnect clients, lock out accounts, ability to auto or unlock at (email-driven) password reset, etc. See `users.failedLogin` in `config.hjson`. +* NNTP support! See [NNTP docs](/docs/servers/nntp.md) for more information. +* `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md). ## 0.0.8-alpha diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 326ebaad..79ce2f31 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -56,6 +56,8 @@ - Build your own - Content Servers - [Web]({{ site.baseurl }}{% link servers/web-server.md %}) + - [Gopher]({{ site.baseurl }}{% link servers/gopher.md %}) + - [NNTP]({{ site.baseurl }}{% link servers/nntp.md %}) - Modding - [Local Doors]({{ site.baseurl }}{% link modding/local-doors.md %}) diff --git a/docs/modding/file-area-list.md b/docs/modding/file-area-list.md index 735986a0..dcc0b958 100644 --- a/docs/modding/file-area-list.md +++ b/docs/modding/file-area-list.md @@ -6,7 +6,7 @@ title: File Area List The built in `file_area_list` module provides a very flexible file listing UI. ## Configuration -## Config Block +### Config Block Available `config` block entries: * `art`: Sub-configuration block used to establish art files used for file browsing: * `browse`: The main browse screen. diff --git a/docs/servers/gopher.md b/docs/servers/gopher.md new file mode 100644 index 00000000..343c7d80 --- /dev/null +++ b/docs/servers/gopher.md @@ -0,0 +1,40 @@ +--- +layout: page +title: Gopher Server +--- +## The Gopher Content Server +The Gopher *content server* provides access to publicly exposed message conferences and areas over Gopher (gopher://). + +## Configuration +Gopher configuration is found in `contentServers.gopher` in `config.hjson`. + +| Item | Required | Description | +|------|----------|-------------| +| `enabled` | :+1: | Set to `true` to enable Gopher | +| `port` | :-1: | Override the default port of `8070` | +| `publicHostName` | :+1: | Set the **public** hostname/domain that Gopher will serve to the outside world. Example: `myfancybbs.com` | +| `publicPort` | :+1: | Set the **public** port that Gopher will serve to the outside world. | +| `messageConferences` | :+1: | An map of *conference tags* to *area tags* that are publicly exposed via Gopher. See example below. | + +Notes on `publicHostName` and `publicPort`: +The Gopher protocol serves content that contains host/domain and port even when referencing it's own documents. Due to this, these members must be set to your publicly addressable Gopher server! + +### Example +Let's suppose you are serving Gopher for your BBS at `myfancybbs.com`. Your ENiGMA½ system is listening on the default Gopher `port` of 8070 but you're behind a firewall and want port 70 exposed to the public. Lastly, you want to expose some fsxNet areas: + +```hjson +contentServers: { + gopher: { + enabled: true + publicHostName: myfancybbs.com + publicPort: 70 + + messageConferences: { + fsxnet: { // fsxNet's conf tag + // Areas of fsxNet we want to expose: + "fsx_gen", "fsx_bbs" + } + } + } +} +``` diff --git a/docs/servers/nntp.md b/docs/servers/nntp.md new file mode 100644 index 00000000..c6ceaf2e --- /dev/null +++ b/docs/servers/nntp.md @@ -0,0 +1,66 @@ +--- +layout: page +title: NNTP Server +--- +## The NNTP Content Server +The NNTP *content server* provides access to publicly exposed message conferences and areas over either **secure** NNTPS (NNTP over TLS or nttps://) and/or non-secure NNTP (nntp://). + +## Configuration +| Item | Required | Description | +|------|----------|-------------| +| `nntp` | :-1: | Configuration block for non-secure NNTP. See Non-Secure NNTP Configuration below. | +| `nntps` | :-1: | Configuration block for secure NNTP. See Secure NNTPS Configuration below. | +| `publicMessageConferences` | :+1: | A map of *conference tags* to *area tags* that are publicly exposed over NNTP. Anonymous users will get read-only access to these areas. | + +### See Non-Secure NNTP Configuration +Under `contentServers.nntp.nntp` the following configuration is allowed: + +| Item | Required | Description | +|------|----------|-------------| +| `enabled` | :+1: | Set to `true` to enable non-secure NNTP access. | +| `port` | :-1: | Override the default port of `8119`. | + +### Secure NNTPS Configuration +Under `contentServers.nntp.nntps` the following configuration is allowed: + +| Item | Required | Description | +|------|----------|-------------| +| `enabled` | :+1: | Set to `true` to enable secure NNTPS access. | +| `port` | :-1: | Override the default port of `8565`. | +| `certPem` | :-1: | Override the default certificate file path of `./config/nntps_cert.pem` | +| `keyPem` | :-1: | Override the default certificate key file path of `./config/nntps_key.pem` | + +#### Certificates and Keys +In order to use secure NNTPS, a TLS certificate and key pair must be provided. You may generate your own but most clients **will not trust** them. A certificate and key from a trusted Certificate Authority is recommended. [Let's Encrypt](https://letsencrypt.org/) provides free TLS certificates. Certificates and private keys must be in [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail). + +##### Generating Your Own +An example of generating your own cert/key pair: +```bash +openssl req -newkey rsa:2048 -nodes -keyout ./config/nntps_key.pem -x509 -days 3050 -out ./config/nntps_cert.pem +``` + +### Example Configuration +```hjson +contentServers: { + nntp: { + publicMessageConferences: { + fsxnet: [ + // Expose these areas of fsxNet + "fsx_gen", "fsx_bbs" + ] + } + + nntp: { + enabled: true + } + + nntps: { + enabled: true + + // These could point to Let's Encrypt provided pairs for example: + certPem: /path/to/some/tls_cert.pem + keyPem: /path/to/some/tls_private_key.pem + } + } +} +``` diff --git a/docs/servers/ssh.md b/docs/servers/ssh.md index 51471e28..a71f8250 100644 --- a/docs/servers/ssh.md +++ b/docs/servers/ssh.md @@ -2,38 +2,41 @@ layout: page title: SSH Server --- -## Generate a SSH Private Key +## SSH Login Server +The ENiGMA½ SSH *login server* allows secure user logins over SSH (ssh://). -To utilize the SSH server, an SSH Private Key will need generated. From the ENiGMA installation directory: +## Configuration +Entries available under `config.loginServers.ssh`: -```bash -openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 -``` +| Item | Required | Description | +|------|----------|-------------| +| `privateKeyPem` | :-1: | Path to private key file. If not set, defaults to `./config/ssh_private_key.pem` | +| `privateKeyPass` | :+1: | Password to private key file. +| `firstMenu` | :-1: | First menu an SSH connected user is presented with. Defaults to `sshConnected`. | +| `firstMenuNewUser` | :-1: | Menu presented to user when logging in with one of the usernames found within `users.newUserNames` in your `config.hjson`. Examples include `new` and `apply`. | +| `enabled` | :+1: | Set to `true` to enable the SSH server. | +| `port` | :-1: | Override the default port of `8443`. | +| `algorithms` | :-1: | Configuration block for SSH algorithms. Includes keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config.js`. +| `traceConnections` | :-1: | Set to `true` to enable full trace-level information on SSH connections. -You then need to enable the SSH server in your `config.hjson`: +### Example Configuration ```hjson { - loginServers: { - ssh: { + loginServers: { + ssh: { enabled: true - port: 8889 - privateKeyPem: /path/to/ssh_private_key.pem - privateKeyPass: YOUR_PK_PASS + port: 8889 + privateKeyPem: /path/to/ssh_private_key.pem + privateKeyPass: sup3rs3kr3tpa55 } } } ``` -### SSH Server Options +## Generate a SSH Private Key +To utilize the SSH server, an SSH Private Key will need generated. OpenSSL can be used for this task: -| Option | Description -|---------------------|--------------------------------------------------------------------------------------| -| `privateKeyPem` | Path to private key file. -| `privateKeyPass` | Password to private key file. -| `firstMenu` | First menu an SSH connected user is presented with. -| `firstMenuNewUser` | Menu presented to user when logging in with `users::newUserNames` in your config.hjson (defaults to `new` and `apply`). -| `enabled` | Enable/disable SSH server. -| `port` | Configure a custom port for the SSH server. -| `algorithms` | Configuration block for SSH algoritms. Includes arrays with keys of `kex`, `cipher`, `hmac`, and `compress`. See the algorithms section in the [ssh2-streams](https://github.com/mscdex/ssh2-streams#ssh2stream-methods) documentation for details. For defaults set by ENiGMA½, see `core/config.js`. -| `traceConnections` | Set to `true` to enable full trace-level information on SSH connections. +```bash +openssl genrsa -des3 -out ./config/ssh_private_key.pem 2048 +``` diff --git a/docs/servers/telnet.md b/docs/servers/telnet.md index 47aba591..ccefc966 100644 --- a/docs/servers/telnet.md +++ b/docs/servers/telnet.md @@ -2,24 +2,28 @@ layout: page title: Telnet Server --- +## Telnet Login Server +The Telnet *login server* provides a standard **non-secure** Telnet login experience. -Telnet is enabled by default on port `8888` in `config.hjson`: +## Configuration +The following configuration can be made in `config.hjson` under the `loginServers.telnet` block: +| Item | Required | Description | +|------|----------|-------------| +| `enabled` | :-1: Defaults to `true`. Set to `false` to disable Telnet | +| `port` | :-1: | Override the default port of `8888`. | +| `firstMenu` | :-1: | First menu a telnet connected user is presented with. Defaults to `telnetConnected`. | + +### Example Configuration ```hjson { - loginServers: { - telnet: { - enabled: true - port: 8888 - } - } + loginServers: { + telnet: { + enabled: true + port: 8888 + } + } } ``` -### Telnet Server Options -| Option | Description -|---------------------|--------------------------------------------------------------------------------------| -| `firstMenu` | First menu a telnet connected user is presented with -| `enabled` | Enable/disable telnet server -| `port` | Configure a custom port for the telnet server diff --git a/docs/servers/websocket.md b/docs/servers/websocket.md index 18271d6f..98f867e7 100644 --- a/docs/servers/websocket.md +++ b/docs/servers/websocket.md @@ -2,6 +2,9 @@ layout: page title: Web Socket / Web Interface Server --- +## WebSocket Login Server +The WebSocket Login Server provides **secure** (wss://) as well as non-secure (ws://) WebSocket login access. This is often combined with a browser based WebSocket client such as VTX or fTelnet. + # VTX Web Client ENiGMA supports the VTX websocket client for connecting to your BBS from a web page. Example usage can be found at [Xibalba](https://l33t.codes/vtx/xibalba.html) and [fORCE9](https://bbs.force9.org/vtx/force9.html). From b1eea4f4b7e0eda122219abbc89a019394795d79 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 11:20:14 -0700 Subject: [PATCH 456/569] Some logging updates --- core/servers/content/nntp.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 98dbdca7..14d5fe9d 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -578,7 +578,13 @@ class NNTPServer extends NNTPServerBase { }); if(!value) { - this.log.trace(`No value for requested header field "${field}"`); + // + // Clients will check some headers just to see if they exist. + // Don't spam logs with these. For others, it's good to know. + // + if(!['references', 'xref'].includes(field)) { + this.log.trace(`No value for requested header field "${field}"`); + } } return value; @@ -588,8 +594,9 @@ class NNTPServer extends NNTPServerBase { return super._getOverviewFmt(session); } - _getNewNews(/*session, time, wildmat*/) { + _getNewNews(session, time, wildmat) { // Currently seems pointless to implement. No semi-modern clients seem to use it anyway. + this.log.debug( { time, wildmat }, 'Request made using unsupported NEWNEWS command'); throw new Errors.Invalid('NEWNEWS is not enabled on this server'); } From 8356f00ba6d66715a8791bb5b1648cd5e920143f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 11:56:07 -0700 Subject: [PATCH 457/569] Fix bug when user has been nuked --- core/last_callers.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/last_callers.js b/core/last_callers.js index c80ce336..f7c2552e 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -42,29 +42,29 @@ exports.getModule = class LastCallersModule extends MenuModule { async.waterfall( [ - (next) => { + (callback) => { this.prepViewController('callers', 0, mciData.menu, err => { - return next(err); + return callback(err); }); }, - (next) => { + (callback) => { this.fetchHistory( (err, loginHistory) => { - return next(err, loginHistory); + return callback(err, loginHistory); }); }, - (loginHistory, next) => { + (loginHistory, callback) => { this.loadUserForHistoryItems(loginHistory, (err, updatedHistory) => { - return next(err, updatedHistory); + return callback(err, updatedHistory); }); }, - (loginHistory, next) => { + (loginHistory, callback) => { const callersView = this.viewControllers.callers.getView(MciViewIds.callerList); if(!callersView) { return cb(Errors.MissingMci(`Missing caller list MCI ${MciViewIds.callerList}`)); } callersView.setItems(loginHistory); callersView.redraw(); - return next(null); + return callback(null); } ], err => { @@ -178,10 +178,10 @@ exports.getModule = class LastCallersModule extends MenuModule { }); } - async.map(loginHistory, (item, next) => { + async.map(loginHistory, (item, nextHistoryItem) => { User.getUserName(item.userId, (err, userName) => { if(err) { - return cb(null, null); + return nextHistoryItem(null, null); } item.userName = item.text = userName; @@ -192,7 +192,7 @@ exports.getModule = class LastCallersModule extends MenuModule { item.realName = (props && props[UserProps.RealName]) || ''; if(!indicatorSumsSql) { - return next(null, item); + return nextHistoryItem(null, item); } sysDb.get( @@ -210,7 +210,7 @@ exports.getModule = class LastCallersModule extends MenuModule { item.actions += indicator; }); } - return next(null, item); + return nextHistoryItem(null, item); } ); }); From 832e04cdf00dea2a9f489f3fdc3f8f0c414b1f98 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 12:08:06 -0700 Subject: [PATCH 458/569] + Initial FILES.BBS support during file scan: A few formats supported so far, more to come... * Detect DESCRIPT.ION, FILES.BBS, etc. during scans --- core/descript_ion_file.js | 7 +- core/files_bbs_file.js | 158 ++++++++++++++++++++++++++++++++ core/oputil/oputil_file_base.js | 41 +++++++-- 3 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 core/files_bbs_file.js diff --git a/core/descript_ion_file.js b/core/descript_ion_file.js index a5d68e1d..d34551ba 100644 --- a/core/descript_ion_file.js +++ b/core/descript_ion_file.js @@ -1,6 +1,8 @@ /* jslint node: true */ 'use strict'; +const { Errors } = require('./enig_error.js'); + // deps const fs = require('graceful-fs'); const iconv = require('iconv-lite'); @@ -64,7 +66,10 @@ module.exports = class DescriptIonFile { return nextLine(null); }, () => { - return cb(null, descIonFile); + return cb( + descIonFile.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized DESCRIPT.ION format'), + descIonFile + ); }); }); } diff --git a/core/files_bbs_file.js b/core/files_bbs_file.js new file mode 100644 index 00000000..989068a6 --- /dev/null +++ b/core/files_bbs_file.js @@ -0,0 +1,158 @@ +/* jslint node: true */ +'use strict'; + +const { Errors } = require('./enig_error.js'); + +// deps +const fs = require('graceful-fs'); +const iconv = require('iconv-lite'); +const moment = require('moment'); + +module.exports = class FilesBBSFile { + 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); + } + + // :TODO: encoding should be default to CP437, but allowed to change - ie for Amiga/etc. + const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); + const filesBbs = new FilesBBSFile(); + + // + // Contrary to popular belief, there is not a FILES.BBS standard. Instead, + // many formats have been used over the years. We'll try to support as much + // as we can within reason. + // + // Resources: + // - Great info from Mystic @ http://wiki.mysticbbs.com/doku.php?id=mutil_import_files.bbs + // - https://alt.bbs.synchronet.narkive.com/I6Vrxq6q/format-of-files-bbs + // + // Example files: + // - https://github.com/NuSkooler/ansi-bbs/tree/master/ancient_formats/files_bbs + // + const detectDecoder = () => { + // + // Try to figure out which decoder to use + // + const decoders = [ + { + // I've been told this is what Syncrhonet uses + tester : /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/, + extract : function() { + for(let i = 0; i < lines.length; ++i) { + let line = lines[i]; + const hdr = line.match(this.tester); + if(!hdr) { + continue; + } + const long = []; + for(let j = i + 1; j < lines.length; ++j) { + line = lines[j]; + if(!line.startsWith(' ')) { + break; + } + long.push(line.trim()); + ++i; + } + const desc = long.join('\r\n') || hdr[3] || ''; + const fileName = hdr[1]; + const timestamp = moment(hdr[2], 'MM/DD/YY'); + + filesBbs.entries.set(fileName, { timestamp, desc } ); + } + } + }, + + { + // + // Aminet Amiga CDROM, March 1994. Walnut Creek CDROM. + // CP/M CDROM, Sep. 1994. Walnut Creek CDROM. + // ...and many others. Basically: <8.3 filename> + // + // May contain headers, but we'll just skip 'em. + // + tester : /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/, + extract : function() { + lines.forEach(line => { + const hdr = line.match(this.tester); + if(!hdr) { + return; // forEach + } + + const fileName = hdr[1].trim(); + const desc = hdr[2].trim(); + + if(desc) { + filesBbs.entries.set(fileName, { desc } ); + } + }); + } + }, + + { + // Found on AMINET CD's & similar + tester : /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/, + extract : function() { + lines.forEach(line => { + const hdr = line.match(this.tester); + if(!hdr) { + return; // forEach + } + + const fileName = hdr[1].trim(); + let size = parseInt(hdr[2]); + const desc = hdr[3].trim(); + + if(!isNaN(size)) { + size *= 1024; // K->bytes. + } + + if(desc) { // omit empty entries + filesBbs.entries.set(fileName, { size, desc } ); + } + }); + } + }, + ]; + + const decoder = decoders.find(d => { + return lines + .slice(0, 10) // 10 lines in should be enough to detect - skipping headers/etc. + .some(l => d.tester.test(l)); + }); + + return decoder; + }; + + const decoder = detectDecoder(); + if(!decoder) { + return cb(Errors.Invalid('Invalid or unrecognized FILES.BBS format')); + } + + decoder.extract(decoder); + + return cb( + filesBbs.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized FILES.BBS format'), + filesBbs + ); + }); + } + + +}; diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 2077b385..18113cc0 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -48,7 +48,7 @@ function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { async.series( [ function getDescFromHandlerIfNeeded(callback) { - if((fileEntry.desc && fileEntry.desc.length > 0 ) && !argv['desc-file']) { + if((fileEntry.desc && fileEntry.descSrc != 'fileName' && fileEntry.desc.length > 0 ) && !argv['desc-file']) { return callback(null); // we have a desc already and are NOT overriding with desc file } @@ -101,18 +101,47 @@ function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { ); } -const SCAN_EXCLUDE_FILENAMES = [ 'DESCRIPT.ION', 'FILES.BBS' ]; +const SCAN_EXCLUDE_FILENAMES = [ + 'DESCRIPT.ION', + 'FILES.BBS', + 'ALLFILES.TXT', +]; function loadDescHandler(path, cb) { - const DescIon = require('../../core/descript_ion_file.js'); + const handlerClassFromFileName = { + 'descript.ion' : require('../../core/descript_ion_file.js'), + 'files.bbs' : require('../../core/files_bbs_file.js'), + }[paths.basename(path).toLowerCase()]; - // :TODO: support FILES.BBS also + if(!handlerClassFromFileName) { + return cb(Errors.DoesNotExist(`No handlers registered for ${paths.basename(path)}`)); + } - DescIon.createFromFile(path, (err, descHandler) => { + handlerClassFromFileName.createFromFile(path, (err, descHandler) => { return cb(err, descHandler); }); } +// +// Try to find a suitable description handler by +// checking for common filenames. +// +function findSuitableDescHandler(basePath, cb) { + const commonFiles = [ 'FILES.BBS', 'DESCRIPT.ION' ]; + + async.eachSeries(commonFiles, (fileName, nextFileName) => { + loadDescHandler(paths.join(basePath, fileName), (err, handler) => { + if(!err && handler) { + return cb(null, handler); + } + return nextFileName(null); + }); + }, + () => { + return cb(Errors.DoesNotExist('No suitable description handler available')); + }); +} + function scanFileAreaForChanges(areaInfo, options, cb) { const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => { @@ -145,7 +174,7 @@ function scanFileAreaForChanges(areaInfo, options, cb) { return callback(null, options.descFileHandler); // we're going to use the global handler } - loadDescHandler(paths.join(storageLoc.dir, 'DESCRIPT.ION'), (err, descHandler) => { + findSuitableDescHandler(storageLoc.dir, (err, descHandler) => { return callback(null, descHandler); }); }, From 996fcb389e08fac4cb50df35bad9ca46e6754e9b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 12:10:55 -0700 Subject: [PATCH 459/569] Minor help update --- core/oputil/oputil_help.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 8e2bc2c2..59693ad3 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -72,10 +72,9 @@ actions: scan args: --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 + --desc-file [PATH] prefer file descriptions from supplied path over other + other sources such as FILE_ID.DIZ. Path must point to + a valid FILES.BBS or DESCRIPT.ION file. --update attempt to update information for existing entries --quick perform quick scan From 2bca9c29776ad5569a03f7dfef6764dab37652be Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 12:11:45 -0700 Subject: [PATCH 460/569] Minor doc update --- docs/admin/oputil.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index dd989a5a..dae9b44c 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -114,10 +114,9 @@ actions: scan args: --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 + --desc-file [PATH] prefer file descriptions from supplied path over other + other sources such as FILE_ID.DIZ. Path must point to + a valid FILES.BBS or DESCRIPT.ION file. --update attempt to update information for existing entries --quick perform quick scan From 84ca97e936231c6609ad8e526c21f5298fb6ee8d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 15:56:09 -0700 Subject: [PATCH 461/569] FILES.BBS handling improvements - WIP --- WHATSNEW.md | 1 + core/files_bbs_file.js | 80 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index d7c26361..61b7235f 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -24,6 +24,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Handling of failed login attempts is now fully in. Disconnect clients, lock out accounts, ability to auto or unlock at (email-driven) password reset, etc. See `users.failedLogin` in `config.hjson`. * NNTP support! See [NNTP docs](/docs/servers/nntp.md) for more information. * `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md). +* Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats. ## 0.0.8-alpha diff --git a/core/files_bbs_file.js b/core/files_bbs_file.js index 989068a6..34483e48 100644 --- a/core/files_bbs_file.js +++ b/core/files_bbs_file.js @@ -47,17 +47,27 @@ module.exports = class FilesBBSFile { // - https://github.com/NuSkooler/ansi-bbs/tree/master/ancient_formats/files_bbs // const detectDecoder = () => { + // helpers + const regExpTestUpTo = (n, re) => { + return lines + .slice(0, n) + .some(l => re.test(l)); + }; + // // Try to figure out which decoder to use // const decoders = [ { // I've been told this is what Syncrhonet uses - tester : /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/, + lineRegExp : /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/, + detect : function() { + return regExpTestUpTo(10, this.lineRegExp); + }, extract : function() { for(let i = 0; i < lines.length; ++i) { let line = lines[i]; - const hdr = line.match(this.tester); + const hdr = line.match(this.lineRegExp); if(!hdr) { continue; } @@ -81,16 +91,55 @@ module.exports = class FilesBBSFile { { // - // Aminet Amiga CDROM, March 1994. Walnut Creek CDROM. - // CP/M CDROM, Sep. 1994. Walnut Creek CDROM. - // ...and many others. Basically: <8.3 filename> + // Examples: + // - Night Owl CD #7, 1992 + // + lineRegExp : /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/, + detect : function() { + return regExpTestUpTo(10, this.lineRegExp); + }, + extract : function() { + for(let i = 0; i < lines.length; ++i) { + let line = lines[i]; + const hdr = line.match(this.lineRegExp); + if(!hdr) { + continue; + } + const long = [ hdr[2].trim() ]; + for(let j = i + 1; j < lines.length; ++j) { + line = lines[j]; + if(!line.startsWith(' | ')) { + break; + } + long.push(line.substr(33)); + ++i; + } + const desc = long.join('\r\n'); + const fileName = hdr[1]; + + filesBbs.entries.set(fileName, { desc } ); + } + } + }, + + { + // + // Examples: + // - Aminet Amiga CDROM, March 1994. Walnut Creek CDROM. + // - CP/M CDROM, Sep. 1994. Walnut Creek CDROM. + // - ...and many others. + // + // Basically: <8.3 filename> // // May contain headers, but we'll just skip 'em. // - tester : /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/, + lineRegExp : /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/, + detect : function() { + return regExpTestUpTo(10, this.lineRegExp); + }, extract : function() { lines.forEach(line => { - const hdr = line.match(this.tester); + const hdr = line.match(this.lineRegExp); if(!hdr) { return; // forEach } @@ -106,8 +155,14 @@ module.exports = class FilesBBSFile { }, { - // Found on AMINET CD's & similar - tester : /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/, + // + // Examples: + // - AMINET CD's & similar + // + lineRegExp : /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/, + detect : function() { + return regExpTestUpTo(10, this.lineRegExp); + }, extract : function() { lines.forEach(line => { const hdr = line.match(this.tester); @@ -131,12 +186,7 @@ module.exports = class FilesBBSFile { }, ]; - const decoder = decoders.find(d => { - return lines - .slice(0, 10) // 10 lines in should be enough to detect - skipping headers/etc. - .some(l => d.tester.test(l)); - }); - + const decoder = decoders.find(d => d.detect()); return decoder; }; From 098e3c2fba37e318cc5d2206808a13bb8b18aad1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 17:23:02 -0700 Subject: [PATCH 462/569] More FILES.BBS support --- core/files_bbs_file.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/files_bbs_file.js b/core/files_bbs_file.js index 34483e48..fcafd165 100644 --- a/core/files_bbs_file.js +++ b/core/files_bbs_file.js @@ -108,6 +108,7 @@ module.exports = class FilesBBSFile { const long = [ hdr[2].trim() ]; for(let j = i + 1; j < lines.length; ++j) { line = lines[j]; + // -------------------------------------------------v 32 if(!line.startsWith(' | ')) { break; } @@ -122,6 +123,42 @@ module.exports = class FilesBBSFile { } }, + { + // + // Simple first line with partial description, + // secondary description lines tabbed out. + // + // Examples + // - GUS archive @ dk.toastednet.org + // + lineRegExp : /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/, + detect : function() { + return regExpTestUpTo(10, this.lineRegExp); + }, + extract : function() { + for(let i = 0; i < lines.length; ++i) { + let line = lines[i]; + const hdr = line.match(this.lineRegExp); + if(!hdr) { + continue; + } + const long = [ hdr[2].trimRight() ]; + for(let j = i + 1; j < lines.length; ++j) { + line = lines[j]; + if(!line.startsWith('\t\t ')) { + break; + } + long.push(line.substr(4)); + ++i; + } + const desc = long.join('\r\n'); + const fileName = hdr[1]; + + filesBbs.entries.set(fileName, { desc } ); + } + } + }, + { // // Examples: From 03662dc05603ae408d50395e3545c7d13f0e5cee Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 21:38:09 -0700 Subject: [PATCH 463/569] Fix major durp in code with CNET codes --- core/color_codes.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index e07b805e..4119a8ce 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -245,20 +245,24 @@ function controlCodesToAnsi(s, client) { } result += s.substr(lastIndex, m.index - lastIndex) + v; - break; case '\x19' : case '\0x11' : // CNET "Y-Style" & "Q-Style" v = m[9] || m[11]; - if('n1' === v) { - result += '\n'; - } else if('f1' === v) { - result += ANSI.clearScreen(); + if(v) { + if('n1' === v) { + v = '\n'; + } else if('f1' === v) { + v = ANSI.clearScreen(); + } else { + v = ansiSgrFromCnetStyleColorCode(v); + } } else { - result += ansiSgrFromCnetStyleColorCode(v); + v = m[0]; } + result += s.substr(lastIndex, m.index - lastIndex) + v; break; } From 1f5ec39778eab6af8b19246d203d018ae4a9d6d8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 21:39:10 -0700 Subject: [PATCH 464/569] Strip pipe codes! --- core/servers/content/nntp.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 14d5fe9d..cbb4d629 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -20,6 +20,9 @@ const { splitTextAtTerms, } = require('../../string_util.js'); const AnsiPrep = require('../../ansi_prep.js'); +const { + stripMciColorCodes +} = require('../../color_codes.js'); // deps const NNTPServerBase = require('nntp-server'); @@ -652,7 +655,7 @@ class NNTPServer extends NNTPServerBase { } ); } else { - message.preparedBody = cleanControlCodes(message.message, { all : true }); + message.preparedBody = stripMciColorCodes(cleanControlCodes(message.message, { all : true })); return cb(null); } } From eeaeef9a8c4cea99564b4c57989025413d4a6ba3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 21:39:25 -0700 Subject: [PATCH 465/569] Yet another FILES.BBS type --- core/files_bbs_file.js | 56 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/core/files_bbs_file.js b/core/files_bbs_file.js index fcafd165..cfbd5a96 100644 --- a/core/files_bbs_file.js +++ b/core/files_bbs_file.js @@ -84,6 +84,9 @@ module.exports = class FilesBBSFile { const fileName = hdr[1]; const timestamp = moment(hdr[2], 'MM/DD/YY'); + if(!timestamp.isValid()) { + continue; + } filesBbs.entries.set(fileName, { timestamp, desc } ); } } @@ -159,6 +162,54 @@ module.exports = class FilesBBSFile { } }, + { + // + // <8.3FileName> + // + // Examples: + // - Expanding Your BBS CD by David Wolfe, 1995 + // + lineRegExp : /^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/, + detect : function() { + return regExpTestUpTo(10, this.lineRegExp); + }, + extract : function() { + for(let i = 0; i < lines.length; ++i) { + let line = lines[i]; + const hdr = line.match(this.lineRegExp); + if(!hdr) { + continue; + } + + const firstDescLine = hdr[4].trimRight(); + // ugly kludge: + if('No ID File Found For This Archive File.' === firstDescLine) { + continue; + } + const long = [ firstDescLine ]; + for(let j = i + 1; j < lines.length; ++j) { + line = lines[j]; + if(!line.startsWith(' '.repeat(34))) { + break; + } + long.push(line.substr(34).trimRight()); + ++i; + } + + const desc = long.join('\r\n'); + const fileName = hdr[1]; + const size = parseInt(hdr[2]); + const timestamp = moment(hdr[3], 'MM-DD-YY'); + + if(isNaN(size) || !timestamp.isValid()) { + continue; + } + + filesBbs.entries.set(fileName, { desc, size, timestamp }); + } + } + }, + { // // Examples: @@ -211,9 +262,10 @@ module.exports = class FilesBBSFile { let size = parseInt(hdr[2]); const desc = hdr[3].trim(); - if(!isNaN(size)) { - size *= 1024; // K->bytes. + if(isNaN(size)) { + return; // forEach } + size *= 1024; // K->bytes. if(desc) { // omit empty entries filesBbs.entries.set(fileName, { size, desc } ); From f4088303ca40f8e33b1437cf19fbeb7241d57e67 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 17 Dec 2018 22:08:59 -0700 Subject: [PATCH 466/569] cleanControlCodes -> stripAnsiControlCodes --- core/fse.js | 6 +++--- core/servers/content/gopher.js | 4 ++-- core/servers/content/nntp.js | 4 ++-- core/string_util.js | 6 ++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/core/fse.js b/core/fse.js index d294f10b..1d9d6dd7 100644 --- a/core/fse.js +++ b/core/fse.js @@ -18,7 +18,7 @@ const { MessageAreaConfTempSwitcher } = require('./mod_mixins.js'); const { - isAnsi, cleanControlCodes, + isAnsi, stripAnsiControlCodes, insert } = require('./string_util.js'); const Config = require('./config.js').get; @@ -400,7 +400,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } ); } else { - bodyMessageView.setText(cleanControlCodes(msg)); + bodyMessageView.setText(stripAnsiControlCodes(msg)); } } } @@ -733,7 +733,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul var bodyMessageView = self.viewControllers.body.getView(MciViewIds.body.message); if(bodyMessageView && _.has(self, 'message.message')) { //self.setBodyMessageViewText(); - bodyMessageView.setText(cleanControlCodes(self.message.message)); + bodyMessageView.setText(stripAnsiControlCodes(self.message.message)); } } break; diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index f5ae9cfa..c47efc5c 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -8,7 +8,7 @@ const Config = require('../../config.js').get; const { splitTextAtTerms, isAnsi, - cleanControlCodes + stripAnsiControlCodes } = require('../../string_util.js'); const { getMessageConferenceByTag, @@ -218,7 +218,7 @@ exports.getModule = class GopherModule extends ServerModule { ); } else { const cleaned = stripMciColorCodes( - cleanControlCodes(body, { all : true } ) + stripAnsiControlCodes(body, { all : true } ) ); const prepped = splitTextAtTerms(cleaned) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index cbb4d629..7135a585 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -16,7 +16,7 @@ const Message = require('../../message.js'); const FTNAddress = require('../../ftn_address.js'); const { isAnsi, - cleanControlCodes, + stripAnsiControlCodes, splitTextAtTerms, } = require('../../string_util.js'); const AnsiPrep = require('../../ansi_prep.js'); @@ -655,7 +655,7 @@ class NNTPServer extends NNTPServerBase { } ); } else { - message.preparedBody = stripMciColorCodes(cleanControlCodes(message.message, { all : true })); + message.preparedBody = stripMciColorCodes(stripAnsiControlCodes(message.message, { all : true })); return cb(null); } } diff --git a/core/string_util.js b/core/string_util.js index 8fab88b8..4f7741ae 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -23,7 +23,7 @@ exports.formatByteSizeAbbr = formatByteSizeAbbr; exports.formatByteSize = formatByteSize; exports.formatCountAbbr = formatCountAbbr; exports.formatCount = formatCount; -exports.cleanControlCodes = cleanControlCodes; +exports.stripAnsiControlCodes = stripAnsiControlCodes; exports.isAnsi = isAnsi; exports.isAnsiLine = isAnsiLine; exports.isFormattedLine = isFormattedLine; @@ -218,8 +218,6 @@ function stringToNullTermBuffer(s, options = { encoding : 'utf8', maxBufLen : -1 } 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_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'); // @@ -357,7 +355,7 @@ const ANSI_OPCODES_ALLOWED_CLEAN = [ 'm', // color ]; -function cleanControlCodes(input, options) { +function stripAnsiControlCodes(input, options) { let m; let pos; let cleaned = ''; From c6e176f5bd3285c10b4c8bdd47a1fe5a01fe440b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 21 Dec 2018 14:39:57 -0700 Subject: [PATCH 467/569] Add oputil fb desc --- WHATSNEW.md | 3 +- core/files_bbs_file.js | 28 +++++++++--- core/oputil/oputil_file_base.js | 80 +++++++++++++++++++++++++++++++-- core/oputil/oputil_help.js | 10 +++-- docs/admin/oputil.md | 10 +++-- 5 files changed, 113 insertions(+), 18 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 61b7235f..5b08e2b8 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -41,7 +41,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Correly parse oddball `INTL`, `TOPT`, `FMPT`, `Via`, etc. FTN kludge lines * NetMail support! You can now send and receive NetMail. To send a NetMail address a external user using `Name
` format from your personal email menu. For example, `Foo Bar <123:123/123>`. The system also detects other formats such asa `Name @ address` (`Foo Bar@123:123/123`) * `oputil.js`: Added `mb areafix` command to quickly send AreaFix messages from the command line. You can manually send them from personal mail as well. -* `oputil.js fb rm|remove|del|delete` functionality to remove file base entries +* `oputil.js fb rm|remove|del|delete` functionality to remove file base entries. +* `oputil.js fb desc` for setting/updating a file entry description. * Users can now (re)set File and Message base pointers * Add `--update` option to `oputil.js fb scan` * Fix @watch path support for event scheduler including FTN, e.g. when looking for a `toss!.now` file produced by Binkd. diff --git a/core/files_bbs_file.js b/core/files_bbs_file.js index cfbd5a96..6e186ab4 100644 --- a/core/files_bbs_file.js +++ b/core/files_bbs_file.js @@ -8,6 +8,12 @@ const fs = require('graceful-fs'); const iconv = require('iconv-lite'); const moment = require('moment'); +// Descriptions found in the wild that mean "no description" /facepalm. +const IgnoredDescriptions = [ + 'No description available', + 'No ID File Found For This Archive File.', +]; + module.exports = class FilesBBSFile { constructor() { this.entries = new Map(); @@ -34,6 +40,10 @@ module.exports = class FilesBBSFile { const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); const filesBbs = new FilesBBSFile(); + const isBadDescription = (desc) => { + return IgnoredDescriptions.find(d => desc.startsWith(d)) ? true : false; + }; + // // Contrary to popular belief, there is not a FILES.BBS standard. Instead, // many formats have been used over the years. We'll try to support as much @@ -84,7 +94,7 @@ module.exports = class FilesBBSFile { const fileName = hdr[1]; const timestamp = moment(hdr[2], 'MM/DD/YY'); - if(!timestamp.isValid()) { + if(isBadDescription(desc) || !timestamp.isValid()) { continue; } filesBbs.entries.set(fileName, { timestamp, desc } ); @@ -121,6 +131,10 @@ module.exports = class FilesBBSFile { const desc = long.join('\r\n'); const fileName = hdr[1]; + if(isBadDescription(desc)) { + continue; + } + filesBbs.entries.set(fileName, { desc } ); } } @@ -157,6 +171,10 @@ module.exports = class FilesBBSFile { const desc = long.join('\r\n'); const fileName = hdr[1]; + if(isBadDescription(desc)) { + continue; + } + filesBbs.entries.set(fileName, { desc } ); } } @@ -182,10 +200,6 @@ module.exports = class FilesBBSFile { } const firstDescLine = hdr[4].trimRight(); - // ugly kludge: - if('No ID File Found For This Archive File.' === firstDescLine) { - continue; - } const long = [ firstDescLine ]; for(let j = i + 1; j < lines.length; ++j) { line = lines[j]; @@ -201,7 +215,7 @@ module.exports = class FilesBBSFile { const size = parseInt(hdr[2]); const timestamp = moment(hdr[3], 'MM-DD-YY'); - if(isNaN(size) || !timestamp.isValid()) { + if(isBadDescription(desc) || isNaN(size) || !timestamp.isValid()) { continue; } @@ -235,7 +249,7 @@ module.exports = class FilesBBSFile { const fileName = hdr[1].trim(); const desc = hdr[2].trim(); - if(desc) { + if(desc && !isBadDescription(desc)) { filesBbs.entries.set(fileName, { desc } ); } }); diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 18113cc0..b12c4d69 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -413,11 +413,11 @@ function dumpFileInfo(shaOrFileId, cb) { ); } -function displayFileAreaInfo() { +function displayFileOrAreaInfo() { // AREA_TAG[@STORAGE_TAG] - // SHA256|PARTIAL + // SHA256|PARTIAL|FILE_ID|FILENAME_WILDCARD // if sha: dump file info - // if area/stoarge dump area(s) + + // if area/storage dump area(s) + async.series( [ @@ -908,6 +908,75 @@ function importFileAreas() { ); } +function setFileDescription() { + // + // ./oputil.js fb set-desc CRITERIA # will prompt + // ./oputil.js fb set-desc CRITERIA "The new description" + // + let fileCriteria; + let desc; + if(argv._.length > 3) { + fileCriteria = argv._[argv._.length - 2]; + desc = argv._[argv._.length - 1]; + } else { + fileCriteria = argv._[argv._.length - 1]; + } + + async.waterfall( + [ + (callback) => { + return initConfigAndDatabases(callback); + }, + (callback) => { + getFileEntries(fileCriteria, (err, entries) => { + if(err) { + return callback(err); + } + + if(entries.length > 1) { + return callback(Errors.General('Criteria not specific enough.')); + } + + return callback(null, entries[0]); + }); + }, + (fileEntry, callback) => { + if(desc) { + return callback(null, fileEntry, desc); + } + + getAnswers([ + { + name : 'userDesc', + message : 'Description:', + type : 'editor', + } + ], + answers => { + if(!answers.userDesc) { + return callback(Errors.General('User canceled')); + } + return callback(null, fileEntry, answers.userDesc); + }); + }, + (fileEntry, newDesc, callback) => { + fileEntry.desc = newDesc; + fileEntry.persist(true, err => { // true=isUpdate + return callback(err); + }); + } + ], + err => { + if(err) { + process.exitCode = ExitCodes.ERROR; + console.error(err.message); + } else { + console.info('Description updated.'); + } + } + ); +} + function handleFileBaseCommand() { function errUsage() { @@ -924,7 +993,7 @@ function handleFileBaseCommand() { const action = argv._[1]; return ({ - info : displayFileAreaInfo, + info : displayFileOrAreaInfo, scan : scanFileAreas, mv : moveFiles, @@ -936,5 +1005,8 @@ function handleFileBaseCommand() { delete : removeFiles, 'import-areas' : importFileAreas, + + desc : setFileDescription, + description : setFileDescription, }[action] || errUsage)(); } \ No newline at end of file diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 59693ad3..f8a0d42c 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -57,9 +57,7 @@ actions: for example: scan some_area *.zip info CRITERIA display information about areas and/or files - where CRITERIA is one of the following: - AREA_TAG|SHA|FILE_ID|FILENAME_WC - SHA may be a full or partial SHA-256 + matching CRITERIA. mv SRC [SRC...] DST move entry(s) from SRC to DST SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] @@ -67,6 +65,9 @@ actions: rm SRC [SRC...] remove entry(s) from the system matching SRC SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + desc CRITERIA sets a new file description for file base entry + matching CRITERIA. Launches an external editor using + $VISUAL, $EDITOR, or vim/notepad. import-areas FILEGATE.ZXX import file base areas using FileGate RAID type format scan args: @@ -93,6 +94,9 @@ import-areas args: general information: AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag example: retro@bbs + + CRITERIA file base entry criteria. in general, can be AREA_TAG, SHA, + FILE_ID, or FILENAME_WC. FILENAME_WC filename with * and ? wildcard support. may match 0:n entries SHA full or partial SHA-256 diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index dae9b44c..bb6dcacf 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -99,9 +99,7 @@ actions: for example: scan some_area *.zip info CRITERIA display information about areas and/or files - where CRITERIA is one of the following: - AREA_TAG|SHA|FILE_ID|FILENAME_WC - SHA may be a full or partial SHA-256 + matching CRITERIA. mv SRC [SRC...] DST move entry(s) from SRC to DST SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] @@ -109,6 +107,9 @@ actions: rm SRC [SRC...] remove entry(s) from the system matching SRC SRC: FILENAME_WC|SHA|FILE_ID|AREA_TAG[@STORAGE_TAG] + desc CRITERIA sets a new file description for file base entry + matching CRITERIA. Launches an external editor using + $VISUAL, $EDITOR, or vim/notepad. import-areas FILEGATE.ZXX import file base areas using FileGate RAID type format scan args: @@ -133,6 +134,9 @@ import-areas args: general information: AREA_TAG[@STORAGE_TAG] can specify an area tag and optionally, a storage specific tag example: retro@bbs + + CRITERIA file base entry criteria. in general, can be AREA_TAG, SHA, + FILE_ID, or FILENAME_WC. FILENAME_WC filename with * and ? wildcard support. may match 0:n entries SHA full or partial SHA-256 From 73e8b0454e3f103df9ecea9bd5e97f3298b81c16 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 24 Dec 2018 15:14:37 -0700 Subject: [PATCH 468/569] Wrap ctx.reject() and catch throws --- core/servers/login/ssh.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index f626cb79..ca1922ca 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -49,8 +49,16 @@ function SSHClient(clientConn) { self.log.trace( { method : ctx.method, username : username, newUser : self.isNewUser }, 'SSH authentication attempt'); + const safeContextReject = (param) => { + try { + return ctx.reject(param); + } catch(e) { + return; + } + }; + function terminateConnection() { - ctx.reject(); + safeContextReject(); return clientConn.end(); } @@ -106,19 +114,19 @@ function SSHClient(clientConn) { return handleSpecialError(err, username); } - return ctx.reject(SSHClient.ValidAuthMethods); + return safeContextReject(SSHClient.ValidAuthMethods); } ctx.accept(); }); } else { if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) { - return ctx.reject(SSHClient.ValidAuthMethods); + return safeContextReject(SSHClient.ValidAuthMethods); } if(0 === username.length) { // :TODO: can we display something here? - return ctx.reject(); + return safeContextReject(); } const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; From 06a1925288fe7730d75ab9be9fa8b97809fcbf4f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 24 Dec 2018 15:32:38 -0700 Subject: [PATCH 469/569] Check bad usernames @ login --- core/enig_error.js | 1 + core/servers/login/ssh.js | 4 ++++ core/user_login.js | 9 +++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/enig_error.js b/core/enig_error.js index 78798a4a..33771564 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -50,4 +50,5 @@ exports.ErrorReasons = { Disabled : 'DISABLED', Inactive : 'INACTIVE', Locked : 'LOCKED', + NotAllowed : 'NOTALLOWED', }; diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index ca1922ca..6c270378 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -114,6 +114,10 @@ function SSHClient(clientConn) { return handleSpecialError(err, username); } + if(Errors.BadLogin().code === err.code) { + return terminateConnection(); + } + return safeContextReject(SSHClient.ValidAuthMethods); } diff --git a/core/user_login.js b/core/user_login.js index 2959e3a7..af764208 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -23,9 +23,14 @@ const _ = require('lodash'); exports.userLogin = userLogin; function userLogin(client, username, password, cb) { - client.user.authenticate(username, password, err => { - const config = Config(); + const config = Config(); + if(config.users.badUserNames.includes(username.toLowerCase())) { + client.log.info( { username : username }, 'Attempt to login with banned username'); + return cb(Errors.BadLogin(ErrorReasons.NotAllowed)); + } + + client.user.authenticate(username, password, err => { if(err) { client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1; const disconnect = config.users.failedLogin.disconnect; From ee93035bb85ad1c5ef0657d76ba2c5be0210e064 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 25 Dec 2018 00:18:04 -0700 Subject: [PATCH 470/569] * Disconnect clients that attempt to login with banned usernames for Telnet as well * Slow disconnects to thwart brute force attacks - these names won't exist anyway, but we want the attacking client to not DoS us --- WHATSNEW.md | 1 + core/servers/login/ssh.js | 47 ++++++++++++++++++++++---------------- core/system_menu_method.js | 5 ++++ core/user_login.js | 6 ++++- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 5b08e2b8..01fc8edc 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -25,6 +25,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * NNTP support! See [NNTP docs](/docs/servers/nntp.md) for more information. * `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md). * Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats. +* Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt. ## 0.0.8-alpha diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 6c270378..27b5aed9 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -57,39 +57,46 @@ function SSHClient(clientConn) { } }; - function terminateConnection() { + const terminateConnection = () => { safeContextReject(); return clientConn.end(); - } + }; - function promptAndTerm(msg) { + // slow version to thwart brute force attacks + const slowTerminateConnection = () => { + setTimeout( () => { + return terminateConnection(); + }, 2000); + }; + + const promptAndTerm = (msg, method = 'standard') => { if('keyboard-interactive' === ctx.method) { ctx.prompt(msg); } - return terminateConnection(); - } + return 'slow' === method ? slowTerminateConnection() : terminateConnection(); + }; - function accountAlreadyLoggedIn(username) { + const accountAlreadyLoggedIn = (username) => { return promptAndTerm(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); - } + }; - function accountDisabled(username) { + const accountDisabled = (username) => { return promptAndTerm(`${username} is disabled.\n(Press any key to continue)`); - } + }; - function accountInactive(username) { + const accountInactive = (username) => { return promptAndTerm(`${username} is waiting for +op activation.\n(Press any key to continue)`); - } + }; - function accountLocked(username) { - return promptAndTerm(`${username} is locked.\n(Press any key to continue)`); - } + const accountLocked = (username) => { + return promptAndTerm(`${username} is locked.\n(Press any key to continue)`, 'slow'); + }; - function isSpecialHandleError(err) { + const isSpecialHandleError = (err) => { return [ ErrorReasons.AlreadyLoggedIn, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked ].includes(err.reasonCode); - } + }; - function handleSpecialError(err, username) { + const handleSpecialError = (err, username) => { switch(err.reasonCode) { case ErrorReasons.AlreadyLoggedIn : return accountAlreadyLoggedIn(username); case ErrorReasons.Inactive : return accountInactive(username); @@ -97,7 +104,7 @@ function SSHClient(clientConn) { case ErrorReasons.Locked : return accountLocked(username); default : return terminateConnection(); } - } + }; // // If the system is open and |isNewUser| is true, the login @@ -115,7 +122,7 @@ function SSHClient(clientConn) { } if(Errors.BadLogin().code === err.code) { - return terminateConnection(); + return slowTerminateConnection(); } return safeContextReject(SSHClient.ValidAuthMethods); @@ -143,7 +150,7 @@ function SSHClient(clientConn) { } if(Errors.BadLogin().code === err.code) { - return terminateConnection(); + return slowTerminateConnection(); } const artOpts = { diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 5e7b651b..ea2cbc09 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -34,6 +34,11 @@ function login(callingMenu, formData, extraArgs, cb) { return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); } + // banned username results in disconnect + if(ErrorReasons.NotAllowed === err.reasonCode) { + return logoff(callingMenu, {}, {}, cb); + } + const ReasonsMenus = [ ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked ]; diff --git a/core/user_login.js b/core/user_login.js index af764208..395c446d 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -27,7 +27,11 @@ function userLogin(client, username, password, cb) { if(config.users.badUserNames.includes(username.toLowerCase())) { client.log.info( { username : username }, 'Attempt to login with banned username'); - return cb(Errors.BadLogin(ErrorReasons.NotAllowed)); + + // slow down a bit to thwart brute force attacks + return setTimeout( () => { + return cb(Errors.BadLogin('Disallowed username', ErrorReasons.NotAllowed)); + }, 2000); } client.user.authenticate(username, password, err => { From 346815a4f2e71002aab1d31765c38c504a80ce2c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 27 Dec 2018 02:14:50 -0700 Subject: [PATCH 471/569] Update to sqlite3-trans 1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89f99a00..a5dcaf3a 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "sane": "^4.0.2", "sanitize-filename": "^1.6.1", "sqlite3": "^4.0.4", - "sqlite3-trans": "^1.2.0", + "sqlite3-trans": "^1.2.1", "ssh2": "^0.6.1", "temptmp": "^1.1.0", "uuid": "^3.2.1", From 9d1815682d2e5b0878be049a47c76212fb50d54a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 27 Dec 2018 02:19:26 -0700 Subject: [PATCH 472/569] * ServerModule's createServer() is now async * Re-write of NNTP Message-ID <> internal message UUIDs --- core/config.js | 9 +- core/listening_server.js | 31 ++-- core/module_util.js | 11 +- core/msg_network.js | 21 ++- core/server_module.js | 19 ++- core/servers/content/gopher.js | 6 +- core/servers/content/nntp.js | 272 ++++++++++++++++++++++++++++---- core/servers/content/web.js | 4 +- core/servers/login/ssh.js | 6 +- core/servers/login/telnet.js | 4 +- core/servers/login/websocket.js | 6 +- 11 files changed, 305 insertions(+), 84 deletions(-) diff --git a/core/config.js b/core/config.js index 90e641d6..9c9c4cd4 100644 --- a/core/config.js +++ b/core/config.js @@ -170,7 +170,7 @@ function getDefaultConfig() { general : { boardName : 'Another Fine ENiGMA½ BBS', - // :TODO: closedSystem and loginAttemps prob belong under users{}? + // :TODO: closedSystem prob belongs under users{}? closedSystem : false, // is the system closed to new users? menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path @@ -947,13 +947,18 @@ function getDefaultConfig() { // action: // - @method:path/to/module.js:theMethodName - // (path is relative to engima base dir) + // (path is relative to ENiGMA base dir) // // - @execute:/path/to/something/executable.sh // action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', }, + nntpMaintenance : { + schedule : 'every 12 hours', // should generally be < trimMessageAreas interval + action : '@method:core/servers/content/nntp.js:performMaintenanceTask', + }, + updateFileAreaStats : { schedule : 'every 1 hours', action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', diff --git a/core/listening_server.js b/core/listening_server.js index f28bea7f..8743d6ce 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -30,30 +30,23 @@ function startListening(cb) { const moduleUtil = require('./module_util.js'); // late load so we get Config async.each( [ 'login', 'content' ], (category, next) => { - moduleUtil.loadModulesForCategory(`${category}Servers`, (err, module) => { - if(err) { - if(ErrorReasons.Disabled === err.reasonCode) { - logger.log.debug(err.message); - } else { - logger.log.info( { err : err }, 'Failed loading module'); - } - return; - } - + moduleUtil.loadModulesForCategory(`${category}Servers`, (module, nextModule) => { const moduleInst = new module.getModule(); try { - moduleInst.createServer(); - if(!moduleInst.listen()) { - throw new Error('Failed listening'); - } - - listeningServers[module.moduleInfo.packageName] = { - instance : moduleInst, - info : module.moduleInfo, - }; + moduleInst.createServer(err => { + if(!moduleInst.listen()) { + throw new Error('Failed listening'); + } + listeningServers[module.moduleInfo.packageName] = { + instance : moduleInst, + info : module.moduleInfo, + }; + return nextModule(err); + }); } catch(e) { logger.log.error(e, 'Exception caught creating server!'); + return nextModule(e); } }, err => { return next(err); diff --git a/core/module_util.js b/core/module_util.js index 0e8e5976..f61929d2 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -93,8 +93,15 @@ function loadModulesForCategory(category, iterator, complete) { async.each(jsModules, (file, next) => { loadModule(paths.basename(file, '.js'), category, (err, mod) => { - iterator(err, mod); - return next(); + if(err) { + if(ErrorReasons.Disabled === err.reasonCode) { + Log.debug(err.message); + } else { + Log.info( { err : err }, 'Failed loading module'); + } + return next(null); // continue no matter what + } + return iterator(mod, next); }); }, err => { if(complete) { diff --git a/core/msg_network.js b/core/msg_network.js index b26d5f1b..e0018ece 100644 --- a/core/msg_network.js +++ b/core/msg_network.js @@ -2,10 +2,10 @@ 'use strict'; // ENiGMA½ -let loadModulesForCategory = require('./module_util.js').loadModulesForCategory; +const loadModulesForCategory = require('./module_util.js').loadModulesForCategory; // standard/deps -let async = require('async'); +const async = require('async'); exports.startup = startup; exports.shutdown = shutdown; @@ -17,16 +17,15 @@ function startup(cb) { async.series( [ function loadModules(callback) { - loadModulesForCategory('scannerTossers', (err, module) => { - if(!err) { - const modInst = new module.getModule(); + loadModulesForCategory('scannerTossers', (module, nextModule) => { + const modInst = new module.getModule(); - modInst.startup(err => { - if(!err) { - msgNetworkModules.push(modInst); - } - }); - } + modInst.startup(err => { + if(!err) { + msgNetworkModules.push(modInst); + } + }); + return nextModule(null); }, err => { callback(err); }); diff --git a/core/server_module.js b/core/server_module.js index c9ba1cab..9ba522bf 100644 --- a/core/server_module.js +++ b/core/server_module.js @@ -1,15 +1,18 @@ /* jslint node: true */ 'use strict'; -var PluginModule = require('./plugin_module.js').PluginModule; +const PluginModule = require('./plugin_module.js').PluginModule; -exports.ServerModule = ServerModule; +exports.ServerModule = class ServerModule extends PluginModule { + constructor(options) { + super(options); + } -function ServerModule() { - PluginModule.call(this); -} + createServer(cb) { + return cb(null); + } -require('util').inherits(ServerModule, PluginModule); - -ServerModule.prototype.createServer = function() { + listen(cb) { + return cb(null); + } }; diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index c47efc5c..7553793e 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -71,9 +71,9 @@ exports.getModule = class GopherModule extends ServerModule { this.log = Log.child( { server : 'Gopher' } ); } - createServer() { + createServer(cb) { if(!this.enabled) { - return; + return cb(null); } const config = Config(); @@ -96,6 +96,8 @@ exports.getModule = class GopherModule extends ServerModule { } }); }); + + return cb(null); } listen() { diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 7135a585..7f20cba7 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -5,10 +5,13 @@ const Log = require('../../logger.js').log; const { ServerModule } = require('../../server_module.js'); const Config = require('../../config.js').get; +const { + getTransactionDatabase, + getModDatabasePath +} = require('../../database.js'); const { getMessageAreaByTag, getMessageConferenceByTag, - getMessageListForArea, } = require('../../message_area.js'); const User = require('../../user.js'); const Errors = require('../../enig_error.js').Errors; @@ -28,10 +31,14 @@ const { const NNTPServerBase = require('nntp-server'); const _ = require('lodash'); const fs = require('fs-extra'); +const forEachSeries = require('async/forEachSeries'); const asyncReduce = require('async/reduce'); const asyncMap = require('async/map'); const asyncSeries = require('async/series'); +const asyncWaterfall = require('async/waterfall'); const LRU = require('lru-cache'); +const sqlite3 = require('sqlite3'); +const paths = require('path'); // // Network News Transfer Protocol (NNTP) @@ -50,13 +57,64 @@ exports.moduleInfo = { packageName : 'codes.l33t.enigma.nntp.server', }; +exports.performMaintenanceTask = performMaintenanceTask; + /* General TODO - ACS checks need worked out. Currently ACS relies on |client|. We need a client spec that can be created even without a login server. Some checks and simply - return false/fail. + return false/fail. */ +// simple DB maps NNTP Message-ID's which are +// sequential per group -> ENiG messages +// A single instance is shared across NNTP and/or NNTPS +class NNTPDatabase +{ + constructor() { + } + + init(cb) { + asyncSeries( + [ + (callback) => { + this.db = getTransactionDatabase(new sqlite3.Database( + getModDatabasePath(exports.moduleInfo), + err => { + return callback(err); + } + )); + }, + (callback) => { + this.db.serialize( () => { + this.db.run( + `CREATE TABLE IF NOT EXISTS nntp_area_message ( + nntp_message_id INTEGER NOT NULL, + message_id INTEGER NOT NULL, + message_area_tag VARCHAR NOT NULL, + message_uuid VARCHAR NOT NULL, + + UNIQUE(nntp_message_id, message_area_tag) + );` + ); + + this.db.run( + `CREATE INDEX IF NOT EXISTS nntp_area_message_by_uuid_index + ON nntp_area_message (message_uuid);` + ); + + return callback(null); + }); + } + ], + err => { + return cb(err); + } + ); + } +} + +let nntpDatabase; class NNTPServer extends NNTPServerBase { constructor(options, serverName) { @@ -97,10 +155,6 @@ class NNTPServer extends NNTPServerBase { }); } - getMessageListIndexByMessageID(id, session) { - return id - _.get(session.groupInfo.messageList, [ 0, 'messageId' ]); - } - isGroupSelected(session) { return Array.isArray(_.get(session, 'groupInfo.messageList')); } @@ -145,7 +199,7 @@ class NNTPServer extends NNTPServerBase { case [ Message.AddressFlavor.Email ] : jamStyleFrom = `${fromName} <${remoteFrom}>`; break; - } + } } if(!jamStyleFrom) { @@ -256,14 +310,11 @@ class NNTPServer extends NNTPServerBase { return null; } - // - // Adjust to offset in message list & get UUID - // This works since we create "pseudo IDs" to return to NNTP - // by using firstRealID + index. A find on |index| member would - // also work, but would be O(n). - // - const mlIndex = this.getMessageListIndexByMessageID(messageId, session); - messageUuid = _.get(session.groupInfo.messageList, [ mlIndex, 'messageUuid']); + const msg = session.groupInfo.messageList.find(m => { + return m.index === messageId; + }); + + messageUuid = msg && msg.messageUuid; } else { // request [ , messageUuid ] = this.getMessageIdentifierParts(messageId); @@ -330,15 +381,12 @@ class NNTPServer extends NNTPServerBase { }); } - _getRange(session, first, last, options) { + _getRange(session, first, last /*options*/) { return new Promise(resolve => { // // Build an array of message objects that can later // be used with the various _build* methods. // - // Messages must belong to the range of *pseudo IDs* - // aka |index|. - // // :TODO: Handle |options| if(!this.isGroupSelected(session)) { return resolve(null); @@ -353,7 +401,7 @@ class NNTPServer extends NNTPServerBase { } return true; }).map(m => { - return { uuid : m.messageUuid, index : m.index } + return { uuid : m.messageUuid, index : m.index }; }); asyncMap(uuids, (msgInfo, nextMessageUuid) => { @@ -507,7 +555,7 @@ class NNTPServer extends NNTPServerBase { return cb(Errors.DoesNotExist(`No area for areaTag "${areaTag}" / confTag "${confTag}"`)); } - getMessageListForArea(null, areaTag, (err, messageList) => { + this.getMappedMessageListForArea(areaTag, (err, messageList) => { if(err) { return cb(err); } @@ -533,15 +581,6 @@ class NNTPServer extends NNTPServerBase { }); } - const firstMsg = messageList[0]; - - // node-nntp wants "index" - let index = firstMsg.messageId; - messageList.forEach(m => { - m.index = index; - ++index; - }); - group = { messageList, confTag, @@ -550,8 +589,8 @@ class NNTPServer extends NNTPServerBase { friendlyDesc : area.desc, nntp : { name : groupName, - min_index : firstMsg.messageId, - max_index : firstMsg.messageId + messageList.length - 1, + min_index : messageList[0].index, + max_index : messageList[messageList.length - 1].index, total : messageList.length, }, }; @@ -562,6 +601,115 @@ class NNTPServer extends NNTPServerBase { }); } + getMappedMessageListForArea(areaTag, cb) { + // + // Get all messages in mapped database. Then, find any messages that are not + // yet mapped with ID's > the highest ID we have. Any new messages will have + // new mappings created. + // + // :TODO: introduce caching + asyncWaterfall( + [ + (callback) => { + nntpDatabase.db.all( + `SELECT nntp_message_id, message_id, message_uuid + FROM nntp_area_message + WHERE message_area_tag = ? + ORDER BY nntp_message_id;`, + [ areaTag ], + (err, rows) => { + if(err) { + return callback(err); + } + + let messageList; + const lastMessageId = rows.length > 0 ? rows[rows.length - 1].message_id : 0; + if(!lastMessageId) { + messageList = []; + } else { + messageList = rows.map(r => { + return { + areaTag, + index : r.nntp_message_id, // node-nntp wants this name + messageUuid : r.message_uuid, + }; + }); + } + + return callback(null, messageList, lastMessageId); + } + ); + }, + (messageList, lastMessageId, callback) => { + // Find any new entries + const filter = { + areaTag, + newerThanMessageId : lastMessageId, + sort : 'messageId', + order : 'ascending', + resultType : 'messageList', + }; + Message.findMessages(filter, (err, newMessageList) => { + if(err) { + return callback(err); + } + + let index = messageList.length > 0 ? + messageList[messageList.length - 1].index + 1 + : 1; + newMessageList = newMessageList.map(m => { + return Object.assign(m, { index : index++ } ); + }); + + if(0 === newMessageList.length) { + return callback(null, messageList); + } + + // populate mapping DB with any new entries + nntpDatabase.db.beginTransaction( (err, trans) => { + if(err) { + return callback(err); + } + + forEachSeries(newMessageList, (newMessage, nextNewMessage) => { + trans.run( + `INSERT INTO nntp_area_message (nntp_message_id, message_id, message_area_tag, message_uuid) + VALUES (?, ?, ?, ?);`, + [ newMessage.index, newMessage.messageId, areaTag, newMessage.messageUuid ], + err => { + return nextNewMessage(err); + } + ); + }, + err => { + if(err) { + return trans.rollback( () => { + return callback(err); + }); + } + + trans.commit( () => { + messageList.push(...newMessageList.map(m => { + return { + areaTag, + index : m.nntpMessageId, + messageUuid : m.messageUuid, + }; + })); + + return callback(null, messageList); + }); + }); + }); + }); + } + ], + (err, messageList) => { + return cb(err, messageList); + } + ); + } + _buildHead(session, message) { return _.map(message.nntpHeaders, (v, k) => `${k}: ${v}`).join('\r\n'); } @@ -617,6 +765,7 @@ class NNTPServer extends NNTPServerBase { } getMessageIdentifier(message) { + // note that we use the *real* message ID here, not the NNTP-specific index. return this.makeMessageIdentifier(message.messageId, message.messageUuid); } @@ -727,9 +876,9 @@ exports.getModule = class NNTPServerModule extends ServerModule { return true; } - createServer() { + createServer(cb) { if(!this.isEnabled() || !this.isConfigured()) { - return; + return cb(null); } const config = Config(); @@ -762,6 +911,11 @@ exports.getModule = class NNTPServerModule extends ServerModule { 'NTTPS' ); } + + nntpDatabase = new NNTPDatabase(); + nntpDatabase.init(err => { + return cb(err); + }); } listen() { @@ -785,3 +939,53 @@ exports.getModule = class NNTPServerModule extends ServerModule { return `${service}://0.0.0.0:${port}`; } }; + +function performMaintenanceTask(args, cb) { + // + // Delete any message mapping that no longer have + // an actual message associated with them. + // + if(!nntpDatabase) { + Log.trace('Cannot perform NNTP maintenance without NNTP database initialized'); + return cb(null); + } + + let attached = false; + asyncSeries( + [ + (callback) => { + const messageDbPath = paths.join(Config().paths.db, 'message.sqlite3'); + nntpDatabase.db.run( + `ATTACH DATABASE "${messageDbPath}" AS msgdb;`, + err => { + attached = !err; + return callback(err); + } + ); + }, + (callback) => { + nntpDatabase.db.run( + `DELETE FROM nntp_area_message + WHERE message_uuid NOT IN ( + SELECT message_uuid + FROM msgdb.message + );`, + function result(err) { // no arrow func; need |this.changes| + if(err) { + Log.warn( { error : err.message }, 'Failed to delete from NNTP database'); + } else { + Log.debug( { count : this.changes }, 'Deleted mapped message IDs from NNTP database'); + } + return callback(err); + } + ); + } + ], + err => { + if(attached) { + nntpDatabase.db.run('DETACH DATABASE msgdb;'); + } + return cb(err); + } + ); +} \ No newline at end of file diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 7088b8f9..f0e0d903 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -104,7 +104,7 @@ exports.getModule = class WebServerModule extends ServerModule { return this.enableHttp || this.enableHttps; } - createServer() { + createServer(cb) { if(this.enableHttp) { this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) ); } @@ -121,6 +121,8 @@ exports.getModule = class WebServerModule extends ServerModule { this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) ); } + + return cb(null); } listen() { diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 27b5aed9..263a3929 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -288,10 +288,10 @@ exports.getModule = class SSHServerModule extends LoginServerModule { super(); } - createServer() { + createServer(cb) { const config = Config(); if(true != config.loginServers.ssh.enabled) { - return; + return cb(null); } const serverConf = { @@ -318,6 +318,8 @@ exports.getModule = class SSHServerModule extends LoginServerModule { Log.info(info, 'New SSH connection'); this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo); }); + + return cb(null); } listen() { diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 1f8afa6a..ae1ecbe9 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -852,7 +852,7 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { super(); } - createServer() { + createServer(cb) { this.server = net.createServer( sock => { const client = new TelnetClient(sock, sock); @@ -876,6 +876,8 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { this.server.on('error', err => { Log.info( { error : err.message }, 'Telnet server error'); }); + + return cb(null); } listen() { diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index 43245e74..b27aaf6d 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -123,7 +123,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { super(); } - createServer() { + createServer(cb) { // // We will actually create up to two servers: // * insecure websocket (ws://) @@ -131,7 +131,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { // const config = _.get(Config(), 'loginServers.webSocket'); if(!_.isObject(config)) { - return; + return cb(null); } const wsPort = _.get(config, 'ws.port'); @@ -161,6 +161,8 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { wsServer : new WebSocketServer( { server : httpServer } ), }; } + + return cb(null); } listen() { From 3864d957c93e63c8202900e7c6fa7817701ff72c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 27 Dec 2018 02:46:16 -0700 Subject: [PATCH 473/569] * Servers now use async listen() --- core/listening_server.js | 26 ++++++++------ core/servers/content/gopher.js | 9 ++--- core/servers/content/nntp.js | 15 +++++--- core/servers/content/web.js | 26 ++++++++------ core/servers/login/ssh.js | 15 ++++---- core/servers/login/telnet.js | 14 +++++--- core/servers/login/websocket.js | 61 +++++++++++++++++++-------------- 7 files changed, 100 insertions(+), 66 deletions(-) diff --git a/core/listening_server.js b/core/listening_server.js index 8743d6ce..aa573fa1 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -2,11 +2,10 @@ 'use strict'; // ENiGMA½ -const logger = require('./logger.js'); -const { ErrorReasons } = require('./enig_error.js'); +const logger = require('./logger.js'); // deps -const async = require('async'); +const async = require('async'); const listeningServers = {}; // packageName -> info @@ -34,15 +33,22 @@ function startListening(cb) { const moduleInst = new module.getModule(); try { moduleInst.createServer(err => { - if(!moduleInst.listen()) { - throw new Error('Failed listening'); + if(err) { + return nextModule(err); } - listeningServers[module.moduleInfo.packageName] = { - instance : moduleInst, - info : module.moduleInfo, - }; - return nextModule(err); + moduleInst.listen( err => { + if(err) { + return nextModule(err); + } + + listeningServers[module.moduleInfo.packageName] = { + instance : moduleInst, + info : module.moduleInfo, + }; + + return nextModule(null); + }); }); } catch(e) { logger.log.error(e, 'Exception caught creating server!'); diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 7553793e..ee889b8c 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -5,6 +5,7 @@ const Log = require('../../logger.js').log; const { ServerModule } = require('../../server_module.js'); const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); const { splitTextAtTerms, isAnsi, @@ -100,19 +101,19 @@ exports.getModule = class GopherModule extends ServerModule { return cb(null); } - listen() { + listen(cb) { if(!this.enabled) { - return true; // nothing to do, but not an error + return cb(null); } const config = Config(); const port = parseInt(config.contentServers.gopher.port); if(isNaN(port)) { this.log.warn( { port : config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); - return false; + return cb(Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`)); } - return this.server.listen(port); + return this.server.listen(port, cb); } get enabled() { diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 7f20cba7..41dda2e8 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -918,21 +918,26 @@ exports.getModule = class NNTPServerModule extends ServerModule { }); } - listen() { + listen(cb) { const config = Config(); - [ 'nntp', 'nntps' ].forEach( service => { + forEachSeries([ 'nntp', 'nntps' ], (service, nextService) => { const server = this[`${service}Server`]; if(server) { const port = config.contentServers.nntp[service].port; server.listen(this.listenURI(port, service)) .catch(e => { Log.warn( { error : e.message, port }, `${service.toUpperCase()} failed to listen`); + return nextService(null); // try next anyway + }).then( () => { + return nextService(null); }); + } else { + return nextService(null); } + }, + err => { + return cb(err); }); - - // :TODO: listen() needs to be async. I always should have been... - return true; } listenURI(port, service = 'nntp') { diff --git a/core/servers/content/web.js b/core/servers/content/web.js index f0e0d903..1a09aace 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -5,6 +5,7 @@ const Log = require('../../logger.js').log; const ServerModule = require('../../server_module.js').ServerModule; const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); // deps const http = require('http'); @@ -13,6 +14,7 @@ const _ = require('lodash'); const fs = require('graceful-fs'); const paths = require('path'); const mimeTypes = require('mime-types'); +const forEachSeries = require('async/forEachSeries'); const ModuleInfo = exports.moduleInfo = { name : 'Web', @@ -125,23 +127,27 @@ exports.getModule = class WebServerModule extends ServerModule { return cb(null); } - listen() { - let ok = true; - + listen(cb) { const config = Config(); - [ 'http', 'https' ].forEach(service => { + forEachSeries([ 'http', 'https' ], (service, nextService) => { const name = `${service}Server`; if(this[name]) { const port = parseInt(config.contentServers.web[service].port); if(isNaN(port)) { - ok = false; - return Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); + Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); + return nextService(Errors.Invalid(`Invalid port: ${config.contentServers.web[service].port}`)); } - return this[name].listen(port); - } - }); - return ok; + this[name].listen(port, err => { + return nextService(err); + }); + } else { + return nextService(null); + } + }, + err => { + return cb(err); + }); } addRoute(route) { diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 263a3929..d67bbafa 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -322,20 +322,23 @@ exports.getModule = class SSHServerModule extends LoginServerModule { return cb(null); } - listen() { + listen(cb) { const config = Config(); if(true != config.loginServers.ssh.enabled) { - return true; // no server, but not an error + return cb(null); } const port = parseInt(config.loginServers.ssh.port); if(isNaN(port)) { Log.error( { server : ModuleInfo.name, port : config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); - return false; + return cb(Errors.Invalid(`Invalid port: ${config.loginServers.ssh.port}`)); } - this.server.listen(port); - Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); - return true; + this.server.listen(port, err => { + if(!err) { + Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + } + return cb(err); + }); } }; diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index ae1ecbe9..ff21b602 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -8,6 +8,7 @@ const LoginServerModule = require('../../login_server_module.js'); const Config = require('../../config.js').get; const EnigAssert = require('../../enigma_assert.js'); const { stringFromNullTermBuffer } = require('../../string_util.js'); +const { Errors } = require('../../enig_error.js'); // deps const net = require('net'); @@ -880,16 +881,19 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { return cb(null); } - listen() { + listen(cb) { const config = Config(); const port = parseInt(config.loginServers.telnet.port); if(isNaN(port)) { Log.error( { server : ModuleInfo.name, port : config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); - return false; + return cb(Errors.Invalid(`Invalid port: ${config.loginServers.telnet.port}`)); } - this.server.listen(port); - Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); - return true; + this.server.listen(port, err => { + if(!err) { + Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + } + return cb(err); + }); } }; diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index b27aaf6d..35bc0757 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -6,6 +6,7 @@ const Config = require('../../config.js').get; const TelnetClient = require('./telnet.js').TelnetClient; const Log = require('../../logger.js').log; const LoginServerModule = require('../../login_server_module.js'); +const { Errors } = require('../../enig_error.js'); // deps const _ = require('lodash'); @@ -14,6 +15,7 @@ const http = require('http'); const https = require('https'); const fs = require('graceful-fs'); const Writable = require('stream'); +const forEachSeries = require('async/forEachSeries'); const ModuleInfo = exports.moduleInfo = { name : 'WebSocket', @@ -165,31 +167,7 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { return cb(null); } - listen() { - WSS_SERVER_TYPES.forEach(serverType => { - const server = this[serverType]; - if(!server) { - return; - } - - const serverName = `${ModuleInfo.name} (${serverType})`; - const port = parseInt(_.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] )); - - if(isNaN(port)) { - Log.error( { server : serverName, port : port }, 'Cannot load server (invalid port)' ); - return; - } - - server.httpServer.listen(port); - - server.wsServer.on('connection', (ws, req) => { - const webSocketClient = new WebSocketClient(ws, req, serverType); - this.handleNewClient(webSocketClient, webSocketClient.socketBridge, ModuleInfo); - }); - - Log.info( { server : serverName, port : port }, 'Listening for connections' ); - }); - + listen(cb) { // // Send pings every 30s // @@ -215,7 +193,38 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { }); }, 30000); - return true; + forEachSeries(WSS_SERVER_TYPES, (serverType, nextServerType) => { + const server = this[serverType]; + if(!server) { + return nextServerType(null); + } + + const serverName = `${ModuleInfo.name} (${serverType})`; + const confPort = _.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws', 'port' ] ); + const port = parseInt(confPort); + + if(isNaN(port)) { + Log.error( { server : serverName, port : confPort }, 'Cannot load server (invalid port)' ); + return nextServerType(Errors.Invalid(`Invalid port: ${confPort}`)); + } + + server.httpServer.listen(port, err => { + if(err) { + return nextServerType(err); + } + + server.wsServer.on('connection', (ws, req) => { + const webSocketClient = new WebSocketClient(ws, req, serverType); + this.handleNewClient(webSocketClient, webSocketClient.socketBridge, ModuleInfo); + }); + + Log.info( { server : serverName, port : port }, 'Listening for connections' ); + return nextServerType(null); + }); + }, + err => { + cb(err); + }); } webSocketConnection(conn) { From 8d46a305c51216f4e39e9887588ddf301cd9b745 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 27 Dec 2018 14:16:59 -0700 Subject: [PATCH 474/569] Add IP address to failed login attempts --- core/user_login.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/user_login.js b/core/user_login.js index 395c446d..3e3f5f04 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -26,7 +26,7 @@ function userLogin(client, username, password, cb) { const config = Config(); if(config.users.badUserNames.includes(username.toLowerCase())) { - client.log.info( { username : username }, 'Attempt to login with banned username'); + client.log.info( { username, ip : client.remoteAddress }, 'Attempt to login with banned username'); // slow down a bit to thwart brute force attacks return setTimeout( () => { @@ -42,7 +42,7 @@ function userLogin(client, username, password, cb) { err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany); } - client.log.info( { username : username, error : err.message }, 'Failed login attempt'); + client.log.info( { username, ip : client.remoteAddress, reason : err.message }, 'Failed login attempt'); return cb(err); } From 046550842b897cc4e9b4d08514d4bb2d70f29137 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 28 Dec 2018 10:39:41 -0700 Subject: [PATCH 475/569] ACS changes in prep for checking from ie content servers such as NNTP * ACS now takes subject { client, user } * ACS checks now consider client/user optional & checks fail (return false) if an object is not available --- core/acs.js | 10 +++--- core/acs_parser.js | 78 ++++++++++++++++++++++++++++++++----------- core/client.js | 2 +- misc/acs_parser.pegjs | 78 ++++++++++++++++++++++++++++++++----------- 4 files changed, 124 insertions(+), 44 deletions(-) diff --git a/core/acs.js b/core/acs.js index a86db329..1ae4fa93 100644 --- a/core/acs.js +++ b/core/acs.js @@ -10,15 +10,15 @@ const assert = require('assert'); const _ = require('lodash'); class ACS { - constructor(client) { - this.client = client; + constructor(subject) { + this.subject = subject; } check(acs, scope, defaultAcs) { acs = acs ? acs[scope] : defaultAcs; acs = acs || defaultAcs; try { - return checkAcs(acs, { client : this.client } ); + return checkAcs(acs, { subject : this.subject } ); } catch(e) { Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); return false; @@ -57,7 +57,7 @@ class ACS { return true; // no ACS check req. } try { - return checkAcs(acs, { client : this.client } ); + return checkAcs(acs, { subject : this.subject } ); } catch(e) { Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); return false; @@ -75,7 +75,7 @@ class ACS { const matchCond = condArray.find( cond => { if(_.has(cond, 'acs')) { try { - return checkAcs(cond.acs, { client : this.client } ); + return checkAcs(cond.acs, { subject : this.subject } ); } catch(e) { Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS'); return false; diff --git a/core/acs_parser.js b/core/acs_parser.js index 6e32cb06..d6983b17 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -844,32 +844,36 @@ function peg$parse(input, options) { } - const client = options.client; - const user = options.client.user; - const UserProps = require('./user_property.js'); + const Log = require('./logger.js').log; + const _ = require('lodash'); const moment = require('moment'); + const client = _.get(options, 'subject.client'); + const user = _.get(options, 'subject.user'); + function checkAccess(acsCode, value) { try { return { LC : function isLocalConnection() { - return client.isLocal(); + return client && client.isLocal(); }, AG : function ageGreaterOrEqualThan() { - return !isNaN(value) && user.getAge() >= value; + return !isNaN(value) && user && user.getAge() >= value; }, AS : function accountStatus() { + if(!user) { + return false; + } if(!Array.isArray(value)) { value = [ value ]; } - const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus); return value.map(n => parseInt(n, 10)).includes(userAccountStatus); }, EC : function isEncoding() { - const encoding = client.term.outputEncoding.toLowerCase(); + const encoding = _.get(client, 'term.outputEncoding', '').toLowerCase(); switch(value) { case 0 : return 'cp437' === encoding; case 1 : return 'utf-8' === encoding; @@ -877,27 +881,41 @@ function peg$parse(input, options) { } }, GM : function isOneOfGroups() { + if(!user) { + return false; + } if(!Array.isArray(value)) { return false; } - return value.some(groupName => user.isGroupMember(groupName)); }, NN : function isNode() { + if(!client) { + return false; + } if(!Array.isArray(value)) { value = [ value ]; } return value.map(n => parseInt(n, 10)).includes(client.node); }, NP : function numberOfPosts() { + if(!user) { + return false; + } const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; return !isNaN(value) && postCount >= value; }, NC : function numberOfCalls() { + if(!user) { + return false; + } const loginCount = user.getPropertyAsNumber(UserProps.LoginCount); return !isNaN(value) && loginCount >= value; }, AA : function accountAge() { + if(!user) { + return false; + } const accountCreated = moment(user.getProperty(UserProps.AccountCreated)); const now = moment(); const daysOld = accountCreated.diff(moment(), 'days'); @@ -907,78 +925,98 @@ function peg$parse(input, options) { daysOld >= value; }, BU : function bytesUploaded() { + if(!user) { + return false; + } const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; return !isNaN(value) && bytesUp >= value; }, UP : function uploads() { + if(!user) { + return false; + } const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; return !isNaN(value) && uls >= value; }, BD : function bytesDownloaded() { + if(!user) { + return false; + } const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; return !isNaN(value) && bytesDown >= value; }, DL : function downloads() { + if(!user) { + return false; + } const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; return !isNaN(value) && dls >= value; }, NR : function uploadDownloadRatioGreaterThan() { + if(!user) { + return false; + } const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; const ratio = ~~((ulCount / dlCount) * 100); return !isNaN(value) && ratio >= value; }, KR : function uploadDownloadByteRatioGreaterThan() { + if(!user) { + return false; + } const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; const ratio = ~~((ulBytes / dlBytes) * 100); return !isNaN(value) && ratio >= value; }, - PC : function postCallRatio() { + PC : function postCallRatio() { + if(!user) { + return false; + } const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0; const ratio = ~~((postCount / loginCount) * 100); return !isNaN(value) && ratio >= value; }, SC : function isSecureConnection() { - return client.session.isSecure; + return _.get(client, 'session.isSecure', false); }, ML : function minutesLeft() { // :TODO: implement me! return false; }, TH : function termHeight() { - return !isNaN(value) && client.term.termHeight >= value; + return !isNaN(value) && _.get(client, 'term.termHeight', 0) >= value; }, TM : function isOneOfThemes() { if(!Array.isArray(value)) { return false; } - - return value.includes(client.currentTheme.name); + return value.includes(_.get(client, 'currentTheme.name')); }, TT : function isOneOfTermTypes() { if(!Array.isArray(value)) { return false; } - - return value.includes(client.term.termType); + return value.includes(_.get(client, 'term.termType')); }, TW : function termWidth() { - return !isNaN(value) && client.term.termWidth >= value; + return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; }, ID : function isUserId(value) { + if(!user) { + return false; + } if(!Array.isArray(value)) { value = [ value ]; } - return value.map(n => parseInt(n, 10)).includes(user.userId); }, WD : function isOneOfDayOfWeek() { if(!Array.isArray(value)) { value = [ value ]; } - return value.map(n => parseInt(n, 10)).includes(new Date().getDay()); }, MM : function isMinutesPastMidnight() { @@ -989,7 +1027,9 @@ function peg$parse(input, options) { } }[acsCode](value); } catch (e) { - client.log.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!'); + const logger = _.get(client, 'log', Log); + logger.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!'); + return false; } } diff --git a/core/client.js b/core/client.js index 60a04af7..300285a3 100644 --- a/core/client.js +++ b/core/client.js @@ -85,7 +85,7 @@ function Client(/*input, output*/) { this.currentTheme = { info : { name : 'N/A', description : 'None' } }; this.lastKeyPressMs = Date.now(); this.menuStack = new MenuStack(this); - this.acs = new ACS(this); + this.acs = new ACS( { client : this, user : this.user } ); this.mciCache = {}; this.interruptQueue = new UserInterruptQueue(this); diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs index ed6089ba..bd6a8d96 100644 --- a/misc/acs_parser.pegjs +++ b/misc/acs_parser.pegjs @@ -1,31 +1,35 @@ { - const client = options.client; - const user = options.client.user; - const UserProps = require('./user_property.js'); + const Log = require('./logger.js').log; + const _ = require('lodash'); const moment = require('moment'); + const client = _.get(options, 'subject.client'); + const user = _.get(options, 'subject.user'); + function checkAccess(acsCode, value) { try { return { LC : function isLocalConnection() { - return client.isLocal(); + return client && client.isLocal(); }, AG : function ageGreaterOrEqualThan() { - return !isNaN(value) && user.getAge() >= value; + return !isNaN(value) && user && user.getAge() >= value; }, AS : function accountStatus() { + if(!user) { + return false; + } if(!Array.isArray(value)) { value = [ value ]; } - const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus); return value.map(n => parseInt(n, 10)).includes(userAccountStatus); }, EC : function isEncoding() { - const encoding = client.term.outputEncoding.toLowerCase(); + const encoding = _.get(client, 'term.outputEncoding', '').toLowerCase(); switch(value) { case 0 : return 'cp437' === encoding; case 1 : return 'utf-8' === encoding; @@ -33,27 +37,41 @@ } }, GM : function isOneOfGroups() { + if(!user) { + return false; + } if(!Array.isArray(value)) { return false; } - return value.some(groupName => user.isGroupMember(groupName)); }, NN : function isNode() { + if(!client) { + return false; + } if(!Array.isArray(value)) { value = [ value ]; } return value.map(n => parseInt(n, 10)).includes(client.node); }, NP : function numberOfPosts() { + if(!user) { + return false; + } const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; return !isNaN(value) && postCount >= value; }, NC : function numberOfCalls() { + if(!user) { + return false; + } const loginCount = user.getPropertyAsNumber(UserProps.LoginCount); return !isNaN(value) && loginCount >= value; }, AA : function accountAge() { + if(!user) { + return false; + } const accountCreated = moment(user.getProperty(UserProps.AccountCreated)); const now = moment(); const daysOld = accountCreated.diff(moment(), 'days'); @@ -63,78 +81,98 @@ daysOld >= value; }, BU : function bytesUploaded() { + if(!user) { + return false; + } const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; return !isNaN(value) && bytesUp >= value; }, UP : function uploads() { + if(!user) { + return false; + } const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; return !isNaN(value) && uls >= value; }, BD : function bytesDownloaded() { + if(!user) { + return false; + } const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; return !isNaN(value) && bytesDown >= value; }, DL : function downloads() { + if(!user) { + return false; + } const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; return !isNaN(value) && dls >= value; }, NR : function uploadDownloadRatioGreaterThan() { + if(!user) { + return false; + } const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; const ratio = ~~((ulCount / dlCount) * 100); return !isNaN(value) && ratio >= value; }, KR : function uploadDownloadByteRatioGreaterThan() { + if(!user) { + return false; + } const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; const ratio = ~~((ulBytes / dlBytes) * 100); return !isNaN(value) && ratio >= value; }, - PC : function postCallRatio() { + PC : function postCallRatio() { + if(!user) { + return false; + } const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0; const ratio = ~~((postCount / loginCount) * 100); return !isNaN(value) && ratio >= value; }, SC : function isSecureConnection() { - return client.session.isSecure; + return _.get(client, 'session.isSecure', false); }, ML : function minutesLeft() { // :TODO: implement me! return false; }, TH : function termHeight() { - return !isNaN(value) && client.term.termHeight >= value; + return !isNaN(value) && _.get(client, 'term.termHeight', 0) >= value; }, TM : function isOneOfThemes() { if(!Array.isArray(value)) { return false; } - - return value.includes(client.currentTheme.name); + return value.includes(_.get(client, 'currentTheme.name')); }, TT : function isOneOfTermTypes() { if(!Array.isArray(value)) { return false; } - - return value.includes(client.term.termType); + return value.includes(_.get(client, 'term.termType')); }, TW : function termWidth() { - return !isNaN(value) && client.term.termWidth >= value; + return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; }, ID : function isUserId(value) { + if(!user) { + return false; + } if(!Array.isArray(value)) { value = [ value ]; } - return value.map(n => parseInt(n, 10)).includes(user.userId); }, WD : function isOneOfDayOfWeek() { if(!Array.isArray(value)) { value = [ value ]; } - return value.map(n => parseInt(n, 10)).includes(new Date().getDay()); }, MM : function isMinutesPastMidnight() { @@ -145,7 +183,9 @@ } }[acsCode](value); } catch (e) { - client.log.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!'); + const logger = _.get(client, 'log', Log); + logger.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!'); + return false; } } From 6b02ddbdae252ac7318e4ade486c9484b84f9bfc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 29 Dec 2018 13:15:58 -0700 Subject: [PATCH 476/569] Fix FILE_ID.DIZ (and other) display issues when ANSI is stored with specific term width in SAUCE + Preserve SAUCE records of desc/long_desc during import (in meta) * Use SAUCE term width for ANSI Prep when displaying desc --- core/file_area_list.js | 27 +++++++++++++++++---------- core/file_base_area.js | 26 +++++++++++++++++++------- core/file_entry.js | 2 ++ core/multi_line_edit_text_view.js | 4 ++-- core/oputil/oputil_file_base.js | 10 ++++++++-- core/sauce.js | 12 +++++++++++- 6 files changed, 59 insertions(+), 22 deletions(-) diff --git a/core/file_area_list.js b/core/file_area_list.js index 35ce3ea6..a62271ca 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -413,16 +413,23 @@ exports.getModule = class FileAreaList extends MenuModule { // const desc = controlCodesToAnsi(self.currentFileEntry.desc); if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { - descView.setAnsi( - desc, - { - prepped : false, - forceLineTerm : true - }, - () => { - return callback(null); - } - ); + const opts = { + prepped : false, + forceLineTerm : true + }; + + // + // if SAUCE states a term width, honor it else we may see + // display corruption + // + const sauceTermWidth = _.get(self.currentFileEntry.meta, 'desc_sauce.Character.characterWidth'); + if(_.isNumber(sauceTermWidth)) { + opts.termWidth = sauceTermWidth; + } + + descView.setAnsi(desc, opts, () => { + return callback(null); + }); } else { descView.setText(self.currentFileEntry.desc); return callback(null); diff --git a/core/file_base_area.js b/core/file_base_area.js index 95040791..44ecb406 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -16,6 +16,7 @@ const wordWrapText = require('./word_wrap.js').wordWrapText; const StatLog = require('./stat_log.js'); const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); +const SAUCE = require('./sauce.js'); // deps const _ = require('lodash'); @@ -386,13 +387,24 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { return next(null); } - // - // Assume FILE_ID.DIZ, NFO files, etc. are CP437. - // - // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... - fileEntry[descType] = iconv.decode(sliceAtSauceMarker(data, 0x1a), 'cp437'); - fileEntry[`${descType}Src`] = 'descFile'; - return next(null); + SAUCE.readSAUCE(data, (err, sauce) => { + if(sauce) { + // if we have SAUCE, this information will be kept as well, + // but separate/pre-parsed. + const metaKey = `desc${'descLong' === descType ? '_long' : ''}_sauce`; + fileEntry.meta[metaKey] = JSON.stringify(sauce); + } + + // + // Assume FILE_ID.DIZ, NFO files, etc. are CP437; we need + // to decode to a native format for storage + // + // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... + const decodedData = iconv.decode(data, 'cp437'); + fileEntry[descType] = sliceAtSauceMarker(decodedData, 0x1a); + fileEntry[`${descType}Src`] = 'descFile'; + return next(null); + }); }); }); }, () => { diff --git a/core/file_entry.js b/core/file_entry.js index 4f5e47d1..0539f137 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -39,6 +39,8 @@ const FILE_WELL_KNOWN_META = { tic_desc : null, // TIC "Desc" tic_ldesc : null, // TIC "Ldesc" joined by '\n' session_temp_dl : (v) => parseInt(v) ? true : false, + desc_sauce : (s) => JSON.parse(s) || {}, + desc_long_sauce : (s) => JSON.parse(s) || {}, }; module.exports = class FileEntry { diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index bd49a73f..9f099b16 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -567,8 +567,8 @@ function MultiLineEditTextView(options) { ansiPrep( ansi, { - termWidth : this.client.term.termWidth, - termHeight : this.client.term.termHeight, + termWidth : options.termWidth || this.client.term.termWidth, + termHeight : options.termHeight || this.client.term.termHeight, cols : this.dimens.width, rows : 'auto', startCol : this.position.col, diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index b12c4d69..f1956cce 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -259,8 +259,10 @@ function scanFileAreaForChanges(areaInfo, options, cb) { if( tagsEq && fileEntry.desc === existingEntry.desc && - fileEntry.descLong == existingEntry.descLong && - fileEntry.meta.est_release_year == existingEntry.meta.est_release_year) + fileEntry.descLong === existingEntry.descLong && + fileEntry.meta.est_release_year === existingEntry.meta.est_release_year && + fileEntry.meta.desc_sauce === existingEntry.meta.desc_sauce + ) { console.info('Dupe'); return nextFile(null); @@ -276,6 +278,10 @@ function scanFileAreaForChanges(areaInfo, options, cb) { existingEntry.meta.est_release_year = fileEntry.meta.est_release_year; } + if(fileEntry.meta.desc_sauce) { + existingEntry.meta.desc_sauce = fileEntry.meta.desc_sauce; + } + updateTags(existingEntry); finalizeEntryAndPersist(true, existingEntry, descHandler, err => { diff --git a/core/sauce.js b/core/sauce.js index 6291c16e..7d5f52fd 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -163,7 +163,7 @@ function parseCharacterSAUCE(sauce) { result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { - // convience: create ansiFlags + // convenience: create ansiFlags sauce.ansiFlags = sauce.flags; let i = 0; @@ -175,6 +175,16 @@ function parseCharacterSAUCE(sauce) { if(fontName.length > 0) { result.fontName = fontName; } + + const setDimen = (v, field) => { + const i = parseInt(v, 10); + if(!isNaN(i)) { + result[field] = i; + } + }; + + setDimen(sauce.tinfo1, 'characterWidth'); + setDimen(sauce.tinfo2, 'characterHeight'); } return result; From 78484a235292cf46013dd14bd27d79c3c6d9d16f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 29 Dec 2018 13:28:08 -0700 Subject: [PATCH 477/569] Compare SAUCE for --update propertly --- core/oputil/oputil_file_base.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index f1956cce..4dc25fd2 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -257,11 +257,16 @@ function scanFileAreaForChanges(areaInfo, options, cb) { const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; const tagsEq = _.isEqual(optTags, existingEntry.hashTags); + let descSauceCompare; + if(existingEntry.meta.desc_sauce) { + descSauceCompare = JSON.stringify(existingEntry.meta.desc_sauce); + } + if( tagsEq && fileEntry.desc === existingEntry.desc && fileEntry.descLong === existingEntry.descLong && fileEntry.meta.est_release_year === existingEntry.meta.est_release_year && - fileEntry.meta.desc_sauce === existingEntry.meta.desc_sauce + fileEntry.meta.desc_sauce === descSauceCompare ) { console.info('Dupe'); From 7450b653fa901a86bd5276a4dca45b0cbd1f64a8 Mon Sep 17 00:00:00 2001 From: FrozenFOXX Date: Sat, 29 Dec 2018 16:00:06 -0600 Subject: [PATCH 478/569] Update docker.md Updated the command to reflect current syntax. --- docs/installation/docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 878c9b33..82e03e5c 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -11,11 +11,11 @@ Download and run the ENiGMA½ BBS image: docker run -d \ -p 8888:8888 \ - davestephens\enigma-bbs + davestephens/enigma-bbs:latest As no config has been supplied the container will use a basic one so that it starts successfully. Note that as no persistence directory has been supplied, once the container stops any changes made will be lost! ## Customised Docker Setup -TBC using Docker Compose \ No newline at end of file +TBC using Docker Compose From 0230d9958c8a2e5e088c710e53c34cf08f48f35b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 31 Dec 2018 11:30:40 -0700 Subject: [PATCH 479/569] Better screen size detection when NAWS/etc. fails: Ask to move cursor to 999,999. We expect that we'll really get something like 80x25 generally. *Then* issue special DSR that should give us screen size. We should get a good "bottom right" aka screen size either way. --- core/connect.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/core/connect.js b/core/connect.js index 0b83f7f0..b9316a3e 100644 --- a/core/connect.js +++ b/core/connect.js @@ -80,12 +80,13 @@ function ansiQueryTermSizeIfNeeded(client, cb) { // // NetRunner for example gives us 1x1 here. Not really useful. Ignore - // values that seem obviously bad. + // values that seem obviously bad. Included in the set is the explicit + // 999x999 values we asked to move to. // - if(h < 10 || w < 10) { + if(h < 10 || h === 999 || w < 10 || w === 999) { client.log.warn( { height : h, width : w }, - 'Ignoring ANSI CPR screen size query response due to very small values'); + 'Ignoring ANSI CPR screen size query response due to non-sane values'); return done(Errors.Invalid('Term size <= 10 considered invalid')); } @@ -111,12 +112,20 @@ function ansiQueryTermSizeIfNeeded(client, cb) { return done(Errors.General('No term size established by CPR within timeout')); }, 2000); - // Start the process: Query for CPR - client.term.rawWrite(ansi.queryScreenSize()); + // Start the process: + // 1 - Ask to goto 999,999 -- a very much "bottom right" (generally 80x25 for example + // is the real size) + // 2 - Query for screen size with bansi.txt style specialized Device Status Report (DSR) + // request. We expect a CPR of: + // a - Terms that support bansi.txt style: Screen size + // b - Terms that do not support bansi.txt style: Since we moved to the bottom right + // we should still be able to determine a screen size. + // + client.term.rawWrite(`${ansi.goto(999, 999)}${ansi.queryScreenSize()}`); } function prepareTerminal(term) { - term.rawWrite(ansi.normal()); + term.rawWrite(`${ansi.normal()}${ansi.clearScreen()}`); } function displayBanner(term) { From 4fb7c4bf532e94db3e8166e224818b3b8ed03364 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 31 Dec 2018 11:33:14 -0700 Subject: [PATCH 480/569] Some minor cleanup --- core/servers/login/telnet.js | 35 ++++++----------------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index ff21b602..29d766ba 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -16,8 +16,6 @@ const buffers = require('buffers'); const { Parser } = require('binary-parser'); const util = require('util'); -//var debug = require('debug')('telnet'); - const ModuleInfo = exports.moduleInfo = { name : 'Telnet', desc : 'Telnet Server', @@ -36,24 +34,7 @@ exports.TelnetClient = TelnetClient; /* TODO: - * Document COMMANDS -- add any missing - * Document OPTIONS -- add any missing - * Internally handle OPTIONS: - * Some should be emitted generically - * Some should be handled internally -- denied, handled, etc. - * - - * Allow term (ttype) to be set by environ sub negotiation - - * Process terms in loop.... research needed - - * Handle will/won't - * Handle do's, .. - * Some won't should close connection - - * Options/Commands we don't understand shouldn't crash the server!! - - + * Various (much lesser used) Telnet command coverage */ const COMMANDS = { @@ -299,10 +280,7 @@ OPTION_IMPLS[OPTIONS.WINDOW_SIZE] = function(bufs, i, event) { }; // Build an array of delimiters for parsing NEW_ENVIRONMENT[_DEP] -const NEW_ENVIRONMENT_DELIMITERS = []; -Object.keys(NEW_ENVIRONMENT_COMMANDS).forEach(function onKey(k) { - NEW_ENVIRONMENT_DELIMITERS.push(NEW_ENVIRONMENT_COMMANDS[k]); -}); +//const NEW_ENVIRONMENT_DELIMITERS = _.values(NEW_ENVIRONMENT_COMMANDS); // Handle the deprecated RFC 1408 & the updated RFC 1572: OPTION_IMPLS[OPTIONS.NEW_ENVIRONMENT_DEP] = @@ -727,10 +705,9 @@ TelnetClient.prototype.handleSbCommand = function(evt) { } }; -const IGNORED_COMMANDS = []; -[ COMMANDS.EL, COMMANDS.GA, COMMANDS.NOP, COMMANDS.DM, COMMANDS.BRK ].forEach(function onCommandCode(cc) { - IGNORED_COMMANDS.push(cc); -}); +const IGNORED_COMMANDS = [ + COMMANDS.EL, COMMANDS.GA, COMMANDS.NOP, COMMANDS.DM, COMMANDS.BRK +]; TelnetClient.prototype.handleMiscCommand = function(evt) { @@ -750,7 +727,7 @@ TelnetClient.prototype.handleMiscCommand = function(evt) { this.log.debug('Are You There (AYT) - Replied "\\b"'); } else if(IGNORED_COMMANDS.indexOf(evt.commandCode)) { - this.log.debug({ evt : evt }, 'Ignoring command'); + this.log.trace({ command : evt.command, commandCode : evt.commandCode }, 'Ignoring command'); } else { this.log.warn({ evt : evt }, 'Unknown command'); } From b23cdd20bfea362c947c31e1ebeb20e971f02e04 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 2 Jan 2019 19:52:15 -0700 Subject: [PATCH 481/569] Listen to 'env' events --- core/servers/login/ssh.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index d67bbafa..c05e9294 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -238,6 +238,14 @@ function SSHClient(clientConn) { } }); + session.on('env', (accept, reject, info) => { + self.log.debug(info, 'SSH env event'); + + if(_.isFunction(accept)) { + accept(); + } + }); + session.on('shell', accept => { self.log.debug('SSH shell event'); From eaace9a81d797971cd2c3621f76d6228f24a73c0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 2 Jan 2019 20:07:46 -0700 Subject: [PATCH 482/569] TODO notes --- core/scanner_tossers/ftn_bso.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 47184ea9..3fec4fc3 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -1677,7 +1677,7 @@ function FTNMessageScanTossModule() { localAreaTags : self.getLocalAreaTagsForTic(), }; - return ticFileInfo.validate(config, (err, localInfo) => { + ticFileInfo.validate(config, (err, localInfo) => { if(err) { Log.trace( { reason : err.message }, 'Validation failure'); return callback(err); @@ -1893,7 +1893,7 @@ function FTNMessageScanTossModule() { ], (err, localInfo) => { if(err) { - Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed import/update TIC record' ); + Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed to import/update TIC' ); } else { Log.info( { tic : ticFileInfo.path, file : ticFileInfo.filePath, area : localInfo.areaTag }, @@ -2109,6 +2109,8 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD async.eachSeries(ticFilesInfo, (ticFileInfo, nextTicInfo) => { self.processSingleTicFile(ticFileInfo, err => { if(err) { + // :TODO: If ENOENT -OR- failed due to CRC mismatch: create a pending state & try again later; the "attached" file may not yet be ready. + // archive rejected TIC stuff (.TIC + attach) async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { if(!path) { // possibly rejected due to "File" not existing/etc. From c5a72c7356cfbafed32f2777f324cb05837e6461 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 2 Jan 2019 20:08:00 -0700 Subject: [PATCH 483/569] TODO notes --- core/servers/content/nntp.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 41dda2e8..4959dc64 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -143,6 +143,7 @@ class NNTPServer extends NNTPServerBase { const user = new User(); user.authenticate(username, password, err => { if(err) { + // :TODO: Log IP address this.log.debug( { username, reason : err.message }, 'Authentication failure'); return resolve(false); } From a34dab6a73326c9241dbb702654a50c7497a0862 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 2 Jan 2019 22:13:42 -0700 Subject: [PATCH 484/569] WIP on user achievements * Hook up events for testing * Start to plug in experimental interrupt --- core/achievement.js | 103 ++++++++++++++++++++++++++++++++++++++++++ core/config.js | 35 ++++++++++++++ core/database.js | 12 +++++ core/stat_log.js | 7 ++- core/system_events.js | 1 + 5 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 core/achievement.js diff --git a/core/achievement.js b/core/achievement.js new file mode 100644 index 00000000..bb11e3c4 --- /dev/null +++ b/core/achievement.js @@ -0,0 +1,103 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const Events = require('./events.js'); +const Config = require('./config.js').get; +const UserDb = require('./database.js').dbs.user; +const UserInterruptQueue = require('./user_interrupt_queue.js'); +const { + getConnectionByUserId +} = require('./client_connections.js'); + +// deps +const _ = require('lodash'); + +class Achievements { + constructor(events) { + this.events = events; + } + + init(cb) { + this.monitorUserStatUpdateEvents(); + return cb(null); + } + + loadAchievementHitCount(user, achievementTag, field, value, cb) { + UserDb.get( + `SELECT COUNT() AS count + FROM user_achievement + WHERE user_id = ? AND achievement_tag = ? AND match_field = ? AND match_value >= ?;`, + [ user.userId, achievementTag, field, value ], + (err, row) => { + return cb(err, row && row.count || 0); + } + ); + } + + monitorUserStatUpdateEvents() { + this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => { + const statValue = parseInt(userStatEvent.statValue, 10); + if(isNaN(statValue)) { + return; + } + + const config = Config(); + const achievementTag = _.findKey( + _.get(config, 'userAchievements.achievements', {}), + achievement => { + if(false === achievement.enabled) { + return false; + } + return 'userStat' === achievement.type && + achievement.statName === userStatEvent.statName; + } + ); + + if(!achievementTag) { + return; + } + + const achievement = config.userAchievements.achievements[achievementTag]; + let matchValue = Object.keys(achievement.match || {}).sort( (a, b) => b - a).find(v => statValue >= v); + if(matchValue) { + const match = achievement.match[matchValue]; + + // + // Check if we've triggered this event before + // + this.loadAchievementHitCount(userStatEvent.user, achievementTag, null, matchValue, (err, count) => { + if(count > 0) { + return; + } + + const conn = getConnectionByUserId(userStatEvent.user.userId); + if(!conn) { + return; + } + + const interruptItem = { + text : match.text, + pause : true, + }; + + UserInterruptQueue.queue(interruptItem, { omit : conn} ); + }); + } + }); + } +} + +let achievements; + +exports.moduleInitialize = (initInfo, cb) => { + + if(false === _.get(Config(), 'userAchievements.enabled')) { + // :TODO: Log disabled + return cb(null); + } + + achievements = new Achievements(initInfo.events); + return achievements.init(cb); +}; + diff --git a/core/config.js b/core/config.js index 9c9c4cd4..f854e2f5 100644 --- a/core/config.js +++ b/core/config.js @@ -1003,6 +1003,41 @@ function getDefaultConfig() { systemEvents : { loginHistoryMax: -1, // set to -1 for forever } + }, + + userAchievements : { + enabled : true, + + artHeader : 'achievement_header', + artFooter : 'achievement_footer', + + achievements : { + user_login_count : { + type : 'userStat', + statName : 'login_count', + retroactive : true, + match : { + 10 : { + title : 'Return Caller', + globalText : '{userName} has logged in {statValue} times!', + text : 'You\'ve logged in {statValue} times!', + points : 5, + }, + 25 : { + title : 'Seems To Like It!', + globalText : '{userName} has logged in {statValue} times!', + text : 'You\'ve logged in {statValue} times!', + points : 10, + }, + 100 : { + title : '{boardName} Addict', + globalText : '{userName} the BBS {boardName} addict has logged in {statValue} times!', + text : 'You\'re a {boardName} addict! You\'ve logged in {statValue} times!', + points : 10, + } + } + } + } } }; } diff --git a/core/database.js b/core/database.js index 040cc1de..4cf2513c 100644 --- a/core/database.js +++ b/core/database.js @@ -189,6 +189,18 @@ const DB_INIT_TABLE = { );` ); + dbs.user.run( + `CREATE TABLE IF NOT EXISTS user_achievement ( + user_id INTEGER NOT NULL, + achievement_tag VARCHAR NOT NULL, + timestamp DATETIME NOT NULL, + match_field VARCHAR NOT NULL, + match_value VARCHAR NOT NULL, + UNIQUE(user_id, achievement_tag, match_field, match_value), + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE + );` + ); + return cb(null); }, diff --git a/core/stat_log.js b/core/stat_log.js index 6cf6198b..ffe099ae 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -120,11 +120,14 @@ class StatLog { // // User specific stats - // These are simply convience methods to the user's properties + // These are simply convenience methods to the user's properties // setUserStat(user, statName, statValue, cb) { // note: cb is optional in PersistUserProperty - return user.persistProperty(statName, statValue, cb); + user.persistProperty(statName, statValue, cb); + + const Events = require('./events.js'); // we need to late load currently + return Events.emit(Events.getSystemEvents().UserStatUpdate, { user, statName, statValue } ); } getUserStat(user, statName) { diff --git a/core/system_events.js b/core/system_events.js index 0f8118a2..80612195 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -21,4 +21,5 @@ module.exports = { UserSendMail : 'codes.l33t.enigma.system.user_send_mail', UserRunDoor : 'codes.l33t.enigma.system.user_run_door', UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', + UserStatUpdate : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } }; From 9f728a2e94ba17a59063bf072e41833b2f81b909 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 3 Jan 2019 21:02:21 -0700 Subject: [PATCH 485/569] Fix longstanding bug with node IDs --- core/client_connections.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index 558d0a0f..33f1df8a 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -60,12 +60,25 @@ function getActiveConnectionList(authUsersOnly) { } function addNewClient(client, clientSock) { - const id = client.session.id = clientConnections.push(client) - 1; - const remoteAddress = client.remoteAddress = clientSock.remoteAddress; + // + // Assign ID/client ID to next lowest & available # + // + let id = 0; + for(let i = 0; i < clientConnections.length; ++i) { + if(clientConnections[i].id > id) { + break; + } + id++; + } + client.session.id = id; + const remoteAddress = client.remoteAddress = clientSock.remoteAddress; // create a unique identifier one-time ID for this session client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ id, moment().valueOf() ]); + clientConnections.push(client); + clientConnections.sort( (c1, c2) => c1.session.id - c2.session.id); + // Create a client specific logger // Note that this will be updated @ login with additional information client.log = logger.log.child( { clientId : id, sessionId : client.session.uniqueId } ); From ea055ab58bf35994d20feeba098979d3659ad204 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 3 Jan 2019 21:02:42 -0700 Subject: [PATCH 486/569] Handle pause for text-only interruptions also --- core/user_interrupt_queue.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index 2e72bbd1..29a52685 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -81,18 +81,28 @@ module.exports = class UserInterruptQueue this.client.term.rawWrite('\r\n\r\n'); } + const maybePauseAndFinish = () => { + if(interruptItem.pause) { + this.client.currentMenuModule.pausePrompt( () => { + return cb(null); + }); + } else { + return cb(null); + } + }; + if(interruptItem.contents) { Art.display(this.client, interruptItem.contents, err => { if(err) { return cb(err); } //this.client.term.rawWrite('\r\n\r\n'); // :TODO: Prob optional based on contents vs text - this.client.currentMenuModule.pausePrompt( () => { - return cb(null); - }); + maybePauseAndFinish(); }); } else { - return this.client.term.write(pipeToAnsi(`${interruptItem.text}\r\n\r\n`, this.client), cb); + this.client.term.write(pipeToAnsi(`${interruptItem.text}\r\n\r\n`, this.client), true, () => { + maybePauseAndFinish(); + }); } } }; \ No newline at end of file From bd03d7a79bf45df0a3603659ebfdcb1f831b6be5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 3 Jan 2019 21:02:57 -0700 Subject: [PATCH 487/569] Fix comment --- core/login_server_module.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/login_server_module.js b/core/login_server_module.js index e5fccb39..8ba1d978 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -37,8 +37,8 @@ module.exports = class LoginServerModule extends ServerModule { handleNewClient(client, clientSock, modInfo) { // - // Start tracking the client. We'll assign it an ID which is - // just the index in our connections array. + // Start tracking the client. A session ID aka client ID + // will be established in addNewClient() below. // if(_.isUndefined(client.session)) { client.session = {}; From 10517b1060073df954569e10ae62f983b99d31d0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 3 Jan 2019 21:03:08 -0700 Subject: [PATCH 488/569] Progress on achivements * Fetch art if available * Queue local and/or global interrupts * Apply text formatting * Bug exists with interruptions in certain scenarios that needs worked out --- core/achievement.js | 155 +++++++++++++++++++++++++++++++++++++++----- core/config.js | 20 +++--- 2 files changed, 149 insertions(+), 26 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index bb11e3c4..46f97e03 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -9,9 +9,16 @@ const UserInterruptQueue = require('./user_interrupt_queue.js'); const { getConnectionByUserId } = require('./client_connections.js'); +const UserProps = require('./user_property.js'); +const { Errors } = require('./enig_error.js'); +const { getThemeArt } = require('./theme.js'); +const { pipeToAnsi } = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); // deps const _ = require('lodash'); +const async = require('async'); +const moment = require('moment'); class Achievements { constructor(events) { @@ -61,31 +68,143 @@ class Achievements { const achievement = config.userAchievements.achievements[achievementTag]; let matchValue = Object.keys(achievement.match || {}).sort( (a, b) => b - a).find(v => statValue >= v); if(matchValue) { - const match = achievement.match[matchValue]; + const details = achievement.match[matchValue]; + matchValue = parseInt(matchValue); - // - // Check if we've triggered this event before - // - this.loadAchievementHitCount(userStatEvent.user, achievementTag, null, matchValue, (err, count) => { - if(count > 0) { - return; - } + async.series( + [ + (callback) => { + this.loadAchievementHitCount(userStatEvent.user, achievementTag, null, matchValue, (err, count) => { + if(err) { + return callback(err); + } + return callback(count > 0 ? Errors.General('Achievement already acquired') : null); + }); + }, + (callback) => { + const client = getConnectionByUserId(userStatEvent.user.userId); + if(!client) { + return callback(Errors.UnexpectedState('Failed to get client for user ID')); + } - const conn = getConnectionByUserId(userStatEvent.user.userId); - if(!conn) { - return; - } + const info = { + achievement, + details, + client, + value : matchValue, + user : userStatEvent.user, + timestamp : moment(), + }; - const interruptItem = { - text : match.text, - pause : true, - }; + this.createAchievementInterruptItems(info, (err, interruptItems) => { + if(err) { + return callback(err); + } - UserInterruptQueue.queue(interruptItem, { omit : conn} ); - }); + if(interruptItems.local) { + UserInterruptQueue.queue(interruptItems.local, { clients : client } ); + } + + if(interruptItems.global) { + UserInterruptQueue.queue(interruptItems.global, { omit : client } ); + } + }); + } + ] + ); } }); } + + createAchievementInterruptItems(info, cb) { + const dateTimeFormat = + info.details.dateTimeFormat || + info.achievement.dateTimeFormat || + info.client.currentTheme.helpers.getDateTimeFormat(); + + const config = Config(); + + const formatObj = { + userName : info.user.username, + userRealName : info.user.properties[UserProps.RealName], + userLocation : info.user.properties[UserProps.Location], + userAffils : info.user.properties[UserProps.Affiliations], + nodeId : info.client.node, + title : info.details.title, + text : info.global ? info.details.globalText : info.details.text, + points : info.details.points, + value : info.value, + timestamp : moment(info.timestamp).format(dateTimeFormat), + boardName : config.general.boardName, + }; + + const title = stringFormat(info.details.title, formatObj); + const text = stringFormat(info.details.text, formatObj); + + let globalText; + if(info.details.globalText) { + globalText = stringFormat(info.details.globalText, formatObj); + } + + const getArt = (name, callback) => { + const spec = + _.get(info.details, `art.${name}`) || + _.get(info.achievement, `art.${name}`) || + _.get(config, `userAchievements.art.${name}`); + if(!spec) { + return callback(null); + } + const getArtOpts = { + name : spec, + client : this.client, + random : false, + }; + getThemeArt(getArtOpts, (err, artInfo) => { + // ignore errors + return callback(artInfo ? artInfo.data : null); + }); + }; + + const interruptItems = {}; + let itemTypes = [ 'local' ]; + if(globalText) { + itemTypes.push('global'); + } + + async.each(itemTypes, (itemType, nextItemType) => { + async.waterfall( + [ + (callback) => { + getArt('header', headerArt => { + return callback(null, headerArt); + }); + }, + (headerArt, callback) => { + getArt('footer', footerArt => { + return callback(null, headerArt, footerArt); + }); + }, + (headerArt, footerArt, callback) => { + const itemText = 'global' === itemType ? globalText : text; + interruptItems[itemType] = { + text : `${title}\r\n${itemText}`, + pause : true, + }; + if(headerArt || footerArt) { + interruptItems[itemType].contents = `${headerArt || ''}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`; + } + return callback(null); + } + ], + err => { + return nextItemType(err); + } + ); + }, + err => { + return cb(err, interruptItems); + }); + } } let achievements; diff --git a/core/config.js b/core/config.js index f854e2f5..b73490cc 100644 --- a/core/config.js +++ b/core/config.js @@ -1008,8 +1008,12 @@ function getDefaultConfig() { userAchievements : { enabled : true, - artHeader : 'achievement_header', - artFooter : 'achievement_footer', + art : { + header : 'achievement_header', + footer : 'achievement_footer', + }, + + // :TODO: achievements should be a path/filename -> achievements.hjson & allow override/theming achievements : { user_login_count : { @@ -1019,20 +1023,20 @@ function getDefaultConfig() { match : { 10 : { title : 'Return Caller', - globalText : '{userName} has logged in {statValue} times!', - text : 'You\'ve logged in {statValue} times!', + globalText : '{userName} has logged in {value} times!', + text : 'You\'ve logged in {value} times!', points : 5, }, 25 : { title : 'Seems To Like It!', - globalText : '{userName} has logged in {statValue} times!', - text : 'You\'ve logged in {statValue} times!', + globalText : '{userName} has logged in {value} times!', + text : 'You\'ve logged in {value} times!', points : 10, }, 100 : { title : '{boardName} Addict', - globalText : '{userName} the BBS {boardName} addict has logged in {statValue} times!', - text : 'You\'re a {boardName} addict! You\'ve logged in {statValue} times!', + globalText : '{userName} the BBS {boardName} addict has logged in {value} times!', + text : 'You\'re a {boardName} addict! You\'ve logged in {value} times!', points : 10, } } From 64106373593aabb818be1b7389b337bf52b5ee52 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 3 Jan 2019 22:03:00 -0700 Subject: [PATCH 489/569] Don't allow real time interrupt until ready --- core/menu_module.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/menu_module.js b/core/menu_module.js index bc631feb..15e7ebce 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -30,6 +30,10 @@ exports.MenuModule = class MenuModule extends PluginModule { this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); this.viewControllers = {}; this.interrupt = (_.get(this.menuConfig.config, 'interrupt', MenuModule.InterruptTypes.Queued)).toLowerCase(); + + if(MenuModule.InterruptTypes.Realtime === this.interrupt) { + this.realTimeInterrupt = 'blocked'; + } } static get InterruptTypes() { @@ -137,6 +141,7 @@ exports.MenuModule = class MenuModule extends PluginModule { }, function finishAndNext(callback) { self.finishedLoading(); + self.realTimeInterrupt = 'allowed'; return self.autoNextMenu(callback); } ], @@ -194,7 +199,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } attemptInterruptNow(interruptItem, cb) { - if(MenuModule.InterruptTypes.Realtime !== this.interrupt) { + if(this.realTimeInterrupt !== 'allowed' || MenuModule.InterruptTypes.Realtime !== this.interrupt) { return cb(null, false); // don't eat up the item; queue for later } From c332b0f3ec6f2cc85b6b8d08eb425d71e3071f91 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 10:49:19 -0700 Subject: [PATCH 490/569] WIP on user achievements + Add MCI codes for points/count + Add docs for MCI codes + Record in stats, stat log, etc. * Do not trigger more than once * Code cleanup & organization, add classes, etc. * Tweaks to DB table --- core/achievement.js | 253 ++++++++++++++++++++++++++++++++--------- core/config.js | 18 +-- core/database.js | 2 +- core/predefined_mci.js | 9 +- core/stat_log.js | 1 + core/system_events.js | 35 +++--- core/user_property.js | 3 + docs/art/mci.md | 10 +- 8 files changed, 242 insertions(+), 89 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index 46f97e03..21cc7ae4 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -5,58 +5,192 @@ const Events = require('./events.js'); const Config = require('./config.js').get; const UserDb = require('./database.js').dbs.user; +const { + getISOTimestampString +} = require('./database.js'); const UserInterruptQueue = require('./user_interrupt_queue.js'); const { getConnectionByUserId } = require('./client_connections.js'); const UserProps = require('./user_property.js'); -const { Errors } = require('./enig_error.js'); +const { + Errors, + ErrorReasons +} = require('./enig_error.js'); const { getThemeArt } = require('./theme.js'); const { pipeToAnsi } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); +const StatLog = require('./stat_log.js'); +const Log = require('./logger.js').log; // deps const _ = require('lodash'); const async = require('async'); const moment = require('moment'); +class Achievement { + constructor(data) { + this.data = data; + } + + static factory(data) { + let achievement; + switch(data.type) { + case Achievement.Types.UserStat : achievement = new UserStatAchievement(data); break; + default : return; + } + + if(achievement.isValid()) { + return achievement; + } + } + + static get Types() { + return { + UserStat : 'userStat', + }; + } + + isValid() { + switch(this.data.type) { + case Achievement.Types.UserStat : + if(!_.isString(this.data.statName)) { + return false; + } + if(!_.isObject(this.data.match)) { + return false; + } + break; + + default : return false; + } + return true; + } + + getMatchDetails(/*matchAgainst*/) { + } + + isValidMatchDetails(details) { + if(!_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) { + return false; + } + return (_.isString(details.globalText) || !details.globalText); + } +} + +class UserStatAchievement extends Achievement { + constructor(data) { + super(data); + } + + isValid() { + if(!super.isValid()) { + return false; + } + return !Object.keys(this.data.match).some(k => !parseInt(k)); + } + + getMatchDetails(matchValue) { + let matchField = Object.keys(this.data.match || {}).sort( (a, b) => b - a).find(v => matchValue >= v); + if(matchField) { + const match = this.data.match[matchField]; + if(this.isValidMatchDetails(match)) { + return [ match, parseInt(matchField), matchValue ]; + } + } + } +} + class Achievements { constructor(events) { this.events = events; } init(cb) { + // :TODO: if enabled/etc., load achievements.hjson -> if theme achievements.hjson{}, merge @ display time? + // merge for local vs global (per theme) clients + // ...only merge/override text this.monitorUserStatUpdateEvents(); return cb(null); } - loadAchievementHitCount(user, achievementTag, field, value, cb) { + loadAchievementHitCount(user, achievementTag, field, cb) { UserDb.get( `SELECT COUNT() AS count FROM user_achievement - WHERE user_id = ? AND achievement_tag = ? AND match_field = ? AND match_value >= ?;`, - [ user.userId, achievementTag, field, value ], + WHERE user_id = ? AND achievement_tag = ? AND match_field = ?;`, + [ user.userId, achievementTag, field], (err, row) => { return cb(err, row && row.count || 0); } ); } + record(info, cb) { + StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1); + StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points); + + UserDb.run( + `INSERT INTO user_achievement (user_id, achievement_tag, timestamp, match_field, match_value) + VALUES (?, ?, ?, ?, ?);`, + [ info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField, info.matchValue ], + err => { + if(err) { + return cb(err); + } + + Events.emit( + Events.getSystemEvents().UserAchievementEarned, + { + user : info.client.user, + achievementTag : info.achievementTag, + points : info.details.points, + } + ); + + return cb(null); + } + ); + } + + display(info, cb) { + this.createAchievementInterruptItems(info, (err, interruptItems) => { + if(err) { + return cb(err); + } + + if(interruptItems.local) { + UserInterruptQueue.queue(interruptItems.local, { clients : info.client } ); + } + + if(interruptItems.global) { + UserInterruptQueue.queue(interruptItems.global, { omit : info.client } ); + } + + return cb(null); + }); + } + monitorUserStatUpdateEvents() { this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => { + if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) { + return; + } + const statValue = parseInt(userStatEvent.statValue, 10); if(isNaN(statValue)) { return; } const config = Config(); + // :TODO: Make this code generic - find + return factory created object const achievementTag = _.findKey( _.get(config, 'userAchievements.achievements', {}), achievement => { if(false === achievement.enabled) { return false; } - return 'userStat' === achievement.type && + return Achievement.Types.UserStat === achievement.type && achievement.statName === userStatEvent.statName; } ); @@ -65,54 +199,60 @@ class Achievements { return; } - const achievement = config.userAchievements.achievements[achievementTag]; - let matchValue = Object.keys(achievement.match || {}).sort( (a, b) => b - a).find(v => statValue >= v); - if(matchValue) { - const details = achievement.match[matchValue]; - matchValue = parseInt(matchValue); - - async.series( - [ - (callback) => { - this.loadAchievementHitCount(userStatEvent.user, achievementTag, null, matchValue, (err, count) => { - if(err) { - return callback(err); - } - return callback(count > 0 ? Errors.General('Achievement already acquired') : null); - }); - }, - (callback) => { - const client = getConnectionByUserId(userStatEvent.user.userId); - if(!client) { - return callback(Errors.UnexpectedState('Failed to get client for user ID')); - } - - const info = { - achievement, - details, - client, - value : matchValue, - user : userStatEvent.user, - timestamp : moment(), - }; - - this.createAchievementInterruptItems(info, (err, interruptItems) => { - if(err) { - return callback(err); - } - - if(interruptItems.local) { - UserInterruptQueue.queue(interruptItems.local, { clients : client } ); - } - - if(interruptItems.global) { - UserInterruptQueue.queue(interruptItems.global, { omit : client } ); - } - }); - } - ] - ); + const achievement = Achievement.factory(config.userAchievements.achievements[achievementTag]); + if(!achievement) { + return; } + + const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); + if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) { + return; + } + + async.waterfall( + [ + (callback) => { + this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { + if(err) { + return callback(err); + } + return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null); + }); + }, + (callback) => { + const client = getConnectionByUserId(userStatEvent.user.userId); + if(!client) { + return callback(Errors.UnexpectedState('Failed to get client for user ID')); + } + + const info = { + achievementTag, + achievement, + details, + client, + matchField, + matchValue, + user : userStatEvent.user, + timestamp : moment(), + }; + + return callback(null, info); + }, + (info, callback) => { + this.record(info, err => { + return callback(err, info); + }); + }, + (info, callback) => { + return this.display(info, callback); + } + ], + err => { + if(err && ErrorReasons.TooMany !== err.reasonCode) { + Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event'); + } + } + ); }); } @@ -133,7 +273,8 @@ class Achievements { title : info.details.title, text : info.global ? info.details.globalText : info.details.text, points : info.details.points, - value : info.value, + matchField : info.matchField, + matchValue : info.matchValue, timestamp : moment(info.timestamp).format(dateTimeFormat), boardName : config.general.boardName, }; @@ -175,12 +316,12 @@ class Achievements { async.waterfall( [ (callback) => { - getArt('header', headerArt => { + getArt(`${itemType}Header`, headerArt => { return callback(null, headerArt); }); }, (headerArt, callback) => { - getArt('footer', footerArt => { + getArt(`${itemType}Footer`, footerArt => { return callback(null, headerArt, footerArt); }); }, @@ -191,7 +332,7 @@ class Achievements { pause : true, }; if(headerArt || footerArt) { - interruptItems[itemType].contents = `${headerArt || ''}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`; + interruptItems[itemType].contents = `${headerArt || ''}\r\n${pipeToAnsi(title)}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`; } return callback(null); } diff --git a/core/config.js b/core/config.js index b73490cc..a74b124c 100644 --- a/core/config.js +++ b/core/config.js @@ -1009,8 +1009,10 @@ function getDefaultConfig() { enabled : true, art : { - header : 'achievement_header', - footer : 'achievement_footer', + localHeader : 'achievement_local_header', + localFooter : 'achievement_local_footer', + globalHeader : 'achievement_global_header', + globalFooter : 'achievement_global_footer', }, // :TODO: achievements should be a path/filename -> achievements.hjson & allow override/theming @@ -1023,20 +1025,20 @@ function getDefaultConfig() { match : { 10 : { title : 'Return Caller', - globalText : '{userName} has logged in {value} times!', - text : 'You\'ve logged in {value} times!', + globalText : '{userName} has logged in {matchValue} times!', + text : 'You\'ve logged in {matchValue} times!', points : 5, }, 25 : { title : 'Seems To Like It!', - globalText : '{userName} has logged in {value} times!', - text : 'You\'ve logged in {value} times!', + globalText : '{userName} has logged in {matchValue} times!', + text : 'You\'ve logged in {matchValue} times!', points : 10, }, 100 : { title : '{boardName} Addict', - globalText : '{userName} the BBS {boardName} addict has logged in {value} times!', - text : 'You\'re a {boardName} addict! You\'ve logged in {value} times!', + globalText : '{userName} the BBS {boardName} addict has logged in {matchValue} times!', + text : 'You\'re a {boardName} addict! You\'ve logged in {matchValue} times!', points : 10, } } diff --git a/core/database.js b/core/database.js index 4cf2513c..371af1ae 100644 --- a/core/database.js +++ b/core/database.js @@ -196,7 +196,7 @@ const DB_INIT_TABLE = { timestamp DATETIME NOT NULL, match_field VARCHAR NOT NULL, match_value VARCHAR NOT NULL, - UNIQUE(user_id, achievement_tag, match_field, match_value), + UNIQUE(user_id, achievement_tag, match_field), FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE );` ); diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 76b34fd5..01b1e285 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -90,7 +90,7 @@ const PREDEFINED_MCI_GENERATORS = { return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat()); }, US : function sex(client) { return userStatAsString(client, UserProps.Sex, ''); }, - UE : function emailAddres(client) { return userStatAsString(client, UserProps.EmailAddress, ''); }, + UE : function emailAddress(client) { return userStatAsString(client, UserProps.EmailAddress, ''); }, UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); }, UF : function affils(client) { return userStatAsString(client, UserProps.Affiliations, ''); }, UT : function themeName(client) { @@ -122,7 +122,7 @@ const PREDEFINED_MCI_GENERATORS = { return getUserRatio(client, UserProps.FileUlTotalBytes, UserProps.FileDlTotalBytes); }, - MS : function accountCreatedclient(client) { + MS : function accountCreated(client) { return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat()); }, PS : function userPostCount(client) { return userStatAsString(client, UserProps.MessagePostCount, 0); }, @@ -152,6 +152,9 @@ const PREDEFINED_MCI_GENERATORS = { SH : function termHeight(client) { return client.term.termHeight.toString(); }, SW : function termWidth(client) { return client.term.termWidth.toString(); }, + AC : function achievementCount(client) { return userStatAsString(client, UserProps.AchievementTotalCount, 0); }, + AP : function achievementPoints(client) { return userStatAsString(client, UserProps.AchievementTotalPoints, 0); }, + // // Date/Time // @@ -166,7 +169,7 @@ const PREDEFINED_MCI_GENERATORS = { OS : function operatingSystem() { return { linux : 'Linux', - darwin : 'Mac OS X', + darwin : 'OS X', win32 : 'Windows', sunos : 'SunOS', freebsd : 'FreeBSD', diff --git a/core/stat_log.js b/core/stat_log.js index ffe099ae..8627b6f2 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -359,6 +359,7 @@ class StatLog { systemEvents.UserUpload, systemEvents.UserDownload, systemEvents.UserPostMessage, systemEvents.UserSendMail, systemEvents.UserRunDoor, systemEvents.UserSendNodeMsg, + systemEvents.UserAchievementEarned, ]; Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => { diff --git a/core/system_events.js b/core/system_events.js index 80612195..50a0c464 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -2,24 +2,25 @@ 'use strict'; module.exports = { - ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } - ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } - TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } + ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } + ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } + TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } - ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } - ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson) - MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) - PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson) + ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } + ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson) + MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) + PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson) // User - includes { user, ...} - NewUser : 'codes.l33t.enigma.system.user_new', - UserLogin : 'codes.l33t.enigma.system.user_login', - UserLogoff : 'codes.l33t.enigma.system.user_logoff', - UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } - UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } - UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { areaTag } - UserSendMail : 'codes.l33t.enigma.system.user_send_mail', - UserRunDoor : 'codes.l33t.enigma.system.user_run_door', - UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', - UserStatUpdate : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } + NewUser : 'codes.l33t.enigma.system.user_new', + UserLogin : 'codes.l33t.enigma.system.user_login', + UserLogoff : 'codes.l33t.enigma.system.user_logoff', + UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } + UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } + UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { areaTag } + UserSendMail : 'codes.l33t.enigma.system.user_send_mail', + UserRunDoor : 'codes.l33t.enigma.system.user_run_door', + UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', + UserStatUpdate : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } + UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // {..., achievementTag, points } }; diff --git a/core/user_property.js b/core/user_property.js index 7f2bf6c5..dafd7170 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -49,5 +49,8 @@ module.exports = { MessageConfTag : 'message_conf_tag', MessageAreaTag : 'message_area_tag', MessagePostCount : 'post_count', + + AchievementTotalCount : 'achievement_total_count', + AchievementTotalPoints : 'achievement_total_points', }; diff --git a/docs/art/mci.md b/docs/art/mci.md index e365f393..5ec86805 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -16,8 +16,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | Code | Description | |------|--------------| | `BN` | Board Name | -| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.3-alpha" | -| `VN` | Version *number*, eg.. "0.0.3-alpha" | +| `VL` | Version *label*, e.g. "ENiGMA½ v0.0.9-alpha" | +| `VN` | Version *number*, eg.. "0.0.9-alpha" | | `SN` | SysOp username | | `SR` | SysOp real name | | `SL` | SysOp location | @@ -30,7 +30,7 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `UR` | Current user's real name | | `LO` | Current user's location | | `UA` | Current user's age | -| `BD` | Current user's birthdate (using theme date format) | +| `BD` | Current user's birthday (using theme date format) | | `US` | Current user's sex | | `UE` | Current user's email address | | `UW` | Current user's web address | @@ -58,6 +58,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `CM` | Current user's active message conference description | | `SH` | Current user's term height | | `SW` | Current user's term width | +| `AC` | Current user's total achievements | +| `AP` | Current user's total achievement points | | `DT` | Current date (using theme date format) | | `CT` | Current time (using theme time format) | | `OS` | System OS (Linux, Windows, etc.) | @@ -149,7 +151,7 @@ Standard style types available for `textStyle` and `focusTextStyle`: | `mixed` | EnIGma BUlLEtIn BoaRd SOfTWarE (randomly assigned) | | `l33t` | 3n1gm4 bull371n b04rd 50f7w4r3 | -### Entry Fromatting +### Entry Formatting Various strings can be formatted using a syntax that allows width & precision specifiers, text styling, etc. Depending on the context, various elements can be referenced by `{name}`. Additional text styles can be supplied as well. The syntax is largely modeled after Python's [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language). ### Additional Text Styles From 2bd51c07250ac93807c9eccb5cf9b1e05c89b30f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 12:18:44 -0700 Subject: [PATCH 491/569] Achievements are now in 'achievements.hjson' + Config.general.achievementFile * Implement (re)caching (aka hot-reload) * Update values a bit --- config/achievements.hjson | 53 ++++++++++++++++++++++ core/achievement.js | 94 +++++++++++++++++++++++++++++++-------- core/config.js | 42 +---------------- core/config_util.js | 1 + 4 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 config/achievements.hjson diff --git a/config/achievements.hjson b/config/achievements.hjson new file mode 100644 index 00000000..cb58ddd1 --- /dev/null +++ b/config/achievements.hjson @@ -0,0 +1,53 @@ +{ + enabled : true, + + art : { + localHeader : 'achievement_local_header', + localFooter : 'achievement_local_footer', + globalHeader : 'achievement_global_header', + globalFooter : 'achievement_global_footer', + }, + + // :TODO: achievements should be a path/filename -> achievements.hjson & allow override/theming + + achievements : { + user_login_count : { + type : 'userStat', + statName : 'login_count', + retroactive : true, + + match : { + 2 : { + title : 'Return Caller', + globalText : '{userName} has returned to {boardName}!', + text : 'You\'ve returned to {boardName}!', + points : 5, + }, + 10 : { + title : '{achievedValue} Logins', + globalText : '{userName} has logged into {boardName} {achievedValue} times!', + text : 'You\'ve logged into {boardName} {achievedValue} times!', + points : 5, + }, + 25 : { + title : '{achievedValue} Logins', + globalText : '{userName} has logged into {boardName} {achievedValue} times!', + text : 'You\'ve logged into {boardName} {achievedValue} times!', + points : 10, + }, + 100 : { + title : '{boardName} Regular', + globalText : '{userName} has logged into {boardName} {achievedValue} times!', + text : 'You\'ve logged into {boardName} {achievedValue} times!', + points : 10, + }, + 500 : { + title : '{boardName} Addict', + globalText : '{userName} the BBS {boardName} addict has logged in {achievedValue} times!', + text : 'You\'re a {boardName} addict! You\'ve logged in {achievedValue} times!', + points : 25, + } + } + } + } +} \ No newline at end of file diff --git a/core/achievement.js b/core/achievement.js index 21cc7ae4..bcd6860f 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -4,6 +4,10 @@ // ENiGMA½ const Events = require('./events.js'); const Config = require('./config.js').get; +const { + getConfigPath, + getFullConfig, +} = require('./config_util.js'); const UserDb = require('./database.js').dbs.user; const { getISOTimestampString @@ -22,11 +26,13 @@ const { pipeToAnsi } = require('./color_codes.js'); const stringFormat = require('./string_format.js'); const StatLog = require('./stat_log.js'); const Log = require('./logger.js').log; +const ConfigCache = require('./config_cache.js'); // deps const _ = require('lodash'); const async = require('async'); const moment = require('moment'); +const paths = require('path'); class Achievement { constructor(data) { @@ -107,11 +113,56 @@ class Achievements { } init(cb) { + let achievementConfigPath = _.get(Config(), 'general.achievementFile'); + if(!achievementConfigPath) { + // :TODO: Log me + return cb(null); + } + achievementConfigPath = getConfigPath(achievementConfigPath); // qualify + + // :TODO: Log enabled + + const configLoaded = (achievementConfig) => { + if(true !== achievementConfig.enabled) { + this.stopMonitoringUserStatUpdateEvents(); + delete this.achievementConfig; + } else { + this.achievementConfig = achievementConfig; + this.monitorUserStatUpdateEvents(); + } + }; + + const changed = ( { fileName, fileRoot } ) => { + const reCachedPath = paths.join(fileRoot, fileName); + if(reCachedPath === achievementConfigPath) { + getFullConfig(achievementConfigPath, (err, achievementConfig) => { + if(err) { + return Log.error( { error : err.message }, 'Failed to reload achievement config from cache'); + } + configLoaded(achievementConfig); + }); + } + }; + + ConfigCache.getConfigWithOptions( + { + filePath : achievementConfigPath, + forceReCache : true, + callback : changed, + }, + (err, achievementConfig) => { + if(err) { + return cb(err); + } + + configLoaded(achievementConfig); + return cb(null); + } + ); + // :TODO: if enabled/etc., load achievements.hjson -> if theme achievements.hjson{}, merge @ display time? // merge for local vs global (per theme) clients - // ...only merge/override text - this.monitorUserStatUpdateEvents(); - return cb(null); + // ...only merge/override text } loadAchievementHitCount(user, achievementTag, field, cb) { @@ -139,7 +190,7 @@ class Achievements { return cb(err); } - Events.emit( + this.events.emit( Events.getSystemEvents().UserAchievementEarned, { user : info.client.user, @@ -172,7 +223,11 @@ class Achievements { } monitorUserStatUpdateEvents() { - this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => { + if(this.userStatEventListener) { + return; // already listening + } + + this.userStatEventListener = this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => { if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) { return; } @@ -182,10 +237,9 @@ class Achievements { return; } - const config = Config(); // :TODO: Make this code generic - find + return factory created object const achievementTag = _.findKey( - _.get(config, 'userAchievements.achievements', {}), + _.get(this.achievementConfig, 'achievements', {}), achievement => { if(false === achievement.enabled) { return false; @@ -199,7 +253,7 @@ class Achievements { return; } - const achievement = Achievement.factory(config.userAchievements.achievements[achievementTag]); + const achievement = Achievement.factory(this.achievementConfig.achievements[achievementTag]); if(!achievement) { return; } @@ -230,10 +284,11 @@ class Achievements { achievement, details, client, - matchField, - matchValue, - user : userStatEvent.user, - timestamp : moment(), + matchField, // match - may be in odd format + matchValue, // actual value + achievedValue : matchField, // achievement value met + user : userStatEvent.user, + timestamp : moment(), }; return callback(null, info); @@ -256,6 +311,13 @@ class Achievements { }); } + stopMonitoringUserStatUpdateEvents() { + if(this.userStatEventListener) { + this.events.removeListener(Events.getSystemEvents().UserStatUpdate, this.userStatEventListener); + delete this.userStatEventListener; + } + } + createAchievementInterruptItems(info, cb) { const dateTimeFormat = info.details.dateTimeFormat || @@ -291,7 +353,7 @@ class Achievements { const spec = _.get(info.details, `art.${name}`) || _.get(info.achievement, `art.${name}`) || - _.get(config, `userAchievements.art.${name}`); + _.get(this.achievementConfig, `art.${name}`); if(!spec) { return callback(null); } @@ -351,12 +413,6 @@ class Achievements { let achievements; exports.moduleInitialize = (initInfo, cb) => { - - if(false === _.get(Config(), 'userAchievements.enabled')) { - // :TODO: Log disabled - return cb(null); - } - achievements = new Achievements(initInfo.events); return achievements.init(cb); }; diff --git a/core/config.js b/core/config.js index a74b124c..fa99d5da 100644 --- a/core/config.js +++ b/core/config.js @@ -175,6 +175,7 @@ function getDefaultConfig() { menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path promptFile : 'prompt.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path + achievementFile : 'achievements.hjson', }, users : { @@ -1004,46 +1005,5 @@ function getDefaultConfig() { loginHistoryMax: -1, // set to -1 for forever } }, - - userAchievements : { - enabled : true, - - art : { - localHeader : 'achievement_local_header', - localFooter : 'achievement_local_footer', - globalHeader : 'achievement_global_header', - globalFooter : 'achievement_global_footer', - }, - - // :TODO: achievements should be a path/filename -> achievements.hjson & allow override/theming - - achievements : { - user_login_count : { - type : 'userStat', - statName : 'login_count', - retroactive : true, - match : { - 10 : { - title : 'Return Caller', - globalText : '{userName} has logged in {matchValue} times!', - text : 'You\'ve logged in {matchValue} times!', - points : 5, - }, - 25 : { - title : 'Seems To Like It!', - globalText : '{userName} has logged in {matchValue} times!', - text : 'You\'ve logged in {matchValue} times!', - points : 10, - }, - 100 : { - title : '{boardName} Addict', - globalText : '{userName} the BBS {boardName} addict has logged in {matchValue} times!', - text : 'You\'re a {boardName} addict! You\'ve logged in {matchValue} times!', - points : 10, - } - } - } - } - } }; } diff --git a/core/config_util.js b/core/config_util.js index bda0e1bd..d64c7a24 100644 --- a/core/config_util.js +++ b/core/config_util.js @@ -10,6 +10,7 @@ const paths = require('path'); const async = require('async'); exports.init = init; +exports.getConfigPath = getConfigPath; exports.getFullConfig = getFullConfig; function getConfigPath(filePath) { From 3cc905ea84146fee919dbcbd27a5ec986ede1ef6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 16:55:25 -0700 Subject: [PATCH 492/569] Notes on Gopher and NNTP --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 230878ed..a0bde9d0 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * 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 * [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. - * Full [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](/docs/configuration/colour-codes.md). * [SQLite](http://sqlite.org/) storage of users, message areas, etc. * Strong [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) backed password encryption. * [Door support](docs/modding/door-servers.md) including common dropfile formats for legacy DOS doors. Built in [BBSLink](http://bbslink.net/), [DoorParty](http://forums.throwbackbbs.com/), [Exodus](https://oddnetwork.org/exodus/) and [CombatNet](http://combatnet.us/) support! * [Bunyan](https://github.com/trentm/node-bunyan) logging! - * [Message networks](docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be set to read-only viewable using a built in Gopher server! + * [Message networks](docs/messageareas/message-networks.md) with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export. Messages Bases can also be exposed via [Gopher](docs/servers/gopher.md), or [NNTP](docs/servers/nntp.md)! * [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/servers/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! * ANSI support in the Full Screen Editor (FSE), file descriptions, etc. From f56a72e0c3aa76ccfa3bca2fd0a576f13ea31160 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 16:55:37 -0700 Subject: [PATCH 493/569] Start of theming of achievements + default text/SGR styles can now be set for quick customization of colors --- art/themes/luciano_blocktronics/theme.hjson | 23 ++++++++ core/achievement.js | 59 ++++++++++++--------- core/theme.js | 7 +-- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index b1fe8aec..d38137c8 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -980,5 +980,28 @@ } } } + + achievements: { + defaults: { + titleSGR: "|11" + textSGR: "|00|03" + globalTextSGR: "|03" + boardName: "|10" + userName: "|11" + achievedValue: "|15" + } + + overrides: { + user_login_count: { + match: { + 2: { + // + // You may override title, text, and globalText here + // + } + } + } + } + } } } \ No newline at end of file diff --git a/core/achievement.js b/core/achievement.js index bcd6860f..cc64779c 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -115,18 +115,18 @@ class Achievements { init(cb) { let achievementConfigPath = _.get(Config(), 'general.achievementFile'); if(!achievementConfigPath) { - // :TODO: Log me + Log.info('Achievements are not configured'); return cb(null); } achievementConfigPath = getConfigPath(achievementConfigPath); // qualify - // :TODO: Log enabled - const configLoaded = (achievementConfig) => { if(true !== achievementConfig.enabled) { + Log.info('Achievements are not enabled'); this.stopMonitoringUserStatUpdateEvents(); delete this.achievementConfig; } else { + Log.info('Achievements are enabled'); this.achievementConfig = achievementConfig; this.monitorUserStatUpdateEvents(); } @@ -318,35 +318,45 @@ class Achievements { } } + getFormattedTextFor(info, textType) { + const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); + const defSgr = themeDefaults[`${textType}SGR`] || '|07'; + + const wrap = (fieldName, value) => { + return `${themeDefaults[fieldName] || defSgr}${value}${defSgr}`; + }; + + const formatObj = { + userName : wrap('userName', info.user.username), + userRealName : wrap('userRealName', info.user.properties[UserProps.RealName]), + userLocation : wrap('userLocation', info.user.properties[UserProps.Location]), + userAffils : wrap('userAffils', info.user.properties[UserProps.Affiliations]), + nodeId : wrap('nodeId', info.client.node), + title : wrap('title', info.details.title), + text : wrap('text', info.global ? info.details.globalText : info.details.text), + points : wrap('points', info.details.points), + achievedValue : wrap('achievedValue', info.achievedValue), + matchField : wrap('matchField', info.matchField), + matchValue : wrap('matchValue', info.matchValue), + timestamp : wrap('timestamp', moment(info.timestamp).format(info.dateTimeFormat)), + boardName : wrap('boardName', Config().general.boardName), + }; + + return stringFormat(`${defSgr}${info.details[textType]}`, formatObj); + } + createAchievementInterruptItems(info, cb) { - const dateTimeFormat = + info.dateTimeFormat = info.details.dateTimeFormat || info.achievement.dateTimeFormat || info.client.currentTheme.helpers.getDateTimeFormat(); - const config = Config(); - - const formatObj = { - userName : info.user.username, - userRealName : info.user.properties[UserProps.RealName], - userLocation : info.user.properties[UserProps.Location], - userAffils : info.user.properties[UserProps.Affiliations], - nodeId : info.client.node, - title : info.details.title, - text : info.global ? info.details.globalText : info.details.text, - points : info.details.points, - matchField : info.matchField, - matchValue : info.matchValue, - timestamp : moment(info.timestamp).format(dateTimeFormat), - boardName : config.general.boardName, - }; - - const title = stringFormat(info.details.title, formatObj); - const text = stringFormat(info.details.text, formatObj); + const title = this.getFormattedTextFor(info, 'title'); + const text = this.getFormattedTextFor(info, 'text'); let globalText; if(info.details.globalText) { - globalText = stringFormat(info.details.globalText, formatObj); + globalText = this.getFormattedTextFor(info, 'globalText'); } const getArt = (name, callback) => { @@ -416,4 +426,3 @@ exports.moduleInitialize = (initInfo, cb) => { achievements = new Achievements(initInfo.events); return achievements.init(cb); }; - diff --git a/core/theme.js b/core/theme.js index 6dfee685..9978fde3 100644 --- a/core/theme.js +++ b/core/theme.js @@ -96,7 +96,7 @@ function loadTheme(themeId, cb) { } if(false === _.get(theme, 'info.enabled')) { - return cb(Errors.General('Theme is not enalbed', ErrorReasons.ErrNotEnabled)); + return cb(Errors.General('Theme is not enabled', ErrorReasons.ErrNotEnabled)); } refreshThemeHelpers(theme); @@ -131,8 +131,9 @@ function getMergedTheme(menuConfig, promptConfig, theme) { // // Add in data we won't be altering directly from the theme // - mergedTheme.info = theme.info; - mergedTheme.helpers = theme.helpers; + mergedTheme.info = theme.info; + mergedTheme.helpers = theme.helpers; + mergedTheme.achievements = _.get(theme, 'customization.achievements'); // // merge customizer to disallow immutable MCI properties From 43bbc3733cb334a87dc56f383430f95ca1692899 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 17:07:36 -0700 Subject: [PATCH 494/569] Tabs -> Spaces --- misc/menu_template.in.hjson | 8044 +++++++++++++++++------------------ 1 file changed, 4022 insertions(+), 4022 deletions(-) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 82a3893f..e3b76c21 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1,16 +1,16 @@ { - /* - ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - + /* + ./\/\.' ENiGMA½ Menu Configuration -/--/-------- - -- - - _____________________ _____ ____________________ __________\_ / - \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! - // __|___// | \// |// | \// | | \// \ /___ /_____ - /____ _____| __________ ___|__| ____| \ / _____ \ - ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ - /__ _\ - <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + _____________________ _____ ____________________ __________\_ / + \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! + // __|___// | \// |// | \// | | \// \ /___ /_____ + /____ _____| __________ ___|__| ____| \ / _____ \ + ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - *-----------------------------------------------------------------------------* + *-----------------------------------------------------------------------------* General Information ------------------------------- - - @@ -47,1713 +47,1713 @@ FTN : BBS Discussion on fsxNet IRC : #enigma-bbs / FreeNode Email : bryan@l33t.codes - */ - menus: { - // - // Send telnet connections to matrix where users can login, apply, etc. - // - telnetConnected: { - art: CONNECT - next: matrix - config: { nextTimeout: 1500 } - } - - // - // SSH connections are pre-authenticated via the SSH server itself. - // Jump directly to the login sequence - // - sshConnected: { - art: CONNECT - next: fullLoginSequenceLoginArt - config: { nextTimeout: 1500 } - } - - // - // Another SSH specialization: If the user logs in with a new user - // name (e.g. "new", "apply", ...) they will be directed to the - // application process. - // - sshConnectedNewUser: { - art: CONNECT - next: newUserApplicationPreSsh - config: { nextTimeout: 1500 } - } - - // Ye ol' standard matrix - matrix: { - art: matrix - form: { - 0: { - VM: { - mci: { - VM1: { - submit: true - focus: true - argName: navSelect - // - // To enable forgot password, you will need to have the web server - // enabled and mail/SMTP configured. Once that is in place, swap out - // the commented lines below as well as in the submit block - // - items: [ - { - text: login - data: login - } - { - text: apply - data: apply - } - { - text: forgot pass - data: forgot - } - { - text: log off - data: logoff - } - ] - } - } - submit: { - *: [ - { - value: { navSelect: "login" } - action: @menu:login - } - { - value: { navSelect: "apply" } - action: @menu:newUserApplicationPre - } - { - value: { navSelect: "forgot" } - action: @menu:forgotPassword - } - { - value: { navSelect: "logoff" } - action: @menu:logoff - } - ] - } - } - } - } - } - - login: { - art: USERLOG - next: fullLoginSequenceLoginArt - config: { - tooNodeMenu: loginAttemptTooNode - inactive: loginAttemptAccountInactive - disabled: loginAttemptAccountDisabled - locked: loginAttemptAccountLocked - } - form: { - 0: { - mci: { - ET1: { - maxLength: @config:users.usernameMax - argName: username - focus: true - } - ET2: { - password: true - maxLength: @config:users.passwordMax - argName: password - submit: true - } - } - submit: { - *: [ - { - value: { password: null } - action: @systemMethod:login - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - loginAttemptTooNode: { - art: TOONODE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountLocked: { - art: ACCOUNTLOCKED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountDisabled: { - art: ACCOUNTDISABLED - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - loginAttemptAccountInactive: { - art: ACCOUNTINACTIVE - config: { - cls: true - nextTimeout: 2000 - } - next: logoff - } - - forgotPassword: { - desc: Forgot password - prompt: forgotPasswordPrompt - submit: [ - { - value: { username: null } - action: @systemMethod:sendForgotPasswordEmail - extraArgs: { next: "forgotPasswordSubmitted" } - } - ] - } - - forgotPasswordSubmitted: { - desc: Forgot password - art: FORGOTPWSENT - config: { - cls: true - pause: true - } - next: @systemMethod:logoff - } - - // :TODO: Prompt Yes/No for logoff confirm - fullLogoffSequence: { - desc: Logging Off - prompt: logoffConfirmation - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLogoffSequencePreAd - } - { - value: { promptValue: 1 } - action: @systemMethod:prevMenu - } - ] - } - - fullLogoffSequencePreAd: { - art: PRELOGAD - desc: Logging Off - next: fullLogoffSequenceRandomBoardAd - config: { - cls: true - nextTimeout: 1500 - } - } - - fullLogoffSequenceRandomBoardAd: { - art: OTHRBBS - desc: Logging Off - next: logoff - config: { - baudRate: 57600 - pause: true - cls: true - } - } - - logoff: { - art: LOGOFF - desc: Logging Off - next: @systemMethod:logoff - } - - // A quick preamble - defaults to warning about broken terminals - newUserApplicationPre: { - art: NEWUSER1 - next: newUserApplication - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - newUserApplication: { - module: nua - art: NUA - next: [ - { - // Initial SysOp does not send feedback to themselves - acs: ID1 - next: fullLoginSequenceLoginArt - } - { - // ...everyone else does - next: newUserFeedbackToSysOpPreamble - } - ] - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - argName: password - password: true - maxLength: @config:users.passwordMax - validate: @systemMethod:validatePasswordSpec - } - ET10: { - argName: passwordConfirm - password: true - maxLength: @config:users.passwordMax - validate: @method:validatePassConfirmMatch - } - TM12: { - argName: submission - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - // A quick preamble - defaults to warning about broken terminals (SSH version) - newUserApplicationPreSsh: { - art: NEWUSER1 - next: newUserApplicationSsh - desc: Applying - config: { - pause: true - cls: true - menuFlags: [ "noHistory" ] - } - } - - // - // SSH specialization of NUA - // Canceling this form logs off vs falling back to matrix - // - newUserApplicationSsh: { - module: nua - art: NUA - fallback: logoff - next: newUserFeedbackToSysOpPreamble - form: { - 0: { - mci: { - ET1: { - focus: true - argName: username - maxLength: @config:users.usernameMax - validate: @systemMethod:validateUserNameAvail - } - ET2: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - } - MET3: { - argName: birthdate - maskPattern: "####/##/##" - validate: @systemMethod:validateBirthdate - } - ME4: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET6: { - argName: affils - maxLength: @config:users.affilsMax - } - ET7: { - argName: email - maxLength: @config:users.emailMax - validate: @systemMethod:validateEmailAvail - } - ET8: { - argName: web - maxLength: @config:users.webMax - } - ET9: { - argName: password - password: true - maxLength: @config:users.passwordMax - validate: @systemMethod:validatePasswordSpec - } - ET10: { - argName: passwordConfirm - password: true - maxLength: @config:users.passwordMax - validate: @method:validatePassConfirmMatch - } - TM12: { - argName: submission - items: [ "apply", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitApplication - extraArgs: { - inactive: userNeedsActivated - error: newUserCreateError - } - } - { - value: { "submission" : 1 } - action: @systemMethod:logoff - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:logoff - } - ] - } - } - } - - newUserFeedbackToSysOpPreamble: { - art: LETTER - config: { pause: true } - next: newUserFeedbackToSysOp - } - - newUserFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - next: [ - { - acs: AS2 - next: fullLoginSequenceLoginArt - } - { - next: newUserInactiveDone - } - ] - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - text: New user feedback - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - newUserInactiveDone: { - desc: Finished with NUA - art: DONE - config: { pause: true } - next: @menu:logoff - } - - fullLoginSequenceLoginArt: { - desc: Logging In - art: WELCOME - config: { pause: true } - next: fullLoginSequenceLastCallers - } - - fullLoginSequenceLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { - pause: true - font: cp437 - } - next: fullLoginSequenceWhosOnline - } - fullLoginSequenceWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - next: fullLoginSequenceOnelinerz - } - - fullLoginSequenceOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - next: [ - { - // calls >= 2 - acs: NC2 - next: fullLoginSequenceNewScanConfirm - } - { - // new users - skip new scan - next: fullLoginSequenceUserStats - } - ] - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - fullLoginSequenceNewScanConfirm: { - desc: Logging In - prompt: loginGlobalNewScan - submit: [ - { - value: { promptValue: 0 } - action: @menu:fullLoginSequenceNewScan - } - { - value: { promptValue: 1 } - action: @menu:fullLoginSequenceUserStats - } - ] - } - - fullLoginSequenceNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - next: fullLoginSequenceSysStats - config: { - messageListMenu: newScanMessageList - } - } - - fullLoginSequenceSysStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - next: fullLoginSequenceUserStats - } - fullLoginSequenceUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - next: mainMenu - } - - newScanMessageList: { - desc: New Messages - module: msg_list - art: NEWMSGS - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "x", "shift + x" ] - action: @method:fullExit - } - { - keys: [ "m", "shift + m" ] - action: @method:markAllRead - } - ] - } - } - } - - 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 - } - } - - 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" - ] - } - } - - 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 - /////////////////////////////////////////////////////////////////////// - mainMenu: { - art: MMENU - desc: Main Menu - prompt: menuCommand - config: { - font: cp437 - interrupt: realtime - } - submit: [ - { - value: { command: "MSG" } - action: @menu:nodeMessage - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "D" } - action: @menu:doorMenu - } - { - value: { command: "F" } - action: @menu:fileBase - } - { - value: { command: "U" } - action: @menu:mainMenuUserList - } - { - value: { command: "L" } - action: @menu:mainMenuLastCallers - } - { - value: { command: "W" } - action: @menu:mainMenuWhosOnline - } - { - value: { command: "Y" } - action: @menu:mainMenuUserStats - } - { - value: { command: "M" } - action: @menu:messageArea - } - { - value: { command: "E" } - action: @menu:mailMenu - } - { - value: { command: "C" } - action: @menu:mainMenuUserConfig - } - { - value: { command: "S" } - action: @menu:mainMenuSystemStats - } - { - value: { command: "!" } - action: @menu:mainMenuGlobalNewScan - } - { - value: { command: "K" } - action: @menu:mainMenuFeedbackToSysOp - } - { - value: { command: "O" } - action: @menu:mainMenuOnelinerz - } - { - value: { command: "R" } - action: @menu:mainMenuRumorz - } - { - value: { command: "BBS"} - action: @menu:bbsList - } - { - value: 1 - action: @menu:mainMenu - } - ] - } - - nodeMessage: { - desc: Node Messaging - module: node_msg - art: NODEMSG - config: { - cls: true - art: { - header: NODEMSGHDR - footer: NODEMSGFTR - } - } - form: { - 0: { - mci: { - SM1: { - argName: node - } - ET2: { - argName: message - submit: true - } - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - submit: { - *: [ - { - value: { message: null } - action: @method:sendMessage - } - ] - } - } - } - } - - mainMenuLastCallers: { - desc: Last Callers - module: last_callers - art: LASTCALL - config: { pause: true } - } - - mainMenuWhosOnline: { - desc: Who's Online - module: whos_online - art: WHOSON - config: { pause: true } - } - - mainMenuUserStats: { - desc: User Stats - art: STATUS - config: { pause: true } - } - - mainMenuSystemStats: { - desc: System Stats - art: SYSSTAT - config: { pause: true } - } - - mainMenuUserList: { - desc: User Listing - module: user_list - art: USERLST - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - } - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuUserConfig: { - module: user_config - art: CONFSCR - form: { - 0: { - mci: { - ET1: { - argName: realName - maxLength: @config:users.realNameMax - validate: @systemMethod:validateNonEmpty - focus: true - } - ME2: { - argName: birthdate - maskPattern: "####/##/##" - } - ME3: { - argName: sex - maskPattern: A - textStyle: upper - validate: @systemMethod:validateNonEmpty - } - ET4: { - argName: location - maxLength: @config:users.locationMax - validate: @systemMethod:validateNonEmpty - } - ET5: { - argName: affils - maxLength: @config:users.affilsMax - } - ET6: { - argName: email - maxLength: @config:users.emailMax - validate: @method:validateEmailAvail - } - ET7: { - argName: web - maxLength: @config:users.webMax - } - ME8: { - maskPattern: "##" - argName: termHeight - validate: @systemMethod:validateNonEmpty - } - SM9: { - argName: theme - } - ET10: { - argName: password - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassword - } - ET11: { - argName: passwordConfirm - maxLength: @config:users.passwordMax - password: true - validate: @method:validatePassConfirmMatch - } - TM25: { - argName: submission - items: [ "save", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { submission: 0 } - action: @method:saveChanges - } - { - value: { submission: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - mainMenuGlobalNewScan: { - desc: Performing New Scan - module: new_scan - art: NEWSCAN - config: { - messageListMenu: newScanMessageList - } - } - - mainMenuFeedbackToSysOp: { - desc: Feedback to SysOp - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - toUserId: 1 /* always to +op */ - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: @sysStat:sysop_username - // :TODO: readOnly: true - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mainMenuOnelinerz: { - desc: Viewing Onelinerz - module: onelinerz - config: { - cls: true - art: { - view: ONELINER - add: ONEADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: oneliner - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - mainMenuRumorz: { - desc: Rumorz - module: rumorz - config: { - cls: true - art: { - entries: RUMORS - add: RUMORADD - } - } - form: { - 0: { - mci: { - VM1: { - focus: false - height: 10 - } - TM2: { - argName: addOrExit - items: [ "yeah!", "nah" ] - "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } - submit: true - focus: true - } - } - submit: { - *: [ - { - value: { addOrExit: 0 } - action: @method:viewAddScreen - } - { - value: { addOrExit: null } - action: @systemMethod:nextMenu - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:nextMenu - } - ] - }, - 1: { - mci: { - ET1: { - focus: true - maxLength: 70 - argName: rumor - } - TL2: { - width: 60 - } - TM3: { - argName: addOrCancel - items: [ "add", "cancel" ] - "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } - submit: true - } - } - - submit: { - *: [ - { - value: { addOrCancel: 0 } - action: @method:addEntry - } - { - value: { addOrCancel: 1 } - action: @method:cancelAdd - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelAdd - } - ] - } - } - } - - bbsList: { - desc: Viewing BBS List - module: bbs_list - config: { - cls: true - art: { - entries: BBSLIST - add: BBSADD - } - } - - form: { - 0: { - mci: { - VM1: { maxLength: 32 } - TL2: { maxLength: 32 } - TL3: { maxLength: 32 } - TL4: { maxLength: 32 } - TL5: { maxLength: 32 } - TL6: { maxLength: 32 } - TL7: { maxLength: 32 } - TL8: { maxLength: 32 } - TL9: { maxLength: 32 } - } - actionKeys: [ - { - keys: [ "a" ] - action: @method:addBBS - } - { - // :TODO: add delete key - keys: [ "d" ] - action: @method:deleteBBS - } - { - keys: [ "q", "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - ET1: { - argName: name - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET2: { - argName: sysop - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET3: { - argName: telnet - maxLength: 32 - validate: @systemMethod:validateNonEmpty - } - ET4: { - argName: www - maxLength: 32 - } - ET5: { - argName: location - maxLength: 32 - } - ET6: { - argName: software - maxLength: 32 - } - ET7: { - argName: notes - maxLength: 32 - } - TM17: { - argName: submission - items: [ "save", "cancel" ] - submit: true - } - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:cancelSubmit - } - ] - - submit: { - *: [ - { - value: { "submission" : 0 } - action: @method:submitBBS - } - { - value: { "submission" : 1 } - action: @method:cancelSubmit - } - ] - } - } - } - } - - /////////////////////////////////////////////////////////////////////// - // Doors Menu - /////////////////////////////////////////////////////////////////////// - doorMenu: { - desc: Doors Menu - art: DOORMNU - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "G" } - action: @menu:logoff - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - // - // The system supports many ways of launching doors including - // modules for DoorParty!, BBSLink, etc. - // - // Below are some examples. See the documentation for more info. - // - { - value: { command: "ABRACADABRA" } - action: @menu:doorAbracadabraExample - } - { - value: { command: "TWBBSLINK" } - action: @menu:doorTradeWars2002BBSLinkExample - } - { - value: { command: "DP" } - action: @menu:doorPartyExample - } - { + */ + menus: { + // + // Send telnet connections to matrix where users can login, apply, etc. + // + telnetConnected: { + art: CONNECT + next: matrix + config: { nextTimeout: 1500 } + } + + // + // SSH connections are pre-authenticated via the SSH server itself. + // Jump directly to the login sequence + // + sshConnected: { + art: CONNECT + next: fullLoginSequenceLoginArt + config: { nextTimeout: 1500 } + } + + // + // Another SSH specialization: If the user logs in with a new user + // name (e.g. "new", "apply", ...) they will be directed to the + // application process. + // + sshConnectedNewUser: { + art: CONNECT + next: newUserApplicationPreSsh + config: { nextTimeout: 1500 } + } + + // Ye ol' standard matrix + matrix: { + art: matrix + form: { + 0: { + VM: { + mci: { + VM1: { + submit: true + focus: true + argName: navSelect + // + // To enable forgot password, you will need to have the web server + // enabled and mail/SMTP configured. Once that is in place, swap out + // the commented lines below as well as in the submit block + // + items: [ + { + text: login + data: login + } + { + text: apply + data: apply + } + { + text: forgot pass + data: forgot + } + { + text: log off + data: logoff + } + ] + } + } + submit: { + *: [ + { + value: { navSelect: "login" } + action: @menu:login + } + { + value: { navSelect: "apply" } + action: @menu:newUserApplicationPre + } + { + value: { navSelect: "forgot" } + action: @menu:forgotPassword + } + { + value: { navSelect: "logoff" } + action: @menu:logoff + } + ] + } + } + } + } + } + + login: { + art: USERLOG + next: fullLoginSequenceLoginArt + config: { + tooNodeMenu: loginAttemptTooNode + inactive: loginAttemptAccountInactive + disabled: loginAttemptAccountDisabled + locked: loginAttemptAccountLocked + } + form: { + 0: { + mci: { + ET1: { + maxLength: @config:users.usernameMax + argName: username + focus: true + } + ET2: { + password: true + maxLength: @config:users.passwordMax + argName: password + submit: true + } + } + submit: { + *: [ + { + value: { password: null } + action: @systemMethod:login + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + loginAttemptTooNode: { + art: TOONODE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountLocked: { + art: ACCOUNTLOCKED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountDisabled: { + art: ACCOUNTDISABLED + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + loginAttemptAccountInactive: { + art: ACCOUNTINACTIVE + config: { + cls: true + nextTimeout: 2000 + } + next: logoff + } + + forgotPassword: { + desc: Forgot password + prompt: forgotPasswordPrompt + submit: [ + { + value: { username: null } + action: @systemMethod:sendForgotPasswordEmail + extraArgs: { next: "forgotPasswordSubmitted" } + } + ] + } + + forgotPasswordSubmitted: { + desc: Forgot password + art: FORGOTPWSENT + config: { + cls: true + pause: true + } + next: @systemMethod:logoff + } + + // :TODO: Prompt Yes/No for logoff confirm + fullLogoffSequence: { + desc: Logging Off + prompt: logoffConfirmation + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLogoffSequencePreAd + } + { + value: { promptValue: 1 } + action: @systemMethod:prevMenu + } + ] + } + + fullLogoffSequencePreAd: { + art: PRELOGAD + desc: Logging Off + next: fullLogoffSequenceRandomBoardAd + config: { + cls: true + nextTimeout: 1500 + } + } + + fullLogoffSequenceRandomBoardAd: { + art: OTHRBBS + desc: Logging Off + next: logoff + config: { + baudRate: 57600 + pause: true + cls: true + } + } + + logoff: { + art: LOGOFF + desc: Logging Off + next: @systemMethod:logoff + } + + // A quick preamble - defaults to warning about broken terminals + newUserApplicationPre: { + art: NEWUSER1 + next: newUserApplication + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + newUserApplication: { + module: nua + art: NUA + next: [ + { + // Initial SysOp does not send feedback to themselves + acs: ID1 + next: fullLoginSequenceLoginArt + } + { + // ...everyone else does + next: newUserFeedbackToSysOpPreamble + } + ] + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + argName: password + password: true + maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec + } + ET10: { + argName: passwordConfirm + password: true + maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch + } + TM12: { + argName: submission + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + // A quick preamble - defaults to warning about broken terminals (SSH version) + newUserApplicationPreSsh: { + art: NEWUSER1 + next: newUserApplicationSsh + desc: Applying + config: { + pause: true + cls: true + menuFlags: [ "noHistory" ] + } + } + + // + // SSH specialization of NUA + // Canceling this form logs off vs falling back to matrix + // + newUserApplicationSsh: { + module: nua + art: NUA + fallback: logoff + next: newUserFeedbackToSysOpPreamble + form: { + 0: { + mci: { + ET1: { + focus: true + argName: username + maxLength: @config:users.usernameMax + validate: @systemMethod:validateUserNameAvail + } + ET2: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + } + MET3: { + argName: birthdate + maskPattern: "####/##/##" + validate: @systemMethod:validateBirthdate + } + ME4: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET6: { + argName: affils + maxLength: @config:users.affilsMax + } + ET7: { + argName: email + maxLength: @config:users.emailMax + validate: @systemMethod:validateEmailAvail + } + ET8: { + argName: web + maxLength: @config:users.webMax + } + ET9: { + argName: password + password: true + maxLength: @config:users.passwordMax + validate: @systemMethod:validatePasswordSpec + } + ET10: { + argName: passwordConfirm + password: true + maxLength: @config:users.passwordMax + validate: @method:validatePassConfirmMatch + } + TM12: { + argName: submission + items: [ "apply", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitApplication + extraArgs: { + inactive: userNeedsActivated + error: newUserCreateError + } + } + { + value: { "submission" : 1 } + action: @systemMethod:logoff + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:logoff + } + ] + } + } + } + + newUserFeedbackToSysOpPreamble: { + art: LETTER + config: { pause: true } + next: newUserFeedbackToSysOp + } + + newUserFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + next: [ + { + acs: AS2 + next: fullLoginSequenceLoginArt + } + { + next: newUserInactiveDone + } + ] + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + text: New user feedback + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + newUserInactiveDone: { + desc: Finished with NUA + art: DONE + config: { pause: true } + next: @menu:logoff + } + + fullLoginSequenceLoginArt: { + desc: Logging In + art: WELCOME + config: { pause: true } + next: fullLoginSequenceLastCallers + } + + fullLoginSequenceLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { + pause: true + font: cp437 + } + next: fullLoginSequenceWhosOnline + } + fullLoginSequenceWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + next: fullLoginSequenceOnelinerz + } + + fullLoginSequenceOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + next: [ + { + // calls >= 2 + acs: NC2 + next: fullLoginSequenceNewScanConfirm + } + { + // new users - skip new scan + next: fullLoginSequenceUserStats + } + ] + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + fullLoginSequenceNewScanConfirm: { + desc: Logging In + prompt: loginGlobalNewScan + submit: [ + { + value: { promptValue: 0 } + action: @menu:fullLoginSequenceNewScan + } + { + value: { promptValue: 1 } + action: @menu:fullLoginSequenceUserStats + } + ] + } + + fullLoginSequenceNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + next: fullLoginSequenceSysStats + config: { + messageListMenu: newScanMessageList + } + } + + fullLoginSequenceSysStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + next: fullLoginSequenceUserStats + } + fullLoginSequenceUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + next: mainMenu + } + + newScanMessageList: { + desc: New Messages + module: msg_list + art: NEWMSGS + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "x", "shift + x" ] + action: @method:fullExit + } + { + keys: [ "m", "shift + m" ] + action: @method:markAllRead + } + ] + } + } + } + + 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 + } + } + + 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" + ] + } + } + + 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 + /////////////////////////////////////////////////////////////////////// + mainMenu: { + art: MMENU + desc: Main Menu + prompt: menuCommand + config: { + font: cp437 + interrupt: realtime + } + submit: [ + { + value: { command: "MSG" } + action: @menu:nodeMessage + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "D" } + action: @menu:doorMenu + } + { + value: { command: "F" } + action: @menu:fileBase + } + { + value: { command: "U" } + action: @menu:mainMenuUserList + } + { + value: { command: "L" } + action: @menu:mainMenuLastCallers + } + { + value: { command: "W" } + action: @menu:mainMenuWhosOnline + } + { + value: { command: "Y" } + action: @menu:mainMenuUserStats + } + { + value: { command: "M" } + action: @menu:messageArea + } + { + value: { command: "E" } + action: @menu:mailMenu + } + { + value: { command: "C" } + action: @menu:mainMenuUserConfig + } + { + value: { command: "S" } + action: @menu:mainMenuSystemStats + } + { + value: { command: "!" } + action: @menu:mainMenuGlobalNewScan + } + { + value: { command: "K" } + action: @menu:mainMenuFeedbackToSysOp + } + { + value: { command: "O" } + action: @menu:mainMenuOnelinerz + } + { + value: { command: "R" } + action: @menu:mainMenuRumorz + } + { + value: { command: "BBS"} + action: @menu:bbsList + } + { + value: 1 + action: @menu:mainMenu + } + ] + } + + nodeMessage: { + desc: Node Messaging + module: node_msg + art: NODEMSG + config: { + cls: true + art: { + header: NODEMSGHDR + footer: NODEMSGFTR + } + } + form: { + 0: { + mci: { + SM1: { + argName: node + } + ET2: { + argName: message + submit: true + } + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + submit: { + *: [ + { + value: { message: null } + action: @method:sendMessage + } + ] + } + } + } + } + + mainMenuLastCallers: { + desc: Last Callers + module: last_callers + art: LASTCALL + config: { pause: true } + } + + mainMenuWhosOnline: { + desc: Who's Online + module: whos_online + art: WHOSON + config: { pause: true } + } + + mainMenuUserStats: { + desc: User Stats + art: STATUS + config: { pause: true } + } + + mainMenuSystemStats: { + desc: System Stats + art: SYSSTAT + config: { pause: true } + } + + mainMenuUserList: { + desc: User Listing + module: user_list + art: USERLST + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + } + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + mainMenuUserConfig: { + module: user_config + art: CONFSCR + form: { + 0: { + mci: { + ET1: { + argName: realName + maxLength: @config:users.realNameMax + validate: @systemMethod:validateNonEmpty + focus: true + } + ME2: { + argName: birthdate + maskPattern: "####/##/##" + } + ME3: { + argName: sex + maskPattern: A + textStyle: upper + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: location + maxLength: @config:users.locationMax + validate: @systemMethod:validateNonEmpty + } + ET5: { + argName: affils + maxLength: @config:users.affilsMax + } + ET6: { + argName: email + maxLength: @config:users.emailMax + validate: @method:validateEmailAvail + } + ET7: { + argName: web + maxLength: @config:users.webMax + } + ME8: { + maskPattern: "##" + argName: termHeight + validate: @systemMethod:validateNonEmpty + } + SM9: { + argName: theme + } + ET10: { + argName: password + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassword + } + ET11: { + argName: passwordConfirm + maxLength: @config:users.passwordMax + password: true + validate: @method:validatePassConfirmMatch + } + TM25: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { submission: 0 } + action: @method:saveChanges + } + { + value: { submission: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + mainMenuGlobalNewScan: { + desc: Performing New Scan + module: new_scan + art: NEWSCAN + config: { + messageListMenu: newScanMessageList + } + } + + mainMenuFeedbackToSysOp: { + desc: Feedback to SysOp + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + toUserId: 1 /* always to +op */ + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: @sysStat:sysop_username + // :TODO: readOnly: true + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + mainMenuOnelinerz: { + desc: Viewing Onelinerz + module: onelinerz + config: { + cls: true + art: { + view: ONELINER + add: ONEADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: oneliner + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + mainMenuRumorz: { + desc: Rumorz + module: rumorz + config: { + cls: true + art: { + entries: RUMORS + add: RUMORADD + } + } + form: { + 0: { + mci: { + VM1: { + focus: false + height: 10 + } + TM2: { + argName: addOrExit + items: [ "yeah!", "nah" ] + "hotKeys" : { "Y" : 0, "N" : 1, "Q" : 1 } + submit: true + focus: true + } + } + submit: { + *: [ + { + value: { addOrExit: 0 } + action: @method:viewAddScreen + } + { + value: { addOrExit: null } + action: @systemMethod:nextMenu + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:nextMenu + } + ] + }, + 1: { + mci: { + ET1: { + focus: true + maxLength: 70 + argName: rumor + } + TL2: { + width: 60 + } + TM3: { + argName: addOrCancel + items: [ "add", "cancel" ] + "hotKeys" : { "A" : 0, "C" : 1, "Q" : 1 } + submit: true + } + } + + submit: { + *: [ + { + value: { addOrCancel: 0 } + action: @method:addEntry + } + { + value: { addOrCancel: 1 } + action: @method:cancelAdd + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelAdd + } + ] + } + } + } + + bbsList: { + desc: Viewing BBS List + module: bbs_list + config: { + cls: true + art: { + entries: BBSLIST + add: BBSADD + } + } + + form: { + 0: { + mci: { + VM1: { maxLength: 32 } + TL2: { maxLength: 32 } + TL3: { maxLength: 32 } + TL4: { maxLength: 32 } + TL5: { maxLength: 32 } + TL6: { maxLength: 32 } + TL7: { maxLength: 32 } + TL8: { maxLength: 32 } + TL9: { maxLength: 32 } + } + actionKeys: [ + { + keys: [ "a" ] + action: @method:addBBS + } + { + // :TODO: add delete key + keys: [ "d" ] + action: @method:deleteBBS + } + { + keys: [ "q", "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + ET1: { + argName: name + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET2: { + argName: sysop + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: telnet + maxLength: 32 + validate: @systemMethod:validateNonEmpty + } + ET4: { + argName: www + maxLength: 32 + } + ET5: { + argName: location + maxLength: 32 + } + ET6: { + argName: software + maxLength: 32 + } + ET7: { + argName: notes + maxLength: 32 + } + TM17: { + argName: submission + items: [ "save", "cancel" ] + submit: true + } + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:cancelSubmit + } + ] + + submit: { + *: [ + { + value: { "submission" : 0 } + action: @method:submitBBS + } + { + value: { "submission" : 1 } + action: @method:cancelSubmit + } + ] + } + } + } + } + + /////////////////////////////////////////////////////////////////////// + // Doors Menu + /////////////////////////////////////////////////////////////////////// + doorMenu: { + desc: Doors Menu + art: DOORMNU + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "G" } + action: @menu:logoff + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + // + // The system supports many ways of launching doors including + // modules for DoorParty!, BBSLink, etc. + // + // Below are some examples. See the documentation for more info. + // + { + value: { command: "ABRACADABRA" } + action: @menu:doorAbracadabraExample + } + { + value: { command: "TWBBSLINK" } + action: @menu:doorTradeWars2002BBSLinkExample + } + { + value: { command: "DP" } + action: @menu:doorPartyExample + } + { value: { command: "CN" } action: @menu:doorCombatNetExample } - { - value: { command: "EXODUS" } - action: @menu:doorExodusCataclysm - } - ] - } + { + value: { command: "EXODUS" } + action: @menu:doorExodusCataclysm + } + ] + } - // - // Local Door Example via abracadabra module - // - // This example assumes launch_door.sh (which is passed args) - // launches the door. - // - doorAbracadabraExample: { - desc: Abracadabra Example - module: abracadabra - config: { - name: Example Door - dropFileType: DORINFO - cmd: /home/enigma/DOS/scripts/launch_door.sh - args: [ - "{node}", - "{dropFile}", - "{srvPort}", - ], - nodeMax: 1 - tooManyArt: DOORMANY - io: socket - } - } - - // - // BBSLink Example (TradeWars 2000) - // - // Register @ https://bbslink.net/ - // - doorTradeWars2002BBSLinkExample: { - desc: Playing TW 2002 (BBSLink) - module: bbs_link - config: { - sysCode: XXXXXXXX - authCode: XXXXXXXX - schemeCode: XXXXXXXX - door: tw - } - } - - // - // DoorParty! Example - // - // Register @ http://throwbackbbs.com/ - // - doorPartyExample: { - desc: Using DoorParty! - module: door_party - config: { - username: XXXXXXXX - password: XXXXXXXX - bbsTag: XX - } - } - - // - // CombatNet Example // - // Register @ http://combatnet.us/ + // Local Door Example via abracadabra module + // + // This example assumes launch_door.sh (which is passed args) + // launches the door. + // + doorAbracadabraExample: { + desc: Abracadabra Example + module: abracadabra + config: { + name: Example Door + dropFileType: DORINFO + cmd: /home/enigma/DOS/scripts/launch_door.sh + args: [ + "{node}", + "{dropFile}", + "{srvPort}", + ], + nodeMax: 1 + tooManyArt: DOORMANY + io: socket + } + } + + // + // BBSLink Example (TradeWars 2000) + // + // Register @ https://bbslink.net/ + // + doorTradeWars2002BBSLinkExample: { + desc: Playing TW 2002 (BBSLink) + module: bbs_link + config: { + sysCode: XXXXXXXX + authCode: XXXXXXXX + schemeCode: XXXXXXXX + door: tw + } + } + + // + // DoorParty! Example + // + // Register @ http://throwbackbbs.com/ + // + doorPartyExample: { + desc: Using DoorParty! + module: door_party + config: { + username: XXXXXXXX + password: XXXXXXXX + bbsTag: XX + } + } + + // + // CombatNet Example + // + // Register @ http://combatnet.us/ // doorCombatNetExample: { desc: Using CombatNet @@ -1765,2316 +1765,2316 @@ } // - // Exodus Example (cataclysm) - // Register @ https://oddnetwork.org/exodus/ + // Exodus Example (cataclysm) + // Register @ https://oddnetwork.org/exodus/ // doorExodusCataclysm: { - desc: Cataclysm - module: exodus - config: { - rejectUnauthorized: false - board: XXX - key: XXXXXXXX - door: cataclysm - } - } - - /////////////////////////////////////////////////////////////////////// - // Message Area Menu - /////////////////////////////////////////////////////////////////////// - messageArea: { - art: MSGMNU - desc: Message Area - prompt: messageMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "P" } - action: @menu:messageAreaNewPost - } - { - value: { command: "J" } - action: @menu:messageAreaChangeCurrentConference - } - { - value: { command: "C" } - action: @menu:messageAreaChangeCurrentArea - } - { - value: { command: "L" } - action: @menu:messageAreaMessageList - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: { command: "<" } - action: @systemMethod:prevConf - } - { - value: { command: ">" } - action: @systemMethod:nextConf - } - { - value: { command: "[" } - action: @systemMethod:prevArea - } - { - value: { command: "]" } - action: @systemMethod:nextArea - } - { - value: { command: "D" } - action: @menu:messageAreaSetNewScanDate - } - { - value: { command: "S" } - action: @menu:messageSearch - } - { - value: 1 - action: @menu:messageArea - } - ] - } - - messageSearch: { - desc: Message Search - module: message_base_search - art: MSEARCH - config: { - messageListMenu: messageAreaSearchMessageList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - SM3: { - argName: confTag - } - SM4: { - argName: areaTag - } - ET5: { - argName: toUserName - maxLength: @config:users.usernameMax - } - ET6: { - argName: fromUserName - maxLength: @config:users.usernameMax - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSearchMessageList: { - desc: Message Search - module: msg_list - art: MSRCHLST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - TL6: { - // theme me! - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageSearchNoResults: { - desc: Message Search - art: MSRCNORES - config: { - pause: true - } - } - - messageAreaChangeCurrentConference: { - art: CCHANGE - module: msg_conf_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: conf - } - } - submit: { - *: [ - { - value: { conf: null } - action: @method:changeConference - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaSetNewScanDate: { - module: set_newscan_date - desc: Message Base - art: SETMNSDATE - config: { - target: message - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - SM2: { - argName: targetSelection - submit: false - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageConfPreArt: { - module: show_art - config: { - method: messageConf - key: confTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaChangeCurrentArea: { - // :TODO: rename this art to ACHANGE - art: CHANGE - module: msg_area_list - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: area - } - } - submit: { - *: [ - { - value: { area: null } - action: @method:changeArea - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - changeMessageAreaPreArt: { - module: show_art - config: { - method: messageArea - key: areaTag - pause: true - cls: true - menuFlags: [ "popParent", "noHistory" ] - } - } - - messageAreaMessageList: { - module: msg_list - art: MSGLIST - config: { - menuViewPost: messageAreaViewPost - } - form: { - 0: { - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - messageAreaViewPost: { - module: msg_area_view_fse - config: { - art: { - header: MSGVHDR - body: MSGBODY - footerView: MSGVFTR - help: MSGVHLP - }, - editorMode: view - editorType: area - } - form: { - 0: { - mci: { - // :TODO: ensure this block isn't even req. for theme to apply... - } - } - 1: { - mci: { - MT1: { - width: 79 - mode: preview - } - } - submit: { - *: [ - { - value: message - action: @method:editModeEscPressed - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 4: { - mci: { - HM1: { - // :TODO: (#)Jump/(L)Index (msg list)/Last - items: [ "prev", "next", "reply", "quit", "help" ] - focusItemIndex: 1 - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:prevMessage - } - { - value: { 1: 1 } - action: @method:nextMessage - } - { - value: { 1: 2 } - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - value: { 1: 3 } - action: @systemMethod:prevMenu - } - { - value: { 1: 4 } - action: @method:viewModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "p", "shift + p" ] - action: @method:prevMessage - } - { - keys: [ "n", "shift + n" ] - action: @method:nextMessage - } - { - keys: [ "r", "shift + r" ] - action: @method:replyMessage - extraArgs: { - menu: messageAreaReplyPost - } - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "?" ] - action: @method:viewModeMenuHelp - } - { - keys: [ "down arrow", "up arrow", "page up", "page down" ] - action: @method:movementKeyPressed - } - ] - } - } - } - - messageAreaReplyPost: { - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - quote: MSGQUOT - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - // :TODO: use appropriate system properties for max lengths - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - validate: @systemMethod:validateNonEmpty - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateNonEmpty - } - TL4: { - // :TODO: this is for RE: line (NYI) - //width: 27 - //textOverflow: ... - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - height: 14 - argName: message - mode: edit - } - } - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ], - viewId: 1 - } - ] - } - - 3: { - mci: { - HM1: { - items: [ "save", "discard", "quote", "help" ] - } - } - - 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" ] - 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 - 5: { - mci: { - MT1: { - width: 79 - height: 7 - } - VM3: { - width: 79 - height: 4 - argName: quote - } - } - - submit: { - *: [ - { - value: { quote: null } - action: @method:appendQuoteEntry - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @method:quoteBuilderEscPressed - } - ] - } - } - } - // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu - messageAreaNewPost: { - desc: Posting message, - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - } - editorMode: edit - editorType: area - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - text: All - validate: @systemMethod:validateNonEmpty - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateNonEmpty - // :TODO: Validate -> close/cancel if empty - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - - 1: { - "mci" : { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - } - 2: { - TLTL: { - mci: { - TL1: { width: 5 } - TL2: { width: 4 } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - "items" : [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - // :TODO: something like the following for overriding keymap - // this should only override specified entries. others will default - /* - "keyMap" : { - "accept" : [ "return" ] - } - */ - } - } - } - } - - - // - // User to User mail aka Email Menu - // - mailMenu: { - art: MAILMNU - desc: Mail Menu - prompt: menuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { command: "C" } - action: @menu:mailMenuCreateMessage - } - { - value: { command: "I" } - action: @menu:mailMenuInbox - } - { - value: { command: "Q" } - action: @systemMethod:prevMenu - } - { - value: { command: "G" } - action: @menu:fullLogoffSequence - } - { - value: 1 - action: @menu:mailMenu - } - ] - } - - mailMenuCreateMessage: { - desc: Mailing Someone - module: msg_area_post_fse - config: { - art: { - header: MSGEHDR - body: MSGBODY - footerEditor: MSGEFTR - footerEditorMenu: MSGEMFT - help: MSGEHLP - }, - editorMode: edit - editorType: email - messageAreaTag: private_mail - } - form: { - 0: { - mci: { - TL1: { - argName: from - } - ET2: { - argName: to - focus: true - validate: @systemMethod:validateGeneralMailAddressedTo - } - ET3: { - argName: subject - maxLength: 72 - submit: true - validate: @systemMethod:validateMessageSubject - } - } - submit: { - 3: [ - { - value: { subject: null } - action: @method:headerSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - 1: { - mci: { - MT1: { - width: 79 - argName: message - mode: edit - } - } - - submit: { - *: [ { value: "message", action: "@method:editModeEscPressed" } ] - } - actionKeys: [ - { - keys: [ "escape" ] - viewId: 1 - } - ] - }, - 2: { - TLTL: { - mci: { - TL1: { - width: 5 - } - TL2: { - width: 4 - } - } - } - } - 3: { - HM: { - mci: { - HM1: { - // :TODO: clear - items: [ "save", "discard", "help" ] - } - } - submit: { - *: [ - { - value: { 1: 0 } - action: @method:editModeMenuSave - } - { - value: { 1: 1 } - action: @systemMethod:prevMenu - } - { - value: { 1: 2 } - action: @method:editModeMenuHelp - } - ] - } - actionKeys: [ - { - keys: [ "escape" ] - action: @method:editModeEscPressed - } - { - keys: [ "?" ] - action: @method:editModeMenuHelp - } - ] - } - } - } - } - - mailMenuInbox: { - module: msg_list - art: PRVMSGLIST - config: { - menuViewPost: messageAreaViewPost - messageAreaTag: private_mail - } - form: { - 0: { // main list - mci: { - VM1: { - focus: true - submit: true - argName: message - } - } - submit: { - *: [ - { - value: { message: null } - action: @method:selectMessage - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - { - keys: [ "delete", "d", "shift + d" ] - action: @method:deleteSelected - } - ] - } - 1: { // delete prompt form - submit: { - *: [ - { - value: { promptValue: 0 } - action: @method:deleteMessageYes - } - { - value: { promptValue: 1 } - action: @method:deleteMessageNo - } - ] - } - } - } - } - - //////////////////////////////////////////////////////////////////////// - // File Base - //////////////////////////////////////////////////////////////////////// - - fileBase: { - desc: File Base - art: FMENU - prompt: fileMenuCommand - config: { - interrupt: realtime - } - submit: [ - { - value: { menuOption: "L" } - action: @menu:fileBaseListEntries - } - { - value: { menuOption: "B" } - action: @menu:fileBaseBrowseByAreaSelect - } - { - value: { menuOption: "F" } - action: @menu:fileAreaFilterEditor - } - { - value: { menuOption: "Q" } - action: @systemMethod:prevMenu - } - { - value: { menuOption: "G" } - action: @menu:fullLogoffSequence - } - { - value: { menuOption: "D" } - action: @menu:fileBaseDownloadManager - } - { - value: { menuOption: "W" } - action: @menu:fileBaseWebDownloadManager - } - { - value: { menuOption: "U" } - action: @menu:fileBaseUploadFiles - } - { - value: { menuOption: "S" } - action: @menu:fileBaseSearch - } - { - value: { menuOption: "P" } - action: @menu:fileBaseSetNewScanDate - } - { - value: { menuOption: "E" } - action: @menu:fileBaseExportListFilter - } - ] - } - - fileBaseExportListFilter: { - module: file_base_search - // :TODO: fixme: - art: FSEARCH - config: { - fileBaseListEntriesMenu: fileBaseExportList - } - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename" - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseExportList: { - module: file_base_user_list_export - art: FBLISTEXP - config: { - pause: true - templates: { - entry: file_list_entry.asc - } - } - form: { - 0: { - mci: { - TL1: { } - TL2: { } - } - } - } - } - - fileBaseExportListNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSetNewScanDate: { - module: set_newscan_date - desc: File Base - art: SETFNSDATE - config: { - target: file - scanDateFormat: YYYYMMDD - } - form: { - 0: { - mci: { - ME1: { - focus: true - submit: true - argName: scanDate - maskPattern: "####/##/##" - } - } - submit: { - *: [ - { - value: { scanDate: null } - action: @method:scanDateSubmit - } - ] - } - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseListEntries: { - module: file_area_list - desc: Browsing Files - config: { - art: { - browse: FBRWSE - details: FDETAIL - detailsGeneral: FDETGEN - detailsNfo: FDETNFO - detailsFileList: FDETLST - help: FBHELP - } - } - form: { - 0: { - mci: { - MT1: { - mode: preview - } - - HM2: { - focus: true - submit: true - argName: navSelect - items: [ - "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" - ] - focusItemIndex: 1 - } - } - - 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: @menu:fileAreaFilterEditor - } - { - value: { navSelect: 6 } - action: @method:displayHelp - } - { - value: { navSelect: 7 } - 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: [ "f", "shift + f" ] - action: @menu:fileAreaFilterEditor - } - { - 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" - ] - } - } - - 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: { - - } - } - } - } - } - - fileBaseBrowseByAreaSelect: { - desc: Browsing File Areas - module: file_base_area_select - art: FAREASEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: areaTag - } - } - - submit: { - *: [ - { - value: { areaTag: null } - action: @method:selectArea - } - ] - } - - actionKeys: [ - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseGetRatingForSelectedEntry: { - desc: Rating a File - prompt: fileBaseRateEntryPrompt - config: { - cls: true - } - submit: [ - // :TODO: handle esc/q - { - // pass data back to caller - value: { rating: null } - action: @systemMethod:prevMenu - } - ] - } - - fileBaseListEntriesNoResults: { - desc: Browsing Files - art: FBNORES - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileBaseSearch: { - module: file_base_search - desc: Searching Files - art: FSEARCH - form: { - 0: { - mci: { - ET1: { - focus: true - argName: searchTerms - } - BT2: { - argName: search - text: search - submit: true - } - ET3: { - maxLength: 64 - argName: tags - } - SM4: { - maxLength: 64 - argName: areaIndex - } - SM5: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - "filename", - ] - argName: sortByIndex - } - SM6: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - BT7: { - argName: advancedSearch - text: advanced search - submit: true - } - } - - submit: { - *: [ - { - value: { search: null } - action: @method:search - } - { - value: { advancedSearch: null } - action: @method:search - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileAreaFilterEditor: { - desc: File Filter Editor - module: file_area_filter_edit - art: FFILEDT - form: { - 0: { - mci: { - ET1: { - argName: searchTerms - } - ET2: { - maxLength: 64 - argName: tags - } - SM3: { - maxLength: 64 - argName: areaIndex - } - SM4: { - items: [ - "upload date", - "uploaded by", - "downloads", - "rating", - "estimated year", - "size", - ] - argName: sortByIndex - } - SM5: { - items: [ - "decending", - "ascending" - ] - argName: orderByIndex - } - ET6: { - maxLength: 64 - argName: name - validate: @systemMethod:validateNonEmpty - } - HM7: { - focus: true - items: [ - "prev", "next", "make active", "save", "new", "delete" - ] - argName: navSelect - focusItemIndex: 1 - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:prevFilter - } - { - value: { navSelect: 1 } - action: @method:nextFilter - } - { - value: { navSelect: 2 } - action: @method:makeFilterActive - } - { - value: { navSelect: 3 } - action: @method:saveFilter - } - { - value: { navSelect: 4 } - action: @method:newFilter - } - { - value: { navSelect: 5 } - action: @method:deleteFilter - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManager: { - desc: Download Manager - module: file_base_download_manager - config: { - art: { - queueManager: FDLMGR - /* - NYI - details: FDLDET - */ - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "download all", "quit" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:downloadAll - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "a", "shift + a" ] - action: @method:downloadAll - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseWebDownloadManager: { - desc: Web D/L Manager - module: file_base_web_download_manager - config: { - art: { - queueManager: FWDLMGR - batchList: BATDLINF - } - emptyQueueMenu: fileBaseDownloadManagerEmptyQueue - } - form: { - 0: { - mci: { - VM1: { - argName: queueItem - } - HM2: { - focus: true - items: [ "get batch link", "quit", "help" ] - argName: navSelect - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:getBatchLink - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - actionKeys: [ - { - keys: [ "b", "shift + b" ] - action: @method:getBatchLink - } - { - keys: [ "delete", "r", "shift + r" ] - action: @method:removeItem - } - { - keys: [ "c", "shift + c" ] - action: @method:clearQueue - } - { - keys: [ "escape", "q", "shift + q" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseDownloadManagerEmptyQueue: { - desc: Empty Download Queue - art: FEMPTYQ - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - fileTransferProtocolSelection: { - desc: Protocol selection - module: file_transfer_protocol_select - art: FPROSEL - form: { - 0: { - mci: { - VM1: { - focus: true - argName: protocol - } - } - - submit: { - *: [ - { - value: { protocol: null } - action: @method:selectProtocol - } - ] - } - - actionKeys: [ - { - keys: [ "escape" ] - action: @systemMethod:prevMenu - } - ] - } - } - } - - fileBaseUploadFiles: { - desc: Uploading - module: upload - config: { - art: { - options: ULOPTS - fileDetails: ULDETAIL - processing: ULCHECK - dupes: ULDUPES - } - } - - form: { - // options - 0: { - mci: { - SM1: { - argName: areaSelect - focus: true - } - TM2: { - argName: uploadType - items: [ "blind", "supply filename" ] - } - ET3: { - argName: fileName - maxLength: 255 - validate: @method:validateNonBlindFileName - } - HM4: { - argName: navSelect - items: [ "continue", "cancel" ] - submit: true - } - } - - submit: { - *: [ - { - value: { navSelect: 0 } - action: @method:optionsNavContinue - } - { - value: { navSelect: 1 } - action: @systemMethod:prevMenu - } - ] - } - - "actionKeys" : [ - { - "keys" : [ "escape" ], - action: @systemMethod:prevMenu - } - ] - } - - 1: { - mci: { } - } - - // file details entry - 2: { - mci: { - MT1: { - argName: shortDesc - tabSwitchesView: true - focus: true - } - - ET2: { - argName: tags - } - - ME3: { - argName: estYear - maskPattern: "####" - } - - BT4: { - argName: continue - text: continue - submit: true - } - } - - submit: { - *: [ - { - value: { continue: null } - action: @method:fileDetailsContinue - } - ] - } - } - - // dupes - 3: { - mci: { - VM1: { - /* - Use 'dupeInfoFormat' to custom format: - - areaDesc - areaName - areaTag - desc - descLong - fileId - fileName - fileSha256 - storageTag - uploadTimestamp - - */ - - mode: preview - } - } - } - } - } - - fileBaseNoUploadAreasAvail: { - desc: File Base - art: ULNOAREA - config: { - pause: true - menuFlags: [ "noHistory", "popParent" ] - } - } - - sendFilesToUser: { - desc: Downloading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: send - } - } - - recvFilesFromUser: { - desc: Uploading - module: file_transfer - config: { - // defaults - generally use extraArgs - protocol: zmodem8kSexyz - direction: recv - } - } - - - //////////////////////////////////////////////////////////////////////// - // Required entries - //////////////////////////////////////////////////////////////////////// - idleLogoff: { - art: IDLELOG - next: @systemMethod:logoff - } - //////////////////////////////////////////////////////////////////////// - // Demo Section - // :TODO: This entire section needs updated!!! - //////////////////////////////////////////////////////////////////////// - "demoMain" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Single Line Text Editing Views", - "Spinner & Toggle Views", - "Mask Edit Views", - "Multi Line Text Editor", - "Vertical Menu Views", - "Horizontal Menu Views", - "Art Display", - "Full Screen Editor" - ], - "height" : 10, - "itemSpacing" : 1, - "justify" : "center", - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoEditTextView" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoSpinAndToggleView" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoMaskEditView" - }, - { - "value" : { "1" : 3 }, - "action" : "@menu:demoMultiLineEditTextView" - }, - { - "value" : { "1" : 4 }, - "action" : "@menu:demoVerticalMenuView" - }, - { - "value" : { "1" : 5 }, - "action" : "@menu:demoHorizontalMenuView" - }, - { - "value" : { "1" : 6 }, - "action" : "@menu:demoArtDisplay" - }, - { - "value" : { "1" : 7 }, - "action" : "@menu:demoFullScreenEditor" - } - ] - } - } - } - } - }, - "demoEditTextView" : { - "art" : "demo_edit_text_view1.ans", - "form" : { - "0" : { - "BTETETETET" : { - "mci" : { - "ET1" : { - "width" : 20, - "maxLength" : 20 - }, - "ET2" : { - "width" : 20, - "maxLength" : 40, - "textOverflow" : "..." - }, - "ET3" : { - "width" : 20, - "fillChar" : "-", - "styleSGR1" : "|00|36", - "maxLength" : 20 - }, - "ET4" : { - "width" : 20, - "maxLength" : 20, - "password" : true - }, - "BT5" : { - "width" : 8, - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoSpinAndToggleView" : { - "art" : "demo_spin_and_toggle.ans", - "form" : { - "0" : { - "BTSMSMTM" : { - "mci" : { - "SM1" : { - "items" : [ "Henry Morgan", "François l'Ollonais", "Roche Braziliano", "Black Bart", "Blackbeard" ] - }, - "SM2" : { - "items" : [ "Razor 1911", "DrinkOrDie", "TRSI" ] - }, - "TM3" : { - "items" : [ "Yarly", "Nowaii" ], - "styleSGR1" : "|00|30|01", - "hotKeys" : { "Y" : 0, "N" : 1 } - }, - "BT8" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 8, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 8 - } - ] - } - } - } - }, - "demoMaskEditView" : { - "art" : "demo_mask_edit_text_view1.ans", - "form" : { - "0" : { - "BTMEME" : { - "mci" : { - "ME1" : { - "maskPattern" : "##/##/##", - "styleSGR1" : "|00|30|01", - //"styleSGR2" : "|00|45|01", - "styleSGR3" : "|00|30|35", - "fillChar" : "#" - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoMultiLineEditTextView" : { - "art" : "demo_multi_line_edit_text_view1.ans", - "form" : { - "0" : { - "BTMT" : { - "mci" : { - "MT1" : { - "width" : 70, - "height" : 17, - //"text" : "@art:demo_multi_line_edit_text_view_text.txt", - // "text" : "@systemMethod:textFromFile" - text: "Hints:\n\t* Insert / CTRL-V toggles overtype mode\n\t* CTRL-Y deletes the current line\n\t* Try Page Up / Page Down\n\t* Home goes to the start of line text\n\t* End goes to the end of a line\n\n\nTab handling:\n-------------------------------------------------\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\nA0\tBB\t1\tCCC\t2\tDDD\t3EEEE\nW\t\tX\t\tY\t\tZ\n\nAn excerpt from A Clockwork Orange:\n\"What sloochatted then, of course, was that my cellmates woke up and started joining in, tolchocking a bit wild in the near-dark, and the shoom seemed to wake up the whole tier, so that you could slooshy a lot of creeching and banging about with tin mugs on the wall, as though all the plennies in all the cells thought a big break was about to commence, O my brothers.\n", - "focus" : true - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoHorizontalMenuView" : { - "art" : "demo_horizontal_menu_view1.ans", - "form" : { - "0" : { - "BTHMHM" : { - "mci" : { - "HM1" : { - "items" : [ "One", "Two", "Three" ], - "hotKeys" : { "1" : 0, "2" : 1, "3" : 2 } - }, - "HM2" : { - "items" : [ "Uno", "Dos", "Tres" ], - "hotKeys" : { "U" : 0, "D" : 1, "T" : 2 } - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - }, - "demoVerticalMenuView" : { - "art" : "demo_vertical_menu_view1.ans", - "form" : { - "0" : { - "BTVM" : { - "mci" : { - "VM1" : { - "items" : [ - "|33Oblivion/2", - "|33iNiQUiTY", - "|33ViSiON/X" - ], - "focusItems" : [ - "|33Oblivion|01/|00|332", - "|01|33i|00|33N|01i|00|33QU|01i|00|33TY", - "|33ViSiON/X" - ] - // - // :TODO: how to do the following: - // 1) Supply a view a string for a standard vs focused item - // "items" : [...], "focusItems" : [ ... ] ? - // "draw" : "@method:drawItemX", then items: [...] - }, - "BT5" : { - "text" : "< Back" - } - }, - "submit" : { - "*" : [ - { - "value" : 5, - "action" : "@menu:demoMain" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 5 - } - ] - } - } - } - - }, - "demoArtDisplay" : { - "art" : "demo_selection_vm.ans", - "form" : { - "0" : { - "VM" : { - "mci" : { - "VM1" : { - "items" : [ - "Defaults - DOS ANSI", - "bw_mindgames.ans - DOS", - "test.ans - DOS", - "Defaults - Amiga", - "Pause at Term Height" - ], - // :TODO: justify not working?? - "focusTextStyle" : "small i" - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@menu:demoDefaultsDosAnsi" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoDefaultsDosAnsi_bw_mindgames" - }, - { - "value" : { "1" : 2 }, - "action" : "@menu:demoDefaultsDosAnsi_test" - } - ] - } - } - } - } - }, - "demoDefaultsDosAnsi" : { - "art" : "DM-ENIG2.ANS" - }, - "demoDefaultsDosAnsi_bw_mindgames" : { - "art" : "bw_mindgames.ans" - }, - "demoDefaultsDosAnsi_test" : { - "art" : "test.ans" - }, - "demoFullScreenEditor" : { - "module" : "fse", - "config" : { - "editorType" : "netMail", - "art" : { - "header" : "demo_fse_netmail_header.ans", - "body" : "demo_fse_netmail_body.ans", - "footerEditor" : "demo_fse_netmail_footer_edit.ans", - "footerEditorMenu" : "demo_fse_netmail_footer_edit_menu.ans", - "footerView" : "demo_fse_netmail_footer_view.ans", - "help" : "demo_fse_netmail_help.ans" - } - }, - "form" : { - "0" : { - "ETETET" : { - "mci" : { - "ET1" : { - // :TODO: from/to may be set by args - // :TODO: focus may change dep on view vs edit - "width" : 36, - "focus" : true, - "argName" : "to" - }, - "ET2" : { - "width" : 36, - "argName" : "from" - }, - "ET3" : { - "width" : 65, - "maxLength" : 72, - "submit" : [ "enter" ], - "argName" : "subject" - } - }, - "submit" : { - "3" : [ - { - "value" : { "subject" : null }, - "action" : "@method:headerSubmit" - } - ] - } - } - }, - "1" : { - "MT" : { - "mci" : { - "MT1" : { - "width" : 79, - "height" : 17, - "text" : "", // :TODO: should not be req. - "argName" : "message" - } - }, - "submit" : { - "*" : [ - { - "value" : "message", - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ - { - "keys" : [ "escape" ], - "viewId" : 1 - } - ] - } - }, - "2" : { - "TLTL" : { - "mci" : { - "TL1" : { - "width" : 5 - }, - "TL2" : { - "width" : 4 - } - } - } - }, - "3" : { - "HM" : { - "mci" : { - "HM1" : { - // :TODO: Continue, Save, Discard, Clear, Quote, Help - "items" : [ "Save", "Discard", "Quote", "Help" ] - } - }, - "submit" : { - "*" : [ - { - "value" : { "1" : 0 }, - "action" : "@method:editModeMenuSave" - }, - { - "value" : { "1" : 1 }, - "action" : "@menu:demoMain" - }, - { - "value" : { "1" : 2 }, - "action" : "@method:editModeMenuQuote" - }, - { - "value" : { "1" : 3 }, - "action" : "@method:editModeMenuHelp" - }, - { - "value" : 1, - "action" : "@method:editModeEscPressed" - } - ] - }, - "actionKeys" : [ // :TODO: Need better name - { - "keys" : [ "escape" ], - "action" : "@method:editModeEscPressed" - } - ] - } - } - } - } - } + desc: Cataclysm + module: exodus + config: { + rejectUnauthorized: false + board: XXX + key: XXXXXXXX + door: cataclysm + } + } + + /////////////////////////////////////////////////////////////////////// + // Message Area Menu + /////////////////////////////////////////////////////////////////////// + messageArea: { + art: MSGMNU + desc: Message Area + prompt: messageMenuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "P" } + action: @menu:messageAreaNewPost + } + { + value: { command: "J" } + action: @menu:messageAreaChangeCurrentConference + } + { + value: { command: "C" } + action: @menu:messageAreaChangeCurrentArea + } + { + value: { command: "L" } + action: @menu:messageAreaMessageList + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: { command: "<" } + action: @systemMethod:prevConf + } + { + value: { command: ">" } + action: @systemMethod:nextConf + } + { + value: { command: "[" } + action: @systemMethod:prevArea + } + { + value: { command: "]" } + action: @systemMethod:nextArea + } + { + value: { command: "D" } + action: @menu:messageAreaSetNewScanDate + } + { + value: { command: "S" } + action: @menu:messageSearch + } + { + value: 1 + action: @menu:messageArea + } + ] + } + + messageSearch: { + desc: Message Search + module: message_base_search + art: MSEARCH + config: { + messageListMenu: messageAreaSearchMessageList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + SM3: { + argName: confTag + } + SM4: { + argName: areaTag + } + ET5: { + argName: toUserName + maxLength: @config:users.usernameMax + } + ET6: { + argName: fromUserName + maxLength: @config:users.usernameMax + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaSearchMessageList: { + desc: Message Search + module: msg_list + art: MSRCHLST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + TL6: { + // theme me! + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageSearchNoResults: { + desc: Message Search + art: MSRCNORES + config: { + pause: true + } + } + + messageAreaChangeCurrentConference: { + art: CCHANGE + module: msg_conf_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: conf + } + } + submit: { + *: [ + { + value: { conf: null } + action: @method:changeConference + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaSetNewScanDate: { + module: set_newscan_date + desc: Message Base + art: SETMNSDATE + config: { + target: message + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + SM2: { + argName: targetSelection + submit: false + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + changeMessageConfPreArt: { + module: show_art + config: { + method: messageConf + key: confTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + + messageAreaChangeCurrentArea: { + // :TODO: rename this art to ACHANGE + art: CHANGE + module: msg_area_list + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: area + } + } + submit: { + *: [ + { + value: { area: null } + action: @method:changeArea + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + changeMessageAreaPreArt: { + module: show_art + config: { + method: messageArea + key: areaTag + pause: true + cls: true + menuFlags: [ "popParent", "noHistory" ] + } + } + + messageAreaMessageList: { + module: msg_list + art: MSGLIST + config: { + menuViewPost: messageAreaViewPost + } + form: { + 0: { + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + messageAreaViewPost: { + module: msg_area_view_fse + config: { + art: { + header: MSGVHDR + body: MSGBODY + footerView: MSGVFTR + help: MSGVHLP + }, + editorMode: view + editorType: area + } + form: { + 0: { + mci: { + // :TODO: ensure this block isn't even req. for theme to apply... + } + } + 1: { + mci: { + MT1: { + width: 79 + mode: preview + } + } + submit: { + *: [ + { + value: message + action: @method:editModeEscPressed + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 4: { + mci: { + HM1: { + // :TODO: (#)Jump/(L)Index (msg list)/Last + items: [ "prev", "next", "reply", "quit", "help" ] + focusItemIndex: 1 + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:prevMessage + } + { + value: { 1: 1 } + action: @method:nextMessage + } + { + value: { 1: 2 } + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + value: { 1: 3 } + action: @systemMethod:prevMenu + } + { + value: { 1: 4 } + action: @method:viewModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "p", "shift + p" ] + action: @method:prevMessage + } + { + keys: [ "n", "shift + n" ] + action: @method:nextMessage + } + { + keys: [ "r", "shift + r" ] + action: @method:replyMessage + extraArgs: { + menu: messageAreaReplyPost + } + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "?" ] + action: @method:viewModeMenuHelp + } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } + ] + } + } + } + + messageAreaReplyPost: { + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + quote: MSGQUOT + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + // :TODO: use appropriate system properties for max lengths + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateNonEmpty + } + TL4: { + // :TODO: this is for RE: line (NYI) + //width: 27 + //textOverflow: ... + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + height: 14 + argName: message + mode: edit + } + } + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ], + viewId: 1 + } + ] + } + + 3: { + mci: { + HM1: { + items: [ "save", "discard", "quote", "help" ] + } + } + + 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" ] + 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 + 5: { + mci: { + MT1: { + width: 79 + height: 7 + } + VM3: { + width: 79 + height: 4 + argName: quote + } + } + + submit: { + *: [ + { + value: { quote: null } + action: @method:appendQuoteEntry + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @method:quoteBuilderEscPressed + } + ] + } + } + } + // :TODO: messageAreaSelect (change msg areas -> call @systemMethod -> fallback to menu + messageAreaNewPost: { + desc: Posting message, + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + } + editorMode: edit + editorType: area + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + text: All + validate: @systemMethod:validateNonEmpty + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateNonEmpty + // :TODO: Validate -> close/cancel if empty + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + + 1: { + "mci" : { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { "value": "message", "action": "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + } + 2: { + TLTL: { + mci: { + TL1: { width: 5 } + TL2: { width: 4 } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + "items" : [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + // :TODO: something like the following for overriding keymap + // this should only override specified entries. others will default + /* + "keyMap" : { + "accept" : [ "return" ] + } + */ + } + } + } + } + + + // + // User to User mail aka Email Menu + // + mailMenu: { + art: MAILMNU + desc: Mail Menu + prompt: menuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { command: "C" } + action: @menu:mailMenuCreateMessage + } + { + value: { command: "I" } + action: @menu:mailMenuInbox + } + { + value: { command: "Q" } + action: @systemMethod:prevMenu + } + { + value: { command: "G" } + action: @menu:fullLogoffSequence + } + { + value: 1 + action: @menu:mailMenu + } + ] + } + + mailMenuCreateMessage: { + desc: Mailing Someone + module: msg_area_post_fse + config: { + art: { + header: MSGEHDR + body: MSGBODY + footerEditor: MSGEFTR + footerEditorMenu: MSGEMFT + help: MSGEHLP + }, + editorMode: edit + editorType: email + messageAreaTag: private_mail + } + form: { + 0: { + mci: { + TL1: { + argName: from + } + ET2: { + argName: to + focus: true + validate: @systemMethod:validateGeneralMailAddressedTo + } + ET3: { + argName: subject + maxLength: 72 + submit: true + validate: @systemMethod:validateMessageSubject + } + } + submit: { + 3: [ + { + value: { subject: null } + action: @method:headerSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + 1: { + mci: { + MT1: { + width: 79 + argName: message + mode: edit + } + } + + submit: { + *: [ { value: "message", action: "@method:editModeEscPressed" } ] + } + actionKeys: [ + { + keys: [ "escape" ] + viewId: 1 + } + ] + }, + 2: { + TLTL: { + mci: { + TL1: { + width: 5 + } + TL2: { + width: 4 + } + } + } + } + 3: { + HM: { + mci: { + HM1: { + // :TODO: clear + items: [ "save", "discard", "help" ] + } + } + submit: { + *: [ + { + value: { 1: 0 } + action: @method:editModeMenuSave + } + { + value: { 1: 1 } + action: @systemMethod:prevMenu + } + { + value: { 1: 2 } + action: @method:editModeMenuHelp + } + ] + } + actionKeys: [ + { + keys: [ "escape" ] + action: @method:editModeEscPressed + } + { + keys: [ "?" ] + action: @method:editModeMenuHelp + } + ] + } + } + } + } + + mailMenuInbox: { + module: msg_list + art: PRVMSGLIST + config: { + menuViewPost: messageAreaViewPost + messageAreaTag: private_mail + } + form: { + 0: { // main list + mci: { + VM1: { + focus: true + submit: true + argName: message + } + } + submit: { + *: [ + { + value: { message: null } + action: @method:selectMessage + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + { + keys: [ "delete", "d", "shift + d" ] + action: @method:deleteSelected + } + ] + } + 1: { // delete prompt form + submit: { + *: [ + { + value: { promptValue: 0 } + action: @method:deleteMessageYes + } + { + value: { promptValue: 1 } + action: @method:deleteMessageNo + } + ] + } + } + } + } + + //////////////////////////////////////////////////////////////////////// + // File Base + //////////////////////////////////////////////////////////////////////// + + fileBase: { + desc: File Base + art: FMENU + prompt: fileMenuCommand + config: { + interrupt: realtime + } + submit: [ + { + value: { menuOption: "L" } + action: @menu:fileBaseListEntries + } + { + value: { menuOption: "B" } + action: @menu:fileBaseBrowseByAreaSelect + } + { + value: { menuOption: "F" } + action: @menu:fileAreaFilterEditor + } + { + value: { menuOption: "Q" } + action: @systemMethod:prevMenu + } + { + value: { menuOption: "G" } + action: @menu:fullLogoffSequence + } + { + value: { menuOption: "D" } + action: @menu:fileBaseDownloadManager + } + { + value: { menuOption: "W" } + action: @menu:fileBaseWebDownloadManager + } + { + value: { menuOption: "U" } + action: @menu:fileBaseUploadFiles + } + { + value: { menuOption: "S" } + action: @menu:fileBaseSearch + } + { + value: { menuOption: "P" } + action: @menu:fileBaseSetNewScanDate + } + { + value: { menuOption: "E" } + action: @menu:fileBaseExportListFilter + } + ] + } + + fileBaseExportListFilter: { + module: file_base_search + // :TODO: fixme: + art: FSEARCH + config: { + fileBaseListEntriesMenu: fileBaseExportList + } + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename" + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseExportList: { + module: file_base_user_list_export + art: FBLISTEXP + config: { + pause: true + templates: { + entry: file_list_entry.asc + } + } + form: { + 0: { + mci: { + TL1: { } + TL2: { } + } + } + } + } + + fileBaseExportListNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileBaseSetNewScanDate: { + module: set_newscan_date + desc: File Base + art: SETFNSDATE + config: { + target: file + scanDateFormat: YYYYMMDD + } + form: { + 0: { + mci: { + ME1: { + focus: true + submit: true + argName: scanDate + maskPattern: "####/##/##" + } + } + submit: { + *: [ + { + value: { scanDate: null } + action: @method:scanDateSubmit + } + ] + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseListEntries: { + module: file_area_list + desc: Browsing Files + config: { + art: { + browse: FBRWSE + details: FDETAIL + detailsGeneral: FDETGEN + detailsNfo: FDETNFO + detailsFileList: FDETLST + help: FBHELP + } + } + form: { + 0: { + mci: { + MT1: { + mode: preview + } + + HM2: { + focus: true + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "change filter", "help", "quit" + ] + focusItemIndex: 1 + } + } + + 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: @menu:fileAreaFilterEditor + } + { + value: { navSelect: 6 } + action: @method:displayHelp + } + { + value: { navSelect: 7 } + 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: [ "f", "shift + f" ] + action: @menu:fileAreaFilterEditor + } + { + 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" + ] + } + } + + 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: { + + } + } + } + } + } + + fileBaseBrowseByAreaSelect: { + desc: Browsing File Areas + module: file_base_area_select + art: FAREASEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: areaTag + } + } + + submit: { + *: [ + { + value: { areaTag: null } + action: @method:selectArea + } + ] + } + + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseGetRatingForSelectedEntry: { + desc: Rating a File + prompt: fileBaseRateEntryPrompt + config: { + cls: true + } + submit: [ + // :TODO: handle esc/q + { + // pass data back to caller + value: { rating: null } + action: @systemMethod:prevMenu + } + ] + } + + fileBaseListEntriesNoResults: { + desc: Browsing Files + art: FBNORES + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileBaseSearch: { + module: file_base_search + desc: Searching Files + art: FSEARCH + form: { + 0: { + mci: { + ET1: { + focus: true + argName: searchTerms + } + BT2: { + argName: search + text: search + submit: true + } + ET3: { + maxLength: 64 + argName: tags + } + SM4: { + maxLength: 64 + argName: areaIndex + } + SM5: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + "filename", + ] + argName: sortByIndex + } + SM6: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + BT7: { + argName: advancedSearch + text: advanced search + submit: true + } + } + + submit: { + *: [ + { + value: { search: null } + action: @method:search + } + { + value: { advancedSearch: null } + action: @method:search + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileAreaFilterEditor: { + desc: File Filter Editor + module: file_area_filter_edit + art: FFILEDT + form: { + 0: { + mci: { + ET1: { + argName: searchTerms + } + ET2: { + maxLength: 64 + argName: tags + } + SM3: { + maxLength: 64 + argName: areaIndex + } + SM4: { + items: [ + "upload date", + "uploaded by", + "downloads", + "rating", + "estimated year", + "size", + ] + argName: sortByIndex + } + SM5: { + items: [ + "decending", + "ascending" + ] + argName: orderByIndex + } + ET6: { + maxLength: 64 + argName: name + validate: @systemMethod:validateNonEmpty + } + HM7: { + focus: true + items: [ + "prev", "next", "make active", "save", "new", "delete" + ] + argName: navSelect + focusItemIndex: 1 + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:prevFilter + } + { + value: { navSelect: 1 } + action: @method:nextFilter + } + { + value: { navSelect: 2 } + action: @method:makeFilterActive + } + { + value: { navSelect: 3 } + action: @method:saveFilter + } + { + value: { navSelect: 4 } + action: @method:newFilter + } + { + value: { navSelect: 5 } + action: @method:deleteFilter + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManager: { + desc: Download Manager + module: file_base_download_manager + config: { + art: { + queueManager: FDLMGR + /* + NYI + details: FDLDET + */ + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "download all", "quit" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:downloadAll + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "a", "shift + a" ] + action: @method:downloadAll + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseWebDownloadManager: { + desc: Web D/L Manager + module: file_base_web_download_manager + config: { + art: { + queueManager: FWDLMGR + batchList: BATDLINF + } + emptyQueueMenu: fileBaseDownloadManagerEmptyQueue + } + form: { + 0: { + mci: { + VM1: { + argName: queueItem + } + HM2: { + focus: true + items: [ "get batch link", "quit", "help" ] + argName: navSelect + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:getBatchLink + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + actionKeys: [ + { + keys: [ "b", "shift + b" ] + action: @method:getBatchLink + } + { + keys: [ "delete", "r", "shift + r" ] + action: @method:removeItem + } + { + keys: [ "c", "shift + c" ] + action: @method:clearQueue + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseDownloadManagerEmptyQueue: { + desc: Empty Download Queue + art: FEMPTYQ + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + fileTransferProtocolSelection: { + desc: Protocol selection + module: file_transfer_protocol_select + art: FPROSEL + form: { + 0: { + mci: { + VM1: { + focus: true + argName: protocol + } + } + + submit: { + *: [ + { + value: { protocol: null } + action: @method:selectProtocol + } + ] + } + + actionKeys: [ + { + keys: [ "escape" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + + fileBaseUploadFiles: { + desc: Uploading + module: upload + config: { + art: { + options: ULOPTS + fileDetails: ULDETAIL + processing: ULCHECK + dupes: ULDUPES + } + } + + form: { + // options + 0: { + mci: { + SM1: { + argName: areaSelect + focus: true + } + TM2: { + argName: uploadType + items: [ "blind", "supply filename" ] + } + ET3: { + argName: fileName + maxLength: 255 + validate: @method:validateNonBlindFileName + } + HM4: { + argName: navSelect + items: [ "continue", "cancel" ] + submit: true + } + } + + submit: { + *: [ + { + value: { navSelect: 0 } + action: @method:optionsNavContinue + } + { + value: { navSelect: 1 } + action: @systemMethod:prevMenu + } + ] + } + + "actionKeys" : [ + { + "keys" : [ "escape" ], + action: @systemMethod:prevMenu + } + ] + } + + 1: { + mci: { } + } + + // file details entry + 2: { + mci: { + MT1: { + argName: shortDesc + tabSwitchesView: true + focus: true + } + + ET2: { + argName: tags + } + + ME3: { + argName: estYear + maskPattern: "####" + } + + BT4: { + argName: continue + text: continue + submit: true + } + } + + submit: { + *: [ + { + value: { continue: null } + action: @method:fileDetailsContinue + } + ] + } + } + + // dupes + 3: { + mci: { + VM1: { + /* + Use 'dupeInfoFormat' to custom format: + + areaDesc + areaName + areaTag + desc + descLong + fileId + fileName + fileSha256 + storageTag + uploadTimestamp + + */ + + mode: preview + } + } + } + } + } + + fileBaseNoUploadAreasAvail: { + desc: File Base + art: ULNOAREA + config: { + pause: true + menuFlags: [ "noHistory", "popParent" ] + } + } + + sendFilesToUser: { + desc: Downloading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: send + } + } + + recvFilesFromUser: { + desc: Uploading + module: file_transfer + config: { + // defaults - generally use extraArgs + protocol: zmodem8kSexyz + direction: recv + } + } + + + //////////////////////////////////////////////////////////////////////// + // Required entries + //////////////////////////////////////////////////////////////////////// + idleLogoff: { + art: IDLELOG + next: @systemMethod:logoff + } + //////////////////////////////////////////////////////////////////////// + // Demo Section + // :TODO: This entire section needs updated!!! + //////////////////////////////////////////////////////////////////////// + "demoMain" : { + "art" : "demo_selection_vm.ans", + "form" : { + "0" : { + "VM" : { + "mci" : { + "VM1" : { + "items" : [ + "Single Line Text Editing Views", + "Spinner & Toggle Views", + "Mask Edit Views", + "Multi Line Text Editor", + "Vertical Menu Views", + "Horizontal Menu Views", + "Art Display", + "Full Screen Editor" + ], + "height" : 10, + "itemSpacing" : 1, + "justify" : "center", + "focusTextStyle" : "small i" + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@menu:demoEditTextView" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoSpinAndToggleView" + }, + { + "value" : { "1" : 2 }, + "action" : "@menu:demoMaskEditView" + }, + { + "value" : { "1" : 3 }, + "action" : "@menu:demoMultiLineEditTextView" + }, + { + "value" : { "1" : 4 }, + "action" : "@menu:demoVerticalMenuView" + }, + { + "value" : { "1" : 5 }, + "action" : "@menu:demoHorizontalMenuView" + }, + { + "value" : { "1" : 6 }, + "action" : "@menu:demoArtDisplay" + }, + { + "value" : { "1" : 7 }, + "action" : "@menu:demoFullScreenEditor" + } + ] + } + } + } + } + }, + "demoEditTextView" : { + "art" : "demo_edit_text_view1.ans", + "form" : { + "0" : { + "BTETETETET" : { + "mci" : { + "ET1" : { + "width" : 20, + "maxLength" : 20 + }, + "ET2" : { + "width" : 20, + "maxLength" : 40, + "textOverflow" : "..." + }, + "ET3" : { + "width" : 20, + "fillChar" : "-", + "styleSGR1" : "|00|36", + "maxLength" : 20 + }, + "ET4" : { + "width" : 20, + "maxLength" : 20, + "password" : true + }, + "BT5" : { + "width" : 8, + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoSpinAndToggleView" : { + "art" : "demo_spin_and_toggle.ans", + "form" : { + "0" : { + "BTSMSMTM" : { + "mci" : { + "SM1" : { + "items" : [ "Henry Morgan", "François l'Ollonais", "Roche Braziliano", "Black Bart", "Blackbeard" ] + }, + "SM2" : { + "items" : [ "Razor 1911", "DrinkOrDie", "TRSI" ] + }, + "TM3" : { + "items" : [ "Yarly", "Nowaii" ], + "styleSGR1" : "|00|30|01", + "hotKeys" : { "Y" : 0, "N" : 1 } + }, + "BT8" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 8, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 8 + } + ] + } + } + } + }, + "demoMaskEditView" : { + "art" : "demo_mask_edit_text_view1.ans", + "form" : { + "0" : { + "BTMEME" : { + "mci" : { + "ME1" : { + "maskPattern" : "##/##/##", + "styleSGR1" : "|00|30|01", + //"styleSGR2" : "|00|45|01", + "styleSGR3" : "|00|30|35", + "fillChar" : "#" + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoMultiLineEditTextView" : { + "art" : "demo_multi_line_edit_text_view1.ans", + "form" : { + "0" : { + "BTMT" : { + "mci" : { + "MT1" : { + "width" : 70, + "height" : 17, + //"text" : "@art:demo_multi_line_edit_text_view_text.txt", + // "text" : "@systemMethod:textFromFile" + text: "Hints:\n\t* Insert / CTRL-V toggles overtype mode\n\t* CTRL-Y deletes the current line\n\t* Try Page Up / Page Down\n\t* Home goes to the start of line text\n\t* End goes to the end of a line\n\n\nTab handling:\n-------------------------------------------------\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\n\tA\tB\tC\tD\tE\tF\nA\tB\tC\tD\tE\tF\tG\tH\nA0\tBB\t1\tCCC\t2\tDDD\t3EEEE\nW\t\tX\t\tY\t\tZ\n\nAn excerpt from A Clockwork Orange:\n\"What sloochatted then, of course, was that my cellmates woke up and started joining in, tolchocking a bit wild in the near-dark, and the shoom seemed to wake up the whole tier, so that you could slooshy a lot of creeching and banging about with tin mugs on the wall, as though all the plennies in all the cells thought a big break was about to commence, O my brothers.\n", + "focus" : true + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoHorizontalMenuView" : { + "art" : "demo_horizontal_menu_view1.ans", + "form" : { + "0" : { + "BTHMHM" : { + "mci" : { + "HM1" : { + "items" : [ "One", "Two", "Three" ], + "hotKeys" : { "1" : 0, "2" : 1, "3" : 2 } + }, + "HM2" : { + "items" : [ "Uno", "Dos", "Tres" ], + "hotKeys" : { "U" : 0, "D" : 1, "T" : 2 } + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + }, + "demoVerticalMenuView" : { + "art" : "demo_vertical_menu_view1.ans", + "form" : { + "0" : { + "BTVM" : { + "mci" : { + "VM1" : { + "items" : [ + "|33Oblivion/2", + "|33iNiQUiTY", + "|33ViSiON/X" + ], + "focusItems" : [ + "|33Oblivion|01/|00|332", + "|01|33i|00|33N|01i|00|33QU|01i|00|33TY", + "|33ViSiON/X" + ] + // + // :TODO: how to do the following: + // 1) Supply a view a string for a standard vs focused item + // "items" : [...], "focusItems" : [ ... ] ? + // "draw" : "@method:drawItemX", then items: [...] + }, + "BT5" : { + "text" : "< Back" + } + }, + "submit" : { + "*" : [ + { + "value" : 5, + "action" : "@menu:demoMain" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 5 + } + ] + } + } + } + + }, + "demoArtDisplay" : { + "art" : "demo_selection_vm.ans", + "form" : { + "0" : { + "VM" : { + "mci" : { + "VM1" : { + "items" : [ + "Defaults - DOS ANSI", + "bw_mindgames.ans - DOS", + "test.ans - DOS", + "Defaults - Amiga", + "Pause at Term Height" + ], + // :TODO: justify not working?? + "focusTextStyle" : "small i" + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@menu:demoDefaultsDosAnsi" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoDefaultsDosAnsi_bw_mindgames" + }, + { + "value" : { "1" : 2 }, + "action" : "@menu:demoDefaultsDosAnsi_test" + } + ] + } + } + } + } + }, + "demoDefaultsDosAnsi" : { + "art" : "DM-ENIG2.ANS" + }, + "demoDefaultsDosAnsi_bw_mindgames" : { + "art" : "bw_mindgames.ans" + }, + "demoDefaultsDosAnsi_test" : { + "art" : "test.ans" + }, + "demoFullScreenEditor" : { + "module" : "fse", + "config" : { + "editorType" : "netMail", + "art" : { + "header" : "demo_fse_netmail_header.ans", + "body" : "demo_fse_netmail_body.ans", + "footerEditor" : "demo_fse_netmail_footer_edit.ans", + "footerEditorMenu" : "demo_fse_netmail_footer_edit_menu.ans", + "footerView" : "demo_fse_netmail_footer_view.ans", + "help" : "demo_fse_netmail_help.ans" + } + }, + "form" : { + "0" : { + "ETETET" : { + "mci" : { + "ET1" : { + // :TODO: from/to may be set by args + // :TODO: focus may change dep on view vs edit + "width" : 36, + "focus" : true, + "argName" : "to" + }, + "ET2" : { + "width" : 36, + "argName" : "from" + }, + "ET3" : { + "width" : 65, + "maxLength" : 72, + "submit" : [ "enter" ], + "argName" : "subject" + } + }, + "submit" : { + "3" : [ + { + "value" : { "subject" : null }, + "action" : "@method:headerSubmit" + } + ] + } + } + }, + "1" : { + "MT" : { + "mci" : { + "MT1" : { + "width" : 79, + "height" : 17, + "text" : "", // :TODO: should not be req. + "argName" : "message" + } + }, + "submit" : { + "*" : [ + { + "value" : "message", + "action" : "@method:editModeEscPressed" + } + ] + }, + "actionKeys" : [ + { + "keys" : [ "escape" ], + "viewId" : 1 + } + ] + } + }, + "2" : { + "TLTL" : { + "mci" : { + "TL1" : { + "width" : 5 + }, + "TL2" : { + "width" : 4 + } + } + } + }, + "3" : { + "HM" : { + "mci" : { + "HM1" : { + // :TODO: Continue, Save, Discard, Clear, Quote, Help + "items" : [ "Save", "Discard", "Quote", "Help" ] + } + }, + "submit" : { + "*" : [ + { + "value" : { "1" : 0 }, + "action" : "@method:editModeMenuSave" + }, + { + "value" : { "1" : 1 }, + "action" : "@menu:demoMain" + }, + { + "value" : { "1" : 2 }, + "action" : "@method:editModeMenuQuote" + }, + { + "value" : { "1" : 3 }, + "action" : "@method:editModeMenuHelp" + }, + { + "value" : 1, + "action" : "@method:editModeEscPressed" + } + ] + }, + "actionKeys" : [ // :TODO: Need better name + { + "keys" : [ "escape" ], + "action" : "@method:editModeEscPressed" + } + ] + } + } + } + } + } } From 33478354488a3a42cfe31a57b53cd9f0633fff94 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 17:07:48 -0700 Subject: [PATCH 495/569] Header to achievements.hjson --- config/achievements.hjson | 42 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/config/achievements.hjson b/config/achievements.hjson index cb58ddd1..4100d2dd 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -1,3 +1,43 @@ + /* + ./\/\.' ENiGMA½ Achievement Configuration -/--/-------- - -- - + + _____________________ _____ ____________________ __________\_ / + \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! + // __|___// | \// |// | \// | | \// \ /___ /_____ + /____ _____| __________ ___|__| ____| \ / _____ \ + ---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ + /__ _\ + <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ + + *-----------------------------------------------------------------------------* + + General Information + ------------------------------- - - + This configuration is in HJSON (http://hjson.org/) format. Strict to-spec + JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. + + See http://hjson.org/ for more information and syntax. + + Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so + on have syntax highlighting for the HJSON format which are highly recommended. + + ------------------------------- -- - - + Achievement Configuration + ------------------------------- - - + Achievements are currently fairly limited in what can trigger them. This is + being expanded upon and more will be available in the near future. For now + you should mostly be interested in: + - Perhaps adding additional *levels* of triggers & points + - Applying customizations via the achievements section in theme.hjson + + Don't forget to RTFM ...er, uh... see the documentation for more information, and + don't be shy to ask for help: + + BBS : Xibalba @ xibalba.l33t.codes + FTN : BBS Discussion on fsxNet or ArakNet + IRC : #enigma-bbs / FreeNode + Email : bryan@l33t.codes +*/ { enabled : true, @@ -8,8 +48,6 @@ globalFooter : 'achievement_global_footer', }, - // :TODO: achievements should be a path/filename -> achievements.hjson & allow override/theming - achievements : { user_login_count : { type : 'userStat', From 3d07f763d1861c0821042c60ebe607c75ab6d95d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 19:04:19 -0700 Subject: [PATCH 496/569] Achievement improvement & more achievements --- art/themes/luciano_blocktronics/STATUS.ANS | Bin 4517 -> 4686 bytes config/achievements.hjson | 203 ++++++++++++++++----- core/achievement.js | 7 +- 3 files changed, 159 insertions(+), 51 deletions(-) diff --git a/art/themes/luciano_blocktronics/STATUS.ANS b/art/themes/luciano_blocktronics/STATUS.ANS index b90ed2d9ffede5f3db62c67ff44131899d870bca..679e21d691e6973dfd63e41c32257b29735eff56 100644 GIT binary patch delta 526 zcmZ3gd`@M;5lIEdU)z8IDI@%ytI(p)9Ep=lD; zzzQf1RsnW}XMiEp8HNUv7x0Tr_GNA20b2|-1Eg~DBsQDLd~A&zK<68QgeF(8YfL`C zwwWDj{$yJYeMZB{3)$ySHsn;8oXT;O4{8@!InabRoLZA3IH$9g>gNFOd2V8IMrLYRYHn&?2}IlETfEOyK#m7m zo19;oR|0d16~rm3j^Ky@8h@DYrZz+j6y^mb#c-8ClMtcn7yvPN^Fsc6jEqkwHwdZ% E0Ky)RGynhq delta 335 zcmX@7vQ&A(5oYOV!-;pbj0{bkfh=odvs?w~Xaj3wBM@zDo(mK-fyfx--oDMHaQik; z8YF7ES&uQ9nFYu)oV=8!RZBYB0;mqCKQBKeRY3u$8)%wAt`*1_vs_g_7c;0v!^xSf z(*z;HnFUZwOr0kmWEW-e3@|jCypTg=vKw0yGf>H7ZZ@0AZ0t)}fouco$rso(7>y=B zVxK>GE4$9*6&(9m%z$=IuH#UiJe_kMW6I>eT+%Eq-iAh#L%Ak0OGlec*5^{=fZJ=y zy=d}3E+tlwo1G^s^Hehf-8uORx8CHX9Q>2l@=Or{83zso>1a@RfIMnB*_rnSNQ=4i gWE;L`%!U@ulim1lf<(-mH+u=(V`Q|MtSO`l0RH`7od5s; diff --git a/config/achievements.hjson b/config/achievements.hjson index 4100d2dd..8f1babfe 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -1,5 +1,5 @@ /* - ./\/\.' ENiGMA½ Achievement Configuration -/--/-------- - -- - + ./\/\." ENiGMA½ Achievement Configuration -/--/-------- - -- - _____________________ _____ ____________________ __________\_ / \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! @@ -14,11 +14,11 @@ General Information ------------------------------- - - This configuration is in HJSON (http://hjson.org/) format. Strict to-spec - JSON is also perfectly valid. Use 'hjson' from npm to convert to/from JSON. + JSON is also perfectly valid. Use "hjson" from npm to convert to/from JSON. See http://hjson.org/ for more information and syntax. - Various editors and IDEs such as Sublime Text 3, Visual Studio Code, and so + Various editors and IDEs such as Sublime Text 3 Visual Studio Code and so on have syntax highlighting for the HJSON format which are highly recommended. ------------------------------- -- - - @@ -30,8 +30,11 @@ - Perhaps adding additional *levels* of triggers & points - Applying customizations via the achievements section in theme.hjson - Don't forget to RTFM ...er, uh... see the documentation for more information, and - don't be shy to ask for help: + Some tips: + - For 'userStat' types, see user_property.js + + Don"t forget to RTFM ...er uh... see the documentation for more information and + don"t be shy to ask for help: BBS : Xibalba @ xibalba.l33t.codes FTN : BBS Discussion on fsxNet or ArakNet @@ -39,51 +42,159 @@ Email : bryan@l33t.codes */ { - enabled : true, + enabled : true art : { - localHeader : 'achievement_local_header', - localFooter : 'achievement_local_footer', - globalHeader : 'achievement_global_header', - globalFooter : 'achievement_global_footer', - }, + localHeader: achievement_local_header + localFooter: achievement_local_footer + globalHeader: achievement_global_header + globalFooter: achievement_global_footer + } - achievements : { - user_login_count : { - type : 'userStat', - statName : 'login_count', - retroactive : true, + achievements: { + user_login_count: { + type: userStat + statName: login_count + retroactive: true + match: { + 2: { + title: "Return Caller" + globalText: "{userName} has returned to {boardName}!" + text: "You\"ve returned to {boardName}!" + points: 5 + } + 10: { + title: "{boardName} Curious" + globalText: "{userName} has logged into {boardName} {achievedValue} times!" + text: "You've logged into {boardName} {achievedValue} times!" + points: 5 + } + 25: { + title: "{boardName} Inquisitive" + globalText: "{userName} has logged into {boardName} {achievedValue} times!" + text: "You've logged into {boardName} {achievedValue} times!" + points: 10 + } + 100: { + title: "{boardName} Regular" + globalText: "{userName} has logged into {boardName} {achievedValue} times!" + text: "You've logged into {boardName} {achievedValue} times!" + points: 10 + } + 500: { + title: "{boardName} Addict" + globalText: "{userName} the BBS {boardName} addict has logged in {achievedValue} times!" + text: "You're a {boardName} addict! You've logged in {achievedValue} times!" + points: 25 + } + } + } - match : { - 2 : { - title : 'Return Caller', - globalText : '{userName} has returned to {boardName}!', - text : 'You\'ve returned to {boardName}!', - points : 5, - }, - 10 : { - title : '{achievedValue} Logins', - globalText : '{userName} has logged into {boardName} {achievedValue} times!', - text : 'You\'ve logged into {boardName} {achievedValue} times!', - points : 5, - }, - 25 : { - title : '{achievedValue} Logins', - globalText : '{userName} has logged into {boardName} {achievedValue} times!', - text : 'You\'ve logged into {boardName} {achievedValue} times!', - points : 10, - }, - 100 : { - title : '{boardName} Regular', - globalText : '{userName} has logged into {boardName} {achievedValue} times!', - text : 'You\'ve logged into {boardName} {achievedValue} times!', - points : 10, - }, - 500 : { - title : '{boardName} Addict', - globalText : '{userName} the BBS {boardName} addict has logged in {achievedValue} times!', - text : 'You\'re a {boardName} addict! You\'ve logged in {achievedValue} times!', - points : 25, + user_post_count: { + type: userStat + statName: post_count + retroactive: true + match: { + 5: { + title: "Poster" + globalText: "{userName} has posted {achievedValue} messages!" + text: "You've posted {achievedValue} messages!" + points: 5 + } + 20: { + title: "Poster... again!", + globalText: "{userName} has posted {achievedValue} messages!" + text: "You've posted {achievedValue} messages!" + points: 10 + } + 100: { + title: "Frequent Poster", + globalText: "{userName} has posted {achievedValue} messages!" + text: "You've posted {achievedValue} messages!" + points: 15 + } + 500: { + title: "Scribe" + globalText: "{userName} the scribe has posted {achievedValue} messages!" + text: "Such a scribe! You've posted {achievedValue} messages!" + points: 25 + } + } + } + + user_upload_count: { + type: userStat + statName: ul_total_count + retroactive: true + match: { + 1: { + title: "Uploader" + globalText: "{userName} has uploaded a file!" + text: "You've uploaded somthing!" + points: 5 + } + 10: { + title: "Moar Uploads!" + globalText: "{userName} has uploaded {achievedValue} files!" + text: "You've uploaded {achievedValue} files!" + points: 10 + } + 50: { + title: "Contributor" + globalText: "{userName} has uploaded {achievedValue} files!" + text: "You've uploaded {achievedValue} files!" + points: 20 + + } + 100: { + title: "Courier" + globalText: "Courier {userName} has uploaded {achievedValue} files!" + text: "You've uploaded {achievedValue} files!" + points: 25 + } + 200: { + title: "Must Be a Drop Site" + globalText: "{userName} has uploaded a whomping {achievedValue} files!" + text: "You've uploaded a whomping {achievedValue} files!" + points: 50 + } + } + } + + user_download_count: { + type: userStat + statName: dl_total_count + retroactive: true + match: { + 1: { + title: "Downloader" + globalText: "{userName} has downloaded a file!" + text: "You've downloaded somthing!" + points: 5 + } + 10: { + title: "Moar Downloads!" + globalText: "{userName} has downloaded {achievedValue} files!" + text: "You've downloaded {achievedValue} files!" + points: 10 + } + 50: { + title: "Leecher" + globalText: "{userName} has leeched {achievedValue} files!" + text: "You've leeched... er... downloaded {achievedValue} files!" + points: 15 + } + 100: { + title: "Hoarder" + globalText: "{userName} has downloaded {achievedValue} files!" + text: "Hoarding files? You've downloaded {achievedValue} files!" + points: 20 + } + 200: { + title: "Digital Archivist" + globalText: "{userName} the digital archivist has {achievedValue} files!" + text: "Building an archive? You've downloaded {achievedValue} files!" + points: 25 } } } diff --git a/core/achievement.js b/core/achievement.js index cc64779c..e5eecd87 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -159,10 +159,6 @@ class Achievements { return cb(null); } ); - - // :TODO: if enabled/etc., load achievements.hjson -> if theme achievements.hjson{}, merge @ display time? - // merge for local vs global (per theme) clients - // ...only merge/override text } loadAchievementHitCount(user, achievementTag, field, cb) { @@ -404,7 +400,8 @@ class Achievements { pause : true, }; if(headerArt || footerArt) { - interruptItems[itemType].contents = `${headerArt || ''}\r\n${pipeToAnsi(title)}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`; + interruptItems[itemType].contents = + `${headerArt || ''}\r\n${pipeToAnsi(title)}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`; } return callback(null); } From 209e3f1f1d6e6868a589461bde5df57d072c6a82 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 21:16:57 -0700 Subject: [PATCH 497/569] Update copyright --- LICENSE.TXT | 2 +- README.md | 3 ++- docs/installation/testing.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/LICENSE.TXT b/LICENSE.TXT index 8db0cf42..74697ba9 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -1,4 +1,4 @@ -Copyright (c) 2015-2018, Bryan D. Ashby +Copyright (c) 2015-2019, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index a0bde9d0..1d178892 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ ENiGMA½ is a modern BBS software with a nostalgic flair! * [Gazelle](https://github.com/WhatCD/Gazelle) inspired File Bases including fast fully indexed full text search (FTS), #tags, and HTTP(S) temporary download URLs using a built in [web server](docs/servers/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! * ANSI support in the Full Screen Editor (FSE), file descriptions, etc. + * A built in achievement system. BBSing gamified! ## Documentation [Browse the docs online](https://nuskooler.github.io/enigma-bbs/). Be sure to checkout the [/docs/](/docs/) folder as well for the latest and greatest documentation. @@ -84,7 +85,7 @@ Please see [Installation Methods](https://nuskooler.github.io/enigma-bbs/install ## License Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license: -Copyright (c) 2015-2018, Bryan D. Ashby +Copyright (c) 2015-2019, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/installation/testing.md b/docs/installation/testing.md index 9238fc61..b23616f2 100644 --- a/docs/installation/testing.md +++ b/docs/installation/testing.md @@ -13,7 +13,7 @@ _Note that if you've used the [Docker](docker) installation method, you've alrea If everything went OK: ```bash -ENiGMA½ Copyright (c) 2014-2018 Bryan Ashby +ENiGMA½ Copyright (c) 2014-2019 Bryan Ashby _____________________ _____ ____________________ __________\_ / \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! // __|___// | \// |// | \// | | \// \ /___ /_____ From 9d39e99c5a71fc3fda593b8a84bf7bb38b60b4ee Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 21:17:18 -0700 Subject: [PATCH 498/569] Update copyright --- core/connect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/connect.js b/core/connect.js index b9316a3e..cda3fe8a 100644 --- a/core/connect.js +++ b/core/connect.js @@ -132,7 +132,7 @@ function displayBanner(term) { // note: intentional formatting: term.pipeWrite(` |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN -|06Copyright (c) 2014-2018 Bryan Ashby |14- |12http://l33t.codes/ +|06Copyright (c) 2014-2019 Bryan Ashby |14- |12http://l33t.codes/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |00` ); From 22b7fdd65c9107be8be3e3cbe2f94bb7dcc8adb1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 21:17:53 -0700 Subject: [PATCH 499/569] Add door stats & new mini format styles + Door runs stat + Door run minutes stat + Door runs MCI + Door run friendly duration MCI + durationHours/Minutes/Seconds mini format styles --- config/achievements.hjson | 70 +++++++++++++++++++++++++++++++++++++++ core/abracadabra.js | 12 +++++++ core/predefined_mci.js | 6 ++++ core/string_format.js | 5 +++ core/user_property.js | 3 ++ docs/art/mci.md | 20 +++++++++++ 6 files changed, 116 insertions(+) diff --git a/config/achievements.hjson b/config/achievements.hjson index 8f1babfe..ce841e93 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -198,5 +198,75 @@ } } } + + user_door_runs: { + type: userStat + statName: door_run_total_count + retroactive: true + match: { + 1: { + title: "Nostalgia Toe Dip", + globalText: "{userName} ran a door!" + text: "You ran a door!" + points: 5 + }, + 10: { + title: "This is Kinda Fun" + globalText: "{userName} ran {achievedValue} doors!" + text: "You've run {achievedValue} doors!" + points: 10 + } + 50: { + title: "Gamer" + globalText: "{userName} ran {achievedValue} doors!" + text: "You've run {achievedValue} doors!" + points: 15 + } + 100: { + title: "Textmode is All You Need" + globalText: "{userName} must really like textmode and has run {achievedValue} doors!" + text: "You've run {achievedValue} doors! You must really like textmode!" + points: 25 + } + 200: { + title: "Dropfile Enthusiast" + globalText: "{userName} the dropfile enthusiast ran {achievedValue} doors!" + text: "You're a dropfile enthusiast! You've run {achievedValue} doors!" + points: 50 + } + } + } + + user_door_total_minutes: { + type: userStat + statName: door_run_total_minutes + retroactive: true + match: { + 1: { + title: "Nevermind!" + globalText: "{userName} ran a door for {achievedValue!durationSeconds}. Guess it's not their thing!" + text: "You ran a door for only {achievedValue!durationSeconds}. Not your thing?" + points: 5 + } + 10: { + title: "It's OK I Guess" + globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" + text: "You ran a door for {achievedValue!durationSeconds}!" + points: 10 + } + 30: { + title: "Good Game" + globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" + text: "You ran a door for {achievedValue!durationSeconds}!" + points: 20 + } + 60: { + title: "Textmode Dragon Slayer" + globalText: "{userName} has spent {achievedValue!durationSeconds} in a door!" + text: "You've spent {achievedValue!durationSeconds} in a door!" + points: 25 + } + } + } } } \ No newline at end of file diff --git a/core/abracadabra.js b/core/abracadabra.js index e448315b..42731ac0 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -8,12 +8,15 @@ const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); const Events = require('./events.js'); const { Errors } = require('./enig_error.js'); +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); const paths = require('path'); +const moment = require('moment'); const activeDoorNodeInstances = {}; @@ -149,6 +152,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { } runDoor() { + StatLog.incrementUserStat(this.client.user, UserProps.DoorRunTotalCount, 1); Events.emit(Events.getSystemEvents().UserRunDoor, { user : this.client.user } ); this.client.term.write(ansi.resetScreen()); @@ -164,7 +168,15 @@ exports.getModule = class AbracadabraModule extends MenuModule { node : this.client.node, }; + const startTime = moment(); + this.doorInstance.run(exeInfo, () => { + const endTime = moment(); + const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + if(runTimeMinutes > 0) { + StatLog.incrementUserStat(this.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + } + // // Try to clean up various settings such as scroll regions that may // have been set within the door diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 01b1e285..39e339cc 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -155,6 +155,12 @@ const PREDEFINED_MCI_GENERATORS = { AC : function achievementCount(client) { return userStatAsString(client, UserProps.AchievementTotalCount, 0); }, AP : function achievementPoints(client) { return userStatAsString(client, UserProps.AchievementTotalPoints, 0); }, + DR : function doorRuns(client) { return userStatAsString(client, UserProps.DoorRunTotalCount, 0); }, + DM : function doorFriendlyRunTime(client) { + const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0; + return moment.duration(minutes, 'minutes').humanize(); + }, + // // Date/Time // diff --git a/core/string_format.js b/core/string_format.js index a756db72..4a5b110c 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -14,6 +14,7 @@ const { // deps const _ = require('lodash'); +const moment = require('moment'); /* String formatting HEAVILY inspired by David Chambers string-format library @@ -281,6 +282,10 @@ const transformers = { countWithAbbr : (n) => formatCount(n, true, 0), countWithoutAbbr : (n) => formatCount(n, false, 0), countAbbr : (n) => formatCountAbbr(n), + + durationHours : (h) => moment.duration(h, 'hours').humanize(), + durationMinutes : (m) => moment.duration(m, 'minutes').humanize(), + durationSeconds : (s) => moment.duration(s, 'seconds').humanize(), }; function transformValue(transformerName, value) { diff --git a/core/user_property.js b/core/user_property.js index dafd7170..a1489e82 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -50,6 +50,9 @@ module.exports = { MessageAreaTag : 'message_area_tag', MessagePostCount : 'post_count', + DoorRunTotalCount : 'door_run_total_count', + DoorRunTotalMinutes : 'door_run_total_minutes', + AchievementTotalCount : 'achievement_total_count', AchievementTotalPoints : 'achievement_total_points', }; diff --git a/docs/art/mci.md b/docs/art/mci.md index 5ec86805..3e5e18c5 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -60,6 +60,8 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `SW` | Current user's term width | | `AC` | Current user's total achievements | | `AP` | Current user's total achievement points | +| `DR` | Current user's number of door runs | +| `DM` | Current user's total amount of time spent in doors | | `DT` | Current date (using theme date format) | | `CT` | Current time (using theme time format) | | `OS` | System OS (Linux, Windows, etc.) | @@ -155,6 +157,21 @@ Standard style types available for `textStyle` and `focusTextStyle`: Various strings can be formatted using a syntax that allows width & precision specifiers, text styling, etc. Depending on the context, various elements can be referenced by `{name}`. Additional text styles can be supplied as well. The syntax is largely modeled after Python's [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language). ### Additional Text Styles +Some of the text styles mentioned above are also available in the mini format language: + +| Style | Description | +|-------|-------------| +| `normal` | Leaves text as-is. This is the default. | +| `toUpperCase` or `styleUpper` | ENIGMA BULLETIN BOARD SOFTWARE | +| `toLowerCase` or `styleLower` | enigma bulletin board software | +| `styleTitle` | Enigma Bulletin Board Software | +| `styleFirstLower` | eNIGMA bULLETIN bOARD sOFTWARE | +| `styleSmallVowels` | eNiGMa BuLLeTiN BoaRD SoFTWaRe | +| `styleBigVowels` | EniGMa bUllEtIn bOArd sOftwArE | +| `styleSmallI` | ENiGMA BULLETiN BOARD SOFTWARE | +| `styleMixed` | EnIGma BUlLEtIn BoaRd SOfTWarE (randomly assigned) | +| `styleL33t` | 3n1gm4 bull371n b04rd 50f7w4r3 | + Additional text styles are available for numbers: | Style | Description | @@ -165,6 +182,9 @@ Additional text styles are available for numbers: | `countWithAbbr` | Count with abbreviation such as `100 K`, `4.3 B`, etc. | | `countWithoutAbbr` | Just the count | | `countAbbr` | Just the abbreviation such as `M` for millions. | +| `durationHours` | Converts the provided *hours* value to something friendly such as `4 hours`, or `4 days`. | +| `durationMinutes` | Converts the provided *minutes* to something friendly such as `10 minutes` or `2 hours` | +| `durationSeconds` | Converts the provided *seconds* to something friendly such as `23 seconds` or `2 minutes` | #### Examples From 6496fd931aa5ed07ce5fdeea7fd989c47164105e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 21:19:43 -0700 Subject: [PATCH 500/569] + Missing art for luciano_blocktronics - node messaging & new achievement stuff. Placeholder mostly for now. --- art/themes/luciano_blocktronics/NODEMSG.ANS | Bin 0 -> 1696 bytes art/themes/luciano_blocktronics/NODEMSGFTR.ANS | Bin 0 -> 221 bytes art/themes/luciano_blocktronics/NODEMSGHDR.ANS | Bin 0 -> 249 bytes .../achievement_global_footer.ans | Bin 0 -> 221 bytes .../achievement_global_header.ans | Bin 0 -> 247 bytes .../achievement_local_footer.ans | Bin 0 -> 221 bytes .../achievement_local_header.ans | Bin 0 -> 247 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 art/themes/luciano_blocktronics/NODEMSG.ANS create mode 100644 art/themes/luciano_blocktronics/NODEMSGFTR.ANS create mode 100644 art/themes/luciano_blocktronics/NODEMSGHDR.ANS create mode 100644 art/themes/luciano_blocktronics/achievement_global_footer.ans create mode 100644 art/themes/luciano_blocktronics/achievement_global_header.ans create mode 100644 art/themes/luciano_blocktronics/achievement_local_footer.ans create mode 100644 art/themes/luciano_blocktronics/achievement_local_header.ans diff --git a/art/themes/luciano_blocktronics/NODEMSG.ANS b/art/themes/luciano_blocktronics/NODEMSG.ANS new file mode 100644 index 0000000000000000000000000000000000000000..ebe742df8fce316add2404cc4ff8a93a102cfc65 GIT binary patch literal 1696 zcmb_c!EVz)5KS)}a^S)ZOR$$7k=E;ALoE(zi7F%lNC{kgszNA8-Et@&R`qvmY5xTA z-psDOiF(A5lI+gR+xOngy69}Xux-`&dC@f&MOW2+7zSfZ(UooGurRX3df|NE;|Ce| zww^gCm1Ws3>XedH1XfFW{0DN1Al(<;YY-@j&c$1@asIEQ)ZTph{C;KD zn@c?v0Wbz^10Ihu0gncR@hBrI;D)RMOSp_NihM*(EXic2l91AY0PzjQIjBfkFVvFB0aHkQ`Mskb;0qgpJAgOC9i`|w zs#tm+l}>6>zIIU}+V}`T)Cfa$P!)1nK_4nU#!Mm}9?}_8yO^)mPA5jOsiE=wycYk} zlWbPe_@lEYS7R4PXH=t+S&>N#Y(kpTwz3e1Pd`Wn+cyqTfbVE+(be%zWxR2|*i0fI zcz>UFRV03w<=72KW}%1lNZN2m-zLhP6Lq{v_^E}}XtYo#C+Xn&yLAqn6GkDEtZ!@xvyv>RytR36PtNzNt?VVED1kb$8q zIPK5x$kd!g&$!?C)6KdpI_DIm_Ag$lcNO0}A_cHGU9k9`^LN)Va0UeHPgh%q4p(D3 zvt)5>}p`3Ks{h&U}#}zXlNYEz`(%B7{I^?q=7)# O)5%vM%-s>fgOdPMvQ=aN literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/NODEMSGHDR.ANS b/art/themes/luciano_blocktronics/NODEMSGHDR.ANS new file mode 100644 index 0000000000000000000000000000000000000000..9e38285aaea354711e7f6bd00807e17cc7e35a76 GIT binary patch literal 249 zcmb1+Hn27^ur@Z&<>Hc#HncW2$W^#=|IY2(_Z2|2f;3Rx*eLfOkO7o5%k}ejaaHhj z4Gwm6cSVyl$h{BL0fM(N08kWe_EfNa_Fp literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/achievement_global_footer.ans b/art/themes/luciano_blocktronics/achievement_global_footer.ans new file mode 100644 index 0000000000000000000000000000000000000000..8cb30568bc48597a44aa443e7be088ff6e00d505 GIT binary patch literal 221 zcmb1+Hn29dHZia^Hpo@DLsh^f73>)5>}p`3Ks{h&U}#}zXlNYEz`(%B7{I^?q=7)# O)5%vM%-s>fgOdPMvQ=aN literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/achievement_global_header.ans b/art/themes/luciano_blocktronics/achievement_global_header.ans new file mode 100644 index 0000000000000000000000000000000000000000..87592fa3fcc8fda4bdcde2b176a922a0355a26b4 GIT binary patch literal 247 zcmb1+Hn27^ur@Z&<>Hc#HncW2$W^#=|IY2(_Z2|2f;3Rx*eLfOkO7o5%XM`2@C-uQo)X)&aMUq3e*Ee28I@fhK9zK3=9m6i~$VH TKpF^yJ)L|N!rUDpJU9seA0$YD literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/achievement_local_footer.ans b/art/themes/luciano_blocktronics/achievement_local_footer.ans new file mode 100644 index 0000000000000000000000000000000000000000..8cb30568bc48597a44aa443e7be088ff6e00d505 GIT binary patch literal 221 zcmb1+Hn29dHZia^Hpo@DLsh^f73>)5>}p`3Ks{h&U}#}zXlNYEz`(%B7{I^?q=7)# O)5%vM%-s>fgOdPMvQ=aN literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/achievement_local_header.ans b/art/themes/luciano_blocktronics/achievement_local_header.ans new file mode 100644 index 0000000000000000000000000000000000000000..87592fa3fcc8fda4bdcde2b176a922a0355a26b4 GIT binary patch literal 247 zcmb1+Hn27^ur@Z&<>Hc#HncW2$W^#=|IY2(_Z2|2f;3Rx*eLfOkO7o5%XM`2@C-uQo)X)&aMUq3e*Ee28I@fhK9zK3=9m6i~$VH TKpF^yJ)L|N!rUDpJU9seA0$YD literal 0 HcmV?d00001 From 2b802cb53439c2448b096cf105a2440fc4a9efce Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 5 Jan 2019 22:51:16 -0700 Subject: [PATCH 501/569] Better theming for achievements --- .../achievement_global_header.ans | Bin 247 -> 240 bytes .../achievement_local_header.ans | Bin 247 -> 240 bytes art/themes/luciano_blocktronics/theme.hjson | 5 +- config/achievements.hjson | 8 +-- core/achievement.js | 57 ++++++++++++------ 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/art/themes/luciano_blocktronics/achievement_global_header.ans b/art/themes/luciano_blocktronics/achievement_global_header.ans index 87592fa3fcc8fda4bdcde2b176a922a0355a26b4..6104c2caccd3734f4fe8705453da53311734ffab 100644 GIT binary patch delta 19 bcmey)_!UQz`BM@9zu delta 44 zcmeys_?>Zrxa{rQ_Z1ZG+`pqB9c^H3Y?S*C$S|}vHp_K%_VApjJD0I?;%QX?pYIVa diff --git a/art/themes/luciano_blocktronics/achievement_local_header.ans b/art/themes/luciano_blocktronics/achievement_local_header.ans index 87592fa3fcc8fda4bdcde2b176a922a0355a26b4..6104c2caccd3734f4fe8705453da53311734ffab 100644 GIT binary patch delta 19 bcmey)_!UQz`BM@9zu delta 44 zcmeys_?>Zrxa{rQ_Z1ZG+`pqB9c^H3Y?S*C$S|}vHp_K%_VApjJD0I?;%QX?pYIVa diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index d38137c8..28f17ff9 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -983,7 +983,10 @@ achievements: { defaults: { - titleSGR: "|11" + format: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" + globalFformat: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" + titleSGR: "|10" + pointsSGR: "|12" textSGR: "|00|03" globalTextSGR: "|03" boardName: "|10" diff --git a/config/achievements.hjson b/config/achievements.hjson index ce841e93..41bfd4c1 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -64,25 +64,25 @@ points: 5 } 10: { - title: "{boardName} Curious" + title: "Curious Caller" globalText: "{userName} has logged into {boardName} {achievedValue} times!" text: "You've logged into {boardName} {achievedValue} times!" points: 5 } 25: { - title: "{boardName} Inquisitive" + title: "Inquisitive Caller" globalText: "{userName} has logged into {boardName} {achievedValue} times!" text: "You've logged into {boardName} {achievedValue} times!" points: 10 } 100: { - title: "{boardName} Regular" + title: "Regular Customer" globalText: "{userName} has logged into {boardName} {achievedValue} times!" text: "You've logged into {boardName} {achievedValue} times!" points: 10 } 500: { - title: "{boardName} Addict" + title: "System Addict" globalText: "{userName} the BBS {boardName} addict has logged in {achievedValue} times!" text: "You're a {boardName} addict! You've logged in {achievedValue} times!" points: 25 diff --git a/core/achievement.js b/core/achievement.js index e5eecd87..adfae332 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -314,29 +314,37 @@ class Achievements { } } - getFormattedTextFor(info, textType) { + getFormatObject(info) { + return { + userName : info.user.username, + userRealName : info.user.properties[UserProps.RealName], + userLocation : info.user.properties[UserProps.Location], + userAffils : info.user.properties[UserProps.Affiliations], + nodeId : info.client.node, + title : info.details.title, + text : info.global ? info.details.globalText : info.details.text, + points : info.details.points, + achievedValue : info.achievedValue, + matchField : info.matchField, + matchValue : info.matchValue, + timestamp : moment(info.timestamp).format(info.dateTimeFormat), + boardName : Config().general.boardName, + }; + } + + getFormattedTextFor(info, textType, defaultSgr = '|07') { const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); - const defSgr = themeDefaults[`${textType}SGR`] || '|07'; + const defSgr = themeDefaults[`${textType}SGR`] || defaultSgr; const wrap = (fieldName, value) => { return `${themeDefaults[fieldName] || defSgr}${value}${defSgr}`; }; - const formatObj = { - userName : wrap('userName', info.user.username), - userRealName : wrap('userRealName', info.user.properties[UserProps.RealName]), - userLocation : wrap('userLocation', info.user.properties[UserProps.Location]), - userAffils : wrap('userAffils', info.user.properties[UserProps.Affiliations]), - nodeId : wrap('nodeId', info.client.node), - title : wrap('title', info.details.title), - text : wrap('text', info.global ? info.details.globalText : info.details.text), - points : wrap('points', info.details.points), - achievedValue : wrap('achievedValue', info.achievedValue), - matchField : wrap('matchField', info.matchField), - matchValue : wrap('matchValue', info.matchValue), - timestamp : wrap('timestamp', moment(info.timestamp).format(info.dateTimeFormat)), - boardName : wrap('boardName', Config().general.boardName), - }; + let formatObj = this.getFormatObject(info); + formatObj = _.reduce(formatObj, (out, v, k) => { + out[k] = wrap(k, v); + return out; + }, {}); return stringFormat(`${defSgr}${info.details[textType]}`, formatObj); } @@ -400,8 +408,21 @@ class Achievements { pause : true, }; if(headerArt || footerArt) { + const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); + const defaultContentsFormat = '{title}\r\n${message}'; + const contentsFormat = 'global' === itemType ? + themeDefaults.globalFormat || defaultContentsFormat : + themeDefaults.format || defaultContentsFormat; + + const formatObj = Object.assign(this.getFormatObject(info), { + title : this.getFormattedTextFor(info, 'title', ''), // ''=defaultSgr + message : itemText, + }); + + const contents = pipeToAnsi(stringFormat(contentsFormat, formatObj)); + interruptItems[itemType].contents = - `${headerArt || ''}\r\n${pipeToAnsi(title)}\r\n${pipeToAnsi(itemText)}\r\n${footerArt || ''}`; + `${headerArt || ''}\r\n${contents}\r\n${footerArt || ''}`; } return callback(null); } From f653d83c1447c9aef69b3891bbc51072094484d9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 6 Jan 2019 10:41:04 -0700 Subject: [PATCH 502/569] Implement retroactive achievements (for userStat types so far) --- art/themes/luciano_blocktronics/STATUS.ANS | Bin 4686 -> 4638 bytes art/themes/luciano_blocktronics/theme.hjson | 2 +- config/achievements.hjson | 8 +-- core/achievement.js | 56 ++++++++++++++++---- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/art/themes/luciano_blocktronics/STATUS.ANS b/art/themes/luciano_blocktronics/STATUS.ANS index 679e21d691e6973dfd63e41c32257b29735eff56..6871ff457b0b3d44ec533e785fac3b02aa03586e 100644 GIT binary patch delta 253 zcmX@7GEZf~5oYOV!-;q8r4^*3jm>iN@>5cQEW=z?KNmCUXoFk@AegMr=*$6@F4fPQ ze1S=ZH&->(&k!UHRBAMN31bnX!DMkJ!^w*o#U{rwWiT2}KF(yqXf*i`Q`6*nW-TCV z74!DV=`1>vC$X&Lwgy_1SpYQ9Ja@7Ss|rwcChLsJ+-!P4byjQ>CtqY!2eN*$ZDq8c zyop^O$bQ5=eR36t-sB%_ypy+bY-Ir&kTUr%m-OVxoU<9tC+l+^pZuLmYqA3OCPvfA nm$?l#Gw~=h34(1%OwPzmElbT!%_}M1Y{vJAnQ`l6O(9hPb2v~S delta 293 zcmbQIa!zH!5lIEdU)z8IDI@%ytI(p)9dtso6wK0fe;S3QpbDpfo z=*$VxRH~mh`5=?bB5#?RCg(A0PYz`j zpS*#2A*)d?Q1|2j7Tw8pEGsxc9H9M~1(OX~Re(CfSZ8nn?KA_48(2?%$EpW3LY8eJ z(72UsIzaX{wyi8+6MES7f!sap(I55a05Ty(KS!asYH|XH_~c~H zS&SBw|8X9le1uDT@&~SsjAoM;aT{#D%&o*E2y%gWZensqW@=e#Zfai1W;VW0%#2Sb I{}faO0Q$^X9{>OV diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 28f17ff9..1dfeac55 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -984,7 +984,7 @@ achievements: { defaults: { format: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" - globalFformat: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" + globalformat: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" titleSGR: "|10" pointsSGR: "|12" textSGR: "|00|03" diff --git a/config/achievements.hjson b/config/achievements.hjson index 41bfd4c1..a711bff4 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -55,12 +55,11 @@ user_login_count: { type: userStat statName: login_count - retroactive: true match: { 2: { title: "Return Caller" globalText: "{userName} has returned to {boardName}!" - text: "You\"ve returned to {boardName}!" + text: "You've returned to {boardName}!" points: 5 } 10: { @@ -93,7 +92,6 @@ user_post_count: { type: userStat statName: post_count - retroactive: true match: { 5: { title: "Poster" @@ -125,7 +123,6 @@ user_upload_count: { type: userStat statName: ul_total_count - retroactive: true match: { 1: { title: "Uploader" @@ -164,7 +161,6 @@ user_download_count: { type: userStat statName: dl_total_count - retroactive: true match: { 1: { title: "Downloader" @@ -202,7 +198,6 @@ user_door_runs: { type: userStat statName: door_run_total_count - retroactive: true match: { 1: { title: "Nostalgia Toe Dip", @@ -240,7 +235,6 @@ user_door_total_minutes: { type: userStat statName: door_run_total_minutes - retroactive: true match: { 1: { title: "Nevermind!" diff --git a/core/achievement.js b/core/achievement.js index adfae332..f3a04f1f 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -37,6 +37,9 @@ const paths = require('path'); class Achievement { constructor(data) { this.data = data; + + // achievements are retroactive by default + this.data.retroactive = _.get(this.data, 'retroactive', true); } static factory(data) { @@ -87,6 +90,9 @@ class Achievement { class UserStatAchievement extends Achievement { constructor(data) { super(data); + + // sort match keys for quick match lookup + this.matchKeys = Object.keys(this.data.match || {}).map(k => parseInt(k)).sort( (a, b) => b - a); } isValid() { @@ -97,7 +103,7 @@ class UserStatAchievement extends Achievement { } getMatchDetails(matchValue) { - let matchField = Object.keys(this.data.match || {}).sort( (a, b) => b - a).find(v => matchValue >= v); + let matchField = this.matchKeys.find(v => matchValue >= v); if(matchField) { const match = this.data.match[matchField]; if(this.isValidMatchDetails(match)) { @@ -218,6 +224,22 @@ class Achievements { }); } + recordAndDisplayAchievement(info, cb) { + async.series( + [ + (callback) => { + return this.record(info, callback); + }, + (callback) => { + return this.display(info, callback); + } + ], + err => { + return cb(err); + } + ); + } + monitorUserStatUpdateEvents() { if(this.userStatEventListener) { return; // already listening @@ -287,15 +309,31 @@ class Achievements { timestamp : moment(), }; - return callback(null, info); - }, - (info, callback) => { - this.record(info, err => { - return callback(err, info); + const achievementsInfo = [ info ]; + if(true === achievement.data.retroactive) { + // For userStat, any lesser match keys(values) are also met. Example: + // matchKeys: [ 500, 200, 100, 20, 10, 2 ] + // ^---- we met here + // ^------------^ retroactive range + // + const index = achievement.matchKeys.findIndex(v => v < matchField); + if(index > -1) { + achievementsInfo.push(...achievement.matchKeys.slice(index).map(k => { + const [ d, f, v ] = achievement.getMatchDetails(k); + return Object.assign({}, info, { details : d, matchField : f, achievedValue : f, matchValue : v } ); + })); + } + } + + // reverse achievementsInfo so we display smallest > largest + achievementsInfo.reverse(); + + async.each(achievementsInfo, (achInfo, nextAchInfo) => { + return this.recordAndDisplayAchievement(achInfo, nextAchInfo); + }, + err => { + return callback(err); }); - }, - (info, callback) => { - return this.display(info, callback); } ], err => { From 8315b6219965b78960f1fa100d642a70479b23c6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 6 Jan 2019 17:42:07 -0700 Subject: [PATCH 503/569] Door stats to BBSLink module --- core/bbs_link.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/core/bbs_link.js b/core/bbs_link.js index 71fa04c1..9144cf0a 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -4,12 +4,16 @@ const { MenuModule } = require('./menu_module.js'); const { resetScreen } = require('./ansi_term.js'); const { Errors } = require('./enig_error.js'); +const Events = require('./events.js'); +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); const http = require('http'); const net = require('net'); const crypto = require('crypto'); +const moment = require('moment'); const packageJson = require('../package.json'); @@ -98,7 +102,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { // // Authenticate the token we acquired previously // - var headers = { + const headers = { 'X-User' : self.client.user.userId.toString(), 'X-System' : self.config.sysCode, 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), @@ -125,17 +129,23 @@ exports.getModule = class BBSLinkModule extends MenuModule { // Authentication with BBSLink successful. Now, we need to create a telnet // bridge from us to them // - var connectOpts = { + const connectOpts = { port : self.config.port, host : self.config.host, }; - var clientTerminated; + let clientTerminated; self.client.term.write(resetScreen()); self.client.term.write(' Connecting to BBSLink.net, please wait...\n'); - var bridgeConnection = net.createConnection(connectOpts, function connected() { + const startTime = moment(); + + const bridgeConnection = net.createConnection(connectOpts, function connected() { + // bump stats, fire events, etc. + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); + Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); + self.client.log.info(connectOpts, 'BBSLink bridge connection established'); self.client.term.output.pipe(bridgeConnection); @@ -147,9 +157,15 @@ exports.getModule = class BBSLinkModule extends MenuModule { }); }); - var restorePipe = function() { + const restorePipe = function() { self.client.term.output.unpipe(bridgeConnection); self.client.term.output.resume(); + + const endTime = moment(); + const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + if(runTimeMinutes > 0) { + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + } }; bridgeConnection.on('data', function incomingData(data) { From 99a95e7648cf63acb32c3eda16074b41adeb8746 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 6 Jan 2019 17:50:22 -0700 Subject: [PATCH 504/569] Door stats to Exodus module --- core/exodus.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/core/exodus.js b/core/exodus.js index 0d439392..eaf4c9a5 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -2,12 +2,17 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const resetScreen = require('./ansi_term.js').resetScreen; -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const Log = require('./logger.js').log; -const getEnigmaUserAgent = require('./misc_util.js').getEnigmaUserAgent; +const { MenuModule } = require('./menu_module.js'); +const { resetScreen } = require('./ansi_term.js'); +const Config = require('./config.js').get; +const { Errors } = require('./enig_error.js'); +const Log = require('./logger.js').log; +const { + getEnigmaUserAgent +} = require('./misc_util.js'); +const Events = require('./events.js'); +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); @@ -151,11 +156,18 @@ exports.getModule = class ExodusModule extends MenuModule { let pipeRestored = false; let pipedStream; + const startTime = moment(); function restorePipe() { if(pipedStream && !pipeRestored && !clientTerminated) { self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); + + const endTime = moment(); + const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + if(runTimeMinutes > 0) { + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + } } } @@ -186,6 +198,9 @@ exports.getModule = class ExodusModule extends MenuModule { }); sshClient.shell(window, options, (err, stream) => { + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); + Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); + pipedStream = stream; // :TODO: ewwwwwwwww hack self.client.term.output.pipe(stream); From 925ca134c6d83705bb5eacc7353e5a78044a1c6a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 6 Jan 2019 18:01:03 -0700 Subject: [PATCH 505/569] Door stats for CombatNet module --- core/combatnet.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/combatnet.js b/core/combatnet.js index abb9a889..bfd98103 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -5,10 +5,14 @@ const { MenuModule } = require('../core/menu_module.js'); const { resetScreen } = require('../core/ansi_term.js'); const { Errors } = require('./enig_error.js'); +const Events = require('./events.js'); +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); const RLogin = require('rlogin'); +const moment = require('moment'); exports.moduleInfo = { name : 'CombatNet', @@ -46,9 +50,17 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.term.write(resetScreen()); self.client.term.write('Connecting to CombatNet, please wait...\n'); + const startTime = moment(); + const restorePipeToNormal = function() { if(self.client.term.output) { self.client.term.output.removeListener('data', sendToRloginBuffer); + + const endTime = moment(); + const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + if(runTimeMinutes > 0) { + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + } } }; @@ -90,6 +102,8 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.log.info('Connected to CombatNet'); self.client.term.output.on('data', sendToRloginBuffer); + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); + Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); } else { return callback(Errors.General('Failed to establish establish CombatNet connection')); } From 34c91780994eddabc9b93def65fe0faa42655b9b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 6 Jan 2019 21:56:12 -0700 Subject: [PATCH 506/569] Achievement & Event improvements * User stat set vs user stat increment system events * Proper addMultipleEventListener() and removeMultipleEventListener() Events APIs * userStatSet vs userStatInc user stat achievement types. userStatInc for example can be used for door minutes used --- config/achievements.hjson | 14 ++++++------ core/achievement.js | 48 +++++++++++++++++++++++++++------------ core/door_party.js | 15 ++++++++++++ core/events.js | 31 +++++++++++++++++++++---- core/stat_log.js | 31 +++++++++++++++++++------ core/system_events.js | 3 ++- 6 files changed, 107 insertions(+), 35 deletions(-) diff --git a/config/achievements.hjson b/config/achievements.hjson index a711bff4..8dbcb637 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -31,7 +31,7 @@ - Applying customizations via the achievements section in theme.hjson Some tips: - - For 'userStat' types, see user_property.js + - For 'userStatSet' types, see user_property.js Don"t forget to RTFM ...er uh... see the documentation for more information and don"t be shy to ask for help: @@ -53,7 +53,7 @@ achievements: { user_login_count: { - type: userStat + type: userStatSet statName: login_count match: { 2: { @@ -90,7 +90,7 @@ } user_post_count: { - type: userStat + type: userStatSet statName: post_count match: { 5: { @@ -121,7 +121,7 @@ } user_upload_count: { - type: userStat + type: userStatSet statName: ul_total_count match: { 1: { @@ -159,7 +159,7 @@ } user_download_count: { - type: userStat + type: userStatSet statName: dl_total_count match: { 1: { @@ -196,7 +196,7 @@ } user_door_runs: { - type: userStat + type: userStatSet statName: door_run_total_count match: { 1: { @@ -233,7 +233,7 @@ } user_door_total_minutes: { - type: userStat + type: userStatInc statName: door_run_total_minutes match: { 1: { diff --git a/core/achievement.js b/core/achievement.js index f3a04f1f..96975628 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -45,7 +45,11 @@ class Achievement { static factory(data) { let achievement; switch(data.type) { - case Achievement.Types.UserStat : achievement = new UserStatAchievement(data); break; + case Achievement.Types.UserStatSet : + case Achievement.Types.UserStatInc : + achievement = new UserStatAchievement(data); + break; + default : return; } @@ -56,13 +60,15 @@ class Achievement { static get Types() { return { - UserStat : 'userStat', + UserStatSet : 'userStatSet', + UserStatInc : 'userStatInc', }; } isValid() { switch(this.data.type) { - case Achievement.Types.UserStat : + case Achievement.Types.UserStatSet : + case Achievement.Types.UserStatInc : if(!_.isString(this.data.statName)) { return false; } @@ -129,12 +135,12 @@ class Achievements { const configLoaded = (achievementConfig) => { if(true !== achievementConfig.enabled) { Log.info('Achievements are not enabled'); - this.stopMonitoringUserStatUpdateEvents(); + this.stopMonitoringUserStatEvents(); delete this.achievementConfig; } else { Log.info('Achievements are enabled'); this.achievementConfig = achievementConfig; - this.monitorUserStatUpdateEvents(); + this.monitorUserStatEvents(); } }; @@ -240,18 +246,22 @@ class Achievements { ); } - monitorUserStatUpdateEvents() { - if(this.userStatEventListener) { + monitorUserStatEvents() { + if(this.userStatEventListeners) { return; // already listening } - this.userStatEventListener = this.events.on(Events.getSystemEvents().UserStatUpdate, userStatEvent => { + const listenEvents = [ + Events.getSystemEvents().UserStatSet, + Events.getSystemEvents().UserStatIncrement + ]; + + this.userStatEventListeners = this.events.addMultipleEventListener(listenEvents, userStatEvent => { if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) { return; } - const statValue = parseInt(userStatEvent.statValue, 10); - if(isNaN(statValue)) { + if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) { return; } @@ -262,7 +272,7 @@ class Achievements { if(false === achievement.enabled) { return false; } - return Achievement.Types.UserStat === achievement.type && + return [ Achievement.Types.UserStatSet, Achievement.Types.UserStatInc ].includes(achievement.type) && achievement.statName === userStatEvent.statName; } ); @@ -276,6 +286,14 @@ class Achievements { return; } + const statValue = parseInt( + Achievement.Types.UserStatSet === achievement.data.type ? userStatEvent.statValue : userStatEvent.statIncrementBy, + 10 + ); + if(isNaN(statValue)) { + return; + } + const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) { return; @@ -345,10 +363,10 @@ class Achievements { }); } - stopMonitoringUserStatUpdateEvents() { - if(this.userStatEventListener) { - this.events.removeListener(Events.getSystemEvents().UserStatUpdate, this.userStatEventListener); - delete this.userStatEventListener; + stopMonitoringUserStatEvents() { + if(this.userStatEventListeners) { + this.events.removeMultipleEventListener(this.userStatEventListeners); + delete this.userStatEventListeners; } } diff --git a/core/door_party.js b/core/door_party.js index f6bc7be9..dcd6037e 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -5,10 +5,14 @@ const { MenuModule } = require('./menu_module.js'); const { resetScreen } = require('./ansi_term.js'); const { Errors } = require('./enig_error.js'); +const Events = require('./events.js'); +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); // deps const async = require('async'); const SSHClient = require('ssh2').Client; +const moment = require('moment'); exports.moduleInfo = { name : 'DoorParty', @@ -54,10 +58,18 @@ exports.getModule = class DoorPartyModule extends MenuModule { let pipeRestored = false; let pipedStream; + const startTime = moment(); + const restorePipe = function() { if(pipedStream && !pipeRestored && !clientTerminated) { self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); + + const endTime = moment(); + const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + if(runTimeMinutes > 0) { + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + } } }; @@ -83,6 +95,9 @@ exports.getModule = class DoorPartyModule extends MenuModule { const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; stream.write(rlogin); + StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); + Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); + pipedStream = stream; // :TODO: this is hacky... self.client.term.output.pipe(stream); diff --git a/core/events.js b/core/events.js index 73253fe3..541a5cae 100644 --- a/core/events.js +++ b/core/events.js @@ -5,6 +5,9 @@ const events = require('events'); const Log = require('./logger.js').log; const SystemEvents = require('./system_events.js'); +// deps +const _ = require('lodash'); + module.exports = new class Events extends events.EventEmitter { constructor() { super(); @@ -35,12 +38,30 @@ module.exports = new class Events extends events.EventEmitter { return super.once(event, listener); } - addListenerMultipleEvents(events, listener) { - Log.trace( { events }, 'Registring event listeners'); + // + // Listen to multiple events for a single listener. + // Called with: listener(event, eventName) + // + // The returned object must be used with removeMultipleEventListener() + // + addMultipleEventListener(events, listener) { + Log.trace( { events }, 'Registering event listeners'); + + const listeners = []; + events.forEach(eventName => { - this.on(eventName, event => { - listener(eventName, event); - }); + const listenWrapper = _.partial(listener, _, eventName); + this.on(eventName, listenWrapper); + listeners.push( { eventName, listenWrapper } ); + }); + + return listeners; + } + + removeMultipleEventListener(listeners) { + Log.trace( { events }, 'Removing listeners'); + listeners.forEach(listener => { + this.removeListener(listener.eventName, listener.listenWrapper); }); } diff --git a/core/stat_log.js b/core/stat_log.js index 8627b6f2..0ff6aff6 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -122,12 +122,18 @@ class StatLog { // User specific stats // These are simply convenience methods to the user's properties // - setUserStat(user, statName, statValue, cb) { + setUserStatWithOptions(user, statName, statValue, options, cb) { // note: cb is optional in PersistUserProperty user.persistProperty(statName, statValue, cb); - const Events = require('./events.js'); // we need to late load currently - return Events.emit(Events.getSystemEvents().UserStatUpdate, { user, statName, statValue } ); + if(!options.noEvent) { + const Events = require('./events.js'); // we need to late load currently + Events.emit(Events.getSystemEvents().UserStatSet, { user, statName, statValue } ); + } + } + + setUserStat(user, statName, statValue, cb) { + return this.setUserStatWithOptions(user, statName, statValue, {}, cb); } getUserStat(user, statName) { @@ -143,16 +149,27 @@ class StatLog { let newValue = parseInt(user.properties[statName]); if(newValue) { - if(!_.isNumber(newValue)) { + if(!_.isNumber(newValue) && cb) { return cb(new Error(`Value for ${statName} is not a number!`)); } - newValue += incrementBy; } else { newValue = incrementBy; } - return this.setUserStat(user, statName, newValue, cb); + this.setUserStatWithOptions(user, statName, newValue, { noEvent : true }, err => { + if(!err) { + const Events = require('./events.js'); // we need to late load currently + Events.emit( + Events.getSystemEvents().UserStatIncrement, + { user, statName, statIncrementBy: incrementBy, statValue : newValue } + ); + } + + if(cb) { + return cb(err); + } + }); } // the time "now" in the ISO format we use and love :) @@ -362,7 +379,7 @@ class StatLog { systemEvents.UserAchievementEarned, ]; - Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => { + Events.addMultipleEventListener(interestedEvents, (event, eventName) => { this.appendUserLogEntry( event.user, 'system_event', diff --git a/core/system_events.js b/core/system_events.js index 50a0c464..c0c09f35 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -21,6 +21,7 @@ module.exports = { UserSendMail : 'codes.l33t.enigma.system.user_send_mail', UserRunDoor : 'codes.l33t.enigma.system.user_run_door', UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', - UserStatUpdate : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } + UserStatSet : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } + UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // {..., statName, statIncrementBy, statValue } UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // {..., achievementTag, points } }; From c9af0edef83d5a417e31f637f24733a69a2311d1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 9 Jan 2019 20:06:30 -0700 Subject: [PATCH 507/569] resetScreen() vs clearScreen() --- core/user_interrupt_queue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index 29a52685..f1aee626 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -76,7 +76,7 @@ module.exports = class UserInterruptQueue displayWithItem(interruptItem, cb) { if(interruptItem.cls) { - this.client.term.rawWrite(ANSI.clearScreen()); + this.client.term.rawWrite(ANSI.resetScreen()); } else { this.client.term.rawWrite('\r\n\r\n'); } From 83c57926d346437d2ebfcea4d38b1e08f7ec758f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 9 Jan 2019 20:06:55 -0700 Subject: [PATCH 508/569] Never interrupt during upload --- core/upload.js | 2 ++ misc/menu_template.in.hjson | 1 + 2 files changed, 3 insertions(+) diff --git a/core/upload.js b/core/upload.js index a2f2c9ea..6eaff2ab 100644 --- a/core/upload.js +++ b/core/upload.js @@ -73,6 +73,8 @@ exports.getModule = class UploadModule extends MenuModule { constructor(options) { super(options); + this.interrupt = MenuModule.InterruptTypes.Never; + if(_.has(options, 'lastMenuResult.recvFilePaths')) { this.recvFilePaths = options.lastMenuResult.recvFilePaths; } diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index e3b76c21..60c57605 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -3440,6 +3440,7 @@ desc: Uploading module: upload config: { + interrupt: never art: { options: ULOPTS fileDetails: ULDETAIL From b96fa154c0fdf5006ffe5bcc97fe89ae84a823fb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 9 Jan 2019 20:07:27 -0700 Subject: [PATCH 509/569] Spelling --- core/door_party.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/door_party.js b/core/door_party.js index dcd6037e..df3d189f 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -137,7 +137,7 @@ exports.getModule = class DoorPartyModule extends MenuModule { self.client.log.warn( { error : err.message }, 'DoorParty error'); } - // if the client is stil here, go to previous + // if the client is still here, go to previous if(!clientTerminated) { self.prevMenu(); } From 2726a7becc1dd6d8290d915f3011781a644d7262 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 9 Jan 2019 20:07:46 -0700 Subject: [PATCH 510/569] New achivements --- config/achievements.hjson | 71 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/config/achievements.hjson b/config/achievements.hjson index 8dbcb637..ba050673 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -42,6 +42,7 @@ Email : bryan@l33t.codes */ { + // Set to false to disable the achievement system enabled : true art : { @@ -158,6 +159,43 @@ } } + user_upload_bytes: { + type: userStatSet + statName: ul_total_bytes + match: { + 524288: { + title: "Kickstart" + globalText: "{userName} has uploaded 512KB, enough for a Kickstart!" + text: "You've uploaded 512KB, enough for a Kickstart!" + points: 10 + } + 1474560: { + title: "America Online 2.5?" + globalText: "{userName} has uploaded 1.44M worth of data. Hopefully it's not AOL 2.5." + title: "You've uploaded 1.44M worth of data. Hopefully it's not AOL 2.5." + points: 15 + } + 6291456: { + title: "A Quake of a Upload" + globalText: "{userName} has uploaded 6 x 1.44MB disks worth of data. That's the size of Quake for DOS!" + text: "You've uploaded 6 x 1.44MB disks worth of data. That's the size of Quake for DOS!" + points: 25 + } + 1073741824: { + title: "Gigabyte!" + globalText: "{userName} has uploaded a Gigabyte worth of data!" + text: "You've uploaded a Gigabyte worth of data!" + points: 50 + } + 3407872000: { + title: "Encarta" + globalText: "{userName} has uploaded 5 x CD-ROM disks worth of data. That's the size of Encarta!" + text: "You've uploaded 5 x CD-ROM disks worth of data. That's the size of Encarta!" + points: 100 + } + } + } + user_download_count: { type: userStatSet statName: dl_total_count @@ -195,6 +233,37 @@ } } + user_download_bytes: { + type: userStatSet + statName: dl_total_bytes + match: { + 655360: { + title: "Ought to be Enough" + globalText: "{userName} has downloaded 640K. Ought to be enough for anyone!" + text: "You've downloaded 640K. Ought to be enough for anyone!" + points: 5 + } + 1474560: { + title: "Fits on a Floppy" + globalText: "{userName} has downloaded 1.44MB worth of data!" + text: "You've downloaded 1.44MB of data!" + points: 10 + } + 104857600: { + title: "Click of Death" + globalText: "{userName} has downloaded 100MB... perhaps to a Zip Disk?" + text: "You've downloaded 100MB of data... perhaps to a Zip Disk?" + points: 15 + } + 681574400: { + title: "A CD-ROM Worth" + globalText: "{userName} has downloaded a CD-ROM's worth of data!" + text: "You've downloaded a CD-ROM's worth of data!" + points: 20 + } + } + } + user_door_runs: { type: userStatSet statName: door_run_total_count @@ -227,7 +296,7 @@ title: "Dropfile Enthusiast" globalText: "{userName} the dropfile enthusiast ran {achievedValue} doors!" text: "You're a dropfile enthusiast! You've run {achievedValue} doors!" - points: 50 + points: 100 } } } From 091a9ae2c7c8b6c4e9c76b47b17b2c06accee0a0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 9 Jan 2019 20:07:59 -0700 Subject: [PATCH 511/569] Fix some bugs, clean up, etc. in achievements --- core/achievement.js | 48 ++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index 96975628..ccd43a54 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -86,7 +86,7 @@ class Achievement { } isValidMatchDetails(details) { - if(!_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) { + if(!details || !_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) { return false; } return (_.isString(details.globalText) || !details.globalText); @@ -109,13 +109,16 @@ class UserStatAchievement extends Achievement { } getMatchDetails(matchValue) { + let ret = []; let matchField = this.matchKeys.find(v => matchValue >= v); if(matchField) { const match = this.data.match[matchField]; - if(this.isValidMatchDetails(match)) { - return [ match, parseInt(matchField), matchValue ]; + matchField = parseInt(matchField); + if(this.isValidMatchDetails(match) && !isNaN(matchField)) { + ret = [ match, matchField, matchValue ]; } } + return ret; } } @@ -180,7 +183,7 @@ class Achievements { WHERE user_id = ? AND achievement_tag = ? AND match_field = ?;`, [ user.userId, achievementTag, field], (err, row) => { - return cb(err, row && row.count || 0); + return cb(err, row ? row.count : 0); } ); } @@ -286,20 +289,20 @@ class Achievements { return; } - const statValue = parseInt( - Achievement.Types.UserStatSet === achievement.data.type ? userStatEvent.statValue : userStatEvent.statIncrementBy, - 10 + const statValue = parseInt(Achievement.Types.UserStatSet === achievement.data.type ? + userStatEvent.statValue : + userStatEvent.statIncrementBy ); if(isNaN(statValue)) { return; } const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); - if(!details || _.isUndefined(matchField) || _.isUndefined(matchValue)) { + if(!details) { return; } - async.waterfall( + async.series( [ (callback) => { this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { @@ -335,19 +338,32 @@ class Achievements { // ^------------^ retroactive range // const index = achievement.matchKeys.findIndex(v => v < matchField); - if(index > -1) { - achievementsInfo.push(...achievement.matchKeys.slice(index).map(k => { - const [ d, f, v ] = achievement.getMatchDetails(k); - return Object.assign({}, info, { details : d, matchField : f, achievedValue : f, matchValue : v } ); - })); + if(index > -1 && Array.isArray(achievement.matchKeys)) { + achievement.matchKeys.slice(index).forEach(k => { + const [ det, fld, val ] = achievement.getMatchDetails(k); + if(det) { + achievementsInfo.push(Object.assign( + {}, + info, + { + details : det, + matchField : fld, + achievedValue : fld, + matchValue : val, + } + )); + } + }); } } // reverse achievementsInfo so we display smallest > largest achievementsInfo.reverse(); - async.each(achievementsInfo, (achInfo, nextAchInfo) => { - return this.recordAndDisplayAchievement(achInfo, nextAchInfo); + async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => { + return this.recordAndDisplayAchievement(achInfo, err => { + return nextAchInfo(err); + }); }, err => { return callback(err); From 2788c37492777c69b904d0380b493041261717f6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Jan 2019 20:34:52 -0700 Subject: [PATCH 512/569] + ACS: AC for achievement count check + ACS: AP for achievement point check + User minutes used on the system are now tracked + MCI: TO for total time spent online system (friendly format) * Fix up a couple ACS bugs with |value| * Fix formatting of achievement text + Add more achievements * Fix achievement duration formatting --- WHATSNEW.md | 1 + art/themes/luciano_blocktronics/theme.hjson | 6 +-- config/achievements.hjson | 47 +++++++++++++++++---- core/achievement.js | 24 ++++++----- core/acs_parser.js | 16 ++++++- core/ansi_term.js | 2 + core/client.js | 38 +++++++++++++++-- core/predefined_mci.js | 4 ++ core/user.js | 16 +++++++ core/user_property.js | 2 + docs/configuration/acs.md | 4 +- misc/acs_parser.pegjs | 16 ++++++- 12 files changed, 149 insertions(+), 27 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 01fc8edc..39dfca49 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -26,6 +26,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md). * Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats. * Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt. +* Total minutes online is now tracked for users. Of course, it only starts after you get the update :) ## 0.0.8-alpha diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 1dfeac55..99164a2b 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -989,9 +989,9 @@ pointsSGR: "|12" textSGR: "|00|03" globalTextSGR: "|03" - boardName: "|10" - userName: "|11" - achievedValue: "|15" + boardNameSGR: "|10" + userNameSGR: "|11" + achievedValueSGR: "|15" } overrides: { diff --git a/config/achievements.hjson b/config/achievements.hjson index ba050673..63ea5ee1 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -307,29 +307,60 @@ match: { 1: { title: "Nevermind!" - globalText: "{userName} ran a door for {achievedValue!durationSeconds}. Guess it's not their thing!" - text: "You ran a door for only {achievedValue!durationSeconds}. Not your thing?" + globalText: "{userName} ran a door for {achievedValue!durationMinutes}. Guess it's not their thing!" + text: "You ran a door for only {achievedValue!durationMinutes}. Not your thing?" points: 5 } 10: { title: "It's OK I Guess" - globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" - text: "You ran a door for {achievedValue!durationSeconds}!" + globalText: "{userName} ran a door for {achievedValue!durationMinutes}!" + text: "You ran a door for {achievedValue!durationMinutes}!" points: 10 } 30: { title: "Good Game" - globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" - text: "You ran a door for {achievedValue!durationSeconds}!" + globalText: "{userName} ran a door for {achievedValue!durationMinutes}!" + text: "You ran a door for {achievedValue!durationMinutes}!" points: 20 } 60: { title: "Textmode Dragon Slayer" - globalText: "{userName} has spent {achievedValue!durationSeconds} in a door!" - text: "You've spent {achievedValue!durationSeconds} in a door!" + globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!" + text: "You've spent {achievedValue!durationMinutes} in a door!" points: 25 } } } + + user_total_system_online_minutes: { + type: userStatSet + statName: minutes_online_total_count + match: { + 30: { + title: "Just Poking Around" + globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!" + text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 5 + } + 60: { + title: "Mildly Interesting" + globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!" + text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 15 + } + 120: { + title: "Nothing Better to Do" + globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!" + text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 25 + } + 1440: { + title: "Idle Bot" + globalText: "{userName} is probably a bot. They've spent {achievedValue!durationMinutes} on {boardName}!" + text: "You're a bot, aren't you? You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 50 + } + } + } } } \ No newline at end of file diff --git a/core/achievement.js b/core/achievement.js index ccd43a54..ad7644ba 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -406,19 +406,23 @@ class Achievements { getFormattedTextFor(info, textType, defaultSgr = '|07') { const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); - const defSgr = themeDefaults[`${textType}SGR`] || defaultSgr; + const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr; - const wrap = (fieldName, value) => { - return `${themeDefaults[fieldName] || defSgr}${value}${defSgr}`; + const formatObj = this.getFormatObject(info); + + const wrap = (input) => { + const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g'); + return input.replace(re, (m, formatVar, formatOpts) => { + const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr; + let r = `${varSgr}{${formatVar}`; + if(formatOpts) { + r += formatOpts; + } + return `${r}}${textTypeSgr}`; + }); }; - let formatObj = this.getFormatObject(info); - formatObj = _.reduce(formatObj, (out, v, k) => { - out[k] = wrap(k, v); - return out; - }, {}); - - return stringFormat(`${defSgr}${info.details[textType]}`, formatObj); + return stringFormat(`${textTypeSgr}${wrap(info.details[textType])}`, formatObj); } createAchievementInterruptItems(info, cb) { diff --git a/core/acs_parser.js b/core/acs_parser.js index d6983b17..d4084b95 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -1004,7 +1004,7 @@ function peg$parse(input, options) { TW : function termWidth() { return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; }, - ID : function isUserId(value) { + ID : function isUserId() { if(!user) { return false; } @@ -1024,6 +1024,20 @@ function peg$parse(input, options) { const midnight = now.clone().startOf('day') const minutesPastMidnight = now.diff(midnight, 'minutes'); return !isNaN(value) && minutesPastMidnight >= value; + }, + AC : function achievementCount() { + if(!user) { + return false; + } + const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0; + return !isNan(value) && points >= value; + }, + AP : function achievementPoints() { + if(!user) { + return false; + } + const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0; + return !isNan(value) && points >= value; } }[acsCode](value); } catch (e) { diff --git a/core/ansi_term.js b/core/ansi_term.js index f00fd011..353c46c8 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -23,6 +23,8 @@ // General // * http://en.wikipedia.org/wiki/ANSI_escape_code // * http://www.inwap.com/pdp10/ansicode.txt +// * Excellent information with many standards covered (for hterm): +// https://chromium.googlesource.com/apps/libapps/+/master/hterm/doc/ControlSequences.md // // Other Implementations // * https://github.com/chjj/term.js/blob/master/src/term.js diff --git a/core/client.js b/core/client.js index 300285a3..894119bf 100644 --- a/core/client.js +++ b/core/client.js @@ -40,6 +40,7 @@ const MenuStack = require('./menu_stack.js'); const ACS = require('./acs.js'); const Events = require('./events.js'); const UserInterruptQueue = require('./user_interrupt_queue.js'); +const UserProps = require('./user_property.js'); // deps const stream = require('stream'); @@ -442,13 +443,36 @@ Client.prototype.startIdleMonitor = function() { // // Every 1m, check for idle. + // We also update minutes spent online the system here, + // if we have a authenticated user. // this.idleCheck = setInterval( () => { const nowMs = Date.now(); - const idleLogoutSeconds = this.user.isAuthenticated() ? - Config().users.idleLogoutSeconds : - Config().users.preAuthIdleLogoutSeconds; + let idleLogoutSeconds; + if(this.user.isAuthenticated()) { + idleLogoutSeconds = Config().users.idleLogoutSeconds; + + // + // We don't really want to be firing off an event every 1m for + // every user, but want at least some updates for various things + // such as achievements. Send off every 5m. + // + const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1); + if(0 === (minOnline % 5)) { + Events.emit( + Events.getSystemEvents().UserStatIncrement, + { + user : this.user, + statName : UserProps.MinutesOnlineTotalCount, + statIncrementBy : 1, + statValue : minOnline + } + ); + } + } else { + idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds; + } if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { this.emit('idle timeout'); @@ -473,6 +497,14 @@ Client.prototype.end = function () { currentModule.leave(); } + // persist time online for authenticated users + if(this.user.isAuthenticated()) { + this.user.persistProperty( + UserProps.MinutesOnlineTotalCount, + this.user.getProperty(UserProps.MinutesOnlineTotalCount) + ); + } + this.stopIdleMonitor(); try { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 39e339cc..2e7ed5ff 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -160,6 +160,10 @@ const PREDEFINED_MCI_GENERATORS = { const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0; return moment.duration(minutes, 'minutes').humanize(); }, + TO : function friendlyTotalTimeOnSystem(client) { + const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0; + return moment.duration(minutes, 'minutes').humanize(); + }, // // Date/Time diff --git a/core/user.js b/core/user.js index ba89b387..3b261dc6 100644 --- a/core/user.js +++ b/core/user.js @@ -443,6 +443,22 @@ module.exports = class User { ); } + setProperty(propName, propValue) { + this.properties[propName] = propValue; + } + + incrementProperty(propName, incrementBy) { + incrementBy = incrementBy || 1; + let newValue = parseInt(this.getProperty(propName)); + if(newValue) { + newValue += incrementBy; + } else { + newValue = incrementBy; + } + this.setProperty(propName, newValue); + return newValue; + } + getProperty(propName) { return this.properties[propName]; } diff --git a/core/user_property.js b/core/user_property.js index a1489e82..56e47e66 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -55,5 +55,7 @@ module.exports = { AchievementTotalCount : 'achievement_total_count', AchievementTotalPoints : 'achievement_total_points', + + MinutesOnlineTotalCount : 'minutes_online_total_count', }; diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index 1ed83bb5..d0a45d06 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -34,7 +34,9 @@ The following are ACS codes available as of this writing: | NRratio | User has upload/download count ratio >= _ratio_ | | KRratio | User has a upload/download byte ratio >= _ratio_ | | PCratio | User has a post/call ratio >= _ratio_ | -| MMminutes | It is currently >= _minutes_ past midnight (system time) +| MMminutes | It is currently >= _minutes_ past midnight (system time) | +| ACachievementCount | User has >= _achievementCount_ achievements | +| APachievementPoints | User has >= _achievementPoints_ achievement points | \* Many more ACS codes are planned for the near future. diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs index bd6a8d96..8a39deea 100644 --- a/misc/acs_parser.pegjs +++ b/misc/acs_parser.pegjs @@ -160,7 +160,7 @@ TW : function termWidth() { return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; }, - ID : function isUserId(value) { + ID : function isUserId() { if(!user) { return false; } @@ -180,6 +180,20 @@ const midnight = now.clone().startOf('day') const minutesPastMidnight = now.diff(midnight, 'minutes'); return !isNaN(value) && minutesPastMidnight >= value; + }, + AC : function achievementCount() { + if(!user) { + return false; + } + const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0; + return !isNan(value) && points >= value; + }, + AP : function achievementPoints() { + if(!user) { + return false; + } + const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0; + return !isNan(value) && points >= value; } }[acsCode](value); } catch (e) { From 3f2e836a83ae810840c13351ca08f7f016ca3b26 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Jan 2019 21:41:32 -0700 Subject: [PATCH 513/569] Minor fixes --- art/themes/luciano_blocktronics/theme.hjson | 2 +- core/achievement.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 99164a2b..f3871ce8 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -984,7 +984,7 @@ achievements: { defaults: { format: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" - globalformat: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" + globalFormat: "|08 > |10{title} |08(|11{points} |03points|08)\r\n\r\n {message}" titleSGR: "|10" pointsSGR: "|12" textSGR: "|00|03" diff --git a/core/achievement.js b/core/achievement.js index ad7644ba..f57e2f06 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -485,7 +485,7 @@ class Achievements { }; if(headerArt || footerArt) { const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); - const defaultContentsFormat = '{title}\r\n${message}'; + const defaultContentsFormat = '{title}\r\n{message}'; const contentsFormat = 'global' === itemType ? themeDefaults.globalFormat || defaultContentsFormat : themeDefaults.format || defaultContentsFormat; From 372494e376bb584a1e43fe3cf413b5d74769981a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 12 Jan 2019 09:39:06 -0700 Subject: [PATCH 514/569] Dependency updates --- package-lock.json | 2530 --------------------------------------------- package.json | 18 +- yarn.lock | 2231 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 2240 insertions(+), 2539 deletions(-) delete mode 100644 package-lock.json create mode 100644 yarn.lock diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a3bc6f81..00000000 --- a/package-lock.json +++ /dev/null @@ -1,2530 +0,0 @@ -{ - "name": "enigma-bbs", - "version": "0.0.9-alpha", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-parser": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-1.3.2.tgz", - "integrity": "sha512-VDhHcpeF1/ZZy1XvDmYD67bBjRNm1gacw+772xNd5BnTH6ax5TzlDV5dl7216/UlQXQoN9vug07ehk7e0PhNUw==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "bunyan": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz", - "integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=", - "requires": { - "dtrace-provider": "~0.8", - "moment": "^2.10.6", - "mv": "~2", - "safe-json-stringify": "~1" - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "capture-exit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-1.2.0.tgz", - "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", - "requires": { - "rsvp": "^3.3.3" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "requires": { - "color-name": "^1.1.1" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", - "requires": { - "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "dtrace-provider": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz", - "integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=", - "optional": true, - "requires": { - "nan": "^2.3.3" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "exec-sh": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.2.tgz", - "integrity": "sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==" - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exiftool": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/exiftool/-/exiftool-0.0.3.tgz", - "integrity": "sha1-9YqSvXcnCtxU8xUc7WGko6tp1wc=" - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "requires": { - "bser": "^2.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globby": { - "version": "6.1.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - } - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hashids": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/hashids/-/hashids-1.1.4.tgz", - "integrity": "sha512-U/fnTE3edW0AV92ZI/BfEluMZuVcu3MDOopsN7jS+HqDYcarQo8rXQiWlsBlm0uX48/taYSdxRsfzh2HRg5Z6w==" - }, - "hjson": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/hjson/-/hjson-3.1.2.tgz", - "integrity": "sha512-2ILrho8eRl2Bniy61mDFiXRAloYqH2T6OwWkoF/8y55DPFgG2RcqQGNXIfBLp432dnAbLOpBJ4pJs63W3X27EA==" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inquirer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.0.0.tgz", - "integrity": "sha512-tISQWRwtcAgrz+SHPhTH7d3e73k31gsOy6i1csonLc0u1dVK/wYvuOnFeiWqC5OXFIYbmrIFInef31wbT8MEJg==", - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.1.0", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "chardet": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.5.0.tgz", - "integrity": "sha512-9ZTaoBaePSCFvNlNGrsyI8ZVACP2svUtq0DkM7t4K2ClAa96sqOIRjAzDTc8zXzFt1cZR46rRzLTiHFSJ+Qw0g==" - }, - "external-editor": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.0.tgz", - "integrity": "sha512-mpkfj0FEdxrIhOC04zk85X7StNtr0yXnG7zCb+8ikO8OJi2jsHh5YGoknNTyXgsbHOf1WOOcVU3kPFWT2WgCkQ==", - "requires": { - "chardet": "^0.5.0", - "iconv-lite": "^0.4.22", - "tmp": "^0.0.33" - } - }, - "rxjs": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.0.tgz", - "integrity": "sha512-qBzf5uu6eOKiCZuAE0SgZ0/Qp+l54oeVxFfC2t+mJ2SFI6IB8gmMdJHs5DUMu5kqifqcCtsKS2XHjhZu6RKvAw==", - "requires": { - "tslib": "^1.9.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=" - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "later": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/later/-/later-1.2.0.tgz", - "integrity": "sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8=" - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "merge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", - "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" - }, - "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", - "requires": { - "mime-db": "~1.37.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", - "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", - "requires": { - "minipass": "^2.2.1" - } - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "moment": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", - "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "optional": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - } - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "optional": true - }, - "needle": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", - "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" - }, - "node-pre-gyp": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", - "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - }, - "dependencies": { - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "^7.0.5" - } - } - } - }, - "node-pty": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.7.4.tgz", - "integrity": "sha512-WxMY1BsGcHJ2Z2qWpYL7QbfOSnkkCzV0H/9+dJ7uQEIJyz0A4fVBLymswBCTc7RoweY5ingib2gNvf87KvJxuA==", - "requires": { - "nan": "^2.6.2" - } - }, - "nodemailer": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.5.tgz", - "integrity": "sha512-+bt+BgmnOXDz1uIaWXfXuTESth8UHkhtu7+X8+X2W+CHAn0AuuCyCk854qnathYQLWEC2jkpx7/pkVHcfmLKDw==" - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" - }, - "npm-packlist": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", - "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "requires": { - "glob": "^6.0.1" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "rlogin": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rlogin/-/rlogin-1.0.0.tgz", - "integrity": "sha1-2wcyKzEhkSZiXZ0KqYctfr6KxAM=" - }, - "rsvp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", - "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==" - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-json-stringify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz", - "integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==", - "optional": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sane": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.0.2.tgz", - "integrity": "sha512-/3STCUfNSgMVpoREJc1i6ajKFlYZ5OflzZTOhlqPLa+01Ey+QR9iGZK7K5/qIRsQbEDCvqEJH/PL7yZywmnWsA==", - "requires": { - "anymatch": "^2.0.0", - "capture-exit": "^1.2.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5", - "watch": "~0.18.0" - } - }, - "sanitize-filename": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", - "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", - "requires": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sqlite3": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.4.tgz", - "integrity": "sha512-CO8vZMyUXBPC+E3iXOCc7Tz2pAdq5BWfLcQmOokCOZW5S5sZ/paijiPOCdvzpdP83RroWHYa5xYlVqCxSqpnQg==", - "requires": { - "nan": "~2.10.0", - "node-pre-gyp": "^0.10.3", - "request": "^2.87.0" - } - }, - "sqlite3-trans": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sqlite3-trans/-/sqlite3-trans-1.2.0.tgz", - "integrity": "sha1-E8/K2wk+1I5m+U7IlWq3RU3clgg=", - "requires": { - "lodash": "^4.17.4" - } - }, - "ssh2": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.6.1.tgz", - "integrity": "sha512-fNvocq+xetsaAZtBG/9Vhh0GDjw1jQeW7Uq/DPh4fVrJd0XxSfXAqBjOGVk4o2jyWHvyC6HiaPFpfHlR12coDw==", - "requires": { - "ssh2-streams": "~0.2.0" - }, - "dependencies": { - "ssh2-streams": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.2.1.tgz", - "integrity": "sha512-3zCOsmunh1JWgPshfhKmBCL3lUtHPoh+a/cyQ49Ft0Q0aF7xgN06b76L+oKtFi0fgO57FLjFztb1GlJcEZ4a3Q==", - "requires": { - "asn1": "~0.2.0", - "semver": "^5.1.0", - "streamsearch": "~0.1.2" - } - } - } - }, - "sshpk": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", - "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "temptmp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/temptmp/-/temptmp-1.1.0.tgz", - "integrity": "sha512-gHelQlePUzxRmodWL1uJ9LiwI+a7a3rkFGS9azTf4noPZgGOlx0dOPV9tZs5+QwGc4Nm8BfFxL9cfvV42GNxPQ==", - "requires": { - "del": "^3.0.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - }, - "truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", - "requires": { - "utf8-byte-length": "^1.0.1" - } - }, - "tslib": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", - "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "utf8-byte-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" - }, - "uuid-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.0.0.tgz", - "integrity": "sha1-9GV3F2JLDkuIrzb5jYlYmlu+5Wk=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "requires": { - "makeerror": "1.0.x" - } - }, - "watch": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/watch/-/watch-0.18.0.tgz", - "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", - "requires": { - "exec-sh": "^0.2.0", - "minimist": "^1.2.0" - }, - "dependencies": { - "exec-sh": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", - "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", - "requires": { - "merge": "^1.2.0" - } - } - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "ws": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", - "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xxhash": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/xxhash/-/xxhash-0.2.4.tgz", - "integrity": "sha1-i4pIFiz8zCG5IPpQAmEYfUAhbDk=", - "requires": { - "nan": "^2.4.0" - } - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" - }, - "yazl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.0.tgz", - "integrity": "sha512-rgptqKwX/f1/7bIRF1FHb4HGsP5k11QyxBpDl1etUDfNpTa7CNjDOYNPFnIaEzZ9dRq0c47IEJS+sy+T39JCLw==", - "requires": { - "buffer-crc32": "~0.2.3" - } - } - } -} diff --git a/package.json b/package.json index a5dcaf3a..f700ce4d 100644 --- a/package.json +++ b/package.json @@ -33,28 +33,28 @@ "hashids": "^1.1.1", "hjson": "^3.1.2", "iconv-lite": "^0.4.23", - "inquirer": "^6.0.0", + "inquirer": "^6.2.1", "later": "1.2.0", "lodash": "^4.17.10", + "lru-cache": "^5.1.1", "mime-types": "^2.1.21", "minimist": "1.2.x", - "moment": "^2.22.2", - "node-pty": "^0.7.4", - "nodemailer": "^4.6.5", + "moment": "^2.23.0", + "nntp-server": "^1.0.3", + "node-pty": "^0.8.0", + "nodemailer": "^5.1.1", "rlogin": "^1.0.0", "sane": "^4.0.2", "sanitize-filename": "^1.6.1", - "sqlite3": "^4.0.4", + "sqlite3": "^4.0.6", "sqlite3-trans": "^1.2.1", - "ssh2": "^0.6.1", + "ssh2": "^0.7.1", "temptmp": "^1.1.0", "uuid": "^3.2.1", "uuid-parse": "^1.0.0", "ws": "^6.1.2", "xxhash": "^0.2.4", - "yazl": "^2.5.0", - "nntp-server": "^1.0.3", - "lru-cache" : "^5.1.1" + "yazl": "^2.5.1" }, "devDependencies": {}, "engines": { diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..96d5a9a2 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2231 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ansi-escapes@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" + integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +asn1@~0.2.0, asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + +async@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== + dependencies: + lodash "^4.17.10" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +binary-parser@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.3.2.tgz#5bd04f948ada1a6d78c528762308a9a335d63db9" + integrity sha512-VDhHcpeF1/ZZy1XvDmYD67bBjRNm1gacw+772xNd5BnTH6ax5TzlDV5dl7216/UlQXQoN9vug07ehk7e0PhNUw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk= + dependencies: + node-int64 "^0.4.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +"buffers@github:NuSkooler/node-buffers": + version "0.1.1" + resolved "https://codeload.github.com/NuSkooler/node-buffers/tar.gz/cd0855598f7048b02f0a51c90e22573973e9e2c2" + +bunyan@^1.8.12: + version "1.8.12" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.12.tgz#f150f0f6748abdd72aeae84f04403be2ef113797" + integrity sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c= + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.10.6" + mv "~2" + safe-json-stringify "~1" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +capture-exit@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" + integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28= + dependencies: + rsvp "^3.3.3" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^2.0.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +chownr@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +combined-stream@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + integrity sha1-cj599ugBrFYTETp+RFqbactjKBg= + dependencies: + delayed-stream "~1.0.0" + +combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + dependencies: + delayed-stream "~1.0.0" + +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" + integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg== + dependencies: + ms "^2.1.1" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU= + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +denque@^1.1.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.0.tgz#79e2f0490195502107f24d9553f374837dabc916" + integrity sha512-gh513ac7aiKrAgjiIBWZG0EASyDF9p4JMWwKA8YU5s9figrL5SRNEMT6FDynsegakuhWd1wVqTvqvqAoDxw7wQ== + +destroy@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +dtrace-provider@~0.8: + version "0.8.7" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04" + integrity sha1-3JObTT4GIM/gwc2APQ0tftBP/QQ= + dependencies: + nan "^2.10.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +end-of-stream@^1.1.0, end-of-stream@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +exec-sh@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" + integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== + dependencies: + merge "^1.2.0" + +exec-sh@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" + integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exiftool@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/exiftool/-/exiftool-0.0.3.tgz#f58a92bd77270adc54f3151ced61a4a3ab69d707" + integrity sha1-9YqSvXcnCtxU8xUc7WGko6tp1wc= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + dependencies: + bser "^2.0.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk= + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +from2@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== + dependencies: + minipass "^2.2.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.15: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + integrity sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA== + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +hashids@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/hashids/-/hashids-1.2.2.tgz#28635c7f2f7360ba463686078eee837479e8eafb" + integrity sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw== + +hjson@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.1.2.tgz#1ae8a3a897a1fab8d45180f98e9abf9b56f95b55" + integrity sha512-2ILrho8eRl2Bniy61mDFiXRAloYqH2T6OwWkoF/8y55DPFgG2RcqQGNXIfBLp432dnAbLOpBJ4pJs63W3X27EA== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@^0.4.23, iconv-lite@^0.4.24, iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + dependencies: + minimatch "^3.0.4" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +inquirer@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52" + integrity sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg== + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.17.10" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^5.0.0" + through "^2.3.6" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +later@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f" + integrity sha1-8s9sTdeVbdL1IK3wMpg26YdrrQ8= + +lodash@^4.17.10, lodash@^4.17.4: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +merge@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + integrity sha1-dTHjnUlJwoGma4xabgJl6LBYlNo= + +micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +mime-db@~1.36.0: + version "1.36.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" + integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw== + +mime-db@~1.37.0: + version "1.37.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.20" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + integrity sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A== + dependencies: + mime-db "~1.36.0" + +mime-types@^2.1.21: + version "2.1.21" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== + dependencies: + mime-db "~1.37.0" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +"minimatch@2 || 3", minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@1.2.x, minimist@^1.1.1, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957" + integrity sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + integrity sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA== + dependencies: + minipass "^2.2.1" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +moment@^2.10.6: + version "2.22.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= + +moment@^2.23.0: + version "2.23.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" + integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + +nan@2.10.0, nan@~2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== + +nan@^2.10.0, nan@^2.4.0: + version "2.11.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" + integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= + +needle@^2.2.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" + integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +nntp-server@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/nntp-server/-/nntp-server-1.0.3.tgz#c556dc0d3481d52d49b1389c0e5d0889d3865088" + integrity sha512-I30wXcciO937DeADhuTkAtqL+FXHcmwKwUbqMhpr69RKlQl5PRfZMm/WNtOoLBta+b55ED/VqBvMmlasE3z4BA== + dependencies: + debug "^4.0.0" + denque "^1.1.1" + destroy "^1.0.4" + end-of-stream "^1.4.0" + from2 "^2.3.0" + glob "^7.1.2" + pump "^3.0.0" + serialize-error "^2.1.0" + split2 "^3.0.0" + through2 "^2.0.3" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-pre-gyp@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" + integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-pty@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.0.tgz#08bccb633f49e2e3f7245eb56ea6b40f37ccd64f" + integrity sha512-g5ggk3gN4gLrDmAllee5ScFyX3YzpOC/U8VJafha4pE7do0TIE1voiIxEbHSRUOPD1xYqmY+uHhOKAd3avbxGQ== + dependencies: + nan "2.10.0" + +nodemailer@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.1.1.tgz#0c48d1ecab02e86d9ff6c620ee75ed944b763505" + integrity sha512-hKGCoeNdFL2W7S76J/Oucbw0/qRlfG815tENdhzcqTpSjKgAN91mFOqU2lQUflRRxFM7iZvCyaFcAR9noc/CqQ== + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +npm-bundled@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" + integrity sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g== + +npm-packlist@^1.1.6: + version "1.1.11" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" + integrity sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +psl@^1.1.24: + version "1.1.29" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" + integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@~2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.0.6.tgz#351302e4c68b5abd6a2ed55376a7f9a25be3057a" + integrity sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request@^2.87.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rimraf@^2.2.8, rimraf@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== + dependencies: + glob "^7.0.5" + +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= + dependencies: + glob "^6.0.1" + +rlogin@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rlogin/-/rlogin-1.0.0.tgz#db07322b31219126625d9d0aa9872d7ebe8ac403" + integrity sha1-2wcyKzEhkSZiXZ0KqYctfr6KxAM= + +rsvp@^3.3.3: + version "3.6.2" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" + integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +rxjs@^6.1.0: + version "6.3.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" + integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== + dependencies: + tslib "^1.9.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.0.2.tgz#5bd4a3f1268fd7a921a2dc657047de635c8f8f25" + integrity sha512-/3STCUfNSgMVpoREJc1i6ajKFlYZ5OflzZTOhlqPLa+01Ey+QR9iGZK7K5/qIRsQbEDCvqEJH/PL7yZywmnWsA== + dependencies: + anymatch "^2.0.0" + capture-exit "^1.2.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.18.0" + +sanitize-filename@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.1.tgz#612da1c96473fa02dccda92dcd5b4ab164a6772a" + integrity sha1-YS2hyWRz+gLczaktzVtKsWSmdyo= + dependencies: + truncate-utf8-bytes "^1.0.0" + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +semver@^5.3.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== + +semver@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + +serialize-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" + integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go= + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +split2@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.0.0.tgz#55057cd560687a7ef6464471597404577ff1735d" + integrity sha512-Cp7G+nUfKJyHCrAI8kze3Q00PFGEG1pMgrAlTFlDbn+GW24evSZHJuMl+iUJx1w/NTRDeBiTgvwnf6YOt94FMw== + dependencies: + readable-stream "^3.0.0" + +sqlite3-trans@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sqlite3-trans/-/sqlite3-trans-1.2.1.tgz#642dff9f6da53d533ccd264b49e68c8818542255" + integrity sha512-KLtR+PBZN/moxDTKWTwWypkunDCJ0oi5vknjht8omjUXswwUEf+MX2DKtgQB1V5Tsjgc4mL4mHjv9zp7+FHs5g== + dependencies: + lodash "^4.17.4" + +sqlite3@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.6.tgz#e587b583b5acc6cb38d4437dedb2572359c080ad" + integrity sha512-EqBXxHdKiwvNMRCgml86VTL5TK1i0IKiumnfxykX0gh6H6jaKijAXvE9O1N7+omfNSawR2fOmIyJZcfe8HYWpw== + dependencies: + nan "~2.10.0" + node-pre-gyp "^0.11.0" + request "^2.87.0" + +ssh2-streams@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.3.1.tgz#65d78447628d3fd2bbcfa1a8e73a06755bd62a1a" + integrity sha512-8lVQaN3FNBCoQdnMEsIpD8X1QN/5V8Cyd9TQhAvw4eD/vfuk/o3eikh3rdnf5HUi3TD7SM1bbM/ZTzjNIRmSFw== + dependencies: + asn1 "~0.2.0" + bcrypt-pbkdf "^1.0.2" + streamsearch "~0.1.2" + +ssh2@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.7.1.tgz#0dcafd75c4e30606d380d0bd57448e8c0efdd718" + integrity sha512-+ZFoxMXMXq/OyDbE7AwJjsYwk300PD4N1xphYrUttsSzqchWz/3BF7X+La8Jq29Y2QTanojuV/vPFjRsyUGaoA== + dependencies: + ssh2-streams "~0.3.1" + +sshpk@^1.7.0: + version "1.14.2" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" + integrity sha1-xvxhZIo9nE52T9P8306hBeSSupg= + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + safer-buffer "^2.0.2" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +streamsearch@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" + integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== + dependencies: + ansi-regex "^4.0.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tar@^4: + version "4.4.6" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" + integrity sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg== + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" + +temptmp@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/temptmp/-/temptmp-1.1.0.tgz#bfbbff858d7f7d59c563fbf069758a7775ecd431" + integrity sha512-gHelQlePUzxRmodWL1uJ9LiwI+a7a3rkFGS9azTf4noPZgGOlx0dOPV9tZs5+QwGc4Nm8BfFxL9cfvV42GNxPQ== + dependencies: + del "^3.0.0" + +through2@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +truncate-utf8-bytes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" + integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys= + dependencies: + utf8-byte-length "^1.0.1" + +tslib@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +utf8-byte-length@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" + integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E= + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid-parse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.0.0.tgz#f4657717624b0e4b88af36f98d89589a5bbee569" + integrity sha1-9GV3F2JLDkuIrzb5jYlYmlu+5Wk= + +uuid@^3.2.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +watch@~0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" + integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY= + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" + integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw== + dependencies: + async-limiter "~1.0.0" + +xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= + +xxhash@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.2.4.tgz#8b8a48162cfccc21b920fa500261187d40216c39" + integrity sha1-i4pIFiz8zCG5IPpQAmEYfUAhbDk= + dependencies: + nan "^2.4.0" + +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + integrity sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k= + +yazl@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== + dependencies: + buffer-crc32 "~0.2.3" From d5bd2d5adf53e904eb3e4d14cfc12815df025c38 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 12 Jan 2019 10:31:03 -0700 Subject: [PATCH 515/569] Experimental UTF-8 -> CP437 (aka nix -> ansi) override to work around terms such as NR that report 'xterm' but want CP437 --- core/connect.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/core/connect.js b/core/connect.js index b9316a3e..f4645133 100644 --- a/core/connect.js +++ b/core/connect.js @@ -56,6 +56,74 @@ function ansiDiscoverHomePosition(client, cb) { client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos } +function ansiAttemptDetectUTF8(client, cb) { + // + // Trick to attempt and detect UTF-8. While there is a lot more than + // just UTF-8 and CP437, many those are the main concerns, when it comes + // terminals that for example tell us they are "xterm" but still want CP437. + // + // Try to detect UTF-8 by discovering the cursor position, writing some + // multi-byte UTF-8, and checking the position again. If the term is really + // UTF-8, we should get a proper position, otherwise we'll be further out. + // + // We currently only do this if the term hasn't already been ID'd as a + // "*nix" terminal -- that is, xterm, etc. + // + if(!client.term.isNixTerm()) { + return cb(null); + } + + let posStage = 1; + let initialPosition; + let giveUpTimer; + + const giveUp = () => { + client.removeListener('cursor position report', cprListener); + clearTimeout(giveUpTimer); + return cb(null); + }; + + const ASCIIPortion = ' Character encoding detection '; + + const cprListener = (pos) => { + switch(posStage) { + case 1 : + posStage = 2; + + initialPosition = pos; + clearTimeout(giveUpTimer); + + giveUpTimer = setTimeout( () => { + return giveUp(); + }, 2000); + + client.once('cursor position report', cprListener); + client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side + client.term.rawWrite(ansi.queryPos()); + break; + + case 2 : + { + clearTimeout(giveUpTimer); + const len = pos[1] - initialPosition[1]; + if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull + client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".'); + client.setTermType('ansi'); + } + } + return cb(null); + + } + }; + + giveUpTimer = setTimeout( () => { + return giveUp(); + }, 2000); + + client.once('cursor position report', cprListener); + client.term.rawWrite(ansi.goHome() + ansi.queryPos()); +} + function ansiQueryTermSizeIfNeeded(client, cb) { if(client.term.termHeight > 0 || client.term.termWidth > 0) { return cb(null); @@ -164,7 +232,7 @@ function connectEntry(client, nextMenu) { // We still don't have something good for term height/width. // Default to DOS size 80x25. // - // :TODO: Netrunner is currenting hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? + // :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!'); term.termHeight = 25; @@ -175,6 +243,9 @@ function connectEntry(client, nextMenu) { return callback(null); }); }, + function checkUtf8IfNeeded(callback) { + return ansiAttemptDetectUTF8(client, callback); + } ], () => { prepareTerminal(term); From 403ee891d55eec802182d3215447e30e2b418f76 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 13 Jan 2019 18:19:00 -0700 Subject: [PATCH 516/569] Change column name, drop a useless one --- config/achievements.hjson | 2 +- core/achievement.js | 8 ++++---- core/database.js | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/config/achievements.hjson b/config/achievements.hjson index 63ea5ee1..d5099f8c 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -70,7 +70,7 @@ points: 5 } 25: { - title: "Inquisitive Caller" + title: "Inquisitive" globalText: "{userName} has logged into {boardName} {achievedValue} times!" text: "You've logged into {boardName} {achievedValue} times!" points: 10 diff --git a/core/achievement.js b/core/achievement.js index f57e2f06..333b968c 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -180,7 +180,7 @@ class Achievements { UserDb.get( `SELECT COUNT() AS count FROM user_achievement - WHERE user_id = ? AND achievement_tag = ? AND match_field = ?;`, + WHERE user_id = ? AND achievement_tag = ? AND match = ?;`, [ user.userId, achievementTag, field], (err, row) => { return cb(err, row ? row.count : 0); @@ -193,9 +193,9 @@ class Achievements { StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points); UserDb.run( - `INSERT INTO user_achievement (user_id, achievement_tag, timestamp, match_field, match_value) - VALUES (?, ?, ?, ?, ?);`, - [ info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField, info.matchValue ], + `INSERT INTO user_achievement (user_id, achievement_tag, timestamp, match) + VALUES (?, ?, ?, ?);`, + [ info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField ], err => { if(err) { return cb(err); diff --git a/core/database.js b/core/database.js index 371af1ae..a6af1930 100644 --- a/core/database.js +++ b/core/database.js @@ -194,9 +194,8 @@ const DB_INIT_TABLE = { user_id INTEGER NOT NULL, achievement_tag VARCHAR NOT NULL, timestamp DATETIME NOT NULL, - match_field VARCHAR NOT NULL, - match_value VARCHAR NOT NULL, - UNIQUE(user_id, achievement_tag, match_field), + match VARCHAR NOT NULL, + UNIQUE(user_id, achievement_tag, match), FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE );` ); From 6408e406044a105cc6b95518ff56adcae8e2d7cd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 13 Jan 2019 19:10:54 -0700 Subject: [PATCH 517/569] Fix minor typo --- core/servers/login/ssh.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index c05e9294..c94267c4 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -167,7 +167,7 @@ function SSHClient(clientConn) { config.users.newUserNames.map(newName => '"' + newName + '"').join(', ') : '(No new user names enabled!)'; - interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password'`; + interactivePrompt.prompt = `Access denied\n${stringFormat(artInfo.data, { newUserNames : newUserNameList })}\n${ctx.username}'s password:`; } return ctx.prompt(interactivePrompt, retryPrompt); }); From 680898b56bf63f1b99a30e6cbad8ac0d35fddfd2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 17 Jan 2019 20:13:49 -0700 Subject: [PATCH 518/569] Add minutes used to logoff event --- core/client_connections.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/client_connections.js b/core/client_connections.js index 33f1df8a..21aa5c1c 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -120,7 +120,8 @@ function removeClient(client) { ); if(client.user && client.user.isValid()) { - Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user } ); + const minutesOnline = moment().diff(moment(client.user.properties[UserProps.LastLoginTs]), 'minutes'); + Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user, minutesOnline } ); } Events.emit( From 483e7f4ee96c14cae48e16a0467ec157529ea2bf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 17 Jan 2019 20:14:33 -0700 Subject: [PATCH 519/569] Add global boolean to node sent event --- core/node_msg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/node_msg.js b/core/node_msg.js index bf22e24a..bb64757c 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -65,7 +65,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { } } - Events.emit(Events.getSystemEvents().UserSendNodeMsg, { user : this.client.user } ); + Events.emit(Events.getSystemEvents().UserSendNodeMsg, { user : this.client.user, global : -1 === nodeId } ); return this.prevMenu(cb); }); From 7c6e3e3ad4499416f06ae763c9fc7f313728d483 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 17 Jan 2019 20:14:59 -0700 Subject: [PATCH 520/569] Cleanup, notes, etc. --- core/system_events.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/core/system_events.js b/core/system_events.js index c0c09f35..1bed2d13 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -2,26 +2,26 @@ 'use strict'; module.exports = { - ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } - ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } - TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } + ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } + ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } + TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } - ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } - ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson) - MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) - PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson) + ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } + ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson) + MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) + PromptsChanged : 'codes.l33t.enigma.system.prompts_changed', // (prompt.hjson) // User - includes { user, ...} - NewUser : 'codes.l33t.enigma.system.user_new', - UserLogin : 'codes.l33t.enigma.system.user_login', - UserLogoff : 'codes.l33t.enigma.system.user_logoff', - UserUpload : 'codes.l33t.enigma.system.user_upload', // {..., files[ fileEntry, ...] } - UserDownload : 'codes.l33t.enigma.system.user_download', // {..., files[ fileEntry, ...] } - UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { areaTag } - UserSendMail : 'codes.l33t.enigma.system.user_send_mail', - UserRunDoor : 'codes.l33t.enigma.system.user_run_door', - UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', + NewUser : 'codes.l33t.enigma.system.user_new', // { ... } + UserLogin : 'codes.l33t.enigma.system.user_login', // { ... } + UserLogoff : 'codes.l33t.enigma.system.user_logoff', // { ... } + UserUpload : 'codes.l33t.enigma.system.user_upload', // { ..., files[ fileEntry, ...] } + UserDownload : 'codes.l33t.enigma.system.user_download', // { ..., files[ fileEntry, ...] } + UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { ..., areaTag } + UserSendMail : 'codes.l33t.enigma.system.user_send_mail', // { ... } + UserRunDoor : 'codes.l33t.enigma.system.user_run_door', // { ... } + UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', // { ..., global } UserStatSet : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } - UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // {..., statName, statIncrementBy, statValue } - UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // {..., achievementTag, points } + UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // { ..., statName, statIncrementBy, statValue } + UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // { ..., achievementTag, points } }; From dc7052105785c9508c3ceac0089adf5066768e9f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 17 Jan 2019 20:18:02 -0700 Subject: [PATCH 521/569] + New, more detailed user event log entries that can be summed/etc. * Last callers indicators now use new user event log entries --- core/last_callers.js | 2 +- core/stat_log.js | 26 ++------------ core/sys_event_user_log.js | 69 ++++++++++++++++++++++++++++++++++++ core/user_log_name.js | 21 +++++++++++ docs/modding/last-callers.md | 16 +++++---- 5 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 core/sys_event_user_log.js create mode 100644 core/user_log_name.js diff --git a/core/last_callers.js b/core/last_callers.js index f7c2552e..9d875b2e 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -174,7 +174,7 @@ exports.getModule = class LastCallersModule extends MenuModule { let indicatorSumsSql; if(actionIndicatorNames.length > 0) { indicatorSumsSql = actionIndicatorNames.map(i => { - return `SUM(CASE WHEN log_value='${_.snakeCase(i)}' THEN 1 ELSE 0 END) AS ${i}`; + return `SUM(CASE WHEN log_name='${_.snakeCase(i)}' THEN 1 ELSE 0 END) AS ${i}`; }); } diff --git a/core/stat_log.js b/core/stat_log.js index 0ff6aff6..f03319d0 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -364,30 +364,8 @@ class StatLog { } initUserEvents(cb) { - // - // We map some user events directly to user stat log entries such that they - // are persisted for a time. - // - const Events = require('./events.js'); - const systemEvents = Events.getSystemEvents(); - - const interestedEvents = [ - systemEvents.NewUser, - systemEvents.UserUpload, systemEvents.UserDownload, - systemEvents.UserPostMessage, systemEvents.UserSendMail, - systemEvents.UserRunDoor, systemEvents.UserSendNodeMsg, - systemEvents.UserAchievementEarned, - ]; - - Events.addMultipleEventListener(interestedEvents, (event, eventName) => { - this.appendUserLogEntry( - event.user, - 'system_event', - eventName.replace(/^codes\.l33t\.enigma\.system\./, ''), // strip package name prefix - 90 - ); - }); - + const systemEventUserLogInit = require('./sys_event_user_log.js'); + systemEventUserLogInit(this); return cb(null); } } diff --git a/core/sys_event_user_log.js b/core/sys_event_user_log.js new file mode 100644 index 00000000..8b9b3f22 --- /dev/null +++ b/core/sys_event_user_log.js @@ -0,0 +1,69 @@ +/* jslint node: true */ +'use strict'; + +const Events = require('./events.js'); +const LogNames = require('./user_log_name.js'); + +const DefaultKeepForDays = 365; + +module.exports = function systemEventUserLogInit(statLog) { + const systemEvents = Events.getSystemEvents(); + + const interestedEvents = [ + systemEvents.NewUser, + systemEvents.UserLogin, systemEvents.UserLogoff, + systemEvents.UserUpload, systemEvents.UserDownload, + systemEvents.UserPostMessage, systemEvents.UserSendMail, + systemEvents.UserRunDoor, systemEvents.UserSendNodeMsg, + systemEvents.UserAchievementEarned, + ]; + + const append = (e, n, v) => { + statLog.appendUserLogEntry(e.user, n, v, DefaultKeepForDays); + }; + + Events.addMultipleEventListener(interestedEvents, (event, eventName) => { + const detailHandler = { + [ systemEvents.NewUser ] : (e) => { + append(e, LogNames.NewUser, 1); + }, + [ systemEvents.UserLogin ] : (e) => { + append(e, LogNames.Login, 1); + }, + [ systemEvents.UserLogoff ] : (e) => { + append(e, LogNames.Logoff, e.minutesOnline); + }, + [ systemEvents.UserUpload ] : (e) => { + append(e, LogNames.UlFiles, e.files.length); + const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.meta.byte_size, 0); + append(e, LogNames.UlFileBytes, totalBytes); + }, + [ systemEvents.UserDownload ] : (e) => { + append(e, LogNames.DlFiles, e.files.length); + const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.meta.byte_size, 0); + append(e, LogNames.DlFileBytes, totalBytes); + }, + [ systemEvents.UserPostMessage ] : (e) => { + append(e, LogNames.PostMessage, e.areaTag); + }, + [ systemEvents.UserSendMail ] : (e) => { + append(e, LogNames.SendMail, 1); + }, + [ systemEvents.UserRunDoor ] : (e) => { + // :TODO: store door tag, else '-' ? + append(e, LogNames.RunDoor, 1); + }, + [ systemEvents.UserSendNodeMsg ] : (e) => { + append(e, LogNames.SendNodeMsg, e.global ? 'global' : 'direct'); + }, + [ systemEvents.UserAchievementEarned ] : (e) => { + append(e, LogNames.AchievementEarned, e.achievementTag); + append(e, LogNames.AchievementPointsEarned, e.points); + } + }[eventName]; + + if(detailHandler) { + detailHandler(event); + } + }); +}; diff --git a/core/user_log_name.js b/core/user_log_name.js new file mode 100644 index 00000000..6186d326 --- /dev/null +++ b/core/user_log_name.js @@ -0,0 +1,21 @@ +/* jslint node: true */ +'use strict'; + +// +// Common (but not all!) user log names +// +module.exports = { + NewUser : 'new_user', + Login : 'login', + Logoff : 'logoff', + UlFiles : 'ul_files', // value=count + UlFileBytes : 'ul_file_bytes', // value=total bytes + DlFiles : 'dl_files', // value=count + DlFileBytes : 'dl_file_bytes', // value=total bytes + PostMessage : 'post_msg', // value=areaTag + SendMail : 'send_mail', + RunDoor : 'run_door', + SendNodeMsg : 'send_node_msg', // value=global|direct + AchievementEarned : 'achievement_earned', // value=achievementTag + AchievementPointsEarned : 'achievement_pts_earned', // value=points earned +}; diff --git a/docs/modding/last-callers.md b/docs/modding/last-callers.md index 830244d7..0e15b4f8 100644 --- a/docs/modding/last-callers.md +++ b/docs/modding/last-callers.md @@ -14,13 +14,15 @@ Available `config` block entries: * `sysop`: Sysop options: * `collapse`: Collapse or roll up entries that fall within the period specified. May be a string in the form of `30 minutes`, `3 weeks`, `1 hour`, etc. * `hide`: Hide all +op logins -* `actionIndicators`: Maps user actions to indicators. For example: `userDownload` to "D". Available indicators: - * `userDownload` - * `userUpload` - * `userPostMsg` - * `userSendMail` - * `userRunDoor` - * `userSendNodeMsg` +* `actionIndicators`: Maps user events/actions to indicators. For example: `userDownload` to "D". Available indicators: + * `newUser`: User is new. + * `dlFiles`: User downloaded file(s). + * `ulFiles`: User uploaded file(s). + * `postMsg`: User posted message(s) to the message base, EchoMail, etc. + * `sendMail`: User sent _private_ mail. + * `runDoor`: User ran door(s). + * `sendNodeMsg`: User sent a node message(s). + * `achievementEarned`: User earned an achievement(s). * `actionIndicatorDefault`: Default indicator when an action is not set. Defaults to "-". Remember that entries such as `actionIndicators` and `actionIndicatorDefault` may contain pipe color codes! From 39e7fe5d69811473fb0ccd31190342dcb555e54d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 17 Jan 2019 20:18:23 -0700 Subject: [PATCH 522/569] + WIP TopX module --- core/top_x.js | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 core/top_x.js diff --git a/core/top_x.js b/core/top_x.js new file mode 100644 index 00000000..7034a3d6 --- /dev/null +++ b/core/top_x.js @@ -0,0 +1,222 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const UserProps = require('./user_property.js'); +const UserLogNames = require('./user_log_name.js'); +const { Errors } = require('./enig_error.js'); +const UserDb = require('./database.js').dbs.user; +const SysDb = require('./database.js').dbs.system; +const User = require('./user.js'); +const stringFormat = require('./string_format.js'); + +// deps +const _ = require('lodash'); +const async = require('async'); + +exports.moduleInfo = { + name : 'TopX', + desc : 'Displays users top X stats', + author : 'NuSkooler', + packageName : 'codes.l33t.enigma.topx', +}; + +const FormIds = { + menu : 0, +}; + +exports.getModule = class TopXModule extends MenuModule { + constructor(options) { + super(options); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.series( + [ + (callback) => { + const userPropValues = _.values(UserProps); + const userLogValues = _.values(UserLogNames); + + return this.validateConfigFields( + { + mciMap : (key, config) => { + const mciCodes = Object.keys(config.mciMap).map(mci => { + return parseInt(mci); + }).filter(mci => !isNaN(mci)); + if(0 === mciCodes.length) { + return false; + } + return mciCodes.every(mci => { + const o = config.mciMap[mci]; + if(!_.isObject(o)) { + return false; + } + const type = o.type; + switch(type) { + case 'userProp' : + if(!userPropValues.includes(o.propName)) { + return false; + } + // VM# must exist for this mci + if(!_.isObject(mciData, [ 'menu', `VM${mci}` ])) { + return false; + } + break; + + case 'userEventLog' : + if(!userLogValues.includes(o.logName)) { + return false; + } + // VM# must exist for this mci + if(!_.isObject(mciData, [ 'menu', `VM${mci}` ])) { + return false; + } + break; + + default : + return false; + } + return true; + }); + } + }, + callback + ); + }, + (callback) => { + return this.prepViewController('menu', FormIds.menu, mciData.menu, callback); + }, + (callback) => { + async.forEachSeries(Object.keys(this.config.mciMap), (mciCode, nextMciCode) => { + return this.populateTopXList(mciCode, nextMciCode); + }, + err => { + return callback(err); + }); + } + ], + err => { + return cb(err); + } + ); + }); + } + + populateTopXList(mciCode, cb) { + const listView = this.viewControllers.menu.getView(mciCode); + if(!listView) { + return cb(Errors.UnexpectedState(`Failed to get view for MCI ${mciCode}`)); + } + + const type = this.config.mciMap[mciCode].type; + switch(type) { + case 'userProp' : return this.populateTopXUserProp(listView, mciCode, cb); + case 'userEventLog' : return this.populateTopXUserEventLog(listView, mciCode, cb); + + // we should not hit here; validation happens up front + default : return cb(Errors.UnexpectedState(`Unexpected type: ${type}`)); + } + } + + rowsToItems(rows, cb) { + async.map(rows, (row, nextRow) => { + this.loadUserInfo(row.user_id, (err, userInfo) => { + if(err) { + return nextRow(err); + } + return nextRow(null, Object.assign(userInfo, { value : row.value })); + }); + }, + (err, items) => { + return cb(err, items); + }); + } + + populateTopXUserEventLog(listView, mciCode, cb) { + const count = listView.dimens.height || 1; + const daysBack = this.config.mciMap[mciCode].daysBack; + const whereDate = daysBack ? `AND DATETIME(timestamp) >= DATETIME('now', '-${daysBack} days')` : ''; + + SysDb.all( + `SELECT user_id, SUM(CASE WHEN typeof(log_value) IS 'text' THEN 1 ELSE CAST(log_value AS INTEGER) END) AS value + FROM user_event_log + WHERE log_name = ? ${whereDate} + GROUP BY user_id + ORDER BY value DESC + LIMIT ${count};`, + [ this.config.mciMap[mciCode].logName ], + (err, rows) => { + if(err) { + return cb(err); + } + + this.rowsToItems(rows, (err, items) => { + if(err) { + return cb(err); + } + listView.setItems(items); + listView.redraw(); + }); + } + ); + } + + populateTopXUserProp(listView, mciCode, cb) { + const count = listView.dimens.height || 1; + UserDb.all( + `SELECT user_id, CAST(prop_value AS INTEGER) AS value + FROM user_property + WHERE prop_name = ? + ORDER BY value DESC + LIMIT ${count};`, + [ this.config.mciMap[mciCode].propName ], + (err, rows) => { + if(err) { + return cb(err); + } + + this.rowsToItems(rows, (err, items) => { + if(err) { + return cb(err); + } + listView.setItems(items); + listView.redraw(); + }); + } + ); + } + + loadUserInfo(userId, cb) { + const getPropOpts = { + names : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations ] + }; + + const userInfo = { userId }; + User.getUserName(userId, (err, userName) => { + if(err) { + return cb(err); + } + + userInfo.userName = userName; + + User.loadProperties(userId, getPropOpts, (err, props) => { + if(err) { + return cb(err); + } + + userInfo.location = props[UserProps.Location] || ''; + userInfo.affils = userInfo.affiliation = props[UserProps.Affiliations] || ''; + userInfo.realName = props[UserProps.RealName] || ''; + + return cb(null, userInfo); + }); + }); + } +}; From 2a3271ef4e23f2a4345e49f56c25fe59690cd5e0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 17 Jan 2019 21:27:25 -0700 Subject: [PATCH 523/569] Fix some events --- core/sys_event_user_log.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/sys_event_user_log.js b/core/sys_event_user_log.js index 8b9b3f22..39120987 100644 --- a/core/sys_event_user_log.js +++ b/core/sys_event_user_log.js @@ -34,14 +34,18 @@ module.exports = function systemEventUserLogInit(statLog) { append(e, LogNames.Logoff, e.minutesOnline); }, [ systemEvents.UserUpload ] : (e) => { - append(e, LogNames.UlFiles, e.files.length); - const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.meta.byte_size, 0); - append(e, LogNames.UlFileBytes, totalBytes); + if(e.files.length) { // we can get here for dupe uploads + append(e, LogNames.UlFiles, e.files.length); + const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.meta.byte_size, 0); + append(e, LogNames.UlFileBytes, totalBytes); + } }, [ systemEvents.UserDownload ] : (e) => { - append(e, LogNames.DlFiles, e.files.length); - const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.meta.byte_size, 0); - append(e, LogNames.DlFileBytes, totalBytes); + if(e.files.length) { + append(e, LogNames.DlFiles, e.files.length); + const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.byteSize, 0); + append(e, LogNames.DlFileBytes, totalBytes); + } }, [ systemEvents.UserPostMessage ] : (e) => { append(e, LogNames.PostMessage, e.areaTag); From 4e1997302e07f1bfce918e99d0aa7a1cf1f0b57a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 17 Jan 2019 21:27:37 -0700 Subject: [PATCH 524/569] Fairly functional --- core/top_x.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/core/top_x.js b/core/top_x.js index 7034a3d6..37b4f4bd 100644 --- a/core/top_x.js +++ b/core/top_x.js @@ -44,6 +44,13 @@ exports.getModule = class TopXModule extends MenuModule { const userPropValues = _.values(UserProps); const userLogValues = _.values(UserLogNames); + const hasMci = (c, t) => { + if(!Array.isArray(t)) { + t = [ t ]; + } + return t.some(t => _.isObject(mciData, [ 'menu', `${t}${c}` ])); + }; + return this.validateConfigFields( { mciMap : (key, config) => { @@ -75,7 +82,7 @@ exports.getModule = class TopXModule extends MenuModule { return false; } // VM# must exist for this mci - if(!_.isObject(mciData, [ 'menu', `VM${mci}` ])) { + if(!hasMci(mci, ['VM'])) { return false; } break; @@ -121,17 +128,18 @@ exports.getModule = class TopXModule extends MenuModule { case 'userEventLog' : return this.populateTopXUserEventLog(listView, mciCode, cb); // we should not hit here; validation happens up front - default : return cb(Errors.UnexpectedState(`Unexpected type: ${type}`)); + default : return cb(Errors.UnexpectedState(`Unexpected type: ${type}`)); } } rowsToItems(rows, cb) { - async.map(rows, (row, nextRow) => { + let position = 1; + async.mapSeries(rows, (row, nextRow) => { this.loadUserInfo(row.user_id, (err, userInfo) => { if(err) { return nextRow(err); } - return nextRow(null, Object.assign(userInfo, { value : row.value })); + return nextRow(null, Object.assign(userInfo, { position : position++, value : row.value })); }); }, (err, items) => { @@ -142,12 +150,15 @@ exports.getModule = class TopXModule extends MenuModule { populateTopXUserEventLog(listView, mciCode, cb) { const count = listView.dimens.height || 1; const daysBack = this.config.mciMap[mciCode].daysBack; - const whereDate = daysBack ? `AND DATETIME(timestamp) >= DATETIME('now', '-${daysBack} days')` : ''; + const shouldSum = _.get(this.config.mciMap[mciCode], 'sum', true); + + const valueSql = shouldSum ? 'SUM(CAST(log_value AS INTEGER))' : 'COUNT()'; + const dateSql = daysBack ? `AND DATETIME(timestamp) >= DATETIME('now', '-${daysBack} days')` : ''; SysDb.all( - `SELECT user_id, SUM(CASE WHEN typeof(log_value) IS 'text' THEN 1 ELSE CAST(log_value AS INTEGER) END) AS value + `SELECT user_id, ${valueSql} AS value FROM user_event_log - WHERE log_name = ? ${whereDate} + WHERE log_name = ? ${dateSql} GROUP BY user_id ORDER BY value DESC LIMIT ${count};`, @@ -163,6 +174,7 @@ exports.getModule = class TopXModule extends MenuModule { } listView.setItems(items); listView.redraw(); + return cb(null); }); } ); @@ -188,6 +200,7 @@ exports.getModule = class TopXModule extends MenuModule { } listView.setItems(items); listView.redraw(); + return cb(null); }); } ); From 77763911842043610de0875060fa43916b207542 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 18 Jan 2019 22:09:10 -0700 Subject: [PATCH 525/569] Door utility and door tracking * Require >= 45s of time in a door before it counts as "run" --- core/bbs_link.js | 22 +++++++--------------- core/combatnet.js | 19 ++++++++----------- core/door_party.js | 22 ++++++++++------------ core/door_util.js | 41 +++++++++++++++++++++++++++++++++++++++++ core/exodus.js | 18 ++++++++---------- 5 files changed, 74 insertions(+), 48 deletions(-) create mode 100644 core/door_util.js diff --git a/core/bbs_link.js b/core/bbs_link.js index 9144cf0a..01eb3bfe 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -4,16 +4,16 @@ const { MenuModule } = require('./menu_module.js'); const { resetScreen } = require('./ansi_term.js'); const { Errors } = require('./enig_error.js'); -const Events = require('./events.js'); -const StatLog = require('./stat_log.js'); -const UserProps = require('./user_property.js'); +const { + trackDoorRunBegin, + trackDoorRunEnd +} = require('./door_util.js'); // deps const async = require('async'); const http = require('http'); const net = require('net'); const crypto = require('crypto'); -const moment = require('moment'); const packageJson = require('../package.json'); @@ -139,13 +139,9 @@ exports.getModule = class BBSLinkModule extends MenuModule { self.client.term.write(resetScreen()); self.client.term.write(' Connecting to BBSLink.net, please wait...\n'); - const startTime = moment(); + const doorTracking = trackDoorRunBegin(self.client, `bbslink_${self.config.door}`); const bridgeConnection = net.createConnection(connectOpts, function connected() { - // bump stats, fire events, etc. - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); - Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); - self.client.log.info(connectOpts, 'BBSLink bridge connection established'); self.client.term.output.pipe(bridgeConnection); @@ -153,7 +149,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { self.client.once('end', function clientEnd() { self.client.log.info('Connection ended. Terminating BBSLink connection'); clientTerminated = true; - bridgeConnection.end(); + bridgeConnection.end(); }); }); @@ -161,11 +157,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { self.client.term.output.unpipe(bridgeConnection); self.client.term.output.resume(); - const endTime = moment(); - const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); - if(runTimeMinutes > 0) { - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); - } + trackDoorRunEnd(doorTracking); }; bridgeConnection.on('data', function incomingData(data) { diff --git a/core/combatnet.js b/core/combatnet.js index bfd98103..8f1a5623 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -5,14 +5,14 @@ const { MenuModule } = require('../core/menu_module.js'); const { resetScreen } = require('../core/ansi_term.js'); const { Errors } = require('./enig_error.js'); -const Events = require('./events.js'); -const StatLog = require('./stat_log.js'); -const UserProps = require('./user_property.js'); +const { + trackDoorRunBegin, + trackDoorRunEnd +} = require('./door_util.js'); // deps const async = require('async'); const RLogin = require('rlogin'); -const moment = require('moment'); exports.moduleInfo = { name : 'CombatNet', @@ -50,16 +50,14 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.term.write(resetScreen()); self.client.term.write('Connecting to CombatNet, please wait...\n'); - const startTime = moment(); + let doorTracking; const restorePipeToNormal = function() { if(self.client.term.output) { self.client.term.output.removeListener('data', sendToRloginBuffer); - const endTime = moment(); - const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); - if(runTimeMinutes > 0) { - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + if(doorTracking) { + trackDoorRunEnd(doorTracking); } } }; @@ -102,8 +100,7 @@ exports.getModule = class CombatNetModule extends MenuModule { self.client.log.info('Connected to CombatNet'); self.client.term.output.on('data', sendToRloginBuffer); - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); - Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); + doorTracking = trackDoorRunBegin(self.client); } else { return callback(Errors.General('Failed to establish establish CombatNet connection')); } diff --git a/core/door_party.js b/core/door_party.js index df3d189f..184416f7 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -5,14 +5,14 @@ const { MenuModule } = require('./menu_module.js'); const { resetScreen } = require('./ansi_term.js'); const { Errors } = require('./enig_error.js'); -const Events = require('./events.js'); -const StatLog = require('./stat_log.js'); -const UserProps = require('./user_property.js'); +const { + trackDoorRunBegin, + trackDoorRunEnd +} = require('./door_util.js'); // deps const async = require('async'); const SSHClient = require('ssh2').Client; -const moment = require('moment'); exports.moduleInfo = { name : 'DoorParty', @@ -58,17 +58,15 @@ exports.getModule = class DoorPartyModule extends MenuModule { let pipeRestored = false; let pipedStream; - const startTime = moment(); + let doorTracking; const restorePipe = function() { if(pipedStream && !pipeRestored && !clientTerminated) { self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); - const endTime = moment(); - const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); - if(runTimeMinutes > 0) { - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + if(doorTracking) { + trackDoorRunEnd(doorTracking); } } }; @@ -87,6 +85,8 @@ exports.getModule = class DoorPartyModule extends MenuModule { return callback(Errors.General('Failed to establish tunnel')); } + doorTracking = trackDoorRunBegin(self.client); + // // Send rlogin // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. @@ -95,9 +95,6 @@ exports.getModule = class DoorPartyModule extends MenuModule { const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; stream.write(rlogin); - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); - Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); - pipedStream = stream; // :TODO: this is hacky... self.client.term.output.pipe(stream); @@ -115,6 +112,7 @@ exports.getModule = class DoorPartyModule extends MenuModule { sshClient.on('error', err => { self.client.log.info(`DoorParty SSH client error: ${err.message}`); + trackDoorRunEnd(doorTracking); }); sshClient.on('close', () => { diff --git a/core/door_util.js b/core/door_util.js new file mode 100644 index 00000000..c1681058 --- /dev/null +++ b/core/door_util.js @@ -0,0 +1,41 @@ +/* jslint node: true */ +'use strict'; + +const UserProps = require('./user_property.js'); +const Events = require('./events.js'); +const StatLog = require('./stat_log.js'); + +const moment = require('moment'); + +exports.trackDoorRunBegin = trackDoorRunBegin; +exports.trackDoorRunEnd = trackDoorRunEnd; + + +function trackDoorRunBegin(client, doorTag) { + const startTime = moment(); + + // door must be running for >= 45s for us to officially record it + const timeout = setTimeout( () => { + StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1); + + const eventInfo = { user : client.user }; + if(doorTag) { + eventInfo.doorTag = doorTag; + } + Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo); + }, 45 * 1000); + + return { startTime, timeout, client, doorTag }; +} + +function trackDoorRunEnd(trackInfo) { + const { startTime, timeout, client } = trackInfo; + + clearTimeout(timeout); + + const endTime = moment(); + const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + if(runTimeMinutes > 0) { + StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + } +} \ No newline at end of file diff --git a/core/exodus.js b/core/exodus.js index eaf4c9a5..5ed29a4e 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -10,9 +10,10 @@ const Log = require('./logger.js').log; const { getEnigmaUserAgent } = require('./misc_util.js'); -const Events = require('./events.js'); -const StatLog = require('./stat_log.js'); -const UserProps = require('./user_property.js'); +const { + trackDoorRunBegin, + trackDoorRunEnd +} = require('./door_util.js'); // deps const async = require('async'); @@ -156,17 +157,15 @@ exports.getModule = class ExodusModule extends MenuModule { let pipeRestored = false; let pipedStream; - const startTime = moment(); + let doorTracking; function restorePipe() { if(pipedStream && !pipeRestored && !clientTerminated) { self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); - const endTime = moment(); - const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); - if(runTimeMinutes > 0) { - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + if(doorTracking) { + trackDoorRunEnd(doorTracking); } } } @@ -198,8 +197,7 @@ exports.getModule = class ExodusModule extends MenuModule { }); sshClient.shell(window, options, (err, stream) => { - StatLog.incrementUserStat(self.client.user, UserProps.DoorRunTotalCount, 1); - Events.emit(Events.getSystemEvents().UserRunDoor, { user : self.client.user } ); + doorTracking = trackDoorRunBegin(self.client, `exodus_${self.config.door}`); pipedStream = stream; // :TODO: ewwwwwwwww hack self.client.term.output.pipe(stream); From 0457a6601fd72ce60dba12c1c790bfbb75dd01b4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 18 Jan 2019 23:12:01 -0700 Subject: [PATCH 526/569] Better door tracking * Send event info with door run time & door tag * Only if >= 45s * Only log minutes if >= 1 * No timer required; track only @ door exit time --- core/abracadabra.js | 19 ++++++------------- core/door_util.js | 33 +++++++++++++++------------------ core/sys_event_user_log.js | 4 ++-- core/system_events.js | 2 +- core/user_log_name.js | 3 ++- 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 42731ac0..34374049 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -6,17 +6,17 @@ const DropFile = require('./dropfile.js'); const Door = require('./door.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); -const Events = require('./events.js'); const { Errors } = require('./enig_error.js'); -const StatLog = require('./stat_log.js'); -const UserProps = require('./user_property.js'); +const { + trackDoorRunBegin, + trackDoorRunEnd +} = require('./door_util.js'); // deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); const paths = require('path'); -const moment = require('moment'); const activeDoorNodeInstances = {}; @@ -152,9 +152,6 @@ exports.getModule = class AbracadabraModule extends MenuModule { } runDoor() { - StatLog.incrementUserStat(this.client.user, UserProps.DoorRunTotalCount, 1); - Events.emit(Events.getSystemEvents().UserRunDoor, { user : this.client.user } ); - this.client.term.write(ansi.resetScreen()); const exeInfo = { @@ -168,14 +165,10 @@ exports.getModule = class AbracadabraModule extends MenuModule { node : this.client.node, }; - const startTime = moment(); + const doorTracking = trackDoorRunBegin(this.client, this.config.name); this.doorInstance.run(exeInfo, () => { - const endTime = moment(); - const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); - if(runTimeMinutes > 0) { - StatLog.incrementUserStat(this.client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); - } + trackDoorRunEnd(doorTracking); // // Try to clean up various settings such as scroll regions that may diff --git a/core/door_util.js b/core/door_util.js index c1681058..6517f1be 100644 --- a/core/door_util.js +++ b/core/door_util.js @@ -10,32 +10,29 @@ const moment = require('moment'); exports.trackDoorRunBegin = trackDoorRunBegin; exports.trackDoorRunEnd = trackDoorRunEnd; - function trackDoorRunBegin(client, doorTag) { const startTime = moment(); - - // door must be running for >= 45s for us to officially record it - const timeout = setTimeout( () => { - StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1); - - const eventInfo = { user : client.user }; - if(doorTag) { - eventInfo.doorTag = doorTag; - } - Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo); - }, 45 * 1000); - - return { startTime, timeout, client, doorTag }; + return { startTime, client, doorTag }; } function trackDoorRunEnd(trackInfo) { - const { startTime, timeout, client } = trackInfo; + const { startTime, client, doorTag } = trackInfo; - clearTimeout(timeout); + const diff = moment.duration(moment().diff(startTime)); + if(diff.asSeconds() >= 45) { + StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1); + } - const endTime = moment(); - const runTimeMinutes = Math.floor(moment.duration(endTime.diff(startTime)).asMinutes()); + const runTimeMinutes = Math.floor(diff.asMinutes()); if(runTimeMinutes > 0) { StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + + const eventInfo = { + runTimeMinutes, + user : client.user, + doorTag : doorTag || 'unknown', + }; + + Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo); } } \ No newline at end of file diff --git a/core/sys_event_user_log.js b/core/sys_event_user_log.js index 39120987..63ae0e55 100644 --- a/core/sys_event_user_log.js +++ b/core/sys_event_user_log.js @@ -54,8 +54,8 @@ module.exports = function systemEventUserLogInit(statLog) { append(e, LogNames.SendMail, 1); }, [ systemEvents.UserRunDoor ] : (e) => { - // :TODO: store door tag, else '-' ? - append(e, LogNames.RunDoor, 1); + append(e, LogNames.RunDoor, e.doorTag); + append(e, LogNames.RunDoorMinutes, e.runTimeMinutes); }, [ systemEvents.UserSendNodeMsg ] : (e) => { append(e, LogNames.SendNodeMsg, e.global ? 'global' : 'direct'); diff --git a/core/system_events.js b/core/system_events.js index 1bed2d13..173f753b 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -19,7 +19,7 @@ module.exports = { UserDownload : 'codes.l33t.enigma.system.user_download', // { ..., files[ fileEntry, ...] } UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { ..., areaTag } UserSendMail : 'codes.l33t.enigma.system.user_send_mail', // { ... } - UserRunDoor : 'codes.l33t.enigma.system.user_run_door', // { ... } + UserRunDoor : 'codes.l33t.enigma.system.user_run_door', // { ..., runTimeMinutes, doorTag|unknown } UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', // { ..., global } UserStatSet : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // { ..., statName, statIncrementBy, statValue } diff --git a/core/user_log_name.js b/core/user_log_name.js index 6186d326..77fa996c 100644 --- a/core/user_log_name.js +++ b/core/user_log_name.js @@ -14,7 +14,8 @@ module.exports = { DlFileBytes : 'dl_file_bytes', // value=total bytes PostMessage : 'post_msg', // value=areaTag SendMail : 'send_mail', - RunDoor : 'run_door', + RunDoor : 'run_door', // value=doorTag|unknown + RunDoorMinutes : 'run_door_minutes', // value=minutes ran SendNodeMsg : 'send_node_msg', // value=global|direct AchievementEarned : 'achievement_earned', // value=achievementTag AchievementPointsEarned : 'achievement_pts_earned', // value=points earned From 4173a2e6db9e55595b14845c20edc2f481b60fa1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 18 Jan 2019 23:13:22 -0700 Subject: [PATCH 527/569] Add docs for TopX module --- docs/_includes/nav.md | 1 + docs/modding/top-x.md | 60 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 docs/modding/top-x.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 79ce2f31..e67f1163 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -82,6 +82,7 @@ - [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %}) - [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %}) - [Node to Node Messaging]({{ site.baseurl }}{% link modding/node-msg.md %}) + - [Top X]({{ site.baseurl }}{% link modding/top-x.md %}) - Administration - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) diff --git a/docs/modding/top-x.md b/docs/modding/top-x.md new file mode 100644 index 00000000..6ab9c545 --- /dev/null +++ b/docs/modding/top-x.md @@ -0,0 +1,60 @@ +--- +layout: page +title: TopX +--- +## The TopX Module +The built in `top_x` module allows for displaying oldschool top user stats for the week, month, etc. Ops can configure what stat(s) are displayed and how far back in days the stats are considered. + +## Configuration +### Config Block +Available `config` block entries: +* `mciMap`: Supplies a mapping of MCI code to data source. See `mciMap` below. + +#### MCI Map (mciMap) +The `mciMap` `config` block configures MCI code mapping to data sources. Currently the following data sources (determined by `type`) are available: + +| Type | Description | +|-------------|-------------| +| `userEventLog` | Top counts or sum of values found in the User Event Log. | +| `userProp` | Top values (aka "scores") from user properties. | + +##### User Event Log (userEventLog) +When `type` is set to `userEventLog`, entries from the User Event Log can be counted (ie: individual instances of a particular log item) or summed in the case of log items that have numeric values. The default is to sum. + +Some current User Event Log `logName` examples include `ul_files`, `dl_file_bytes`, or `achievement_earned`. See [user_log_name.js](/core/user_log_name.js) for additional information. + +Example `userEventLog` entry: +```hjson +mciMap: { + 1: { // e.g.: %VM1 + type: userEventLog + logName: achievement_pts_earned // top achievement points earned + sum: true // this is the default + daysBack: 7 // omit daysBack for all-of-time + } +} +``` + +#### User Properties (userProp) +When `type` is set to `userProp`, data is collected from individual user's properties. For example a `propName` of `minutes_online_total_count`. See [user_property.js](/core/user_property.js) for more information. + +Example `userProp` entry: +```hjson +mciMap: { + 2: { // e.g.: %VM2 + type: userProp + propName: minutes_online_total_count // top users by minutes spent on the board + } +} +``` + +### Theming +Generally `mciMap` entries will point to a Vertical List View Menu (`%VM1`, `%VM2`, etc.). The following `itemFormat` object is provided: +* `value`: The value acquired from the supplied data source. +* `userName`: User's username. +* `realName`: User's real name. +* `location`: User's location. +* `affils` or `affiliation`: Users affiliations. +* `position`: Rank position (numeric). + +Remember that string format rules apply, so for example, if displaying top uploaded bytes (`ul_file_bytes`), a `itemFormat` may be `{userName} - {value!sizeWithAbbr}` yielding something like "TopDude - 4 GB". See [MCI](/docs/art/mci.md) for additional information. From 4696bd9ff27a9ab70a3a3ae913e65750bdd12a8f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 18 Jan 2019 23:46:15 -0700 Subject: [PATCH 528/569] Fix PCBoard/WildCat! color codes --- core/color_codes.js | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/core/color_codes.js b/core/color_codes.js index 4119a8ce..ff08275e 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -131,7 +131,7 @@ function renegadeToAnsi(s, client) { // // Supported control code formats: // * Renegade : |## -// * PCBoard : @X## where the first number/char is FG color, and second is BG +// * PCBoard : @X## where the first number/char is BG color, and second is FG // * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix // * WWIV : ^# // * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format @@ -179,26 +179,6 @@ function controlCodesToAnsi(s, client) { v = m[4]; } - fg = { - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], - - 8 : [ 'blink', 'black' ], - 9 : [ 'blink', 'blue' ], - A : [ 'blink', 'green' ], - B : [ 'blink', 'cyan' ], - C : [ 'blink', 'red' ], - D : [ 'blink', 'magenta' ], - E : [ 'blink', 'yellow' ], - F : [ 'blink', 'white' ], - }[v.charAt(0)] || ['normal']; - bg = { 0 : [ 'blackBG' ], 1 : [ 'blueBG' ], @@ -217,7 +197,27 @@ function controlCodesToAnsi(s, client) { D : [ 'bold', 'magentaBG' ], E : [ 'bold', 'yellowBG' ], F : [ 'bold', 'whiteBG' ], - }[v.charAt(1)] || [ 'normal' ]; + }[v.charAt(0)] || [ 'normal' ]; + + fg = { + 0 : [ 'reset', 'black' ], + 1 : [ 'reset', 'blue' ], + 2 : [ 'reset', 'green' ], + 3 : [ 'reset', 'cyan' ], + 4 : [ 'reset', 'red' ], + 5 : [ 'reset', 'magenta' ], + 6 : [ 'reset', 'yellow' ], + 7 : [ 'reset', 'white' ], + + 8 : [ 'blink', 'black' ], + 9 : [ 'blink', 'blue' ], + A : [ 'blink', 'green' ], + B : [ 'blink', 'cyan' ], + C : [ 'blink', 'red' ], + D : [ 'blink', 'magenta' ], + E : [ 'blink', 'yellow' ], + F : [ 'blink', 'white' ], + }[v.charAt(1)] || ['normal']; v = ANSI.sgr(fg.concat(bg)); result += s.substr(lastIndex, m.index - lastIndex) + v; From 9b7b5c6fffa9c1c6c260934a8313e4bbe87fc3b3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 18 Jan 2019 23:47:00 -0700 Subject: [PATCH 529/569] Initial to_ansi util for color codes -> ANSI --- util/to_ansi.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100755 util/to_ansi.js diff --git a/util/to_ansi.js b/util/to_ansi.js new file mode 100755 index 00000000..72838493 --- /dev/null +++ b/util/to_ansi.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +/* jslint node: true */ +/* eslint-disable no-console */ +'use strict'; + +const { controlCodesToAnsi } = require('../core/color_codes.js'); + +const fs = require('graceful-fs'); +const iconv = require('iconv-lite'); + +const ToolVersion = '1.0.0'; + +function main() { + const argv = exports.argv = require('minimist')(process.argv.slice(2), { + alias : { + h : 'help', + v : 'version', + } + }); + + if(argv.version) { + console.info(ToolVersion); + return 0; + } + + if(0 === argv._.length || argv.help) { + console.info('usage: to_ansi.js [--version] [--help] PATH'); + return 0; + } + + const path = argv._[0]; + + fs.readFile(path, (err, data) => { + if(err) { + console.error(err.message); + return -1; + } + + data = iconv.decode(data, 'cp437'); + console.info(controlCodesToAnsi(data)); + return 0; + }); +} + +main(); From 34f0afc1752ac022f184e45c2b0b62483dfa9a13 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 20 Jan 2019 12:22:42 -0700 Subject: [PATCH 530/569] Fix INSERT clause for cases of overlap --- core/achievement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/achievement.js b/core/achievement.js index 333b968c..d2ac2508 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -193,7 +193,7 @@ class Achievements { StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points); UserDb.run( - `INSERT INTO user_achievement (user_id, achievement_tag, timestamp, match) + `INSERT OR IGNORE INTO user_achievement (user_id, achievement_tag, timestamp, match) VALUES (?, ?, ?, ?);`, [ info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField ], err => { From 18a7a79f14e264bc6826b36aa340efb254d1e3a2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 20 Jan 2019 21:57:31 -0700 Subject: [PATCH 531/569] TopX mciMap standardized on "value" vs (propName, logName, etc.) --- core/top_x.js | 18 +++++++++--------- docs/modding/top-x.md | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/top_x.js b/core/top_x.js index 37b4f4bd..2403c380 100644 --- a/core/top_x.js +++ b/core/top_x.js @@ -9,7 +9,6 @@ const { Errors } = require('./enig_error.js'); const UserDb = require('./database.js').dbs.user; const SysDb = require('./database.js').dbs.system; const User = require('./user.js'); -const stringFormat = require('./string_format.js'); // deps const _ = require('lodash'); @@ -68,7 +67,7 @@ exports.getModule = class TopXModule extends MenuModule { const type = o.type; switch(type) { case 'userProp' : - if(!userPropValues.includes(o.propName)) { + if(!userPropValues.includes(o.value)) { return false; } // VM# must exist for this mci @@ -78,7 +77,7 @@ exports.getModule = class TopXModule extends MenuModule { break; case 'userEventLog' : - if(!userLogValues.includes(o.logName)) { + if(!userLogValues.includes(o.value)) { return false; } // VM# must exist for this mci @@ -122,7 +121,7 @@ exports.getModule = class TopXModule extends MenuModule { return cb(Errors.UnexpectedState(`Failed to get view for MCI ${mciCode}`)); } - const type = this.config.mciMap[mciCode].type; + const type = this.config.mciMap[mciCode].type; switch(type) { case 'userProp' : return this.populateTopXUserProp(listView, mciCode, cb); case 'userEventLog' : return this.populateTopXUserEventLog(listView, mciCode, cb); @@ -148,9 +147,10 @@ exports.getModule = class TopXModule extends MenuModule { } populateTopXUserEventLog(listView, mciCode, cb) { - const count = listView.dimens.height || 1; - const daysBack = this.config.mciMap[mciCode].daysBack; - const shouldSum = _.get(this.config.mciMap[mciCode], 'sum', true); + const mciMap = this.config.mciMap[mciCode]; + const count = listView.dimens.height || 1; + const daysBack = mciMap.daysBack; + const shouldSum = _.get(mciMap, 'sum', true); const valueSql = shouldSum ? 'SUM(CAST(log_value AS INTEGER))' : 'COUNT()'; const dateSql = daysBack ? `AND DATETIME(timestamp) >= DATETIME('now', '-${daysBack} days')` : ''; @@ -162,7 +162,7 @@ exports.getModule = class TopXModule extends MenuModule { GROUP BY user_id ORDER BY value DESC LIMIT ${count};`, - [ this.config.mciMap[mciCode].logName ], + [ mciMap.value ], (err, rows) => { if(err) { return cb(err); @@ -188,7 +188,7 @@ exports.getModule = class TopXModule extends MenuModule { WHERE prop_name = ? ORDER BY value DESC LIMIT ${count};`, - [ this.config.mciMap[mciCode].propName ], + [ this.config.mciMap[mciCode].value ], (err, rows) => { if(err) { return cb(err); diff --git a/docs/modding/top-x.md b/docs/modding/top-x.md index 6ab9c545..50d69bee 100644 --- a/docs/modding/top-x.md +++ b/docs/modding/top-x.md @@ -3,7 +3,7 @@ layout: page title: TopX --- ## The TopX Module -The built in `top_x` module allows for displaying oldschool top user stats for the week, month, etc. Ops can configure what stat(s) are displayed and how far back in days the stats are considered. +The built in `top_x` module allows for displaying oLDSKOOL (?!) top user stats for the week, month, etc. Ops can configure what stat(s) are displayed and how far back in days the stats are considered. ## Configuration ### Config Block @@ -21,14 +21,14 @@ The `mciMap` `config` block configures MCI code mapping to data sources. Current ##### User Event Log (userEventLog) When `type` is set to `userEventLog`, entries from the User Event Log can be counted (ie: individual instances of a particular log item) or summed in the case of log items that have numeric values. The default is to sum. -Some current User Event Log `logName` examples include `ul_files`, `dl_file_bytes`, or `achievement_earned`. See [user_log_name.js](/core/user_log_name.js) for additional information. +Some current User Event Log `value` examples include `ul_files`, `dl_file_bytes`, or `achievement_earned`. See [user_log_name.js](/core/user_log_name.js) for additional information. Example `userEventLog` entry: ```hjson mciMap: { 1: { // e.g.: %VM1 type: userEventLog - logName: achievement_pts_earned // top achievement points earned + value: achievement_pts_earned // top achievement points earned sum: true // this is the default daysBack: 7 // omit daysBack for all-of-time } @@ -36,14 +36,14 @@ mciMap: { ``` #### User Properties (userProp) -When `type` is set to `userProp`, data is collected from individual user's properties. For example a `propName` of `minutes_online_total_count`. See [user_property.js](/core/user_property.js) for more information. +When `type` is set to `userProp`, data is collected from individual user's properties. For example a `value` of `minutes_online_total_count`. See [user_property.js](/core/user_property.js) for more information. Example `userProp` entry: ```hjson mciMap: { 2: { // e.g.: %VM2 type: userProp - propName: minutes_online_total_count // top users by minutes spent on the board + value: minutes_online_total_count // top users by minutes spent on the board } } ``` From 16e903d4c67bee9222161ca3c844ae36cae1e2e7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 20 Jan 2019 21:58:00 -0700 Subject: [PATCH 532/569] Achievements are now recorded in more detail such that they can be retrieved *as they were* at the time of earning --- core/achievement.js | 120 ++++++++++++++++++++++++++++++++++---------- core/database.js | 3 ++ 2 files changed, 96 insertions(+), 27 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index d2ac2508..3c200c9b 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -22,7 +22,10 @@ const { ErrorReasons } = require('./enig_error.js'); const { getThemeArt } = require('./theme.js'); -const { pipeToAnsi } = require('./color_codes.js'); +const { + pipeToAnsi, + stripMciColorCodes +} = require('./color_codes.js'); const stringFormat = require('./string_format.js'); const StatLog = require('./stat_log.js'); const Log = require('./logger.js').log; @@ -127,6 +130,56 @@ class Achievements { this.events = events; } + getAchievementsEarnedByUser(userId, cb) { + if(!this.isEnabled()) { + return cb(Errors.General('Achievements not enabled', ErrorReasons.Disabled)); + } + + UserDb.all( + `SELECT achievement_tag, timestamp, match, title, text, points + FROM user_achievement + WHERE user_id = ? + ORDER BY DATETIME(timestamp);`, + [ userId ], + (err, rows) => { + if(err) { + return cb(err); + } + + const earned = rows.map(row => { + const achievement = Achievement.factory(this.achievementConfig.achievements[row.achievement_tag]); + if(!achievement) { + return; + } + + const earnedInfo = { + achievementTag : row.achievement_tag, + type : achievement.data.type, + retroactive : achievement.data.retroactive, + title : row.title, + text : row.text, + points : row.points, + }; + + switch(earnedInfo.type) { + case [ Achievement.Types.UserStatSet ] : + case [ Achievement.Types.UserStatInc ] : + earnedInfo.statName = achievement.data.statName; + break; + } + + return earnedInfo; + }).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore). + + return cb(null, earned); + } + ); + } + + isEnabled() { + return !_.isUndefined(this.achievementConfig); + } + init(cb) { let achievementConfigPath = _.get(Config(), 'general.achievementFile'); if(!achievementConfigPath) { @@ -188,14 +241,19 @@ class Achievements { ); } - record(info, cb) { + record(info, localInterruptItem, cb) { StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1); StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points); + const recordData = [ + info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField, + stripMciColorCodes(localInterruptItem.title), stripMciColorCodes(localInterruptItem.achievText), info.details.points, + ]; + UserDb.run( - `INSERT OR IGNORE INTO user_achievement (user_id, achievement_tag, timestamp, match) - VALUES (?, ?, ?, ?);`, - [ info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField ], + `INSERT OR IGNORE INTO user_achievement (user_id, achievement_tag, timestamp, match, title, text, points) + VALUES (?, ?, ?, ?, ?, ?, ?);`, + recordData, err => { if(err) { return cb(err); @@ -215,32 +273,31 @@ class Achievements { ); } - display(info, cb) { - this.createAchievementInterruptItems(info, (err, interruptItems) => { - if(err) { - return cb(err); - } + display(info, interruptItems, cb) { + if(interruptItems.local) { + UserInterruptQueue.queue(interruptItems.local, { clients : info.client } ); + } - if(interruptItems.local) { - UserInterruptQueue.queue(interruptItems.local, { clients : info.client } ); - } + if(interruptItems.global) { + UserInterruptQueue.queue(interruptItems.global, { omit : info.client } ); + } - if(interruptItems.global) { - UserInterruptQueue.queue(interruptItems.global, { omit : info.client } ); - } - - return cb(null); - }); + return cb(null); } recordAndDisplayAchievement(info, cb) { - async.series( + async.waterfall( [ (callback) => { - return this.record(info, callback); + return this.createAchievementInterruptItems(info, callback); }, - (callback) => { - return this.display(info, callback); + (interruptItems, callback) => { + this.record(info, interruptItems.local, err => { + return callback(err, interruptItems); + }); + }, + (interruptItems, callback) => { + return this.display(info, interruptItems, callback); } ], err => { @@ -394,7 +451,7 @@ class Achievements { userAffils : info.user.properties[UserProps.Affiliations], nodeId : info.client.node, title : info.details.title, - text : info.global ? info.details.globalText : info.details.text, + //text : info.global ? info.details.globalText : info.details.text, points : info.details.points, achievedValue : info.achievedValue, matchField : info.matchField, @@ -480,8 +537,10 @@ class Achievements { (headerArt, footerArt, callback) => { const itemText = 'global' === itemType ? globalText : text; interruptItems[itemType] = { - text : `${title}\r\n${itemText}`, - pause : true, + title, + achievText : itemText, + text : `${title}\r\n${itemText}`, + pause : true, }; if(headerArt || footerArt) { const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); @@ -518,5 +577,12 @@ let achievements; exports.moduleInitialize = (initInfo, cb) => { achievements = new Achievements(initInfo.events); - return achievements.init(cb); + achievements.init( err => { + if(err) { + return cb(err); + } + + exports.achievements = achievements; + return cb(null); + }); }; diff --git a/core/database.js b/core/database.js index a6af1930..91f56a04 100644 --- a/core/database.js +++ b/core/database.js @@ -195,6 +195,9 @@ const DB_INIT_TABLE = { achievement_tag VARCHAR NOT NULL, timestamp DATETIME NOT NULL, match VARCHAR NOT NULL, + title VARCHAR NOT NULL, + text VARCHAR NOT NULL, + points INTEGER NOT NULL, UNIQUE(user_id, achievement_tag, match), FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE );` From 94609eef3c1b4f490f7569d40bf18518c6b850b0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 22 Jan 2019 20:18:38 -0700 Subject: [PATCH 533/569] Minor updates --- docs/installation/windows.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/windows.md b/docs/installation/windows.md index a68afe97..a9fcf060 100644 --- a/docs/installation/windows.md +++ b/docs/installation/windows.md @@ -1,14 +1,14 @@ --- layout: page -title: Windows Full Install +title: Installation Under Windows --- +## Installation Under Windows -ENiGMA½ will run on both 32bit and 64bit Windows. If you want to run 16bit doors natively then you should use a 32bit Windows. - +ENiGMA½ will run on both 32bit and 64bit Windows. If you want to run 16bit doors natively then you should use a 32bit Windows. ### Basic Instructions -1. Download and Install [Node.JS](https://nodejs.org/en/download/). +1. Download and Install [Node.JS](https://nodejs.org/). 1. Upgrade NPM : At this time node comes with NPM 5.6 preinstalled. To upgrade to a newer version now or in the future on windows follow this method. `*Run PowerShell as Administrator` From b45cccaef711a29651671cd1337b8c8fb7275ff9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 22 Jan 2019 20:19:05 -0700 Subject: [PATCH 534/569] Don't real-time interrupt while you interrupt... yo dawg. --- core/menu_module.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 15e7ebce..52784042 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -203,17 +203,24 @@ exports.MenuModule = class MenuModule extends PluginModule { return cb(null, false); // don't eat up the item; queue for later } + this.realTimeInterrupt = 'blocked'; + // // Default impl: clear screen -> standard display -> reload menu // + const done = (err, removeFromQueue) => { + this.realTimeInterrupt = 'allowed'; + return cb(err, removeFromQueue); + }; + this.client.interruptQueue.displayWithItem( Object.assign({}, interruptItem, { cls : true }), err => { if(err) { - return cb(err, false); + return done(err, false); } this.reload(err => { - return cb(err, err ? false : true); + return done(err, err ? false : true); }); }); } From 4b763cc3692803f284075f8794ae5cf0b51c3c88 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 22 Jan 2019 21:54:12 -0700 Subject: [PATCH 535/569] Spelling --- core/menu_module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/menu_module.js b/core/menu_module.js index 52784042..06c0f6d7 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -329,7 +329,7 @@ exports.MenuModule = class MenuModule extends PluginModule { // A quick rundown: // * We may have mciData.menu, mciData.prompt, or both. // * Prompt form is favored over menu form if both are present. - // * Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve) + // * Standard/predefined MCI entries must load both (e.g. %BN is expected to resolve) // const self = this; From 4f0ade6ce139d30007d7529842748033c9a90757 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 22 Jan 2019 21:54:37 -0700 Subject: [PATCH 536/569] * getAchievementsEarnedByUser() exported as standard method using global inst * Added timestamp info --- core/achievement.js | 105 +++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index 3c200c9b..dc933151 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -37,6 +37,8 @@ const async = require('async'); const moment = require('moment'); const paths = require('path'); +exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser; + class Achievement { constructor(data) { this.data = data; @@ -130,50 +132,8 @@ class Achievements { this.events = events; } - getAchievementsEarnedByUser(userId, cb) { - if(!this.isEnabled()) { - return cb(Errors.General('Achievements not enabled', ErrorReasons.Disabled)); - } - - UserDb.all( - `SELECT achievement_tag, timestamp, match, title, text, points - FROM user_achievement - WHERE user_id = ? - ORDER BY DATETIME(timestamp);`, - [ userId ], - (err, rows) => { - if(err) { - return cb(err); - } - - const earned = rows.map(row => { - const achievement = Achievement.factory(this.achievementConfig.achievements[row.achievement_tag]); - if(!achievement) { - return; - } - - const earnedInfo = { - achievementTag : row.achievement_tag, - type : achievement.data.type, - retroactive : achievement.data.retroactive, - title : row.title, - text : row.text, - points : row.points, - }; - - switch(earnedInfo.type) { - case [ Achievement.Types.UserStatSet ] : - case [ Achievement.Types.UserStatInc ] : - earnedInfo.statName = achievement.data.statName; - break; - } - - return earnedInfo; - }).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore). - - return cb(null, earned); - } - ); + getAchievementByTag(tag) { + return this.achievementConfig.achievements[tag]; } isEnabled() { @@ -341,7 +301,7 @@ class Achievements { return; } - const achievement = Achievement.factory(this.achievementConfig.achievements[achievementTag]); + const achievement = Achievement.factory(this.getAchievementByTag(achievementTag)); if(!achievement) { return; } @@ -573,16 +533,63 @@ class Achievements { } } -let achievements; +let achievementsInstance; + +function getAchievementsEarnedByUser(userId, cb) { + if(!achievementsInstance) { + return cb(Errors.UnexpectedState('Achievements not initialized')); + } + + UserDb.all( + `SELECT achievement_tag, timestamp, match, title, text, points + FROM user_achievement + WHERE user_id = ? + ORDER BY DATETIME(timestamp);`, + [ userId ], + (err, rows) => { + if(err) { + return cb(err); + } + + const earned = rows.map(row => { + + const achievement = Achievement.factory(achievementsInstance.getAchievementByTag(row.achievement_tag)); + if(!achievement) { + return; + } + + const earnedInfo = { + achievementTag : row.achievement_tag, + type : achievement.data.type, + retroactive : achievement.data.retroactive, + title : row.title, + text : row.text, + points : row.points, + timestamp : moment(row.timestamp), + }; + + switch(earnedInfo.type) { + case [ Achievement.Types.UserStatSet ] : + case [ Achievement.Types.UserStatInc ] : + earnedInfo.statName = achievement.data.statName; + break; + } + + return earnedInfo; + }).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore). + + return cb(null, earned); + } + ); +} exports.moduleInitialize = (initInfo, cb) => { - achievements = new Achievements(initInfo.events); - achievements.init( err => { + achievementsInstance = new Achievements(initInfo.events); + achievementsInstance.init( err => { if(err) { return cb(err); } - exports.achievements = achievements; return cb(null); }); }; From ae2a225e3a2c131e82cd5da5f6b8e44d136271bf Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 22 Jan 2019 21:55:28 -0700 Subject: [PATCH 537/569] Module for listing user achievements earned --- core/user_achievements_earned.js | 101 +++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 core/user_achievements_earned.js diff --git a/core/user_achievements_earned.js b/core/user_achievements_earned.js new file mode 100644 index 00000000..b6aee4f8 --- /dev/null +++ b/core/user_achievements_earned.js @@ -0,0 +1,101 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const { MenuModule } = require('./menu_module.js'); +const { Errors } = require('./enig_error.js'); +const { + getAchievementsEarnedByUser +} = require('./achievement.js'); +const UserProps = require('./user_property.js'); + +// deps +const async = require('async'); +const _ = require('lodash'); + +exports.moduleInfo = { + name : 'User Achievements Earned', + desc : 'Lists achievements earned by a user', + author : 'NuSkooler', +}; + +const MciViewIds = { + achievementList : 1, + customRangeStart : 10, // updated @ index update +}; + +exports.getModule = class UserAchievementsEarned extends MenuModule { + constructor(options) { + super(options); + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if(err) { + return cb(err); + } + + async.waterfall( + [ + (callback) => { + this.prepViewController('achievements', 0, mciData.menu, err => { + return callback(err); + }); + }, + (callback) => { + return this.validateMCIByViewIds('achievements', MciViewIds.achievementList, callback); + }, + (callback) => { + return getAchievementsEarnedByUser(this.client.user.userId, callback); + }, + (achievementsEarned, callback) => { + this.achievementsEarned = achievementsEarned; + + const achievementListView = this.viewControllers.achievements.getView(MciViewIds.achievementList); + + achievementListView.on('index update', idx => { + this.selectionIndexUpdate(idx); + }); + + const dateTimeFormat = _.get( + this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateFormat('short')); + + achievementListView.setItems(achievementsEarned.map(achiev => Object.assign( + achiev, + this.getUserInfo(), + { + ts : achiev.timestamp.format(dateTimeFormat), + } + ))); + achievementListView.redraw(); + this.selectionIndexUpdate(0); + + return callback(null); + } + ], + err => { + return cb(err); + } + ); + }); + } + + getUserInfo() { + // :TODO: allow args to pass in a different user - ie from user list -> press A for achievs, so on... + return { + userId : this.client.user.userId, + userName : this.client.user.username, + realName : this.client.user.getProperty(UserProps.RealName), + location : this.client.user.getProperty(UserProps.Location), + affils : this.client.user.getProperty(UserProps.Affiliations), + }; + } + + selectionIndexUpdate(index) { + const achiev = this.achievementsEarned[index]; + if(!achiev) { + return; + } + this.updateCustomViewTextsWithFilter('achievements', MciViewIds.customRangeStart, achiev); + } +}; From aa9cd8899c4bbea5fd3892cb8e6b87702fbe0264 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 24 Jan 2019 21:53:31 -0700 Subject: [PATCH 538/569] New 'userStatIncNewVal' achievement type --- core/achievement.js | 142 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 134 insertions(+), 8 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index dc933151..7b6a4b16 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -52,6 +52,7 @@ class Achievement { switch(data.type) { case Achievement.Types.UserStatSet : case Achievement.Types.UserStatInc : + case Achievement.Types.UserStatIncNewVal : achievement = new UserStatAchievement(data); break; @@ -65,8 +66,9 @@ class Achievement { static get Types() { return { - UserStatSet : 'userStatSet', - UserStatInc : 'userStatInc', + UserStatSet : 'userStatSet', + UserStatInc : 'userStatInc', + UserStatIncNewVal : 'userStatIncNewVal', }; } @@ -74,6 +76,7 @@ class Achievement { switch(this.data.type) { case Achievement.Types.UserStatSet : case Achievement.Types.UserStatInc : + case Achievement.Types.UserStatIncNewVal : if(!_.isString(this.data.statName)) { return false; } @@ -286,14 +289,135 @@ class Achievements { } // :TODO: Make this code generic - find + return factory created object + const achievementTags = Object.keys(_.pickBy( + _.get(this.achievementConfig, 'achievements', {}), + achievement => { + if(false === achievement.enabled) { + return false; + } + const acceptedTypes = [ + Achievement.Types.UserStatSet, + Achievement.Types.UserStatInc, + Achievement.Types.UserStatIncNewVal, + ]; + return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName; + } + )); + + if(0 === achievementTags.length) { + return; + } + + async.eachSeries(achievementTags, (achievementTag, nextAchievementTag) => { + const achievement = Achievement.factory(this.getAchievementByTag(achievementTag)); + if(!achievement) { + return nextAchievementTag(null); + } + + const statValue = parseInt( + [ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ? + userStatEvent.statValue : + userStatEvent.statIncrementBy + ); + if(isNaN(statValue)) { + return nextAchievementTag(null); + } + + const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); + if(!details) { + return nextAchievementTag(null); + } + + async.series( + [ + (callback) => { + this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { + if(err) { + return callback(err); + } + return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null); + }); + }, + (callback) => { + const client = getConnectionByUserId(userStatEvent.user.userId); + if(!client) { + return callback(Errors.UnexpectedState('Failed to get client for user ID')); + } + + const info = { + achievementTag, + achievement, + details, + client, + matchField, // match - may be in odd format + matchValue, // actual value + achievedValue : matchField, // achievement value met + user : userStatEvent.user, + timestamp : moment(), + }; + + const achievementsInfo = [ info ]; + if(true === achievement.data.retroactive) { + // For userStat, any lesser match keys(values) are also met. Example: + // matchKeys: [ 500, 200, 100, 20, 10, 2 ] + // ^---- we met here + // ^------------^ retroactive range + // + const index = achievement.matchKeys.findIndex(v => v < matchField); + if(index > -1 && Array.isArray(achievement.matchKeys)) { + achievement.matchKeys.slice(index).forEach(k => { + const [ det, fld, val ] = achievement.getMatchDetails(k); + if(det) { + achievementsInfo.push(Object.assign( + {}, + info, + { + details : det, + matchField : fld, + achievedValue : fld, + matchValue : val, + } + )); + } + }); + } + } + + // reverse achievementsInfo so we display smallest > largest + achievementsInfo.reverse(); + + async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => { + return this.recordAndDisplayAchievement(achInfo, err => { + return nextAchInfo(err); + }); + }, + err => { + return callback(err); + }); + } + ], + err => { + if(err && ErrorReasons.TooMany !== err.reasonCode) { + Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event'); + } + return nextAchievementTag(null); // always try the next, regardless + } + ); + }); + + /* const achievementTag = _.findKey( _.get(this.achievementConfig, 'achievements', {}), achievement => { if(false === achievement.enabled) { return false; } - return [ Achievement.Types.UserStatSet, Achievement.Types.UserStatInc ].includes(achievement.type) && - achievement.statName === userStatEvent.statName; + const acceptedTypes = [ + Achievement.Types.UserStatSet, + Achievement.Types.UserStatInc, + Achievement.Types.UserStatIncNewVal, + ]; + return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName; } ); @@ -306,9 +430,10 @@ class Achievements { return; } - const statValue = parseInt(Achievement.Types.UserStatSet === achievement.data.type ? - userStatEvent.statValue : - userStatEvent.statIncrementBy + const statValue = parseInt( + [ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ? + userStatEvent.statValue : + userStatEvent.statIncrementBy ); if(isNaN(statValue)) { return; @@ -392,7 +517,7 @@ class Achievements { Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event'); } } - ); + );*/ }); } @@ -571,6 +696,7 @@ function getAchievementsEarnedByUser(userId, cb) { switch(earnedInfo.type) { case [ Achievement.Types.UserStatSet ] : case [ Achievement.Types.UserStatInc ] : + case [ Achievement.Types.UserStatIncNewVal ] : earnedInfo.statName = achievement.data.statName; break; } From eea9e7b5e68931403bde859f261414f92ad75706 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 24 Jan 2019 21:53:45 -0700 Subject: [PATCH 539/569] Don't use Errors --- core/user_achievements_earned.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/user_achievements_earned.js b/core/user_achievements_earned.js index b6aee4f8..ef793023 100644 --- a/core/user_achievements_earned.js +++ b/core/user_achievements_earned.js @@ -3,7 +3,6 @@ // ENiGMA½ const { MenuModule } = require('./menu_module.js'); -const { Errors } = require('./enig_error.js'); const { getAchievementsEarnedByUser } = require('./achievement.js'); From 0efa148f63e52d6ca3a8cfd8c0d7ed0a12b8de44 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 24 Jan 2019 21:54:16 -0700 Subject: [PATCH 540/569] Better incrementUserStat() --- core/stat_log.js | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/core/stat_log.js b/core/stat_log.js index f03319d0..af88ff57 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -147,29 +147,34 @@ class StatLog { incrementUserStat(user, statName, incrementBy, cb) { incrementBy = incrementBy || 1; - let newValue = parseInt(user.properties[statName]); - if(newValue) { - if(!_.isNumber(newValue) && cb) { - return cb(new Error(`Value for ${statName} is not a number!`)); - } - newValue += incrementBy; - } else { - newValue = incrementBy; - } + const oldValue = user.getPropertyAsNumber(statName) || 0; + const newValue = oldValue + incrementBy; - this.setUserStatWithOptions(user, statName, newValue, { noEvent : true }, err => { - if(!err) { - const Events = require('./events.js'); // we need to late load currently - Events.emit( - Events.getSystemEvents().UserStatIncrement, - { user, statName, statIncrementBy: incrementBy, statValue : newValue } - ); - } + this.setUserStatWithOptions( + user, + statName, + newValue, + { noEvent : true }, + err => { + if(!err) { + const Events = require('./events.js'); // we need to late load currently + Events.emit( + Events.getSystemEvents().UserStatIncrement, + { + user, + statName, + oldValue, + statIncrementBy : incrementBy, + statValue : newValue + } + ); + } - if(cb) { - return cb(err); + if(cb) { + return cb(err); + } } - }); + ); } // the time "now" in the ISO format we use and love :) From 69247eadf13153b4f1b812de2190cfdec13d1b5b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 24 Jan 2019 21:54:56 -0700 Subject: [PATCH 541/569] Minor adjust --- art/themes/luciano_blocktronics/STATUS.ANS | Bin 4638 -> 4639 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/STATUS.ANS b/art/themes/luciano_blocktronics/STATUS.ANS index 6871ff457b0b3d44ec533e785fac3b02aa03586e..dc2b0ca88cc1f74e440a91bba90cf1758ad776d4 100644 GIT binary patch delta 47 zcmbQIGGArG2R;@BAej7xPie9!|6}HYlH$p6EMn}A0n*V1xeA*l1RgLlZkzm5P!#}o CLl15M delta 47 zcmbQQGEZg02R;_*XjA9OU-* Date: Thu, 24 Jan 2019 21:55:03 -0700 Subject: [PATCH 542/569] Many updates + user_door_run_total_minutes with new userStatIncNewVal type * Balance & add some new brackets to existing --- config/achievements.hjson | 144 ++++++++++++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 30 deletions(-) diff --git a/config/achievements.hjson b/config/achievements.hjson index d5099f8c..4ad30565 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -67,7 +67,7 @@ title: "Curious Caller" globalText: "{userName} has logged into {boardName} {achievedValue} times!" text: "You've logged into {boardName} {achievedValue} times!" - points: 5 + points: 10 } 25: { title: "Inquisitive" @@ -75,17 +75,29 @@ text: "You've logged into {boardName} {achievedValue} times!" points: 10 } + 75: { + title: "Still Interested!" + globalText: "{userName} has logged into {boardName} {achievedValue} times!" + text: "You've logged into {boardName} {achievedValue} times!" + points: 15 + } 100: { title: "Regular Customer" globalText: "{userName} has logged into {boardName} {achievedValue} times!" text: "You've logged into {boardName} {achievedValue} times!" - points: 10 + points: 25 + } + 250: { + title: "Speed Dial", + globalText: "{userName} has logged into {boardName} {achievedValue} times!" + text: "You've logged into {boardName} {achievedValue} times!" + points: 50 } 500: { title: "System Addict" globalText: "{userName} the BBS {boardName} addict has logged in {achievedValue} times!" text: "You're a {boardName} addict! You've logged in {achievedValue} times!" - points: 25 + points: 50 } } } @@ -94,29 +106,41 @@ type: userStatSet statName: post_count match: { - 5: { + 2: { title: "Poster" globalText: "{userName} has posted {achievedValue} messages!" text: "You've posted {achievedValue} messages!" points: 5 } - 20: { + 5: { title: "Poster... again!", globalText: "{userName} has posted {achievedValue} messages!" text: "You've posted {achievedValue} messages!" + points: 5 + } + 20: { + title: "Just Want to Talk", + globalText: "{userName} has posted {achievedValue} messages!" + text: "You've posted {achievedValue} messages!" points: 10 } 100: { - title: "Frequent Poster", + title: "Probably Just Spam", globalText: "{userName} has posted {achievedValue} messages!" text: "You've posted {achievedValue} messages!" - points: 15 + points: 25 } - 500: { + 250: { title: "Scribe" globalText: "{userName} the scribe has posted {achievedValue} messages!" text: "Such a scribe! You've posted {achievedValue} messages!" - points: 25 + points: 50 + } + 500: { + title: "Writing a Book" + globalText: "{userName} is writing a book and has posted {achievedValue} messages!" + text: "You've posted {achievedValue} messages!" + points: 50 } } } @@ -141,20 +165,20 @@ title: "Contributor" globalText: "{userName} has uploaded {achievedValue} files!" text: "You've uploaded {achievedValue} files!" - points: 20 + points: 25 } 100: { title: "Courier" globalText: "Courier {userName} has uploaded {achievedValue} files!" text: "You've uploaded {achievedValue} files!" - points: 25 + points: 50 } 200: { title: "Must Be a Drop Site" globalText: "{userName} has uploaded a whomping {achievedValue} files!" text: "You've uploaded a whomping {achievedValue} files!" - points: 50 + points: 55 } } } @@ -170,15 +194,21 @@ points: 10 } 1474560: { - title: "America Online 2.5?" - globalText: "{userName} has uploaded 1.44M worth of data. Hopefully it's not AOL 2.5." - title: "You've uploaded 1.44M worth of data. Hopefully it's not AOL 2.5." - points: 15 + title: "AOL Disk Anyone?" + globalText: "{userName} has uploaded 1.44M worth of data. Hopefully it's not AOL!" + title: "You've uploaded 1.44M worth of data. Hopefully it's not AOL!" + points: 10 } 6291456: { title: "A Quake of a Upload" globalText: "{userName} has uploaded 6 x 1.44MB disks worth of data. That's the size of Quake for DOS!" text: "You've uploaded 6 x 1.44MB disks worth of data. That's the size of Quake for DOS!" + points: 20 + } + 104857600: { + title: "Zip 100" + globalText: "{userName} has uploaded a Zip 100 disk's worth of data!" + text: "You've uploaded a Zip 100 disk's worth of data!" points: 25 } 1073741824: { @@ -189,8 +219,8 @@ } 3407872000: { title: "Encarta" - globalText: "{userName} has uploaded 5 x CD-ROM disks worth of data. That's the size of Encarta!" - text: "You've uploaded 5 x CD-ROM disks worth of data. That's the size of Encarta!" + globalText: "{userName} has uploaded 5xCD discs worth of data. That's the size of Encarta!" + text: "You've uploaded 5xCD discs worth of data. That's the size of Encarta!" points: 100 } } @@ -247,19 +277,30 @@ title: "Fits on a Floppy" globalText: "{userName} has downloaded 1.44MB worth of data!" text: "You've downloaded 1.44MB of data!" - points: 10 + points: 5 } 104857600: { title: "Click of Death" globalText: "{userName} has downloaded 100MB... perhaps to a Zip Disk?" text: "You've downloaded 100MB of data... perhaps to a Zip Disk?" - points: 15 + points: 10 } 681574400: { - title: "A CD-ROM Worth" + title: "CD Rip" globalText: "{userName} has downloaded a CD-ROM's worth of data!" text: "You've downloaded a CD-ROM's worth of data!" - points: 20 + points: 15 + } + 1073741824: { + title: "Like One Hundred Floppys, Man" + globalText: "{userName} has downloaded {achievedValue!sizeWithAbbr} of data!" + text: "You've downloaded {achievedValue!sizeWithAbbr} of data!" + points: 25 + } + 5368709120: { + title: "That's a Lot of Bits!" + globalText: "{userName} has downloaded {achievedValue!sizeWithAbbr} of data!" + text: "You've downloaded {achievedValue!sizeWithAbbr} of data!" } } } @@ -284,24 +325,24 @@ title: "Gamer" globalText: "{userName} ran {achievedValue} doors!" text: "You've run {achievedValue} doors!" - points: 15 + points: 20 } 100: { - title: "Textmode is All You Need" + title: "Trying Them All" globalText: "{userName} must really like textmode and has run {achievedValue} doors!" text: "You've run {achievedValue} doors! You must really like textmode!" - points: 25 + points: 50 } 200: { title: "Dropfile Enthusiast" globalText: "{userName} the dropfile enthusiast ran {achievedValue} doors!" text: "You're a dropfile enthusiast! You've run {achievedValue} doors!" - points: 100 + points: 55 } } } - user_door_total_minutes: { + user_individual_door_run_minutes: { type: userStatInc statName: door_run_total_minutes match: { @@ -324,11 +365,54 @@ points: 20 } 60: { - title: "Textmode Dragon Slayer" + title: "What? Limited Turns?!" globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!" text: "You've spent {achievedValue!durationMinutes} in a door!" points: 25 } + 120: { + title: "It's the Only One I Know!" + globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!" + text: "You've spent {achievedValue!durationMinutes} in a door!" + points: 50 + } + 240: { + title: "Possible Addict" + globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!" + text: "You've spent {achievedValue!durationMinutes} in a door!" + points: 55 + } + } + } + + user_door_run_total_minutes: { + type: userStatIncNewVal + statName: door_run_total_minutes + match: { + 10: { + title: "Enough for the Instructions" + globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!" + text: "You've spent {achievedValue!durationMinutes} playing doors!" + points: 10 + } + 30: { + title: "Probably Just L.O.R.D." + globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!" + text: "You've spent {achievedValue!durationMinutes} playing doors!" + points: 20 + } + 60: { + title: "Retro or Bust" + globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!" + text: "You've spent {achievedValue!durationMinutes} playing doors!" + points: 25 + } + 240: { + title: "Textmode Dragon Slayer" + globalText: "{userName} has spent {achievedValue!durationMinutes} playing doors!" + text: "You've spent {achievedValue!durationMinutes} playing doors!" + points: 50 + } } } @@ -358,9 +442,9 @@ title: "Idle Bot" globalText: "{userName} is probably a bot. They've spent {achievedValue!durationMinutes} on {boardName}!" text: "You're a bot, aren't you? You've been on {boardName} for a total of {achievedValue!durationMinutes}!" - points: 50 + points: 55 } } } } -} \ No newline at end of file +} From 289e49f0b986aada60ca212ef64e3b63b2a67012 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 24 Jan 2019 21:56:30 -0700 Subject: [PATCH 543/569] User achievements list --- art/themes/luciano_blocktronics/USERACHIEV.ANS | Bin 0 -> 492 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 art/themes/luciano_blocktronics/USERACHIEV.ANS diff --git a/art/themes/luciano_blocktronics/USERACHIEV.ANS b/art/themes/luciano_blocktronics/USERACHIEV.ANS new file mode 100644 index 0000000000000000000000000000000000000000..f061f04a5ee37b07fa8bd60bc7d131aeb63b642a GIT binary patch literal 492 zcmb_YJx{|h6iimH419R;(wS!`Gy+c{SWu~{Xj8IyMBRAmfW!~rZ#KgJ1aWpkg%Bfe zc(UJ}@9tT8vL)G~Vxgojh-0sKI7qK;iNhd$NjwPY6BdUSv@p`b5WoZh9XK9qTNwU~ zDs!%zhlT51>sH%Nxq7p5cM$;oTMjPB0c8Wnq%wwr%`{DZxKawLV@{+v3?D&Ew(t)s zh;L~~safA@@uQN+7$NqGXWONwv^m(v3EoJ5HE*VE&dz}l-=HTJzRNK0-*&*Uv+FNjzW~Hjbo~GT literal 0 HcmV?d00001 From 3450500d273c246d073b0b439a5c2e175d818acc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 26 Jan 2019 12:04:59 -0700 Subject: [PATCH 544/569] factory() should not crash if data is null --- core/achievement.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/achievement.js b/core/achievement.js index 7b6a4b16..a5de541f 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -48,6 +48,9 @@ class Achievement { } static factory(data) { + if(!data) { + return; + } let achievement; switch(data.type) { case Achievement.Types.UserStatSet : From c98e1474d026ea864b813149703ce1b79431c38a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 26 Jan 2019 12:05:07 -0700 Subject: [PATCH 545/569] Add totalPoints, totalCount --- core/user_achievements_earned.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/user_achievements_earned.js b/core/user_achievements_earned.js index ef793023..4292cb91 100644 --- a/core/user_achievements_earned.js +++ b/core/user_achievements_earned.js @@ -87,6 +87,8 @@ exports.getModule = class UserAchievementsEarned extends MenuModule { realName : this.client.user.getProperty(UserProps.RealName), location : this.client.user.getProperty(UserProps.Location), affils : this.client.user.getProperty(UserProps.Affiliations), + totalCount : this.client.user.getProperty(UserProps.AchievementTotalCount), + totalPoints : this.client.user.getProperty(UserProps.AchievementTotalPoints), }; } From 301aacd9d8ba50b278f47ce72a000ebecc08c981 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 26 Jan 2019 12:55:25 -0700 Subject: [PATCH 546/569] Add mainMenuUserAchievementsEarned --- misc/menu_template.in.hjson | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index 60c57605..d303f452 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -1062,6 +1062,10 @@ value: { command: "BBS"} action: @menu:bbsList } + { + value: { command: "UA" } + action: @menu:mainMenuUserAchievementsEarned + } { value: 1 action: @menu:mainMenu @@ -1069,6 +1073,27 @@ ] } + mainMenuUserAchievementsEarned: { + desc: Achievements + module: user_achievements_earned + art: USERACHIEV + form: { + 0: { + mci: { + VM1: { + focus: true + } + } + actionKeys: [ + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + } + } + nodeMessage: { desc: Node Messaging module: node_msg From 207711f1d144b15789d680ac97c4fca49782c9a5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 26 Jan 2019 12:56:45 -0700 Subject: [PATCH 547/569] Achievements earned --- art/themes/luciano_blocktronics/MMENU.ANS | Bin 3574 -> 3610 bytes art/themes/luciano_blocktronics/theme.hjson | 22 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index ad029e33f765113432e247acfd1b6a04a6e67e4d..bd8a483f56406abf49f17d727730aab0a4a157ff 100644 GIT binary patch delta 56 zcmew+JxgYT7_XY4fwOe9VQy)nf^@WjwXs=lVsb`iYFTP-YF Date: Sat, 26 Jan 2019 12:57:07 -0700 Subject: [PATCH 548/569] totalCount & totalPoints should be numbers --- core/user_achievements_earned.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/user_achievements_earned.js b/core/user_achievements_earned.js index 4292cb91..1004a9d0 100644 --- a/core/user_achievements_earned.js +++ b/core/user_achievements_earned.js @@ -87,8 +87,8 @@ exports.getModule = class UserAchievementsEarned extends MenuModule { realName : this.client.user.getProperty(UserProps.RealName), location : this.client.user.getProperty(UserProps.Location), affils : this.client.user.getProperty(UserProps.Affiliations), - totalCount : this.client.user.getProperty(UserProps.AchievementTotalCount), - totalPoints : this.client.user.getProperty(UserProps.AchievementTotalPoints), + totalCount : this.client.user.getPropertyAsNumber(UserProps.AchievementTotalCount), + totalPoints : this.client.user.getPropertyAsNumber(UserProps.AchievementTotalPoints), }; } From 6193dca58a712fa3ae5cb6fc7c24b1ee87a598f9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 26 Jan 2019 12:57:29 -0700 Subject: [PATCH 549/569] Stats that are numbers should be formatted --- core/predefined_mci.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 2e7ed5ff..a1182a79 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -11,7 +11,9 @@ const { const clientConnections = require('./client_connections.js'); const StatLog = require('./stat_log.js'); const FileBaseFilters = require('./file_base_filter.js'); -const { formatByteSize } = require('./string_util.js'); +const { + formatByteSize, +} = require('./string_util.js'); const ANSI = require('./ansi_term.js'); const UserProps = require('./user_property.js'); const SysProps = require('./system_property.js'); @@ -54,6 +56,15 @@ function userStatAsString(client, statName, defaultValue) { return (StatLog.getUserStat(client.user, statName) || defaultValue).toLocaleString(); } +function toNumberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +function userStatAsCountString(client, statName, defaultValue) { + const value = StatLog.getUserStatNum(client.user, statName) || defaultValue; + return toNumberWithCommas(value); +} + function sysStatAsString(statName, defaultValue) { return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString(); } @@ -97,7 +108,7 @@ const PREDEFINED_MCI_GENERATORS = { return _.get(client, 'currentTheme.info.name', userStatAsString(client, UserProps.ThemeId, '')); }, UD : function themeId(client) { return userStatAsString(client, UserProps.ThemeId, ''); }, - UC : function loginCount(client) { return userStatAsString(client, UserProps.LoginCount, 0); }, + UC : function loginCount(client) { return userStatAsCountString(client, UserProps.LoginCount, 0); }, ND : function connectedNode(client) { return client.node.toString(); }, IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version ST : function serverName(client) { return client.session.serverName; }, @@ -105,12 +116,12 @@ const PREDEFINED_MCI_GENERATORS = { const activeFilter = FileBaseFilters.getActiveFilter(client); return activeFilter ? activeFilter.name : '(Unknown)'; }, - DN : function userNumDownloads(client) { return userStatAsString(client, UserProps.FileDlTotalCount, 0); }, // Obv/2 + DN : function userNumDownloads(client) { return userStatAsCountString(client, UserProps.FileDlTotalCount, 0); }, // Obv/2 DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileDlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, - UP : function userNumUploads(client) { return userStatAsString(client, UserProps.FileUlTotalCount, 0); }, // Obv/2 + UP : function userNumUploads(client) { return userStatAsCountString(client, UserProps.FileUlTotalCount, 0); }, // Obv/2 UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileUlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr @@ -125,7 +136,7 @@ const PREDEFINED_MCI_GENERATORS = { MS : function accountCreated(client) { return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat()); }, - PS : function userPostCount(client) { return userStatAsString(client, UserProps.MessagePostCount, 0); }, + PS : function userPostCount(client) { return userStatAsCountString(client, UserProps.MessagePostCount, 0); }, PC : function userPostCallRatio(client) { return getUserRatio(client, UserProps.MessagePostCount, UserProps.LoginCount); }, MD : function currentMenuDescription(client) { @@ -152,10 +163,10 @@ const PREDEFINED_MCI_GENERATORS = { SH : function termHeight(client) { return client.term.termHeight.toString(); }, SW : function termWidth(client) { return client.term.termWidth.toString(); }, - AC : function achievementCount(client) { return userStatAsString(client, UserProps.AchievementTotalCount, 0); }, - AP : function achievementPoints(client) { return userStatAsString(client, UserProps.AchievementTotalPoints, 0); }, + AC : function achievementCount(client) { return userStatAsCountString(client, UserProps.AchievementTotalCount, 0); }, + AP : function achievementPoints(client) { return userStatAsCountString(client, UserProps.AchievementTotalPoints, 0); }, - DR : function doorRuns(client) { return userStatAsString(client, UserProps.DoorRunTotalCount, 0); }, + DR : function doorRuns(client) { return userStatAsCountString(client, UserProps.DoorRunTotalCount, 0); }, DM : function doorFriendlyRunTime(client) { const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0; return moment.duration(minutes, 'minutes').humanize(); From a4f60dd574f659bc6579d465cdaf7df5197df6cc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 29 Jan 2019 20:36:35 -0700 Subject: [PATCH 550/569] Update packages --- package.json | 8 ++++---- yarn.lock | 57 ++++++++++++++++++++++++++++------------------------ 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index f700ce4d..15add31e 100644 --- a/package.json +++ b/package.json @@ -39,20 +39,20 @@ "lru-cache": "^5.1.1", "mime-types": "^2.1.21", "minimist": "1.2.x", - "moment": "^2.23.0", + "moment": "^2.24.0", "nntp-server": "^1.0.3", - "node-pty": "^0.8.0", + "node-pty": "^0.8.1", "nodemailer": "^5.1.1", "rlogin": "^1.0.0", "sane": "^4.0.2", "sanitize-filename": "^1.6.1", "sqlite3": "^4.0.6", "sqlite3-trans": "^1.2.1", - "ssh2": "^0.7.1", + "ssh2": "^0.8.2", "temptmp": "^1.1.0", "uuid": "^3.2.1", "uuid-parse": "^1.0.0", - "ws": "^6.1.2", + "ws": "^6.1.3", "xxhash": "^0.2.4", "yazl": "^2.5.1" }, diff --git a/yarn.lock b/yarn.lock index 96d5a9a2..8a18bc57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1234,10 +1234,10 @@ moment@^2.10.6: resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= -moment@^2.23.0: - version "2.23.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" - integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA== +moment@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== ms@2.0.0: version "2.0.0" @@ -1263,16 +1263,21 @@ mv@~2: ncp "~2.0.0" rimraf "~2.4.0" -nan@2.10.0, nan@~2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nan@2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" + integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== nan@^2.10.0, nan@^2.4.0: version "2.11.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA== +nan@~2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -1346,12 +1351,12 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-pty@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.0.tgz#08bccb633f49e2e3f7245eb56ea6b40f37ccd64f" - integrity sha512-g5ggk3gN4gLrDmAllee5ScFyX3YzpOC/U8VJafha4pE7do0TIE1voiIxEbHSRUOPD1xYqmY+uHhOKAd3avbxGQ== +node-pty@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.1.tgz#94b457bec013e7a09b8d9141f63b0787fa25c23f" + integrity sha512-j+/g0Q5dR+vkELclpJpz32HcS3O/3EdPSGPvDXJZVJQLCvgG0toEbfmymxAEyQyZEpaoKHAcoL+PvKM+4N9nlw== dependencies: - nan "2.10.0" + nan "2.12.1" nodemailer@^5.1.1: version "5.1.1" @@ -1887,21 +1892,21 @@ sqlite3@^4.0.6: node-pre-gyp "^0.11.0" request "^2.87.0" -ssh2-streams@~0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.3.1.tgz#65d78447628d3fd2bbcfa1a8e73a06755bd62a1a" - integrity sha512-8lVQaN3FNBCoQdnMEsIpD8X1QN/5V8Cyd9TQhAvw4eD/vfuk/o3eikh3rdnf5HUi3TD7SM1bbM/ZTzjNIRmSFw== +ssh2-streams@~0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.2.tgz#bac0d18727396d16049f5f0c8517a46516b45719" + integrity sha512-2rSj3oTIJnbAIzR3+XwIYef9wCOVrPQZNLL+fFPPjnPxf09tKkAbgrlYgh/1qynBTz65AUOS+s1zuko4M/GKCw== dependencies: asn1 "~0.2.0" bcrypt-pbkdf "^1.0.2" streamsearch "~0.1.2" -ssh2@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.7.1.tgz#0dcafd75c4e30606d380d0bd57448e8c0efdd718" - integrity sha512-+ZFoxMXMXq/OyDbE7AwJjsYwk300PD4N1xphYrUttsSzqchWz/3BF7X+La8Jq29Y2QTanojuV/vPFjRsyUGaoA== +ssh2@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.2.tgz#f7a172458d3a7a13d520438264f90de8a3ee72af" + integrity sha512-oaXu7faddvPFGavnLBkk0RFwLXvIzCPq6KqAC3ExlnFPAVIE1uo7pWHe9xmhNHXm+nIe7yg9qsssOm+ip2jijw== dependencies: - ssh2-streams "~0.3.1" + ssh2-streams "~0.4.2" sshpk@^1.7.0: version "1.14.2" @@ -2199,10 +2204,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" - integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw== +ws@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.3.tgz#d2d2e5f0e3c700ef2de89080ebc0ac6e1bf3a72d" + integrity sha512-tbSxiT+qJI223AP4iLfQbkbxkwdFcneYinM2+x46Gx2wgvbaOMO36czfdfVUBRTHvzAMRhDd98sA5d/BuWbQdg== dependencies: async-limiter "~1.0.0" From f15629682c5682f173c3932f43c5a5ad09160cf1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 29 Jan 2019 20:36:45 -0700 Subject: [PATCH 551/569] Fix outstanding SSH bug seen with NetRunner and SyncTERM with ugly hack: Disable keep-alives --- core/servers/login/ssh.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index c94267c4..d13acf4b 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -321,6 +321,13 @@ exports.getModule = class SSHServerModule extends LoginServerModule { algorithms : config.loginServers.ssh.algorithms, }; + // + // This is a terrible hack, and we should not have to do it; + // However, as of this writing, NetRunner and SyncTERM both + // fail to respond to OpenSSH keep-alive pings (keepalive@openssh.com) + // + ssh2.Server.KEEPALIVE_INTERVAL = 0; + this.server = ssh2.Server(serverConf); this.server.on('connection', (conn, info) => { Log.info(info, 'New SSH connection'); From 21b54eda7eceb4747bacffe78bc43f1c5ed70c58 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 29 Jan 2019 21:31:39 -0700 Subject: [PATCH 552/569] Fix interrupt bug when connecting over SSH with multi-node --- core/user_interrupt_queue.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index f1aee626..67b880c8 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -47,13 +47,17 @@ module.exports = class UserInterruptQueue // pause defaulted on interruptItem.pause = _.get(interruptItem, 'pause', true); - this.client.currentMenuModule.attemptInterruptNow(interruptItem, (err, ateIt) => { - if(err) { - // :TODO: Log me - } else if(true !== ateIt) { - this.queue.push(interruptItem); - } - }); + try { + this.client.currentMenuModule.attemptInterruptNow(interruptItem, (err, ateIt) => { + if(err) { + // :TODO: Log me + } else if(true !== ateIt) { + this.queue.push(interruptItem); + } + }); + } catch(e) { + this.queue.push(interruptItem); + } } hasItems() { From 8458d47f0ccc22d41968370f1d12bc529b630c74 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 30 Jan 2019 20:42:30 -0700 Subject: [PATCH 553/569] Fix funkyness with theme getting overridden from prefs when using SSH --- core/login_server_module.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/login_server_module.js b/core/login_server_module.js index 8ba1d978..041f317c 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -19,6 +19,10 @@ module.exports = class LoginServerModule extends ServerModule { // :TODO: we need to max connections -- e.g. from config 'maxConnections' prepareClient(client, cb) { + if(client.user.isAuthenticated()) { + return cb(null); + } + const theme = require('./theme.js'); // @@ -32,7 +36,7 @@ module.exports = class LoginServerModule extends ServerModule { } theme.setClientTheme(client, client.user.properties[UserProps.ThemeId]); - return cb(null); // note: currently useless to use cb here - but this may change...again... + return cb(null); } handleNewClient(client, clientSock, modInfo) { From 4bb4a06e66ff131a8773f4a49585d7999a23e1cb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 30 Jan 2019 21:01:49 -0700 Subject: [PATCH 554/569] Fix duplicate announcements for retroactive achievements --- core/achievement.js | 183 +++++++++++--------------------------------- 1 file changed, 44 insertions(+), 139 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index a5de541f..68dc482f 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -331,7 +331,7 @@ class Achievements { return nextAchievementTag(null); } - async.series( + async.waterfall( [ (callback) => { this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { @@ -360,32 +360,51 @@ class Achievements { }; const achievementsInfo = [ info ]; - if(true === achievement.data.retroactive) { - // For userStat, any lesser match keys(values) are also met. Example: - // matchKeys: [ 500, 200, 100, 20, 10, 2 ] - // ^---- we met here - // ^------------^ retroactive range - // - const index = achievement.matchKeys.findIndex(v => v < matchField); - if(index > -1 && Array.isArray(achievement.matchKeys)) { - achievement.matchKeys.slice(index).forEach(k => { - const [ det, fld, val ] = achievement.getMatchDetails(k); - if(det) { - achievementsInfo.push(Object.assign( - {}, - info, - { - details : det, - matchField : fld, - achievedValue : fld, - matchValue : val, - } - )); - } - }); - } + return callback(null, achievementsInfo, info); + }, + (achievementsInfo, basicInfo, callback) => { + if(true !== achievement.data.retroactive) { + return callback(null, achievementsInfo); } + const index = achievement.matchKeys.findIndex(v => v < matchField); + if(-1 === index || !Array.isArray(achievement.matchKeys)) { + return callback(null, achievementsInfo); + } + + // For userStat, any lesser match keys(values) are also met. Example: + // matchKeys: [ 500, 200, 100, 20, 10, 2 ] + // ^---- we met here + // ^------------^ retroactive range + // + async.eachSeries(achievement.matchKeys.slice(index), (k, nextKey) => { + const [ det, fld, val ] = achievement.getMatchDetails(k); + if(!det) { + return nextKey(null); + } + + this.loadAchievementHitCount(userStatEvent.user, achievementTag, fld, (err, count) => { + if(!err || count && 0 === count) { + achievementsInfo.push(Object.assign( + {}, + basicInfo, + { + details : det, + matchField : fld, + achievedValue : fld, + matchValue : val, + } + )); + } + + return nextKey(null); + }); + }, + () => { + return callback(null, achievementsInfo); + }); + }, + (achievementsInfo, callback) => { // reverse achievementsInfo so we display smallest > largest achievementsInfo.reverse(); @@ -407,120 +426,6 @@ class Achievements { } ); }); - - /* - const achievementTag = _.findKey( - _.get(this.achievementConfig, 'achievements', {}), - achievement => { - if(false === achievement.enabled) { - return false; - } - const acceptedTypes = [ - Achievement.Types.UserStatSet, - Achievement.Types.UserStatInc, - Achievement.Types.UserStatIncNewVal, - ]; - return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName; - } - ); - - if(!achievementTag) { - return; - } - - const achievement = Achievement.factory(this.getAchievementByTag(achievementTag)); - if(!achievement) { - return; - } - - const statValue = parseInt( - [ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ? - userStatEvent.statValue : - userStatEvent.statIncrementBy - ); - if(isNaN(statValue)) { - return; - } - - const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); - if(!details) { - return; - } - - async.series( - [ - (callback) => { - this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { - if(err) { - return callback(err); - } - return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null); - }); - }, - (callback) => { - const client = getConnectionByUserId(userStatEvent.user.userId); - if(!client) { - return callback(Errors.UnexpectedState('Failed to get client for user ID')); - } - - const info = { - achievementTag, - achievement, - details, - client, - matchField, // match - may be in odd format - matchValue, // actual value - achievedValue : matchField, // achievement value met - user : userStatEvent.user, - timestamp : moment(), - }; - - const achievementsInfo = [ info ]; - if(true === achievement.data.retroactive) { - // For userStat, any lesser match keys(values) are also met. Example: - // matchKeys: [ 500, 200, 100, 20, 10, 2 ] - // ^---- we met here - // ^------------^ retroactive range - // - const index = achievement.matchKeys.findIndex(v => v < matchField); - if(index > -1 && Array.isArray(achievement.matchKeys)) { - achievement.matchKeys.slice(index).forEach(k => { - const [ det, fld, val ] = achievement.getMatchDetails(k); - if(det) { - achievementsInfo.push(Object.assign( - {}, - info, - { - details : det, - matchField : fld, - achievedValue : fld, - matchValue : val, - } - )); - } - }); - } - } - - // reverse achievementsInfo so we display smallest > largest - achievementsInfo.reverse(); - - async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => { - return this.recordAndDisplayAchievement(achInfo, err => { - return nextAchInfo(err); - }); - }, - err => { - return callback(err); - }); - } - ], - err => { - if(err && ErrorReasons.TooMany !== err.reasonCode) { - Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event'); - } - } - );*/ }); } From 6aa6712edc4996936422cd4baf2519cacee1223c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 30 Jan 2019 23:36:00 -0700 Subject: [PATCH 555/569] Minor achievement event improvements --- core/achievement.js | 7 ++++++- core/system_events.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/achievement.js b/core/achievement.js index 68dc482f..20e32603 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -211,9 +211,12 @@ class Achievements { StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1); StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points); + const cleanTitle = stripMciColorCodes(localInterruptItem.title); + const cleanText = stripMciColorCodes(localInterruptItem.achievText); + const recordData = [ info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField, - stripMciColorCodes(localInterruptItem.title), stripMciColorCodes(localInterruptItem.achievText), info.details.points, + cleanTitle, cleanText, info.details.points, ]; UserDb.run( @@ -231,6 +234,8 @@ class Achievements { user : info.client.user, achievementTag : info.achievementTag, points : info.details.points, + title : cleanTitle, + text : cleanText, } ); diff --git a/core/system_events.js b/core/system_events.js index 173f753b..129dacfd 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -23,5 +23,5 @@ module.exports = { UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', // { ..., global } UserStatSet : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // { ..., statName, statIncrementBy, statValue } - UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // { ..., achievementTag, points } + UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // { ..., achievementTag, points, title, text } }; From 43c11dc2883bb22fd52eeb5b20e23c69038b9bec Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Feb 2019 10:20:06 -0700 Subject: [PATCH 556/569] A few more upload bytes brackets --- config/achievements.hjson | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/config/achievements.hjson b/config/achievements.hjson index 4ad30565..e1430bcc 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -187,6 +187,12 @@ type: userStatSet statName: ul_total_bytes match: { + 10240: { + text: "UNIVAC Drum" + globalText: "{userName} has uploaded 10k. Enough to fill a UNIVAC drum!" + text: "You've uploaded 10k. Enough to fill a UNIVAC drum!" + points: 5 + } 524288: { title: "Kickstart" globalText: "{userName} has uploaded 512KB, enough for a Kickstart!" @@ -221,8 +227,20 @@ title: "Encarta" globalText: "{userName} has uploaded 5xCD discs worth of data. That's the size of Encarta!" text: "You've uploaded 5xCD discs worth of data. That's the size of Encarta!" + points: 50 + } + 7025459200: { + title: "NFL_Madden_2007_USA_BLURAY_DIRFIX_PS3-PARADOX" + globalText: "{userName} has uploaded 67x100 MiB worth of data, the size of the worlds first PS3 rip!" + text: "You've uploaded 67x100 MiB worth of data, the size of the world first PS3 rip!" points: 100 } + 25018184499: { + title: "WaYsTeD" + globalText: "{userName} has uploaded 23.3 GiB of data, the size of the first PS4 rip: Watch.Dogs.PS4-WaYsTeD!" + text: "You've uploaded 23.3 GiB of data, the size of the first PS4 rip: Watch.Dogs.PS4-WaYsTeD!" + points: 150 + } } } From 8ba80426e33f8c1cb715d8a0a7c55170dbbf6ec4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Feb 2019 10:20:22 -0700 Subject: [PATCH 557/569] Better disconnect - should resolve issues with SSH --- core/client.js | 7 ++++++- core/servers/login/ssh.js | 8 ++++++-- core/servers/login/telnet.js | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/client.js b/core/client.js index 894119bf..d340981b 100644 --- a/core/client.js +++ b/core/client.js @@ -511,7 +511,12 @@ Client.prototype.end = function () { // // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH // - return this.output.end.apply(this.output, arguments); + if(_.isFunction(this.disconnect)) { + return this.disconnect(); + } else { + // legacy fallback + return this.output.end.apply(this.output, arguments); + } } catch(e) { // ie TypeError } diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index d13acf4b..ee63ac78 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -278,13 +278,17 @@ function SSHClient(clientConn) { }); }); - clientConn.on('end', () => { - self.emit('end'); // remove client connection/tracking + clientConn.once('end', () => { + return self.emit('end'); // remove client connection/tracking }); clientConn.on('error', err => { self.log.warn( { error : err.message, code : err.code }, 'SSH connection error'); }); + + this.disconnect = function() { + return clientConn.end(); + }; } util.inherits(SSHClient, baseClient.Client); diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 29d766ba..fb6ca745 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -550,6 +550,15 @@ function TelnetClient(input, output) { this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); } }; + + this.disconnect = function() { + try { + return this.output.end.apply(this.output, arguments); + } + catch(e) { + // nothing + } + }; } util.inherits(TelnetClient, baseClient.Client); From 50c1a60838bf2c74ab144d68255dfce9b96b3a57 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Feb 2019 19:04:36 -0700 Subject: [PATCH 558/569] File scan improvements * Support more versions of RAR signatures & file listings * Better FILE_ID.DIZ, NFO, etc. extraction --- core/config.js | 19 +++++++++++++++---- core/file_base_area.js | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/config.js b/core/config.js index fa99d5da..bd152e99 100644 --- a/core/config.js +++ b/core/config.js @@ -553,7 +553,7 @@ function getDefaultConfig() { }, 'application/x-rar-compressed' : { desc : 'RAR Archive', - sig : '526172211a0700', + sig : '526172211a07', offset : 0, archiveHandler : 'Rar', }, @@ -704,7 +704,7 @@ function getDefaultConfig() { list : { cmd : 'unrar', args : [ 'l', '{archivePath}' ], - entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', + entryMatch : '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2,4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', }, extract : { cmd : 'unrar', @@ -880,12 +880,23 @@ function getDefaultConfig() { // FILE_ID.DIZ - https://en.wikipedia.org/wiki/FILE_ID.DIZ // Some groups include a FILE_ID.ANS. We try to use that over FILE_ID.DIZ if available. desc : [ - '^[^/\]*FILE_ID\.ANS$', '^[^/\]*FILE_ID\.DIZ$', '^[^/\]*DESC\.SDI$', '^[^/\]*DESCRIPT\.ION$', '^[^/\]*FILE\.DES$', '^[^/\]*FILE\.SDI$', '^[^/\]*DISK\.ID$' // eslint-disable-line no-useless-escape + '^.*FILE_ID\.ANS$', '^.*FILE_ID\.DIZ$', // eslint-disable-line no-useless-escape + '^.*DESC\.SDI$', // eslint-disable-line no-useless-escape + '^.*DESCRIPT\.ION$', // eslint-disable-line no-useless-escape + '^.*FILE\.DES$', // eslint-disable-line no-useless-escape + '^.*FILE\.SDI$', // eslint-disable-line no-useless-escape + '^.*DISK\.ID$' // eslint-disable-line no-useless-escape ], // common README filename - https://en.wikipedia.org/wiki/README descLong : [ - '^[^/\]*\.NFO$', '^[^/\]*README\.1ST$', '^[^/\]*README\.NOW$', '^[^/\]*README\.TXT$', '^[^/\]*READ\.ME$', '^[^/\]*README$', '^[^/\]*README\.md$',// eslint-disable-line no-useless-escape + '^[^/\]*\.NFO$', // eslint-disable-line no-useless-escape + '^.*README\.1ST$', // eslint-disable-line no-useless-escape + '^.*README\.NOW$', // eslint-disable-line no-useless-escape + '^.*README\.TXT$', // eslint-disable-line no-useless-escape + '^.*READ\.ME$', // eslint-disable-line no-useless-escape + '^.*README$', // eslint-disable-line no-useless-escape + '^.*README\.md$', // eslint-disable-line no-useless-escape '^RELEASE-INFO.ASC$' // eslint-disable-line no-useless-escape ], }, diff --git a/core/file_base_area.js b/core/file_base_area.js index 44ecb406..ec36c7ec 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -354,8 +354,8 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } const descFiles = { - desc : shortDescFile ? paths.join(tempDir, shortDescFile.fileName) : null, - descLong : longDescFile ? paths.join(tempDir, longDescFile.fileName) : null, + desc : shortDescFile ? paths.join(tempDir, paths.basename(shortDescFile.fileName)) : null, + descLong : longDescFile ? paths.join(tempDir, paths.basename(longDescFile.fileName)) : null, }; return callback(null, descFiles); From db59bc7254bb1ee01fbfac4e2c29909cc6362b62 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 5 Feb 2019 18:42:10 -0700 Subject: [PATCH 559/569] Some doc updates --- docs/configuration/menu-hjson.md | 43 +++++++++++++++++++++++++++++--- docs/modding/existing-mods.md | 2 ++ docs/modding/menu-modules.md | 13 ++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 docs/modding/menu-modules.md diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 4d8dec9b..a59e5b24 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -1,8 +1,8 @@ --- layout: page -title: Menus +title: Menu HSJON --- -## Menus +## Menu HJSON The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/yourboardname-menu.hjson` or similar. This file determines the menus (or screens) a user can see, the order they come in and how they interact with each other, ACS configuration, etc. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information. Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: @@ -24,9 +24,14 @@ Below is a table of **common** menu entry members. These members apply to most e | `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in `prompt.hjson`. | | `submit` | Defines a submit handler when using `prompt`. | `form` | An object defining one or more *forms* available on this menu. | -| `module` | Sets the module name to use for this menu. | +| `module` | Sets the module name to use for this menu. See **Menu Modules** below. | | `config` | An object containing additional configuration. See **Config Block** below. | +### Menu Modules +A given menu entry is backed by a *menu module*. That is, the code behind it. Menus are considered "standard" if the `module` member is not specified (and therefore backed by `core/standard_menu.js`). + +See [Menu Modules](/docs/modding/menu-modules.md) for more information. + ### Config Block The `config` block for a menu entry can contain common members as well as a per-module (when `module` is used) settings. @@ -57,7 +62,37 @@ Menus may also support more than one layout type by using a *MCI key*. A MCI key For more information on views and associated MCI codes, see [MCI Codes](/docs/art/mci.md). ## Submit Handlers -TODO +When a form is submitted, it's data is matched against a *submit handler*. When a match is found, it's *action* is performed. + +### Submit Actions +Submit actions are declared using the `action` member of a submit handler block. Actions can be kick off system/global or local-to-module methods, launch other menus, etc. + +| Action | Description | +|--------|-------------| +| `@menu:menuName` | Takes the user to the *menuName* menu | +| `@systemMethod:methodName` | Executes the system/global method *methodName*. See **System Methods** below. | +| `@method:methodName` | Executes *methodName* local to the calling module. That is, the module set by the `module` member of a menu entry. | +| `@method:/path/to/some_module.js:methodName` | Executes *methodName* exported by the module at */path/to/some_module.js*. | + +#### Method Signature +Methods executed using `@method`, or `@systemMethod` have the following signature: +``` +(callingMenu, formData, extraArgs, callback) +``` + +#### System Methods +Many built in global/system methods exist. Below are a few. See [system_menu_method](/core/system_menu_method.js) for more information. + +| Method | Description | +|--------|-------------| +| `login` | Performs a standard login. | +| `logoff` | Performs a standard system logoff. | +| `prevMenu` | Goes to the previous menu. | +| `nextMenu` | Goes to the next menu (as set by `next`) | +| `prevConf` | Sets the users message conference to the previous available. | +| `nextConf` | Sets the users message conference to the next available. | +| `prevArea` | Sets the users message area to the previous available. | +| `nextArea` | Sets the users message area to the next available. | ## Example Let's look a couple basic menu entries: diff --git a/docs/modding/existing-mods.md b/docs/modding/existing-mods.md index af1b2b74..c64469d4 100644 --- a/docs/modding/existing-mods.md +++ b/docs/modding/existing-mods.md @@ -2,6 +2,8 @@ layout: page title: Existing Mods --- +Many "addon" modules exist and have been released. Below are a few: + | Name | Author | Description | |-----------------------------|-------------|-------------| | Married Bob Fetch Event | NuSkooler | An event for fetching the latest Married Bob ANSI's for display on you board. ACiDic release [ACD-MB4E.ZIP](https://l33t.codes/outgoing/ACD/ACD-MB4E.ZIP). Can also be [found on GitHub](https://github.com/NuSkooler/enigma-bbs-married_bob_evt) | diff --git a/docs/modding/menu-modules.md b/docs/modding/menu-modules.md new file mode 100644 index 00000000..0ab67956 --- /dev/null +++ b/docs/modding/menu-modules.md @@ -0,0 +1,13 @@ +--- +layout: page +title: Local Doors +--- +## Menu Modules +Menu entries found within `menu.hjson` are backed by *menu modules*. + +## Creating a New Module + +### Lifecycle +TODO + + From 032e6d59b8d4e091111b7287292b4e26a23ebe20 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 5 Feb 2019 18:53:38 -0700 Subject: [PATCH 560/569] Doc updates --- docs/installation/installation-methods.md | 6 +++--- docs/messageareas/netmail.md | 9 +++------ docs/modding/menu-modules.md | 1 + 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/installation/installation-methods.md b/docs/installation/installation-methods.md index bb55dcce..5d751ea0 100644 --- a/docs/installation/installation-methods.md +++ b/docs/installation/installation-methods.md @@ -8,9 +8,9 @@ things manually versus have it automated for you. | Method | Operating System Compatibility | Notes | |--------|--------------------------------|-------| -| [Installation Script](install-script) | Linux, BSD, OSX | Quick and easy installation under most Linux/UNIX like environments (Linux, BSD, OS X, ...) | -| [Docker Images](docker) | Linux, BSD, OSX, Windows | Easy upgrades, compatible with all operating systems, no dependencies to install | -| [Manual](manual) | Linux, Windows (probably others but untested! | If you like doing things manually, or are running Windows | +| [Installation Script](install-script.md) | Linux, BSD, OSX | Quick and easy installation under most Linux/UNIX like environments (Linux, BSD, OS X, ...) | +| [Docker Images](docker.md) | Linux, BSD, OSX, Windows | Easy upgrades, compatible with all operating systems, no dependencies to install | +| [Manual](manual.md) | Linux, Windows (probably others but untested! | If you like doing things manually, or are running Windows | ## Keeping Up To Date After installing, you'll want to [keep your system updated](/docs/admin/updating.md). \ No newline at end of file diff --git a/docs/messageareas/netmail.md b/docs/messageareas/netmail.md index 3e947e68..f43c2a4b 100644 --- a/docs/messageareas/netmail.md +++ b/docs/messageareas/netmail.md @@ -2,16 +2,13 @@ layout: page title: Netmail --- -ENiGMA support import and export of Netmail from the Private Mail area. `RiPuk @ 21:1/136` and -`RiPuk <21:1/136>` 'To' address formats are supported. +ENiGMA support import and export of Netmail from the Private Mail area. `RiPuk @ 21:1/136` and `RiPuk <21:1/136>` 'To' address formats are supported. ## Netmail Routing -A configuration block must be added to the `scannerTossers::ftn_bso` `config.hjson` section to tell the -ENiGMA½ tosser where to route netmail. +A configuration block must be added to the `scannerTossers::ftn_bso` `config.hjson` section to tell the ENiGMA½ tosser where to route NetMail. -The following configuration would tell ENiGMA½ to route all netmail addressed to 21:* through 21:1/100, -and all 46:* netmail through 46:1/100: +The following configuration would tell ENiGMA½ to route all netmail addressed to 21:* through 21:1/100, and all 46:* netmail through 46:1/100: ````hjson diff --git a/docs/modding/menu-modules.md b/docs/modding/menu-modules.md index 0ab67956..ea3ea4a7 100644 --- a/docs/modding/menu-modules.md +++ b/docs/modding/menu-modules.md @@ -6,6 +6,7 @@ title: Local Doors Menu entries found within `menu.hjson` are backed by *menu modules*. ## Creating a New Module +TODO ### Lifecycle TODO From 6a7aa5acb809f504025d8e5334662a00bed65568 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 9 Feb 2019 10:56:36 -0700 Subject: [PATCH 561/569] Reset daily stats @ midnight, duh --- core/config.js | 4 ++++ core/misc_scheduled_events.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 core/misc_scheduled_events.js diff --git a/core/config.js b/core/config.js index bd152e99..73179ced 100644 --- a/core/config.js +++ b/core/config.js @@ -953,6 +953,10 @@ function getDefaultConfig() { eventScheduler : { events : { + resetDailyStats : { + schedule : 'at 12:00:01am', // note: Later.js places this right @ midnight + action : '@method:core/misc_scheduled_events.js:resetDailyStatsScheduledEvent', + }, trimMessageAreas : { // may optionally use [or ]@watch:/path/to/file schedule : 'every 24 hours', diff --git a/core/misc_scheduled_events.js b/core/misc_scheduled_events.js new file mode 100644 index 00000000..64a73ea5 --- /dev/null +++ b/core/misc_scheduled_events.js @@ -0,0 +1,18 @@ +/* jslint node: true */ +'use strict'; + +const StatLog = require('./stat_log.js'); +const SysProps = require('./system_property.js'); + +exports.resetDailyStatsScheduledEvent = resetDailyStatsScheduledEvent; + +function resetDailyStatsScheduledEvent(args, cb) { + // + // Various stats need reset daily + // + [ SysProps.LoginsToday, SysProps.MessagesToday ].forEach(prop => { + StatLog.setNonPersistentSystemStat(prop, 0); + }); + + return cb(null); +} From a314b40eb8fa483031a9dc3d9c8dfb59734827fe Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 10 Feb 2019 12:31:25 -0700 Subject: [PATCH 562/569] Update to daily maintenance schedule --- core/ansi_term.js | 3 +++ core/config.js | 6 +++--- core/misc_scheduled_events.js | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/ansi_term.js b/core/ansi_term.js index 353c46c8..20cc1d78 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -17,6 +17,9 @@ // * http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/ansisys.txt // * http://academic.evergreen.edu/projects/biophysics/technotes/program/ansi_esc.htm // +// Modern Windows (Win10+) +// * https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences +// // VTX // * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt // diff --git a/core/config.js b/core/config.js index 73179ced..4a71e634 100644 --- a/core/config.js +++ b/core/config.js @@ -953,9 +953,9 @@ function getDefaultConfig() { eventScheduler : { events : { - resetDailyStats : { - schedule : 'at 12:00:01am', // note: Later.js places this right @ midnight - action : '@method:core/misc_scheduled_events.js:resetDailyStatsScheduledEvent', + dailyMaintenance : { + schedule : 'at 11:59pm', + action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', }, trimMessageAreas : { // may optionally use [or ]@watch:/path/to/file diff --git a/core/misc_scheduled_events.js b/core/misc_scheduled_events.js index 64a73ea5..1650b697 100644 --- a/core/misc_scheduled_events.js +++ b/core/misc_scheduled_events.js @@ -4,9 +4,9 @@ const StatLog = require('./stat_log.js'); const SysProps = require('./system_property.js'); -exports.resetDailyStatsScheduledEvent = resetDailyStatsScheduledEvent; +exports.dailyMaintenanceScheduledEvent = dailyMaintenanceScheduledEvent; -function resetDailyStatsScheduledEvent(args, cb) { +function dailyMaintenanceScheduledEvent(args, cb) { // // Various stats need reset daily // From 49c10ed3de416d445954df8c7eea7e1ea48c985d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 10 Feb 2019 13:39:27 -0700 Subject: [PATCH 563/569] Door run minutes should not be retroactive --- config/achievements.hjson | 1 + 1 file changed, 1 insertion(+) diff --git a/config/achievements.hjson b/config/achievements.hjson index e1430bcc..758af562 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -363,6 +363,7 @@ user_individual_door_run_minutes: { type: userStatInc statName: door_run_total_minutes + retroactive: false match: { 1: { title: "Nevermind!" From f8bbc23951f3b9472b9fe1586fb3a0ba7e4a53ec Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 12:37:17 -0700 Subject: [PATCH 564/569] Add Luciano art for message search --- art/themes/luciano_blocktronics/MSEARCH.ANS | Bin 0 -> 1004 bytes art/themes/luciano_blocktronics/MSRCHLST.ANS | Bin 0 -> 2219 bytes art/themes/luciano_blocktronics/MSRCNORES.ANS | Bin 0 -> 226 bytes art/themes/luciano_blocktronics/theme.hjson | 43 ++++++++++++++++++ core/ansi_term.js | 3 ++ 5 files changed, 46 insertions(+) create mode 100644 art/themes/luciano_blocktronics/MSEARCH.ANS create mode 100644 art/themes/luciano_blocktronics/MSRCHLST.ANS create mode 100644 art/themes/luciano_blocktronics/MSRCNORES.ANS diff --git a/art/themes/luciano_blocktronics/MSEARCH.ANS b/art/themes/luciano_blocktronics/MSEARCH.ANS new file mode 100644 index 0000000000000000000000000000000000000000..0e6a7afd22cc4cab9437d42ee20b26428009f37e GIT binary patch literal 1004 zcmb_bu};G<5N*ZQl?4$AymVo5oZ_^sDwyi$rPojK*CfCNPGaF8H@Z0;@vsU zLfJ^AUhRAL-n+{+C}pycNfet|P)2cPyPyn{MQp4M%7ru**U})RGnYsrX?lVh+uI&! zfVu&s&KLxQrAG*XG=QK0WfCde;|lz&2%6!R`RFJO7kLKHD?Kl=Ox$`8&X3Wsvw^Y1mU?yuuIsZ zHZX*5ZrAzMW|Bb-o9>jDHh9!6%hx; zh+EXAYMW|z#bT&Hq`f+YL}*|VJs%x=-?VHH4oaQ9Z+G$Jt5fFTIP%co#K_{kY}zd? zFj6R?4H@U92M)wv9KIoX&9L&m{vDj^EJJVJ`zoE8 zZ+Gpc5ki7JB;N0DXXg9X$z)Y7X60g9`eL%GeAo6XbfKH9N)LOr#0P_5>jUr<7*rJe zwC^IO%u|Lu%IU^M+gicr=B;@s515rOOdYq=h6lQxJ^cdEwyeUq+MjO`3hXKauk$x7 zMEB&px*c)B^t2v;z;eHXBGr(EG9nivT&1@KK zM8`$P4vSSzg0~BMtkO{gDCPS%b37wVV>FLr1hb2#IC-4paXf;fR{O5Q93xS4glBEB zpE)!PUx?tGda>kqVOuR`H9Lz~`8fM@@hY_P)2XtugBDhoJqAE+W;qgedD5xJAa!sW z&T0S(%TDFtG-#(ba2tIj5@G4wh&ZMUsHGH%OctmBf(;4c(TJf`wnYF^LcOy7I? literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/MSRCNORES.ANS b/art/themes/luciano_blocktronics/MSRCNORES.ANS new file mode 100644 index 0000000000000000000000000000000000000000..81464593221b56740a0e43328c712d3df40868ab GIT binary patch literal 226 zcmb1+Hn27^ur@Z&4.4} |08/ |15{msgNumTotal:<4.4}" + // Fri Sep 25th + dateTimeFormat: ddd MMM Do + } + mci: { + VM1: { + height: 14 + width: 71 + itemFormat: "|00|15 {msgNum:<4.4} |03{subject:<27.26} |07{toUserName:<13.12} {fromUserName:<13.12} |03{ts:<12.12}" + focusItemFormat: "|00|19> |15{msgNum:<4.4} {subject:<27.26} {toUserName:<13.12} {fromUserName:<13.12} {ts:<12.12}" + } + } + } messageAreaViewPost: { 0: { diff --git a/core/ansi_term.js b/core/ansi_term.js index 20cc1d78..cac29681 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -20,6 +20,9 @@ // Modern Windows (Win10+) // * https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences // +// VT100 +// * http://www.noah.org/python/pexpect/ANSI-X3.64.htm +// // VTX // * https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt // From bae61d114ccddbd7a643f1a1e5a7ec8fb899f696 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 13:07:52 -0700 Subject: [PATCH 565/569] Add export option to file menu --- art/themes/luciano_blocktronics/FMENU.ANS | Bin 3635 -> 3677 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/FMENU.ANS b/art/themes/luciano_blocktronics/FMENU.ANS index 55879dd7e6b50547ef71dbaa3c7090cac8a5ddea..1e340430d29969b8878f277e59900836640942c6 100644 GIT binary patch delta 63 zcmdlib5~|VKf8{lvvjm!ZmNQGw1Ks;S#D}YL4Hw*LRw}{szOd?aY<%gda-o0v3V{~ SmFeb*>@{qRpC Date: Fri, 15 Feb 2019 13:16:22 -0700 Subject: [PATCH 566/569] Fix Luciano art for private mail / inbox list --- .../luciano_blocktronics/PRVMSGLIST.ANS | Bin 2249 -> 2229 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/PRVMSGLIST.ANS b/art/themes/luciano_blocktronics/PRVMSGLIST.ANS index e1e0ddf86c304f43a5d013de1c28e9a6b0bbad54..180ace2b077bc7bcc5370277e08f310cff326424 100644 GIT binary patch delta 34 qcmX>pxK(h%dM@c`WAj|;XhU=7jb}}m7>zbFF)w6g^qstfLlpqS2?|dD delta 54 zcmdlgcv5h}db!-zf-V_L|{ Km^gU?hbjQmBM^cB From d94b0af09e1b388b08dbe1c18e4f54c6291b81a5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 13:29:06 -0700 Subject: [PATCH 567/569] Fix missing advanced search button --- art/themes/luciano_blocktronics/FSEARCH.ANS | Bin 967 -> 967 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/art/themes/luciano_blocktronics/FSEARCH.ANS b/art/themes/luciano_blocktronics/FSEARCH.ANS index efb19617fd11db0273af49e0a95a2676a9b42547..e97ec3d6d6f919eb299dafcc0f71ea3a2512bfd7 100644 GIT binary patch delta 27 icmX@kew=*+8#BMTbhM$hiJ7&rd9JEci1}nu=1l-+`3Fe= delta 15 XcmX@kew=*+8}sD*jG~jJnb!aSEZ+q? From 7d82935986312ce88eb37e8d68c5e82998bc0b22 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 13:43:34 -0700 Subject: [PATCH 568/569] Fix up Luciano art for file base list export --- art/themes/luciano_blocktronics/FBLISTEXP.ANS | Bin 0 -> 309 bytes .../luciano_blocktronics/FBLISTEXPSEARCH.ANS | Bin 0 -> 1028 bytes art/themes/luciano_blocktronics/theme.hjson | 45 ++++++++++++++++++ misc/menu_template.in.hjson | 3 +- 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 art/themes/luciano_blocktronics/FBLISTEXP.ANS create mode 100644 art/themes/luciano_blocktronics/FBLISTEXPSEARCH.ANS diff --git a/art/themes/luciano_blocktronics/FBLISTEXP.ANS b/art/themes/luciano_blocktronics/FBLISTEXP.ANS new file mode 100644 index 0000000000000000000000000000000000000000..73a567f046d7f9abb4c673a8d5c7d4ef075e0067 GIT binary patch literal 309 zcmb_WyAHxI3{2OoY;^I$h~%U^1f9|nsz`xWqy?l-sA400BA1!zYLb=D8e@0nSis&`4f>!H7sQ3LE)l1`={UexO}Gm~2TYJ@xdW^xf^*-5-4FRFi8&_+LmA8c!YZ_riVqL96xzO*JGCiJ&p*sewwGut3^s{4f5rZtOoHzS(y1 zK)tynU1s*po42o1BjUcpecKZ~BeK0fmPTayjwhrvB8Ll!G#5B>gZjX?xu^&#WF;%0 z0V*4ib)!H)NLq|B90LdnQ2MroJW}9S9YND%6(5CMs3OgvdAa7rsUogQ^e)kX)FJq; z=&woQWLZ|SL=0eCJyoKxX26agJC;!CNfD9+SN$sBYezh-2^s2!V{`?1uOTpRHA1O0 zB8uU`LEW~N>x%pFBxIBEe8Ixk+2hlKu;kPMEHD|Da)RV@zsm|1v*mkU>;!vDHwoLM zd690{>^-OFKHGhyt7OHNSz07n8j}i3b#w(1B3He&Nq0(8KU|n-AZXXBF#|xlqCZ+# z>Q%Wk9CgG6p=^2^Kos%%4^q1!wHqgNMVuvZn~eF?Q59;ZX&aDp0vch&)pf&S(Fme1 zW?+}ClB}I+&@f>?GLfI`$fMQd@U)%7Z8)%>aOf-j+T;Q4JK82hWD{U+{mGzrI49-x zg}6}OlcZ(5T_&5}<$U-Ygq-7E|FtY`dfejf*IB32?an$E9ktHKqbYlN7}nA8`2$`V BEgAp- literal 0 HcmV?d00001 diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index ee555c1a..4a35eb15 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -851,6 +851,51 @@ } } + fileBaseExportListFilter: { + mci: { + ET1: { + width: 42 + } + BT2: { + focusTextStyle: first lower + } + ET3: { + width: 42 + } + SM4: { + width: 14 + justify: left + } + SM5: { + width: 14 + justify: left + } + SM6: { + width: 14 + justify: left + } + BT7: { + focusTextStyle: first lower + } + } + } + + fileBaseExportList: { + config: { + progBarChar: "|15▒" + mainInfoFormat10: "|07{currentFile} |08/ |07{totalFileCount} |08(|07{progress} %|08)" + } + mci: { + TL1: { + width: 60 + } + TL2: { + width: 56 + fillChar: "|06░" + } + } + } + fileAreaFilterEditor: { mci: { ET1: { diff --git a/misc/menu_template.in.hjson b/misc/menu_template.in.hjson index d303f452..bdcf97cd 100644 --- a/misc/menu_template.in.hjson +++ b/misc/menu_template.in.hjson @@ -2798,8 +2798,7 @@ fileBaseExportListFilter: { module: file_base_search - // :TODO: fixme: - art: FSEARCH + art: FBLISTEXPSEARCH config: { fileBaseListEntriesMenu: fileBaseExportList } From 04c8167a153f24deb53e41780de1f54698c600b6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 15 Feb 2019 13:55:11 -0700 Subject: [PATCH 569/569] Minor doc updates --- UPGRADE.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 359abec1..038c5c0a 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,16 +1,17 @@ # Introduction This document covers basic upgrade notes for major ENiGMA½ version updates. - # Before Upgrading -* Always back up your system! +* Always back up your system! +* Seriously, always back up your system! * At least back up the `db` directory and your `menu.hjson` (or renamed equivalent) - # General Notes -Upgrades often come with changes to the default `menu.hjson`. It is wise to -use a *different* file name for your BBS's version of this file and point to -it via `config.hjson`. For example: +## Configuration File Updates +In general, look at the `menu_template.in.hjson`, and `config_template.in.hjson` as well as the defualt `luciano_blocktronics/theme.hjson` files when you update. These files may come with new sections you wish to merge into your system! + +### menu.hjson +Upgrades often come with changes to the default `menu_template.in.hjson`. It is wise to use a *different* file name for your BBS's version of this file and point to it via `config.hjson`. For example: ```hjson general: { @@ -21,6 +22,9 @@ general: { After updating code, use a program such as DiffMerge to merge in updates to `my_bbs.hjson` from the shipping `menu.hjson`. +### theme.hjson +Any custom themes you have created may now be missing features as well. Take a look at the default `luciano_blocktronics/theme.hjson` file. You can use missing sections in your `theme.hjson` (which will generally correspond to sections you've also merged in to your `menu.hjson`). + # Upgrading the Code Upgrading from GitHub is easy: @@ -32,7 +36,6 @@ rm -rf npm_modules # do this any time you update Node.js itself npm install ``` - # Problems Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues).