From e9787cee3e2a6ef8bc0b1446c4db349dc9ea2610 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 21 Jun 2018 23:15:04 -0600 Subject: [PATCH] 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; }