From eecfb33ad555b5bf451a5b9e89d62fe32b4ccda8 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Jun 2022 13:53:57 -0600 Subject: [PATCH 1/5] Add Prettier --- .prettierrc.json | 19 +++++++++++++++++++ package.json | 3 ++- yarn.lock | 5 +++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..470c44c3 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,19 @@ +{ + "arrowParens": "avoid", + "bracketSameLine": false, + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 90, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "es5", + "useTabs": false, + "vueIndentScriptAndStyle": false +} diff --git a/package.json b/package.json index a1617dea..fca97f9e 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "yazl": "^2.5.1" }, "devDependencies": { - "eslint": "^8.13.0" + "eslint": "^8.13.0", + "prettier": "2.6.2" }, "engines": { "node": ">=14" diff --git a/yarn.lock b/yarn.lock index 4b21ebf5..eab9528a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1482,6 +1482,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" + integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== + process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" From 4881c2123a1f01e108cad51c166aec6621bc81d1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Jun 2022 14:04:25 -0600 Subject: [PATCH 2/5] First pass formatting with Prettier * Added .prettierrc.json * Added .prettierignore * Formatted --- .eslintrc.json | 25 +- .prettierignore | 12 + .prettierrc.json | 34 +- core/abracadabra.js | 145 +- core/achievement.js | 724 +++--- core/acs.js | 57 +- core/acs_parser.js | 2117 +++++++++--------- core/ansi_escape_parser.js | 335 +-- core/ansi_prep.js | 137 +- core/ansi_term.js | 328 +-- core/archaicnet.js | 138 +- core/archive_util.js | 212 +- core/art.js | 187 +- core/asset.js | 63 +- core/autosig_edit.js | 54 +- core/bbs.js | 188 +- core/bbs_link.js | 190 +- core/bbs_list.js | 310 +-- core/button_view.js | 22 +- core/client.js | 488 +++-- core/client_connections.js | 115 +- core/client_term.js | 108 +- core/color_codes.js | 289 +-- core/combatnet.js | 89 +- core/conf_area_util.js | 10 +- core/config.js | 10 +- core/config_cache.js | 69 +- core/config_default.js | 1159 +++++----- core/config_loader.js | 124 +- core/connect.js | 100 +- core/cp437util.js | 300 ++- core/crc.js | 48 +- core/database.js | 127 +- core/descript_ion_file.js | 87 +- core/door.js | 96 +- core/door_party.js | 143 +- core/door_util.js | 28 +- core/download_queue.js | 80 +- core/dropfile.js | 284 +-- core/edit_text_view.js | 66 +- core/email.js | 18 +- core/enig_error.js | 78 +- core/enigma_assert.js | 14 +- core/event_scheduler.js | 241 +- core/events.js | 30 +- core/exodus.js | 183 +- core/file_area_filter_edit.js | 215 +- core/file_area_list.js | 636 +++--- core/file_area_web.js | 370 ++-- core/file_base_area.js | 854 +++++--- core/file_base_area_select.js | 77 +- core/file_base_download_manager.js | 172 +- core/file_base_filter.js | 79 +- core/file_base_list_export.js | 482 ++-- core/file_base_search.js | 93 +- core/file_base_user_list_export.js | 212 +- core/file_base_web_download_manager.js | 224 +- core/file_entry.js | 393 ++-- core/file_transfer.js | 490 +++-- core/file_transfer_protocol_select.js | 109 +- core/file_util.js | 49 +- core/files_bbs_file.js | 181 +- core/fnv1a.js | 20 +- core/fse.js | 2364 +++++++++++--------- core/ftn_address.js | 65 +- core/ftn_mail_packet.js | 759 ++++--- core/ftn_util.js | 222 +- core/full_menu_view.js | 870 ++++---- core/horizontal_menu_view.js | 106 +- core/key_entry_view.js | 52 +- core/last_callers.js | 234 +- core/listening_server.js | 82 +- core/logger.js | 51 +- core/login_server_module.js | 36 +- core/mail_packet.js | 12 +- core/mail_util.js | 73 +- core/mask_edit_text_view.js | 150 +- core/mbf.js | 14 +- core/mci_view_factory.js | 190 +- core/menu_module.js | 398 ++-- core/menu_stack.js | 141 +- core/menu_util.js | 238 +- core/menu_view.js | 238 +- core/message.js | 790 ++++--- core/message_area.js | 453 ++-- core/message_base_qwk_export.js | 334 +-- core/message_base_search.js | 107 +- core/mime_util.js | 26 +- core/misc_scheduled_events.js | 6 +- core/misc_util.js | 44 +- core/mod_mixins.js | 58 +- core/module_util.js | 207 +- core/mrc.js | 313 +-- core/msg_area_list.js | 114 +- core/msg_area_post_fse.js | 38 +- core/msg_area_reply_fse.js | 10 +- core/msg_area_view_fse.js | 110 +- core/msg_conf_list.js | 137 +- core/msg_list.js | 403 ++-- core/msg_network.js | 58 +- core/msg_scan_toss_module.js | 11 +- core/multi_line_edit_text_view.js | 699 +++--- core/my_messages.js | 46 +- core/new_scan.js | 229 +- core/node_msg.js | 218 +- core/nua.js | 126 +- core/onelinerz.js | 189 +- core/oputil/oputil_common.js | 109 +- core/oputil/oputil_config.js | 202 +- core/oputil/oputil_file_base.js | 1081 +++++---- core/oputil/oputil_help.js | 28 +- core/oputil/oputil_main.js | 44 +- core/oputil/oputil_message_base.js | 466 ++-- core/oputil/oputil_user.js | 387 ++-- core/plugin_module.js | 5 +- core/predefined_mci.js | 405 ++-- core/qwk_mail_packet.js | 937 ++++---- core/rumorz.js | 181 +- core/sauce.js | 181 +- core/scanner_tossers/ftn_bso.js | 2799 ++++++++++++++---------- core/servers/chat/mrc_multiplexer.js | 260 ++- core/servers/content/gopher.js | 327 +-- core/servers/content/nntp.js | 792 ++++--- core/servers/content/web.js | 214 +- core/servers/login/ssh.js | 278 ++- core/servers/login/telnet.js | 111 +- core/servers/login/websocket.js | 189 +- core/set_newscan_date.js | 248 ++- core/show_art.js | 145 +- core/spinner_menu_view.js | 87 +- core/standard_menu.js | 10 +- core/stat_log.js | 175 +- core/string_format.js | 294 +-- core/string_util.js | 329 +-- core/sys_event_user_log.js | 55 +- core/system_events.js | 36 +- core/system_log.js | 5 +- core/system_menu_method.js | 234 +- core/system_property.js | 32 +- core/system_view_validate.js | 88 +- core/telnet_bridge.js | 137 +- core/text_view.js | 139 +- core/theme.js | 396 ++-- core/tic_file_info.js | 164 +- core/toggle_menu_view.js | 67 +- core/top_x.js | 192 +- core/upload.js | 720 +++--- core/user.js | 522 +++-- core/user_2fa_otp.js | 128 +- core/user_2fa_otp_config.js | 213 +- core/user_2fa_otp_web_register.js | 203 +- core/user_achievements_earned.js | 93 +- core/user_config.js | 255 ++- core/user_group.js | 60 +- core/user_interrupt_queue.js | 82 +- core/user_list.js | 82 +- core/user_log_name.js | 28 +- core/user_login.js | 182 +- core/user_property.js | 92 +- core/user_temp_token.js | 77 +- core/uuid_util.js | 23 +- core/vertical_menu_view.js | 240 +- core/view.js | 258 ++- core/view_controller.js | 603 ++--- core/web_password_reset.js | 246 ++- core/whos_online.js | 56 +- core/word_wrap.js | 79 +- main.js | 2 +- package.json | 136 +- util/dump_ftn_packet.js | 80 +- util/exiftool2desc.js | 77 +- util/to_ansi.js | 20 +- 172 files changed, 23696 insertions(+), 18029 deletions(-) create mode 100644 .prettierignore diff --git a/.eslintrc.json b/.eslintrc.json index fbe0b672..6eebcc94 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,33 +3,22 @@ "es6": true, "node": true }, - "extends": [ - "eslint:recommended" - ], + "extends": ["eslint:recommended"], "rules": { "indent": [ "error", 4, { - "SwitchCase" : 1 + "SwitchCase": 1 } ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single"], + "semi": ["error", "always"], "comma-dangle": 0, - "no-trailing-spaces" :"warn" + "no-trailing-spaces": "warn" }, "parserOptions": { "ecmaVersion": 2020 } -} \ No newline at end of file +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..d284cb67 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +art +config +db +docs +drop +gopher +logs +misc +www +mkdocs.yml +*.md +.github diff --git a/.prettierrc.json b/.prettierrc.json index 470c44c3..71dc71ec 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,19 +1,19 @@ { - "arrowParens": "avoid", - "bracketSameLine": false, - "bracketSpacing": true, - "embeddedLanguageFormatting": "auto", - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "jsxSingleQuote": false, - "printWidth": 90, - "proseWrap": "preserve", - "quoteProps": "as-needed", - "requirePragma": false, - "semi": true, - "singleQuote": true, - "tabWidth": 4, - "trailingComma": "es5", - "useTabs": false, - "vueIndentScriptAndStyle": false + "arrowParens": "avoid", + "bracketSameLine": false, + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 90, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "es5", + "useTabs": false, + "vueIndentScriptAndStyle": false } diff --git a/core/abracadabra.js b/core/abracadabra.js index a8a72b1d..a1e12e99 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -1,31 +1,28 @@ /* jslint node: true */ 'use strict'; -const { MenuModule } = require('./menu_module.js'); -const DropFile = require('./dropfile.js'); -const Door = require('./door.js'); -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const { Errors } = require('./enig_error.js'); -const { - trackDoorRunBegin, - trackDoorRunEnd -} = require('./door_util.js'); -const Log = require('./logger').log; +const { MenuModule } = require('./menu_module.js'); +const DropFile = require('./dropfile.js'); +const Door = require('./door.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const { Errors } = require('./enig_error.js'); +const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js'); +const Log = require('./logger').log; // deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const paths = require('path'); -const fs = require('graceful-fs'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const paths = require('path'); +const fs = require('graceful-fs'); const activeDoorNodeInstances = {}; exports.moduleInfo = { - name : 'Abracadabra', - desc : 'External BBS Door Module', - author : 'NuSkooler', + name: 'Abracadabra', + desc: 'External BBS Door Module', + author: 'NuSkooler', }; /* @@ -71,15 +68,15 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.config = options.menuConfig.config; // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } // .. and/or EnigAssert - assert(_.isString(this.config.name, 'Config \'name\' is required')); - assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); + assert(_.isString(this.config.name, "Config 'name' 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 || []; } incrementActiveDoorNodeInstances() { - if(activeDoorNodeInstances[this.config.name]) { + if (activeDoorNodeInstances[this.config.name]) { activeDoorNodeInstances[this.config.name] += 1; } else { activeDoorNodeInstances[this.config.name] = 1; @@ -88,7 +85,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { } decrementActiveDoorNodeInstances() { - if(true === this.activeDoorInstancesIncremented) { + if (true === this.activeDoorInstancesIncremented) { activeDoorNodeInstances[this.config.name] -= 1; this.activeDoorInstancesIncremented = false; } @@ -100,29 +97,43 @@ exports.getModule = class AbracadabraModule extends MenuModule { async.series( [ function validateNodeCount(callback) { - if(self.config.nodeMax > 0 && + if ( + self.config.nodeMax > 0 && _.isNumber(activeDoorNodeInstances[self.config.name]) && - activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) - { + activeDoorNodeInstances[self.config.name] + 1 > + self.config.nodeMax + ) { self.client.log.info( { - name : self.config.name, - activeCount : activeDoorNodeInstances[self.config.name] + name: self.config.name, + activeCount: activeDoorNodeInstances[self.config.name], }, - 'Too many active instances'); + 'Too many active instances' + ); - if(_.isString(self.config.tooManyArt)) { - theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { - self.pausePrompt( () => { - return callback(Errors.AccessDenied('Too many active instances')); - }); - }); + if (_.isString(self.config.tooManyArt)) { + theme.displayThemeArt( + { client: self.client, name: self.config.tooManyArt }, + function displayed() { + self.pausePrompt(() => { + return callback( + Errors.AccessDenied( + 'Too many active instances' + ) + ); + }); + } + ); } else { - self.client.term.write('\nToo many active instances. Try again later.\n'); + self.client.term.write( + '\nToo many active instances. Try again later.\n' + ); // :TODO: Use MenuModule.pausePrompt() - self.pausePrompt( () => { - return callback(Errors.AccessDenied('Too many active instances')); + self.pausePrompt(() => { + return callback( + Errors.AccessDenied('Too many active instances') + ); }); } } else { @@ -135,21 +146,26 @@ exports.getModule = class AbracadabraModule extends MenuModule { return self.doorInstance.prepare(self.config.io || 'stdio', callback); }, function generateDropfile(callback) { - if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') { + if ( + !self.config.dropFileType || + self.config.dropFileType.toLowerCase() === 'none' + ) { return callback(null); } - self.dropFile = new DropFile( - self.client, - { fileType : self.config.dropFileType } - ); + self.dropFile = new DropFile(self.client, { + fileType: self.config.dropFileType, + }); return self.dropFile.createFile(callback); - } + }, ], function complete(err) { - if(err) { - self.client.log.warn( { error : err.toString() }, 'Could not start door'); + if (err) { + self.client.log.warn( + { error: err.toString() }, + 'Could not start door' + ); self.lastError = err; self.prevMenu(); } else { @@ -163,18 +179,18 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.client.term.write(ansi.resetScreen()); const exeInfo = { - cmd : this.config.cmd, - cwd : this.config.cwd || paths.dirname(this.config.cmd), - args : this.config.args, - io : this.config.io || 'stdio', - encoding : this.config.encoding || 'cp437', - node : this.client.node, - env : this.config.env, + cmd: this.config.cmd, + cwd: this.config.cwd || paths.dirname(this.config.cmd), + args: this.config.args, + io: this.config.io || 'stdio', + encoding: this.config.encoding || 'cp437', + node: this.client.node, + env: this.config.env, }; if (this.dropFile) { - exeInfo.dropFile = this.dropFile.fileName; - exeInfo.dropFilePath = this.dropFile.fullPath; + exeInfo.dropFile = this.dropFile.fileName; + exeInfo.dropFilePath = this.dropFile.fullPath; } const doorTracking = trackDoorRunBegin(this.client, this.config.name); @@ -187,14 +203,17 @@ exports.getModule = class AbracadabraModule extends MenuModule { if (exeInfo.dropFilePath) { fs.unlink(exeInfo.dropFilePath, err => { if (err) { - Log.warn({ error : err, path : exeInfo.dropFilePath }, 'Failed to remove drop file.'); + Log.warn( + { error: err, path: exeInfo.dropFilePath }, + 'Failed to remove drop file.' + ); } }); } // client may have disconnected while process was active - // we're done here if so. - if(!this.client.term.output) { + if (!this.client.term.output) { return; } @@ -204,10 +223,10 @@ exports.getModule = class AbracadabraModule extends MenuModule { // this.client.term.rawWrite( ansi.normal() + - ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + - ansi.setScrollRegion() + - ansi.goto(this.client.term.termHeight, 0) + - '\r\n\r\n' + ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + + ansi.setScrollRegion() + + ansi.goto(this.client.term.termHeight, 0) + + '\r\n\r\n' ); this.autoNextMenu(); diff --git a/core/achievement.js b/core/achievement.js index 3c58d4d3..d73372ca 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -2,38 +2,28 @@ 'use strict'; // ENiGMA½ -const Events = require('./events.js'); -const Config = require('./config.js').get; -const ConfigLoader = require('./config_loader'); -const { getConfigPath } = require('./config_util'); -const UserDb = require('./database.js').dbs.user; -const { - getISOTimestampString -} = require('./database.js'); -const UserInterruptQueue = require('./user_interrupt_queue.js'); -const { - getConnectionByUserId -} = require('./client_connections.js'); -const UserProps = require('./user_property.js'); -const { - Errors, - ErrorReasons -} = require('./enig_error.js'); -const { getThemeArt } = require('./theme.js'); -const { - pipeToAnsi, - stripMciColorCodes -} = require('./color_codes.js'); -const stringFormat = require('./string_format.js'); -const StatLog = require('./stat_log.js'); -const Log = require('./logger.js').log; +const Events = require('./events.js'); +const Config = require('./config.js').get; +const ConfigLoader = require('./config_loader'); +const { getConfigPath } = require('./config_util'); +const UserDb = require('./database.js').dbs.user; +const { getISOTimestampString } = require('./database.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); +const { getConnectionByUserId } = require('./client_connections.js'); +const UserProps = require('./user_property.js'); +const { Errors, ErrorReasons } = require('./enig_error.js'); +const { getThemeArt } = require('./theme.js'); +const { pipeToAnsi, stripMciColorCodes } = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); +const StatLog = require('./stat_log.js'); +const Log = require('./logger.js').log; // deps -const _ = require('lodash'); -const async = require('async'); -const moment = require('moment'); +const _ = require('lodash'); +const async = require('async'); +const moment = require('moment'); -exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser; +exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser; class Achievement { constructor(data) { @@ -44,59 +34,65 @@ class Achievement { } static factory(data) { - if(!data) { + if (!data) { return; } let achievement; - switch(data.type) { - case Achievement.Types.UserStatSet : - case Achievement.Types.UserStatInc : - case Achievement.Types.UserStatIncNewVal : + switch (data.type) { + case Achievement.Types.UserStatSet: + case Achievement.Types.UserStatInc: + case Achievement.Types.UserStatIncNewVal: achievement = new UserStatAchievement(data); break; - default : return; + default: + return; } - if(achievement.isValid()) { + if (achievement.isValid()) { return achievement; } } static get Types() { return { - UserStatSet : 'userStatSet', - UserStatInc : 'userStatInc', - UserStatIncNewVal : 'userStatIncNewVal', + UserStatSet: 'userStatSet', + UserStatInc: 'userStatInc', + UserStatIncNewVal: 'userStatIncNewVal', }; } isValid() { - switch(this.data.type) { - case Achievement.Types.UserStatSet : - case Achievement.Types.UserStatInc : - case Achievement.Types.UserStatIncNewVal : - if(!_.isString(this.data.statName)) { + switch (this.data.type) { + case Achievement.Types.UserStatSet: + case Achievement.Types.UserStatInc: + case Achievement.Types.UserStatIncNewVal: + if (!_.isString(this.data.statName)) { return false; } - if(!_.isObject(this.data.match)) { + if (!_.isObject(this.data.match)) { return false; } break; - default : return false; + default: + return false; } return true; } - getMatchDetails(/*matchAgainst*/) { - } + getMatchDetails(/*matchAgainst*/) {} isValidMatchDetails(details) { - if(!details || !_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) { + if ( + !details || + !_.isString(details.title) || + !_.isString(details.text) || + !_.isNumber(details.points) + ) { return false; } - return (_.isString(details.globalText) || !details.globalText); + return _.isString(details.globalText) || !details.globalText; } } @@ -105,11 +101,13 @@ class UserStatAchievement extends Achievement { super(data); // sort match keys for quick match lookup - this.matchKeys = Object.keys(this.data.match || {}).map(k => parseInt(k)).sort( (a, b) => b - a); + this.matchKeys = Object.keys(this.data.match || {}) + .map(k => parseInt(k)) + .sort((a, b) => b - a); } isValid() { - if(!super.isValid()) { + if (!super.isValid()) { return false; } return !Object.keys(this.data.match).some(k => !parseInt(k)); @@ -118,11 +116,11 @@ class UserStatAchievement extends Achievement { getMatchDetails(matchValue) { let ret = []; let matchField = this.matchKeys.find(v => matchValue >= v); - if(matchField) { + if (matchField) { const match = this.data.match[matchField]; matchField = parseInt(matchField); - if(this.isValidMatchDetails(match) && !isNaN(matchField)) { - ret = [ match, matchField, matchValue ]; + if (this.isValidMatchDetails(match) && !isNaN(matchField)) { + ret = [match, matchField, matchValue]; } } return ret; @@ -151,7 +149,7 @@ class Achievements { } const configLoaded = () => { - if(true !== this.config.get().enabled) { + if (true !== this.config.get().enabled) { Log.info('Achievements are not enabled'); this.enabled = false; this.stopMonitoringUserStatEvents(); @@ -163,11 +161,11 @@ class Achievements { }; this.config = new ConfigLoader({ - onReload : err => { + onReload: err => { if (!err) { configLoaded(); } - } + }, }); this.config.init(configPath, err => { @@ -182,10 +180,10 @@ class Achievements { _getConfigPath() { const path = _.get(Config(), 'general.achievementFile'); - if(!path) { + if (!path) { return; } - return getConfigPath(path); // qualify + return getConfigPath(path); // qualify } loadAchievementHitCount(user, achievementTag, field, cb) { @@ -193,7 +191,7 @@ class Achievements { `SELECT COUNT() AS count FROM user_achievement WHERE user_id = ? AND achievement_tag = ? AND match = ?;`, - [ user.userId, achievementTag, field], + [user.userId, achievementTag, field], (err, row) => { return cb(err, row ? row.count : 0); } @@ -202,14 +200,23 @@ class Achievements { record(info, localInterruptItem, cb) { StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1); - StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points); + StatLog.incrementUserStat( + info.client.user, + UserProps.AchievementTotalPoints, + info.details.points + ); - const cleanTitle = stripMciColorCodes(localInterruptItem.title); - const cleanText = stripMciColorCodes(localInterruptItem.achievText); + const cleanTitle = stripMciColorCodes(localInterruptItem.title); + const cleanText = stripMciColorCodes(localInterruptItem.achievText); const recordData = [ - info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField, - cleanTitle, cleanText, info.details.points, + info.client.user.userId, + info.achievementTag, + getISOTimestampString(info.timestamp), + info.matchField, + cleanTitle, + cleanText, + info.details.points, ]; UserDb.run( @@ -217,20 +224,17 @@ class Achievements { VALUES (?, ?, ?, ?, ?, ?, ?);`, recordData, err => { - if(err) { + if (err) { return cb(err); } - this.events.emit( - Events.getSystemEvents().UserAchievementEarned, - { - user : info.client.user, - achievementTag : info.achievementTag, - points : info.details.points, - title : cleanTitle, - text : cleanText, - } - ); + this.events.emit(Events.getSystemEvents().UserAchievementEarned, { + user: info.client.user, + achievementTag: info.achievementTag, + points: info.details.points, + title: cleanTitle, + text: cleanText, + }); return cb(null); } @@ -238,12 +242,12 @@ class Achievements { } display(info, interruptItems, cb) { - if(interruptItems.local) { - UserInterruptQueue.queue(interruptItems.local, { clients : info.client } ); + if (interruptItems.local) { + UserInterruptQueue.queue(interruptItems.local, { clients: info.client }); } - if(interruptItems.global) { - UserInterruptQueue.queue(interruptItems.global, { omit : info.client } ); + if (interruptItems.global) { + UserInterruptQueue.queue(interruptItems.global, { omit: info.client }); } return cb(null); @@ -252,7 +256,7 @@ class Achievements { recordAndDisplayAchievement(info, cb) { async.waterfall( [ - (callback) => { + callback => { return this.createAchievementInterruptItems(info, callback); }, (interruptItems, callback) => { @@ -262,7 +266,7 @@ class Achievements { }, (interruptItems, callback) => { return this.display(info, interruptItems, callback); - } + }, ], err => { return cb(err); @@ -271,164 +275,228 @@ class Achievements { } monitorUserStatEvents() { - if(this.userStatEventListeners) { + if (this.userStatEventListeners) { return; // already listening } const listenEvents = [ Events.getSystemEvents().UserStatSet, - Events.getSystemEvents().UserStatIncrement + Events.getSystemEvents().UserStatIncrement, ]; - this.userStatEventListeners = this.events.addMultipleEventListener(listenEvents, userStatEvent => { - if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) { - return; - } - - if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) { - return; - } - - // :TODO: Make this code generic - find + return factory created object - const achievementTags = Object.keys(_.pickBy( - _.get(this.config.get(), 'achievements', {}), - achievement => { - if(false === achievement.enabled) { - return false; - } - const acceptedTypes = [ - Achievement.Types.UserStatSet, - Achievement.Types.UserStatInc, - Achievement.Types.UserStatIncNewVal, - ]; - return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName; - } - )); - - if(0 === achievementTags.length) { - return; - } - - async.eachSeries(achievementTags, (achievementTag, nextAchievementTag) => { - const achievement = Achievement.factory(this.getAchievementByTag(achievementTag)); - if(!achievement) { - return nextAchievementTag(null); - } - - const statValue = parseInt( - [ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ? - userStatEvent.statValue : - userStatEvent.statIncrementBy - ); - if(isNaN(statValue)) { - return nextAchievementTag(null); - } - - const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); - if(!details) { - return nextAchievementTag(null); - } - - async.waterfall( + this.userStatEventListeners = this.events.addMultipleEventListener( + listenEvents, + userStatEvent => { + if ( [ - (callback) => { - this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { - if(err) { - return callback(err); - } - return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null); - }); - }, - (callback) => { - const client = getConnectionByUserId(userStatEvent.user.userId); - if(!client) { - return callback(Errors.UnexpectedState('Failed to get client for user ID')); + UserProps.AchievementTotalCount, + UserProps.AchievementTotalPoints, + ].includes(userStatEvent.statName) + ) { + return; + } + + if ( + !_.isNumber(userStatEvent.statValue) && + !_.isNumber(userStatEvent.statIncrementBy) + ) { + return; + } + + // :TODO: Make this code generic - find + return factory created object + const achievementTags = Object.keys( + _.pickBy( + _.get(this.config.get(), 'achievements', {}), + achievement => { + if (false === achievement.enabled) { + return false; } + const acceptedTypes = [ + Achievement.Types.UserStatSet, + Achievement.Types.UserStatInc, + Achievement.Types.UserStatIncNewVal, + ]; + return ( + acceptedTypes.includes(achievement.type) && + achievement.statName === userStatEvent.statName + ); + } + ) + ); - const info = { - achievementTag, - achievement, - details, - client, - matchField, // match - may be in odd format - matchValue, // actual value - achievedValue : matchField, // achievement value met - user : userStatEvent.user, - timestamp : moment(), - }; + if (0 === achievementTags.length) { + return; + } - const achievementsInfo = [ info ]; - return callback(null, achievementsInfo, info); - }, - (achievementsInfo, basicInfo, callback) => { - if(true !== achievement.data.retroactive) { - return callback(null, achievementsInfo); - } + async.eachSeries( + achievementTags, + (achievementTag, nextAchievementTag) => { + const achievement = Achievement.factory( + this.getAchievementByTag(achievementTag) + ); + if (!achievement) { + return nextAchievementTag(null); + } - const index = achievement.matchKeys.findIndex(v => v < matchField); - if(-1 === index || !Array.isArray(achievement.matchKeys)) { - return callback(null, achievementsInfo); - } + const statValue = parseInt( + [ + Achievement.Types.UserStatSet, + Achievement.Types.UserStatIncNewVal, + ].includes(achievement.data.type) + ? userStatEvent.statValue + : userStatEvent.statIncrementBy + ); + if (isNaN(statValue)) { + return nextAchievementTag(null); + } - // For userStat, any lesser match keys(values) are also met. Example: - // matchKeys: [ 500, 200, 100, 20, 10, 2 ] - // ^---- we met here - // ^------------^ retroactive range - // - async.eachSeries(achievement.matchKeys.slice(index), (k, nextKey) => { - const [ det, fld, val ] = achievement.getMatchDetails(k); - if(!det) { - return nextKey(null); - } + const [details, matchField, matchValue] = + achievement.getMatchDetails(statValue); + if (!details) { + return nextAchievementTag(null); + } - this.loadAchievementHitCount(userStatEvent.user, achievementTag, fld, (err, count) => { - if(!err || count && 0 === count) { - achievementsInfo.push(Object.assign( - {}, - basicInfo, - { - details : det, - matchField : fld, - achievedValue : fld, - matchValue : val, + async.waterfall( + [ + callback => { + this.loadAchievementHitCount( + userStatEvent.user, + achievementTag, + matchField, + (err, count) => { + if (err) { + return callback(err); } - )); + return callback( + count > 0 + ? Errors.General( + 'Achievement already acquired', + ErrorReasons.TooMany + ) + : null + ); + } + ); + }, + callback => { + const client = getConnectionByUserId( + userStatEvent.user.userId + ); + if (!client) { + return callback( + Errors.UnexpectedState( + 'Failed to get client for user ID' + ) + ); } - return nextKey(null); - }); - }, - () => { - return callback(null, achievementsInfo); - }); - }, - (achievementsInfo, callback) => { - // reverse achievementsInfo so we display smallest > largest - achievementsInfo.reverse(); + const info = { + achievementTag, + achievement, + details, + client, + matchField, // match - may be in odd format + matchValue, // actual value + achievedValue: matchField, // achievement value met + user: userStatEvent.user, + timestamp: moment(), + }; - async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => { - return this.recordAndDisplayAchievement(achInfo, err => { - return nextAchInfo(err); - }); - }, + const achievementsInfo = [info]; + return callback(null, achievementsInfo, info); + }, + (achievementsInfo, basicInfo, callback) => { + if (true !== achievement.data.retroactive) { + return callback(null, achievementsInfo); + } + + const index = achievement.matchKeys.findIndex( + v => v < matchField + ); + if ( + -1 === index || + !Array.isArray(achievement.matchKeys) + ) { + return callback(null, achievementsInfo); + } + + // For userStat, any lesser match keys(values) are also met. Example: + // matchKeys: [ 500, 200, 100, 20, 10, 2 ] + // ^---- we met here + // ^------------^ retroactive range + // + async.eachSeries( + achievement.matchKeys.slice(index), + (k, nextKey) => { + const [det, fld, val] = + achievement.getMatchDetails(k); + if (!det) { + return nextKey(null); + } + + this.loadAchievementHitCount( + userStatEvent.user, + achievementTag, + fld, + (err, count) => { + if (!err || (count && 0 === count)) { + achievementsInfo.push( + Object.assign({}, basicInfo, { + details: det, + matchField: fld, + achievedValue: fld, + matchValue: val, + }) + ); + } + + return nextKey(null); + } + ); + }, + () => { + return callback(null, achievementsInfo); + } + ); + }, + (achievementsInfo, callback) => { + // reverse achievementsInfo so we display smallest > largest + achievementsInfo.reverse(); + + async.eachSeries( + achievementsInfo, + (achInfo, nextAchInfo) => { + return this.recordAndDisplayAchievement( + achInfo, + err => { + return nextAchInfo(err); + } + ); + }, + err => { + return callback(err); + } + ); + }, + ], err => { - return callback(err); - }); - } - ], - err => { - if(err && ErrorReasons.TooMany !== err.reasonCode) { - Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event'); - } - return nextAchievementTag(null); // always try the next, regardless + if (err && ErrorReasons.TooMany !== err.reasonCode) { + Log.warn( + { error: err.message, userStatEvent }, + 'Error handling achievement for user stat event' + ); + } + return nextAchievementTag(null); // always try the next, regardless + } + ); } ); - }); - }); + } + ); } stopMonitoringUserStatEvents() { - if(this.userStatEventListeners) { + if (this.userStatEventListeners) { this.events.removeMultipleEventListener(this.userStatEventListeners); delete this.userStatEventListeners; } @@ -436,34 +504,38 @@ class Achievements { getFormatObject(info) { return { - userName : info.user.username, - userRealName : info.user.properties[UserProps.RealName], - userLocation : info.user.properties[UserProps.Location], - userAffils : info.user.properties[UserProps.Affiliations], - nodeId : info.client.node, - title : info.details.title, + userName: info.user.username, + userRealName: info.user.properties[UserProps.RealName], + userLocation: info.user.properties[UserProps.Location], + userAffils: info.user.properties[UserProps.Affiliations], + nodeId: info.client.node, + title: info.details.title, //text : info.global ? info.details.globalText : info.details.text, - points : info.details.points, - achievedValue : info.achievedValue, - matchField : info.matchField, - matchValue : info.matchValue, - timestamp : moment(info.timestamp).format(info.dateTimeFormat), - boardName : Config().general.boardName, + points: info.details.points, + achievedValue: info.achievedValue, + matchField: info.matchField, + matchValue: info.matchValue, + timestamp: moment(info.timestamp).format(info.dateTimeFormat), + boardName: Config().general.boardName, }; } getFormattedTextFor(info, textType, defaultSgr = '|07') { - const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); - const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr; + const themeDefaults = _.get( + info.client.currentTheme, + 'achievements.defaults', + {} + ); + const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr; const formatObj = this.getFormatObject(info); - const wrap = (input) => { + const wrap = input => { const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g'); return input.replace(re, (m, formatVar, formatOpts) => { const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr; let r = `${varSgr}{${formatVar}`; - if(formatOpts) { + if (formatOpts) { r += formatOpts; } return `${r}}${textTypeSgr}`; @@ -480,10 +552,10 @@ class Achievements { info.client.currentTheme.helpers.getDateTimeFormat(); const title = this.getFormattedTextFor(info, 'title'); - const text = this.getFormattedTextFor(info, 'text'); + const text = this.getFormattedTextFor(info, 'text'); let globalText; - if(info.details.globalText) { + if (info.details.globalText) { globalText = this.getFormattedTextFor(info, 'globalText'); } @@ -492,13 +564,13 @@ class Achievements { _.get(info.details, `art.${name}`) || _.get(info.achievement, `art.${name}`) || _.get(this.config.get(), `art.${name}`); - if(!spec) { + if (!spec) { return callback(null); } const getArtOpts = { - name : spec, - client : this.client, - random : false, + name: spec, + client: this.client, + random: false, }; getThemeArt(getArtOpts, (err, artInfo) => { // ignore errors @@ -507,67 +579,86 @@ class Achievements { }; const interruptItems = {}; - let itemTypes = [ 'local' ]; - if(globalText) { + let itemTypes = ['local']; + if (globalText) { itemTypes.push('global'); } - async.each(itemTypes, (itemType, nextItemType) => { - async.waterfall( - [ - (callback) => { - getArt(`${itemType}Header`, headerArt => { - return callback(null, headerArt); - }); - }, - (headerArt, callback) => { - getArt(`${itemType}Footer`, footerArt => { - return callback(null, headerArt, footerArt); - }); - }, - (headerArt, footerArt, callback) => { - const itemText = 'global' === itemType ? globalText : text; - interruptItems[itemType] = { - title, - achievText : itemText, - text : `${title}\r\n${itemText}`, - pause : true, - }; - if(headerArt || footerArt) { - const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); - const defaultContentsFormat = '{title}\r\n{message}'; - const contentsFormat = 'global' === itemType ? - themeDefaults.globalFormat || defaultContentsFormat : - themeDefaults.format || defaultContentsFormat; - - const formatObj = Object.assign(this.getFormatObject(info), { - title : this.getFormattedTextFor(info, 'title', ''), // ''=defaultSgr - message : itemText, + async.each( + itemTypes, + (itemType, nextItemType) => { + async.waterfall( + [ + callback => { + getArt(`${itemType}Header`, headerArt => { + return callback(null, headerArt); }); + }, + (headerArt, callback) => { + getArt(`${itemType}Footer`, footerArt => { + return callback(null, headerArt, footerArt); + }); + }, + (headerArt, footerArt, callback) => { + const itemText = 'global' === itemType ? globalText : text; + interruptItems[itemType] = { + title, + achievText: itemText, + text: `${title}\r\n${itemText}`, + pause: true, + }; + if (headerArt || footerArt) { + const themeDefaults = _.get( + info.client.currentTheme, + 'achievements.defaults', + {} + ); + const defaultContentsFormat = '{title}\r\n{message}'; + const contentsFormat = + 'global' === itemType + ? themeDefaults.globalFormat || + defaultContentsFormat + : themeDefaults.format || defaultContentsFormat; - const contents = pipeToAnsi(stringFormat(contentsFormat, formatObj)); + const formatObj = Object.assign( + this.getFormatObject(info), + { + title: this.getFormattedTextFor( + info, + 'title', + '' + ), // ''=defaultSgr + message: itemText, + } + ); - interruptItems[itemType].contents = - `${headerArt || ''}\r\n${contents}\r\n${footerArt || ''}`; - } - return callback(null); + const contents = pipeToAnsi( + stringFormat(contentsFormat, formatObj) + ); + + interruptItems[itemType].contents = `${ + headerArt || '' + }\r\n${contents}\r\n${footerArt || ''}`; + } + return callback(null); + }, + ], + err => { + return nextItemType(err); } - ], - err => { - return nextItemType(err); - } - ); - }, - err => { - return cb(err, interruptItems); - }); + ); + }, + err => { + return cb(err, interruptItems); + } + ); } } let achievementsInstance; function getAchievementsEarnedByUser(userId, cb) { - if(!achievementsInstance) { + if (!achievementsInstance) { return cb(Errors.UnexpectedState('Achievements not initialized')); } @@ -576,39 +667,42 @@ function getAchievementsEarnedByUser(userId, cb) { FROM user_achievement WHERE user_id = ? ORDER BY DATETIME(timestamp);`, - [ userId ], + [userId], (err, rows) => { - if(err) { + if (err) { return cb(err); } - const earned = rows.map(row => { + const earned = rows + .map(row => { + const achievement = Achievement.factory( + achievementsInstance.getAchievementByTag(row.achievement_tag) + ); + if (!achievement) { + return; + } - const achievement = Achievement.factory(achievementsInstance.getAchievementByTag(row.achievement_tag)); - if(!achievement) { - return; - } + const earnedInfo = { + achievementTag: row.achievement_tag, + type: achievement.data.type, + retroactive: achievement.data.retroactive, + title: row.title, + text: row.text, + points: row.points, + timestamp: moment(row.timestamp), + }; - const earnedInfo = { - achievementTag : row.achievement_tag, - type : achievement.data.type, - retroactive : achievement.data.retroactive, - title : row.title, - text : row.text, - points : row.points, - timestamp : moment(row.timestamp), - }; + switch (earnedInfo.type) { + case [Achievement.Types.UserStatSet]: + case [Achievement.Types.UserStatInc]: + case [Achievement.Types.UserStatIncNewVal]: + earnedInfo.statName = achievement.data.statName; + break; + } - switch(earnedInfo.type) { - case [ Achievement.Types.UserStatSet ] : - case [ Achievement.Types.UserStatInc ] : - case [ Achievement.Types.UserStatIncNewVal ] : - earnedInfo.statName = achievement.data.statName; - break; - } - - return earnedInfo; - }).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore). + return earnedInfo; + }) + .filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore). return cb(null, earned); } @@ -617,8 +711,8 @@ function getAchievementsEarnedByUser(userId, cb) { exports.moduleInitialize = (initInfo, cb) => { achievementsInstance = new Achievements(initInfo.events); - achievementsInstance.init( err => { - if(err) { + achievementsInstance.init(err => { + if (err) { return cb(err); } diff --git a/core/acs.js b/core/acs.js index f1596d5d..835b80d3 100644 --- a/core/acs.js +++ b/core/acs.js @@ -2,12 +2,12 @@ 'use strict'; // ENiGMA½ -const checkAcs = require('./acs_parser.js').parse; -const Log = require('./logger.js').log; +const checkAcs = require('./acs_parser.js').parse; +const Log = require('./logger.js').log; // deps -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); class ACS { constructor(subject) { @@ -16,15 +16,15 @@ class ACS { static get Defaults() { return { - MessageConfRead : 'GM[users]', // list/read - MessageConfWrite : 'GM[users]', // post/write + MessageConfRead: 'GM[users]', // list/read + MessageConfWrite: 'GM[users]', // post/write - MessageAreaRead : 'GM[users]', // list/read; requires parent conf read - MessageAreaWrite : 'GM[users]', // post/write; requires parent conf write + MessageAreaRead: 'GM[users]', // list/read; requires parent conf read + MessageAreaWrite: 'GM[users]', // post/write; requires parent conf write - FileAreaRead : 'GM[users]', // list - FileAreaWrite : 'GM[sysops]', // upload - FileAreaDownload : 'GM[users]', // download + FileAreaRead: 'GM[users]', // list + FileAreaWrite: 'GM[sysops]', // upload + FileAreaDownload: 'GM[users]', // download }; } @@ -32,9 +32,9 @@ class ACS { acs = acs ? acs[scope] : defaultAcs; acs = acs || defaultAcs; try { - return checkAcs(acs, { subject : this.subject } ); - } catch(e) { - Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); + return checkAcs(acs, { subject: this.subject }); + } catch (e) { + Log.warn({ exception: e, acs: acs }, 'Exception caught checking ACS'); return false; } } @@ -76,39 +76,42 @@ class ACS { hasMenuModuleAccess(modInst) { const acs = _.get(modInst, 'menuConfig.config.acs'); - if(!_.isString(acs)) { - return true; // no ACS check req. + if (!_.isString(acs)) { + return true; // no ACS check req. } try { - return checkAcs(acs, { subject : this.subject } ); - } catch(e) { - Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); + return checkAcs(acs, { subject: this.subject }); + } catch (e) { + Log.warn({ exception: e, acs: acs }, 'Exception caught checking ACS'); return false; } } getConditionalValue(condArray, memberName) { - if(!Array.isArray(condArray)) { + if (!Array.isArray(condArray)) { // no cond array, just use the value return condArray; } assert(_.isString(memberName)); - const matchCond = condArray.find( cond => { - if(_.has(cond, 'acs')) { + const matchCond = condArray.find(cond => { + if (_.has(cond, 'acs')) { try { - return checkAcs(cond.acs, { subject : this.subject } ); - } catch(e) { - Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS'); + return checkAcs(cond.acs, { subject: this.subject }); + } catch (e) { + Log.warn( + { exception: e, acs: cond }, + 'Exception caught checking ACS' + ); return false; } } else { - return true; // no ACS check req. + return true; // no ACS check req. } }); - if(matchCond) { + if (matchCond) { return matchCond[memberName]; } } diff --git a/core/acs_parser.js b/core/acs_parser.js index 9d645a2f..55e2e820 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -4,1096 +4,1211 @@ * http://pegjs.org/ */ -"use strict"; +'use strict'; function peg$subclass(child, parent) { - function ctor() { this.constructor = child; } - ctor.prototype = parent.prototype; - child.prototype = new ctor(); + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); } function peg$SyntaxError(message, expected, found, location) { - this.message = message; - this.expected = expected; - this.found = found; - this.location = location; - this.name = "SyntaxError"; + this.message = message; + this.expected = expected; + this.found = found; + this.location = location; + this.name = 'SyntaxError'; - if (typeof Error.captureStackTrace === "function") { - Error.captureStackTrace(this, peg$SyntaxError); - } + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, peg$SyntaxError); + } } peg$subclass(peg$SyntaxError, Error); -peg$SyntaxError.buildMessage = function(expected, found) { - var DESCRIBE_EXPECTATION_FNS = { - literal: function(expectation) { - return "\"" + literalEscape(expectation.text) + "\""; +peg$SyntaxError.buildMessage = function (expected, found) { + var DESCRIBE_EXPECTATION_FNS = { + literal: function (expectation) { + return '"' + literalEscape(expectation.text) + '"'; }, - "class": function(expectation) { - var escapedParts = "", - i; + class: function (expectation) { + var escapedParts = '', + i; - for (i = 0; i < expectation.parts.length; i++) { - escapedParts += expectation.parts[i] instanceof Array - ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1]) - : classEscape(expectation.parts[i]); - } + for (i = 0; i < expectation.parts.length; i++) { + escapedParts += + expectation.parts[i] instanceof Array + ? classEscape(expectation.parts[i][0]) + + '-' + + classEscape(expectation.parts[i][1]) + : classEscape(expectation.parts[i]); + } - return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]"; + return '[' + (expectation.inverted ? '^' : '') + escapedParts + ']'; }, - any: function(expectation) { - return "any character"; + any: function (expectation) { + return 'any character'; }, - end: function(expectation) { - return "end of input"; + end: function (expectation) { + return 'end of input'; }, - other: function(expectation) { - return expectation.description; + other: function (expectation) { + return expectation.description; + }, + }; + + function hex(ch) { + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + + function literalEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function (ch) { + return '\\x0' + hex(ch); + }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { + return '\\x' + hex(ch); + }); + } + + function classEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/\]/g, '\\]') + .replace(/\^/g, '\\^') + .replace(/-/g, '\\-') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function (ch) { + return '\\x0' + hex(ch); + }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { + return '\\x' + hex(ch); + }); + } + + function describeExpectation(expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + + function describeExpected(expected) { + var descriptions = new Array(expected.length), + i, + j; + + for (i = 0; i < expected.length; i++) { + descriptions[i] = describeExpectation(expected[i]); } - }; - function hex(ch) { - return ch.charCodeAt(0).toString(16).toUpperCase(); - } + descriptions.sort(); - function literalEscape(s) { - return s - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\0/g, '\\0') - .replace(/\t/g, '\\t') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) - .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); - } - - function classEscape(s) { - return s - .replace(/\\/g, '\\\\') - .replace(/\]/g, '\\]') - .replace(/\^/g, '\\^') - .replace(/-/g, '\\-') - .replace(/\0/g, '\\0') - .replace(/\t/g, '\\t') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) - .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); - } - - function describeExpectation(expectation) { - return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); - } - - function describeExpected(expected) { - var descriptions = new Array(expected.length), - i, j; - - for (i = 0; i < expected.length; i++) { - descriptions[i] = describeExpectation(expected[i]); - } - - descriptions.sort(); - - if (descriptions.length > 0) { - for (i = 1, j = 1; i < descriptions.length; i++) { - if (descriptions[i - 1] !== descriptions[i]) { - descriptions[j] = descriptions[i]; - j++; + if (descriptions.length > 0) { + for (i = 1, j = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i]; + j++; + } + } + descriptions.length = j; + } + + switch (descriptions.length) { + case 1: + return descriptions[0]; + + case 2: + return descriptions[0] + ' or ' + descriptions[1]; + + default: + return ( + descriptions.slice(0, -1).join(', ') + + ', or ' + + descriptions[descriptions.length - 1] + ); } - } - descriptions.length = j; } - switch (descriptions.length) { - case 1: - return descriptions[0]; - - case 2: - return descriptions[0] + " or " + descriptions[1]; - - default: - return descriptions.slice(0, -1).join(", ") - + ", or " - + descriptions[descriptions.length - 1]; + function describeFound(found) { + return found ? '"' + literalEscape(found) + '"' : 'end of input'; } - } - function describeFound(found) { - return found ? "\"" + literalEscape(found) + "\"" : "end of input"; - } - - return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; + return ( + 'Expected ' + + describeExpected(expected) + + ' but ' + + describeFound(found) + + ' found.' + ); }; function peg$parse(input, options) { - options = options !== void 0 ? options : {}; + options = options !== void 0 ? options : {}; - var peg$FAILED = {}, + var peg$FAILED = {}, + peg$startRuleFunctions = { start: peg$parsestart }, + peg$startRuleFunction = peg$parsestart, + peg$c0 = '|', + peg$c1 = peg$literalExpectation('|', false), + peg$c2 = '&', + peg$c3 = peg$literalExpectation('&', false), + peg$c4 = '!', + peg$c5 = peg$literalExpectation('!', false), + peg$c6 = '(', + peg$c7 = peg$literalExpectation('(', false), + peg$c8 = ')', + peg$c9 = peg$literalExpectation(')', false), + peg$c10 = function (left, right) { + return left || right; + }, + peg$c11 = function (left, right) { + return left && right; + }, + peg$c12 = function (value) { + return !value; + }, + peg$c13 = function (value) { + return value; + }, + peg$c14 = ',', + peg$c15 = peg$literalExpectation(',', false), + peg$c16 = ' ', + peg$c17 = peg$literalExpectation(' ', false), + peg$c18 = '[', + peg$c19 = peg$literalExpectation('[', false), + peg$c20 = ']', + peg$c21 = peg$literalExpectation(']', false), + peg$c22 = function (acs, a) { + return checkAccess(acs, a); + }, + peg$c23 = /^[A-Z]/, + peg$c24 = peg$classExpectation([['A', 'Z']], false, false), + peg$c25 = function (c) { + return c.join(''); + }, + peg$c26 = /^[A-Za-z0-9\-_+]/, + peg$c27 = peg$classExpectation( + [['A', 'Z'], ['a', 'z'], ['0', '9'], '-', '_', '+'], + false, + false + ), + peg$c28 = function (a) { + return a.join(''); + }, + peg$c29 = function (v) { + return v; + }, + peg$c30 = function (start, last) { + return start.concat(last); + }, + peg$c31 = function (l) { + return l; + }, + peg$c32 = /^[0-9]/, + peg$c33 = peg$classExpectation([['0', '9']], false, false), + peg$c34 = function (d) { + return parseInt(d.join(''), 10); + }, + peg$currPos = 0, + peg$savedPos = 0, + peg$posDetailsCache = [{ line: 1, column: 1 }], + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + peg$result; - peg$startRuleFunctions = { start: peg$parsestart }, - peg$startRuleFunction = peg$parsestart, - - peg$c0 = "|", - peg$c1 = peg$literalExpectation("|", false), - peg$c2 = "&", - peg$c3 = peg$literalExpectation("&", false), - peg$c4 = "!", - peg$c5 = peg$literalExpectation("!", false), - peg$c6 = "(", - peg$c7 = peg$literalExpectation("(", false), - peg$c8 = ")", - peg$c9 = peg$literalExpectation(")", false), - peg$c10 = function(left, right) { return left || right; }, - peg$c11 = function(left, right) { return left && right; }, - peg$c12 = function(value) { return !value; }, - peg$c13 = function(value) { return value; }, - peg$c14 = ",", - peg$c15 = peg$literalExpectation(",", false), - peg$c16 = " ", - peg$c17 = peg$literalExpectation(" ", false), - peg$c18 = "[", - peg$c19 = peg$literalExpectation("[", false), - peg$c20 = "]", - peg$c21 = peg$literalExpectation("]", false), - peg$c22 = function(acs, a) { return checkAccess(acs, a); }, - peg$c23 = /^[A-Z]/, - peg$c24 = peg$classExpectation([["A", "Z"]], false, false), - peg$c25 = function(c) { return c.join(''); }, - peg$c26 = /^[A-Za-z0-9\-_+]/, - peg$c27 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "-", "_", "+"], false, false), - peg$c28 = function(a) { return a.join('') }, - peg$c29 = function(v) { return v; }, - peg$c30 = function(start, last) { return start.concat(last); }, - peg$c31 = function(l) { return l; }, - peg$c32 = /^[0-9]/, - peg$c33 = peg$classExpectation([["0", "9"]], false, false), - peg$c34 = function(d) { return parseInt(d.join(''), 10); }, - - peg$currPos = 0, - peg$savedPos = 0, - peg$posDetailsCache = [{ line: 1, column: 1 }], - peg$maxFailPos = 0, - peg$maxFailExpected = [], - peg$silentFails = 0, - - peg$result; - - if ("startRule" in options) { - if (!(options.startRule in peg$startRuleFunctions)) { - throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); - } - - peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; - } - - function text() { - return input.substring(peg$savedPos, peg$currPos); - } - - function location() { - return peg$computeLocation(peg$savedPos, peg$currPos); - } - - function expected(description, location) { - location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) - - throw peg$buildStructuredError( - [peg$otherExpectation(description)], - input.substring(peg$savedPos, peg$currPos), - location - ); - } - - function error(message, location) { - location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) - - throw peg$buildSimpleError(message, location); - } - - function peg$literalExpectation(text, ignoreCase) { - return { type: "literal", text: text, ignoreCase: ignoreCase }; - } - - function peg$classExpectation(parts, inverted, ignoreCase) { - return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; - } - - function peg$anyExpectation() { - return { type: "any" }; - } - - function peg$endExpectation() { - return { type: "end" }; - } - - function peg$otherExpectation(description) { - return { type: "other", description: description }; - } - - function peg$computePosDetails(pos) { - var details = peg$posDetailsCache[pos], p; - - if (details) { - return details; - } else { - p = pos - 1; - while (!peg$posDetailsCache[p]) { - p--; - } - - details = peg$posDetailsCache[p]; - details = { - line: details.line, - column: details.column - }; - - while (p < pos) { - if (input.charCodeAt(p) === 10) { - details.line++; - details.column = 1; - } else { - details.column++; + if ('startRule' in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error( + 'Can\'t start parsing from rule "' + options.startRule + '".' + ); } - p++; - } - - peg$posDetailsCache[pos] = details; - return details; - } - } - - function peg$computeLocation(startPos, endPos) { - var startPosDetails = peg$computePosDetails(startPos), - endPosDetails = peg$computePosDetails(endPos); - - return { - start: { - offset: startPos, - line: startPosDetails.line, - column: startPosDetails.column - }, - end: { - offset: endPos, - line: endPosDetails.line, - column: endPosDetails.column - } - }; - } - - function peg$fail(expected) { - if (peg$currPos < peg$maxFailPos) { return; } - - if (peg$currPos > peg$maxFailPos) { - peg$maxFailPos = peg$currPos; - peg$maxFailExpected = []; + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; } - peg$maxFailExpected.push(expected); - } - - function peg$buildSimpleError(message, location) { - return new peg$SyntaxError(message, null, null, location); - } - - function peg$buildStructuredError(expected, found, location) { - return new peg$SyntaxError( - peg$SyntaxError.buildMessage(expected, found), - expected, - found, - location - ); - } - - function peg$parsestart() { - var s0; - - s0 = peg$parseorExpr(); - - return s0; - } - - function peg$parseOR() { - var s0; - - if (input.charCodeAt(peg$currPos) === 124) { - s0 = peg$c0; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c1); } + function text() { + return input.substring(peg$savedPos, peg$currPos); } - return s0; - } - - function peg$parseAND() { - var s0; - - if (input.charCodeAt(peg$currPos) === 38) { - s0 = peg$c2; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c3); } + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); } - return s0; - } + function expected(description, location) { + location = + location !== void 0 + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); - function peg$parseNOT() { - var s0; - - if (input.charCodeAt(peg$currPos) === 33) { - s0 = peg$c4; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c5); } + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ); } - return s0; - } + function error(message, location) { + location = + location !== void 0 + ? location + : peg$computeLocation(peg$savedPos, peg$currPos); - function peg$parsegroupOpen() { - var s0; - - if (input.charCodeAt(peg$currPos) === 40) { - s0 = peg$c6; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c7); } + throw peg$buildSimpleError(message, location); } - return s0; - } - - function peg$parsegroupClose() { - var s0; - - if (input.charCodeAt(peg$currPos) === 41) { - s0 = peg$c8; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c9); } + function peg$literalExpectation(text, ignoreCase) { + return { type: 'literal', text: text, ignoreCase: ignoreCase }; } - return s0; - } + function peg$classExpectation(parts, inverted, ignoreCase) { + return { + type: 'class', + parts: parts, + inverted: inverted, + ignoreCase: ignoreCase, + }; + } - function peg$parseorExpr() { - var s0, s1, s2, s3; + function peg$anyExpectation() { + return { type: 'any' }; + } - s0 = peg$currPos; - s1 = peg$parseandExpr(); - if (s1 !== peg$FAILED) { - s2 = peg$parseOR(); - if (s2 !== peg$FAILED) { - s3 = peg$parseorExpr(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c10(s1, s3); - s0 = s1; + function peg$endExpectation() { + return { type: 'end' }; + } + + function peg$otherExpectation(description) { + return { type: 'other', description: description }; + } + + function peg$computePosDetails(pos) { + var details = peg$posDetailsCache[pos], + p; + + if (details) { + return details; } else { - peg$currPos = s0; - s0 = peg$FAILED; + p = pos - 1; + while (!peg$posDetailsCache[p]) { + p--; + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column, + }; + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++; + details.column = 1; + } else { + details.column++; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + return details; } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseandExpr(); } - return s0; - } + function peg$computeLocation(startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos), + endPosDetails = peg$computePosDetails(endPos); - function peg$parseandExpr() { - var s0, s1, s2, s3; + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column, + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column, + }, + }; + } - s0 = peg$currPos; - s1 = peg$parsenotExpr(); - if (s1 !== peg$FAILED) { - s2 = peg$parseAND(); - if (s2 === peg$FAILED) { - s2 = null; - } - if (s2 !== peg$FAILED) { - s3 = peg$parseorExpr(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c11(s1, s3); - s0 = s1; + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { + return; + } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildSimpleError(message, location) { + return new peg$SyntaxError(message, null, null, location); + } + + function peg$buildStructuredError(expected, found, location) { + return new peg$SyntaxError( + peg$SyntaxError.buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parsestart() { + var s0; + + s0 = peg$parseorExpr(); + + return s0; + } + + function peg$parseOR() { + var s0; + + if (input.charCodeAt(peg$currPos) === 124) { + s0 = peg$c0; + peg$currPos++; } else { - peg$currPos = s0; - s0 = peg$FAILED; + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c1); + } } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parsenotExpr(); + + return s0; } - return s0; - } + function peg$parseAND() { + var s0; - function peg$parsenotExpr() { - var s0, s1, s2; + if (input.charCodeAt(peg$currPos) === 38) { + s0 = peg$c2; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c3); + } + } - s0 = peg$currPos; - s1 = peg$parseNOT(); - if (s1 !== peg$FAILED) { - s2 = peg$parseatom(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c12(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$parseatom(); + return s0; } - return s0; - } + function peg$parseNOT() { + var s0; - function peg$parseatom() { - var s0, s1, s2, s3; + if (input.charCodeAt(peg$currPos) === 33) { + s0 = peg$c4; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c5); + } + } - s0 = peg$parseacsCheck(); - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = peg$parsegroupOpen(); - if (s1 !== peg$FAILED) { - s2 = peg$parseorExpr(); - if (s2 !== peg$FAILED) { - s3 = peg$parsegroupClose(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c13(s2); - s0 = s1; - } else { + return s0; + } + + function peg$parsegroupOpen() { + var s0; + + if (input.charCodeAt(peg$currPos) === 40) { + s0 = peg$c6; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c7); + } + } + + return s0; + } + + function peg$parsegroupClose() { + var s0; + + if (input.charCodeAt(peg$currPos) === 41) { + s0 = peg$c8; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c9); + } + } + + return s0; + } + + function peg$parseorExpr() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parseandExpr(); + if (s1 !== peg$FAILED) { + s2 = peg$parseOR(); + if (s2 !== peg$FAILED) { + s3 = peg$parseorExpr(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c10(s1, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { peg$currPos = s0; s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } - - return s0; - } - - function peg$parsecomma() { - var s0; - - if (input.charCodeAt(peg$currPos) === 44) { - s0 = peg$c14; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c15); } - } - - return s0; - } - - function peg$parsews() { - var s0; - - if (input.charCodeAt(peg$currPos) === 32) { - s0 = peg$c16; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c17); } - } - - return s0; - } - - function peg$parseoptWs() { - var s0, s1; - - s0 = []; - s1 = peg$parsews(); - while (s1 !== peg$FAILED) { - s0.push(s1); - s1 = peg$parsews(); - } - - return s0; - } - - function peg$parselistOpen() { - var s0; - - if (input.charCodeAt(peg$currPos) === 91) { - s0 = peg$c18; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c19); } - } - - return s0; - } - - function peg$parselistClose() { - var s0; - - if (input.charCodeAt(peg$currPos) === 93) { - s0 = peg$c20; - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c21); } - } - - return s0; - } - - function peg$parseacsCheck() { - var s0, s1, s2; - - s0 = peg$currPos; - s1 = peg$parseacsCode(); - if (s1 !== peg$FAILED) { - s2 = peg$parsearg(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c22(s1, s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseacsCode() { - var s0, s1, s2, s3; - - s0 = peg$currPos; - s1 = peg$currPos; - if (peg$c23.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c24); } - } - if (s2 !== peg$FAILED) { - if (peg$c23.test(input.charAt(peg$currPos))) { - s3 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c24); } - } - if (s3 !== peg$FAILED) { - s2 = [s2, s3]; - s1 = s2; - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - } else { - peg$currPos = s1; - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c25(s1); - } - s0 = s1; - - return s0; - } - - function peg$parseargVar() { - var s0, s1, s2; - - s0 = peg$currPos; - s1 = []; - if (peg$c26.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c27); } - } - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - if (peg$c26.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c27); } + if (s0 === peg$FAILED) { + s0 = peg$parseandExpr(); } - } - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c28(s1); - } - s0 = s1; - return s0; - } - - function peg$parsecommaList() { - var s0, s1, s2, s3, s4, s5, s6; - - s0 = peg$currPos; - s1 = []; - s2 = peg$currPos; - s3 = peg$parseargVar(); - if (s3 !== peg$FAILED) { - s4 = peg$parseoptWs(); - if (s4 !== peg$FAILED) { - s5 = peg$parsecomma(); - if (s5 !== peg$FAILED) { - s6 = peg$parseoptWs(); - if (s6 !== peg$FAILED) { - peg$savedPos = s2; - s3 = peg$c29(s3); - s2 = s3; - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; + return s0; } - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$currPos; - s3 = peg$parseargVar(); - if (s3 !== peg$FAILED) { - s4 = peg$parseoptWs(); - if (s4 !== peg$FAILED) { - s5 = peg$parsecomma(); - if (s5 !== peg$FAILED) { - s6 = peg$parseoptWs(); - if (s6 !== peg$FAILED) { - peg$savedPos = s2; - s3 = peg$c29(s3); - s2 = s3; - } else { - peg$currPos = s2; - s2 = peg$FAILED; + + function peg$parseandExpr() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parsenotExpr(); + if (s1 !== peg$FAILED) { + s2 = peg$parseAND(); + if (s2 === peg$FAILED) { + s2 = null; } - } else { + if (s2 !== peg$FAILED) { + s3 = peg$parseorExpr(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c11(s1, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parsenotExpr(); + } + + return s0; + } + + function peg$parsenotExpr() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parseNOT(); + if (s1 !== peg$FAILED) { + s2 = peg$parseatom(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c12(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$parseatom(); + } + + return s0; + } + + function peg$parseatom() { + var s0, s1, s2, s3; + + s0 = peg$parseacsCheck(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parsegroupOpen(); + if (s1 !== peg$FAILED) { + s2 = peg$parseorExpr(); + if (s2 !== peg$FAILED) { + s3 = peg$parsegroupClose(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c13(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parsecomma() { + var s0; + + if (input.charCodeAt(peg$currPos) === 44) { + s0 = peg$c14; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c15); + } + } + + return s0; + } + + function peg$parsews() { + var s0; + + if (input.charCodeAt(peg$currPos) === 32) { + s0 = peg$c16; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c17); + } + } + + return s0; + } + + function peg$parseoptWs() { + var s0, s1; + + s0 = []; + s1 = peg$parsews(); + while (s1 !== peg$FAILED) { + s0.push(s1); + s1 = peg$parsews(); + } + + return s0; + } + + function peg$parselistOpen() { + var s0; + + if (input.charCodeAt(peg$currPos) === 91) { + s0 = peg$c18; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c19); + } + } + + return s0; + } + + function peg$parselistClose() { + var s0; + + if (input.charCodeAt(peg$currPos) === 93) { + s0 = peg$c20; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c21); + } + } + + return s0; + } + + function peg$parseacsCheck() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parseacsCode(); + if (s1 !== peg$FAILED) { + s2 = peg$parsearg(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c22(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseacsCode() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$currPos; + if (peg$c23.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c24); + } + } + if (s2 !== peg$FAILED) { + if (peg$c23.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c24); + } + } + if (s3 !== peg$FAILED) { + s2 = [s2, s3]; + s1 = s2; + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + } else { + peg$currPos = s1; + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c25(s1); + } + s0 = s1; + + return s0; + } + + function peg$parseargVar() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = []; + if (peg$c26.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c27); + } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c26.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c27); + } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c28(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsecommaList() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = []; + s2 = peg$currPos; + s3 = peg$parseargVar(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoptWs(); + if (s4 !== peg$FAILED) { + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parseoptWs(); + if (s6 !== peg$FAILED) { + peg$savedPos = s2; + s3 = peg$c29(s3); + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { peg$currPos = s2; s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; } - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } - if (s1 !== peg$FAILED) { - s2 = peg$parseargVar(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c30(s1, s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parselist() { - var s0, s1, s2, s3; - - s0 = peg$currPos; - s1 = peg$parselistOpen(); - if (s1 !== peg$FAILED) { - s2 = peg$parsecommaList(); - if (s2 !== peg$FAILED) { - s3 = peg$parselistClose(); - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c31(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$currPos; + s3 = peg$parseargVar(); + if (s3 !== peg$FAILED) { + s4 = peg$parseoptWs(); + if (s4 !== peg$FAILED) { + s5 = peg$parsecomma(); + if (s5 !== peg$FAILED) { + s6 = peg$parseoptWs(); + if (s6 !== peg$FAILED) { + peg$savedPos = s2; + s3 = peg$c29(s3); + s2 = s3; + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } + } else { + peg$currPos = s2; + s2 = peg$FAILED; + } } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; + if (s1 !== peg$FAILED) { + s2 = peg$parseargVar(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c30(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; } - return s0; - } + function peg$parselist() { + var s0, s1, s2, s3; - function peg$parsenumber() { - var s0, s1, s2; + s0 = peg$currPos; + s1 = peg$parselistOpen(); + if (s1 !== peg$FAILED) { + s2 = peg$parsecommaList(); + if (s2 !== peg$FAILED) { + s3 = peg$parselistClose(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c31(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } - s0 = peg$currPos; - s1 = []; - if (peg$c32.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } + return s0; } - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); + + function peg$parsenumber() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = []; if (peg$c32.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; + s2 = input.charAt(peg$currPos); + peg$currPos++; } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c33); + } } - } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c32.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c33); + } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c34(s1); + } + s0 = s1; + + return s0; + } + + function peg$parsearg() { + var s0; + + s0 = peg$parselist(); + if (s0 === peg$FAILED) { + s0 = peg$parsenumber(); + if (s0 === peg$FAILED) { + s0 = null; + } + } + + return s0; + } + + const UserProps = require('./user_property.js'); + const Log = require('./logger.js').log; + const User = require('./user.js'); + + const _ = require('lodash'); + const moment = require('moment'); + + const client = _.get(options, 'subject.client'); + const user = _.get(options, 'subject.user'); + + function checkAccess(acsCode, value) { + try { + return { + LC: function isLocalConnection() { + return client && client.isLocal(); + }, + AG: function ageGreaterOrEqualThan() { + return !isNaN(value) && user && user.getAge() >= value; + }, + AS: function accountStatus() { + if (!user) { + return false; + } + if (!Array.isArray(value)) { + value = [value]; + } + const userAccountStatus = user.getPropertyAsNumber( + UserProps.AccountStatus + ); + return value.map(n => parseInt(n, 10)).includes(userAccountStatus); + }, + EC: function isEncoding() { + const encoding = _.get( + client, + 'term.outputEncoding', + '' + ).toLowerCase(); + switch (value) { + case 0: + return 'cp437' === encoding; + case 1: + return 'utf-8' === encoding; + default: + return false; + } + }, + GM: function isOneOfGroups() { + if (!user) { + return false; + } + if (!Array.isArray(value)) { + return false; + } + return value.some(groupName => user.isGroupMember(groupName)); + }, + NN: function isNode() { + if (!client) { + return false; + } + if (!Array.isArray(value)) { + value = [value]; + } + return value.map(n => parseInt(n, 10)).includes(client.node); + }, + NP: function numberOfPosts() { + if (!user) { + return false; + } + const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; + return !isNaN(value) && postCount >= value; + }, + NC: function numberOfCalls() { + if (!user) { + return false; + } + const loginCount = user.getPropertyAsNumber(UserProps.LoginCount); + return !isNaN(value) && loginCount >= value; + }, + AA: function accountAge() { + if (!user) { + return false; + } + const accountCreated = moment( + user.getProperty(UserProps.AccountCreated) + ); + const now = moment(); + const daysOld = accountCreated.diff(moment(), 'days'); + return ( + !isNaN(value) && + accountCreated.isValid() && + now.isAfter(accountCreated) && + daysOld >= value + ); + }, + BU: function bytesUploaded() { + if (!user) { + return false; + } + const bytesUp = + user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; + return !isNaN(value) && bytesUp >= value; + }, + UP: function uploads() { + if (!user) { + return false; + } + const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; + return !isNaN(value) && uls >= value; + }, + BD: function bytesDownloaded() { + if (!user) { + return false; + } + const bytesDown = + user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; + return !isNaN(value) && bytesDown >= value; + }, + DL: function downloads() { + if (!user) { + return false; + } + const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; + return !isNaN(value) && dls >= value; + }, + NR: function uploadDownloadRatioGreaterThan() { + if (!user) { + return false; + } + const ulCount = + user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; + const dlCount = + user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; + const ratio = ~~((ulCount / dlCount) * 100); + return !isNaN(value) && ratio >= value; + }, + KR: function uploadDownloadByteRatioGreaterThan() { + if (!user) { + return false; + } + const ulBytes = + user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; + const dlBytes = + user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; + const ratio = ~~((ulBytes / dlBytes) * 100); + return !isNaN(value) && ratio >= value; + }, + PC: function postCallRatio() { + if (!user) { + return false; + } + const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; + const loginCount = + user.getPropertyAsNumber(UserProps.LoginCount) || 0; + const ratio = ~~((postCount / loginCount) * 100); + return !isNaN(value) && ratio >= value; + }, + SC: function isSecureConnection() { + return _.get(client, 'session.isSecure', false); + }, + AF: function currentAuthFactor() { + if (!user) { + return false; + } + return !isNaN(value) && user.authFactor >= value; + }, + AR: function authFactorRequired() { + if (!user) { + return false; + } + switch (value) { + case 1: + return true; + case 2: + return user.getProperty(UserProps.AuthFactor2OTP) + ? true + : false; + default: + return false; + } + }, + ML: function minutesLeft() { + // :TODO: implement me! + return false; + }, + TH: function termHeight() { + return !isNaN(value) && _.get(client, 'term.termHeight', 0) >= value; + }, + TM: function isOneOfThemes() { + if (!Array.isArray(value)) { + return false; + } + return value.includes(_.get(client, 'currentTheme.name')); + }, + TT: function isOneOfTermTypes() { + if (!Array.isArray(value)) { + return false; + } + return value.includes(_.get(client, 'term.termType')); + }, + TW: function termWidth() { + return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; + }, + ID: function isUserId() { + if (!user) { + return false; + } + if (!Array.isArray(value)) { + value = [value]; + } + return value.map(n => parseInt(n, 10)).includes(user.userId); + }, + WD: function isOneOfDayOfWeek() { + if (!Array.isArray(value)) { + value = [value]; + } + return value.map(n => parseInt(n, 10)).includes(new Date().getDay()); + }, + MM: function isMinutesPastMidnight() { + const now = moment(); + const midnight = now.clone().startOf('day'); + const minutesPastMidnight = now.diff(midnight, 'minutes'); + return !isNaN(value) && minutesPastMidnight >= value; + }, + AC: function achievementCount() { + if (!user) { + return false; + } + const count = + user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0; + return !isNan(value) && points >= value; + }, + AP: function achievementPoints() { + if (!user) { + return false; + } + const points = + user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0; + return !isNan(value) && points >= value; + }, + PV: function userPropValue() { + if (!user || !Array.isArray(value) || value.length !== 2) { + return false; + } + const [propName, propValue] = value; + const actualPropValue = user.getProperty(propName); + return actualPropValue === propValue; + }, + }[acsCode](value); + } catch (e) { + const logger = _.get(client, 'log', Log); + logger.warn({ acsCode: acsCode, value: value }, 'Invalid ACS string!'); + + return false; + } + } + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; } else { - s1 = peg$FAILED; + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()); + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c34(s1); - } - s0 = s1; - - return s0; - } - - function peg$parsearg() { - var s0; - - s0 = peg$parselist(); - if (s0 === peg$FAILED) { - s0 = peg$parsenumber(); - if (s0 === peg$FAILED) { - s0 = null; - } - } - - return s0; - } - - - const UserProps = require('./user_property.js'); - const Log = require('./logger.js').log; - const User = require('./user.js'); - - const _ = require('lodash'); - const moment = require('moment'); - - const client = _.get(options, 'subject.client'); - const user = _.get(options, 'subject.user'); - - function checkAccess(acsCode, value) { - try { - return { - LC : function isLocalConnection() { - return client && client.isLocal(); - }, - AG : function ageGreaterOrEqualThan() { - return !isNaN(value) && user && user.getAge() >= value; - }, - AS : function accountStatus() { - if(!user) { - return false; - } - if(!Array.isArray(value)) { - value = [ value ]; - } - const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus); - return value.map(n => parseInt(n, 10)).includes(userAccountStatus); - }, - EC : function isEncoding() { - const encoding = _.get(client, 'term.outputEncoding', '').toLowerCase(); - switch(value) { - case 0 : return 'cp437' === encoding; - case 1 : return 'utf-8' === encoding; - default : return false; - } - }, - GM : function isOneOfGroups() { - if(!user) { - return false; - } - if(!Array.isArray(value)) { - return false; - } - return value.some(groupName => user.isGroupMember(groupName)); - }, - NN : function isNode() { - if(!client) { - return false; - } - if(!Array.isArray(value)) { - value = [ value ]; - } - return value.map(n => parseInt(n, 10)).includes(client.node); - }, - NP : function numberOfPosts() { - if(!user) { - return false; - } - const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; - return !isNaN(value) && postCount >= value; - }, - NC : function numberOfCalls() { - if(!user) { - return false; - } - const loginCount = user.getPropertyAsNumber(UserProps.LoginCount); - return !isNaN(value) && loginCount >= value; - }, - AA : function accountAge() { - if(!user) { - return false; - } - const accountCreated = moment(user.getProperty(UserProps.AccountCreated)); - const now = moment(); - const daysOld = accountCreated.diff(moment(), 'days'); - return !isNaN(value) && - accountCreated.isValid() && - now.isAfter(accountCreated) && - daysOld >= value; - }, - BU : function bytesUploaded() { - if(!user) { - return false; - } - const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; - return !isNaN(value) && bytesUp >= value; - }, - UP : function uploads() { - if(!user) { - return false; - } - const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; - return !isNaN(value) && uls >= value; - }, - BD : function bytesDownloaded() { - if(!user) { - return false; - } - const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; - return !isNaN(value) && bytesDown >= value; - }, - DL : function downloads() { - if(!user) { - return false; - } - const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; - return !isNaN(value) && dls >= value; - }, - NR : function uploadDownloadRatioGreaterThan() { - if(!user) { - return false; - } - const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; - const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; - const ratio = ~~((ulCount / dlCount) * 100); - return !isNaN(value) && ratio >= value; - }, - KR : function uploadDownloadByteRatioGreaterThan() { - if(!user) { - return false; - } - const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; - const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; - const ratio = ~~((ulBytes / dlBytes) * 100); - return !isNaN(value) && ratio >= value; - }, - PC : function postCallRatio() { - if(!user) { - return false; - } - const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; - const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0; - const ratio = ~~((postCount / loginCount) * 100); - return !isNaN(value) && ratio >= value; - }, - SC : function isSecureConnection() { - return _.get(client, 'session.isSecure', false); - }, - AF : function currentAuthFactor() { - if(!user) { - return false; - } - return !isNaN(value) && user.authFactor >= value; - }, - AR : function authFactorRequired() { - if(!user) { - return false; - } - switch(value) { - case 1 : return true; - case 2 : return user.getProperty(UserProps.AuthFactor2OTP) ? true : false; - default : return false; - } - }, - ML : function minutesLeft() { - // :TODO: implement me! - return false; - }, - TH : function termHeight() { - return !isNaN(value) && _.get(client, 'term.termHeight', 0) >= value; - }, - TM : function isOneOfThemes() { - if(!Array.isArray(value)) { - return false; - } - return value.includes(_.get(client, 'currentTheme.name')); - }, - TT : function isOneOfTermTypes() { - if(!Array.isArray(value)) { - return false; - } - return value.includes(_.get(client, 'term.termType')); - }, - TW : function termWidth() { - return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; - }, - ID : function isUserId() { - if(!user) { - return false; - } - if(!Array.isArray(value)) { - value = [ value ]; - } - return value.map(n => parseInt(n, 10)).includes(user.userId); - }, - WD : function isOneOfDayOfWeek() { - if(!Array.isArray(value)) { - value = [ value ]; - } - return value.map(n => parseInt(n, 10)).includes(new Date().getDay()); - }, - MM : function isMinutesPastMidnight() { - const now = moment(); - const midnight = now.clone().startOf('day') - const minutesPastMidnight = now.diff(midnight, 'minutes'); - return !isNaN(value) && minutesPastMidnight >= value; - }, - AC : function achievementCount() { - if(!user) { - return false; - } - const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0; - return !isNan(value) && points >= value; - }, - AP : function achievementPoints() { - if(!user) { - return false; - } - const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0; - return !isNan(value) && points >= value; - }, - PV : function userPropValue() { - if (!user || !Array.isArray(value) || value.length !== 2) { - return false; - } - const [propName, propValue] = value; - const actualPropValue = user.getProperty(propName); - return actualPropValue === propValue; - } - }[acsCode](value); - } catch (e) { - const logger = _.get(client, 'log', Log); - logger.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!'); - - return false; - } - } - - - peg$result = peg$startRuleFunction(); - - if (peg$result !== peg$FAILED && peg$currPos === input.length) { - return peg$result; - } else { - if (peg$result !== peg$FAILED && peg$currPos < input.length) { - peg$fail(peg$endExpectation()); - } - - throw peg$buildStructuredError( - peg$maxFailExpected, - peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, - peg$maxFailPos < input.length - ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) - : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) - ); - } } module.exports = { - SyntaxError: peg$SyntaxError, - parse: peg$parse + SyntaxError: peg$SyntaxError, + parse: peg$parse, }; diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 438f6203..27d5490d 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -const miscUtil = require('./misc_util.js'); -const ansi = require('./ansi_term.js'); -const Log = require('./logger.js').log; +const miscUtil = require('./misc_util.js'); +const ansi = require('./ansi_term.js'); +const Log = require('./logger.js').log; // deps -const events = require('events'); -const util = require('util'); -const _ = require('lodash'); +const events = require('events'); +const util = require('util'); +const _ = require('lodash'); -exports.ANSIEscapeParser = ANSIEscapeParser; +exports.ANSIEscapeParser = ANSIEscapeParser; const CR = 0x0d; const LF = 0x0a; @@ -20,49 +20,47 @@ function ANSIEscapeParser(options) { events.EventEmitter.call(this); - this.column = 1; - this.graphicRendition = {}; + this.column = 1; + this.graphicRendition = {}; this.parseState = { - re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex + re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex }; options = miscUtil.valueWithDefault(options, { - mciReplaceChar : '', - termHeight : 25, - termWidth : 80, - trailingLF : 'default', // default|omit|no|yes, ... + mciReplaceChar: '', + termHeight: 25, + termWidth: 80, + trailingLF: 'default', // default|omit|no|yes, ... }); - - this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); - this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); - this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); - this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); - + this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); + this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); + this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); + this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); this.row = Math.min(options?.startRow ?? 1, this.termHeight); - self.moveCursor = function(cols, rows) { + self.moveCursor = function (cols, rows) { self.column += cols; - self.row += rows; + 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.min(self.column, self.termWidth); // can't move past term width + self.row = Math.max(self.row, 1); self.positionUpdated(); }; - self.saveCursorPosition = function() { + self.saveCursorPosition = function () { self.savedPosition = { - row : self.row, - column : self.column + row: self.row, + column: self.column, }; }; - self.restoreCursorPosition = function() { - self.row = self.savedPosition.row; + self.restoreCursorPosition = function () { + self.row = self.savedPosition.row; self.column = self.savedPosition.column; delete self.savedPosition; @@ -70,29 +68,28 @@ function ANSIEscapeParser(options) { // self.rowUpdated(); }; - self.clearScreen = function() { + self.clearScreen = function () { self.column = 1; - self.row = 1; + self.row = 1; self.emit('clear screen'); }; - - self.positionUpdated = function() { + self.positionUpdated = function () { self.emit('position update', self.row, self.column); }; function literal(text) { - const len = text.length; - let pos = 0; - let start = 0; + const len = text.length; + let pos = 0; + let start = 0; let charCode; let lastCharCode; - while(pos < len) { + while (pos < len) { charCode = text.charCodeAt(pos) & 0xff; // 8bit clean - switch(charCode) { - case CR : + switch (charCode) { + case CR: self.emit('literal', text.slice(start, pos)); start = pos; @@ -101,7 +98,7 @@ function ANSIEscapeParser(options) { self.positionUpdated(); break; - case LF : + case LF: // Handle ANSI saved with UNIX-style LF's only // vs the CRLF pairs if (lastCharCode !== CR) { @@ -116,13 +113,13 @@ function ANSIEscapeParser(options) { self.positionUpdated(); break; - default : - if(self.column === self.termWidth) { + default: + if (self.column === self.termWidth) { self.emit('literal', text.slice(start, pos + 1)); start = pos + 1; self.column = 1; - self.row += 1; + self.row += 1; self.positionUpdated(); } else { @@ -138,15 +135,15 @@ function ANSIEscapeParser(options) { // // Finalize this chunk // - if(self.column > self.termWidth) { + if (self.column > self.termWidth) { self.column = 1; - self.row += 1; + self.row += 1; self.positionUpdated(); } const rem = text.slice(start); - if(rem) { + if (rem) { self.emit('literal', rem); } } @@ -161,18 +158,18 @@ function ANSIEscapeParser(options) { var id; do { - pos = mciRe.lastIndex; - match = mciRe.exec(buffer); + pos = mciRe.lastIndex; + match = mciRe.exec(buffer); - if(null !== match) { - if(match.index > pos) { + if (null !== match) { + if (match.index > pos) { literal(buffer.slice(pos, match.index)); } mciCode = match[1]; - id = match[2] || null; + id = match[2] || null; - if(match[3]) { + if (match[3]) { args = match[3].split(','); } else { args = []; @@ -180,58 +177,62 @@ function ANSIEscapeParser(options) { // if MCI codes are changing, save off the current color var fullMciCode = mciCode + (id || ''); - if(self.lastMciCode !== fullMciCode) { - + if (self.lastMciCode !== fullMciCode) { self.lastMciCode = fullMciCode; self.graphicRenditionForErase = _.clone(self.graphicRendition); } - self.emit('mci', { - position : [self.row, self.column], - mci : mciCode, - id : id ? parseInt(id, 10) : null, - args : args, - SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + position: [self.row, self.column], + 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]); } } + } while (0 !== mciRe.lastIndex); - } while(0 !== mciRe.lastIndex); - - if(pos < buffer.length) { + if (pos < buffer.length) { literal(buffer.slice(pos)); } } - self.reset = function(input) { + self.reset = function (input) { self.column = 1; self.row = Math.min(options?.startRow ?? 1, self.termHeight); 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, + 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.stop = function () { self.parseState.stop = true; }; - self.parse = function(input) { - if(input) { + self.parse = function (input) { + if (input) { self.reset(input); } @@ -240,53 +241,53 @@ function ANSIEscapeParser(options) { var match; var opCode; var args; - var re = self.parseState.re; - var buffer = self.parseState.buffer; + var re = self.parseState.re; + var buffer = self.parseState.buffer; self.parseState.stop = false; do { - if(self.parseState.stop) { + 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) { + 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); //self.emit('chunk', match[0]); self.emit('control', match[0], opCode, args); } - } while(0 !== re.lastIndex); + } while (0 !== re.lastIndex); - if(pos < buffer.length) { + 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' : + 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) { + if (this.termHeight === self.row) { lastBit = lastBit.slice(0, -2); } break; - case 'omit' : - case 'no' : - case false : + case 'omit': + case 'no': + case false: lastBit = lastBit.slice(0, -2); break; } @@ -343,69 +344,69 @@ function ANSIEscapeParser(options) { function escape(opCode, args) { let arg; - switch(opCode) { + switch (opCode) { // cursor up - case 'A' : + case 'A': //arg = args[0] || 1; arg = isNaN(args[0]) ? 1 : args[0]; self.moveCursor(0, -arg); break; - // cursor down - case 'B' : + // 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' : + // 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' : + // 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 + 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.row = isNaN(args[0]) ? 1 : args[0]; self.column = isNaN(args[1]) ? 1 : args[1]; //self.rowUpdated(); self.positionUpdated(); break; - // save position - case 's' : + // save position + case 's': self.saveCursorPosition(); break; - // restore position - case 'u' : + // restore position + case 'u': self.restoreCursorPosition(); break; - // set graphic rendition - case 'm' : + // set graphic rendition + case 'm': self.graphicRendition.reset = false; - for(let i = 0, len = args.length; i < len; ++i) { + for (let i = 0, len = args.length; i < len; ++i) { arg = args[i]; - if(ANSIEscapeParser.foregroundColors[arg]) { + if (ANSIEscapeParser.foregroundColors[arg]) { self.graphicRendition.fg = arg; - } else if(ANSIEscapeParser.backgroundColors[arg]) { + } else if (ANSIEscapeParser.backgroundColors[arg]) { self.graphicRendition.bg = arg; - } else if(ANSIEscapeParser.styles[arg]) { - switch(arg) { - case 0 : + } else if (ANSIEscapeParser.styles[arg]) { + switch (arg) { + case 0: // clear out everything delete self.graphicRendition.intensity; delete self.graphicRendition.underline; @@ -421,49 +422,52 @@ function ANSIEscapeParser(options) { //self.graphicRendition.bg = 49; break; - case 1 : - case 2 : - case 22 : + case 1: + case 2: + case 22: self.graphicRendition.intensity = arg; break; - case 4 : - case 24 : + case 4: + case 24: self.graphicRendition.underline = arg; break; - case 5 : - case 6 : - case 25 : + case 5: + case 6: + case 25: self.graphicRendition.blink = arg; break; - case 7 : - case 27 : + case 7: + case 27: self.graphicRendition.negative = arg; break; - case 8 : - case 28 : + case 8: + case 28: self.graphicRendition.invisible = arg; break; - default : - Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI'); + default: + Log.trace( + { attribute: arg }, + 'Unknown attribute while parsing ANSI' + ); break; } } } self.emit('sgr update', self.graphicRendition); - break; // m + break; // m - // :TODO: s, u, K + // :TODO: s, u, K - // erase display/screen - case 'J' : + // erase display/screen + case 'J': // :TODO: Handle other 'J' types! - if(2 === args[0]) { + if (2 === args[0]) { self.clearScreen(); } break; @@ -474,30 +478,30 @@ function ANSIEscapeParser(options) { util.inherits(ANSIEscapeParser, events.EventEmitter); ANSIEscapeParser.foregroundColors = { - 30 : 'black', - 31 : 'red', - 32 : 'green', - 33 : 'yellow', - 34 : 'blue', - 35 : 'magenta', - 36 : 'cyan', - 37 : 'white', - 39 : 'default', // same as white for most implementations + 30: 'black', + 31: 'red', + 32: 'green', + 33: 'yellow', + 34: 'blue', + 35: 'magenta', + 36: 'cyan', + 37: 'white', + 39: 'default', // same as white for most implementations - 90 : 'grey' + 90: 'grey', }; Object.freeze(ANSIEscapeParser.foregroundColors); ANSIEscapeParser.backgroundColors = { - 40 : 'black', - 41 : 'red', - 42 : 'green', - 43 : 'yellow', - 44 : 'blue', - 45 : 'magenta', - 46 : 'cyan', - 47 : 'white', - 49 : 'default', // same as black for most implementations + 40: 'black', + 41: 'red', + 42: 'green', + 43: 'yellow', + 44: 'blue', + 45: 'magenta', + 46: 'cyan', + 47: 'white', + 49: 'default', // same as black for most implementations }; Object.freeze(ANSIEscapeParser.backgroundColors); @@ -512,24 +516,23 @@ 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 09c9bbf6..77eb633a 100644 --- a/core/ansi_prep.js +++ b/core/ansi_prep.js @@ -2,58 +2,61 @@ 'use strict'; // ENiGMA½ -const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; -const ANSI = require('./ansi_term.js'); -const { - splitTextAtTerms, - renderStringLength -} = require('./string_util.js'); +const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; +const ANSI = require('./ansi_term.js'); +const { splitTextAtTerms, renderStringLength } = require('./string_util.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); module.exports = function ansiPrep(input, options, cb) { - if(!input) { + 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 } ); + const canvas = Array.from( + { length: 'auto' === options.rows ? 25 : options.rows }, + () => Array.from({ length: options.cols }, () => new Object()) + ); + const parser = new ANSIEscapeParser({ + termHeight: options.termHeight, + termWidth: options.termWidth, + }); const state = { - row : 0, - col : 0, + row: 0, + col: 0, }; let lastRow = 0; function ensureRow(row) { - if(canvas[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; + state.row = row - 1; + state.col = col - 1; - if(0 === state.col) { + if (0 === state.col) { state.initialSgr = state.lastSgr; } - lastRow = Math.max(state.row, lastRow); + lastRow = Math.max(state.row, lastRow); }); parser.on('literal', literal => { @@ -62,20 +65,23 @@ module.exports = function ansiPrep(input, options, cb) { // 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)) { + for (let c of literal) { + if ( + state.col < options.cols && + ('auto' === options.rows || state.row < options.rows) + ) { ensureRow(state.row); - if(0 === state.col) { + if (0 === state.col) { canvas[state.row][state.col].initialSgr = state.initialSgr; } canvas[state.row][state.col].char = c; - if(state.sgr) { - canvas[state.row][state.col].sgr = _.clone(state.sgr); - state.lastSgr = canvas[state.row][state.col].sgr; - state.sgr = null; + if (state.sgr) { + canvas[state.row][state.col].sgr = _.clone(state.sgr); + state.lastSgr = canvas[state.row][state.col].sgr; + state.sgr = null; } } @@ -86,9 +92,9 @@ module.exports = function ansiPrep(input, options, cb) { 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; + 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; } @@ -96,8 +102,8 @@ module.exports = function ansiPrep(input, options, cb) { function getLastPopulatedColumn(row) { let col = row.length; - while(--col > 0) { - if(row[col].char || row[col].sgr) { + while (--col > 0) { + if (row[col].char || row[col].sgr) { break; } } @@ -113,18 +119,23 @@ module.exports = function ansiPrep(input, options, cb) { const lastCol = getLastPopulatedColumn(row) + 1; let i; - line = options.indent ? - output.length > 0 ? ' '.repeat(options.indent) : '' : - ''; + line = options.indent + ? output.length > 0 + ? ' '.repeat(options.indent) + : '' + : ''; - for(i = 0; i < lastCol; ++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) { + if (!options.asciiMode && col.sgr) { sgr += ANSI.getSGRFromGraphicRendition(col.sgr); } @@ -133,19 +144,22 @@ module.exports = function ansiPrep(input, options, cb) { output += line; - if(i < row.length) { + if (i < row.length) { output += `${options.asciiMode ? '' : ANSI.blackBG()}`; - if(options.fillLines) { - output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; + if (options.fillLines) { + output += `${row + .slice(i) + .map(() => ' ') + .join('')}`; //${lastSgr}`; } } - if(options.startCol + i < options.termWidth || options.forceLineTerm) { + if (options.startCol + i < options.termWidth || options.forceLineTerm) { output += '\r\n'; } }); - if(options.exportMode) { + if (options.exportMode) { // // If we're in export mode, we do some additional hackery: // @@ -156,7 +170,7 @@ module.exports = function ansiPrep(input, options, cb) { // * 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 + const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with let exportOutput = ''; let m; @@ -167,30 +181,30 @@ module.exports = function ansiPrep(input, options, cb) { splitTextAtTerms(output).forEach(fullLine => { renderStart = 0; - while(fullLine.length > 0) { + while (fullLine.length > 0) { let splitAt; const ANSI_REGEXP = ANSI.getFullMatchRegExp(); wantMore = true; - while((m = ANSI_REGEXP.exec(fullLine))) { + while ((m = ANSI_REGEXP.exec(fullLine))) { afterSeq = m.index + m[0].length; - if(afterSeq < MAX_CHARS) { + if (afterSeq < MAX_CHARS) { // after current seq splitAt = afterSeq; } else { - if(m.index < MAX_CHARS) { + if (m.index < MAX_CHARS) { // before last found seq splitAt = m.index; - wantMore = false; // can't eat up any more + wantMore = false; // can't eat up any more } - break; // seq's beyond this point are >= MAX_CHARS + break; // seq's beyond this point are >= MAX_CHARS } } - if(splitAt) { - if(wantMore) { + if (splitAt) { + if (wantMore) { splitAt = Math.min(fullLine.length, MAX_CHARS - 1); } } else { @@ -202,7 +216,8 @@ module.exports = function ansiPrep(input, options, cb) { renderStart += renderStringLength(part); exportOutput += `${part}\r\n`; - if(fullLine.length > 0) { // more to go for this line? + if (fullLine.length > 0) { + // more to go for this line? exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; } else { exportOutput += ANSI.up(); diff --git a/core/ansi_term.js b/core/ansi_term.js index e90a92c3..94a56400 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -43,48 +43,48 @@ // // ENiGMA½ -const miscUtil = require('./misc_util.js'); +const miscUtil = require('./misc_util.js'); // deps -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); -exports.getFullMatchRegExp = getFullMatchRegExp; -exports.getFGColorValue = getFGColorValue; -exports.getBGColorValue = getBGColorValue; -exports.sgr = sgr; -exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition; -exports.clearScreen = clearScreen; -exports.resetScreen = resetScreen; -exports.normal = normal; -exports.goHome = goHome; -exports.disableVT100LineWrapping = disableVT100LineWrapping; -exports.setSyncTermFont = setSyncTermFont; -exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias; -exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; -exports.setCursorStyle = setCursorStyle; -exports.setEmulatedBaudRate = setEmulatedBaudRate; -exports.vtxHyperlink = vtxHyperlink; +exports.getFullMatchRegExp = getFullMatchRegExp; +exports.getFGColorValue = getFGColorValue; +exports.getBGColorValue = getBGColorValue; +exports.sgr = sgr; +exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition; +exports.clearScreen = clearScreen; +exports.resetScreen = resetScreen; +exports.normal = normal; +exports.goHome = goHome; +exports.disableVT100LineWrapping = disableVT100LineWrapping; +exports.setSyncTermFont = setSyncTermFont; +exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias; +exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; +exports.setCursorStyle = setCursorStyle; +exports.setEmulatedBaudRate = setEmulatedBaudRate; +exports.vtxHyperlink = vtxHyperlink; // // See also // https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js -const ESC_CSI = '\u001b['; +const ESC_CSI = '\u001b['; const CONTROL = { - up : 'A', - down : 'B', + up: 'A', + down: 'B', - forward : 'C', - right : 'C', + forward: 'C', + right: 'C', - back : 'D', - left : 'D', + back: 'D', + left: 'D', - nextLine : 'E', - prevLine : 'F', - horizAbsolute : 'G', + nextLine: 'E', + prevLine: 'F', + horizAbsolute: 'G', // // CSI [ p1 ] J @@ -103,10 +103,10 @@ const CONTROL = { // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 // and screen remainder // - eraseData : 'J', + eraseData: 'J', - eraseLine : 'K', - insertLine : 'L', + eraseLine: 'K', + insertLine: 'L', // // CSI [ p1 ] M @@ -128,28 +128,28 @@ const CONTROL = { // incompatibilities & oddities around this sequence. ANSI-BBS // states that it *should* work with any value of p1. // - deleteLine : 'M', - ansiMusic : 'M', + deleteLine: 'M', + ansiMusic: 'M', - scrollUp : 'S', - scrollDown : 'T', - setScrollRegion : 'r', - savePos : 's', - restorePos : 'u', - queryPos : '6n', - queryScreenSize : '255n', // See bansi.txt - goto : 'H', // row Pr, column Pc -- same as f - gotoAlt : 'f', // same as H + scrollUp: 'S', + scrollDown: 'T', + setScrollRegion: 'r', + savePos: 's', + restorePos: 'u', + queryPos: '6n', + queryScreenSize: '255n', // See bansi.txt + goto: 'H', // row Pr, column Pc -- same as f + gotoAlt: 'f', // same as H - blinkToBrightIntensity : '?33h', - blinkNormal : '?33l', + 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 @@ -160,41 +160,44 @@ 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 + 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) { @@ -205,7 +208,6 @@ function getBGColorValue(name) { return SGRValues[name + 'BG']; } - // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // :TODO: document // :TODO: Create mappings for aliases... maybe make this a map to values instead @@ -275,49 +277,48 @@ 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', - 'amiga_mosoul' : 'mo_soul', + mo_soul: 'mo_soul', + mosoul: 'mo_soul', + "mo'soul": 'mo_soul', + amiga_mosoul: 'mo_soul', - 'amiga_microknight' : 'microknight', - 'amiga_microknight+' : 'microknight_plus', - - 'atari' : 'atari', - 'atarist' : 'atari', + amiga_microknight: 'microknight', + 'amiga_microknight+': 'microknight_plus', + atari: 'atari', + atarist: 'atari', }; function setSyncTermFont(name, fontPage) { @@ -326,7 +327,7 @@ function setSyncTermFont(name, fontPage) { assert(p1 >= 0 && p1 <= 3); const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name); - if(p2 > -1) { + if (p2 > -1) { return `${ESC_CSI}${p1};${p2} D`; } @@ -343,31 +344,30 @@ function setSyncTermFontWithAlias(nameOrAlias) { } const DEC_CURSOR_STYLE = { - 'blinking block' : 0, - 'default' : 1, - 'steady block' : 2, - 'blinking underline' : 3, - 'steady underline' : 4, - 'blinking bar' : 5, - 'steady bar' : 6, + 'blinking block': 0, + default: 1, + 'steady block': 2, + 'blinking underline': 3, + 'steady underline': 4, + 'blinking bar': 5, + 'steady bar': 6, }; function setCursorStyle(cursorStyle) { const ps = DEC_CURSOR_STYLE[cursorStyle]; - if(ps) { + 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]; - exports[name] = function() { + exports[name] = function () { let c = code; - if(arguments.length > 0) { + if (arguments.length > 0) { // arguments are array like -- we want an array c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; } @@ -376,10 +376,10 @@ Object.keys(CONTROL).forEach(function onControlName(name) { }); // Create various color methods such as white(), yellowBG(), reset(), ... -Object.keys(SGRValues).forEach( name => { +Object.keys(SGRValues).forEach(name => { const code = SGRValues[name]; - exports[name] = function() { + exports[name] = function () { return `${ESC_CSI}${code}m`; }; }); @@ -390,18 +390,18 @@ function sgr() { // - Each element can be either a integer or string found in SGRValues // which in turn maps to a integer // - if(arguments.length <= 0) { + 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) { + for (let i = 0; i < args.length; ++i) { const arg = args[i]; - if(_.isString(arg) && arg in SGRValues) { + if (_.isString(arg) && arg in SGRValues) { result.push(SGRValues[arg]); - } else if(_.isNumber(arg)) { + } else if (_.isNumber(arg)) { result.push(arg); } } @@ -414,25 +414,25 @@ 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]) { + ['intensity', 'underline', 'blink', 'negative', 'invisible'].forEach(s => { + if (graphicRendition[s]) { sgrSeq.push(graphicRendition[s]); ++styleCount; } }); - if(graphicRendition.fg) { + if (graphicRendition.fg) { sgrSeq.push(graphicRendition.fg); } - if(graphicRendition.bg) { + if (graphicRendition.bg) { sgrSeq.push(graphicRendition.bg); } - if(0 === styleCount || initialReset) { + if (0 === styleCount || initialReset) { sgrSeq.unshift(0); } @@ -452,11 +452,11 @@ function resetScreen() { } 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 } // @@ -476,32 +476,36 @@ function disableVT100LineWrapping() { } function setEmulatedBaudRate(rate) { - const speed = { - unlimited : 0, - off : 0, - 0 : 0, - 300 : 1, - 600 : 2, - 1200 : 3, - 2400 : 4, - 4800 : 5, - 9600 : 6, - 19200 : 7, - 38400 : 8, - 57600 : 9, - 76800 : 10, - 115200 : 11, - }[rate] || 0; + 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')) { + if (!client.terminalSupports('vtx_hyperlink')) { return ''; } len = len || url.length; - url = url.split('').map(c => c.charCodeAt(0)).join(';'); + 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/archaicnet.js b/core/archaicnet.js index 31ce4dc2..8764407a 100644 --- a/core/archaicnet.js +++ b/core/archaicnet.js @@ -2,19 +2,19 @@ 'use strict'; // enigma-bbs -const { MenuModule } = require('../core/menu_module.js'); -const { resetScreen } = require('../core/ansi_term.js'); -const { Errors } = require('../core/enig_error.js'); +const { MenuModule } = require('../core/menu_module.js'); +const { resetScreen } = require('../core/ansi_term.js'); +const { Errors } = require('../core/enig_error.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const SSHClient = require('ssh2').Client; +const async = require('async'); +const _ = require('lodash'); +const SSHClient = require('ssh2').Client; exports.moduleInfo = { - name : 'ArchaicNET', - desc : 'ArchaicNET Access Module', - author : 'NuSkooler', + name: 'ArchaicNET', + desc: 'ArchaicNET Access Module', + author: 'NuSkooler', }; exports.getModule = class ArchaicNETModule extends MenuModule { @@ -22,10 +22,10 @@ exports.getModule = class ArchaicNETModule extends MenuModule { super(options); // establish defaults - this.config = options.menuConfig.config; - this.config.host = this.config.host || 'bbs.archaicbinary.net'; - this.config.sshPort = this.config.sshPort || 2222; - this.config.rloginPort = this.config.rloginPort || 8513; + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'bbs.archaicbinary.net'; + this.config.sshPort = this.config.sshPort || 2222; + this.config.rloginPort = this.config.rloginPort || 8513; } initSequence() { @@ -35,10 +35,12 @@ exports.getModule = class ArchaicNETModule extends MenuModule { async.series( [ function validateConfig(callback) { - const reqConfs = [ 'username', 'password', 'bbsTag' ]; - for(let req of reqConfs) { - if(!_.isString(_.get(self, [ 'config', req ]))) { - return callback(Errors.MissingConfig(`Config requires "${req}"`)); + const reqConfs = ['username', 'password', 'bbsTag']; + for (let req of reqConfs) { + if (!_.isString(_.get(self, ['config', req]))) { + return callback( + Errors.MissingConfig(`Config requires "${req}"`) + ); } } return callback(null); @@ -51,8 +53,8 @@ exports.getModule = class ArchaicNETModule extends MenuModule { let needRestore = false; //let pipedStream; - const restorePipe = function() { - if(needRestore && !clientTerminated) { + const restorePipe = function () { + if (needRestore && !clientTerminated) { self.client.restoreDataHandler(); needRestore = false; } @@ -61,75 +63,91 @@ exports.getModule = class ArchaicNETModule extends MenuModule { sshClient.on('ready', () => { // track client termination so we can clean up early self.client.once('end', () => { - self.client.log.info('Connection ended. Terminating ArchaicNET connection'); + self.client.log.info( + 'Connection ended. Terminating ArchaicNET connection' + ); clientTerminated = true; return sshClient.end(); }); // establish tunnel for rlogin const fwdPort = self.config.rloginPort + self.client.node; - sshClient.forwardOut('127.0.0.1', fwdPort, self.config.host, self.config.rloginPort, (err, stream) => { - if(err) { - return sshClient.end(); + sshClient.forwardOut( + '127.0.0.1', + fwdPort, + self.config.host, + self.config.rloginPort, + (err, stream) => { + if (err) { + return sshClient.end(); + } + + // + // Send rlogin - [] e.g. [Xibalba]NuSkooler + // + const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; + stream.write(rlogin); + + // we need to filter I/O for escape/de-escaping zmodem and the like + self.client.setTemporaryDirectDataHandler(data => { + const tmp = data + .toString('binary') + .replace(/\xff{2}/g, '\xff'); // de-escape + stream.write(Buffer.from(tmp, 'binary')); + }); + needRestore = true; + + stream.on('data', data => { + const tmp = data + .toString('binary') + .replace(/\xff/g, '\xff\xff'); // escape + self.client.term.rawWrite(Buffer.from(tmp, 'binary')); + }); + + stream.on('close', () => { + restorePipe(); + return sshClient.end(); + }); } - - // - // Send rlogin - [] e.g. [Xibalba]NuSkooler - // - const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`; - stream.write(rlogin); - - // we need to filter I/O for escape/de-escaping zmodem and the like - self.client.setTemporaryDirectDataHandler(data => { - const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape - stream.write(Buffer.from(tmp, 'binary')); - }); - needRestore = true; - - stream.on('data', data => { - const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape - self.client.term.rawWrite(Buffer.from(tmp, 'binary')); - }); - - stream.on('close', () => { - restorePipe(); - return sshClient.end(); - }); - }); + ); }); sshClient.on('error', err => { - return self.client.log.info(`ArchaicNET SSH client error: ${err.message}`); + return self.client.log.info( + `ArchaicNET SSH client error: ${err.message}` + ); }); sshClient.on('close', hadError => { - if(hadError) { + if (hadError) { self.client.warn('Closing ArchaicNET SSH due to error'); } restorePipe(); return callback(null); }); - self.client.log.trace( { host : self.config.host, port : self.config.sshPort }, 'Connecting to ArchaicNET'); - sshClient.connect( { - host : self.config.host, - port : self.config.sshPort, - username : self.config.username, - password : self.config.password, + self.client.log.trace( + { host: self.config.host, port: self.config.sshPort }, + 'Connecting to ArchaicNET' + ); + sshClient.connect({ + host: self.config.host, + port: self.config.sshPort, + username: self.config.username, + password: self.config.password, }); - } + }, ], err => { - if(err) { - self.client.log.warn( { error : err.message }, 'ArchaicNET error'); + if (err) { + self.client.log.warn({ error: err.message }, 'ArchaicNET error'); } // if the client is stil here, go to previous - if(!clientTerminated) { + if (!clientTerminated) { self.prevMenu(); } } ); } }; - diff --git a/core/archive_util.js b/core/archive_util.js index ab4e1dd9..a142584b 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -2,26 +2,26 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const stringFormat = require('./string_format.js'); -const Errors = require('./enig_error.js').Errors; -const resolveMimeType = require('./mime_util.js').resolveMimeType; -const Events = require('./events.js'); +const Config = require('./config.js').get; +const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; +const resolveMimeType = require('./mime_util.js').resolveMimeType; +const Events = require('./events.js'); // base/modules -const fs = require('graceful-fs'); -const _ = require('lodash'); -const pty = require('node-pty'); -const paths = require('path'); +const fs = require('graceful-fs'); +const _ = require('lodash'); +const pty = require('node-pty'); +const paths = require('path'); let archiveUtil; class Archiver { constructor(config) { - this.compress = config.compress; + this.compress = config.compress; this.decompress = config.decompress; - this.list = config.list; - this.extract = config.extract; + this.list = config.list; + this.extract = config.extract; } ok() { @@ -29,21 +29,32 @@ class Archiver { } can(what) { - if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) { + 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; @@ -51,7 +62,7 @@ module.exports = class ArchiveUtil { // singleton access static getInstance(hotReload = true) { - if(!archiveUtil) { + if (!archiveUtil) { archiveUtil = new ArchiveUtil(); archiveUtil.init(hotReload); } @@ -60,7 +71,7 @@ module.exports = class ArchiveUtil { init(hotReload = true) { this.reloadConfig(); - if(hotReload) { + if (hotReload) { Events.on(Events.getSystemEvents().ConfigChanged, () => { this.reloadConfig(); }); @@ -69,13 +80,12 @@ module.exports = class ArchiveUtil { reloadConfig() { const config = Config(); - if(_.has(config, 'archives.archivers')) { + 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()) { + if (!archiver.ok()) { // :TODO: Log warning - bad archiver/config } @@ -83,27 +93,27 @@ module.exports = class ArchiveUtil { }); } - 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) { + if (sigLen > this.longestSignature) { this.longestSignature = sigLen; } }; Object.keys(config.fileTypes).forEach(mimeType => { const fileType = config.fileTypes[mimeType]; - if(Array.isArray(fileType)) { + if (Array.isArray(fileType)) { fileType.forEach(ft => { - if(ft.sig) { + if (ft.sig) { updateSig(ft); } }); - } else if(fileType.sig) { + } else if (fileType.sig) { updateSig(fileType); } }); @@ -113,15 +123,16 @@ module.exports = class ArchiveUtil { getArchiver(mimeTypeOrExtension, justExtention) { const mimeType = resolveMimeType(mimeTypeOrExtension); - if(!mimeType) { // lookup returns false on failure + if (!mimeType) { + // lookup returns false on failure return; } const config = Config(); - let fileType = _.get(config, [ 'fileTypes', mimeType ] ); + let fileType = _.get(config, ['fileTypes', mimeType]); - if(Array.isArray(fileType)) { - if(!justExtention) { + if (Array.isArray(fileType)) { + if (!justExtention) { // need extention for lookup; ambiguous as-is :( return; } @@ -129,12 +140,12 @@ module.exports = class ArchiveUtil { fileType = fileType.find(ft => justExtention === ft.ext); } - if(!_.isObject(fileType)) { + if (!_.isObject(fileType)) { return; } - if(fileType.archiveHandler) { - return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] ); + if (fileType.archiveHandler) { + return _.get(config, ['archives', 'archivers', fileType.archiveHandler]); } } @@ -149,37 +160,41 @@ module.exports = class ArchiveUtil { */ detectType(path, cb) { - const closeFile = (fd) => { - fs.close(fd, () => { /* sadface */ }); + const closeFile = fd => { + fs.close(fd, () => { + /* sadface */ + }); }; fs.open(path, 'r', (err, fd) => { - if(err) { + if (err) { return cb(err); } const buf = Buffer.alloc(this.longestSignature); fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { - if(err) { + if (err) { closeFile(fd); return cb(err); } const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => { - const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ]; + const fileTypeInfos = Array.isArray(fileTypeInfo) + ? fileTypeInfo + : [fileTypeInfo]; return fileTypeInfos.find(fti => { - if(!fti.sig || !fti.archiveHandler) { + if (!fti.sig || !fti.archiveHandler) { return false; } const lenNeeded = fti.offset + fti.sig.length; - if(bytesRead < lenNeeded) { + if (bytesRead < lenNeeded) { return false; } const comp = buf.slice(fti.offset, fti.offset + fti.sig.length); - return (fti.sig.equals(comp)); + return fti.sig.equals(comp); }); }); @@ -194,20 +209,26 @@ module.exports = class ArchiveUtil { // so we have this horrible, horrible hack: let err; proc.once('data', d => { - if(_.isString(d) && d.startsWith('execvp(3) failed.')) { + 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); + return cb( + exitCode + ? Errors.ExternalProcess( + `${action} failed with exit code: ${exitCode}` + ) + : err + ); }); } compressTo(archType, archivePath, files, workDir, cb) { const archiver = this.getArchiver(archType, paths.extname(archivePath)); - if(!archiver) { + if (!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } @@ -217,17 +238,17 @@ module.exports = class ArchiveUtil { } const fmtObj = { - archivePath : archivePath, - fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! + archivePath: archivePath, + fileList: files.join(' '), // :TODO: probably need same hack as extractTo here! }; // :TODO: DRY with extractTo() - const args = archiver.compress.args.map( arg => { + const args = archiver.compress.args.map(arg => { return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); }); const fileListPos = args.indexOf('{fileList}'); - if(fileListPos > -1) { + if (fileListPos > -1) { // replace {fileList} with 0:n sep file list arguments args.splice.apply(args, [fileListPos, 1].concat(files)); } @@ -235,9 +256,13 @@ module.exports = class ArchiveUtil { let proc; try { proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts(workDir)); - } catch(e) { - return cb(Errors.ExternalProcess( - `Error spawning archiver process "${archiver.compress.cmd}" with args "${args.join(' ')}": ${e.message}`) + } catch (e) { + return cb( + Errors.ExternalProcess( + `Error spawning archiver process "${ + archiver.compress.cmd + }" with args "${args.join(' ')}": ${e.message}` + ) ); } @@ -247,7 +272,7 @@ module.exports = class ArchiveUtil { extractTo(archivePath, extractPath, archType, fileList, cb) { let haveFileList; - if(!cb && _.isFunction(fileList)) { + if (!cb && _.isFunction(fileList)) { cb = fileList; fileList = []; haveFileList = false; @@ -257,29 +282,29 @@ module.exports = class ArchiveUtil { const archiver = this.getArchiver(archType, paths.extname(archivePath)); - if(!archiver) { + if (!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } const fmtObj = { - archivePath : archivePath, - extractPath : extractPath, + archivePath: archivePath, + extractPath: extractPath, }; let action = haveFileList ? 'extract' : 'decompress'; - if('extract' === action && !_.isObject(archiver[action])) { + 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 => { + const args = archiver[action].args.map(arg => { return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); }); const fileListPos = args.indexOf('{fileList}'); - if(fileListPos > -1) { + if (fileListPos > -1) { // replace {fileList} with 0:n sep file list arguments args.splice.apply(args, [fileListPos, 1].concat(fileList)); } @@ -287,34 +312,42 @@ module.exports = class ArchiveUtil { let proc; try { proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath)); - } catch(e) { - return cb(Errors.ExternalProcess( - `Error spawning archiver process "${archiver[action].cmd}" with args "${args.join(' ')}": ${e.message}`) + } catch (e) { + return cb( + Errors.ExternalProcess( + `Error spawning archiver process "${ + archiver[action].cmd + }" with args "${args.join(' ')}": ${e.message}` + ) ); } - return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb); + return this.spawnHandler(proc, haveFileList ? 'Extraction' : 'Decompression', cb); } listEntries(archivePath, archType, cb) { const archiver = this.getArchiver(archType, paths.extname(archivePath)); - if(!archiver) { + if (!archiver) { return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); } const fmtObj = { - archivePath : archivePath, + archivePath: archivePath, }; - const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); + const args = archiver.list.args.map(arg => stringFormat(arg, fmtObj)); let proc; try { proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); - } catch(e) { - return cb(Errors.ExternalProcess( - `Error spawning archiver process "${archiver.list.cmd}" with args "${args.join(' ')}": ${e.message}`) + } catch (e) { + return cb( + Errors.ExternalProcess( + `Error spawning archiver process "${ + archiver.list.cmd + }" with args "${args.join(' ')}": ${e.message}` + ) ); } @@ -326,19 +359,24 @@ module.exports = class ArchiveUtil { }); proc.once('exit', exitCode => { - if(exitCode) { - return cb(Errors.ExternalProcess(`List failed with exit code: ${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))) { + while ((m = entryMatchRe.exec(output))) { entries.push({ - byteSize : parseInt(m[entryGroupOrder.byteSize]), - fileName : m[entryGroupOrder.fileName].trim(), + byteSize: parseInt(m[entryGroupOrder.byteSize]), + fileName: m[entryGroupOrder.fileName].trim(), }); } @@ -348,12 +386,12 @@ module.exports = class ArchiveUtil { getPtyOpts(cwd) { const opts = { - name : 'enigma-archiver', - cols : 80, - rows : 24, - env : process.env, + name: 'enigma-archiver', + cols: 80, + rows: 24, + env: process.env, }; - if(cwd) { + if (cwd) { opts.cwd = cwd; } // :TODO: set cwd to supplied temp path if not sepcific extract diff --git a/core/art.js b/core/art.js index 5e77a915..5e46591c 100644 --- a/core/art.js +++ b/core/art.js @@ -2,24 +2,24 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const miscUtil = require('./misc_util.js'); -const ansi = require('./ansi_term.js'); -const aep = require('./ansi_escape_parser.js'); -const sauce = require('./sauce.js'); -const { Errors } = require('./enig_error.js'); +const Config = require('./config.js').get; +const miscUtil = require('./misc_util.js'); +const ansi = require('./ansi_term.js'); +const aep = require('./ansi_escape_parser.js'); +const sauce = require('./sauce.js'); +const { Errors } = require('./enig_error.js'); // deps -const fs = require('graceful-fs'); -const paths = require('path'); -const assert = require('assert'); -const iconv = require('iconv-lite'); -const _ = require('lodash'); +const fs = require('graceful-fs'); +const paths = require('path'); +const assert = require('assert'); +const iconv = require('iconv-lite'); +const _ = require('lodash'); -exports.getArt = getArt; -exports.getArtFromPath = getArtFromPath; -exports.display = display; -exports.defaultEncodingFromExtension = defaultEncodingFromExtension; +exports.getArt = getArt; +exports.getArtFromPath = getArtFromPath; +exports.display = display; +exports.defaultEncodingFromExtension = defaultEncodingFromExtension; // :TODO: Return MCI code information // :TODO: process SAUCE comments @@ -28,37 +28,37 @@ exports.defaultEncodingFromExtension = defaultEncodingFromExtension; 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 }, + '.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 }, + '.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) { + 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]) { + for (let i = eof - 1; i > stopPos; i--) { + if (eofMarker === data[i]) { eof = i; break; } } if (eof === data.length) { - return data; // nothing to do + return data; // nothing to do } // try to prevent goofs @@ -71,43 +71,46 @@ function sliceAtEOF(data, eofMarker) { function getArtFromPath(path, options, cb) { fs.readFile(path, (err, data) => { - if(err) { + if (err) { return cb(err); } // // Convert from encodedAs -> j // - const ext = paths.extname(path).toLowerCase(); - const encoding = options.encodedAs || defaultEncodingFromExtension(ext); + const ext = paths.extname(path).toLowerCase(); + const encoding = options.encodedAs || defaultEncodingFromExtension(ext); // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? function sliceOfData() { - if(options.fullFile === true) { + if (options.fullFile === true) { return iconv.decode(data, encoding); } else { const eofMarker = defaultEofFromExtension(ext); - return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding); + return iconv.decode( + eofMarker ? sliceAtEOF(data, eofMarker) : data, + encoding + ); } } function getResult(sauce) { const result = { - data : sliceOfData(), - fromPath : path, + data: sliceOfData(), + fromPath: path, }; - if(sauce) { + if (sauce) { result.sauce = sauce; } return result; } - if(options.readSauce === true) { + if (options.readSauce === true) { sauce.readSAUCE(data, (err, sauce) => { - if(err) { + if (err) { return cb(null, getResult()); } @@ -115,7 +118,7 @@ function getArtFromPath(path, options, cb) { // If a encoding was not provided & we have a mapping from // the information provided by SAUCE, use that. // - if(!options.encodedAs) { + if (!options.encodedAs) { /* if(sauce.Character && sauce.Character.fontName) { var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; @@ -136,56 +139,58 @@ function getArtFromPath(path, options, cb) { function getArt(name, options, cb) { const ext = paths.extname(name); - options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); - options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); + options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); + options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); // :TODO: make use of asAnsi option and convert from supported -> ansi - if('' !== ext) { - options.types = [ ext.toLowerCase() ]; + if ('' !== ext) { + options.types = [ext.toLowerCase()]; } else { - if(_.isUndefined(options.types)) { + if (_.isUndefined(options.types)) { options.types = Object.keys(SUPPORTED_ART_TYPES); - } else if(_.isString(options.types)) { - options.types = [ options.types.toLowerCase() ]; + } 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.isAbsolute(name) ? name : paths.join(options.basePath, name); + if ('' !== ext) { + const directPath = paths.isAbsolute(name) + ? name + : paths.join(options.basePath, name); return getArtFromPath(directPath, options, cb); } fs.readdir(options.basePath, (err, files) => { - if(err) { + if (err) { return cb(err); } - const filtered = files.filter( file => { + const filtered = files.filter(file => { // // Ignore anything not allowed in |options.types| // const fext = paths.extname(file); - if(!options.types.includes(fext.toLowerCase())) { + if (!options.types.includes(fext.toLowerCase())) { return false; } const bn = paths.basename(file, fext).toLowerCase(); - if(options.random) { + 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)) { + if (!bn.startsWith(suppliedBn)) { return false; } const num = bn.substr(suppliedBn.length); - if(num.length > 0) { - if(isNaN(parseInt(num, 10))) { + if (num.length > 0) { + if (isNaN(parseInt(num, 10))) { return false; } } @@ -194,7 +199,7 @@ function getArt(name, options, cb) { // We've already validated the extension (above). Must be an exact // match to basename here // - if(bn != paths.basename(name, fext).toLowerCase()) { + if (bn != paths.basename(name, fext).toLowerCase()) { return false; } } @@ -202,15 +207,18 @@ function getArt(name, options, cb) { return true; }); - if(filtered.length > 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)]); + 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]); @@ -230,7 +238,7 @@ function defaultEncodingFromExtension(ext) { function defaultEofFromExtension(ext) { const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; - if(artType) { + if (artType) { return artType.eof; } } @@ -240,12 +248,12 @@ function defaultEofFromExtension(ext) { // * Cancel (disabled | ) // * Resume from pause -> continous (disabled | ) function display(client, art, options, cb) { - if(_.isFunction(options) && !cb) { + if (_.isFunction(options) && !cb) { cb = options; options = {}; } - if(!art || !art.length) { + if (!art || !art.length) { return cb(Errors.Invalid('No art supplied!')); } @@ -255,19 +263,19 @@ function display(client, art, options, cb) { // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. // 2) CPR driven - if(!_.isBoolean(options.iceColors)) { + if (!_.isBoolean(options.iceColors)) { // try to detect from SAUCE - if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { + 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, - startRow : options.startRow, + mciReplaceChar: options.mciReplaceChar, + termHeight: client.term.termHeight, + termWidth: client.term.termWidth, + trailingLF: options.trailingLF, + startRow: options.startRow, }); const mciMap = {}; @@ -275,37 +283,36 @@ function display(client, art, options, cb) { 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]; + 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; + if (mapEntry) { + mapEntry.focusSGR = mciInfo.SGR; + mapEntry.focusArgs = mciInfo.args; } else { mciMap[mapKey] = { - position : mciInfo.position, - args : mciInfo.args, - SGR : mciInfo.SGR, - code : mciInfo.mci, - id : id, + position: mciInfo.position, + args: mciInfo.args, + SGR: mciInfo.SGR, + code: mciInfo.mci, + id: id, }; - if(!mciInfo.id) { + if (!mciInfo.id) { ++generatedId; } } - }); - 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', () => { ansiParser.removeAllListeners(); const extraInfo = { - height : ansiParser.row - 1, + height: ansiParser.row - 1, }; return cb(null, mciMap, extraInfo); @@ -313,11 +320,11 @@ function display(client, art, options, cb) { let initSeq = ''; if (client.term.syncTermFontsEnabled) { - if(options.font) { + if (options.font) { initSeq = ansi.setSyncTermFontWithAlias(options.font); - } else if(options.sauce) { + } else if (options.sauce) { let fontName = getFontNameFromSAUCE(options.sauce); - if(fontName) { + if (fontName) { fontName = ansi.getSyncTermFontFromAlias(fontName); } @@ -327,18 +334,18 @@ function display(client, art, options, cb) { // 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) { + if (fontName && client.term.currentSyncFont != fontName) { client.term.currentSyncFont = fontName; initSeq = ansi.setSyncTermFont(fontName); } } } - if(options.iceColors) { + if (options.iceColors) { initSeq += ansi.blinkToBrightIntensity(); } - if(initSeq) { + if (initSeq) { client.term.rawWrite(initSeq); } diff --git a/core/asset.js b/core/asset.js index b3d62154..21cd1925 100644 --- a/core/asset.js +++ b/core/asset.js @@ -2,20 +2,20 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const StatLog = require('./stat_log.js'); +const Config = require('./config.js').get; +const StatLog = require('./stat_log.js'); // deps -const _ = require('lodash'); -const assert = require('assert'); +const _ = require('lodash'); +const assert = require('assert'); -exports.parseAsset = parseAsset; -exports.getAssetWithShorthand = getAssetWithShorthand; -exports.getArtAsset = getArtAsset; -exports.getModuleAsset = getModuleAsset; -exports.resolveConfigAsset = resolveConfigAsset; -exports.resolveSystemStatAsset = resolveSystemStatAsset; -exports.getViewPropertyAsset = getViewPropertyAsset; +exports.parseAsset = parseAsset; +exports.getAssetWithShorthand = getAssetWithShorthand; +exports.getArtAsset = getArtAsset; +exports.getModuleAsset = getModuleAsset; +exports.resolveConfigAsset = resolveConfigAsset; +exports.resolveSystemStatAsset = resolveSystemStatAsset; +exports.getViewPropertyAsset = getViewPropertyAsset; const ALL_ASSETS = [ 'art', @@ -30,18 +30,17 @@ const ALL_ASSETS = [ ]; const ASSET_RE = new RegExp( - '^@(' + ALL_ASSETS.join('|') + ')' + - /:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source + '^@(' + ALL_ASSETS.join('|') + ')' + /:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source ); function parseAsset(s) { const m = ASSET_RE.exec(s); - if(m) { - const result = { type : m[1] }; + if (m) { + const result = { type: m[1] }; - if(m[3]) { + if (m[3]) { result.asset = m[3]; - if(m[2]) { + if (m[2]) { result.location = m[2]; } } else { @@ -53,11 +52,11 @@ function parseAsset(s) { } function getAssetWithShorthand(spec, defaultType) { - if(!_.isString(spec)) { + if (!_.isString(spec)) { return null; } - if('@' === spec[0]) { + if ('@' === spec[0]) { const asset = parseAsset(spec); assert(_.isString(asset.type)); @@ -65,43 +64,43 @@ function getAssetWithShorthand(spec, defaultType) { } return { - type : defaultType, - asset : spec, + type: defaultType, + asset: spec, }; } function getArtAsset(spec) { const asset = getAssetWithShorthand(spec, 'art'); - if(!asset) { + if (!asset) { return null; } - assert( ['art', 'method' ].indexOf(asset.type) > -1); + assert(['art', 'method'].indexOf(asset.type) > -1); return asset; } function getModuleAsset(spec) { const asset = getAssetWithShorthand(spec, 'systemModule'); - if(!asset) { + if (!asset) { return null; } - assert( ['userModule', 'systemModule' ].includes(asset.type) ); + assert(['userModule', 'systemModule'].includes(asset.type)); return asset; } function resolveConfigAsset(spec) { const asset = parseAsset(spec); - if(asset) { + 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]])) { + 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]]; @@ -114,7 +113,7 @@ function resolveConfigAsset(spec) { function resolveSystemStatAsset(spec) { const asset = parseAsset(spec); - if(!asset) { + if (!asset) { return spec; } @@ -124,7 +123,7 @@ function resolveSystemStatAsset(spec) { } function getViewPropertyAsset(src) { - if(!_.isString(src) || '@' !== src.charAt(0)) { + if (!_.isString(src) || '@' !== src.charAt(0)) { return null; } diff --git a/core/autosig_edit.js b/core/autosig_edit.js index c9995280..4351e887 100644 --- a/core/autosig_edit.js +++ b/core/autosig_edit.js @@ -2,60 +2,68 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const UserProps = require('./user_property.js'); +const { MenuModule } = require('./menu_module.js'); +const UserProps = require('./user_property.js'); // deps -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'User Auto-Sig Editor', - desc : 'Module for editing auto-sigs', - author : 'NuSkooler', + name: 'User Auto-Sig Editor', + desc: 'Module for editing auto-sigs', + author: 'NuSkooler', }; const FormIds = { - edit : 0, + edit: 0, }; const MciViewIds = { - editor : 1, - save : 2, + editor: 1, + save: 2, }; exports.getModule = class UserAutoSigEditorModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { + extraArgs: options.extraArgs, + }); this.menuMethods = { - saveChanges : (formData, extraArgs, cb) => { + saveChanges: (formData, extraArgs, cb) => { return this.saveChanges(cb); - } + }, }; } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (callback) => { - return this.prepViewController('edit', FormIds.edit, mciData.menu, callback); + callback => { + return this.prepViewController( + 'edit', + FormIds.edit, + mciData.menu, + callback + ); }, - (callback) => { - const requiredCodes = [ MciViewIds.editor, MciViewIds.save ]; + callback => { + const requiredCodes = [MciViewIds.editor, MciViewIds.save]; return this.validateMCIByViewIds('edit', requiredCodes, callback); }, - (callback) => { - const sig = this.client.user.getProperty(UserProps.AutoSignature) || ''; + callback => { + const sig = + this.client.user.getProperty(UserProps.AutoSignature) || ''; this.setViewText('edit', MciViewIds.editor, sig); return callback(null); - } + }, ], err => { return cb(err); @@ -67,8 +75,8 @@ exports.getModule = class UserAutoSigEditorModule extends MenuModule { saveChanges(cb) { const sig = this.getView('edit', MciViewIds.editor).getData().trim(); this.client.user.persistProperty(UserProps.AutoSignature, sig, err => { - if(err) { - this.client.log.error( { error : err.message }, 'Could not save auto-sig'); + if (err) { + this.client.log.error({ error: err.message }, 'Could not save auto-sig'); } return this.prevMenu(cb); }); diff --git a/core/bbs.js b/core/bbs.js index 29d4f219..b1e1a670 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -6,35 +6,36 @@ //SegfaultHandler.registerHandler('enigma-bbs-segfault.log'); // ENiGMA½ -const conf = require('./config.js'); -const logger = require('./logger.js'); -const database = require('./database.js'); -const resolvePath = require('./misc_util.js').resolvePath; -const UserProps = require('./user_property.js'); -const SysProps = require('./system_property.js'); -const SysLogKeys = require('./system_log.js'); +const conf = require('./config.js'); +const logger = require('./logger.js'); +const database = require('./database.js'); +const resolvePath = require('./misc_util.js').resolvePath; +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); +const SysLogKeys = require('./system_log.js'); // deps -const async = require('async'); -const util = require('util'); -const _ = require('lodash'); -const mkdirs = require('fs-extra').mkdirs; -const fs = require('graceful-fs'); -const paths = require('path'); -const moment = require('moment'); +const async = require('async'); +const util = require('util'); +const _ = require('lodash'); +const mkdirs = require('fs-extra').mkdirs; +const fs = require('graceful-fs'); +const paths = require('path'); +const moment = require('moment'); // our main entry point -exports.main = main; +exports.main = main; // object with various services we want to de-init/shutdown cleanly if possible const initServices = {}; // only include bbs.js once @ startup; this should be fine -const COPYRIGHT = fs.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8').split(/\r?\n/g)[0]; +const COPYRIGHT = fs + .readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8') + .split(/\r?\n/g)[0]; -const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`; -const HELP = -`${FULL_COPYRIGHT} +const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`; +const HELP = `${FULL_COPYRIGHT} usage: main.js eg : main.js --config /enigma_install_path/config/ @@ -61,17 +62,21 @@ function main() { function processArgs(callback) { const argv = require('minimist')(process.argv.slice(2)); - if(argv.help) { + if (argv.help) { return printHelpAndExit(); } - if(argv.version) { + if (argv.version) { return printVersionAndExit(); } const configOverridePath = argv.config; - return callback(null, configOverridePath || conf.Config.getDefaultPath(), _.isString(configOverridePath)); + return callback( + null, + configOverridePath || conf.Config.getDefaultPath(), + _.isString(configOverridePath) + ); }, function initConfig(configPath, configPathSupplied, callback) { const configFile = configPath + 'config.hjson'; @@ -81,12 +86,14 @@ function main() { // 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); + if (err) { + if ('ENOENT' === err.code) { + if (configPathSupplied) { + console.error( + 'Configuration file does not exist: ' + configFile + ); } else { - configPathSupplied = null; // make non-fatal; we'll go with defaults + configPathSupplied = null; // make non-fatal; we'll go with defaults } } else { errorDisplayed = true; @@ -104,26 +111,30 @@ function main() { }, function initSystem(callback) { initialize(function init(err) { - if(err) { + if (err) { console.error('Error initializing: ' + util.inspect(err)); } return callback(err); }); - } + }, ], function complete(err) { - if(!err) { + if (!err) { // note this is escaped: - fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { - console.info(FULL_COPYRIGHT); - if(!err) { - console.info(banner); + 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!'); } - console.info('System started!'); - }); + ); } - if(err && !errorDisplayed) { + if (err && !errorDisplayed) { console.error('Error initializing: ' + util.inspect(err)); return process.exit(); } @@ -142,37 +153,39 @@ function shutdownSystem() { const ClientConns = require('./client_connections.js'); const activeConnections = ClientConns.getActiveConnections(); let i = activeConnections.length; - while(i--) { + while (i--) { const activeTerm = activeConnections[i].term; - if(activeTerm) { - activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); + 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 + 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 + 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 + require('./file_area_web.js').startup(() => { + return callback(null); // ignore err }); }, function stopMsgNetwork(callback) { require('./msg_network.js').shutdown(callback); - } + }, ], () => { console.info('Goodbye!'); @@ -186,30 +199,39 @@ function initialize(cb) { [ function createMissingDirectories(callback) { const Config = conf.get(); - async.each(Object.keys(Config.paths), function entry(pathKey, next) { - mkdirs(Config.paths[pathKey], function dirCreated(err) { - if(err) { - console.error('Could not create path: ' + Config.paths[pathKey] + ': ' + err.toString()); - } - return next(err); - }); - }, function dirCreationComplete(err) { - return callback(err); - }); + async.each( + Object.keys(Config.paths), + function entry(pathKey, next) { + mkdirs(Config.paths[pathKey], function dirCreated(err) { + if (err) { + console.error( + 'Could not create path: ' + + 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, - nodeVersion : process.version, + version: require('../package.json').version, + nodeVersion: process.version, }, '**** ENiGMA½ Bulletin Board System Starting Up! ****' ); process.on('SIGINT', shutdownSystem); - require('@breejs/later').date.localTime(); // use local times for later.js/scheduling + require('@breejs/later').date.localTime(); // use local times for later.js/scheduling return callback(null); }, @@ -236,9 +258,12 @@ function initialize(cb) { const User = require('./user.js'); const propLoadOpts = { - names : [ - UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, - UserProps.Location, UserProps.Affiliations, + names: [ + UserProps.RealName, + UserProps.Sex, + UserProps.EmailAddress, + UserProps.Location, + UserProps.Affiliations, ], }; @@ -248,15 +273,19 @@ function initialize(cb) { return User.getUserName(1, next); }, function getOpProps(opUserName, next) { - User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { - return next(err, opUserName, opProps); - }); + User.loadProperties( + User.RootUserID, + propLoadOpts, + (err, opProps) => { + return next(err, opUserName, opProps); + } + ); }, ], (err, opUserName, opProps) => { const StatLog = require('./stat_log.js'); - if(err) { + if (err) { propLoadOpts.names.concat('username').forEach(v => { StatLog.setNonPersistentSystemStat(`sysop_${v}`, 'N/A'); }); @@ -275,14 +304,17 @@ function initialize(cb) { function initCallsToday(callback) { const StatLog = require('./stat_log.js'); const filter = { - logName : SysLogKeys.UserLoginHistory, - resultType : 'count', - date : moment(), + logName: SysLogKeys.UserLoginHistory, + resultType: 'count', + date: moment(), }; StatLog.findSystemLogEntries(filter, (err, callsToday) => { - if(!err) { - StatLog.setNonPersistentSystemStat(SysProps.LoginsToday, callsToday); + if (!err) { + StatLog.setNonPersistentSystemStat( + SysProps.LoginsToday, + callsToday + ); } return callback(null); }); @@ -312,7 +344,8 @@ function initialize(cb) { return require('./file_area_web.js').startup(callback); }, function readyPasswordReset(callback) { - const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; + const WebPasswordReset = + require('./web_password_reset.js').WebPasswordReset; return WebPasswordReset.startup(callback); }, function ready2FA_OTPRegister(callback) { @@ -320,15 +353,16 @@ function initialize(cb) { return User2FA_OTPWebRegister.startup(callback); }, function readyEventScheduler(callback) { - const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; - EventSchedulerModule.loadAndStart( (err, modInst) => { + const EventSchedulerModule = + require('./event_scheduler.js').EventSchedulerModule; + EventSchedulerModule.loadAndStart((err, modInst) => { initServices.eventScheduler = modInst; return callback(err); }); }, function listenUserEventsForStatLog(callback) { return require('./stat_log.js').initUserEvents(callback); - } + }, ], function onComplete(err) { return cb(err); diff --git a/core/bbs_link.js b/core/bbs_link.js index 127f85db..b5258123 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -1,21 +1,18 @@ /* jslint node: true */ 'use strict'; -const { MenuModule } = require('./menu_module.js'); -const { resetScreen } = require('./ansi_term.js'); -const { Errors } = require('./enig_error.js'); -const { - trackDoorRunBegin, - trackDoorRunEnd -} = require('./door_util.js'); +const { MenuModule } = require('./menu_module.js'); +const { resetScreen } = require('./ansi_term.js'); +const { Errors } = require('./enig_error.js'); +const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js'); // deps -const async = require('async'); -const http = require('http'); -const net = require('net'); -const crypto = require('crypto'); +const async = require('async'); +const http = require('http'); +const net = require('net'); +const crypto = require('crypto'); -const packageJson = require('../package.json'); +const packageJson = require('../package.json'); /* Expected configuration block: @@ -42,18 +39,18 @@ 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); - 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() { @@ -67,12 +64,12 @@ exports.getModule = class BBSLinkModule extends MenuModule { function validateConfig(callback) { return self.validateConfigFields( { - host : 'string', - sysCode : 'string', - authCode : 'string', - schemeCode : 'string', - door : 'string', - port : 'number', + host: 'string', + sysCode: 'string', + authCode: 'string', + schemeCode: 'string', + door: 'string', + port: 'number', }, callback ); @@ -82,19 +79,26 @@ exports.getModule = class BBSLinkModule extends MenuModule { // Acquire an authentication token // crypto.randomBytes(16, function rand(ex, buf) { - if(ex) { + 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); + 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); + } } - }); + ); } }); }, @@ -103,26 +107,40 @@ exports.getModule = class BBSLinkModule extends MenuModule { // Authenticate the token we acquired previously // const headers = { - 'X-User' : self.client.user.userId.toString(), - 'X-System' : self.config.sysCode, - 'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), - 'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), - 'X-Rows' : self.client.term.termHeight.toString(), - 'X-Key' : randomKey, - 'X-Door' : self.config.door, - 'X-Token' : token, - 'X-Type' : 'enigma-bbs', - 'X-Version' : packageJson.version, + 'X-User': self.client.user.userId.toString(), + 'X-System': self.config.sysCode, + 'X-Auth': crypto + .createHash('md5') + .update(self.config.authCode + token) + .digest('hex'), + 'X-Code': crypto + .createHash('md5') + .update(self.config.schemeCode + token) + .digest('hex'), + 'X-Rows': self.client.term.termHeight.toString(), + 'X-Key': randomKey, + 'X-Door': self.config.door, + 'X-Token': token, + 'X-Type': 'enigma-bbs', + 'X-Version': packageJson.version, }; - self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { - var status = body.trim(); + self.simpleHttpRequest( + '/auth.php?key=' + randomKey, + headers, + function resp(err, body) { + var status = body.trim(); - if('complete' === status) { - return callback(null); + if ('complete' === status) { + return callback(null); + } + return callback( + Errors.AccessDenied( + `Bad authentication status: ${status}` + ) + ); } - return callback(Errors.AccessDenied(`Bad authentication status: ${status}`)); - }); + ); }, function createTelnetBridge(callback) { // @@ -130,35 +148,48 @@ exports.getModule = class BBSLinkModule extends MenuModule { // bridge from us to them // const connectOpts = { - port : self.config.port, - host : self.config.host, + port: self.config.port, + host: self.config.host, }; let dataOut; self.client.term.write(resetScreen()); - self.client.term.write(` Connecting to ${self.config.host}, please wait...\n`); + self.client.term.write( + ` Connecting to ${self.config.host}, please wait...\n` + ); - const doorTracking = trackDoorRunBegin(self.client, `bbslink_${self.config.door}`); + const doorTracking = trackDoorRunBegin( + self.client, + `bbslink_${self.config.door}` + ); - const bridgeConnection = net.createConnection(connectOpts, function connected() { - self.client.log.info(connectOpts, 'BBSLink bridge connection established'); + const bridgeConnection = net.createConnection( + connectOpts, + function connected() { + self.client.log.info( + connectOpts, + 'BBSLink bridge connection established' + ); - dataOut = (data) => { - return bridgeConnection.write(data); - }; + dataOut = data => { + return bridgeConnection.write(data); + }; - self.client.term.output.on('data', dataOut); + self.client.term.output.on('data', dataOut); - 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(); + }); + } + ); const restore = () => { - if(dataOut && self.client.term.output) { + if (dataOut && self.client.term.output) { self.client.term.output.removeListener('data', dataOut); dataOut = null; } @@ -174,22 +205,31 @@ exports.getModule = class BBSLinkModule extends MenuModule { bridgeConnection.on('end', function connectionEnd() { restore(); - return callback(clientTerminated ? Errors.General('Client connection terminated') : null); + return callback( + clientTerminated + ? Errors.General('Client connection terminated') + : null + ); }); bridgeConnection.on('error', function error(err) { - self.client.log.info('BBSLink bridge connection error: ' + err.message); + self.client.log.info( + 'BBSLink bridge connection error: ' + err.message + ); restore(); return callback(err); }); - } + }, ], function complete(err) { - if(err) { - self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); + if (err) { + self.client.log.warn( + { error: err.toString() }, + 'BBSLink connection error' + ); } - if(!clientTerminated) { + if (!clientTerminated) { self.prevMenu(); } } @@ -198,9 +238,9 @@ exports.getModule = class BBSLinkModule extends MenuModule { simpleHttpRequest(path, headers, cb) { const getOpts = { - host : this.config.host, - path : path, - headers : headers, + host: this.config.host, + path: path, + headers: headers, }; const req = http.get(getOpts, function response(resp) { diff --git a/core/bbs_list.js b/core/bbs_list.js index 82943a80..f87349fd 100644 --- a/core/bbs_list.js +++ b/core/bbs_list.js @@ -2,72 +2,69 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; +const MenuModule = require('./menu_module.js').MenuModule; -const { - getModDatabasePath, - getTransactionDatabase -} = require('./database.js'); +const { getModDatabasePath, getTransactionDatabase } = require('./database.js'); -const ViewController = require('./view_controller.js').ViewController; -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const User = require('./user.js'); -const stringFormat = require('./string_format.js'); +const ViewController = require('./view_controller.js').ViewController; +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const User = require('./user.js'); +const stringFormat = require('./string_format.js'); // deps -const async = require('async'); -const sqlite3 = require('sqlite3'); -const _ = require('lodash'); +const async = require('async'); +const sqlite3 = require('sqlite3'); +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' -}; +const moduleInfo = (exports.moduleInfo = { + 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, + 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, }, - 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 { @@ -79,10 +76,10 @@ exports.getModule = class BBSListModule extends MenuModule { // // Validators // - viewValidationListener : function(err, cb) { + viewValidationListener: function (err, cb) { const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); - if(errMsgView) { - if(err) { + if (errMsgView) { + if (err) { errMsgView.setText(err.message); } else { errMsgView.clearText(); @@ -95,39 +92,48 @@ exports.getModule = class BBSListModule extends MenuModule { // // Key & submit handlers // - addBBS : function(formData, extraArgs, cb) { + addBBS: function (formData, extraArgs, cb) { self.displayAddScreen(cb); }, - deleteBBS : function(formData, extraArgs, cb) { - if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) { + 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()) { + 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) { + if (!entry) { return cb(null); } self.database.run( `DELETE FROM bbs_list WHERE id=?;`, - [ entry.id ], + [entry.id], err => { if (err) { - self.client.log.error( { err : err }, 'Error deleting from BBS list'); + self.client.log.error( + { err: err }, + 'Error deleting from BBS list' + ); } else { self.entries.splice(self.selectedBBS, 1); self.setEntries(entriesView); - if(self.entries.length > 0) { + if (self.entries.length > 0) { entriesView.focusPrevious(); } @@ -138,15 +144,19 @@ exports.getModule = class BBSListModule extends MenuModule { } ); }, - submitBBS : function(formData, extraArgs, cb) { - + submitBBS: function (formData, extraArgs, cb) { let ok = true; - [ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => { - if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) { + ['BBSName', 'Sysop', 'Telnet'].forEach(mciName => { + if ( + '' === + self.viewControllers.add + .getView(MciViewIds.add[mciName]) + .getData() + ) { ok = false; } }); - if(!ok) { + if (!ok) { // validators should prevent this! return cb(null); } @@ -155,12 +165,21 @@ exports.getModule = class BBSListModule extends MenuModule { `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, [ - formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, - formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes + formData.value.name, + formData.value.sysop, + formData.value.telnet, + formData.value.www, + formData.value.location, + formData.value.software, + self.client.user.userId, + formData.value.notes, ], err => { - if(err) { - self.client.log.error( { err : err }, 'Error adding to BBS list'); + if (err) { + self.client.log.error( + { err: err }, + 'Error adding to BBS list' + ); } self.clearAddForm(); @@ -168,10 +187,10 @@ exports.getModule = class BBSListModule extends MenuModule { } ); }, - cancelSubmit : function(formData, extraArgs, cb) { + cancelSubmit: function (formData, extraArgs, cb) { self.clearAddForm(); self.displayBBSList(true, cb); - } + }, }; } @@ -184,10 +203,10 @@ exports.getModule = class BBSListModule extends MenuModule { }, function display(callback) { self.displayBBSList(false, callback); - } + }, ], err => { - if(err) { + if (err) { // :TODO: Handle me -- initSequence() should really take a completion callback } self.finishedLoading(); @@ -196,21 +215,28 @@ exports.getModule = class BBSListModule extends MenuModule { } drawSelectedEntry(entry) { - if(!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!)'; + 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]) { - - if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) { - this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry)); + 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); + this.setViewText('view', MciViewIds.view[mciName], t); } } }); @@ -227,7 +253,7 @@ exports.getModule = class BBSListModule extends MenuModule { async.waterfall( [ function clearAndDisplayArt(callback) { - if(self.viewControllers.add) { + if (self.viewControllers.add) { self.viewControllers.add.setFocus(false); } if (clearScreen) { @@ -236,34 +262,41 @@ exports.getModule = class BBSListModule extends MenuModule { theme.displayThemedAsset( self.menuConfig.config.art.entries, self.client, - { font : self.menuConfig.font, trailingLF : false }, + { font: self.menuConfig.font, trailingLF: false }, (err, artData) => { return callback(err, artData); } ); }, function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { + if (_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( 'view', - new ViewController( { client : self.client, formId : FormIds.View } ) + new ViewController({ + client: self.client, + formId: FormIds.View, + }) ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, + 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(); + self.viewControllers.view + .getView(MciViewIds.view.BBSList) + .redraw(); return callback(null); } }, function fetchEntries(callback) { - const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); + const entriesView = self.viewControllers.view.getView( + MciViewIds.view.BBSList + ); self.entries = []; self.database.each( @@ -272,16 +305,16 @@ exports.getModule = class BBSListModule extends MenuModule { (err, row) => { if (!err) { self.entries.push({ - text : row.bbs_name, // standard field - id : row.id, - bbsName : row.bbs_name, - sysOp : row.sysop, - telnet : row.telnet, - www : row.www, - location : row.location, - software : row.software, - submitterUserId : row.submitter_user_id, - notes : row.notes, + text: row.bbs_name, // standard field + 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, }); } }, @@ -291,18 +324,22 @@ exports.getModule = class BBSListModule extends MenuModule { ); }, 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); - }); + 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); @@ -312,7 +349,7 @@ exports.getModule = class BBSListModule extends MenuModule { self.drawSelectedEntry(entry); - if(!entry) { + if (!entry) { self.selectedBBS = -1; } else { self.selectedBBS = idx; @@ -331,10 +368,10 @@ exports.getModule = class BBSListModule extends MenuModule { entriesView.redraw(); return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -353,23 +390,26 @@ exports.getModule = class BBSListModule extends MenuModule { theme.displayThemedAsset( self.menuConfig.config.art.add, self.client, - { font : self.menuConfig.font }, + { font: self.menuConfig.font }, (err, artData) => { return callback(err, artData); } ); }, function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { + if (_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( 'add', - new ViewController( { client : self.client, formId : FormIds.Add } ) + new ViewController({ + client: self.client, + formId: FormIds.Add, + }) ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, + callingMenu: self, + mciMap: artData.mciMap, + formId: FormIds.Add, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -379,10 +419,10 @@ exports.getModule = class BBSListModule extends MenuModule { self.viewControllers.add.switchFocus(MciViewIds.add.BBSName); return callback(null); } - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -390,7 +430,16 @@ exports.getModule = class BBSListModule extends MenuModule { } clearAddForm() { - [ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => { + [ + 'BBSName', + 'Sysop', + 'Telnet', + 'Www', + 'Location', + 'Software', + 'Error', + 'Notes', + ].forEach(mciName => { this.setViewText('add', MciViewIds.add[mciName], ''); }); } @@ -401,13 +450,12 @@ exports.getModule = class BBSListModule extends MenuModule { async.series( [ function openDatabase(callback) { - self.database = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(moduleInfo), - callback - )); + self.database = getTransactionDatabase( + new sqlite3.Database(getModDatabasePath(moduleInfo), callback) + ); }, function createTables(callback) { - self.database.serialize( () => { + self.database.serialize(() => { self.database.run( `CREATE TABLE IF NOT EXISTS bbs_list ( id INTEGER PRIMARY KEY, @@ -423,7 +471,7 @@ exports.getModule = class BBSListModule extends MenuModule { ); }); callback(null); - } + }, ], err => { return cb(err); diff --git a/core/button_view.js b/core/button_view.js index 2aebf347..31fb7dc3 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -1,17 +1,17 @@ /* jslint node: true */ 'use strict'; -const TextView = require('./text_view.js').TextView; -const miscUtil = require('./misc_util.js'); -const util = require('util'); +const TextView = require('./text_view.js').TextView; +const miscUtil = require('./misc_util.js'); +const util = require('util'); -exports.ButtonView = ButtonView; +exports.ButtonView = ButtonView; function ButtonView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.justify = miscUtil.valueWithDefault(options.justify, 'center'); - options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.justify = miscUtil.valueWithDefault(options.justify, 'center'); + options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); TextView.call(this, options); @@ -20,8 +20,8 @@ function ButtonView(options) { util.inherits(ButtonView, TextView); -ButtonView.prototype.onKeyPress = function(ch, key) { - if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) { +ButtonView.prototype.onKeyPress = function (ch, key) { + if (this.isKeyMapped('accept', key ? key.name : ch) || ' ' === ch) { this.submitData = 'accept'; this.emit('action', 'accept'); delete this.submitData; @@ -30,6 +30,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) { } }; -ButtonView.prototype.getData = function() { +ButtonView.prototype.getData = function () { return this.submitData || null; }; diff --git a/core/client.js b/core/client.js index 9e0e2f82..3d900b66 100644 --- a/core/client.js +++ b/core/client.js @@ -32,22 +32,22 @@ ----/snip/---------------------- */ // ENiGMA½ -const term = require('./client_term.js'); -const ansi = require('./ansi_term.js'); -const User = require('./user.js'); -const Config = require('./config.js').get; -const MenuStack = require('./menu_stack.js'); -const ACS = require('./acs.js'); -const Events = require('./events.js'); -const UserInterruptQueue = require('./user_interrupt_queue.js'); -const UserProps = require('./user_property.js'); +const term = require('./client_term.js'); +const ansi = require('./ansi_term.js'); +const User = require('./user.js'); +const Config = require('./config.js').get; +const MenuStack = require('./menu_stack.js'); +const ACS = require('./acs.js'); +const Events = require('./events.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); +const UserProps = require('./user_property.js'); // deps -const stream = require('stream'); -const assert = require('assert'); -const _ = require('lodash'); +const stream = require('stream'); +const assert = require('assert'); +const _ = require('lodash'); -exports.Client = Client; +exports.Client = Client; // :TODO: Move all of the key stuff to it's own module @@ -56,86 +56,93 @@ exports.Client = Client; // * http://www.ansi-bbs.org/ansi-bbs-core-server.html // /* eslint-disable no-control-regex */ -const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; +const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; -const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; -const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); -const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ - '(\\d+)(?:;(\\d+))?([~^$])', - '(?:M([@ #!a`])(.)(.))', // mouse stuff - '(?:1;)?(\\d+)?([a-zA-Z@])' -].join('|') + ')'); +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@])', + ].join('|') + + ')' +); /* eslint-enable no-control-regex */ -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 // eslint-disable-line no-control-regex -].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, // eslint-disable-line no-control-regex + ].join('|') +); function Client(/*input, output*/) { stream.call(this); - const self = this; + const self = this; - this.user = new User(); - this.currentThemeConfig = { info : { name : 'N/A', description : 'None' } }; - this.lastActivityTime = Date.now(); - this.menuStack = new MenuStack(this); - this.acs = new ACS( { client : this, user : this.user } ); - this.interruptQueue = new UserInterruptQueue(this); + this.user = new User(); + this.currentThemeConfig = { info: { name: 'N/A', description: 'None' } }; + this.lastActivityTime = Date.now(); + this.menuStack = new MenuStack(this); + this.acs = new ACS({ client: this, user: this.user }); + this.interruptQueue = new UserInterruptQueue(this); Object.defineProperty(this, 'currentTheme', { - get : () => { + get: () => { if (this.currentThemeConfig) { return this.currentThemeConfig.get(); } else { return { - info : { - name : 'N/A', - author : 'N/A', - description : 'N/A', - group : 'N/A', - } + info: { + name: 'N/A', + author: 'N/A', + description: 'N/A', + group: 'N/A', + }, }; } }, - set : (theme) => { + set: theme => { this.currentThemeConfig = theme; - } + }, }); Object.defineProperty(this, 'node', { - get : function() { + get: function () { return self.session.id; - } + }, }); Object.defineProperty(this, 'currentMenuModule', { - get : function() { + get: function () { return self.menuStack.currentModule; - } + }, }); - this.setTemporaryDirectDataHandler = function(handler) { - this.dataPassthrough = true; // let implementations do with what they will here + this.setTemporaryDirectDataHandler = function (handler) { + this.dataPassthrough = true; // let implementations do with what they will here this.input.removeAllListeners('data'); this.input.on('data', handler); }; - this.restoreDataHandler = function() { + this.restoreDataHandler = function () { this.dataPassthrough = false; this.input.removeAllListeners('data'); this.input.on('data', this.dataHandler); }; - this.themeChangedListener = function( { themeId } ) { - if(_.get(self.currentTheme, 'info.themeId') === themeId) { - self.currentThemeConfig = require('./theme.js').getAvailableThemes().get(themeId); + this.themeChangedListener = function ({ themeId }) { + if (_.get(self.currentTheme, 'info.themeId') === themeId) { + self.currentThemeConfig = require('./theme.js') + .getAvailableThemes() + .get(themeId); } }; @@ -151,14 +158,14 @@ function Client(/*input, output*/) { // * http://www.ansi-bbs.org/ansi-bbs-core-server.html // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ // - this.getTermClient = function(deviceAttr) { + this.getTermClient = function (deviceAttr) { let termClient = { - '63;1;2' : 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android) - '50;86;84;88' : 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt + '63;1;2': 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android) + '50;86;84;88': 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt }[deviceAttr]; - if(!termClient) { - if(_.startsWith(deviceAttr, '67;84;101;114;109')) { + if (!termClient) { + if (_.startsWith(deviceAttr, '67;84;101;114;109')) { // // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // @@ -173,176 +180,178 @@ function Client(/*input, output*/) { }; /* eslint-disable no-control-regex */ - this.isMouseInput = function(data) { - return /\x1b\[M/.test(data) || + this.isMouseInput = function (data) { + return ( + /\x1b\[M/.test(data) || /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || /\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || - /\u001b\[(O|I)/.test(data); + /\u001b\[(O|I)/.test(data) + ); }; /* eslint-enable no-control-regex */ - this.getKeyComponentsFromCode = function(code) { + this.getKeyComponentsFromCode = function (code) { return { // xterm/gnome - 'OP' : { name : 'f1' }, - 'OQ' : { name : 'f2' }, - 'OR' : { name : 'f3' }, - 'OS' : { name : 'f4' }, + 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' }, + '[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' }, + '[[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' }, + '[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' }, + '[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' }, + '[[5~': { name: 'page up' }, + '[[6~': { name: 'page down' }, // rvxt - '[7~' : { name : 'home' }, - '[8~' : { name : 'end' }, + '[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 }, + '[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' }, + '[K': { name: 'end' }, + '[@': { name: 'insert' }, + '[V': { name: 'page up' }, + '[U': { name: 'page down' }, // other - '[Z' : { name : 'tab', shift : true }, + '[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]) { + 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)) { + if (self.isMouseInput(data)) { return; } var buf = []; var m; - while((m = RE_ESC_CODE_ANYWHERE.exec(data))) { + 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, + seq: s, + name: undefined, + ctrl: false, + meta: false, + shift: false, }; 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) { + 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))) { + } else if ((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) { assert('c' === parts[2]); var termClient = self.getTermClient(parts[1]); - if(termClient) { + if (termClient) { self.term.termClient = termClient; } - } else if('\r' === s) { + } else if ('\r' === s) { key.name = 'return'; - } else if('\n' === s) { + } else if ('\n' === s) { key.name = 'line feed'; - } else if('\t' === s) { + } else if ('\t' === s) { key.name = 'tab'; - } else if('\x7f' === s) { + } else if ('\x7f' === s) { // // Backspace vs delete is a crazy thing, especially in *nix. // - ANSI-BBS uses 0x7f for DEL @@ -351,61 +360,63 @@ function Client(/*input, output*/) { // 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'; + if (self.term.isNixTerm()) { + key.name = 'backspace'; } else { - key.name = 'delete'; + key.name = 'delete'; } } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { // backspace, CTRL-H - key.name = 'backspace'; - key.meta = ('\x1b' === s.charAt(0)); - } else if('\x1b' === s || '\x1b\x1b' === s) { - key.name = 'escape'; - key.meta = (2 === s.length); + 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') { + 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') { + 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; + 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))) { + key.name = parts[1].toLowerCase(); + key.meta = true; + key.shift = /^[A-Z]$/.test(parts[1]); + } else if ((parts = RE_FUNCTION_KEYCODE.exec(s))) { var code = - (parts[1] || '') + (parts[2] || '') + - (parts[4] || '') + (parts[9] || ''); + (parts[1] || '') + + (parts[2] || '') + + (parts[4] || '') + + (parts[9] || ''); var modifier = (parts[3] || parts[8] || 1) - 1; - key.ctrl = !!(modifier & 4); - key.meta = !!(modifier & 10); - key.shift = !!(modifier & 1); - key.code = code; + key.ctrl = !!(modifier & 4); + key.meta = !!(modifier & 10); + key.shift = !!(modifier & 1); + key.code = code; _.assign(key, self.getKeyComponentsFromCode(code)); } var ch; - if(1 === s.length) { + if (1 === s.length) { ch = s; - } else if('space' === key.name) { + } else if ('space' === key.name) { // stupid hack to always get space as a regular char ch = ' '; } - if(_.isUndefined(key.name)) { + if (_.isUndefined(key.name)) { key = undefined; } else { // @@ -418,14 +429,14 @@ function Client(/*input, output*/) { 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.lastActivityTime = Date.now(); - if(!self.ignoreInput) { + if (!self.ignoreInput) { self.emit('key press', ch, key); } } @@ -435,23 +446,23 @@ function Client(/*input, output*/) { require('util').inherits(Client, stream); -Client.prototype.setInputOutput = function(input, output) { - this.input = input; +Client.prototype.setInputOutput = function (input, 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; +Client.prototype.setTermType = function (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() { +Client.prototype.startIdleMonitor = function () { // clear existing, if any - if(this.idleCheck) { + if (this.idleCheck) { this.stopIdleMonitor(); } @@ -462,11 +473,11 @@ Client.prototype.startIdleMonitor = function() { // We also update minutes spent online the system here, // if we have a authenticated user. // - this.idleCheck = setInterval( () => { + this.idleCheck = setInterval(() => { const nowMs = Date.now(); let idleLogoutSeconds; - if(this.user.isAuthenticated()) { + if (this.user.isAuthenticated()) { idleLogoutSeconds = Config().users.idleLogoutSeconds; // @@ -474,17 +485,17 @@ Client.prototype.startIdleMonitor = function() { // every user, but want at least some updates for various things // such as achievements. Send off every 5m. // - const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1); - if(0 === (minOnline % 5)) { - Events.emit( - Events.getSystemEvents().UserStatIncrement, - { - user : this.user, - statName : UserProps.MinutesOnlineTotalCount, - statIncrementBy : 1, - statValue : minOnline - } - ); + const minOnline = this.user.incrementProperty( + UserProps.MinutesOnlineTotalCount, + 1 + ); + if (0 === minOnline % 5) { + Events.emit(Events.getSystemEvents().UserStatIncrement, { + user: this.user, + statName: UserProps.MinutesOnlineTotalCount, + statIncrementBy: 1, + statValue: minOnline, + }); } } else { idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds; @@ -493,46 +504,52 @@ Client.prototype.startIdleMonitor = function() { // use override value if set idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds; - if(idleLogoutSeconds > 0 && (nowMs - this.lastActivityTime >= (idleLogoutSeconds * 1000))) { + if ( + idleLogoutSeconds > 0 && + nowMs - this.lastActivityTime >= idleLogoutSeconds * 1000 + ) { this.emit('idle timeout'); } }, 1000 * 60); }; -Client.prototype.stopIdleMonitor = function() { - if(this.idleCheck) { +Client.prototype.stopIdleMonitor = function () { + if (this.idleCheck) { clearInterval(this.idleCheck); delete this.idleCheck; } }; -Client.prototype.explicitActivityTimeUpdate = function() { +Client.prototype.explicitActivityTimeUpdate = function () { this.lastActivityTime = Date.now(); }; -Client.prototype.overrideIdleLogoutSeconds = function(seconds) { +Client.prototype.overrideIdleLogoutSeconds = function (seconds) { this.idleLogoutSecondsOverride = seconds; }; -Client.prototype.restoreIdleLogoutSeconds = function() { +Client.prototype.restoreIdleLogoutSeconds = function () { delete this.idleLogoutSecondsOverride; }; Client.prototype.end = function () { - if(this.term) { + if (this.term) { this.term.disconnect(); } - Events.removeListener(Events.getSystemEvents().ThemeChanged, this.themeChangedListener); + Events.removeListener( + Events.getSystemEvents().ThemeChanged, + this.themeChangedListener + ); const currentModule = this.menuStack.getCurrentModule; - if(currentModule) { + if (currentModule) { currentModule.leave(); } // persist time online for authenticated users - if(this.user.isAuthenticated()) { + if (this.user.isAuthenticated()) { this.user.persistProperty( UserProps.MinutesOnlineTotalCount, this.user.getProperty(UserProps.MinutesOnlineTotalCount) @@ -545,13 +562,13 @@ Client.prototype.end = function () { // // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH // - if(_.isFunction(this.disconnect)) { + if (_.isFunction(this.disconnect)) { return this.disconnect(); } else { // legacy fallback return this.output.end.apply(this.output, arguments); } - } catch(e) { + } catch (e) { // ie TypeError } }; @@ -564,15 +581,15 @@ Client.prototype.destroySoon = function () { return this.output.destroySoon.apply(this.output, arguments); }; -Client.prototype.waitForKeyPress = function(cb) { +Client.prototype.waitForKeyPress = function (cb) { this.once('key press', function kp(ch, key) { cb(ch, key); }); }; -Client.prototype.isLocal = function() { +Client.prototype.isLocal = function () { // :TODO: Handle ipv6 better - return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); + return ['127.0.0.1', '::ffff:127.0.0.1'].includes(this.remoteAddress); }; /////////////////////////////////////////////////////////////////////////////// @@ -580,7 +597,7 @@ Client.prototype.isLocal = function() { /////////////////////////////////////////////////////////////////////////////// // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something -Client.prototype.defaultHandlerMissingMod = function() { +Client.prototype.defaultHandlerMissingMod = function () { var self = this; function handler(err) { @@ -591,7 +608,6 @@ Client.prototype.defaultHandlerMissingMod = function() { self.term.write('This has been logged for your SysOp to review.\n'); self.term.write('\nGoodbye!\n'); - //self.term.write(err); //if(miscUtil.isDevelopment() && err.stack) { @@ -604,18 +620,18 @@ Client.prototype.defaultHandlerMissingMod = function() { return handler; }; -Client.prototype.terminalSupports = function(query) { +Client.prototype.terminalSupports = function (query) { const termClient = this.term.termClient; - switch(query) { - case 'vtx_audio' : + switch (query) { + case 'vtx_audio': // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt return 'vtx' === termClient; - case 'vtx_hyperlink' : + case 'vtx_hyperlink': return 'vtx' === termClient; - default : + default: return false; } }; diff --git a/core/client_connections.js b/core/client_connections.js index d1a6be6e..99f81cf2 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -2,34 +2,33 @@ 'use strict'; // ENiGMA½ -const logger = require('./logger.js'); -const Events = require('./events.js'); -const UserProps = require('./user_property.js'); +const logger = require('./logger.js'); +const Events = require('./events.js'); +const UserProps = require('./user_property.js'); // deps -const _ = require('lodash'); -const moment = require('moment'); -const hashids = require('hashids/cjs'); +const _ = require('lodash'); +const moment = require('moment'); +const hashids = require('hashids/cjs'); -exports.getActiveConnections = getActiveConnections; +exports.getActiveConnections = getActiveConnections; exports.getActiveConnectionList = getActiveConnectionList; -exports.addNewClient = addNewClient; -exports.removeClient = removeClient; -exports.getConnectionByUserId = getConnectionByUserId; -exports.getConnectionByNodeId = getConnectionByNodeId; +exports.addNewClient = addNewClient; +exports.removeClient = removeClient; +exports.getConnectionByUserId = getConnectionByUserId; +exports.getConnectionByNodeId = getConnectionByNodeId; const clientConnections = []; exports.clientConnections = clientConnections; function getActiveConnections(authUsersOnly = false) { return clientConnections.filter(conn => { - return ((authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly); + return (authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly; }); } function getActiveConnectionList(authUsersOnly) { - - if(!_.isBoolean(authUsersOnly)) { + if (!_.isBoolean(authUsersOnly)) { authUsersOnly = true; } @@ -37,23 +36,26 @@ function getActiveConnectionList(authUsersOnly) { return _.map(getActiveConnections(authUsersOnly), ac => { const entry = { - node : ac.node, - authenticated : ac.user.isAuthenticated(), - userId : ac.user.userId, - action : _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'), + node: ac.node, + authenticated: ac.user.isAuthenticated(), + userId: ac.user.userId, + action: _.get(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[UserProps.RealName]; - entry.location = ac.user.properties[UserProps.Location]; - entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations]; + if (ac.user.isAuthenticated()) { + entry.userName = ac.user.username; + entry.realName = ac.user.properties[UserProps.RealName]; + entry.location = ac.user.properties[UserProps.Location]; + entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations]; - const diff = now.diff(moment(ac.user.properties[UserProps.LastLoginTs]), 'minutes'); - entry.timeOn = moment.duration(diff, 'minutes'); + const diff = now.diff( + moment(ac.user.properties[UserProps.LastLoginTs]), + 'minutes' + ); + entry.timeOn = moment.duration(diff, 'minutes'); } return entry; }); @@ -67,39 +69,42 @@ function addNewClient(client, clientSock) { for (nodeId = 1; nodeId < Number.MAX_SAFE_INTEGER; ++nodeId) { const existing = clientConnections.find(client => nodeId === client.node); if (!existing) { - break; // available slot + break; // available slot } } client.session.id = nodeId; - const remoteAddress = client.remoteAddress = clientSock.remoteAddress; + const remoteAddress = (client.remoteAddress = clientSock.remoteAddress); // create a unique identifier one-time ID for this session - client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ nodeId, moment().valueOf() ]); + client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ + nodeId, + moment().valueOf(), + ]); clientConnections.push(client); - clientConnections.sort( (c1, c2) => c1.session.id - c2.session.id); + clientConnections.sort((c1, c2) => c1.session.id - c2.session.id); // Create a client specific logger // Note that this will be updated @ login with additional information - client.log = logger.log.child( { nodeId, sessionId : client.session.uniqueId } ); + client.log = logger.log.child({ nodeId, sessionId: client.session.uniqueId }); const connInfo = { - remoteAddress : remoteAddress, - serverName : client.session.serverName, - isSecure : client.session.isSecure, + remoteAddress: remoteAddress, + serverName: client.session.serverName, + isSecure: client.session.isSecure, }; - if(client.log.debug()) { - connInfo.port = clientSock.localPort; - connInfo.family = clientSock.localFamily; + if (client.log.debug()) { + connInfo.port = clientSock.localPort; + connInfo.family = clientSock.localFamily; } 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 nodeId; } @@ -108,33 +113,39 @@ function removeClient(client) { client.end(); const i = clientConnections.indexOf(client); - if(i > -1) { + if (i > -1) { clientConnections.splice(i, 1); logger.log.info( { - connectionCount : clientConnections.length, - nodeId : client.node, + connectionCount: clientConnections.length, + nodeId: client.node, }, 'Client disconnected' ); - if(client.user && client.user.isValid()) { - const minutesOnline = moment().diff(moment(client.user.properties[UserProps.LastLoginTs]), 'minutes'); - Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user, minutesOnline } ); + if (client.user && client.user.isValid()) { + const minutesOnline = moment().diff( + moment(client.user.properties[UserProps.LastLoginTs]), + 'minutes' + ); + Events.emit(Events.getSystemEvents().UserLogoff, { + user: client.user, + minutesOnline, + }); } - 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); } function getConnectionByNodeId(nodeId) { - return getActiveConnections().find( ac => nodeId == ac.node ); + return getActiveConnections().find(ac => nodeId == ac.node); } diff --git a/core/client_term.js b/core/client_term.js index 16900772..fa7f8ba2 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -2,24 +2,23 @@ 'use strict'; // ENiGMA½ -var Log = require('./logger.js').log; -var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; -const Config = require('./config.js').get; -var iconv = require('iconv-lite'); -var assert = require('assert'); -var _ = require('lodash'); +var Log = require('./logger.js').log; +var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; +const Config = require('./config.js').get; +var iconv = require('iconv-lite'); +var assert = require('assert'); +var _ = require('lodash'); - -exports.ClientTerminal = ClientTerminal; +exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { - this.output = output; + this.output = output; var outputEncoding = 'cp437'; assert(iconv.encodingExists(outputEncoding)); // convert line feeds such as \n -> \r\n - this.convertLF = true; + this.convertLF = true; this.syncTermFontsEnabled = false; @@ -27,37 +26,37 @@ function ClientTerminal(output) { // Some terminal we handle specially // They can also be found in this.env{} // - var termType = 'unknown'; - var termHeight = 0; - var termWidth = 0; - var termClient = 'unknown'; + var termType = 'unknown'; + var termHeight = 0; + var termWidth = 0; + var termClient = 'unknown'; - this.currentSyncFont = 'not_set'; + this.currentSyncFont = 'not_set'; // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. - this.env = {}; + this.env = {}; Object.defineProperty(this, 'outputEncoding', { - get : function() { + get: function () { return outputEncoding; }, - set : function(enc) { - if(iconv.encodingExists(enc)) { + set: function (enc) { + if (iconv.encodingExists(enc)) { outputEncoding = enc; } else { - Log.warn({ encoding : enc }, 'Unknown encoding'); + Log.warn({ encoding: enc }, 'Unknown encoding'); } - } + }, }); Object.defineProperty(this, 'termType', { - get : function() { + get: function () { return termType; }, - set : function(ttype) { + set: function (ttype) { termType = ttype.toLowerCase(); - if(this.isANSI()) { + if (this.isANSI()) { this.outputEncoding = 'cp437'; } else { // :TODO: See how x84 does this -- only set if local/remote are binary @@ -68,53 +67,56 @@ function ClientTerminal(output) { // 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() { + get: function () { return termWidth; }, - set : function(width) { - if(width > 0) { + set: function (width) { + if (width > 0) { termWidth = width; } - } + }, }); Object.defineProperty(this, 'termHeight', { - get : function() { + get: function () { return termHeight; }, - set : function(height) { - if(height > 0) { + set: function (height) { + if (height > 0) { termHeight = height; } - } + }, }); Object.defineProperty(this, 'termClient', { - get : function() { + get: function () { return termClient; }, - set : function(tc) { + 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() { +ClientTerminal.prototype.disconnect = function () { this.output = null; }; -ClientTerminal.prototype.isNixTerm = function() { +ClientTerminal.prototype.isNixTerm = function () { // // Standard *nix type terminals // - if(this.termType.startsWith('xterm')) { + if (this.termType.startsWith('xterm')) { return true; } @@ -122,7 +124,7 @@ ClientTerminal.prototype.isNixTerm = function() { return utf8TermList.includes(this.termType); }; -ClientTerminal.prototype.isANSI = function() { +ClientTerminal.prototype.isANSI = function () { // // ANSI terminals should be encoded to CP437 // @@ -163,35 +165,33 @@ ClientTerminal.prototype.isANSI = function() { // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) -ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) { +ClientTerminal.prototype.write = function (s, convertLineFeeds, cb) { this.rawWrite(this.encode(s, convertLineFeeds), cb); }; -ClientTerminal.prototype.rawWrite = function(s, cb) { - if(this.output && this.output.writable) { +ClientTerminal.prototype.rawWrite = function (s, cb) { + if (this.output && this.output.writable) { this.output.write(s, err => { - if(cb) { + 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, cb) { - this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds| +ClientTerminal.prototype.pipeWrite = function (s, cb) { + this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds| }; -ClientTerminal.prototype.encode = function(s, convertLineFeeds) { +ClientTerminal.prototype.encode = function (s, convertLineFeeds) { convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF; - if(convertLineFeeds && _.isString(s)) { + 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 da6c8f5d..5c81f3ec 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -const ANSI = require('./ansi_term.js'); +const ANSI = require('./ansi_term.js'); const { getPredefinedMCIValue } = require('./predefined_mci.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); -exports.stripMciColorCodes = stripMciColorCodes; -exports.pipeStringLength = pipeStringLength; -exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; -exports.controlCodesToAnsi = controlCodesToAnsi; +exports.stripMciColorCodes = stripMciColorCodes; +exports.pipeStringLength = pipeStringLength; +exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; +exports.controlCodesToAnsi = controlCodesToAnsi; // :TODO: Not really happy with the module name of "color_codes". Would like something better ... control_code_string? @@ -23,97 +23,101 @@ function pipeStringLength(s) { } function ansiSgrFromRenegadeColorCode(cc) { - return ANSI.sgr({ - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], + 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 ansiSgrFromCnetStyleColorCode(cc) { - return ANSI.sgr({ - c0 : [ 'reset', 'black' ], - c1 : [ 'reset', 'red' ], - c2 : [ 'reset', 'green' ], - c3 : [ 'reset', 'yellow' ], - c4 : [ 'reset', 'blue' ], - c5 : [ 'reset', 'magenta' ], - c6 : [ 'reset', 'cyan' ], - c7 : [ 'reset', 'white' ], + return ANSI.sgr( + { + c0: ['reset', 'black'], + c1: ['reset', 'red'], + c2: ['reset', 'green'], + c3: ['reset', 'yellow'], + c4: ['reset', 'blue'], + c5: ['reset', 'magenta'], + c6: ['reset', 'cyan'], + c7: ['reset', 'white'], - c8 : [ 'bold', 'black' ], - c9 : [ 'bold', 'red' ], - ca : [ 'bold', 'green' ], - cb : [ 'bold', 'yellow' ], - cc : [ 'bold', 'blue' ], - cd : [ 'bold', 'magenta' ], - ce : [ 'bold', 'cyan' ], - cf : [ 'bold', 'white' ], + c8: ['bold', 'black'], + c9: ['bold', 'red'], + ca: ['bold', 'green'], + cb: ['bold', 'yellow'], + cc: ['bold', 'blue'], + cd: ['bold', 'magenta'], + ce: ['bold', 'cyan'], + cf: ['bold', 'white'], - z0 : [ 'blackBG' ], - z1 : [ 'redBG' ], - z2 : [ 'greenBG' ], - z3 : [ 'yellowBG' ], - z4 : [ 'blueBG' ], - z5 : [ 'magentaBG' ], - z6 : [ 'cyanBG' ], - z7 : [ 'whiteBG' ], - }[cc] || 'normal'); + z0: ['blackBG'], + z1: ['redBG'], + z2: ['greenBG'], + z3: ['yellowBG'], + z4: ['blueBG'], + z5: ['magentaBG'], + z6: ['cyanBG'], + z7: ['whiteBG'], + }[cc] || 'normal' + ); } function renegadeToAnsi(s, client) { - if(-1 == s.indexOf('|')) { - return s; // no pipe codes present + if (-1 == s.indexOf('|')) { + return s; // no pipe codes present } - let result = ''; - const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g; + let result = ''; + const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g; let m; let lastIndex = 0; - while((m = re.exec(s))) { - if(m[3]) { + while ((m = re.exec(s))) { + if (m[3]) { // |## color const val = parseInt(m[3], 10); const attr = ansiSgrFromRenegadeColorCode(val); result += s.substr(lastIndex, m.index - lastIndex) + attr; - } else if(m[4] || m[1]) { + } else if (m[4] || m[1]) { // |AA MCI code or |Cx## movement where ## is in m[1] let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]); - val = _.isString(val) ? val : m[0]; // value itself or literal + val = _.isString(val) ? val : m[0]; // value itself or literal result += s.substr(lastIndex, m.index - lastIndex) + val; - } else if(m[5]) { + } else if (m[5]) { // || -- literal '|', that is. result += '|'; } @@ -121,7 +125,7 @@ function renegadeToAnsi(s, client) { lastIndex = re.lastIndex; } - return (0 === result.length ? s : result + s.substr(lastIndex)); + return 0 === result.length ? s : result + s.substr(lastIndex); } // @@ -144,26 +148,27 @@ function renegadeToAnsi(s, client) { // * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt // function controlCodesToAnsi(s, client) { - const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex + const RE = + /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex let m; - let result = ''; - let lastIndex = 0; + let result = ''; + let lastIndex = 0; let v; let fg; let bg; - while((m = RE.exec(s))) { - switch(m[0].charAt(0)) { - case '|' : + while ((m = RE.exec(s))) { + switch (m[0].charAt(0)) { + case '|': // Renegade |## 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)) { + if (_.isString(v)) { result += s.substr(lastIndex, m.index - lastIndex) + v; } else { v = ansiSgrFromRenegadeColorCode(v); @@ -171,9 +176,9 @@ function controlCodesToAnsi(s, client) { } break; - case '@' : + case '@': // PCBoard @X## or Wildcat! @##@ - if('@' === m[0].substr(-1)) { + if ('@' === m[0].substr(-1)) { // Wildcat! v = m[6]; } else { @@ -181,81 +186,83 @@ function controlCodesToAnsi(s, client) { } bg = { - 0 : [ 'blackBG' ], - 1 : [ 'blueBG' ], - 2 : [ 'greenBG' ], - 3 : [ 'cyanBG' ], - 4 : [ 'redBG' ], - 5 : [ 'magentaBG' ], - 6 : [ 'yellowBG' ], - 7 : [ 'whiteBG' ], + 0: ['blackBG'], + 1: ['blueBG'], + 2: ['greenBG'], + 3: ['cyanBG'], + 4: ['redBG'], + 5: ['magentaBG'], + 6: ['yellowBG'], + 7: ['whiteBG'], - 8 : [ 'bold', 'blackBG' ], - 9 : [ 'bold', 'blueBG' ], - A : [ 'bold', 'greenBG' ], - B : [ 'bold', 'cyanBG' ], - C : [ 'bold', 'redBG' ], - D : [ 'bold', 'magentaBG' ], - E : [ 'bold', 'yellowBG' ], - F : [ 'bold', 'whiteBG' ], - }[v.charAt(0)] || [ '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(0)] || ['normal']; fg = { - 0 : [ 'reset', 'black' ], - 1 : [ 'reset', 'blue' ], - 2 : [ 'reset', 'green' ], - 3 : [ 'reset', 'cyan' ], - 4 : [ 'reset', 'red' ], - 5 : [ 'reset', 'magenta' ], - 6 : [ 'reset', 'yellow' ], - 7 : [ 'reset', 'white' ], + 0: ['reset', 'black'], + 1: ['reset', 'blue'], + 2: ['reset', 'green'], + 3: ['reset', 'cyan'], + 4: ['reset', 'red'], + 5: ['reset', 'magenta'], + 6: ['reset', 'yellow'], + 7: ['reset', 'white'], - 8 : [ 'blink', 'black' ], - 9 : [ 'blink', 'blue' ], - A : [ 'blink', 'green' ], - B : [ 'blink', 'cyan' ], - C : [ 'blink', 'red' ], - D : [ 'blink', 'magenta' ], - E : [ 'blink', 'yellow' ], - F : [ 'blink', 'white' ], + 8: ['blink', 'black'], + 9: ['blink', 'blue'], + A: ['blink', 'green'], + B: ['blink', 'cyan'], + C: ['blink', 'red'], + D: ['blink', 'magenta'], + E: ['blink', 'yellow'], + F: ['blink', 'white'], }[v.charAt(1)] || ['normal']; v = ANSI.sgr(fg.concat(bg)); result += s.substr(lastIndex, m.index - lastIndex) + v; break; - case '\x03' : + case '\x03': // WWIV v = parseInt(m[8], 10); - if(isNaN(v)) { + 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'); + 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; break; - case '\x19' : - case '\0x11' : + case '\x19': + case '\0x11': // CNET "Y-Style" & "Q-Style" v = m[9] || m[11]; - if(v) { - if('n1' === v) { + if (v) { + if ('n1' === v) { v = '\n'; - } else if('f1' === v) { + } else if ('f1' === v) { v = ANSI.clearScreen(); } else { v = ansiSgrFromCnetStyleColorCode(v); @@ -270,5 +277,5 @@ function controlCodesToAnsi(s, client) { lastIndex = RE.lastIndex; } - return (0 === result.length ? s : result + s.substr(lastIndex)); -} \ No newline at end of file + return 0 === result.length ? s : result + s.substr(lastIndex); +} diff --git a/core/combatnet.js b/core/combatnet.js index 8f1a5623..b85a9669 100644 --- a/core/combatnet.js +++ b/core/combatnet.js @@ -2,22 +2,19 @@ 'use strict'; // enigma-bbs -const { MenuModule } = require('../core/menu_module.js'); -const { resetScreen } = require('../core/ansi_term.js'); -const { Errors } = require('./enig_error.js'); -const { - trackDoorRunBegin, - trackDoorRunEnd -} = require('./door_util.js'); +const { MenuModule } = require('../core/menu_module.js'); +const { resetScreen } = require('../core/ansi_term.js'); +const { Errors } = require('./enig_error.js'); +const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js'); // deps -const async = require('async'); -const RLogin = require('rlogin'); +const async = require('async'); +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 { @@ -25,9 +22,9 @@ exports.getModule = class CombatNetModule extends MenuModule { 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; + this.config = options.menuConfig.config; + this.config.host = this.config.host || 'bbs.combatnet.us'; + this.config.rloginPort = this.config.rloginPort || 4513; } initSequence() { @@ -38,10 +35,10 @@ exports.getModule = class CombatNetModule extends MenuModule { function validateConfig(callback) { return self.validateConfigFields( { - host : 'string', - password : 'string', - bbsTag : 'string', - rloginPort : 'number', + host: 'string', + password: 'string', + bbsTag: 'string', + rloginPort: 'number', }, callback ); @@ -52,30 +49,33 @@ exports.getModule = class CombatNetModule extends MenuModule { let doorTracking; - 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 + ); - if(doorTracking) { + if (doorTracking) { trackDoorRunEnd(doorTracking); } } }; - 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}`); + self.client.log.info( + `CombatNet rlogin client error: ${err.message}` + ); restorePipeToNormal(); return callback(err); }); @@ -91,24 +91,29 @@ exports.getModule = class CombatNetModule extends MenuModule { rlogin.send(buffer); } - rlogin.on('connect', + 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) { + function (state) { + if (state) { self.client.log.info('Connected to CombatNet'); self.client.term.output.on('data', sendToRloginBuffer); doorTracking = trackDoorRunBegin(self.client); } else { - return callback(Errors.General('Failed to establish establish CombatNet connection')); + return callback( + Errors.General( + 'Failed to establish establish CombatNet connection' + ) + ); } } ); // If data (a Buffer) has been received from the server ... - rlogin.on('data', (data) => { + rlogin.on('data', data => { self.client.term.rawWrite(data); }); @@ -116,11 +121,11 @@ exports.getModule = class CombatNetModule extends MenuModule { rlogin.connect(); // note: no explicit callback() until we're finished! - } + }, ], err => { - if(err) { - self.client.log.warn( { error : err.message }, 'CombatNet error'); + if (err) { + self.client.log.warn({ error: err.message }, 'CombatNet error'); } // if the client is still here, go to previous diff --git a/core/conf_area_util.js b/core/conf_area_util.js index 1c0b65c4..167ffb24 100644 --- a/core/conf_area_util.js +++ b/core/conf_area_util.js @@ -2,9 +2,9 @@ 'use strict'; // deps -const _ = require('lodash'); +const _ = require('lodash'); -exports.sortAreasOrConfs = sortAreasOrConfs; +exports.sortAreasOrConfs = sortAreasOrConfs; // // Method for sorting message, file, etc. areas and confs @@ -19,12 +19,12 @@ function sortAreasOrConfs(areasOrConfs, type) { entryA = type ? a[type] : a; entryB = type ? b[type] : b; - if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { + 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 + 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 4f615c18..6cce3b09 100644 --- a/core/config.js +++ b/core/config.js @@ -25,13 +25,11 @@ exports.Config = class Config extends ConfigLoader { 'loginServers.ssh.algorithms.compress', ]; - const replaceKeys = [ - 'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch', - ]; + const replaceKeys = ['args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch']; const configOptions = Object.assign({}, options, { - defaultConfig : DefaultConfig, - defaultsCustomizer : (defaultVal, configVal, key, path) => { + defaultConfig: DefaultConfig, + defaultsCustomizer: (defaultVal, configVal, key, path) => { if (Array.isArray(defaultVal) && Array.isArray(configVal)) { if (replacePaths.includes(path) || replaceKeys.includes(key)) { // full replacement using user config value @@ -42,7 +40,7 @@ exports.Config = class Config extends ConfigLoader { } } }, - onReload : err => { + onReload: err => { if (!err) { const Events = require('./events.js'); Events.emit(Events.getSystemEvents().ConfigChanged); diff --git a/core/config_cache.js b/core/config_cache.js index fc3ba878..a0f6749c 100644 --- a/core/config_cache.js +++ b/core/config_cache.js @@ -2,43 +2,49 @@ 'use strict'; // deps -const paths = require('path'); -const fs = require('graceful-fs'); -const hjson = require('hjson'); -const sane = require('sane'); -const _ = require('lodash'); +const paths = require('path'); +const fs = require('graceful-fs'); +const hjson = require('hjson'); +const sane = require('sane'); +const _ = require('lodash'); -module.exports = new class ConfigCache -{ +module.exports = new (class ConfigCache { constructor() { - this.cache = new Map(); // path->parsed config + this.cache = new Map(); // path->parsed config } getConfigWithOptions(options, cb) { options.hotReload = _.get(options, 'hotReload', true); const cached = this.cache.has(options.filePath); - if(options.forceReCache || !cached) { + if (options.forceReCache || !cached) { this.recacheConfigFromFile(options.filePath, (err, config) => { - if(!err && !cached) { - if(options.hotReload) { - const watcher = sane( - paths.dirname(options.filePath), - { - glob : `**/${paths.basename(options.filePath)}` - } - ); + if (!err && !cached) { + if (options.hotReload) { + 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'); + 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, configCache : this } ); + this.recacheConfigFromFile( + paths.join(fileRoot, fileName), + err => { + if (!err) { + if (options.callback) { + options.callback({ + fileName, + fileRoot, + configCache: this, + }); + } } } - }); + ); }); } } @@ -50,12 +56,12 @@ module.exports = new class ConfigCache } getConfig(filePath, cb) { - return this.getConfigWithOptions( { filePath }, cb); + return this.getConfigWithOptions({ filePath }, cb); } recacheConfigFromFile(path, cb) { - fs.readFile(path, { encoding : 'utf-8' }, (err, data) => { - if(err) { + fs.readFile(path, { encoding: 'utf-8' }, (err, data) => { + if (err) { return cb(err); } @@ -63,10 +69,13 @@ module.exports = new class ConfigCache try { parsed = hjson.parse(data); this.cache.set(path, parsed); - } catch(e) { + } catch (e) { try { - require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); - } catch(ignored) { + require('./logger.js').log.error( + { filePath: path, error: e.message }, + 'Failed to re-cache' + ); + } catch (ignored) { // nothing - we may be failing to parse the config in which we can't log here! } return cb(e); @@ -75,4 +84,4 @@ module.exports = new class ConfigCache return cb(null, parsed); }); } -}; +})(); diff --git a/core/config_default.js b/core/config_default.js index 25b14bb7..d31efca5 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -2,26 +2,26 @@ const paths = require('path'); module.exports = () => { return { - general : { - boardName : 'Another Fine ENiGMA½ BBS', - prettyBoardName : '|08A|07nother |07F|08ine |07E|08NiGMA|07½ B|08BS', - telnetHostname : '', - sshHostname : '', - website : 'https://enigma-bbs.github.io', - description : 'An ENiGMA½ BBS', + general: { + boardName: 'Another Fine ENiGMA½ BBS', + prettyBoardName: '|08A|07nother |07F|08ine |07E|08NiGMA|07½ B|08BS', + telnetHostname: '', + sshHostname: '', + website: 'https://enigma-bbs.github.io', + description: 'An ENiGMA½ BBS', // :TODO: closedSystem prob belongs under users{}? - closedSystem : false, // is the system closed to new users? + closedSystem: false, // is the system closed to new users? - menuFile : 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path - achievementFile : 'achievements.hjson', + menuFile: 'menu.hjson', // 'oputil.js config new' will set this appropriately in config.hjson; may be full path + achievementFile: 'achievements.hjson', }, - term : { + term: { // checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals. // Using this with a terminal that does not support cursor position reports results in a 2 second delay // during the connect process, but provides better autoconfiguration of utf-8 - checkUtf8Encoding : true, + checkUtf8Encoding: true, // Checking the ANSI home position also requires the use of cursor position reports, which are not // supported on all terminals. Using this with a terminal that does not support cursor position reports @@ -30,18 +30,37 @@ module.exports = () => { checkAnsiHomePosition: true, // List of terms that should be assumed to use cp437 encoding - cp437TermList : ['ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color', 'ansi-256color-rgb'], + cp437TermList: [ + 'ansi', + 'pcansi', + 'pc-ansi', + 'ansi-bbs', + 'qansi', + 'scoansi', + 'syncterm', + 'ansi-256color', + 'ansi-256color-rgb', + ], // List of terms that should be assumed to use utf8 encoding - utf8TermList : ['xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator'], + utf8TermList: [ + 'xterm', + 'linux', + 'screen', + 'dumb', + 'rxvt', + 'konsole', + 'gnome', + 'x11 terminal emulator', + ], }, - 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, + passwordMin: 6, + passwordMax: 128, // // The bad password list is a text file containing a password per line. @@ -52,99 +71,118 @@ module.exports = () => { // // Current list source: https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/probable-v2-top12000.txt // - badPassFile : paths.join(__dirname, '../misc/bad_passwords.txt'), + badPassFile: paths.join(__dirname, '../misc/bad_passwords.txt'), - 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', - 'server', 'client', 'notme' + badUserNames: [ + 'sysop', + 'admin', + 'administrator', + 'root', + 'all', + 'areamgr', + 'filemgr', + 'filefix', + 'areafix', + 'allfix', + 'server', + 'client', + 'notme', ], - preAuthIdleLogoutSeconds : 60 * 3, // 3m - idleLogoutSeconds : 60 * 6, // 6m + preAuthIdleLogoutSeconds: 60 * 3, // 3m + idleLogoutSeconds: 60 * 6, // 6m - failedLogin : { - disconnect : 3, // 0=disabled - lockAccount : 9, // 0=disabled; Mark user status as "locked" if >= N - autoUnlockMinutes : 60 * 6, // 0=disabled; Auto unlock after N minutes. + failedLogin: { + disconnect: 3, // 0=disabled + lockAccount: 9, // 0=disabled; Mark user status as "locked" if >= N + autoUnlockMinutes: 60 * 6, // 0=disabled; Auto unlock after N minutes. }, - unlockAtEmailPwReset : true, // if true, password reset via email will unlock locked accounts + unlockAtEmailPwReset: true, // if true, password reset via email will unlock locked accounts - twoFactorAuth : { - method : 'googleAuth', + twoFactorAuth: { + method: 'googleAuth', - otp : { - registerEmailText : paths.join(__dirname, '../misc/otp_register_email.template.txt'), - registerEmailHtml : paths.join(__dirname, '../misc/otp_register_email.template.html'), - registerPageTemplate : paths.join(__dirname, '../www/otp_register.template.html'), - } - } + otp: { + registerEmailText: paths.join( + __dirname, + '../misc/otp_register_email.template.txt' + ), + registerEmailHtml: paths.join( + __dirname, + '../misc/otp_register_email.template.html' + ), + registerPageTemplate: paths.join( + __dirname, + '../www/otp_register.template.html' + ), + }, + }, }, - theme : { - default : 'luciano_blocktronics', - preLogin : 'luciano_blocktronics', + theme: { + default: 'luciano_blocktronics', + preLogin: 'luciano_blocktronics', - passwordChar : '*', - dateFormat : { - short : 'MM/DD/YYYY', - long : 'ddd, MMMM Do, YYYY', + passwordChar: '*', + dateFormat: { + short: 'MM/DD/YYYY', + long: 'ddd, MMMM Do, YYYY', }, - timeFormat : { - short : 'h:mm a', + timeFormat: { + short: 'h:mm a', + }, + dateTimeFormat: { + short: 'MM/DD/YYYY h:mm a', + long: 'ddd, MMMM Do, YYYY, 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/'), - security : paths.join(__dirname, './../config/security'), // certs, keys, etc. - mods : paths.join(__dirname, './../mods/'), - loginServers : paths.join(__dirname, './servers/login/'), - contentServers : paths.join(__dirname, './servers/content/'), - chatServers : paths.join(__dirname, './servers/chat/'), + paths: { + config: paths.join(__dirname, './../config/'), + security: paths.join(__dirname, './../config/security'), // certs, keys, etc. + mods: paths.join(__dirname, './../mods/'), + loginServers: paths.join(__dirname, './servers/login/'), + contentServers: paths.join(__dirname, './servers/content/'), + chatServers: paths.join(__dirname, './servers/chat/'), - 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/'), - db : paths.join(__dirname, './../db/'), - modsDb : paths.join(__dirname, './../db/mods/'), - dropFiles : paths.join(__dirname, './../drop/'), // + "/node/ - misc : paths.join(__dirname, './../misc/'), + art: paths.join(__dirname, './../art/general/'), + themes: paths.join(__dirname, './../art/themes/'), + logs: paths.join(__dirname, './../logs/'), + db: paths.join(__dirname, './../db/'), + modsDb: paths.join(__dirname, './../db/mods/'), + dropFiles: paths.join(__dirname, './../drop/'), // + "/node/ + misc: paths.join(__dirname, './../misc/'), }, - loginServers : { - telnet : { - port : 8888, - enabled : true, - firstMenu : 'telnetConnected', + loginServers: { + telnet: { + port: 8888, + enabled: true, + firstMenu: 'telnetConnected', }, - ssh : { - port : 8889, - enabled : false, // default to false as PK/pass in config.hjson are required + ssh: { + port: 8889, + enabled: false, // default to false as PK/pass in config.hjson are required // // To enable SSH, perform the following steps: // @@ -167,9 +205,12 @@ module.exports = () => { // - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/ // - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b // - privateKeyPem : paths.join(__dirname, './../config/security/ssh_private_key.pem'), - firstMenu : 'sshConnected', - firstMenuNewUser : 'sshConnectedNewUser', + privateKeyPem: paths.join( + __dirname, + './../config/security/ssh_private_key.pem' + ), + firstMenu: 'sshConnected', + firstMenuNewUser: 'sshConnectedNewUser', // // SSH details that can affect security. Stronger ciphers are better for example, @@ -179,8 +220,8 @@ module.exports = () => { // See https://github.com/mscdex/ssh2-streams for the full list of supported // algorithms. // - algorithms : { - kex : [ + algorithms: { + kex: [ 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', @@ -190,7 +231,7 @@ module.exports = () => { // 'diffie-hellman-group-exchange-sha256', // 'diffie-hellman-group-exchange-sha1', ], - cipher : [ + cipher: [ 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', @@ -208,7 +249,7 @@ module.exports = () => { 'cast128-cbc', 'arcfour', ], - hmac : [ + hmac: [ 'hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1', @@ -220,33 +261,33 @@ module.exports = () => { 'hmac-md5-96', ], // note that we disable compression by default due to issues with many clients. YMMV. - compress : [ 'none' ] + compress: ['none'], }, }, - webSocket : { - ws : { + webSocket: { + ws: { // non-secure ws:// - enabled : false, - port : 8810, + enabled: false, + port: 8810, }, - wss : { + 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'), + 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 : { + resetPassword: { // // The following templates have these variables available to them: // @@ -257,32 +298,41 @@ module.exports = () => { // 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 + 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'), + resetPageTemplate: paths.join( + __dirname, + './../www/reset_password.template.html' + ), }, - http : { - enabled : false, - port : 8080, + 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'), }, - https : { - enabled : false, - port : 8443, - certPem : paths.join(__dirname, './../config/https_cert.pem'), - keyPem : paths.join(__dirname, './../config/https_cert_key.pem'), - } }, - gopher : { - enabled : false, - port : 8070, - publicHostname : 'another-fine-enigma-bbs.org', - publicPort : 8070, // adjust if behind NAT/etc. - staticRoot : paths.join(__dirname, './../gopher'), + gopher: { + enabled: false, + port: 8070, + publicHostname: 'another-fine-enigma-bbs.org', + publicPort: 8070, // adjust if behind NAT/etc. + staticRoot: paths.join(__dirname, './../gopher'), // // Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ] @@ -290,11 +340,11 @@ module.exports = () => { // }, - nntp : { + nntp: { // internal caching of groups, message lists, etc. - cache : { - maxItems : 200, - maxAge : 1000 * 30, // 30s + cache: { + maxItems: 200, + maxAge: 1000 * 30, // 30s }, // @@ -304,57 +354,68 @@ module.exports = () => { // publicMessageConferences: {}, - nntp : { - enabled : false, - port : 8119, + nntp: { + enabled: false, + port: 8119, }, - nntps : { - enabled : false, - port : 8563, - certPem : paths.join(__dirname, './../config/nntps_cert.pem'), - keyPem : paths.join(__dirname, './../config/nntps_key.pem'), - } - } + nntps: { + enabled: false, + port: 8563, + certPem: paths.join(__dirname, './../config/nntps_cert.pem'), + keyPem: paths.join(__dirname, './../config/nntps_key.pem'), + }, + }, }, - chatServers : { + chatServers: { mrc: { - enabled : false, - serverHostname : 'mrc.bottomlessabyss.net', - serverPort : 5000, - retryDelay : 10000, - multiplexerPort : 5000, - } + enabled: false, + serverHostname: 'mrc.bottomlessabyss.net', + serverPort: 5000, + retryDelay: 10000, + multiplexerPort: 5000, + }, }, - infoExtractUtils : { - Exiftool2Desc : { - cmd : `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x + infoExtractUtils: { + Exiftool2Desc: { + cmd: `${__dirname}/../util/exiftool2desc.js`, // ensure chmod +x }, - Exiftool : { - cmd : 'exiftool', - args : [ - '-charset', 'utf8', '{filePath}', + Exiftool: { + cmd: 'exiftool', + args: [ + '-charset', + 'utf8', + '{filePath}', // exclude the following: - '--directory', '--filepermissions', '--exiftoolversion', '--filename', '--filesize', - '--filemodifydate', '--fileaccessdate', '--fileinodechangedate', '--createdate', '--modifydate', - '--metadatadate', '--xmptoolkit' - ] + '--directory', + '--filepermissions', + '--exiftoolversion', + '--filename', + '--filesize', + '--filemodifydate', + '--fileaccessdate', + '--fileinodechangedate', + '--createdate', + '--modifydate', + '--metadatadate', + '--xmptoolkit', + ], }, - XDMS2Desc : { + XDMS2Desc: { // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'd', '{filePath}' ] + cmd: 'xdms', + args: ['d', '{filePath}'], }, - XDMS2LongDesc : { + XDMS2LongDesc: { // http://manpages.ubuntu.com/manpages/trusty/man1/xdms.1.html - cmd : 'xdms', - args : [ 'f', '{filePath}' ] + cmd: 'xdms', + args: ['f', '{filePath}'], }, }, - fileTypes : { + fileTypes: { // // File types explicitly known to the system. Here we can configure // information extraction, archive treatment, etc. @@ -372,65 +433,65 @@ module.exports = () => { // // Audio // - 'audio/mpeg' : { - desc : 'MP3 Audio', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'audio/mpeg': { + desc: 'MP3 Audio', + shortDescUtil: 'Exiftool2Desc', + longDescUtil: 'Exiftool', }, - 'application/pdf' : { - desc : 'Adobe PDF', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'application/pdf': { + desc: 'Adobe PDF', + shortDescUtil: 'Exiftool2Desc', + longDescUtil: 'Exiftool', }, // // Video // - 'video/mp4' : { - desc : 'MPEG Video', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'video/mp4': { + desc: 'MPEG Video', + shortDescUtil: 'Exiftool2Desc', + longDescUtil: 'Exiftool', }, - 'video/x-matroska ' : { - desc : 'Matroska 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', + 'video/x-msvideo': { + desc: 'Audio Video Interleave', + shortDescUtil: 'Exiftool2Desc', + longDescUtil: 'Exiftool', }, // // Images // - 'image/jpeg' : { - desc : 'JPEG Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'image/jpeg': { + desc: 'JPEG Image', + shortDescUtil: 'Exiftool2Desc', + longDescUtil: 'Exiftool', }, - 'image/png' : { - desc : 'Portable Network Graphic Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'image/png': { + desc: 'Portable Network Graphic Image', + shortDescUtil: 'Exiftool2Desc', + longDescUtil: 'Exiftool', }, - 'image/gif' : { - desc : 'Graphics Interchange Format Image', - shortDescUtil : 'Exiftool2Desc', - longDescUtil : 'Exiftool', + 'image/gif': { + desc: 'Graphics Interchange Format Image', + shortDescUtil: 'Exiftool2Desc', + longDescUtil: 'Exiftool', }, - 'image/webp' : { - desc : 'WebP 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 : 'InfoZip', + 'application/zip': { + desc: 'ZIP Archive', + sig: '504b0304', + offset: 0, + archiveHandler: 'InfoZip', }, /* 'application/x-cbr' : { @@ -438,326 +499,375 @@ module.exports = () => { sig : '504b0304', }, */ - 'application/x-arj' : { - desc : 'ARJ Archive', - sig : '60ea', - offset : 0, - archiveHandler : 'Arj', + 'application/x-arj': { + desc: 'ARJ Archive', + sig: '60ea', + offset: 0, + archiveHandler: 'Arj', }, - 'application/x-rar-compressed' : { - desc : 'RAR Archive', - sig : '526172211a07', - offset : 0, - archiveHandler : 'Rar', + 'application/x-rar-compressed': { + desc: 'RAR Archive', + sig: '526172211a07', + offset: 0, + archiveHandler: 'Rar', }, - 'application/gzip' : { - desc : 'Gzip Archive', - sig : '1f8b', - offset : 0, - archiveHandler : 'TarGz', + '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-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-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-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-7z-compressed': { + desc: '7-Zip Archive', + sig: '377abcaf271c', + offset: 0, + archiveHandler: '7Zip', }, // // Generics that need further mapping // - 'application/octet-stream' : [ + 'application/octet-stream': [ { - desc : 'Amiga DISKMASHER', - sig : '444d5321', // DMS! - ext : '.dms', - shortDescUtil : 'XDMS2Desc', - longDescUtil : 'XDMS2LongDesc', + desc: 'Amiga DISKMASHER', + sig: '444d5321', // DMS! + ext: '.dms', + shortDescUtil: 'XDMS2Desc', + longDescUtil: 'XDMS2LongDesc', }, { - desc : 'SIO2PC Atari Disk Image', - sig : '9602', // 16bit sum of "NICKATARI" - ext : '.atr', - archiveHandler : 'Atr', - } - ] + desc: 'SIO2PC Atari Disk Image', + sig: '9602', // 16bit sum of "NICKATARI" + ext: '.atr', + archiveHandler: 'Atr', + }, + ], }, - archives : { - archivers : { - '7Zip' : { // p7zip package - compress : { - cmd : '7za', - args : [ 'a', '-tzip', '{archivePath}', '{fileList}' ], + archives: { + archivers: { + '7Zip': { + // p7zip package + compress: { + cmd: '7za', + args: ['a', '-tzip', '{archivePath}', '{fileList}'], }, - decompress : { - cmd : '7za', - args : [ 'e', '-y', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? + decompress: { + cmd: '7za', + args: ['e', '-y', '-o{extractPath}', '{archivePath}'], // :TODO: should be 'x'? }, - list : { - cmd : '7za', - args : [ 'l', '{archivePath}' ], - entryMatch : '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', + list: { + cmd: '7za', + args: ['l', '{archivePath}'], + entryMatch: + '^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\s[A-Za-z\\.]{5}\\s+([0-9]+)\\s+[0-9]+\\s+([^\\r\\n]+)$', }, - extract : { - cmd : '7za', - args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ], + extract: { + cmd: '7za', + args: [ + 'e', + '-y', + '-o{extractPath}', + '{archivePath}', + '{fileList}', + ], }, }, InfoZip: { - compress : { - cmd : 'zip', - args : [ '{archivePath}', '{fileList}' ], + compress: { + cmd: 'zip', + args: ['{archivePath}', '{fileList}'], }, - decompress : { - cmd : 'unzip', - args : [ '-n', '{archivePath}', '-d', '{extractPath}' ], + decompress: { + cmd: 'unzip', + args: ['-n', '{archivePath}', '-d', '{extractPath}'], }, - list : { - cmd : 'unzip', - args : [ '-l', '{archivePath}' ], + list: { + cmd: 'unzip', + args: ['-l', '{archivePath}'], // Annoyingly, dates can be in YYYY-MM-DD or MM-DD-YYYY format - entryMatch : '^\\s*([0-9]+)\\s+[0-9]{2,4}-[0-9]{2}-[0-9]{2,4}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$', + entryMatch: + '^\\s*([0-9]+)\\s+[0-9]{2,4}-[0-9]{2}-[0-9]{2,4}\\s+[0-9]{2}:[0-9]{2}\\s+([^\\r\\n]+)$', + }, + extract: { + cmd: 'unzip', + args: [ + '-n', + '{archivePath}', + '{fileList}', + '-d', + '{extractPath}', + ], }, - extract : { - cmd : 'unzip', - args : [ '-n', '{archivePath}', '{fileList}', '-d', '{extractPath}' ], - } }, - Lha : { + Lha: { // // 'lha' command can be obtained from: // * apt-get: lhasa // // (compress not currently supported) // - decompress : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}' ], + decompress: { + cmd: 'lha', + args: ['-efw={extractPath}', '{archivePath}'], }, - list : { - cmd : 'lha', - args : [ '-l', '{archivePath}' ], - entryMatch : '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', + list: { + cmd: 'lha', + args: ['-l', '{archivePath}'], + entryMatch: + '^[\\[a-z\\]]+(?:\\s+[0-9]+\\s+[0-9]+|\\s+)([0-9]+)\\s+[0-9]{2}\\.[0-9]\\%\\s+[A-Za-z]{3}\\s+[0-9]{1,2}\\s+[0-9]{4}\\s+([^\\r\\n]+)$', + }, + extract: { + cmd: 'lha', + args: ['-efw={extractPath}', '{archivePath}', '{fileList}'], }, - extract : { - cmd : 'lha', - args : [ '-efw={extractPath}', '{archivePath}', '{fileList}' ] - } }, - Lzx : { + 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', + decompress: { + cmd: 'unlzx', // unzlx doesn't have a output dir option, but we'll cwd to the temp output dir first - args : [ '-x', '{archivePath}' ], + args: ['-x', '{archivePath}'], + }, + list: { + cmd: 'unlzx', + args: ['-v', '{archivePath}'], + entryMatch: + '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', }, - list : { - cmd : 'unlzx', - args : [ '-v', '{archivePath}' ], - entryMatch : '^\\s+([0-9]+)\\s+[^\\s]+\\s+[0-9]{2}:[0-9]{2}:[0-9]{2}\\s+[0-9]{1,2}-[a-z]{3}-[0-9]{4}\\s+[a-z\\-]+\\s+\\"([^"]+)\\"$', - } }, - Arj : { + Arj: { // // 'arj' command can be obtained from: // * apt-get: arj // - decompress : { - cmd : 'arj', - args : [ 'x', '{archivePath}', '{extractPath}' ], + decompress: { + cmd: 'arj', + args: ['x', '{archivePath}', '{extractPath}'], }, - list : { - cmd : 'arj', - args : [ 'l', '{archivePath}' ], - entryMatch : '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', - entryGroupOrder : { // defaults to { byteSize : 1, fileName : 2 } - fileName : 1, - byteSize : 2, - } + list: { + cmd: 'arj', + args: ['l', '{archivePath}'], + entryMatch: + '^([^\\s]+)\\s+([0-9]+)\\s+[0-9]+\\s[0-9\\.]+\\s+[0-9]{2}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\s+(?:[^\\r\\n]+)$', + entryGroupOrder: { + // defaults to { byteSize : 1, fileName : 2 } + fileName: 1, + byteSize: 2, + }, + }, + extract: { + cmd: 'arj', + args: ['e', '{archivePath}', '{extractPath}', '{fileList}'], }, - extract : { - cmd : 'arj', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], - } }, - Rar : { - decompress : { - cmd : 'unrar', - args : [ 'x', '{archivePath}', '{extractPath}' ], + 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,4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', + list: { + cmd: 'unrar', + args: ['l', '{archivePath}'], + entryMatch: + '^\\s+[\\.A-Z]+\\s+([\\d]+)\\s{2}[0-9]{2,4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s{2}([^\\r\\n]+)$', + }, + extract: { + cmd: 'unrar', + args: ['e', '{archivePath}', '{extractPath}', '{fileList}'], }, - extract : { - cmd : 'unrar', - args : [ 'e', '{archivePath}', '{extractPath}', '{fileList}' ], - } }, - TarGz : { - decompress : { - cmd : 'tar', - args : [ '-xf', '{archivePath}', '-C', '{extractPath}', '--strip-components=1' ], + 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]+)$', + list: { + cmd: 'tar', + args: ['-tvf', '{archivePath}'], + entryMatch: + '^[drwx\\-]{10}\\s[A-Za-z0-9\\/]+\\s+([0-9]+)\\s[0-9]{4}\\-[0-9]{2}\\-[0-9]{2}\\s[0-9]{2}\\:[0-9]{2}\\s([^\\r\\n]+)$', + }, + extract: { + cmd: 'tar', + args: [ + '-xvf', + '{archivePath}', + '-C', + '{extractPath}', + '{fileList}', + ], }, - extract : { - cmd : 'tar', - args : [ '-xvf', '{archivePath}', '-C', '{extractPath}', '{fileList}' ], - } }, - Atr : { - decompress : { - cmd : 'atr', - args : [ '{archivePath}', 'x', '-a', '-o', '{extractPath}' ] + Atr: { + decompress: { + cmd: 'atr', + args: ['{archivePath}', 'x', '-a', '-o', '{extractPath}'], }, - list : { - cmd : 'atr', - args : [ '{archivePath}', 'ls', '-la1' ], - entryMatch : '^[rwxs-]{5}\\s+([0-9]+)\\s\\([0-9\\s]+\\)\\s([^\\r\\n\\s]*)(?:[^\\r\\n]+)?$', + list: { + cmd: 'atr', + args: ['{archivePath}', 'ls', '-la1'], + entryMatch: + '^[rwxs-]{5}\\s+([0-9]+)\\s\\([0-9\\s]+\\)\\s([^\\r\\n\\s]*)(?:[^\\r\\n]+)?$', }, - extract : { - cmd : 'atr', + extract: { + cmd: 'atr', // note: -l converts Atari 0x9b line feeds to 0x0a; not ideal if we're dealing with a binary of course. - args : [ '{archivePath}', 'x', '-a', '-l', '-o', '{extractPath}', '{fileList}' ] - } - } + args: [ + '{archivePath}', + 'x', + '-a', + '-l', + '-o', + '{extractPath}', + '{fileList}', + ], + }, + }, }, }, - fileTransferProtocols : { + fileTransferProtocols: { // // See http://www.synchro.net/docs/sexyz.txt for information on SEXYZ // - zmodem8kSexyz : { - name : 'ZModem 8k (SEXYZ)', - type : 'external', - sort : 1, - external : { + 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 // Linux x86_64 binary: https://l33t.codes/outgoing/sexyz - sendCmd : 'sexyz', - sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ], - recvCmd : 'sexyz', - recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ], - recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ], - } + 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 : [ + 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}' + '--zmodem', + '--try-8k', + '--binary', + '--restricted', + '{filePaths}', ], - recvCmd : 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" - recvArgs : [ - '--zmodem', '--binary', '--restricted', '--keep-uppercase', // dumps to CWD which is set to {uploadDir} + recvCmd: 'rz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" + recvArgs: [ + '--zmodem', + '--binary', + '--restricted', + '--keep-uppercase', // dumps to CWD which is set to {uploadDir} ], - processIACs : true, // escape/de-escape IACs (0xff) - } - } + processIACs: true, // escape/de-escape IACs (0xff) + }, + }, }, - messageAreaDefaults : { + messageAreaDefaults: { // // The following can be override per-area as well // - maxMessages : 1024, // 0 = unlimited - maxAgeDays : 0, // 0 = unlimited + maxMessages: 1024, // 0 = unlimited + maxAgeDays: 0, // 0 = unlimited }, - messageConferences : { - system_internal : { - name : 'System Internal', - desc : 'Built in conference for private messages, bulletins, etc.', + 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. + 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 }, @@ -767,50 +877,51 @@ module.exports = () => { // Actual sizes may be slightly larger when we must place a full // PKT contents *somewhere* // - packetTargetByteSize : 512000, // 512k, before placing messages in a new pkt - bundleTargetByteSize : 2048000, // 2M, before creating another archive - packetMsgEncoding : 'utf8', // default packet encoding. Override per node if desired. - packetAnsiMsgEncoding : 'cp437', // packet encoding for *ANSI ART* messages + packetTargetByteSize: 512000, // 512k, before placing messages in a new pkt + bundleTargetByteSize: 2048000, // 2M, before creating another archive + packetMsgEncoding: 'utf8', // default packet encoding. Override per node if desired. + packetAnsiMsgEncoding: 'cp437', // packet encoding for *ANSI ART* messages - tic : { - secureInOnly : true, // only bring in from secure inbound (|secInbound| path, password protected) - uploadBy : 'ENiGMA TIC', // default upload by username (override @ network) - allowReplace : false, // use "Replaces" TIC field - descPriority : 'diz', // May be diz=.DIZ/etc., or tic=from TIC Ldesc - } - } + 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/'), + 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$', // eslint-disable-line no-useless-escape - '^.*DESC\.SDI$', // eslint-disable-line no-useless-escape - '^.*DESCRIPT\.ION$', // eslint-disable-line no-useless-escape - '^.*FILE\.DES$', // eslint-disable-line no-useless-escape - '^.*FILE\.SDI$', // eslint-disable-line no-useless-escape - '^.*DISK\.ID$' // eslint-disable-line no-useless-escape + desc: [ + '^.*FILE_ID.ANS$', + '^.*FILE_ID.DIZ$', // eslint-disable-line no-useless-escape + '^.*DESC.SDI$', // eslint-disable-line no-useless-escape + '^.*DESCRIPT.ION$', // eslint-disable-line no-useless-escape + '^.*FILE.DES$', // eslint-disable-line no-useless-escape + '^.*FILE.SDI$', // eslint-disable-line no-useless-escape + '^.*DISK.ID$', // eslint-disable-line no-useless-escape ], // common README filename - https://en.wikipedia.org/wiki/README - descLong : [ - '^[^/\]*\.NFO$', // eslint-disable-line no-useless-escape - '^.*README\.1ST$', // eslint-disable-line no-useless-escape - '^.*README\.NOW$', // eslint-disable-line no-useless-escape - '^.*README\.TXT$', // eslint-disable-line no-useless-escape - '^.*READ\.ME$', // eslint-disable-line no-useless-escape - '^.*README$', // eslint-disable-line no-useless-escape - '^.*README\.md$', // eslint-disable-line no-useless-escape - '^RELEASE-INFO.ASC$' // eslint-disable-line no-useless-escape + descLong: [ + '^[^/]*.NFO$', // eslint-disable-line no-useless-escape + '^.*README.1ST$', // eslint-disable-line no-useless-escape + '^.*README.NOW$', // eslint-disable-line no-useless-escape + '^.*README.TXT$', // eslint-disable-line no-useless-escape + '^.*READ.ME$', // eslint-disable-line no-useless-escape + '^.*README$', // eslint-disable-line no-useless-escape + '^.*README.md$', // eslint-disable-line no-useless-escape + '^RELEASE-INFO.ASC$', // eslint-disable-line no-useless-escape ], }, @@ -819,60 +930,59 @@ module.exports = () => { // 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]\\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, ... + '\\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', + 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' ], + 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 : { - - events : { - dailyMaintenance : { - schedule : 'at 11:59pm', - action : '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', + eventScheduler: { + events: { + dailyMaintenance: { + schedule: 'at 11:59pm', + action: '@method:core/misc_scheduled_events.js:dailyMaintenanceScheduledEvent', }, - trimMessageAreas : { + trimMessageAreas: { // may optionally use [or ]@watch:/path/to/file - schedule : 'every 24 hours', + schedule: 'every 24 hours', // action: // - @method:path/to/module.js:theMethodName @@ -880,32 +990,32 @@ module.exports = () => { // // - @execute:/path/to/something/executable.sh // - action : '@method:core/message_area.js:trimMessageAreasScheduledEvent', + action: '@method:core/message_area.js:trimMessageAreasScheduledEvent', }, - nntpMaintenance : { - schedule : 'every 12 hours', // should generally be < trimMessageAreas interval - action : '@method:core/servers/content/nntp.js:performMaintenanceTask', + nntpMaintenance: { + schedule: 'every 12 hours', // should generally be < trimMessageAreas interval + action: '@method:core/servers/content/nntp.js:performMaintenanceTask', }, - updateFileAreaStats : { - schedule : 'every 1 hours', - action : '@method:core/file_base_area.js:updateAreaStatsScheduledEvent', + 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 }, - twoFactorRegisterTokenMaintenance : { - schedule : 'every 24 hours', - action : '@method:core/user_temp_token.js:temporaryTokenMaintenanceTask', - args : [ + twoFactorRegisterTokenMaintenance: { + schedule: 'every 24 hours', + action: '@method:core/user_temp_token.js:temporaryTokenMaintenanceTask', + args: [ 'auth_factor2_otp_register', '24 hours', // expire time - ] + ], }, // @@ -918,29 +1028,30 @@ module.exports = () => { action : '@method:core/file_base_list_export.js:updateFileBaseDescFilesScheduledEvent', } */ - } + }, }, - logging : { - rotatingFile : { // set to 'disabled' or false to disable - type : 'rotating-file', - fileName : 'enigma-bbs.log', - period : '1d', - count : 3, - level : 'debug', - } + logging: { + 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 }, - debug : { - assertsEnabled : false, + debug: { + assertsEnabled: false, }, - statLog : { - systemEvents : { + statLog: { + systemEvents: { loginHistoryMax: -1, // set to -1 for forever - } + }, }, }; -}; \ No newline at end of file +}; diff --git a/core/config_loader.js b/core/config_loader.js index 28473d88..c84f0186 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -14,23 +14,21 @@ module.exports = class ConfigLoader { defaultsCustomizer = null, onReload = null, keepWsc = false, - } = - { - hotReload : true, - defaultConfig : {}, - defaultsCustomizer : null, - onReload : null, - keepWsc : false, + } = { + hotReload: true, + defaultConfig: {}, + defaultsCustomizer: null, + onReload: null, + keepWsc: false, } - ) - { + ) { this.current = {}; - this.hotReload = hotReload; - this.defaultConfig = defaultConfig; + this.hotReload = hotReload; + this.defaultConfig = defaultConfig; this.defaultsCustomizer = defaultsCustomizer; - this.onReload = onReload; - this.keepWsc = keepWsc; + this.onReload = onReload; + this.keepWsc = keepWsc; } init(baseConfigPath, cb) { @@ -61,7 +59,7 @@ module.exports = class ConfigLoader { // async.waterfall( [ - (callback) => { + callback => { return this._loadConfigFile(baseConfigPath, callback); }, (config, callback) => { @@ -72,16 +70,17 @@ module.exports = class ConfigLoader { config, (defaultVal, configVal, key, target, source) => { var path; - while (true) { // eslint-disable-line no-constant-condition + while (true) { + // eslint-disable-line no-constant-condition if (!stack.length) { - stack.push({source, path : []}); + stack.push({ source, path: [] }); } const prev = stack[stack.length - 1]; if (source === prev.source) { path = prev.path.concat(key); - stack.push({source : configVal, path}); + stack.push({ source: configVal, path }); break; } @@ -89,7 +88,12 @@ module.exports = class ConfigLoader { } path = path.join('.'); - return this.defaultsCustomizer(defaultVal, configVal, key, path); + return this.defaultsCustomizer( + defaultVal, + configVal, + key, + path + ); } ); @@ -118,12 +122,12 @@ module.exports = class ConfigLoader { _convertTo(value, type) { switch (type) { - case 'bool' : - case 'boolean' : - value = ('1' === value || 'true' === value.toLowerCase()); + case 'bool': + case 'boolean': + value = '1' === value || 'true' === value.toLowerCase(); break; - case 'number' : + case 'number': { const num = parseInt(value); if (!isNaN(num)) { @@ -132,15 +136,15 @@ module.exports = class ConfigLoader { } break; - case 'object' : + case 'object': try { value = JSON.parse(value); - } catch(e) { + } catch (e) { // ignored } break; - case 'timestamp' : + case 'timestamp': { const m = moment(value); if (m.isValid()) { @@ -162,7 +166,9 @@ module.exports = class ConfigLoader { let value = process.env[varName]; if (!value) { // console is about as good as we can do here - return console.info(`WARNING: environment variable "${varName}" from spec "${spec}" not found!`); + return console.info( + `WARNING: environment variable "${varName}" from spec "${spec}" not found!` + ); } if ('array' === array) { @@ -179,9 +185,9 @@ module.exports = class ConfigLoader { const options = { filePath, - hotReload : this.hotReload, - keepWsc : this.keepWsc, - callback : this._configFileChanged.bind(this), + hotReload: this.hotReload, + keepWsc: this.keepWsc, + callback: this._configFileChanged.bind(this), }; ConfigCache.getConfigWithOptions(options, (err, config) => { @@ -192,7 +198,7 @@ module.exports = class ConfigLoader { }); } - _configFileChanged({fileName, fileRoot}) { + _configFileChanged({ fileName, fileRoot }) { const reCachedPath = paths.join(fileRoot, fileName); if (this.configPaths.includes(reCachedPath)) { this._reload(this.baseConfigPath, err => { @@ -205,44 +211,44 @@ module.exports = class ConfigLoader { _resolveIncludes(configRoot, config, cb) { if (!Array.isArray(config.includes)) { - this.configPaths = [ this.baseConfigPath ]; + this.configPaths = [this.baseConfigPath]; return cb(null, config); } // If a included file is changed, we need to re-cache, so this // must be tracked... const includePaths = config.includes.map(inc => paths.join(configRoot, inc)); - async.eachSeries(includePaths, (includePath, nextIncludePath) => { - this._loadConfigFile(includePath, (err, includedConfig) => { - if (err) { - return nextIncludePath(err); - } - - _.defaultsDeep(config, includedConfig); - return nextIncludePath(null); - }); - }, - err => { - this.configPaths = [ this.baseConfigPath, ...includePaths ]; - return cb(err, config); - }); - } - - _resolveAtSpecs(config) { - return mapValuesDeep( - config, - value => { - if (_.isString(value) && '@' === value.charAt(0)) { - if (value.startsWith('@reference:')) { - const refPath = value.slice(11); - value = _.get(config, refPath, value); - } else if (value.startsWith('@environment:')) { - value = this._resolveEnvironmentVariable(value) || value; + async.eachSeries( + includePaths, + (includePath, nextIncludePath) => { + this._loadConfigFile(includePath, (err, includedConfig) => { + if (err) { + return nextIncludePath(err); } - } - return value; + _.defaultsDeep(config, includedConfig); + return nextIncludePath(null); + }); + }, + err => { + this.configPaths = [this.baseConfigPath, ...includePaths]; + return cb(err, config); } ); } + + _resolveAtSpecs(config) { + return mapValuesDeep(config, value => { + if (_.isString(value) && '@' === value.charAt(0)) { + if (value.startsWith('@reference:')) { + const refPath = value.slice(11); + value = _.get(config, refPath, value); + } else if (value.startsWith('@environment:')) { + value = this._resolveEnvironmentVariable(value) || value; + } + } + + return value; + }); + } }; diff --git a/core/connect.js b/core/connect.js index 7301e098..83b6348e 100644 --- a/core/connect.js +++ b/core/connect.js @@ -2,26 +2,26 @@ 'use strict'; // ENiGMA½ -const ansi = require('./ansi_term.js'); -const Events = require('./events.js'); -const Config = require('./config.js').get; -const { Errors } = require('./enig_error.js'); +const ansi = require('./ansi_term.js'); +const Events = require('./events.js'); +const Config = require('./config.js').get; +const { Errors } = require('./enig_error.js'); // deps -const async = require('async'); +const async = require('async'); -exports.connectEntry = connectEntry; +exports.connectEntry = connectEntry; const withCursorPositionReport = (client, cprHandler, failMessage, cb) => { let giveUpTimer; - const done = function(err) { + const done = function (err) { client.removeListener('cursor position report', cprListener); clearTimeout(giveUpTimer); return cb(err); }; - const cprListener = (pos) => { + const cprListener = pos => { cprHandler(pos); return done(null); }; @@ -29,10 +29,10 @@ const withCursorPositionReport = (client, cprHandler, failMessage, cb) => { client.once('cursor position report', cprListener); // give up after 2s - giveUpTimer = setTimeout( () => { + giveUpTimer = setTimeout(() => { return done(Errors.General(failMessage)); }, 2000); -} +}; function ansiDiscoverHomePosition(client, cb) { // @@ -41,7 +41,7 @@ function ansiDiscoverHomePosition(client, cb) { // think of home as 0,0. If this is the case, we need to offset // our positioning to accommodate for such. // - if( !Config().term.checkAnsiHomePosition ) { + if (!Config().term.checkAnsiHomePosition) { // Skip (and assume 1,1) if the home position check is disabled. return cb(null); } @@ -54,11 +54,14 @@ function ansiDiscoverHomePosition(client, cb) { // // We expect either 0,0, or 1,1. Anything else will be filed as bad data // - if(h > 1 || w > 1) { - return client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); + if (h > 1 || w > 1) { + return client.log.warn( + { height: h, width: w }, + 'Ignoring ANSI home position CPR due to unexpected values' + ); } - if(0 === h & 0 === w) { + if ((0 === h) & (0 === w)) { // // Store a CPR offset in the client. All CPR's from this point on will offset by this amount // @@ -70,7 +73,7 @@ function ansiDiscoverHomePosition(client, cb) { cb ); - client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos + client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos } function ansiAttemptDetectUTF8(client, cb) { @@ -87,7 +90,7 @@ function ansiAttemptDetectUTF8(client, cb) { // "*nix" terminal -- that is, xterm, etc. // Also skip this check if checkUtf8Encoding is disabled in the config - if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) { + if (!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) { return cb(null); } @@ -99,20 +102,24 @@ function ansiAttemptDetectUTF8(client, cb) { pos => { initialPosition = pos; - withCursorPositionReport(client, + withCursorPositionReport( + client, pos => { const [_, w] = pos; const len = w - initialPosition[1]; - if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull - client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".'); + if (!isNaN(len) && len >= ASCIIPortion.length + 6) { + // CP437 displays 3 chars each Unicode skull + client.log.info( + 'Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".' + ); client.setTermType('ansi'); } }, 'Detect UTF-8 stage 2 timed out', - cb, + cb ); - client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side + client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side client.term.rawWrite(ansi.queryPos()); }, 'Detect UTF-8 stage 1 timed out', @@ -150,7 +157,9 @@ const ansiQuerySyncTermFontSupport = (client, cb) => { const [_, w] = pos; if (w === 1) { // cursor didn't move - client.log.info('Client supports SyncTERM fonts or properly ignores unknown ESC sequence'); + client.log.info( + 'Client supports SyncTERM fonts or properly ignores unknown ESC sequence' + ); client.term.syncTermFontsEnabled = true; } }, @@ -158,11 +167,13 @@ const ansiQuerySyncTermFontSupport = (client, cb) => { cb ); - client.term.rawWrite(`${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`); -} + client.term.rawWrite( + `${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}` + ); +}; function ansiQueryTermSizeIfNeeded(client, cb) { - if(client.term.termHeight > 0 || client.term.termWidth > 0) { + if (client.term.termHeight > 0 || client.term.termWidth > 0) { return cb(null); } @@ -172,7 +183,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) { // // If we've already found out, disregard // - if(client.term.termHeight > 0 || client.term.termWidth > 0) { + if (client.term.termHeight > 0 || client.term.termWidth > 0) { return; } @@ -182,20 +193,21 @@ function ansiQueryTermSizeIfNeeded(client, cb) { // 999x999 values we asked to move to. // const [h, w] = pos; - if(h < 10 || h === 999 || w < 10 || w === 999) { + if (h < 10 || h === 999 || w < 10 || w === 999) { return client.log.warn( - { height : h, width : w }, - 'Ignoring ANSI CPR screen size query response due to non-sane values'); + { height: h, width: w }, + 'Ignoring ANSI CPR screen size query response due to non-sane values' + ); } - client.term.termHeight = h; - client.term.termWidth = w; + client.term.termHeight = h; + client.term.termWidth = w; client.log.debug( { - termWidth : client.term.termWidth, - termHeight : client.term.termHeight, - source : 'ANSI CPR' + termWidth: client.term.termWidth, + termHeight: client.term.termHeight, + source: 'ANSI CPR', }, 'Window size updated' ); @@ -226,8 +238,7 @@ function displayBanner(term) { |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN |06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ -|00` - ); +|00`); } function connectEntry(client, nextMenu) { @@ -245,20 +256,23 @@ function connectEntry(client, nextMenu) { }, function queryTermSizeByNonStandardAnsi(callback) { ansiQueryTermSizeIfNeeded(client, err => { - if(err) { + if (err) { // // Check again; We may have got via NAWS/similar before CPR completed. // - if(0 === term.termHeight || 0 === term.termWidth) { + 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 currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? - client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!'); + client.log.warn( + { reason: err.message }, + 'Failed to negotiate term size; Defaulting to 80x25!' + ); term.termHeight = 25; - term.termWidth = 80; + term.termWidth = 80; } } @@ -268,7 +282,7 @@ function connectEntry(client, nextMenu) { function checkUtf8IfNeeded(callback) { return ansiAttemptDetectUTF8(client, callback); }, - function querySyncTERMFontSupport(callback) { + function querySyncTERMFontSupport(callback) { return ansiQuerySyncTermFontSupport(client, callback); }, ], @@ -281,9 +295,9 @@ function connectEntry(client, nextMenu) { displayBanner(term); // fire event - Events.emit(Events.getSystemEvents().TermDetected, { client : client } ); + Events.emit(Events.getSystemEvents().TermDetected, { client: client }); - setTimeout( () => { + setTimeout(() => { return client.menuStack.goto(nextMenu); }, 500); } diff --git a/core/cp437util.js b/core/cp437util.js index 9e0b2033..9fad745c 100644 --- a/core/cp437util.js +++ b/core/cp437util.js @@ -1,47 +1,265 @@ - - const CP437UnicodeTable = [ - '\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', - '\u0007', '\u0008', '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', - '\u000E', '\u000F', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', - '\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001A', '\u001B', - '\u001C', '\u001D', '\u001E', '\u001F', '\u0020', '\u0021', '\u0022', - '\u0023', '\u0024', '\u0025', '\u0026', '\u0027', '\u0028', '\u0029', - '\u002A', '\u002B', '\u002C', '\u002D', '\u002E', '\u002F', '\u0030', - '\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', - '\u0038', '\u0039', '\u003A', '\u003B', '\u003C', '\u003D', '\u003E', - '\u003F', '\u0040', '\u0041', '\u0042', '\u0043', '\u0044', '\u0045', - '\u0046', '\u0047', '\u0048', '\u0049', '\u004A', '\u004B', '\u004C', - '\u004D', '\u004E', '\u004F', '\u0050', '\u0051', '\u0052', '\u0053', - '\u0054', '\u0055', '\u0056', '\u0057', '\u0058', '\u0059', '\u005A', - '\u005B', '\u005C', '\u005D', '\u005E', '\u005F', '\u0060', '\u0061', - '\u0062', '\u0063', '\u0064', '\u0065', '\u0066', '\u0067', '\u0068', - '\u0069', '\u006A', '\u006B', '\u006C', '\u006D', '\u006E', '\u006F', - '\u0070', '\u0071', '\u0072', '\u0073', '\u0074', '\u0075', '\u0076', - '\u0077', '\u0078', '\u0079', '\u007A', '\u007B', '\u007C', '\u007D', - '\u007E', '\u007F', '\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4', - '\u00E0', '\u00E5', '\u00E7', '\u00EA', '\u00EB', '\u00E8', '\u00EF', - '\u00EE', '\u00EC', '\u00C4', '\u00C5', '\u00C9', '\u00E6', '\u00C6', - '\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9', '\u00FF', '\u00D6', - '\u00DC', '\u00A2', '\u00A3', '\u00A5', '\u20A7', '\u0192', '\u00E1', - '\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1', '\u00AA', '\u00BA', - '\u00BF', '\u2310', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB', - '\u00BB', '\u2591', '\u2592', '\u2593', '\u2502', '\u2524', '\u2561', - '\u2562', '\u2556', '\u2555', '\u2563', '\u2551', '\u2557', '\u255D', - '\u255C', '\u255B', '\u2510', '\u2514', '\u2534', '\u252C', '\u251C', - '\u2500', '\u253C', '\u255E', '\u255F', '\u255A', '\u2554', '\u2569', - '\u2566', '\u2560', '\u2550', '\u256C', '\u2567', '\u2568', '\u2564', - '\u2565', '\u2559', '\u2558', '\u2552', '\u2553', '\u256B', '\u256A', - '\u2518', '\u250C', '\u2588', '\u2584', '\u258C', '\u2590', '\u2580', - '\u03B1', '\u00DF', '\u0393', '\u03C0', '\u03A3', '\u03C3', '\u00B5', - '\u03C4', '\u03A6', '\u0398', '\u03A9', '\u03B4', '\u221E', '\u03C6', - '\u03B5', '\u2229', '\u2261', '\u00B1', '\u2265', '\u2264', '\u2320', - '\u2321', '\u00F7', '\u2248', '\u00B0', '\u2219', '\u00B7', '\u221A', - '\u207F', '\u00B2', '\u25A0', '\u00A0' + '\u0000', + '\u0001', + '\u0002', + '\u0003', + '\u0004', + '\u0005', + '\u0006', + '\u0007', + '\u0008', + '\u0009', + '\u000A', + '\u000B', + '\u000C', + '\u000D', + '\u000E', + '\u000F', + '\u0010', + '\u0011', + '\u0012', + '\u0013', + '\u0014', + '\u0015', + '\u0016', + '\u0017', + '\u0018', + '\u0019', + '\u001A', + '\u001B', + '\u001C', + '\u001D', + '\u001E', + '\u001F', + '\u0020', + '\u0021', + '\u0022', + '\u0023', + '\u0024', + '\u0025', + '\u0026', + '\u0027', + '\u0028', + '\u0029', + '\u002A', + '\u002B', + '\u002C', + '\u002D', + '\u002E', + '\u002F', + '\u0030', + '\u0031', + '\u0032', + '\u0033', + '\u0034', + '\u0035', + '\u0036', + '\u0037', + '\u0038', + '\u0039', + '\u003A', + '\u003B', + '\u003C', + '\u003D', + '\u003E', + '\u003F', + '\u0040', + '\u0041', + '\u0042', + '\u0043', + '\u0044', + '\u0045', + '\u0046', + '\u0047', + '\u0048', + '\u0049', + '\u004A', + '\u004B', + '\u004C', + '\u004D', + '\u004E', + '\u004F', + '\u0050', + '\u0051', + '\u0052', + '\u0053', + '\u0054', + '\u0055', + '\u0056', + '\u0057', + '\u0058', + '\u0059', + '\u005A', + '\u005B', + '\u005C', + '\u005D', + '\u005E', + '\u005F', + '\u0060', + '\u0061', + '\u0062', + '\u0063', + '\u0064', + '\u0065', + '\u0066', + '\u0067', + '\u0068', + '\u0069', + '\u006A', + '\u006B', + '\u006C', + '\u006D', + '\u006E', + '\u006F', + '\u0070', + '\u0071', + '\u0072', + '\u0073', + '\u0074', + '\u0075', + '\u0076', + '\u0077', + '\u0078', + '\u0079', + '\u007A', + '\u007B', + '\u007C', + '\u007D', + '\u007E', + '\u007F', + '\u00C7', + '\u00FC', + '\u00E9', + '\u00E2', + '\u00E4', + '\u00E0', + '\u00E5', + '\u00E7', + '\u00EA', + '\u00EB', + '\u00E8', + '\u00EF', + '\u00EE', + '\u00EC', + '\u00C4', + '\u00C5', + '\u00C9', + '\u00E6', + '\u00C6', + '\u00F4', + '\u00F6', + '\u00F2', + '\u00FB', + '\u00F9', + '\u00FF', + '\u00D6', + '\u00DC', + '\u00A2', + '\u00A3', + '\u00A5', + '\u20A7', + '\u0192', + '\u00E1', + '\u00ED', + '\u00F3', + '\u00FA', + '\u00F1', + '\u00D1', + '\u00AA', + '\u00BA', + '\u00BF', + '\u2310', + '\u00AC', + '\u00BD', + '\u00BC', + '\u00A1', + '\u00AB', + '\u00BB', + '\u2591', + '\u2592', + '\u2593', + '\u2502', + '\u2524', + '\u2561', + '\u2562', + '\u2556', + '\u2555', + '\u2563', + '\u2551', + '\u2557', + '\u255D', + '\u255C', + '\u255B', + '\u2510', + '\u2514', + '\u2534', + '\u252C', + '\u251C', + '\u2500', + '\u253C', + '\u255E', + '\u255F', + '\u255A', + '\u2554', + '\u2569', + '\u2566', + '\u2560', + '\u2550', + '\u256C', + '\u2567', + '\u2568', + '\u2564', + '\u2565', + '\u2559', + '\u2558', + '\u2552', + '\u2553', + '\u256B', + '\u256A', + '\u2518', + '\u250C', + '\u2588', + '\u2584', + '\u258C', + '\u2590', + '\u2580', + '\u03B1', + '\u00DF', + '\u0393', + '\u03C0', + '\u03A3', + '\u03C3', + '\u00B5', + '\u03C4', + '\u03A6', + '\u0398', + '\u03A9', + '\u03B4', + '\u221E', + '\u03C6', + '\u03B5', + '\u2229', + '\u2261', + '\u00B1', + '\u2265', + '\u2264', + '\u2320', + '\u2321', + '\u00F7', + '\u2248', + '\u00B0', + '\u2219', + '\u00B7', + '\u221A', + '\u207F', + '\u00B2', + '\u25A0', + '\u00A0', ]; -const NonCP437EncodableRegExp = /[^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\u00A0]/; // eslint-disable-line no-control-regex -const isCP437Encodable = (s) => { +const NonCP437EncodableRegExp = + /[^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\u00A0]/; // eslint-disable-line no-control-regex +const isCP437Encodable = s => { if (!s.length) { return true; } diff --git a/core/crc.js b/core/crc.js index f90ac961..634df525 100644 --- a/core/crc.js +++ b/core/crc.js @@ -38,7 +38,7 @@ const CRC32_TABLE = new Int32Array([ 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 + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, ]); exports.CRC32 = class CRC32 { @@ -52,40 +52,40 @@ exports.CRC32 = class CRC32 { } update_4(input) { - const len = input.length - 3; - let i = 0; + const len = input.length - 3; + let i = 0; - for(i = 0; i < len;) { - this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; - 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 ]; + 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 ]; + 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; + 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 ]; + 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 ]; + while (i < len + 7) { + this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff]; } } finalize() { - return (this.crc ^ (-1)) >>> 0; + return (this.crc ^ -1) >>> 0; } }; diff --git a/core/database.js b/core/database.js index 55ed4e2e..9266001d 100644 --- a/core/database.js +++ b/core/database.js @@ -5,25 +5,25 @@ const conf = require('./config'); // deps -const sqlite3 = require('sqlite3'); -const sqlite3Trans = require('sqlite3-trans'); -const paths = require('path'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); -const moment = require('moment'); +const sqlite3 = require('sqlite3'); +const sqlite3Trans = require('sqlite3-trans'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); +const moment = require('moment'); // database handles const dbs = {}; -exports.getTransactionDatabase = getTransactionDatabase; -exports.getModDatabasePath = getModDatabasePath; -exports.loadDatabaseForMod = loadDatabaseForMod; -exports.getISOTimestampString = getISOTimestampString; -exports.sanitizeString = sanitizeString; -exports.initializeDatabases = initializeDatabases; +exports.getTransactionDatabase = getTransactionDatabase; +exports.getModDatabasePath = getModDatabasePath; +exports.loadDatabaseForMod = loadDatabaseForMod; +exports.getISOTimestampString = getISOTimestampString; +exports.sanitizeString = sanitizeString; +exports.initializeDatabases = initializeDatabases; -exports.dbs = dbs; +exports.dbs = dbs; function getTransactionDatabase(db) { return sqlite3Trans.wrap(db); @@ -40,37 +40,38 @@ function getModDatabasePath(moduleInfo, suffix) { // We expect that moduleInfo defines packageName which will be the base of the modules // filename. An optional suffix may be supplied as well. // - const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; + const HOST_RE = + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; assert(_.isObject(moduleInfo)); assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); let full = moduleInfo.packageName; - if(suffix) { + 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'); + full.split('.').length > 1 && HOST_RE.test(full), + 'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation' + ); const Config = conf.get(); return paths.join(Config.paths.modsDb, `${full}.sqlite3`); } function loadDatabaseForMod(modInfo, cb) { - const db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(modInfo), - err => { + const db = getTransactionDatabase( + new sqlite3.Database(getModDatabasePath(modInfo), err => { return cb(err, db); - } - )); + }) + ); } function getISOTimestampString(ts) { ts = ts || moment(); - if(!moment.isMoment(ts)) { - if(_.isString(ts)) { + if (!moment.isMoment(ts)) { + if (_.isString(ts)) { ts = ts.replace(/\//g, '-'); } ts = moment(ts); @@ -79,42 +80,55 @@ function getISOTimestampString(ts) { } function sanitizeString(s) { - return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex + return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { + // eslint-disable-line no-control-regex switch (c) { - case '\0' : return '\\0'; - case '\x08' : return '\\b'; - case '\x09' : return '\\t'; - case '\x1a' : return '\\z'; - case '\n' : return '\\n'; - case '\r' : return '\\r'; + case '\0': + return '\\0'; + case '\x08': + return '\\b'; + case '\x09': + return '\\t'; + case '\x1a': + return '\\z'; + case '\n': + return '\\n'; + case '\r': + return '\\r'; - case '"' : - case '\'' : + case '"': + case "'": return `${c}${c}`; - case '\\' : - case '%' : + case '\\': + case '%': return `\\${c}`; } }); } function initializeDatabases(cb) { - async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { - dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { - 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) { @@ -122,7 +136,7 @@ function enableForeignKeys(db) { } const DB_INIT_TABLE = { - system : (cb) => { + system: cb => { enableForeignKeys(dbs.system); // Various stat/event logging - see stat_log.js @@ -160,7 +174,7 @@ const DB_INIT_TABLE = { return cb(null); }, - user : (cb) => { + user: cb => { enableForeignKeys(dbs.user); dbs.user.run( @@ -229,7 +243,7 @@ const DB_INIT_TABLE = { return cb(null); }, - message : (cb) => { + message: cb => { enableForeignKeys(dbs.message); dbs.message.run( @@ -296,7 +310,6 @@ const DB_INIT_TABLE = { );` ); - // :TODO: need SQL to ensure cleaned up if delete from message? /* dbs.message.run( @@ -337,7 +350,7 @@ const DB_INIT_TABLE = { return cb(null); }, - file : (cb) => { + file: cb => { enableForeignKeys(dbs.file); dbs.file.run( @@ -457,5 +470,5 @@ const DB_INIT_TABLE = { ); return cb(null); - } -}; \ No newline at end of file + }, +}; diff --git a/core/descript_ion_file.js b/core/descript_ion_file.js index d34551ba..e47699a6 100644 --- a/core/descript_ion_file.js +++ b/core/descript_ion_file.js @@ -1,12 +1,12 @@ /* jslint node: true */ 'use strict'; -const { Errors } = require('./enig_error.js'); +const { Errors } = require('./enig_error.js'); // deps -const fs = require('graceful-fs'); -const iconv = require('iconv-lite'); -const async = require('async'); +const fs = require('graceful-fs'); +const iconv = require('iconv-lite'); +const async = require('async'); module.exports = class DescriptIonFile { constructor() { @@ -19,14 +19,14 @@ module.exports = class DescriptIonFile { getDescription(fileName) { const entry = this.get(fileName); - if(entry) { + if (entry) { return entry.desc; } } static createFromFile(path, cb) { fs.readFile(path, (err, descData) => { - if(err) { + if (err) { return cb(err); } @@ -35,43 +35,48 @@ module.exports = class DescriptIonFile { // DESCRIPT.ION entries are terminated with a CR and/or LF const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); - async.each(lines, (entryData, nextLine) => { - // - // We allow quoted (long) filenames or non-quoted filenames. - // 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]; - - // - // 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], + 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); } - ); - return nextLine(null); - }, - () => { - return cb( - descIonFile.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized DESCRIPT.ION format'), - descIonFile - ); - }); + const fileName = parts[1] || parts[2]; + + // + // Un-escape CR/LF's + // - escapped \r and/or \n + // - BBBS style @n - See https://www.bbbs.net/sysop.html + // + const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n'); + + descIonFile.entries.set(fileName, { + desc: desc, + programId: parts[4], + programData: parts[5], + }); + + return nextLine(null); + }, + () => { + return cb( + descIonFile.entries.size > 0 + ? null + : Errors.Invalid( + 'Invalid or unrecognized DESCRIPT.ION format' + ), + descIonFile + ); + } + ); }); } }; - diff --git a/core/door.js b/core/door.js index 59d18dff..8474f762 100644 --- a/core/door.js +++ b/core/door.js @@ -1,28 +1,28 @@ /* jslint node: true */ 'use strict'; -const stringFormat = require('./string_format.js'); -const { Errors } = require('./enig_error.js'); -const Events = require('./events'); +const stringFormat = require('./string_format.js'); +const { Errors } = require('./enig_error.js'); +const Events = require('./events'); // deps -const pty = require('node-pty'); -const decode = require('iconv-lite').decode; -const createServer = require('net').createServer; -const paths = require('path'); -const _ = require('lodash'); +const pty = require('node-pty'); +const decode = require('iconv-lite').decode; +const createServer = require('net').createServer; +const paths = require('path'); +const _ = require('lodash'); module.exports = class Door { constructor(client) { - this.client = client; - this.restored = false; + this.client = client; + this.restored = false; } prepare(ioType, cb) { this.io = ioType; // we currently only have to do any real setup for 'socket' - if('socket' !== ioType) { + if ('socket' !== ioType) { return cb(null); } @@ -32,13 +32,16 @@ module.exports = class Door { }); conn.once('error', err => { - this.client.log.info( { error : err.message }, 'Door socket server connection'); + this.client.log.info( + { error: err.message }, + 'Door socket server connection' + ); return this.restoreIo(conn); }); - this.sockServer.getConnections( (err, count) => { + this.sockServer.getConnections((err, count) => { // We expect only one connection from our DOOR/emulator/etc. - if(!err && count <= 1) { + if (!err && count <= 1) { this.client.term.output.pipe(conn); conn.on('data', this.doorDataHandler.bind(this)); } @@ -53,39 +56,39 @@ module.exports = class Door { run(exeInfo, cb) { this.encoding = (exeInfo.encoding || 'cp437').toLowerCase(); - if('socket' === this.io && !this.sockServer) { + if ('socket' === this.io && !this.sockServer) { return cb(Errors.UnexpectedState('Socket server is not running')); } const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd); const formatObj = { - dropFile : exeInfo.dropFile, - dropFilePath : exeInfo.dropFilePath, - node : exeInfo.node.toString(), - srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', - userId : this.client.user.userId.toString(), - userName : this.client.user.getSanitizedName(), - userNameRaw : this.client.user.username, - cwd : cwd, + dropFile: exeInfo.dropFile, + dropFilePath: exeInfo.dropFilePath, + node: exeInfo.node.toString(), + srvPort: this.sockServer ? this.sockServer.address().port.toString() : '-1', + userId: this.client.user.userId.toString(), + userName: this.client.user.getSanitizedName(), + userNameRaw: this.client.user.username, + cwd: cwd, }; - const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); + const args = exeInfo.args.map(arg => stringFormat(arg, formatObj)); this.client.log.info( - { cmd : exeInfo.cmd, args, io : this.io }, + { cmd: exeInfo.cmd, args, io: this.io }, 'Executing external door process' ); try { this.doorPty = pty.spawn(exeInfo.cmd, args, { - cols : this.client.term.termWidth, - rows : this.client.term.termHeight, - cwd : cwd, - env : exeInfo.env, - encoding : null, // we want to handle all encoding ourself + cols: this.client.term.termWidth, + rows: this.client.term.termHeight, + cwd: cwd, + env: exeInfo.env, + encoding: null, // we want to handle all encoding ourself }); - } catch(e) { + } catch (e) { return cb(e); } @@ -93,9 +96,12 @@ module.exports = class Door { // PID is launched. Make sure it's killed off if the user disconnects. // Events.once(Events.getSystemEvents().ClientDisconnected, evt => { - if (this.doorPty && this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId')) { + if ( + this.doorPty && + this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId') + ) { this.client.log.info( - { pid : this.doorPty.pid }, + { pid: this.doorPty.pid }, 'User has disconnected; Killing door process.' ); this.doorPty.kill(); @@ -103,10 +109,11 @@ module.exports = class Door { }); this.client.log.debug( - { processId : this.doorPty.pid }, 'External door process spawned' + { processId: this.doorPty.pid }, + 'External door process spawned' ); - if('stdio' === this.io) { + if ('stdio' === this.io) { this.client.log.debug('Using stdio for door I/O'); this.client.term.output.pipe(this.doorPty); @@ -116,22 +123,25 @@ module.exports = class Door { this.doorPty.once('close', () => { return this.restoreIo(this.doorPty); }); - } else if('socket' === this.io) { + } else if ('socket' === this.io) { this.client.log.debug( - { srvPort : this.sockServer.address().port, srvSocket : this.sockServerSocket }, + { + srvPort: this.sockServer.address().port, + srvSocket: this.sockServerSocket, + }, 'Using temporary socket server for door I/O' ); } this.doorPty.once('exit', exitCode => { - this.client.log.info( { exitCode : exitCode }, 'Door exited'); + this.client.log.info({ exitCode: exitCode }, 'Door exited'); - if(this.sockServer) { + if (this.sockServer) { this.sockServer.close(); } // we may not get a close - if('stdio' === this.io) { + if ('stdio' === this.io) { this.restoreIo(this.doorPty); } @@ -147,13 +157,13 @@ module.exports = class Door { } restoreIo(piped) { - if(!this.restored) { - if(this.doorPty) { + if (!this.restored) { + if (this.doorPty) { this.doorPty.kill(); } const output = this.client.term.output; - if(output) { + if (output) { output.unpipe(piped); output.resume(); } diff --git a/core/door_party.js b/core/door_party.js index aee4438c..784f9554 100644 --- a/core/door_party.js +++ b/core/door_party.js @@ -2,22 +2,19 @@ 'use strict'; // enigma-bbs -const { MenuModule } = require('./menu_module.js'); -const { resetScreen } = require('./ansi_term.js'); -const { Errors } = require('./enig_error.js'); -const { - trackDoorRunBegin, - trackDoorRunEnd -} = require('./door_util.js'); +const { MenuModule } = require('./menu_module.js'); +const { resetScreen } = require('./ansi_term.js'); +const { Errors } = require('./enig_error.js'); +const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js'); // deps -const async = require('async'); -const SSHClient = require('ssh2').Client; +const async = require('async'); +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 { @@ -25,10 +22,10 @@ exports.getModule = class DoorPartyModule extends MenuModule { 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; + 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() { @@ -40,12 +37,12 @@ exports.getModule = class DoorPartyModule extends MenuModule { function validateConfig(callback) { return self.validateConfigFields( { - host : 'string', - username : 'string', - password : 'string', - bbsTag : 'string', - sshPort : 'number', - rloginPort : 'number', + host: 'string', + username: 'string', + password: 'string', + bbsTag: 'string', + sshPort: 'number', + rloginPort: 'number', }, callback ); @@ -60,12 +57,12 @@ exports.getModule = class DoorPartyModule extends MenuModule { let pipedStream; let doorTracking; - const restorePipe = function() { - if(pipedStream && !pipeRestored && !clientTerminated) { + const restorePipe = function () { + if (pipedStream && !pipeRestored && !clientTerminated) { self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); - if(doorTracking) { + if (doorTracking) { trackDoorRunEnd(doorTracking); doorTracking = null; } @@ -75,48 +72,60 @@ exports.getModule = class DoorPartyModule extends MenuModule { 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'); + 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(Errors.General('Failed to establish tunnel')); + sshClient.forwardOut( + '127.0.0.1', + self.config.sshPort, + self.config.host, + self.config.rloginPort, + (err, stream) => { + if (err) { + return callback( + Errors.General('Failed to establish tunnel') + ); + } + + doorTracking = trackDoorRunBegin(self.client); + + // + // Send rlogin + // DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g. + // [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); + + stream.on('data', d => { + // :TODO: we should just pipe this... + self.client.term.rawWrite(d); + }); + + stream.on('end', () => { + sshClient.end(); + }); + + stream.on('close', () => { + restorePipe(); + sshClient.end(); + }); } - - doorTracking = trackDoorRunBegin(self.client); - - // - // 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); - - stream.on('data', d => { - // :TODO: we should just pipe this... - self.client.term.rawWrite(d); - }); - - stream.on('end', () => { - sshClient.end(); - }); - - stream.on('close', () => { - restorePipe(); - sshClient.end(); - }); - }); + ); }); sshClient.on('error', err => { - self.client.log.info(`DoorParty SSH client error: ${err.message}`); + self.client.log.info( + `DoorParty SSH client error: ${err.message}` + ); trackDoorRunEnd(doorTracking); }); @@ -125,23 +134,23 @@ exports.getModule = class DoorPartyModule extends MenuModule { 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'); + if (err) { + self.client.log.warn({ error: err.message }, 'DoorParty error'); } // if the client is still here, go to previous - if(!clientTerminated) { + if (!clientTerminated) { self.prevMenu(); } } diff --git a/core/door_util.js b/core/door_util.js index 24c2cd80..4b394714 100644 --- a/core/door_util.js +++ b/core/door_util.js @@ -1,14 +1,14 @@ /* jslint node: true */ 'use strict'; -const UserProps = require('./user_property.js'); -const Events = require('./events.js'); -const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); +const Events = require('./events.js'); +const StatLog = require('./stat_log.js'); -const moment = require('moment'); +const moment = require('moment'); -exports.trackDoorRunBegin = trackDoorRunBegin; -exports.trackDoorRunEnd = trackDoorRunEnd; +exports.trackDoorRunBegin = trackDoorRunBegin; +exports.trackDoorRunEnd = trackDoorRunEnd; function trackDoorRunBegin(client, doorTag) { const startTime = moment(); @@ -23,20 +23,24 @@ function trackDoorRunEnd(trackInfo) { const { startTime, client, doorTag } = trackInfo; const diff = moment.duration(moment().diff(startTime)); - if(diff.asSeconds() >= 45) { + if (diff.asSeconds() >= 45) { StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1); } const runTimeMinutes = Math.floor(diff.asMinutes()); - if(runTimeMinutes > 0) { - StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); + if (runTimeMinutes > 0) { + StatLog.incrementUserStat( + client.user, + UserProps.DoorRunTotalMinutes, + runTimeMinutes + ); const eventInfo = { runTimeMinutes, - user : client.user, - doorTag : doorTag || 'unknown', + user: client.user, + doorTag: doorTag || 'unknown', }; Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo); } -} \ No newline at end of file +} diff --git a/core/download_queue.js b/core/download_queue.js index 4380ded1..33080c87 100644 --- a/core/download_queue.js +++ b/core/download_queue.js @@ -1,9 +1,9 @@ /* jslint node: true */ 'use strict'; -const FileEntry = require('./file_entry'); -const UserProps = require('./user_property'); -const Events = require('./events'); +const FileEntry = require('./file_entry'); +const UserProps = require('./user_property'); +const Events = require('./events'); // deps const _ = require('lodash'); @@ -12,9 +12,11 @@ module.exports = class DownloadQueue { constructor(client) { this.client = client; - if(!Array.isArray(this.client.user.downloadQueue)) { - if(this.client.user.properties[UserProps.DownloadQueue]) { - this.loadFromProperty(this.client.user.properties[UserProps.DownloadQueue]); + if (!Array.isArray(this.client.user.downloadQueue)) { + if (this.client.user.properties[UserProps.DownloadQueue]) { + this.loadFromProperty( + this.client.user.properties[UserProps.DownloadQueue] + ); } else { this.client.user.downloadQueue = []; } @@ -33,68 +35,86 @@ module.exports = class DownloadQueue { 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); + 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) { + add(fileEntry, systemFile = false) { this.client.user.downloadQueue.push({ - fileId : fileEntry.fileId, - areaTag : fileEntry.areaTag, - fileName : fileEntry.fileName, - path : fileEntry.filePath, - byteSize : fileEntry.meta.byte_size || 0, - systemFile : systemFile, + fileId: fileEntry.fileId, + areaTag: fileEntry.areaTag, + fileName: fileEntry.fileName, + path: fileEntry.filePath, + byteSize: fileEntry.meta.byte_size || 0, + systemFile: systemFile, }); } removeItems(fileIds) { - if(!Array.isArray(fileIds)) { - fileIds = [ fileIds ]; + if (!Array.isArray(fileIds)) { + fileIds = [fileIds]; } - const [ remain, removed ] = _.partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); + const [remain, removed] = _.partition( + this.client.user.downloadQueue, + e => -1 === fileIds.indexOf(e.fileId) + ); this.client.user.downloadQueue = remain; return removed; } isQueued(entryOrId) { - if(entryOrId instanceof FileEntry) { + 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) { + } 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' + ); } } addTemporaryDownload(entry) { - this.add(entry, true); // true=systemFile + this.add(entry, true); // true=systemFile // clean up after ourselves when the session ends const thisUniqueId = this.client.session.uniqueId; Events.once(Events.getSystemEvents().ClientDisconnected, evt => { - if(thisUniqueId === _.get(evt, 'client.session.uniqueId')) { - FileEntry.removeEntry(entry, { removePhysFile : true }, err => { + if (thisUniqueId === _.get(evt, 'client.session.uniqueId')) { + FileEntry.removeEntry(entry, { removePhysFile: true }, err => { const Log = require('./logger').log; - if(err) { - Log.warn( { fileId : entry.fileId, path : entry.filePath }, 'Failed removing temporary session download' ); + if (err) { + Log.warn( + { fileId: entry.fileId, path: entry.filePath }, + 'Failed removing temporary session download' + ); } else { - Log.debug( { fileId : entry.fileId, path : entry.filePath }, 'Removed temporary session download item' ); + Log.debug( + { fileId: entry.fileId, path: entry.filePath }, + 'Removed temporary session download item' + ); } }); } diff --git a/core/dropfile.js b/core/dropfile.js index 0c66d38a..a57c6ce3 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -2,18 +2,18 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const StatLog = require('./stat_log.js'); -const UserProps = require('./user_property.js'); -const SysProps = require('./system_property.js'); +const Config = require('./config.js').get; +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps -const fs = require('graceful-fs'); -const paths = require('path'); -const _ = require('lodash'); -const moment = require('moment'); -const iconv = require('iconv-lite'); -const { mkdirs } = require('fs-extra'); +const fs = require('graceful-fs'); +const paths = require('path'); +const _ = require('lodash'); +const moment = require('moment'); +const iconv = require('iconv-lite'); +const { mkdirs } = require('fs-extra'); // // Resources @@ -25,31 +25,34 @@ const { mkdirs } = require('fs-extra'); // * http://lord.lordlegacy.com/dosemu/ // module.exports = class DropFile { - constructor(client, { fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {} ) { - this.client = client; - this.fileType = fileType.toUpperCase(); - this.baseDir = baseDir; + constructor( + client, + { fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {} + ) { + this.client = client; + this.fileType = fileType.toUpperCase(); + this.baseDir = baseDir; } get fullPath() { - return paths.join(this.baseDir, ('node' + this.client.node), this.fileName); + return paths.join(this.baseDir, 'node' + this.client.node, this.fileName); } get fileName() { return { - DOOR : 'DOOR.SYS', // GAP BBS, many others - DOOR32 : 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec) - CALLINFO : 'CALLINFO.BBS', // Citadel? - DORINFO : this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... - CHAIN : 'CHAIN.TXT', // WWIV - CURRUSER : 'CURRUSER.BBS', // RyBBS - SFDOORS : 'SFDOORS.DAT', // Spitfire - PCBOARD : 'PCBOARD.SYS', // PCBoard - TRIBBS : 'TRIBBS.SYS', // TriBBS - USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ - JUMPER : 'JUMPER.DAT', // 2AM BBS - SXDOOR : 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE - INFO : 'INFO.BBS', // Phoenix BBS + DOOR: 'DOOR.SYS', // GAP BBS, many others + DOOR32: 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec) + CALLINFO: 'CALLINFO.BBS', // Citadel? + DORINFO: this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... + CHAIN: 'CHAIN.TXT', // WWIV + CURRUSER: 'CURRUSER.BBS', // RyBBS + SFDOORS: 'SFDOORS.DAT', // Spitfire + PCBOARD: 'PCBOARD.SYS', // PCBoard + TRIBBS: 'TRIBBS.SYS', // TriBBS + USERINFO: 'USERINFO.DAT', // Wildcat! 3.0+ + JUMPER: 'JUMPER.DAT', // 2AM BBS + SXDOOR: 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE + INFO: 'INFO.BBS', // Phoenix BBS }[this.fileType]; } @@ -59,9 +62,9 @@ module.exports = class DropFile { getHandler() { return { - DOOR : this.getDoorSysBuffer, - DOOR32 : this.getDoor32Buffer, - DORINFO : this.getDoorInfoDefBuffer, + DOOR: this.getDoorSysBuffer, + DOOR32: this.getDoor32Buffer, + DORINFO: this.getDoorInfoDefBuffer, }[this.fileType]; } @@ -73,9 +76,9 @@ module.exports = class DropFile { getDoorInfoFileName() { let x; const node = this.client.node; - if(10 === node) { + if (10 === node) { x = 0; - } else if(node < 10) { + } else if (node < 10) { x = node; } else { x = String.fromCharCode('a'.charCodeAt(0) + (node - 11)); @@ -84,75 +87,82 @@ module.exports = class DropFile { } getDoorSysBuffer() { - const prop = this.client.user.properties; - const now = moment(); - const secLevel = this.client.user.getLegacySecurityLevel().toString(); - const fullName = this.client.user.getSanitizedName('real'); - const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY'); + const prop = this.client.user.properties; + const now = moment(); + const secLevel = this.client.user.getLegacySecurityLevel().toString(); + const fullName = this.client.user.getSanitizedName('real'); + const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY'); - const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024); - const downK = Math.floor((parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024); + const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024); + const downK = Math.floor( + (parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024 + ); - const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format('hh:mm'); + const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format( + 'hh:mm' + ); // :TODO: fix time remaining // :TODO: fix default protocol -- user prop: transfer_protocol - return iconv.encode( [ - 'COM1:', // "Comm Port - COM0: = LOCAL MODE" - '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) - '8', // "Parity - 7 or 8" - this.client.node.toString(), // "Node Number - 1 to 99" - '57600', // "DTE Rate. Actual BPS rate to use. (kg)" - 'Y', // "Screen Display - Y=On N=Off (Default to Y)" - 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" - 'Y', // "Page Bell - Y=On N=Off (Default to Y)" - 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" - fullName, // "User Full Name" - prop[UserProps.Location]|| 'Anywhere', // "Calling From" - '123-456-7890', // "Home Phone" - '123-456-7890', // "Work/Data Phone" - 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) - secLevel, // "Security Level" - prop[UserProps.LoginCount].toString(), // "Total Times On" - now.format('MM/DD/YY'), // "Last Date Called" - '15360', // "Seconds Remaining THIS call (for those that particular)" - '256', // "Minutes Remaining THIS call" - 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" - this.client.term.termHeight.toString(), // "Page Length" - 'N', // "User Mode - Y = Expert, N = Novice" - '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" - '1', // "Conference Exited To DOOR From (G)" - '01/01/99', // "User Expiration Date (mm/dd/yy)" - this.client.user.userId.toString(), // "User File's Record Number" - 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." - // :TODO: fix up, down, etc. form user properties - '0', // "Total Uploads" - '0', // "Total Downloads" - '0', // "Daily Download "K" Total" - '999999', // "Daily Download Max. "K" Limit" - bd, // "Caller's Birthdate" - 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" - 'X:\\GEN\\', // "Path to the GEN directory" - StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)" - this.client.user.getSanitizedName(), // "Alias name" - '00:05', // "Event time (hh:mm)" (note: wat?) - 'Y', // "If its an error correcting connection (Y/N)" - 'Y', // "ANSI supported & caller using NG mode (Y/N)" - '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)" - timeOfCall, // "Time of This Call" - timeOfCall, // "Time of Last Call (hh:mm)" - '9999', // "Maximum daily files available" - '0', // "Files d/led so far today" - upK.toString(), // "Total "K" Bytes Uploaded" - downK.toString(), // "Total "K" Bytes Downloaded" - prop[UserProps.UserComment] || 'None', // "User Comment" - '0', // "Total Doors Opened" - '0', // "Total Messages Left" - ].join('\r\n') + '\r\n', 'cp437'); + 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" + this.client.node.toString(), // "Node Number - 1 to 99" + '57600', // "DTE Rate. Actual BPS rate to use. (kg)" + 'Y', // "Screen Display - Y=On N=Off (Default to Y)" + 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" + 'Y', // "Page Bell - Y=On N=Off (Default to Y)" + 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" + fullName, // "User Full Name" + prop[UserProps.Location] || 'Anywhere', // "Calling From" + '123-456-7890', // "Home Phone" + '123-456-7890', // "Work/Data Phone" + 'NOPE', // "Password" (Note: this is never given out or even stored plaintext) + secLevel, // "Security Level" + prop[UserProps.LoginCount].toString(), // "Total Times On" + now.format('MM/DD/YY'), // "Last Date Called" + '15360', // "Seconds Remaining THIS call (for those that particular)" + '256', // "Minutes Remaining THIS call" + 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" + this.client.term.termHeight.toString(), // "Page Length" + 'N', // "User Mode - Y = Expert, N = Novice" + '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" + '1', // "Conference Exited To DOOR From (G)" + '01/01/99', // "User Expiration Date (mm/dd/yy)" + this.client.user.userId.toString(), // "User File's Record Number" + 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." + // :TODO: fix up, down, etc. form user properties + '0', // "Total Uploads" + '0', // "Total Downloads" + '0', // "Daily Download "K" Total" + '999999', // "Daily Download Max. "K" Limit" + bd, // "Caller's Birthdate" + 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" + 'X:\\GEN\\', // "Path to the GEN directory" + StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)" + this.client.user.getSanitizedName(), // "Alias name" + '00:05', // "Event time (hh:mm)" (note: wat?) + 'Y', // "If its an error correcting connection (Y/N)" + 'Y', // "ANSI supported & caller using NG mode (Y/N)" + '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)" + timeOfCall, // "Time of This Call" + timeOfCall, // "Time of Last Call (hh:mm)" + '9999', // "Maximum daily files available" + '0', // "Files d/led so far today" + upK.toString(), // "Total "K" Bytes Uploaded" + downK.toString(), // "Total "K" Bytes Downloaded" + prop[UserProps.UserComment] || 'None', // "User Comment" + '0', // "Total Doors Opened" + '0', // "Total Messages Left" + ].join('\r\n') + '\r\n', + 'cp437' + ); } getDoor32Buffer() { @@ -163,26 +173,29 @@ module.exports = class DropFile { // // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! const Door32CommTypes = { - Local : 0, - Serial : 1, - Telnet : 2, + Local: 0, + Serial: 1, + Telnet: 2, }; const commType = Door32CommTypes.Telnet; - return iconv.encode([ - commType.toString(), - '-1', - '115200', - Config().general.boardName, - this.client.user.userId.toString(), - this.client.user.getSanitizedName('real'), - this.client.user.getSanitizedName(), - this.client.user.getLegacySecurityLevel().toString(), - '546', // :TODO: Minutes left! - '1', // ANSI - this.client.node.toString(), - ].join('\r\n') + '\r\n', 'cp437'); + return iconv.encode( + [ + commType.toString(), + '-1', + '115200', + Config().general.boardName, + this.client.user.userId.toString(), + this.client.user.getSanitizedName('real'), + this.client.user.getSanitizedName(), + this.client.user.getLegacySecurityLevel().toString(), + '546', // :TODO: Minutes left! + '1', // ANSI + this.client.node.toString(), + ].join('\r\n') + '\r\n', + 'cp437' + ); } getDoorInfoDefBuffer() { @@ -194,31 +207,36 @@ module.exports = class DropFile { // // Note that usernames are just used for first/last names here // - const opUserName = /[^\s]*/.exec(StatLog.getSystemStat(SysProps.SysOpUsername))[0]; - const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0]; - const secLevel = this.client.user.getLegacySecurityLevel().toString(); - const location = this.client.user.properties[UserProps.Location]; + const opUserName = /[^\s]*/.exec( + StatLog.getSystemStat(SysProps.SysOpUsername) + )[0]; + const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0]; + const secLevel = this.client.user.getLegacySecurityLevel().toString(); + const location = this.client.user.properties[UserProps.Location]; - return iconv.encode( [ - Config().general.boardName, // "The name of the system." - opUserName, // "The sysop's name up to the first space." - opUserName, // "The sysop's name following the first space." - 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." - '57600', // "The current port (DTE) rate." - '0', // "The number "0"" - userName, // "The current user's name, up to the first space." - userName, // "The current user's name, following the first space." - 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." + opUserName, // "The sysop's name up to the first space." + opUserName, // "The sysop's name following the first space." + 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." + '57600', // "The current port (DTE) rate." + '0', // "The number "0"" + userName, // "The current user's name, up to the first space." + userName, // "The current user's name, following the first space." + location || '', // "Where the user lives, or a blank line if unknown." + '1', // "The number "0" if TTY, or "1" if ANSI." + secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." + '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." + '-1', // "The number "-1" if using an external serial driver or "0" if using internal serial routines." + ].join('\r\n') + '\r\n', + 'cp437' + ); } createFile(cb) { mkdirs(paths.dirname(this.fullPath), err => { - if(err) { + if (err) { return cb(err); } return fs.writeFile(this.fullPath, this.getContents(), cb); diff --git a/core/edit_text_view.js b/core/edit_text_view.js index 05c7224d..56643d33 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -2,57 +2,59 @@ 'use strict'; // ENiGMA½ -const TextView = require('./text_view.js').TextView; -const miscUtil = require('./misc_util.js'); -const strUtil = require('./string_util.js'); +const TextView = require('./text_view.js').TextView; +const miscUtil = require('./misc_util.js'); +const strUtil = require('./string_util.js'); const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT; // deps -const _ = require('lodash'); +const _ = require('lodash'); -exports.EditTextView = EditTextView; +exports.EditTextView = EditTextView; const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, { - delete : [ 'delete', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ + delete: ['delete', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ }); 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; - if(!_.isObject(options.specialKeyMap)) { + if (!_.isObject(options.specialKeyMap)) { options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP; } TextView.call(this, options); this.initDefaultWidth(); - this.cursorPos = { row : 0, col : 0 }; + this.cursorPos = { row: 0, col: 0 }; - this.clientBackspace = function() { + this.clientBackspace = function () { this.text = this.text.substr(0, this.text.length - 1); - if(this.text.length >= this.dimens.width) { + if (this.text.length >= this.dimens.width) { this.redraw(); } else { this.cursorPos.col -= 1; - if(this.cursorPos.col >= 0) { + if (this.cursorPos.col >= 0) { const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); - this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); + 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) { +EditTextView.prototype.onKeyPress = function (ch, key) { + if (key) { + if (this.isKeyMapped('backspace', key.name)) { + if (this.text.length > 0) { this.clientBackspace(); } @@ -63,29 +65,29 @@ EditTextView.prototype.onKeyPress = function(ch, key) { if (this.text.length > 0 && this.cursorPos.col === this.text.length) { this.clientBackspace(); } - } else if(this.isKeyMapped('clearLine', key.name)) { - this.text = ''; - this.cursorPos.col = 0; - this.setFocus(true); // resetting focus will redraw & adjust cursor + } 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); } } - if(ch && strUtil.isPrintable(ch)) { - if(this.text.length < this.maxLength) { + if (ch && strUtil.isPrintable(ch)) { + if (this.text.length < this.maxLength) { ch = strUtil.stylizeString(ch, this.textStyle); this.text += ch; - if(this.text.length > this.dimens.width) { + 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) { + if (_.isString(this.textMaskChar)) { + if (this.textMaskChar.length > 0) { this.client.term.write(this.textMaskChar); } } else { @@ -98,10 +100,10 @@ EditTextView.prototype.onKeyPress = function(ch, key) { EditTextView.super_.prototype.onKeyPress.call(this, ch, key); }; -EditTextView.prototype.setText = function(text) { +EditTextView.prototype.setText = function (text) { // draw & set |text| EditTextView.super_.prototype.setText.call(this, text); // adjust local cursor tracking - this.cursorPos = { row : 0, col : text.length }; + this.cursorPos = { row: 0, col: text.length }; }; diff --git a/core/email.js b/core/email.js index 4a41106a..d71430e0 100644 --- a/core/email.js +++ b/core/email.js @@ -2,26 +2,26 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const Log = require('./logger.js').log; +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const Log = require('./logger.js').log; // deps -const _ = require('lodash'); -const nodeMailer = require('nodemailer'); +const _ = require('lodash'); +const nodeMailer = require('nodemailer'); -exports.sendMail = sendMail; +exports.sendMail = sendMail; function sendMail(message, cb) { const config = Config(); - if(!_.has(config, 'email.transport')) { + if (!_.has(config, 'email.transport')) { return cb(Errors.MissingConfig('Email "email.transport" configuration missing')); } 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); diff --git a/core/enig_error.js b/core/enig_error.js index 4d88cfa1..1470dced 100644 --- a/core/enig_error.js +++ b/core/enig_error.js @@ -5,53 +5,65 @@ class EnigError extends Error { constructor(message, code, reason, reasonCode) { super(message); - this.name = this.constructor.name; - this.message = message; - this.code = code; - this.reason = reason; + this.name = this.constructor.name; + this.message = message; + this.code = code; + this.reason = reason; this.reasonCode = reasonCode; - if(this.reason) { + if (this.reason) { this.message += `: ${this.reason}`; } - if(typeof Error.captureStackTrace === 'function') { + if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor); } else { - this.stack = (new Error(message)).stack; + this.stack = new Error(message).stack; } } } -exports.EnigError = EnigError; +exports.EnigError = EnigError; exports.Errors = { - General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), - MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), - DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), - AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), - Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), - ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), - MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), - UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), - MissingParam : (reason, reasonCode) => new EnigError('Missing paramter(s)', -32008, reason, reasonCode), - MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), - BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode), - UserInterrupt : (reason, reasonCode) => new EnigError('User interrupted', -32011, reason, reasonCode), - NothingToDo : (reason, reasonCode) => new EnigError('Nothing to do', -32012, 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 paramter(s)', -32008, reason, reasonCode), + MissingMci: (reason, reasonCode) => + new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), + BadLogin: (reason, reasonCode) => + new EnigError('Bad login attempt', -32010, reason, reasonCode), + UserInterrupt: (reason, reasonCode) => + new EnigError('User interrupted', -32011, reason, reasonCode), + NothingToDo: (reason, reasonCode) => + new EnigError('Nothing to do', -32012, reason, reasonCode), }; exports.ErrorReasons = { - AlreadyThere : 'ALREADYTHERE', - InvalidNextMenu : 'BADNEXT', - NoPreviousMenu : 'NOPREV', - NoConditionMatch : 'NOCONDMATCH', - NotEnabled : 'NOTENABLED', - AlreadyLoggedIn : 'ALREADYLOGGEDIN', - TooMany : 'TOOMANY', - Disabled : 'DISABLED', - Inactive : 'INACTIVE', - Locked : 'LOCKED', - NotAllowed : 'NOTALLOWED', - Invalid2FA : 'INVALID2FA', + AlreadyThere: 'ALREADYTHERE', + InvalidNextMenu: 'BADNEXT', + NoPreviousMenu: 'NOPREV', + NoConditionMatch: 'NOCONDMATCH', + NotEnabled: 'NOTENABLED', + AlreadyLoggedIn: 'ALREADYLOGGEDIN', + TooMany: 'TOOMANY', + Disabled: 'DISABLED', + Inactive: 'INACTIVE', + Locked: 'LOCKED', + NotAllowed: 'NOTALLOWED', + Invalid2FA: 'INVALID2FA', }; diff --git a/core/enigma_assert.js b/core/enigma_assert.js index 34f9beed..ca20bc14 100644 --- a/core/enigma_assert.js +++ b/core/enigma_assert.js @@ -2,17 +2,17 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const Log = require('./logger.js').log; +const Config = require('./config.js').get; +const Log = require('./logger.js').log; // deps -const assert = require('assert'); +const assert = require('assert'); -module.exports = function(condition, message) { - if(Config().debug.assertsEnabled) { +module.exports = function (condition, message) { + if (Config().debug.assertsEnabled) { assert.apply(this, arguments); - } else if(!(condition)) { + } else if (!condition) { const stack = new Error().stack; - Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' ); + Log.error({ condition: condition, stack: stack }, message || 'Assertion failed'); } }; diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 4a464cd8..5c7f4150 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -2,48 +2,52 @@ 'use strict'; // ENiGMA½ -const PluginModule = require('./plugin_module.js').PluginModule; -const Config = require('./config.js').get; -const Log = require('./logger.js').log; -const { Errors } = require('./enig_error.js'); +const PluginModule = require('./plugin_module.js').PluginModule; +const Config = require('./config.js').get; +const Log = require('./logger.js').log; +const { Errors } = require('./enig_error.js'); -const _ = require('lodash'); -const later = require('@breejs/later'); -const path = require('path'); -const pty = require('node-pty'); -const sane = require('sane'); -const moment = require('moment'); -const paths = require('path'); -const fse = require('fs-extra'); +const _ = require('lodash'); +const later = require('@breejs/later'); +const path = require('path'); +const pty = require('node-pty'); +const sane = require('sane'); +const moment = require('moment'); +const paths = require('path'); +const fse = require('fs-extra'); -exports.getModule = EventSchedulerModule; -exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart +exports.getModule = EventSchedulerModule; +exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart exports.moduleInfo = { - name : 'Event Scheduler', - desc : 'Support for scheduling arbritary events', - author : 'NuSkooler', + name: 'Event Scheduler', + desc: 'Support for scheduling arbritary events', + author: 'NuSkooler', }; -const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; -const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; +const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; +const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; class ScheduledEvent { constructor(events, name) { - this.name = name; - this.schedule = this.parseScheduleString(events[name].schedule); - this.action = this.parseActionSpec(events[name].action); - if(this.action) { + this.name = name; + this.schedule = this.parseScheduleString(events[name].schedule); + this.action = this.parseActionSpec(events[name].action); + if (this.action) { this.action.args = events[name].args || []; } } get isValid() { - if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { + if ( + !this.schedule || + (!this.schedule.sched && !this.schedule.watchFile) || + !this.action + ) { return false; } - if('method' === this.action.type && !this.action.location) { + if ('method' === this.action.type && !this.action.location) { return false; } @@ -51,118 +55,132 @@ class ScheduledEvent { } parseScheduleString(schedStr) { - if(!schedStr) { + if (!schedStr) { return false; } let schedule = {}; const m = SCHEDULE_REGEXP.exec(schedStr); - if(m) { + if (m) { schedStr = schedStr.substr(0, m.index).trim(); - if('@watch:' === m[1]) { + if ('@watch:' === m[1]) { schedule.watchFile = m[2]; } } - if(schedStr.length > 0) { + if (schedStr.length > 0) { const sched = later.parse.text(schedStr); - if(-1 === sched.error) { + if (-1 === sched.error) { schedule.sched = sched; } } // return undefined if we couldn't parse out anything useful - if(!_.isEmpty(schedule)) { + if (!_.isEmpty(schedule)) { return schedule; } } parseActionSpec(actionSpec) { - if(actionSpec) { - if('@' === actionSpec[0]) { + if (actionSpec) { + if ('@' === actionSpec[0]) { const m = ACTION_REGEXP.exec(actionSpec); - if(m) { - if(m[2].indexOf(':') > -1) { + if (m) { + if (m[2].indexOf(':') > -1) { const parts = m[2].split(':'); return { - type : m[1], - location : parts[0], - what : parts[1], + type: m[1], + location: parts[0], + what: parts[1], }; } else { return { - type : m[1], - what : m[2], + type: m[1], + what: m[2], }; } } } else { return { - type : 'execute', - what : actionSpec, + type: 'execute', + what: actionSpec, }; } } } executeAction(reason, cb) { - Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...'); + 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') + 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) { + if (err) { Log.debug( - { error : err.message, eventName : this.name, action : this.action }, - 'Error performing scheduled event action'); + { + error: err.message, + eventName: this.name, + action: this.action, + }, + 'Error performing scheduled event action' + ); } return cb(err); }); - } catch(e) { + } catch (e) { Log.warn( - { error : e.message, eventName : this.name, action : this.action }, - 'Failed to perform scheduled event action'); + { error: e.message, eventName: this.name, action: this.action }, + 'Failed to perform scheduled event action' + ); return cb(e); } - } else if('execute' === this.action.type) { + } else if ('execute' === this.action.type) { const opts = { // :TODO: cwd - name : this.name, - cols : 80, - rows : 24, - env : process.env, + name: this.name, + cols: 80, + rows: 24, + env: process.env, }; let proc; try { proc = pty.spawn(this.action.what, this.action.args, opts); - } catch(e) { - Log.warn( - { - error : 'Failed to spawn @execute process', - reason : e.message, - eventName : this.name, - action : this.action, - what : this.action.what, - args : this.action.args - } - ); + } catch (e) { + Log.warn({ + error: 'Failed to spawn @execute process', + reason: e.message, + eventName: this.name, + action: this.action, + what: this.action.what, + args: this.action.args, + }); return cb(e); } proc.once('exit', exitCode => { - if(exitCode) { + if (exitCode) { Log.warn( - { eventName : this.name, action : this.action, exitCode : exitCode }, - 'Bad exit code while performing scheduled event action'); + { eventName: this.name, action: this.action, exitCode: exitCode }, + 'Bad exit code while performing scheduled event action' + ); } - return cb(exitCode ? Errors.ExternalProcess(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); + return cb( + exitCode + ? Errors.ExternalProcess( + `Bad exit code while performing scheduled event action: ${exitCode}` + ) + : null + ); }); } } @@ -172,15 +190,15 @@ function EventSchedulerModule(options) { PluginModule.call(this, options); const config = Config(); - if(_.has(config, 'eventScheduler')) { + if (_.has(config, 'eventScheduler')) { this.moduleConfig = config.eventScheduler; } const self = this; this.runningActions = new Set(); - this.performAction = function(schedEvent, reason) { - if(self.runningActions.has(schedEvent.name)) { + this.performAction = function (schedEvent, reason) { + if (self.runningActions.has(schedEvent.name)) { return; // already running } @@ -193,80 +211,85 @@ function EventSchedulerModule(options) { } // convienence static method for direct load + start -EventSchedulerModule.loadAndStart = function(cb) { +EventSchedulerModule.loadAndStart = function (cb) { const loadModuleEx = require('./module_util.js').loadModuleEx; const loadOpts = { - name : path.basename(__filename, '.js'), - path : __dirname, + name: path.basename(__filename, '.js'), + path: __dirname, }; loadModuleEx(loadOpts, (err, mod) => { - if(err) { + if (err) { return cb(err); } const modInst = new mod.getModule(); - modInst.startup( err => { + modInst.startup(err => { return cb(err, modInst); }); }); }; -EventSchedulerModule.prototype.startup = function(cb) { - - this.eventTimers = []; +EventSchedulerModule.prototype.startup = function (cb) { + this.eventTimers = []; const self = this; - if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { - const events = Object.keys(this.moduleConfig.events).map( name => { + 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'); + 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', + 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? - [ 'change', 'add', 'delete' ].forEach(event => { + ['change', 'add', 'delete'].forEach(event => { watcher.on(event, (fileName, fileRoot) => { const eventPath = paths.join(fileRoot, fileName); - if(schedEvent.schedule.watchFile === eventPath) { + 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}`); + if (exists) { + self.performAction( + schedEvent, + `Watch file: ${schedEvent.schedule.watchFile}` + ); } }); } @@ -276,9 +299,9 @@ EventSchedulerModule.prototype.startup = function(cb) { cb(null); }; -EventSchedulerModule.prototype.shutdown = function(cb) { - if(this.eventTimers) { - this.eventTimers.forEach( et => et.clear() ); +EventSchedulerModule.prototype.shutdown = function (cb) { + if (this.eventTimers) { + this.eventTimers.forEach(et => et.clear()); } cb(null); diff --git a/core/events.js b/core/events.js index 541a5cae..717b5c18 100644 --- a/core/events.js +++ b/core/events.js @@ -1,17 +1,17 @@ /* jslint node: true */ 'use strict'; -const events = require('events'); -const Log = require('./logger.js').log; -const SystemEvents = require('./system_events.js'); +const events = require('events'); +const Log = require('./logger.js').log; +const SystemEvents = require('./system_events.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); -module.exports = new class Events extends events.EventEmitter { +module.exports = new (class Events extends events.EventEmitter { constructor() { super(); - this.setMaxListeners(64); // :TODO: play with this... + this.setMaxListeners(64); // :TODO: play with this... } getSystemEvents() { @@ -19,22 +19,22 @@ module.exports = new class Events extends events.EventEmitter { } addListener(event, listener) { - Log.trace( { event : event }, 'Registering event listener'); + Log.trace({ event: event }, 'Registering event listener'); return super.addListener(event, listener); } emit(event, ...args) { - Log.trace( { event : event }, 'Emitting event'); + Log.trace({ event: event }, 'Emitting event'); return super.emit(event, ...args); } on(event, listener) { - Log.trace( { event : event }, 'Registering 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'); + Log.trace({ event: event }, 'Registering single use event listener'); return super.once(event, listener); } @@ -45,32 +45,32 @@ module.exports = new class Events extends events.EventEmitter { // The returned object must be used with removeMultipleEventListener() // addMultipleEventListener(events, listener) { - Log.trace( { events }, 'Registering event listeners'); + Log.trace({ events }, 'Registering event listeners'); const listeners = []; events.forEach(eventName => { const listenWrapper = _.partial(listener, _, eventName); this.on(eventName, listenWrapper); - listeners.push( { eventName, listenWrapper } ); + listeners.push({ eventName, listenWrapper }); }); return listeners; } removeMultipleEventListener(listeners) { - Log.trace( { events }, 'Removing listeners'); + Log.trace({ events }, 'Removing listeners'); listeners.forEach(listener => { this.removeListener(listener.eventName, listener.listenWrapper); }); } removeListener(event, listener) { - Log.trace( { event : event }, 'Removing listener'); + Log.trace({ event: event }, 'Removing listener'); return super.removeListener(event, listener); } startup(cb) { return cb(null); } -}; +})(); diff --git a/core/exodus.js b/core/exodus.js index 5ed29a4e..624b826b 100644 --- a/core/exodus.js +++ b/core/exodus.js @@ -2,29 +2,24 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const { resetScreen } = require('./ansi_term.js'); -const Config = require('./config.js').get; -const { Errors } = require('./enig_error.js'); -const Log = require('./logger.js').log; -const { - getEnigmaUserAgent -} = require('./misc_util.js'); -const { - trackDoorRunBegin, - trackDoorRunEnd -} = require('./door_util.js'); +const { MenuModule } = require('./menu_module.js'); +const { resetScreen } = require('./ansi_term.js'); +const Config = require('./config.js').get; +const { Errors } = require('./enig_error.js'); +const Log = require('./logger.js').log; +const { getEnigmaUserAgent } = require('./misc_util.js'); +const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const joinPath = require('path').join; -const crypto = require('crypto'); -const moment = require('moment'); -const https = require('https'); -const querystring = require('querystring'); -const fs = require('fs-extra'); -const SSHClient = require('ssh2').Client; +const async = require('async'); +const _ = require('lodash'); +const joinPath = require('path').join; +const crypto = require('crypto'); +const moment = require('moment'); +const https = require('https'); +const querystring = require('querystring'); +const fs = require('fs-extra'); +const SSHClient = require('ssh2').Client; /* Configuration block: @@ -55,41 +50,47 @@ 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); - this.config = options.menuConfig.config || {}; - this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; - this.config.ticketPort = this.config.ticketPort || 1984, - this.config.ticketPath = this.config.ticketPath || '/exodus'; - this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); - this.config.sshHost = this.config.sshHost || this.config.ticketHost; - this.config.sshPort = this.config.sshPort || 22; - this.config.sshUser = this.config.sshUser || 'exodus_server'; - this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); + this.config = options.menuConfig.config || {}; + this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; + (this.config.ticketPort = this.config.ticketPort || 1984), + (this.config.ticketPath = this.config.ticketPath || '/exodus'); + this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); + this.config.sshHost = this.config.sshHost || this.config.ticketHost; + this.config.sshPort = this.config.sshPort || 22; + this.config.sshUser = this.config.sshUser || 'exodus_server'; + this.config.sshKeyPem = + this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); } initSequence() { - - const self = this; - let clientTerminated = false; + const self = this; + let clientTerminated = false; async.waterfall( [ function validateConfig(callback) { // very basic validation on optionals - async.each( [ 'board', 'key', 'door' ], (key, next) => { - return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`)); - }, callback); + 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)) { + if (!_.isString(self.config.caPem)) { return callback(null, null); } @@ -98,31 +99,34 @@ exports.getModule = class ExodusModule extends MenuModule { }); }, function getTicket(certAuthorities, callback) { - const now = moment.utc().unix(); - const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); - const token = `${sha256}|${now}`; + const now = moment.utc().unix(); + const sha256 = crypto + .createHash('sha256') + .update(`${self.config.key}${now}`) + .digest('hex'); + const token = `${sha256}|${now}`; - const postData = querystring.stringify({ - token : token, - board : self.config.board, - user : self.client.user.username, - door : self.config.door, + const postData = querystring.stringify({ + token: token, + board: self.config.board, + user: self.client.user.username, + door: self.config.door, }); const reqOptions = { - hostname : self.config.ticketHost, - port : self.config.ticketPort, - path : self.config.ticketPath, - rejectUnauthorized : self.config.rejectUnauthorized, - method : 'POST', - headers : { - 'Content-Type' : 'application/x-www-form-urlencoded', - 'Content-Length' : postData.length, - 'User-Agent' : getEnigmaUserAgent(), - } + hostname: self.config.ticketHost, + port: self.config.ticketPort, + path: self.config.ticketPath, + rejectUnauthorized: self.config.rejectUnauthorized, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postData.length, + 'User-Agent': getEnigmaUserAgent(), + }, }; - if(certAuthorities) { + if (certAuthorities) { reqOptions.ca = certAuthorities; } @@ -133,8 +137,10 @@ exports.getModule = class ExodusModule extends MenuModule { }); res.on('end', () => { - if(ticket.length !== 36) { - return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`)); + if (ticket.length !== 36) { + return callback( + Errors.Invalid(`Invalid Exodus ticket: ${ticket}`) + ); } return callback(null, ticket); @@ -154,52 +160,58 @@ exports.getModule = class ExodusModule extends MenuModule { }); }, function establishSecureConnection(ticket, privateKey, callback) { - let pipeRestored = false; let pipedStream; let doorTracking; function restorePipe() { - if(pipedStream && !pipeRestored && !clientTerminated) { + if (pipedStream && !pipeRestored && !clientTerminated) { self.client.term.output.unpipe(pipedStream); self.client.term.output.resume(); - if(doorTracking) { + if (doorTracking) { trackDoorRunEnd(doorTracking); } } } self.client.term.write(resetScreen()); - self.client.term.write('Connecting to Exodus server, please wait...\n'); + self.client.term.write( + 'Connecting to Exodus server, please wait...\n' + ); const sshClient = new SSHClient(); const window = { - rows : self.client.term.termHeight, - cols : self.client.term.termWidth, - width : 0, - height : 0, - term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( + 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, + env: { + exodus: ticket, }, }; sshClient.on('ready', () => { self.client.once('end', () => { - self.client.log.info('Connection ended. Terminating Exodus connection'); + self.client.log.info( + 'Connection ended. Terminating Exodus connection' + ); clientTerminated = true; return sshClient.end(); }); sshClient.shell(window, options, (err, stream) => { - doorTracking = trackDoorRunBegin(self.client, `exodus_${self.config.door}`); + doorTracking = trackDoorRunBegin( + self.client, + `exodus_${self.config.door}` + ); - pipedStream = stream; // :TODO: ewwwwwwwww hack + pipedStream = stream; // :TODO: ewwwwwwwww hack self.client.term.output.pipe(stream); stream.on('data', d => { @@ -212,7 +224,10 @@ exports.getModule = class ExodusModule extends MenuModule { }); stream.on('error', err => { - Log.warn( { error : err.message }, 'Exodus SSH client stream error'); + Log.warn( + { error: err.message }, + 'Exodus SSH client stream error' + ); }); }); }); @@ -223,19 +238,19 @@ exports.getModule = class ExodusModule extends MenuModule { }); sshClient.connect({ - host : self.config.sshHost, - port : self.config.sshPort, - username : self.config.sshUser, - privateKey : privateKey, + host: self.config.sshHost, + port: self.config.sshPort, + username: self.config.sshUser, + privateKey: privateKey, }); - } + }, ], err => { - if(err) { - self.client.log.warn( { error : err.message }, 'Exodus error'); + if (err) { + self.client.log.warn({ error: err.message }, 'Exodus error'); } - if(!clientTerminated) { + if (!clientTerminated) { self.prevMenu(); } } diff --git a/core/file_area_filter_edit.js b/core/file_area_filter_edit.js index e20d766b..28a8b38d 100644 --- a/core/file_area_filter_edit.js +++ b/core/file_area_filter_edit.js @@ -2,84 +2,88 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('./file_base_filter.js'); -const stringFormat = require('./string_format.js'); -const UserProps = require('./user_property.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getSortedAvailableFileAreas = + require('./file_base_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('./file_base_filter.js'); +const stringFormat = require('./string_format.js'); +const UserProps = require('./user_property.js'); // deps -const async = require('async'); +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 - } + selectedFilterInfo: 10, // { ...filter object ... } + activeFilterInfo: 11, // { ...filter object ... } + error: 12, // validation errors + }, }; exports.getModule = class FileAreaFilterEdit extends MenuModule { constructor(options) { super(options); - this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them - this.currentFilterIndex = 0; // into |filtersArray| + this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them + this.currentFilterIndex = 0; // into |filtersArray| // // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| // const activeFilter = FileBaseFilters.getActiveFilter(this.client); - this.filtersArray.sort( (filterA, filterB) => { - if(activeFilter) { - if(filterA.uuid === activeFilter.uuid) { + this.filtersArray.sort((filterA, filterB) => { + if (activeFilter) { + if (filterA.uuid === activeFilter.uuid) { return -1; } - if(filterB.uuid === activeFilter.uuid) { + 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) => { + saveFilter: (formData, extraArgs, cb) => { return this.saveCurrentFilter(formData, cb); }, - prevFilter : (formData, extraArgs, cb) => { + prevFilter: (formData, extraArgs, cb) => { this.currentFilterIndex -= 1; - if(this.currentFilterIndex < 0) { + if (this.currentFilterIndex < 0) { this.currentFilterIndex = this.filtersArray.length - 1; } this.loadDataForFilter(this.currentFilterIndex); return cb(null); }, - nextFilter : (formData, extraArgs, cb) => { + nextFilter: (formData, extraArgs, cb) => { this.currentFilterIndex += 1; - if(this.currentFilterIndex >= this.filtersArray.length) { + if (this.currentFilterIndex >= this.filtersArray.length) { this.currentFilterIndex = 0; } this.loadDataForFilter(this.currentFilterIndex); return cb(null); }, - makeFilterActive : (formData, extraArgs, cb) => { + makeFilterActive: (formData, extraArgs, cb) => { const filters = new FileBaseFilters(this.client); filters.setActive(this.filtersArray[this.currentFilterIndex].uuid); @@ -87,45 +91,49 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { return cb(null); }, - newFilter : (formData, extraArgs, cb) => { + 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; + deleteFilter: (formData, extraArgs, cb) => { + const selectedFilter = this.filtersArray[this.currentFilterIndex]; + const filterUuid = selectedFilter.uuid; // cannot delete built-in/system filters - if(true === selectedFilter.system) { + 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( () => { - + filters.persist(() => { // // If the item was also the active filter, we need to make a new one active // - if(filterUuid === this.client.user.properties[UserProps.FileBaseFilterActiveUuid]) { + if ( + filterUuid === + this.client.user.properties[UserProps.FileBaseFilterActiveUuid] + ) { const newActive = this.filtersArray[this.currentFilterIndex]; - if(newActive) { + if (newActive) { filters.setActive(newActive.uuid); } else { // nothing to set active to - this.client.user.removeProperty('file_base_filter_active_uuid'); + this.client.user.removeProperty( + 'file_base_filter_active_uuid' + ); } } // update UI this.updateActiveLabel(); - if(this.filtersArray.length > 0) { + if (this.filtersArray.length > 0) { this.loadDataForFilter(this.currentFilterIndex); } else { this.clearForm(); @@ -134,14 +142,16 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { }); }, - viewValidationListener : (err, cb) => { - const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); + viewValidationListener: (err, cb) => { + const errorView = this.viewControllers.editor.getView( + MciViewIds.editor.error + ); let newFocusId; - if(errorView) { - if(err) { + if (errorView) { + if (err) { errorView.setText(err.message); - err.view.clearText(); // clear out the invalid data + err.view.clearText(); // clear out the invalid data } else { errorView.clearText(); } @@ -154,8 +164,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { showError(errMsg) { const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); - if(errorView) { - if(errMsg) { + if (errorView) { + if (errMsg) { errorView.setText(errMsg); } else { errorView.clearText(); @@ -165,31 +175,39 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(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); + return vc.loadFromMenuConfig( + { callingMenu: self, mciMap: mciData.menu }, + callback + ); }, function populateAreas(callback) { - self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); + 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 ) ); + 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); @@ -204,36 +222,45 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setText(mciId, text) { const view = this.viewControllers.editor.getView(mciId); - if(view) { + if (view) { view.setText(text); } } updateActiveLabel() { const activeFilter = FileBaseFilters.getActiveFilter(this.client); - if(activeFilter) { + if (activeFilter) { const activeFormat = this.menuConfig.config.activeFormat || '{name}'; - this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); + this.setText( + MciViewIds.editor.activeFilterInfo, + stringFormat(activeFormat, activeFilter) + ); } } setFocusItemIndex(mciId, index) { const view = this.viewControllers.editor.getView(mciId); - if(view) { + if (view) { view.setFocusItemIndex(index); } } clearForm(newFocusId) { - [ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => { + [ + 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) { + if (newFocusId) { this.viewControllers.editor.switchFocus(newFocusId); } else { this.viewControllers.editor.resetInitialFocus(); @@ -241,11 +268,11 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } getSelectedAreaTag(index) { - if(0 === index) { - return ''; // -ALL- + if (0 === index) { + return ''; // -ALL- } const area = this.availAreas[index]; - if(!area) { + if (!area) { return ''; } return area.areaTag; @@ -258,9 +285,12 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setAreaIndexFromCurrentFilter() { let index; const filter = this.getCurrentFilter(); - if(filter) { + if (filter) { // special treatment: areaTag saved as blank ("") if -ALL- - index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; + index = + (filter.areaTag && + this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || + 0; } else { index = 0; } @@ -270,8 +300,9 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setOrderByFromCurrentFilter() { let index; const filter = this.getCurrentFilter(); - if(filter) { - index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0; + if (filter) { + index = + FileBaseFilters.OrderByValues.findIndex(ob => filter.order === ob) || 0; } else { index = 0; } @@ -281,8 +312,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { setSortByFromCurrentFilter() { let index; const filter = this.getCurrentFilter(); - if(filter) { - index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0; + if (filter) { + index = FileBaseFilters.SortByValues.findIndex(sb => filter.sort === sb) || 0; } else { index = 0; } @@ -294,19 +325,19 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { } setFilterValuesFromFormData(filter, formData) { - filter.name = formData.value.name; - filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); - filter.terms = formData.value.searchTerms; - filter.tags = formData.value.tags; - filter.order = this.getOrderBy(formData.value.orderByIndex); - filter.sort = this.getSortBy(formData.value.sortByIndex); + filter.name = formData.value.name; + filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); + filter.terms = formData.value.searchTerms; + filter.tags = formData.value.tags; + filter.order = this.getOrderBy(formData.value.orderByIndex); + filter.sort = this.getSortBy(formData.value.sortByIndex); } saveCurrentFilter(formData, cb) { - const filters = new FileBaseFilters(this.client); - const selectedFilter = this.filtersArray[this.currentFilterIndex]; + const filters = new FileBaseFilters(this.client); + const selectedFilter = this.filtersArray[this.currentFilterIndex]; - if(selectedFilter) { + if (selectedFilter) { // *update* currently selected filter this.setFilterValuesFromFormData(selectedFilter, formData); filters.replace(selectedFilter.uuid, selectedFilter); @@ -327,10 +358,10 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule { loadDataForFilter(filterIndex) { const filter = this.filtersArray[filterIndex]; - if(filter) { + if (filter) { this.setText(MciViewIds.editor.searchTerms, filter.terms); - this.setText(MciViewIds.editor.tags, filter.tags); - this.setText(MciViewIds.editor.filterName, filter.name); + this.setText(MciViewIds.editor.tags, filter.tags); + this.setText(MciViewIds.editor.filterName, filter.name); this.setAreaIndexFromCurrentFilter(); this.setSortByFromCurrentFilter(); diff --git a/core/file_area_list.js b/core/file_area_list.js index 637c3c3e..52f14fc2 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -2,150 +2,149 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const FileEntry = require('./file_entry.js'); -const stringFormat = require('./string_format.js'); -const FileArea = require('./file_base_area.js'); -const Errors = require('./enig_error.js').Errors; -const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; -const ArchiveUtil = require('./archive_util.js'); -const Config = require('./config.js').get; -const DownloadQueue = require('./download_queue.js'); -const FileAreaWeb = require('./file_area_web.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const resolveMimeType = require('./mime_util.js').resolveMimeType; -const isAnsi = require('./string_util.js').isAnsi; -const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi; +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const FileEntry = require('./file_entry.js'); +const stringFormat = require('./string_format.js'); +const FileArea = require('./file_base_area.js'); +const Errors = require('./enig_error.js').Errors; +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const ArchiveUtil = require('./archive_util.js'); +const Config = require('./config.js').get; +const DownloadQueue = require('./download_queue.js'); +const FileAreaWeb = require('./file_area_web.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const resolveMimeType = require('./mime_util.js').resolveMimeType; +const isAnsi = require('./string_util.js').isAnsi; +const controlCodesToAnsi = require('./color_codes.js').controlCodesToAnsi; // deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); -const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); +const paths = require('path'); exports.moduleInfo = { - name : 'File Area List', - desc : 'Lists contents of file an file area', - author : 'NuSkooler', + name: 'File Area List', + desc: 'Lists contents of file an file area', + author: 'NuSkooler', }; const FormIds = { - browse : 0, - details : 1, - detailsGeneral : 2, - detailsNfo : 3, - detailsFileList : 4, + browse: 0, + details: 1, + detailsGeneral: 2, + detailsNfo: 3, + detailsFileList: 4, }; const MciViewIds = { - browse : { - desc : 1, - navMenu : 2, + browse: { + desc: 1, + navMenu: 2, - customRangeStart : 10, // 10+ = customs + customRangeStart: 10, // 10+ = customs }, - details : { - navMenu : 1, - infoXyTop : 2, // %XY starting position for info area - infoXyBottom : 3, + details: { + navMenu: 1, + infoXyTop: 2, // %XY starting position for info area + infoXyBottom: 3, - customRangeStart : 10, // 10+ = customs + customRangeStart: 10, // 10+ = customs }, - detailsGeneral : { - customRangeStart : 10, // 10+ = customs + detailsGeneral: { + customRangeStart: 10, // 10+ = customs }, - detailsNfo : { - nfo : 1, + detailsNfo: { + nfo: 1, - customRangeStart : 10, // 10+ = customs + customRangeStart: 10, // 10+ = customs }, - detailsFileList : { - fileList : 1, + 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); + this.filterCriteria = _.get(options, 'extraArgs.filterCriteria'); + this.fileList = _.get(options, 'extraArgs.fileList'); + this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); - if(this.fileList) { + if (this.fileList) { // we'll need to adjust position as well! this.fileListPosition = 0; } this.dlQueue = new DownloadQueue(this.client); - if(!this.filterCriteria) { + if (!this.filterCriteria) { this.filterCriteria = FileBaseFilters.getActiveFilter(this.client); } - if(_.isString(this.filterCriteria)) { + if (_.isString(this.filterCriteria)) { this.filterCriteria = JSON.parse(this.filterCriteria); } - if(_.has(options, 'lastMenuResult.value')) { + if (_.has(options, 'lastMenuResult.value')) { this.lastMenuResultValue = options.lastMenuResult.value; } this.menuMethods = { - nextFile : (formData, extraArgs, cb) => { - if(this.fileListPosition + 1 < this.fileList.length) { + nextFile: (formData, extraArgs, cb) => { + if (this.fileListPosition + 1 < this.fileList.length) { this.fileListPosition += 1; - return this.displayBrowsePage(true, cb); // true=clerarScreen + return this.displayBrowsePage(true, cb); // true=clerarScreen } - if(this.lastFileNextExit) { + if (this.lastFileNextExit) { return this.prevMenu(cb); } return cb(null); }, - prevFile : (formData, extraArgs, cb) => { - if(this.fileListPosition > 0) { + prevFile: (formData, extraArgs, cb) => { + if (this.fileListPosition > 0) { --this.fileListPosition; - return this.displayBrowsePage(true, cb); // true=clearScreen + return this.displayBrowsePage(true, cb); // true=clearScreen } return cb(null); }, - viewDetails : (formData, extraArgs, cb) => { + viewDetails: (formData, extraArgs, cb) => { this.viewControllers.browse.setFocus(false); return this.displayDetailsPage(cb); }, - detailsQuit : (formData, extraArgs, cb) => { - [ 'detailsNfo', 'detailsFileList', 'details' ].forEach(n => { + detailsQuit: (formData, extraArgs, cb) => { + ['detailsNfo', 'detailsFileList', 'details'].forEach(n => { const vc = this.viewControllers[n]; - if(vc) { + if (vc) { vc.detachClientEvents(); } }); - return this.displayBrowsePage(true, cb); // true=clearScreen + return this.displayBrowsePage(true, cb); // true=clearScreen }, - toggleQueue : (formData, extraArgs, cb) => { + toggleQueue: (formData, extraArgs, cb) => { this.dlQueue.toggle(this.currentFileEntry); this.updateQueueIndicator(); return cb(null); }, - showWebDownloadLink : (formData, extraArgs, cb) => { + showWebDownloadLink: (formData, extraArgs, cb) => { return this.fetchAndDisplayWebDownloadLink(cb); }, - displayHelp : (formData, extraArgs, cb) => { + displayHelp: (formData, extraArgs, cb) => { return this.displayHelpPage(cb); }, - movementKeyPressed : (formData, extraArgs, cb) => { + movementKeyPressed: (formData, extraArgs, cb) => { return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb); }, }; @@ -161,31 +160,39 @@ exports.getModule = class FileAreaList extends MenuModule { getSaveState() { return { - fileList : this.fileList, - fileListPosition : this.fileListPosition, + fileList: this.fileList, + fileListPosition: this.fileListPosition, }; } restoreSavedState(savedState) { - if(savedState) { - this.fileList = savedState.fileList; - this.fileListPosition = savedState.fileListPosition; + if (savedState) { + this.fileList = savedState.fileList; + this.fileListPosition = savedState.fileListPosition; } } updateFileEntryWithMenuResult(cb) { - if(!this.lastMenuResultValue) { + if (!this.lastMenuResultValue) { return cb(null); } - if(_.isNumber(this.lastMenuResultValue.rating)) { + 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' ); + 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); } - return cb(null); - }); + ); } else { return cb(null); } @@ -204,12 +211,15 @@ exports.getModule = class FileAreaList extends MenuModule { }, function display(callback) { return self.displayBrowsePage(false, err => { - if(err) { - self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults'); + if (err) { + self.gotoMenu( + self.menuConfig.config.noResultsMenu || + 'fileBaseListEntriesNoResults' + ); } return callback(err); }); - } + }, ], () => { self.finishedLoading(); @@ -218,31 +228,37 @@ exports.getModule = class FileAreaList extends MenuModule { } populateCurrentEntryInfo(cb) { - const config = this.menuConfig.config; - const currEntry = this.currentFileEntry; + const config = this.menuConfig.config; + const currEntry = this.currentFileEntry; - const uploadTimestampFormat = config.uploadTimestampFormat || this.client.currentTheme.helpers.getDateFormat('short'); - const area = FileArea.getFileAreaByTag(currEntry.areaTag); - const hashTagsSep = config.hashTagsSep || ', '; - const isQueuedIndicator = config.isQueuedIndicator || 'Y'; - const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; + const uploadTimestampFormat = + config.uploadTimestampFormat || + this.client.currentTheme.helpers.getDateFormat('short'); + const area = FileArea.getFileAreaByTag(currEntry.areaTag); + const hashTagsSep = config.hashTagsSep || ', '; + const isQueuedIndicator = config.isQueuedIndicator || 'Y'; + 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 - }; + 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 @@ -250,19 +266,23 @@ exports.getModule = class FileAreaList extends MenuModule { // const metaValues = FileEntry.WellKnownMetaValues; metaValues.forEach(name => { - const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A'; + const value = !_.isUndefined(currEntry.meta[name]) + ? currEntry.meta[name] + : 'N/A'; entryInfo[_.camelCase(name)] = value; }); - if(entryInfo.archiveType) { + if (entryInfo.archiveType) { const mimeType = resolveMimeType(entryInfo.archiveType); let desc; - if(mimeType) { - let fileType = _.get(Config(), [ 'fileTypes', mimeType ] ); + if (mimeType) { + let fileType = _.get(Config(), ['fileTypes', mimeType]); - if(Array.isArray(fileType)) { + if (Array.isArray(fileType)) { // further refine by extention - fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext); + fileType = fileType.find( + ft => paths.extname(currEntry.fileName) === ft.ext + ); } desc = fileType && fileType.desc; } @@ -271,89 +291,114 @@ exports.getModule = class FileAreaList extends MenuModule { entryInfo.archiveTypeDesc = 'N/A'; } - entryInfo.uploadByUsername = entryInfo.uploadByUserName = entryInfo.uploadByUsername || 'N/A'; // may be imported - entryInfo.hashTags = entryInfo.hashTags || '(none)'; + entryInfo.uploadByUsername = 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) ); + 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'; + 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 { - entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; + const webDlExpireTimeFormat = + config.webDlExpireTimeFormat || + this.client.currentTheme.helpers.getDateTimeFormat('short'); + + entryInfo.webDlLink = + ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; + entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format( + webDlExpireTimeFormat + ); } - } else { - const webDlExpireTimeFormat = config.webDlExpireTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); - entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; - entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); + return cb(null); } - - return cb(null); - }); + ); } populateCustomLabels(category, startId) { - return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo); + return this.updateCustomViewTextsWithFilter( + category, + startId, + this.currentFileEntry.entryInfo + ); } displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; async.waterfall( [ function readyAndDisplayArt(callback) { - if(options.clearScreen) { + if (options.clearScreen) { self.client.term.rawWrite(ansi.resetScreen()); } theme.displayThemedAsset( config.art[name], self.client, - { font : self.menuConfig.font, trailingLF : false }, + { font: self.menuConfig.font, trailingLF: false }, (err, artData) => { return callback(err, artData); } ); }, function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { + if (_.isUndefined(self.viewControllers[name])) { const vcOpts = { - client : self.client, - formId : FormIds[name], + client: self.client, + formId: FormIds[name], }; - if(!_.isUndefined(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) + ); - if('details' === name) { + if ('details' === name) { try { self.detailsInfoArea = { - top : artData.mciMap.XY2.position, - bottom : artData.mciMap.XY3.position, + top: artData.mciMap.XY2.position, + bottom: artData.mciMap.XY3.position, }; - } catch(e) { - return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!')); + } catch (e) { + return callback( + Errors.DoesNotExist( + 'Missing XY2 and XY3 position indicators!' + ) + ); } } const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], + callingMenu: self, + mciMap: artData.mciMap, + formId: FormIds[name], }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -361,7 +406,6 @@ exports.getModule = class FileAreaList extends MenuModule { self.viewControllers[name].setFocus(true); return callback(null); - }, ], err => { @@ -371,40 +415,51 @@ exports.getModule = class FileAreaList extends MenuModule { } displayBrowsePage(clearScreen, cb) { - const self = this; + const self = this; async.series( [ function fetchEntryData(callback) { - if(self.fileList) { + if (self.fileList) { return callback(null); } - return self.loadFileIds(false, callback); // false=do not force + return self.loadFileIds(false, callback); // false=do not force }, function checkEmptyResults(callback) { - if(0 === self.fileList.length) { - return callback(Errors.General('No results for criteria', 'NORESULTS')); + 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); + 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); - } + self.currentFileEntry.load( + self.fileList[self.fileListPosition], + err => { + if (err) { + return callback(err); + } - return self.populateCurrentEntryInfo(callback); - }); + return self.populateCurrentEntryInfo(callback); + } + ); }, function populateDesc(callback) { - if(_.isString(self.currentFileEntry.desc)) { - const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); - if(descView) { + 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 @@ -415,18 +470,24 @@ exports.getModule = class FileAreaList extends MenuModule { // it as text. // const desc = controlCodesToAnsi(self.currentFileEntry.desc); - if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { + if ( + desc.length != self.currentFileEntry.desc.length || + isAnsi(desc) + ) { const opts = { - prepped : false, - forceLineTerm : true + prepped: false, + forceLineTerm: true, }; // // if SAUCE states a term width, honor it else we may see // display corruption // - const sauceTermWidth = _.get(self.currentFileEntry.meta, 'desc_sauce.Character.characterWidth'); - if(_.isNumber(sauceTermWidth)) { + const sauceTermWidth = _.get( + self.currentFileEntry.meta, + 'desc_sauce.Character.characterWidth' + ); + if (_.isNumber(sauceTermWidth)) { opts.termWidth = sauceTermWidth; } @@ -444,12 +505,15 @@ exports.getModule = class FileAreaList extends MenuModule { }, function populateAdditionalViews(callback) { self.updateQueueIndicator(); - self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart); + self.populateCustomLabels( + 'browse', + MciViewIds.browse.customRangeStart + ); return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -457,38 +521,47 @@ exports.getModule = class FileAreaList extends MenuModule { } displayDetailsPage(cb) { - const self = this; + const self = this; async.series( [ function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback); + return self.displayArtAndPrepViewController( + 'details', + { clearScreen: true }, + callback + ); }, function populateViews(callback) { - self.populateCustomLabels('details', MciViewIds.details.customRangeStart); + 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); + 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', + 0: 'general', + 1: 'nfo', + 2: 'fileList', }[index]; - if(sectionName) { + if (sectionName) { self.displayDetailsSection(sectionName, true); } }); return callback(null); - } + }, ], err => { return cb(err); @@ -497,28 +570,32 @@ exports.getModule = class FileAreaList extends MenuModule { } displayHelpPage(cb) { - this.displayAsset( - this.menuConfig.config.art.help, - { clearScreen : true }, - () => { - this.client.waitForKeyPress( () => { - return this.displayBrowsePage(true, cb); - }); - } - ); + this.displayAsset(this.menuConfig.config.art.help, { clearScreen: true }, () => { + this.client.waitForKeyPress(() => { + return this.displayBrowsePage(true, cb); + }); + }); } _handleMovementKeyPress(keyName, cb) { - const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc); + const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc); if (!descView) { return cb(null); } switch (keyName) { - case 'down arrow' : descView.scrollDocumentUp(); break; - case 'up arrow' : descView.scrollDocumentDown(); break; - case 'page up' : descView.keyPressPageUp(); break; - case 'page down' : descView.keyPressPageDown(); break; + case 'down arrow': + descView.scrollDocumentUp(); + break; + case 'up arrow': + descView.scrollDocumentDown(); + break; + case 'page up': + descView.keyPressPageUp(); + break; + case 'page down': + descView.keyPressPageDown(); + break; } this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu); @@ -531,28 +608,34 @@ exports.getModule = class FileAreaList extends MenuModule { async.series( [ function generateLinkIfNeeded(callback) { - - if(self.currentFileEntry.webDlExpireTime < moment()) { + if (self.currentFileEntry.webDlExpireTime < moment()) { return callback(null); } - const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); + const expireTime = moment().add( + Config().fileBase.web.expireMinutes, + 'minutes' + ); FileAreaWeb.createAndServeTempDownload( self.client, self.currentFileEntry, - { expireTime : expireTime }, + { expireTime: expireTime }, (err, url) => { - if(err) { + if (err) { return callback(err); } self.currentFileEntry.webDlExpireTime = expireTime; - const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + const webDlExpireTimeFormat = + self.menuConfig.config.webDlExpireTimeFormat || + 'YYYY-MMM-DD @ h:mm'; - self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; - self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); + self.currentFileEntry.entryInfo.webDlLink = + ansi.vtxHyperlink(self.client, url) + url; + self.currentFileEntry.entryInfo.webDlExpire = + expireTime.format(webDlExpireTimeFormat); return callback(null); } @@ -561,11 +644,12 @@ exports.getModule = class FileAreaList extends MenuModule { function updateActiveViews(callback) { self.updateCustomViewTextsWithFilter( 'browse', - MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, - { filter : [ '{webDlLink}', '{webDlExpire}' ] } + MciViewIds.browse.customRangeStart, + self.currentFileEntry.entryInfo, + { filter: ['{webDlLink}', '{webDlExpire}'] } ); return callback(null); - } + }, ], err => { return cb(err); @@ -574,156 +658,178 @@ exports.getModule = class FileAreaList extends MenuModule { } updateQueueIndicator() { - const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; - const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; + const isQueuedIndicator = this.menuConfig.config.isQueuedIndicator || 'Y'; + const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; this.currentFileEntry.entryInfo.isQueued = stringFormat( - this.dlQueue.isQueued(this.currentFileEntry) ? - isQueuedIndicator : - isNotQueuedIndicator + this.dlQueue.isQueued(this.currentFileEntry) + ? isQueuedIndicator + : isNotQueuedIndicator ); this.updateCustomViewTextsWithFilter( 'browse', MciViewIds.browse.customRangeStart, this.currentFileEntry.entryInfo, - { filter : [ '{isQueued}' ] } + { filter: ['{isQueued}'] } ); } cacheArchiveEntries(cb) { // check cache - if(this.currentFileEntry.archiveEntries) { + if (this.currentFileEntry.archiveEntries) { return cb(null, 'cache'); } const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag); - if(!areaInfo) { + if (!areaInfo) { return cb(Errors.Invalid('Invalid area tag')); } - const filePath = this.currentFileEntry.filePath; - const archiveUtil = ArchiveUtil.getInstance(); + const filePath = this.currentFileEntry.filePath; + const archiveUtil = ArchiveUtil.getInstance(); - archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => { - if(err) { - return cb(err); + archiveUtil.listEntries( + filePath, + this.currentFileEntry.entryInfo.archiveType, + (err, entries) => { + if (err) { + return cb(err); + } + + // assign and add standard "text" member for itemFormat + this.currentFileEntry.archiveEntries = entries.map(e => + Object.assign(e, { text: `${e.fileName} (${e.byteSize})` }) + ); + return cb(null, 're-cached'); } - - // assign and add standard "text" member for itemFormat - this.currentFileEntry.archiveEntries = entries.map(e => Object.assign(e, { text : `${e.fileName} (${e.byteSize})` } )); - return cb(null, 're-cached'); - }); + ); } setFileListNoListing(text) { - const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); - if(fileListView) { + const fileListView = this.viewControllers.detailsFileList.getView( + MciViewIds.detailsFileList.fileList + ); + if (fileListView) { fileListView.complexItems = false; - fileListView.setItems( [ text ] ); + fileListView.setItems([text]); fileListView.redraw(); } } populateFileListing() { - const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); + const fileListView = this.viewControllers.detailsFileList.getView( + MciViewIds.detailsFileList.fileList + ); - if(this.currentFileEntry.entryInfo.archiveType) { - this.cacheArchiveEntries( (err, cacheStatus) => { - if(err) { + if (this.currentFileEntry.entryInfo.archiveType) { + this.cacheArchiveEntries((err, cacheStatus) => { + if (err) { return this.setFileListNoListing('Failed to get file listing'); } - if('re-cached' === cacheStatus) { + if ('re-cached' === cacheStatus) { fileListView.setItems(this.currentFileEntry.archiveEntries); fileListView.redraw(); } }); } else { - const notAnArchiveFileName = stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ); + const notAnArchiveFileName = stringFormat( + this.menuConfig.config.notAnArchiveFormat || 'Not an archive', + { fileName: this.currentFileEntry.fileName } + ); this.setFileListNoListing(notAnArchiveFileName); } } displayDetailsSection(sectionName, clearArea, cb) { - const self = this; - const name = `details${_.upperFirst(sectionName)}`; + const self = this; + const name = `details${_.upperFirst(sectionName)}`; async.series( [ function detachPrevious(callback) { - if(self.lastDetailsViewController) { + 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)); + self.client.term.rawWrite( + ansi.goto(self.detailsInfoArea.top[0], 1) + ); } gotoTopPos(); - if(clearArea) { + if (clearArea) { self.client.term.rawWrite(ansi.reset()); - let pos = self.detailsInfoArea.top[0]; - const bottom = self.detailsInfoArea.bottom[0]; + let pos = self.detailsInfoArea.top[0]; + const bottom = self.detailsInfoArea.bottom[0]; - while(pos++ <= bottom) { + while (pos++ <= bottom) { self.client.term.rawWrite(ansi.eraseLine() + ansi.down()); } gotoTopPos(); } - return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback); + return self.displayArtAndPrepViewController( + name, + { clearScreen: false, noInput: true }, + callback + ); }, function populateViews(callback) { self.lastDetailsViewController = self.viewControllers[name]; - switch(sectionName) { - case 'nfo' : + switch (sectionName) { + case 'nfo': { - const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo); - if(!nfoView) { + const nfoView = self.viewControllers.detailsNfo.getView( + MciViewIds.detailsNfo.nfo + ); + if (!nfoView) { return callback(null); } - if(isAnsi(self.currentFileEntry.entryInfo.descLong)) { + if (isAnsi(self.currentFileEntry.entryInfo.descLong)) { nfoView.setAnsi( self.currentFileEntry.entryInfo.descLong, { - prepped : false, - forceLineTerm : true, + prepped: false, + forceLineTerm: true, }, () => { return callback(null); } ); } else { - nfoView.setText(self.currentFileEntry.entryInfo.descLong); + nfoView.setText( + self.currentFileEntry.entryInfo.descLong + ); return callback(null); } } break; - case 'fileList' : + case 'fileList': self.populateFileListing(); return callback(null); - default : + default: return callback(null); } }, function setLabels(callback) { self.populateCustomLabels(name, MciViewIds[name].customRangeStart); return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -731,11 +837,15 @@ exports.getModule = class FileAreaList extends MenuModule { } loadFileIds(force, cb) { - if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) { - this.fileListPosition = 0; + if ( + force || + _.isUndefined(this.fileList) || + _.isUndefined(this.fileListPosition) + ) { + this.fileListPosition = 0; const filterCriteria = Object.assign({}, this.filterCriteria); - if(!filterCriteria.areaTag) { + if (!filterCriteria.areaTag) { filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client); } diff --git a/core/file_area_web.js b/core/file_area_web.js index b6a3d8ae..17daa3a0 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -2,30 +2,30 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const FileDb = require('./database.js').dbs.file; +const Config = require('./config.js').get; +const FileDb = require('./database.js').dbs.file; const getISOTimestampString = require('./database.js').getISOTimestampString; -const FileEntry = require('./file_entry.js'); -const getServer = require('./listening_server.js').getServer; -const Errors = require('./enig_error.js').Errors; -const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; -const StatLog = require('./stat_log.js'); -const User = require('./user.js'); -const Log = require('./logger.js').log; +const FileEntry = require('./file_entry.js'); +const getServer = require('./listening_server.js').getServer; +const Errors = require('./enig_error.js').Errors; +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const Log = require('./logger.js').log; const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; -const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const Events = require('./events.js'); -const UserProps = require('./user_property.js'); -const SysProps = require('./system_menu_method.js'); +const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const Events = require('./events.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_menu_method.js'); // deps -const hashids = require('hashids/cjs'); -const moment = require('moment'); -const paths = require('path'); -const async = require('async'); -const fs = require('graceful-fs'); -const mimeTypes = require('mime-types'); -const yazl = require('yazl'); +const hashids = require('hashids/cjs'); +const moment = require('moment'); +const paths = require('path'); +const async = require('async'); +const fs = require('graceful-fs'); +const mimeTypes = require('mime-types'); +const yazl = require('yazl'); function notEnabledError() { return Errors.General('Web server is not enabled', ErrNotEnabled); @@ -33,8 +33,8 @@ function notEnabledError() { class FileAreaWebAccess { constructor() { - this.hashids = new hashids(Config().general.boardName); - this.expireTimers = {}; // hashId->timer + this.hashids = new hashids(Config().general.boardName); + this.expireTimers = {}; // hashId->timer } startup(cb) { @@ -47,21 +47,27 @@ class FileAreaWebAccess { }, function addWebRoute(callback) { self.webServer = getServer(webServerPackageName); - if(!self.webServer) { - return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`)); + if (!self.webServer) { + return callback( + Errors.DoesNotExist( + `Server with package name "${webServerPackageName}" does not exist` + ) + ); } - if(self.isEnabled()) { + if (self.isEnabled()) { const routeAdded = self.webServer.instance.addRoute({ - method : 'GET', - path : Config().fileBase.web.routePath, - handler : self.routeWebRequest.bind(self), + method: 'GET', + path: Config().fileBase.web.routePath, + handler: self.routeWebRequest.bind(self), }); - return callback(routeAdded ? null : Errors.General('Failed adding route')); + return callback( + routeAdded ? null : Errors.General('Failed adding route') + ); } else { - return callback(null); // not enabled, but no error + return callback(null); // not enabled, but no error } - } + }, ], err => { return cb(err); @@ -79,8 +85,8 @@ class FileAreaWebAccess { static getHashIdTypes() { return { - SingleFile : 0, - BatchArchive : 1, + SingleFile: 0, + BatchArchive: 1, }; } @@ -92,7 +98,7 @@ class FileAreaWebAccess { `SELECT hash_id, expire_timestamp FROM file_web_serve;`, (err, row) => { - if(row) { + if (row) { this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); } }, @@ -109,29 +115,28 @@ class FileAreaWebAccess { FileDb.run( `DELETE FROM file_web_serve WHERE hash_id = ?;`, - [ hashId ] + [hashId] ); delete this.expireTimers[hashId]; } scheduleExpire(hashId, expireTime) { - // remove any previous entry for this hashId const previous = this.expireTimers[hashId]; - if(previous) { + if (previous) { clearTimeout(previous); delete this.expireTimers[hashId]; } const timeoutMs = expireTime.diff(moment()); - if(timeoutMs <= 0) { - setImmediate( () => { + if (timeoutMs <= 0) { + setImmediate(() => { this.removeEntry(hashId); }); } else { - this.expireTimers[hashId] = setTimeout( () => { + this.expireTimers[hashId] = setTimeout(() => { this.removeEntry(hashId); }, timeoutMs); } @@ -142,27 +147,32 @@ class FileAreaWebAccess { `SELECT expire_timestamp FROM file_web_serve WHERE hash_id = ?`, - [ hashId ], + [hashId], (err, result) => { - if(err || !result) { - return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID')); + if (err || !result) { + return cb( + err ? err : Errors.DoesNotExist('Invalid or missing hash ID') + ); } const decoded = this.hashids.decode(hashId); // decode() should provide an array of [ userId, hashIdType, id, ... ] - if(!Array.isArray(decoded) || decoded.length < 3) { + if (!Array.isArray(decoded) || decoded.length < 3) { return cb(Errors.Invalid('Invalid or unknown hash ID')); } const servedItem = { - hashId : hashId, - userId : decoded[0], - hashIdType : decoded[1], - expireTimestamp : moment(result.expire_timestamp), + hashId: hashId, + userId: decoded[0], + hashIdType: decoded[1], + expireTimestamp: moment(result.expire_timestamp), }; - if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { + if ( + FileAreaWebAccess.getHashIdTypes().SingleFile === + servedItem.hashIdType + ) { servedItem.fileIds = decoded.slice(2); } @@ -172,11 +182,17 @@ class FileAreaWebAccess { } getSingleFileHashId(client, fileEntry) { - return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] ); + return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ + fileEntry.fileId, + ]); } getBatchArchiveHashId(client, batchId) { - return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId); + return this.getHashId( + client, + FileAreaWebAccess.getHashIdTypes().BatchArchive, + batchId + ); } getHashId(client, hashIdType, identifier) { @@ -194,13 +210,13 @@ class FileAreaWebAccess { } getExistingTempDownloadServeItem(client, fileEntry, cb) { - if(!this.isEnabled()) { + if (!this.isEnabled()) { return cb(notEnabledError()); } const hashId = this.getSingleFileHashId(client, fileEntry); this.loadServedHashId(hashId, (err, servedItem) => { - if(err) { + if (err) { return cb(err); } @@ -215,9 +231,9 @@ class FileAreaWebAccess { dbOrTrans.run( `REPLACE INTO file_web_serve (hash_id, expire_timestamp) VALUES (?, ?);`, - [ hashId, getISOTimestampString(expireTime) ], + [hashId, getISOTimestampString(expireTime)], err => { - if(err) { + if (err) { return cb(err); } @@ -229,13 +245,13 @@ class FileAreaWebAccess { } createAndServeTempDownload(client, fileEntry, options, cb) { - if(!this.isEnabled()) { + 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); @@ -243,41 +259,45 @@ class FileAreaWebAccess { } createAndServeTempBatchDownload(client, fileEntries, options, cb) { - if(!this.isEnabled()) { + 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) { + FileDb.beginTransaction((err, trans) => { + if (err) { return cb(err); } this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => { - if(err) { - return trans.rollback( () => { + 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); + }); + } + ); }); }); } @@ -289,47 +309,46 @@ class FileAreaWebAccess { 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) => { - - if(err) { + if (err) { return this.fileNotFound(resp); } const hashIdTypes = FileAreaWebAccess.getHashIdTypes(); - switch(servedItem.hashIdType) { - case hashIdTypes.SingleFile : + switch (servedItem.hashIdType) { + case hashIdTypes.SingleFile: return this.routeWebRequestForSingleFile(servedItem, req, resp); - case hashIdTypes.BatchArchive : + case hashIdTypes.BatchArchive: return this.routeWebRequestForBatchArchive(servedItem, req, resp); - default : + default: return this.fileNotFound(resp); } }); } routeWebRequestForSingleFile(servedItem, req, resp) { - Log.debug( { servedItem : servedItem }, 'Single file web request'); + Log.debug({ servedItem: servedItem }, 'Single file web request'); const fileEntry = new FileEntry(); servedItem.fileId = servedItem.fileIds[0]; fileEntry.load(servedItem.fileId, err => { - if(err) { + if (err) { return this.fileNotFound(resp); } const filePath = fileEntry.filePath; - if(!filePath) { + if (!filePath) { return this.fileNotFound(resp); } fs.stat(filePath, (err, stats) => { - if(err) { + if (err) { return this.fileNotFound(resp); } @@ -340,13 +359,18 @@ class FileAreaWebAccess { resp.on('finish', () => { // transfer completed fully - this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); + this.updateDownloadStatsForUserIdAndSystem( + servedItem.userId, + stats.size, + [fileEntry] + ); }); const headers = { - 'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), - 'Content-Length' : stats.size, - 'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, + 'Content-Type': + mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), + 'Content-Length': stats.size, + 'Content-Disposition': `attachment; filename="${fileEntry.fileName}"`, }; const readStream = fs.createReadStream(filePath); @@ -357,7 +381,7 @@ class FileAreaWebAccess { } routeWebRequestForBatchArchive(servedItem, req, resp) { - Log.debug( { servedItem : servedItem }, 'Batch file web request'); + Log.debug({ servedItem: servedItem }, 'Batch file web request'); // // We are going to build an on-the-fly zip file stream of 1:n @@ -374,53 +398,80 @@ class FileAreaWebAccess { `SELECT file_id FROM file_web_serve_batch WHERE hash_id = ?;`, - [ servedItem.hashId ], + [servedItem.hashId], (err, fileIdRows) => { - if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { - return callback(Errors.DoesNotExist('Could not get file IDs for batch')); + 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)); + 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')); - } + 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); - }); + 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'); + Log.trace( + { filePaths: filePaths }, + 'Creating zip archive for batch web request' + ); const zipFile = new yazl.ZipFile(); zipFile.on('error', err => { - Log.warn( { error : err.message }, 'Error adding file to batch web request archive'); + Log.warn( + { error: err.message }, + 'Error adding file to batch web request archive' + ); }); filePaths.forEach(fp => { zipFile.addFile( - fp, // path to physical file + fp, // path to physical file paths.basename(fp), // filename/path *stored in archive* { - compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. + compress: false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. } ); }); - 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', () => { @@ -430,24 +481,30 @@ class FileAreaWebAccess { resp.on('finish', () => { // transfer completed fully - self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); + self.updateDownloadStatsForUserIdAndSystem( + servedItem.userId, + finalZipSize, + fileEntries + ); }); const batchFileName = `batch_${servedItem.hashId}.zip`; const headers = { - 'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), - 'Content-Length' : finalZipSize, - 'Content-Disposition' : `attachment; filename="${batchFileName}"`, + 'Content-Type': + mimeTypes.contentType(batchFileName) || + mimeTypes.contentType('.bin'), + 'Content-Length': finalZipSize, + 'Content-Disposition': `attachment; filename="${batchFileName}"`, }; resp.writeHead(200, headers); return zipFile.outputStream.pipe(resp); }); - } + }, ], err => { - if(err) { + if (err) { // :TODO: Log me! return this.fileNotFound(resp); } @@ -458,41 +515,36 @@ class FileAreaWebAccess { } 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, UserProps.FileDlTotalCount, 1); - StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes); - - StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1); - StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes); - - return callback(null, user); - }, - function sendEvent(user, callback) { - Events.emit( - Events.getSystemEvents().UserDownload, - { - user : user, - files : fileEntries, - } - ); - return callback(null); + 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, UserProps.FileDlTotalCount, 1); + StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes); + + StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1); + StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes); + + 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 +module.exports = new FileAreaWebAccess(); diff --git a/core/file_base_area.js b/core/file_base_area.js index 57cf9cc7..aeb40513 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -2,78 +2,81 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; -const FileEntry = require('./file_entry.js'); -const FileDb = require('./database.js').dbs.file; -const ArchiveUtil = require('./archive_util.js'); -const CRC32 = require('./crc.js').CRC32; -const Log = require('./logger.js').log; -const resolveMimeType = require('./mime_util.js').resolveMimeType; -const stringFormat = require('./string_format.js'); -const wordWrapText = require('./word_wrap.js').wordWrapText; -const StatLog = require('./stat_log.js'); -const UserProps = require('./user_property.js'); -const SysProps = require('./system_property.js'); -const SAUCE = require('./sauce.js'); +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; +const FileEntry = require('./file_entry.js'); +const FileDb = require('./database.js').dbs.file; +const ArchiveUtil = require('./archive_util.js'); +const CRC32 = require('./crc.js').CRC32; +const Log = require('./logger.js').log; +const resolveMimeType = require('./mime_util.js').resolveMimeType; +const stringFormat = require('./string_format.js'); +const wordWrapText = require('./word_wrap.js').wordWrapText; +const StatLog = require('./stat_log.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); +const SAUCE = require('./sauce.js'); const { wildcardMatch } = require('./string_util'); // deps -const _ = require('lodash'); -const async = require('async'); -const fs = require('graceful-fs'); -const crypto = require('crypto'); -const paths = require('path'); -const temptmp = require('temptmp').createTrackedSession('file_area'); -const iconv = require('iconv-lite'); -const execFile = require('child_process').execFile; -const moment = require('moment'); +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const crypto = require('crypto'); +const paths = require('path'); +const temptmp = require('temptmp').createTrackedSession('file_area'); +const iconv = require('iconv-lite'); +const execFile = require('child_process').execFile; +const moment = require('moment'); -exports.startup = startup; -exports.isInternalArea = isInternalArea; -exports.getAvailableFileAreas = getAvailableFileAreas; -exports.getAvailableFileAreaTags = getAvailableFileAreaTags; -exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; -exports.isValidStorageTag = isValidStorageTag; -exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag; -exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; -exports.getAreaStorageLocations = getAreaStorageLocations; -exports.getDefaultFileAreaTag = getDefaultFileAreaTag; -exports.getFileAreaByTag = getFileAreaByTag; -exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule; -exports.getFileEntryPath = getFileEntryPath; -exports.changeFileAreaWithOptions = changeFileAreaWithOptions; -exports.scanFile = scanFile; +exports.startup = startup; +exports.isInternalArea = isInternalArea; +exports.getAvailableFileAreas = getAvailableFileAreas; +exports.getAvailableFileAreaTags = getAvailableFileAreaTags; +exports.getSortedAvailableFileAreas = getSortedAvailableFileAreas; +exports.isValidStorageTag = isValidStorageTag; +exports.getAreaStorageDirectoryByTag = getAreaStorageDirectoryByTag; +exports.getAreaDefaultStorageDirectory = getAreaDefaultStorageDirectory; +exports.getAreaStorageLocations = getAreaStorageLocations; +exports.getDefaultFileAreaTag = getDefaultFileAreaTag; +exports.getFileAreaByTag = getFileAreaByTag; +exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule; +exports.getFileEntryPath = getFileEntryPath; +exports.changeFileAreaWithOptions = changeFileAreaWithOptions; +exports.scanFile = scanFile; //exports.scanFileAreaForChanges = scanFileAreaForChanges; -exports.getDescFromFileName = getDescFromFileName; -exports.getAreaStats = getAreaStats; -exports.cleanUpTempSessionItems = cleanUpTempSessionItems; +exports.getDescFromFileName = getDescFromFileName; +exports.getAreaStats = getAreaStats; +exports.cleanUpTempSessionItems = cleanUpTempSessionItems; // for scheduler: -exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; +exports.updateAreaStatsScheduledEvent = updateAreaStatsScheduledEvent; -const WellKnownAreaTags = exports.WellKnownAreaTags = { - Invalid : '', - MessageAreaAttach : 'system_message_attachment', - TempDownloads : 'system_temporary_download', -}; +const WellKnownAreaTags = (exports.WellKnownAreaTags = { + Invalid: '', + MessageAreaAttach: 'system_message_attachment', + TempDownloads: 'system_temporary_download', +}); function startup(cb) { async.series( [ - (callback) => { + callback => { return cleanUpTempSessionItems(callback); }, - (callback) => { - getAreaStats( (err, stats) => { - if(!err) { - StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats); + callback => { + getAreaStats((err, stats) => { + if (!err) { + StatLog.setNonPersistentSystemStat( + SysProps.FileBaseAreaStats, + stats + ); } return callback(null); }); - } + }, ], err => { return cb(err); @@ -82,26 +85,31 @@ function startup(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 } )); + const allAreas = _.map(Config().fileBase.areas, (areaInfo, areaTag) => + Object.assign(areaInfo, { areaTag: areaTag }) + ); return _.omitBy(allAreas, areaInfo => { - if(!options.includeSystemInternal && isInternalArea(areaInfo.areaTag)) { + 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); @@ -121,16 +129,19 @@ function getSortedAvailableFileAreas(client, options) { function getDefaultFileAreaTag(client, disableAcsCheck) { const config = Config(); let defaultArea = _.findKey(config.fileBase, o => o.default); - if(defaultArea) { + if (defaultArea) { const area = config.fileBase.areas[defaultArea]; - if(true === disableAcsCheck || client.acs.hasFileAreaRead(area)) { + 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)); + return ( + WellKnownAreaTags.MessageAreaAttach !== areaTag && + (true === disableAcsCheck || client.acs.hasFileAreaRead(area)) + ); }); return defaultArea; @@ -138,7 +149,7 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { function getFileAreaByTag(areaTag) { const areaInfo = Config().fileBase.areas[areaTag]; - if(areaInfo) { + if (areaInfo) { // normalize |hashTags| if (_.isString(areaInfo.hashTags)) { areaInfo.hashTags = areaInfo.hashTags.trim().split(','); @@ -146,17 +157,16 @@ function getFileAreaByTag(areaTag) { if (Array.isArray(areaInfo.hashTags)) { areaInfo.hashTags = new Set(areaInfo.hashTags.map(t => t.trim())); } - areaInfo.areaTag = areaTag; // convenience! - areaInfo.storage = getAreaStorageLocations(areaInfo); + areaInfo.areaTag = areaTag; // convenience! + areaInfo.storage = getAreaStorageLocations(areaInfo); return areaInfo; } } function getFileAreasByTagWildcardRule(rule) { - const areaTags = Object.keys(Config().fileBase.areas) - .filter(areaTag => { - return !isInternalArea(areaTag) && wildcardMatch(areaTag, rule); - }); + const areaTags = Object.keys(Config().fileBase.areas).filter(areaTag => { + return !isInternalArea(areaTag) && wildcardMatch(areaTag, rule); + }); return areaTags.map(areaTag => getFileAreaByTag(areaTag)); } @@ -166,15 +176,18 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) { [ function getArea(callback) { const area = getFileAreaByTag(areaTag); - return callback(area ? null : Errors.Invalid('Invalid file areaTag'), area); + return callback( + area ? null : Errors.Invalid('Invalid file areaTag'), + area + ); }, function validateAccess(area, callback) { - if(!client.acs.hasFileAreaRead(area)) { + if (!client.acs.hasFileAreaRead(area)) { return callback(Errors.AccessDenied('No access to this area')); } }, function changeArea(area, callback) { - if(true === options.persist) { + if (true === options.persist) { client.user.persistProperty(UserProps.FileAreaTag, areaTag, err => { return callback(err, area); }); @@ -182,13 +195,19 @@ function changeFileAreaWithOptions(client, areaTag, options, cb) { client.user.properties[UserProps.FileAreaTag] = areaTag; return callback(null, area); } - } + }, ], (err, area) => { - if(!err) { - client.log.info( { areaTag : areaTag, area : area }, 'Current file area changed'); + 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'); + client.log.warn( + { areaTag: areaTag, area: area, error: err.message }, + 'Could not change file area' + ); } return cb(err); @@ -202,7 +221,7 @@ function isValidStorageTag(storageTag) { function getAreaStorageDirectoryByTag(storageTag) { const config = Config(); - const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); + const storageLocation = storageTag && config.fileBase.storageTags[storageTag]; return paths.resolve(config.fileBase.areaStoragePrefix, storageLocation || ''); } @@ -212,26 +231,27 @@ function getAreaDefaultStorageDirectory(areaInfo) { } 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; - 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) { + if (areaInfo) { return paths.join(areaInfo.storageDirectory, fileEntry.fileName); } } @@ -243,12 +263,12 @@ function getExistingFileEntriesBySha256(sha256, cb) { `SELECT file_id, area_tag FROM file WHERE file_sha256=?;`, - [ sha256 ], + [sha256], (err, fileRow) => { - if(fileRow) { + if (fileRow) { entries.push({ - fileId : fileRow.file_id, - areaTag : fileRow.area_tag, + fileId: fileRow.file_id, + areaTag: fileRow.area_tag, }); } }, @@ -260,11 +280,11 @@ function getExistingFileEntriesBySha256(sha256, cb) { // :TODO: This is basically sliceAtEOF() from art.js .... DRY! function sliceAtSauceMarker(data) { - let eof = data.length; - const stopPos = Math.max(data.length - (256), 0); // 256 = 2 * sizeof(SAUCE) + 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]) { + for (let i = eof - 1; i > stopPos; i--) { + if (0x1a === data[i]) { eof = i; break; } @@ -274,14 +294,14 @@ function sliceAtSauceMarker(data) { function attemptSetEstimatedReleaseDate(fileEntry) { // :TODO: yearEstPatterns RegExp's should be cached - we can do this @ Config (re)load time - const patterns = Config().fileBase.yearEstPatterns.map( p => new RegExp(p, 'gmi')); + const patterns = Config().fileBase.yearEstPatterns.map(p => new RegExp(p, 'gmi')); function getMatch(input) { - if(input) { + if (input) { let m; - for(let i = 0; i < patterns.length; ++i) { + for (let i = 0; i < patterns.length; ++i) { m = patterns[i].exec(input); - if(m) { + if (m) { return m; } } @@ -297,12 +317,12 @@ function attemptSetEstimatedReleaseDate(fileEntry) { const maxYear = moment().add(2, 'year').year(); const match = getMatch(fileEntry.desc) || getMatch(fileEntry.descLong); - if(match && match[1]) { + if (match && match[1]) { let year; - if(2 === match[1].length) { + if (2 === match[1].length) { year = parseInt(match[1]); - if(year) { - if(year > 70) { + if (year) { + if (year > 70) { year += 1900; } else { year += 2000; @@ -312,7 +332,7 @@ function attemptSetEstimatedReleaseDate(fileEntry) { year = parseInt(match[1]); } - if(year && year <= maxYear) { + if (year && year <= maxYear) { fileEntry.meta.est_release_year = year; } } @@ -320,10 +340,10 @@ function attemptSetEstimatedReleaseDate(fileEntry) { // a simple log proxy for when we call from oputil.js const maybeLog = (obj, msg, level) => { - if(Log) { + if (Log) { Log[level](obj, msg); } else if ('error' === level) { - console.error(`${msg}: ${JSON.stringify(obj)}`); // eslint-disable-line no-console + console.error(`${msg}: ${JSON.stringify(obj)}`); // eslint-disable-line no-console } }; @@ -340,99 +360,139 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { 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) { + 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) { + 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) { + 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); + archiveUtil.extractTo( + filePath, + tempDir, + fileEntry.meta.archive_type, + extractList, + err => { + if (err) { + return callback(err); + } + + const descFiles = { + desc: shortDescFile + ? paths.join( + tempDir, + paths.basename(shortDescFile.fileName) + ) + : null, + descLong: longDescFile + ? paths.join( + tempDir, + paths.basename(longDescFile.fileName) + ) + : null, + }; + + return callback(null, descFiles); } - - const descFiles = { - desc : shortDescFile ? paths.join(tempDir, paths.basename(shortDescFile.fileName)) : null, - descLong : longDescFile ? paths.join(tempDir, paths.basename(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); - } - - fs.stat(path, (err, stats) => { - if(err) { + async.each( + Object.keys(descFiles), + (descType, next) => { + const path = descFiles[descType]; + if (!path) { 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) { + fs.stat(path, (err, stats) => { + if (err) { return next(null); } - SAUCE.readSAUCE(data, (err, sauce) => { - if(sauce) { - // if we have SAUCE, this information will be kept as well, - // but separate/pre-parsed. - const metaKey = `desc${'descLong' === descType ? '_long' : ''}_sauce`; - fileEntry.meta[metaKey] = JSON.stringify(sauce); + // 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); } - // - // Assume FILE_ID.DIZ, NFO files, etc. are CP437; we need - // to decode to a native format for storage - // - // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... - const decodedData = iconv.decode(data, 'cp437'); - fileEntry[descType] = sliceAtSauceMarker(decodedData); - fileEntry[`${descType}Src`] = 'descFile'; - return next(null); + SAUCE.readSAUCE(data, (err, sauce) => { + if (sauce) { + // if we have SAUCE, this information will be kept as well, + // but separate/pre-parsed. + const metaKey = `desc${ + 'descLong' === descType ? '_long' : '' + }_sauce`; + fileEntry.meta[metaKey] = JSON.stringify(sauce); + } + + // + // Assume FILE_ID.DIZ, NFO files, etc. are CP437; we need + // to decode to a native format for storage + // + // :TODO: This isn't really always the case - how to handle this? We could do a quick detection... + const decodedData = iconv.decode(data, 'cp437'); + fileEntry[descType] = sliceAtSauceMarker(decodedData); + 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 - logTrace( { paths : paths, sessionId : temptmp.sessionId }, 'Cleaned up temporary files' ); - }); - return callback(null); - }); + }, + () => { + // cleanup but don't wait + temptmp.cleanup(paths => { + // note: don't use client logger here - may not be avail + logTrace( + { paths: paths, sessionId: temptmp.sessionId }, + 'Cleaned up temporary files' + ); + }); + return callback(null); + } + ); }, ], err => { @@ -442,39 +502,46 @@ function extractAndProcessDescFiles(fileEntry, filePath, archiveEntries, cb) { } 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) { + temptmp.mkdir({ prefix: 'enigextract-' }, (err, tempDir) => { + if (err) { return callback(err); } 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); + 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])); } - - 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'; + if (!fileEntry.desc) { + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; } return callback(err); }); - } + }, ], err => { return cb(err); @@ -483,8 +550,8 @@ function extractAndProcessSingleArchiveEntry(fileEntry, filePath, archiveEntries } function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, cb) { - const archiveUtil = ArchiveUtil.getInstance(); - const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() + const archiveUtil = ArchiveUtil.getInstance(); + const archiveType = fileEntry.meta.archive_type; // we set this previous to populateFileEntryWithArchive() async.waterfall( [ @@ -492,12 +559,12 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c stepInfo.step = 'archive_list_start'; iterator(err => { - if(err) { + if (err) { return callback(err); } archiveUtil.listEntries(filePath, archiveType, (err, entries) => { - if(err) { + if (err) { stepInfo.step = 'archive_list_failed'; } else { stepInfo.step = 'archive_list_finish'; @@ -505,7 +572,7 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c } iterator(iterErr => { - return callback( iterErr, entries || [] ); // ignore original |err| here + return callback(iterErr, entries || []); // ignore original |err| here }); }); }); @@ -525,7 +592,10 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c // Otherwise, try to find particular desc files such as FILE_ID.DIZ // and README.1ST // - const archDescHandler = (1 === entries.length) ? extractAndProcessSingleArchiveEntry : extractAndProcessDescFiles; + const archDescHandler = + 1 === entries.length + ? extractAndProcessSingleArchiveEntry + : extractAndProcessDescFiles; archDescHandler(fileEntry, filePath, entries, err => { return callback(err); }); @@ -547,24 +617,24 @@ function populateFileEntryWithArchive(fileEntry, filePath, stepInfo, iterator, c function getInfoExtractUtilForDesc(mimeType, filePath, descType) { const config = Config(); - let fileType = _.get(config, [ 'fileTypes', mimeType ] ); + let fileType = _.get(config, ['fileTypes', mimeType]); - if(Array.isArray(fileType)) { + if (Array.isArray(fileType)) { // further refine by extention fileType = fileType.find(ft => paths.extname(filePath) === ft.ext); } - if(!_.isObject(fileType)) { + if (!_.isObject(fileType)) { return; } let util = _.get(fileType, `${descType}DescUtil`); - if(!_.isString(util)) { + if (!_.isString(util)) { return; } - util = _.get(config, [ 'infoExtractUtils', util ]); - if(!util || !_.isString(util.cmd)) { + util = _.get(config, ['infoExtractUtils', util]); + if (!util || !_.isString(util.cmd)) { return; } @@ -573,54 +643,61 @@ function getInfoExtractUtilForDesc(mimeType, filePath, descType) { function populateFileEntryInfoFromFile(fileEntry, filePath, cb) { const mimeType = resolveMimeType(filePath); - if(!mimeType) { + if (!mimeType) { return cb(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 } ) ); - - 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'; - } + async.eachSeries( + ['short', 'long'], + (descType, nextDesc) => { + const util = getInfoExtractUtilForDesc(mimeType, filePath, descType); + if (!util) { + return nextDesc(null); } - return nextDesc(null); - }); - }, () => { - return cb(null); - }); + 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'); + } + + fileEntry[key] = stdout; + fileEntry[`${key}Src`] = 'infoTool'; + } + } + + return nextDesc(null); + }); + }, + () => { + return cb(null); + } + ); } function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb) { - async.series( [ function processDescFilesStart(callback) { @@ -629,9 +706,9 @@ function populateFileEntryNonArchive(fileEntry, filePath, stepInfo, iterator, cb }, function getDescriptions(callback) { populateFileEntryInfoFromFile(fileEntry, filePath, err => { - if(!fileEntry.desc) { - fileEntry.desc = getDescFromFileName(filePath); - fileEntry.descSrc = 'fileName'; + if (!fileEntry.desc) { + fileEntry.desc = getDescFromFileName(filePath); + fileEntry.descSrc = 'fileName'; } return callback(err); }); @@ -654,7 +731,7 @@ function addNewFileEntry(fileEntry, filePath, cb) { [ function addNewDbRecord(callback) { return fileEntry.persist(callback); - } + }, ], err => { return cb(err); @@ -662,42 +739,41 @@ function addNewFileEntry(fileEntry, filePath, cb) { ); } -const HASH_NAMES = [ 'sha1', 'sha256', 'md5', 'crc32' ]; +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 + areaTag: options.areaTag, + meta: options.meta, + hashTags: options.hashTags, // Set() or Array + fileName: paths.basename(filePath), + storageTag: options.storageTag, + fileSha256: options.sha256, // caller may know this already }); const stepInfo = { - filePath : filePath, - fileName : paths.basename(filePath), + filePath: filePath, + fileName: paths.basename(filePath), }; - const callIter = (next) => { + const callIter = next => { return iterator ? iterator(stepInfo, next) : next(null); }; const readErrorCallIter = (origError, next) => { - stepInfo.step = 'read_error'; - stepInfo.error = origError.message; + stepInfo.step = 'read_error'; + stepInfo.error = origError.message; - callIter( () => { + callIter(() => { return next(origError); }); }; @@ -705,12 +781,12 @@ function scanFile(filePath, options, iterator, cb) { 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) { + const hashesToCalc = HASH_NAMES.filter(hn => { + if ('sha256' === hn && fileEntry.fileSha256) { return false; } - if(`file_${hn}` in fileEntry.meta) { + if (`file_${hn}` in fileEntry.meta) { return false; } @@ -721,12 +797,12 @@ function scanFile(filePath, options, iterator, cb) { [ function startScan(callback) { fs.stat(filePath, (err, stats) => { - if(err) { + 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); }); @@ -736,15 +812,15 @@ function scanFile(filePath, options, iterator, cb) { const hashes = {}; hashesToCalc.forEach(hashName => { - if('crc32' === hashName) { - hashes.crc32 = new CRC32; + if ('crc32' === hashName) { + hashes.crc32 = new CRC32(); } else { hashes[hashName] = crypto.createHash(hashName); } }); - const updateHashes = (data) => { - for(let i = 0; i < hashesToCalc.length; ++i) { + const updateHashes = data => { + for (let i = 0; i < hashesToCalc.length; ++i) { hashes[hashesToCalc[i]].update(data); } }; @@ -758,61 +834,82 @@ function scanFile(filePath, options, iterator, cb) { const buffer = Buffer.allocUnsafe(chunkSize); fs.open(filePath, 'r', (err, fd) => { - if(err) { + if (err) { return readErrorCallIter(err, callback); } const nextChunk = () => { fs.read(fd, buffer, 0, chunkSize, null, (err, bytesRead) => { - if(err) { + if (err) { return fs.close(fd, closeErr => { - if(closeErr) { - logError( { filePath, error : err.message }, 'Failed to close file'); + if (closeErr) { + logError( + { filePath, error: err.message }, + 'Failed to close file' + ); } return readErrorCallIter(err, callback); }); } - if(0 === bytesRead) { + if (0 === bytesRead) { // done - finalize fileEntry.meta.byte_size = stepInfo.bytesProcessed; - for(let i = 0; i < hashesToCalc.length; ++i) { + 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); + if ('sha256' === hashName) { + stepInfo.sha256 = fileEntry.fileSha256 = + hashes.sha256.digest('hex'); + } else if ( + 'sha1' === hashName || + 'md5' === hashName + ) { + stepInfo[hashName] = fileEntry.meta[ + `file_${hashName}` + ] = hashes[hashName].digest('hex'); + } else if ('crc32' === hashName) { + stepInfo.crc32 = fileEntry.meta.file_crc32 = + hashes.crc32.finalize().toString(16); } } stepInfo.step = 'hash_finish'; return fs.close(fd, closeErr => { - if(closeErr) { - logError( { filePath, error : err.message }, 'Failed to close file'); + if (closeErr) { + logError( + { filePath, error: err.message }, + 'Failed to close file' + ); } 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 noticeable percentage change in progress // - const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; - if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { + 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'; + stepInfo.step = 'hash_update'; callIter(err => { - if(err) { + if (err) { return callback(err); } @@ -830,46 +927,73 @@ function scanFile(filePath, options, iterator, cb) { const archiveUtil = ArchiveUtil.getInstance(); archiveUtil.detectType(filePath, (err, archiveType) => { - if(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); + 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'); + populateFileEntryNonArchive( + fileEntry, + filePath, + stepInfo, + callIter, + err => { + if (err) { + logDebug( + { error: err.message }, + 'Non-archive file entry population failed' + ); + } + return callback(null); // ignore err } - return callback(null); // ignore err - }); + ); } }); }, function fetchExistingEntry(callback) { - getExistingFileEntriesBySha256(fileEntry.fileSha256, (err, dupeEntries) => { - return callback(err, dupeEntries); - }); + getExistingFileEntriesBySha256( + fileEntry.fileSha256, + (err, dupeEntries) => { + return callback(err, dupeEntries); + } + ); }, function finished(dupeEntries, callback) { stepInfo.step = 'finished'; - callIter( () => { + callIter(() => { return callback(null, dupeEntries); }); - } + }, ], (err, dupeEntries) => { - if(err) { + if (err) { return cb(err); } @@ -980,11 +1104,12 @@ function getDescFromFileName(fileName) { // * https://scenerules.org/ // - const ext = paths.extname(fileName); - const name = paths.basename(fileName, ext); - const asIsRe = /([vV]?(?:[0-9]{1,4})(?:\.[0-9]{1,4})+[-+]?(?:[a-z]{1,4})?)|(Incl\.)|(READ\.NFO)/g; + const ext = paths.extname(fileName); + const name = paths.basename(fileName, ext); + const asIsRe = + /([vV]?(?:[0-9]{1,4})(?:\.[0-9]{1,4})+[-+]?(?:[a-z]{1,4})?)|(Incl\.)|(READ\.NFO)/g; - const normalize = (s) => { + const normalize = s => { return _.upperFirst(s.replace(/[-_.+]/g, ' ').replace(/\s+/g, ' ')); }; @@ -994,15 +1119,15 @@ function getDescFromFileName(fileName) { do { pos = asIsRe.lastIndex; m = asIsRe.exec(name); - if(m) { - if(m.index > pos) { + if (m) { + if (m.index > pos) { out += normalize(name.slice(pos, m.index)); } - out += m[0]; // as-is + out += m[0]; // as-is } - } while(0 != asIsRe.lastIndex); + } while (0 != asIsRe.lastIndex); - if(pos < name.length) { + if (pos < name.length) { out += normalize(name.slice(pos)); } @@ -1031,25 +1156,25 @@ function getAreaStats(cb) { WHERE f.file_id = m.file_id AND m.meta_name='byte_size' GROUP BY f.area_tag;`, (err, statRows) => { - if(err) { + if (err) { return cb(err); } - if(!statRows || 0 === statRows.length) { + if (!statRows || 0 === statRows.length) { return cb(Errors.DoesNotExist('No file areas to acquire stats from')); } return cb( null, - statRows.reduce( (stats, v) => { + 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[v.area_tag] = { - files : v.total_files, - bytes : v.total_byte_size, + files: v.total_files, + bytes: v.total_byte_size, }; return stats; }, {}) @@ -1060,8 +1185,8 @@ function getAreaStats(cb) { // method exposed for event scheduler function updateAreaStatsScheduledEvent(args, cb) { - getAreaStats( (err, stats) => { - if(!err) { + getAreaStats((err, stats) => { + if (!err) { StatLog.setNonPersistentSystemStat(SysProps.FileBaseAreaStats, stats); } @@ -1072,37 +1197,50 @@ function updateAreaStatsScheduledEvent(args, cb) { function cleanUpTempSessionItems(cb) { // find (old) temporary session items and nuke 'em const filter = { - areaTag : WellKnownAreaTags.TempDownloads, - metaPairs : [ + areaTag: WellKnownAreaTags.TempDownloads, + metaPairs: [ { - name : 'session_temp_dl', - value : 1 - } - ] + name: 'session_temp_dl', + value: 1, + }, + ], }; FileEntry.findFiles(filter, (err, fileIds) => { - if(err) { + if (err) { return cb(err); } - async.each(fileIds, (fileId, nextFileId) => { - const fileEntry = new FileEntry(); - fileEntry.load(fileId, err => { - if(err) { - Log.warn( { fileId }, 'Failed loading temporary session download item for cleanup'); - return nextFileId(null); - } - - FileEntry.removeEntry(fileEntry, { removePhysFile : true }, err => { - if(err) { - Log.warn( { fileId : fileEntry.fileId, filePath : fileEntry.filePath }, 'Failed to clean up temporary session download item'); + 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); } - 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); - }); + }, + () => { + 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 a8f74322..f936207d 100644 --- a/core/file_base_area_select.js +++ b/core/file_base_area_select.js @@ -2,22 +2,22 @@ 'use strict'; // enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const { getSortedAvailableFileAreas } = require('./file_base_area.js'); -const StatLog = require('./stat_log.js'); -const SysProps = require('./system_property.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const { getSortedAvailableFileAreas } = require('./file_base_area.js'); +const StatLog = require('./stat_log.js'); +const SysProps = require('./system_property.js'); // deps -const async = require('async'); +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 { @@ -25,26 +25,31 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { super(options); this.menuMethods = { - selectArea : (formData, extraArgs, cb) => { + selectArea: (formData, extraArgs, cb) => { const filterCriteria = { - areaTag : formData.value.areaTag, + areaTag: formData.value.areaTag, }; const menuOpts = { - extraArgs : { - filterCriteria : filterCriteria, + extraArgs: { + filterCriteria: filterCriteria, }, - menuFlags : [ 'popParent', 'mergeFlags' ], + menuFlags: ['popParent', 'mergeFlags'], }; - return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); - } + return this.gotoMenu( + this.menuConfig.config.fileBaseListEntriesMenu || + 'fileBaseListEntries', + menuOpts, + cb + ); + }, }; } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } @@ -53,7 +58,9 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { async.waterfall( [ function mergeAreaStats(callback) { - const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || { areas : {} }; + const areaStats = StatLog.getSystemStat( + SysProps.FileBaseAreaStats + ) || { areas: {} }; // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override const availAreas = getSortedAvailableFileAreas(self.client); @@ -66,18 +73,30 @@ exports.getModule = class FileAreaSelectModule extends MenuModule { return callback(null, availAreas); }, function prepView(availAreas, callback) { - self.prepViewController('allViews', 0, mciData.menu, (err, vc) => { - if(err) { - return callback(err); + 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(); + + return callback(null); } - - 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); diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index 8487697f..5d232365 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -2,91 +2,101 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const DownloadQueue = require('./download_queue.js'); -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const Errors = require('./enig_error.js').Errors; -const FileAreaWeb = require('./file_area_web.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const DownloadQueue = require('./download_queue.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const Errors = require('./enig_error.js').Errors; +const FileAreaWeb = require('./file_area_web.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { - name : 'File Base Download Queue Manager', - desc : 'Module for interacting with download queue/batch', - author : 'NuSkooler', + name: 'File Base Download Queue Manager', + desc: 'Module for interacting with download queue/batch', + author: 'NuSkooler', }; const FormIds = { - queueManager : 0, + queueManager: 0, }; const MciViewIds = { - queueManager : { - queue : 1, - navMenu : 2, + queueManager: { + queue: 1, + navMenu: 2, - customRangeStart : 10, + customRangeStart: 10, }, }; exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { - constructor(options) { super(options); this.dlQueue = new DownloadQueue(this.client); - if(_.has(options, 'lastMenuResult.sentFileIds')) { + if (_.has(options, 'lastMenuResult.sentFileIds')) { this.sentFileIds = options.lastMenuResult.sentFileIds; } this.fallbackOnly = options.lastMenuResult ? true : false; this.menuMethods = { - downloadAll : (formData, extraArgs, cb) => { + downloadAll: (formData, extraArgs, cb) => { const modOpts = { - extraArgs : { - sendQueue : this.dlQueue.items, - direction : 'send', - } + extraArgs: { + sendQueue: this.dlQueue.items, + direction: 'send', + }, }; - return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); + return this.gotoMenu( + this.menuConfig.config.fileTransferProtocolSelection || + 'fileTransferProtocolSelection', + modOpts, + cb + ); }, - removeItem : (formData, extraArgs, cb) => { + removeItem: (formData, extraArgs, cb) => { const selectedItem = this.dlQueue.items[formData.value.queueItem]; - if(!selectedItem) { + if (!selectedItem) { return cb(null); } this.dlQueue.removeItems(selectedItem.fileId); // :TODO: broken: does not redraw menu properly - needs fixed! - return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); + return this.removeItemsFromDownloadQueueView( + formData.value.queueItem, + cb + ); }, - clearQueue : (formData, extraArgs, cb) => { + clearQueue: (formData, extraArgs, cb) => { this.dlQueue.clear(); // :TODO: broken: does not redraw menu properly - needs fixed! return this.removeItemsFromDownloadQueueView('all', cb); - } + }, }; } initSequence() { - if(0 === this.dlQueue.items.length) { - if(this.sendFileIds) { + 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'); + return this.gotoMenu( + this.menuConfig.config.emptyQueueMenu || + 'fileBaseDownloadManagerEmptyQueue' + ); } const self = this; @@ -98,7 +108,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { }, function display(callback) { return self.displayQueueManagerPage(false, callback); - } + }, ], () => { return self.finishedLoading(); @@ -107,12 +117,14 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { } removeItemsFromDownloadQueueView(itemIndex, cb) { - const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); - if(!queueView) { + const queueView = this.viewControllers.queueManager.getView( + MciViewIds.queueManager.queue + ); + if (!queueView) { return cb(Errors.DoesNotExist('Queue view does not exist')); } - if('all' === itemIndex) { + if ('all' === itemIndex) { queueView.setItems([]); queueView.setFocusItems([]); } else { @@ -124,28 +136,40 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { } displayWebDownloadLinkForFileEntry(fileEntry) { - FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => { - if(serveItem && serveItem.url) { - const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + 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) { + const queueView = this.viewControllers.queueManager.getView( + MciViewIds.queueManager.queue + ); + if (!queueView) { return cb(Errors.DoesNotExist('Queue view does not exist')); } @@ -168,14 +192,18 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { async.series( [ function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); + return self.displayArtAndPrepViewController( + 'queueManager', + { clearScreen: clearScreen }, + callback + ); }, function populateViews(callback) { return self.updateDownloadQueueView(callback); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -183,42 +211,45 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { } displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; async.waterfall( [ function readyAndDisplayArt(callback) { - if(options.clearScreen) { + if (options.clearScreen) { self.client.term.rawWrite(ansi.resetScreen()); } theme.displayThemedAsset( config.art[name], self.client, - { font : self.menuConfig.font, trailingLF : false }, + { font: self.menuConfig.font, trailingLF: false }, (err, artData) => { return callback(err, artData); } ); }, function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { + if (_.isUndefined(self.viewControllers[name])) { const vcOpts = { - client : self.client, - formId : FormIds[name], + client: self.client, + formId: FormIds[name], }; - if(!_.isUndefined(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], + callingMenu: self, + mciMap: artData.mciMap, + formId: FormIds[name], }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -226,7 +257,6 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { self.viewControllers[name].setFocus(true); return callback(null); - }, ], err => { diff --git a/core/file_base_filter.js b/core/file_base_filter.js index ecf857fa..b1e357f3 100644 --- a/core/file_base_filter.js +++ b/core/file_base_filter.js @@ -4,8 +4,8 @@ const UserProps = require('./user_property.js'); // deps -const _ = require('lodash'); -const { v4 : UUIDv4 } = require('uuid'); +const _ = require('lodash'); +const { v4: UUIDv4 } = require('uuid'); module.exports = class FileBaseFilters { constructor(client) { @@ -15,7 +15,7 @@ module.exports = class FileBaseFilters { } static get OrderByValues() { - return [ 'descending', 'ascending' ]; + return ['descending', 'ascending']; } static get SortByValues() { @@ -32,7 +32,7 @@ module.exports = class FileBaseFilters { toArray() { return _.map(this.filters, (filter, uuid) => { - return Object.assign( { uuid : uuid }, filter ); + return Object.assign({ uuid: uuid }, filter); }); } @@ -52,7 +52,7 @@ module.exports = class FileBaseFilters { replace(filterUuid, filterInfo) { const filter = this.get(filterUuid); - if(!filter) { + if (!filter) { return false; } @@ -68,22 +68,25 @@ module.exports = class FileBaseFilters { load() { let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters]; let defaulted; - if(!filtersProperty) { + 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 :( + } 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' ); + this.client.log.error( + { error: e.message, property: filtersProperty }, + 'Failed parsing file base filters property' + ); } - if(defaulted) { - this.persist( err => { - if(!err) { + if (defaulted) { + this.persist(err => { + if (!err) { const defaultActiveUuid = this.toArray()[0].uuid; this.setActive(defaultActiveUuid); } @@ -92,19 +95,29 @@ module.exports = class FileBaseFilters { } persist(cb) { - return this.client.user.persistProperty(UserProps.FileBaseFilters, JSON.stringify(this.filters), cb); + return this.client.user.persistProperty( + UserProps.FileBaseFilters, + JSON.stringify(this.filters), + cb + ); } cleanTags(tags) { - return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim(); + return tags + .toLowerCase() + .replace(/,?\s+|,/g, ' ') + .trim(); } setActive(filterUuid) { const activeFilter = this.get(filterUuid); - if(activeFilter) { + if (activeFilter) { this.activeFilter = activeFilter; - this.client.user.persistProperty(UserProps.FileBaseFilterActiveUuid, filterUuid); + this.client.user.persistProperty( + UserProps.FileBaseFilterActiveUuid, + filterUuid + ); return true; } @@ -112,41 +125,43 @@ module.exports = class FileBaseFilters { } static getBuiltInSystemFilters() { - const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; + const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; const filters = { - [ U_LATEST ] : { - name : 'By Date Added', - areaTag : '', // all - terms : '', // * - tags : '', // * - order : 'descending', - sort : 'upload_timestamp', - uuid : U_LATEST, - system : true, - } + [U_LATEST]: { + name: 'By Date Added', + areaTag: '', // all + terms: '', // * + tags: '', // * + order: 'descending', + sort: 'upload_timestamp', + uuid: U_LATEST, + system: true, + }, }; return filters; } static getActiveFilter(client) { - return new FileBaseFilters(client).get(client.user.properties[UserProps.FileBaseFilterActiveUuid]); + return new FileBaseFilters(client).get( + client.user.properties[UserProps.FileBaseFilterActiveUuid] + ); } static getFileBaseLastViewedFileIdByUser(user) { - return parseInt((user.properties[UserProps.FileBaseLastViewedId] || 0)); + return parseInt(user.properties[UserProps.FileBaseLastViewedId] || 0); } static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { - if(!cb && _.isFunction(allowOlder)) { + if (!cb && _.isFunction(allowOlder)) { cb = allowOlder; allowOlder = false; } const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user); - if(!allowOlder && fileId < current) { - if(cb) { + if (!allowOlder && fileId < current) { + if (cb) { cb(null); } return; diff --git a/core/file_base_list_export.js b/core/file_base_list_export.js index 5c4991f3..5c92c370 100644 --- a/core/file_base_list_export.js +++ b/core/file_base_list_export.js @@ -2,235 +2,283 @@ 'use strict'; // ENiGMA½ -const stringFormat = require('./string_format.js'); -const FileEntry = require('./file_entry.js'); -const FileArea = require('./file_base_area.js'); -const Config = require('./config.js').get; -const { Errors } = require('./enig_error.js'); -const { - splitTextAtTerms, - isAnsi, -} = require('./string_util.js'); -const AnsiPrep = require('./ansi_prep.js'); -const Log = require('./logger.js').log; +const stringFormat = require('./string_format.js'); +const FileEntry = require('./file_entry.js'); +const FileArea = require('./file_base_area.js'); +const Config = require('./config.js').get; +const { Errors } = require('./enig_error.js'); +const { splitTextAtTerms, isAnsi } = require('./string_util.js'); +const AnsiPrep = require('./ansi_prep.js'); +const Log = require('./logger.js').log; // deps -const _ = require('lodash'); -const async = require('async'); -const fs = require('graceful-fs'); -const paths = require('path'); -const iconv = require('iconv-lite'); -const moment = require('moment'); +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const paths = require('path'); +const iconv = require('iconv-lite'); +const moment = require('moment'); -exports.exportFileList = exportFileList; -exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; +exports.exportFileList = exportFileList; +exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; function exportFileList(filterCriteria, options, cb) { - options.templateEncoding = options.templateEncoding || 'utf8'; - options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; - options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; - options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec - options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? + options.templateEncoding = options.templateEncoding || 'utf8'; + options.entryTemplate = + options.entryTemplate || 'descript_ion_export_entry_template.asc'; + options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; + options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec + options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? - if(true === options.escapeDesc) { + if (true === options.escapeDesc) { options.escapeDesc = '\\n'; } const state = { - total : 0, - current : 0, - step : 'preparing', - status : 'Preparing', + total: 0, + current: 0, + step: 'preparing', + status: 'Preparing', }; - const updateProgress = _.isFunction(options.progress) ? - 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) { + if (err) { return callback(err); } const templateFiles = [ - { name : options.headerTemplate, req : false }, - { name : options.entryTemplate, req : true } + { name: options.headerTemplate, req: false }, + { name: options.entryTemplate, req: true }, ]; const config = Config(); - async.map(templateFiles, (template, nextTemplate) => { - if(!template.name && !template.req) { - return nextTemplate(null, Buffer.from([])); - } + 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)); - } - - // 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 + 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)); + } - return callback(null, templates[0], templates[1], descIndent); - }); + // 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 + }); + } + + return callback(null, templates[0], templates[1], descIndent); + } + ); }); }, function findFiles(headerTemplate, entryTemplate, descIndent, callback) { - state.step = 'gathering'; - state.status = 'Gathering files for supplied criteria'; + state.step = 'gathering'; + state.status = 'Gathering files for supplied criteria'; updateProgress(err => { - if(err) { + if (err) { return callback(err); } FileEntry.findFiles(filterCriteria, (err, fileIds) => { - if(0 === fileIds.length) { - return callback(Errors.General('No results for criteria', 'NORESULTS')); + if (0 === fileIds.length) { + return callback( + Errors.General('No results for criteria', 'NORESULTS') + ); } - return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); + return callback( + err, + headerTemplate, + entryTemplate, + descIndent, + fileIds + ); }); }); }, - function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { + function buildListEntries( + headerTemplate, + entryTemplate, + descIndent, + fileIds, + callback + ) { const formatObj = { - totalFileCount : fileIds.length, + totalFileCount: fileIds.length, }; - let current = 0; - let listBody = ''; - const totals = { fileCount : fileIds.length, bytes : 0 }; - state.total = fileIds.length; + let current = 0; + let listBody = ''; + const totals = { fileCount: fileIds.length, bytes: 0 }; + state.total = fileIds.length; - state.step = 'file'; + state.step = 'file'; - async.eachSeries(fileIds, (fileId, nextFileId) => { - const fileInfo = new FileEntry(); - 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 - } - - totals.bytes += fileInfo.meta.byte_size; - - const appendFileInfo = () => { - if(options.escapeDesc) { - formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc); + fileInfo.load(fileId, err => { + if (err) { + return nextFileId(null); // failed, but try the next } - if(options.maxDescLen) { - formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen); - } + totals.bytes += fileInfo.meta.byte_size; - listBody += stringFormat(entryTemplate, formatObj); - - state.current = current; - state.status = `Processing ${fileInfo.fileName}`; - state.fileInfo = formatObj; - - updateProgress(err => { - return nextFileId(err); - }); - }; - - const area = FileArea.getFileAreaByTag(fileInfo.areaTag); - - formatObj.fileId = fileId; - formatObj.areaName = _.get(area, 'name') || 'N/A'; - formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; - formatObj.userRating = fileInfo.userRating || 0; - formatObj.fileName = fileInfo.fileName; - formatObj.fileSize = fileInfo.meta.byte_size; - formatObj.fileDesc = fileInfo.desc || ''; - formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); - formatObj.fileSha256 = fileInfo.fileSha256; - formatObj.fileCrc32 = fileInfo.meta.file_crc32; - formatObj.fileMd5 = fileInfo.meta.file_md5; - formatObj.fileSha1 = fileInfo.meta.file_sha1; - formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; - formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); - formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A'; - formatObj.currentFile = current; - formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); - - if(isAnsi(fileInfo.desc)) { - AnsiPrep( - fileInfo.desc, - { - cols : Math.min(options.descWidth, 79 - descIndent), - forceLineTerm : true, // ensure each line is term'd - asciiMode : true, // export to ASCII - fillLines : false, // don't fill up to |cols| - indent : descIndent, - }, - (err, desc) => { - if(desc) { - formatObj.fileDesc = desc; - } - return appendFileInfo(); + const appendFileInfo = () => { + if (options.escapeDesc) { + formatObj.fileDesc = formatObj.fileDesc.replace( + /\r?\n/g, + options.escapeDesc + ); } + + if (options.maxDescLen) { + formatObj.fileDesc = formatObj.fileDesc.slice( + 0, + options.maxDescLen + ); + } + + listBody += stringFormat(entryTemplate, formatObj); + + state.current = current; + state.status = `Processing ${fileInfo.fileName}`; + state.fileInfo = formatObj; + + updateProgress(err => { + return nextFileId(err); + }); + }; + + const area = FileArea.getFileAreaByTag(fileInfo.areaTag); + + formatObj.fileId = fileId; + formatObj.areaName = _.get(area, 'name') || 'N/A'; + formatObj.areaDesc = _.get(area, 'desc') || 'N/A'; + formatObj.userRating = fileInfo.userRating || 0; + formatObj.fileName = fileInfo.fileName; + formatObj.fileSize = fileInfo.meta.byte_size; + formatObj.fileDesc = fileInfo.desc || ''; + formatObj.fileDescShort = formatObj.fileDesc.slice( + 0, + options.descWidth ); - } 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); - }); + formatObj.fileSha256 = fileInfo.fileSha256; + formatObj.fileCrc32 = fileInfo.meta.file_crc32; + formatObj.fileMd5 = fileInfo.meta.file_md5; + formatObj.fileSha1 = fileInfo.meta.file_sha1; + formatObj.uploadBy = + fileInfo.meta.upload_by_username || 'N/A'; + formatObj.fileUploadTs = moment( + fileInfo.uploadTimestamp + ).format(options.tsFormat); + formatObj.fileHashTags = + fileInfo.hashTags.size > 0 + ? Array.from(fileInfo.hashTags).join(', ') + : 'N/A'; + formatObj.currentFile = current; + formatObj.progress = Math.floor( + (current / fileIds.length) * 100 + ); + + if (isAnsi(fileInfo.desc)) { + AnsiPrep( + fileInfo.desc, + { + cols: Math.min( + options.descWidth, + 79 - descIndent + ), + forceLineTerm: true, // ensure each line is term'd + asciiMode: true, // export to ASCII + fillLines: false, // don't fill up to |cols| + indent: descIndent, + }, + (err, desc) => { + if (desc) { + formatObj.fileDesc = desc; + } + return appendFileInfo(); + } + ); + } else { + const indentSpc = + descIndent > 0 ? ' '.repeat(descIndent) : ''; + formatObj.fileDesc = + splitTextAtTerms(formatObj.fileDesc).join( + `\r\n${indentSpc}` + ) + '\r\n'; + return appendFileInfo(); + } + }); + }, + err => { + return callback(err, listBody, headerTemplate, totals); + } + ); }, function buildHeader(listBody, headerTemplate, totals, callback) { // header is built last such that we can have totals/etc. let filterAreaName; let filterAreaDesc; - if(filterCriteria.areaTag) { - const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); - filterAreaName = _.get(area, 'name') || 'N/A'; - filterAreaDesc = _.get(area, 'desc') || 'N/A'; + 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'; + filterAreaName = '-ALL-'; + filterAreaDesc = 'All areas'; } const headerFormatObj = { - nowTs : moment().format(options.tsFormat), - boardName : Config().general.boardName, - totalFileCount : totals.fileCount, - totalFileSize : totals.bytes, - filterAreaTag : filterCriteria.areaTag || '-ALL-', - filterAreaName : filterAreaName, - filterAreaDesc : filterAreaDesc, - filterTerms : filterCriteria.terms || '(none)', - filterHashTags : filterCriteria.tags || '(none)', + nowTs: moment().format(options.tsFormat), + boardName: Config().general.boardName, + totalFileCount: totals.fileCount, + totalFileSize: totals.bytes, + filterAreaTag: filterCriteria.areaTag || '-ALL-', + filterAreaName: filterAreaName, + filterAreaDesc: filterAreaDesc, + filterTerms: filterCriteria.terms || '(none)', + filterHashTags: filterCriteria.tags || '(none)', }; listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; @@ -238,13 +286,14 @@ function exportFileList(filterCriteria, options, cb) { }, function done(listBody, callback) { delete state.fileInfo; - state.step = 'finished'; - state.status = 'Finished processing'; - updateProgress( () => { + state.step = 'finished'; + state.status = 'Finished processing'; + updateProgress(() => { return callback(null, listBody); }); - } - ], (err, listBody) => { + }, + ], + (err, listBody) => { return cb(err, listBody); } ); @@ -260,42 +309,59 @@ function updateFileBaseDescFilesScheduledEvent(args, cb) { // * Multi line descriptions are stored with *escaped* \r\n pairs // * Default template uses 0x2c for as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec // - const entryTemplate = args[0]; - const headerTemplate = args[1]; + const entryTemplate = args[0]; + const headerTemplate = args[1]; - const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true }); - async.each(areas, (area, nextArea) => { - 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) => { - - 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); - }); + exportFileList(filterCriteria, exportOpts, (err, listBody) => { + const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION'); + fs.writeFile( + descIonPath, + iconv.encode(listBody, 'cp437'), + err => { + if (err) { + Log.warn( + { error: err.message, path: descIonPath }, + 'Failed (re)creating DESCRIPT.ION' + ); + } else { + Log.debug( + { path: descIonPath }, + '(Re)generated DESCRIPT.ION' + ); + } + return nextStorageLoc(null); + } + ); + }); + }, + () => { + return nextArea(null); + } + ); + }, + () => { + return cb(null); + } + ); } diff --git a/core/file_base_search.js b/core/file_base_search.js index 168ed39a..13e109fb 100644 --- a/core/file_base_search.js +++ b/core/file_base_search.js @@ -2,30 +2,31 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; -const FileBaseFilters = require('./file_base_filter.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const getSortedAvailableFileAreas = + require('./file_base_area.js').getSortedAvailableFileAreas; +const FileBaseFilters = require('./file_base_filter.js'); // deps -const async = require('async'); +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 { @@ -33,7 +34,7 @@ exports.getModule = class FileBaseSearch extends MenuModule { super(options); this.menuMethods = { - search : (formData, extraArgs, cb) => { + search: (formData, extraArgs, cb) => { const isAdvanced = formData.submitId === MciViewIds.search.advSearch; return this.searchNow(formData, isAdvanced, cb); }, @@ -42,28 +43,36 @@ exports.getModule = class FileBaseSearch extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(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); + return vc.loadFromMenuConfig( + { callingMenu: self, mciMap: mciData.menu }, + callback + ); }, function populateAreas(callback) { - self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); + self.availAreas = [{ name: '-ALL-' }].concat( + getSortedAvailableFileAreas(self.client) || [] + ); const areasView = vc.getView(MciViewIds.search.area); - areasView.setItems( self.availAreas.map( a => a.name ) ); + areasView.setItems(self.availAreas.map(a => a.name)); areasView.redraw(); vc.switchFocus(MciViewIds.search.searchTerms); return callback(null); - } + }, ], err => { return cb(err); @@ -73,11 +82,11 @@ exports.getModule = class FileBaseSearch extends MenuModule { } getSelectedAreaTag(index) { - if(0 === index) { - return ''; // -ALL- + if (0 === index) { + return ''; // -ALL- } const area = this.availAreas[index]; - if(!area) { + if (!area) { return ''; } return area.areaTag; @@ -92,16 +101,16 @@ exports.getModule = class FileBaseSearch extends MenuModule { } getFilterValuesFromFormData(formData, isAdvanced) { - const areaIndex = isAdvanced ? formData.value.areaIndex : 0; - const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; - const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; + const areaIndex = isAdvanced ? formData.value.areaIndex : 0; + const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; + const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; return { - areaTag : this.getSelectedAreaTag(areaIndex), - terms : formData.value.searchTerms, - tags : isAdvanced ? formData.value.tags : '', - order : this.getOrderBy(orderByIndex), - sort : this.getSortBy(sortByIndex), + areaTag: this.getSelectedAreaTag(areaIndex), + terms: formData.value.searchTerms, + tags: isAdvanced ? formData.value.tags : '', + order: this.getOrderBy(orderByIndex), + sort: this.getSortBy(sortByIndex), }; } @@ -109,12 +118,16 @@ exports.getModule = class FileBaseSearch extends MenuModule { const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced); const menuOpts = { - extraArgs : { - filterCriteria : filterCriteria, + extraArgs: { + filterCriteria: filterCriteria, }, - menuFlags : [ 'popParent' ], + menuFlags: ['popParent'], }; - return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); + 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 c3f3b6f8..57b28ac2 100644 --- a/core/file_base_user_list_export.js +++ b/core/file_base_user_list_export.js @@ -2,23 +2,23 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const FileEntry = require('./file_entry.js'); -const FileArea = require('./file_base_area.js'); -const { renderSubstr } = require('./string_util.js'); -const { Errors } = require('./enig_error.js'); -const DownloadQueue = require('./download_queue.js'); -const { exportFileList } = require('./file_base_list_export.js'); +const { MenuModule } = require('./menu_module.js'); +const FileEntry = require('./file_entry.js'); +const FileArea = require('./file_base_area.js'); +const { renderSubstr } = require('./string_util.js'); +const { Errors } = require('./enig_error.js'); +const DownloadQueue = require('./download_queue.js'); +const { exportFileList } = require('./file_base_list_export.js'); // deps -const _ = require('lodash'); -const async = require('async'); -const fs = require('graceful-fs'); -const fse = require('fs-extra'); -const paths = require('path'); -const moment = require('moment'); -const { v4 : UUIDv4 } = require('uuid'); -const yazl = require('yazl'); +const _ = require('lodash'); +const async = require('async'); +const fs = require('graceful-fs'); +const fse = require('fs-extra'); +const paths = require('path'); +const moment = require('moment'); +const { v4: UUIDv4 } = require('uuid'); +const yazl = require('yazl'); /* Module config block can contain the following: @@ -44,52 +44,66 @@ 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); + 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) { + if (err) { return cb(err); } async.series( [ - (callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), - (callback) => this.prepareList(callback), + 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'); + if (err) { + if ('NORESULTS' === err.reasonCode) { + return this.gotoMenu( + this.menuConfig.config.noResultsMenu || + 'fileBaseExportListNoResults' + ); } return this.prevMenu(); @@ -108,16 +122,18 @@ exports.getModule = class FileBaseListExport extends MenuModule { const self = this; const statusView = self.viewControllers.main.getView(MciViewIds.main.status); - const updateStatus = (status) => { - if(statusView) { + const updateStatus = status => { + if (statusView) { statusView.setText(status); } }; - const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); + const progBarView = self.viewControllers.main.getView( + MciViewIds.main.progressBar + ); const updateProgressBar = (curr, total) => { - if(progBarView) { - const prog = Math.floor( (curr / total) * progBarView.dimens.width ); + if (progBarView) { + const prog = Math.floor((curr / total) * progBarView.dimens.width); progBarView.setText(self.config.progBarChar.repeat(prog)); } }; @@ -125,17 +141,21 @@ exports.getModule = class FileBaseListExport extends MenuModule { let cancel = false; const exportListProgress = (state, progNext) => { - switch(state.step) { - case 'preparing' : - case 'gathering' : + switch (state.step) { + case 'preparing': + case 'gathering': updateStatus(state.status); break; - case 'file' : + case 'file': updateStatus(state.status); updateProgressBar(state.current, state.total); - self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo); + self.updateCustomViewTextsWithFilter( + 'main', + MciViewIds.main.customRangeStart, + state.fileInfo + ); break; - default : + default: break; } @@ -143,7 +163,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { }; const keyPressHandler = (ch, key) => { - if('escape' === key.name) { + if ('escape' === key.name) { cancel = true; self.client.removeListener('key press', keyPressHandler); } @@ -158,17 +178,27 @@ exports.getModule = class FileBaseListExport extends MenuModule { self.client.on('key press', keyPressHandler); const filterCriteria = Object.assign({}, self.config.filterCriteria); - if(!filterCriteria.areaTag) { - filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); + 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, + 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) => { @@ -178,47 +208,65 @@ exports.getModule = class FileBaseListExport extends MenuModule { function persistList(listBody, callback) { updateStatus('Persisting list'); - const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); - const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); + const sysTempDownloadArea = FileArea.getFileAreaByTag( + FileArea.WellKnownAreaTags.TempDownloads + ); + const sysTempDownloadDir = + FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); fse.mkdirs(sysTempDownloadDir, err => { - if(err) { + if (err) { return callback(err); } const outputFileName = paths.join( sysTempDownloadDir, - `file_list_${UUIDv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` + `file_list_${UUIDv4().substr(-8)}_${moment().format( + 'YYYY-MM-DD' + )}.txt` ); fs.writeFile(outputFileName, listBody, 'utf8', err => { - if(err) { + if (err) { return callback(err); } - self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { - return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); - }); + self.getSizeAndCompressIfMeetsSizeThreshold( + outputFileName, + (err, finalOutputFileName, fileSize) => { + return callback( + err, + finalOutputFileName, + fileSize, + sysTempDownloadArea + ); + } + ); }); }); }, - function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { + function persistFileEntry( + outputFileName, + fileSize, + sysTempDownloadArea, + callback + ) { const newEntry = new FileEntry({ - areaTag : sysTempDownloadArea.areaTag, - fileName : paths.basename(outputFileName), - storageTag : sysTempDownloadArea.storageTags[0], - meta : { - upload_by_username : self.client.user.username, - upload_by_user_id : self.client.user.userId, - byte_size : fileSize, - session_temp_dl : 1, // download is valid until session is over - } + areaTag: sysTempDownloadArea.areaTag, + fileName: paths.basename(outputFileName), + storageTag: sysTempDownloadArea.storageTags[0], + meta: { + upload_by_username: self.client.user.username, + upload_by_user_id: self.client.user.userId, + byte_size: fileSize, + session_temp_dl: 1, // download is valid until session is over + }, }); newEntry.desc = 'File List Export'; newEntry.persist(err => { - if(!err) { + if (!err) { // queue it! DownloadQueue.get(self.client).addTemporaryDownload(newEntry); } @@ -232,7 +280,7 @@ exports.getModule = class FileBaseListExport extends MenuModule { updateStatus('Exported list has been added to your download queue'); return callback(null); - } + }, ], err => { self.client.removeListener('key press', keyPressHandler); @@ -243,11 +291,11 @@ exports.getModule = class FileBaseListExport extends MenuModule { getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { fse.stat(filePath, (err, stats) => { - if(err) { + if (err) { return cb(err); } - if(stats.size < this.config.compressThreshold) { + if (stats.size < this.config.compressThreshold) { // small enough, keep orig return cb(null, filePath, stats.size); } @@ -256,13 +304,13 @@ exports.getModule = class FileBaseListExport extends MenuModule { const zipFile = new yazl.ZipFile(); zipFile.addFile(filePath, paths.basename(filePath)); - zipFile.end( () => { + zipFile.end(() => { const outZipFile = fs.createWriteStream(zipFilePath); zipFile.outputStream.pipe(outZipFile); zipFile.outputStream.on('finish', () => { // delete the original fse.unlink(filePath, err => { - if(err) { + if (err) { return cb(err); } @@ -275,4 +323,4 @@ exports.getModule = class FileBaseListExport extends MenuModule { }); }); } -}; \ 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 cf509cd9..61ff1871 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -2,74 +2,79 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const DownloadQueue = require('./download_queue.js'); -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const Errors = require('./enig_error.js').Errors; -const FileAreaWeb = require('./file_area_web.js'); -const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; -const Config = require('./config.js').get; +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const DownloadQueue = require('./download_queue.js'); +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const Errors = require('./enig_error.js').Errors; +const FileAreaWeb = require('./file_area_web.js'); +const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; +const Config = require('./config.js').get; // deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { - name : 'File Base Download Web Queue Manager', - desc : 'Module for interacting with web backed download queue/batch', - author : 'NuSkooler', + name: 'File Base Download Web Queue Manager', + desc: 'Module for interacting with web backed download queue/batch', + author: 'NuSkooler', }; const FormIds = { - queueManager : 0 + queueManager: 0, }; const MciViewIds = { - queueManager : { - queue : 1, - navMenu : 2, + queueManager: { + queue: 1, + navMenu: 2, - customRangeStart : 10, - } + customRangeStart: 10, + }, }; exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { - constructor(options) { super(options); this.dlQueue = new DownloadQueue(this.client); this.menuMethods = { - removeItem : (formData, extraArgs, cb) => { + removeItem: (formData, extraArgs, cb) => { const selectedItem = this.dlQueue.items[formData.value.queueItem]; - if(!selectedItem) { + if (!selectedItem) { return cb(null); } this.dlQueue.removeItems(selectedItem.fileId); // :TODO: broken: does not redraw menu properly - needs fixed! - return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); + return this.removeItemsFromDownloadQueueView( + formData.value.queueItem, + cb + ); }, - clearQueue : (formData, extraArgs, 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) => { + getBatchLink: (formData, extraArgs, cb) => { return this.generateAndDisplayBatchLink(cb); - } + }, }; } initSequence() { - if(0 === this.dlQueue.items.length) { - return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); + if (0 === this.dlQueue.items.length) { + return this.gotoMenu( + this.menuConfig.config.emptyQueueMenu || + 'fileBaseDownloadManagerEmptyQueue' + ); } const self = this; @@ -81,7 +86,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { }, function display(callback) { return self.displayQueueManagerPage(false, callback); - } + }, ], () => { return self.finishedLoading(); @@ -90,12 +95,14 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { } removeItemsFromDownloadQueueView(itemIndex, cb) { - const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); - if(!queueView) { + const queueView = this.viewControllers.queueManager.getView( + MciViewIds.queueManager.queue + ); + if (!queueView) { return cb(Errors.DoesNotExist('Queue view does not exist')); } - if('all' === itemIndex) { + if ('all' === itemIndex) { queueView.setItems([]); queueView.setFocusItems([]); } else { @@ -109,14 +116,17 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { displayFileInfoForFileEntry(fileEntry) { this.updateCustomViewTextsWithFilter( 'queueManager', - MciViewIds.queueManager.customRangeStart, fileEntry, - { filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... + MciViewIds.queueManager.customRangeStart, + fileEntry, + { filter: ['{webDlLink}', '{webDlExpire}', '{fileName}'] } // :TODO: Others.... ); } updateDownloadQueueView(cb) { - const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); - if(!queueView) { + const queueView = this.viewControllers.queueManager.getView( + MciViewIds.queueManager.queue + ); + if (!queueView) { return cb(Errors.DoesNotExist('Queue view does not exist')); } @@ -140,26 +150,28 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { this.client, this.dlQueue.items, { - expireTime : expireTime + expireTime: expireTime, }, (err, webBatchDlLink) => { // :TODO: handle not enabled -> display such - if(err) { + 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), + 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 + '}' ) } + { filter: Object.keys(formatObj).map(k => '{' + k + '}') } ); return cb(null); @@ -173,54 +185,82 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { async.series( [ function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); + return self.displayArtAndPrepViewController( + 'queueManager', + { clearScreen: clearScreen }, + callback + ); }, function prepareQueueDownloadLinks(callback) { - const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; + 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 expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); - - FileAreaWeb.createAndServeTempDownload( - self.client, - fileEntry, - { expireTime : expireTime }, - (err, url) => { - if(err) { - return nextFileEntry(err); + 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 } - fileEntry.webDlLinkRaw = url; - fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; - fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); + const expireTime = moment().add( + config.fileBase.web.expireMinutes, + 'minutes' + ); + 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 + ); + + 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); } - ); - } 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); - }); + } + ); + }, + err => { + return callback(err); + } + ); }, function populateViews(callback) { return self.updateDownloadQueueView(callback); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -228,42 +268,45 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { } displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; + const self = this; + const config = this.menuConfig.config; async.waterfall( [ function readyAndDisplayArt(callback) { - if(options.clearScreen) { + if (options.clearScreen) { self.client.term.rawWrite(ansi.resetScreen()); } theme.displayThemedAsset( config.art[name], self.client, - { font : self.menuConfig.font, trailingLF : false }, + { font: self.menuConfig.font, trailingLF: false }, (err, artData) => { return callback(err, artData); } ); }, function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { + if (_.isUndefined(self.viewControllers[name])) { const vcOpts = { - client : self.client, - formId : FormIds[name], + client: self.client, + formId: FormIds[name], }; - if(!_.isUndefined(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], + callingMenu: self, + mciMap: artData.mciMap, + formId: FormIds[name], }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -271,7 +314,6 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { self.viewControllers[name].setFocus(true); return callback(null); - }, ], err => { diff --git a/core/file_entry.js b/core/file_entry.js index 476864ca..55cc6146 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -1,57 +1,60 @@ /* jslint node: true */ 'use strict'; -const fileDb = require('./database.js').dbs.file; -const Errors = require('./enig_error.js').Errors; -const { - getISOTimestampString, - sanitizeString -} = require('./database.js'); -const Config = require('./config.js').get; +const fileDb = require('./database.js').dbs.file; +const Errors = require('./enig_error.js').Errors; +const { getISOTimestampString, sanitizeString } = require('./database.js'); +const Config = require('./config.js').get; // deps -const async = require('async'); -const _ = require('lodash'); -const paths = require('path'); -const fse = require('fs-extra'); -const { unlink, readFile } = require('graceful-fs'); -const crypto = require('crypto'); -const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); +const paths = require('path'); +const fse = require('fs-extra'); +const { unlink, readFile } = require('graceful-fs'); +const crypto = require('crypto'); +const moment = require('moment'); -const FILE_TABLE_MEMBERS = [ - 'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', - 'desc', 'desc_long', 'upload_timestamp' +const FILE_TABLE_MEMBERS = [ + 'file_id', + 'area_tag', + 'file_sha256', + 'file_name', + 'storage_tag', + 'desc', + 'desc_long', + 'upload_timestamp', ]; const FILE_WELL_KNOWN_META = { // name -> *read* converter, if any - upload_by_username : null, - upload_by_user_id : (u) => parseInt(u) || 0, - file_md5 : null, - file_sha1 : null, - file_crc32 : null, - est_release_year : (y) => parseInt(y) || new Date().getFullYear(), - dl_count : (d) => parseInt(d) || 0, - byte_size : (b) => parseInt(b) || 0, - archive_type : null, - short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import - tic_origin : null, // TIC "Origin" - tic_desc : null, // TIC "Desc" - tic_ldesc : null, // TIC "Ldesc" joined by '\n' - session_temp_dl : (v) => parseInt(v) ? true : false, - desc_sauce : (s) => JSON.parse(s) || {}, - desc_long_sauce : (s) => JSON.parse(s) || {}, + 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), + desc_sauce: s => JSON.parse(s) || {}, + desc_long_sauce: s => JSON.parse(s) || {}, }; module.exports = class FileEntry { constructor(options) { - options = options || {}; + options = options || {}; - this.fileId = options.fileId || 0; - this.areaTag = options.areaTag || ''; - this.meta = Object.assign( { dl_count : 0 }, options.meta); - this.hashTags = options.hashTags || new Set(); - this.fileName = options.fileName; + this.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; } @@ -64,13 +67,13 @@ module.exports = class FileEntry { FROM file WHERE file_id=? LIMIT 1;`, - [ fileId ], + [fileId], (err, file) => { - if(err) { + if (err) { return cb(err); } - if(!file) { + if (!file) { return cb(Errors.DoesNotExist('No file is available by that ID')); } @@ -100,7 +103,7 @@ module.exports = class FileEntry { }, function loadUserRating(callback) { return self.loadRating(callback); - } + }, ], err => { return cb(err); @@ -109,7 +112,7 @@ module.exports = class FileEntry { } persist(isUpdate, cb) { - if(!cb && _.isFunction(isUpdate)) { + if (!cb && _.isFunction(isUpdate)) { cb = isUpdate; isUpdate = false; } @@ -119,22 +122,30 @@ module.exports = class FileEntry { async.waterfall( [ function check(callback) { - if(isUpdate && !self.fileId) { - return callback(Errors.Invalid('Cannot update file entry without an existing "fileId" member')); + 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) { + 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) { + if (err) { return callback(err); } @@ -148,11 +159,20 @@ module.exports = class FileEntry { return fileDb.beginTransaction(callback); }, function storeEntry(trans, callback) { - if(isUpdate) { + 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() ], + [ + self.fileId, + self.areaTag, + self.fileSha256, + self.fileName, + self.storageTag, + self.desc, + self.descLong, + getISOTimestampString(), + ], err => { return callback(err, trans); } @@ -161,9 +181,18 @@ module.exports = class FileEntry { 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.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); @@ -172,27 +201,44 @@ module.exports = class FileEntry { } }, 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); - }); + 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); - }); - } + 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) { + if (trans) { trans[err ? 'rollback' : 'commit'](transErr => { return cb(transErr ? transErr : err); }); @@ -205,10 +251,10 @@ module.exports = class FileEntry { static getAreaStorageDirectoryByTag(storageTag) { const config = Config(); - const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); + const storageLocation = storageTag && config.fileBase.storageTags[storageTag]; // absolute paths as-is - if(storageLocation && '/' === storageLocation.charAt(0)) { + if (storageLocation && '/' === storageLocation.charAt(0)) { return storageLocation; } @@ -227,7 +273,7 @@ module.exports = class FileEntry { FROM file WHERE file_name = ? LIMIT 1;`, - [ paths.basename(fullPath) ], + [paths.basename(fullPath)], (err, rows) => { return err ? cb(err) : cb(null, rows.count > 0 ? true : false); } @@ -238,7 +284,7 @@ module.exports = class FileEntry { return fileDb.run( `REPLACE INTO file_user_rating (file_id, user_id, rating) VALUES (?, ?, ?);`, - [ fileId, userId, rating ], + [fileId, userId, rating], cb ); } @@ -247,13 +293,13 @@ module.exports = class FileEntry { return fileDb.run( `DELETE FROM file_user_rating WHERE user_id = ?;`, - [ userId ], + [userId], cb ); } static persistMetaValue(fileId, name, value, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + if (!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; transOrDb = fileDb; } @@ -261,7 +307,7 @@ module.exports = class FileEntry { return transOrDb.run( `REPLACE INTO file_meta (file_id, meta_name, meta_value) VALUES (?, ?, ?);`, - [ fileId, name, value ], + [fileId, name, value], cb ); } @@ -272,9 +318,9 @@ module.exports = class FileEntry { `UPDATE file_meta SET meta_value = meta_value + ? WHERE file_id = ? AND meta_name = ?;`, - [ incrementBy, fileId, name ], + [incrementBy, fileId, name], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -286,11 +332,13 @@ module.exports = class FileEntry { `SELECT meta_name, meta_value FROM file_meta WHERE file_id=?;`, - [ this.fileId ], + [this.fileId], (err, meta) => { - if(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; + this.meta[meta.meta_name] = conv + ? conv(meta.meta_value) + : meta.meta_value; } }, err => { @@ -300,16 +348,16 @@ module.exports = class FileEntry { } static persistHashTag(fileId, hashTag, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + if (!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; transOrDb = fileDb; } - transOrDb.serialize( () => { + transOrDb.serialize(() => { transOrDb.run( `INSERT OR IGNORE INTO hash_tag (hash_tag) VALUES (?);`, - [ hashTag ] + [hashTag] ); transOrDb.run( @@ -320,7 +368,7 @@ module.exports = class FileEntry { WHERE hash_tag = ?), ? );`, - [ hashTag, fileId ], + [hashTag, fileId], err => { return cb(err); } @@ -337,9 +385,9 @@ module.exports = class FileEntry { FROM file_hash_tag WHERE file_id=? );`, - [ this.fileId ], + [this.fileId], (err, hashTag) => { - if(hashTag) { + if (hashTag) { this.hashTags.add(hashTag.hash_tag); } }, @@ -356,9 +404,9 @@ module.exports = class FileEntry { INNER JOIN file f ON f.file_id = fur.file_id AND f.file_id = ?`, - [ this.fileId ], + [this.fileId], (err, result) => { - if(result) { + if (result) { this.userRating = result.avg_rating; } return cb(err); @@ -367,16 +415,16 @@ module.exports = class FileEntry { } setHashTags(hashTags) { - if(_.isString(hashTags)) { + if (_.isString(hashTags)) { this.hashTags = new Set(hashTags.split(/[\s,]+/)); - } else if(Array.isArray(hashTags)) { + } else if (Array.isArray(hashTags)) { this.hashTags = new Set(hashTags); - } else if(hashTags instanceof Set) { + } else if (hashTags instanceof Set) { this.hashTags = hashTags; } } - static get WellKnownMetaValues() { + static get WellKnownMetaValues() { return Object.keys(FILE_WELL_KNOWN_META); } @@ -386,17 +434,17 @@ module.exports = class FileEntry { `SELECT file_id FROM file WHERE file_sha256 LIKE "${sha}%" - LIMIT 2;`, // limit 2 such that we can find if there are dupes + LIMIT 2;`, // limit 2 such that we can find if there are dupes (err, fileIdRows) => { - if(err) { + if (err) { return cb(err); } - if(!fileIdRows || 0 === fileIdRows.length) { + if (!fileIdRows || 0 === fileIdRows.length) { return cb(Errors.DoesNotExist('No matches')); } - if(fileIdRows.length > 1) { + if (fileIdRows.length > 1) { return cb(Errors.Invalid('SHA is ambiguous')); } @@ -413,17 +461,17 @@ module.exports = class FileEntry { static findByFullPath(fullPath, cb) { // first, basic by-filename lookup. FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => { - if(err) { + if (err) { return cb(err); } - if(!entries || !entries.length || entries.length > 1) { + if (!entries || !entries.length || entries.length > 1) { return cb(Errors.DoesNotExist('No matches')); } // ensure the *full* path has not changed // :TODO: if FS is case-insensitive, we probably want a better check here const possibleMatch = entries[0]; - if(possibleMatch.fullPath === fullPath) { + if (possibleMatch.fullPath === fullPath) { return cb(null, possibleMatch); } @@ -441,27 +489,30 @@ module.exports = class FileEntry { WHERE file_name LIKE "${wc}" `, (err, fileIdRows) => { - if(err) { + if (err) { return cb(err); } - if(!fileIdRows || 0 === fileIdRows.length) { + 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); - }); + 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); + } + ); } ); } @@ -484,12 +535,12 @@ module.exports = class FileEntry { let sqlOrderBy; const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; - if(moment.isMoment(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 ) { + if (['dl_count', 'est_release_year', 'byte_size'].indexOf(filter.sort) > -1) { return `ORDER BY CAST(${ob} AS INTEGER)`; } @@ -497,7 +548,7 @@ module.exports = class FileEntry { } function appendWhereClause(clause) { - if(sqlWhere) { + if (sqlWhere) { sqlWhere += ' AND '; } else { sqlWhere += ' WHERE '; @@ -505,20 +556,21 @@ module.exports = class FileEntry { 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 = - `SELECT DISTINCT f.file_id + 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 = - `SELECT DISTINCT f.file_id, + if ('user_rating' === filter.sort) { + sql = `SELECT DISTINCT f.file_id, (SELECT IFNULL(AVG(rating), 0) rating FROM file_user_rating WHERE file_id = f.file_id) @@ -527,23 +579,22 @@ module.exports = class FileEntry { sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; } else { - sql = - `SELECT DISTINCT f.file_id + sql = `SELECT DISTINCT f.file_id FROM file f`; - sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; + sqlOrderBy = + getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; } } } else { - sql = - `SELECT DISTINCT f.file_id + sql = `SELECT DISTINCT f.file_id FROM file f`; sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; } - if(filter.areaTag && filter.areaTag.length > 0) { - if(Array.isArray(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 { @@ -551,10 +602,9 @@ module.exports = class FileEntry { } } - if(filter.metaPairs && filter.metaPairs.length > 0) { - + if (filter.metaPairs && filter.metaPairs.length > 0) { filter.metaPairs.forEach(mp => { - if(mp.wildcards) { + 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( @@ -576,11 +626,11 @@ module.exports = class FileEntry { }); } - if(filter.storageTag && filter.storageTag.length > 0) { + if (filter.storageTag && filter.storageTag.length > 0) { appendWhereClause(`f.storage_tag="${filter.storageTag}"`); } - if(filter.terms && filter.terms.length > 0) { + if (filter.terms && filter.terms.length > 0) { const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms); if ('fts_match' === queryType) { @@ -606,9 +656,14 @@ module.exports = class FileEntry { filter.tags = filter.tags.toString(); } - if(filter.tags && filter.tags.length > 0) { + 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 => `"${sanitizeString(tag)}"` ).join(','); + const tags = filter.tags + .replace(/,/g, ' ') + .replace(/\s{2,}/g, ' ') + .split(' ') + .map(tag => `"${sanitizeString(tag)}"`) + .join(','); appendWhereClause( `f.file_id IN ( @@ -623,35 +678,43 @@ module.exports = class FileEntry { ); } - 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)) { + if (_.isNumber(filter.newerThanFileId)) { appendWhereClause(`f.file_id > ${filter.newerThanFileId}`); } sql += `${sqlWhere} ${sqlOrderBy}`; - if(_.isNumber(filter.limit)) { + if (_.isNumber(filter.limit)) { sql += ` LIMIT ${filter.limit}`; } sql += ';'; fileDb.all(sql, (err, rows) => { - if(err) { + if (err) { return cb(err); } - if(!rows || 0 === rows.length) { - return cb(null, []); // no matches + if (!rows || 0 === rows.length) { + return cb(null, []); // no matches } - return cb(null, rows.map(r => r.file_id)); + return cb( + null, + rows.map(r => r.file_id) + ); }); } static removeEntry(srcFileEntry, options, cb) { - if(!_.isFunction(cb) && _.isFunction(options)) { + if (!_.isFunction(cb) && _.isFunction(options)) { cb = options; options = {}; } @@ -662,21 +725,21 @@ module.exports = class FileEntry { fileDb.run( `DELETE FROM file WHERE file_id = ?;`, - [ srcFileEntry.fileId ], + [srcFileEntry.fileId], err => { return callback(err); } ); }, function optionallyRemovePhysicalFile(callback) { - if(true !== options.removePhysFile) { + if (true !== options.removePhysFile) { return callback(null); } unlink(srcFileEntry.filePath, err => { return callback(err); }); - } + }, ], err => { return cb(err); @@ -685,25 +748,25 @@ module.exports = class FileEntry { } static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) { - if(!cb && _.isFunction(destFileName)) { + 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) { + 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 + if (srcPath === dstPath) { + return callback(null); // don't need to move file, but may change areas } fse.move(srcPath, dstPath, err => { @@ -715,12 +778,12 @@ module.exports = class FileEntry { `UPDATE file SET area_tag = ?, file_name = ?, storage_tag = ? WHERE file_id = ?;`, - [ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ], + [destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId], err => { return callback(err); } ); - } + }, ], err => { return cb(err); @@ -735,7 +798,7 @@ module.exports = class FileEntry { // No wildcards? const hasSingleCharWC = terms.indexOf('?') > -1; if (terms.indexOf('*') === -1 && !hasSingleCharWC) { - return [ terms, 'fts_match' ]; + return [terms, 'fts_match']; } const prepareLike = () => { @@ -746,7 +809,7 @@ module.exports = class FileEntry { // Any ? wildcards? if (hasSingleCharWC) { - return [ prepareLike(terms), 'like' ]; + return [prepareLike(terms), 'like']; } const split = terms.replace(/\s+/g, ' ').split(' '); @@ -764,9 +827,9 @@ module.exports = class FileEntry { }); if (useLike) { - return [ prepareLike(terms), 'like' ]; + return [prepareLike(terms), 'like']; } - return [ terms, 'fts_match' ]; + return [terms, 'fts_match']; } }; diff --git a/core/file_transfer.js b/core/file_transfer.js index 44ebdc11..631fa325 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -2,30 +2,30 @@ 'use strict'; // enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const Config = require('./config.js').get; -const stringFormat = require('./string_format.js'); -const Errors = require('./enig_error.js').Errors; -const DownloadQueue = require('./download_queue.js'); -const StatLog = require('./stat_log.js'); -const FileEntry = require('./file_entry.js'); -const Log = require('./logger.js').log; -const Events = require('./events.js'); -const UserProps = require('./user_property.js'); -const SysProps = require('./system_property.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const Config = require('./config.js').get; +const stringFormat = require('./string_format.js'); +const Errors = require('./enig_error.js').Errors; +const DownloadQueue = require('./download_queue.js'); +const StatLog = require('./stat_log.js'); +const FileEntry = require('./file_entry.js'); +const Log = require('./logger.js').log; +const Events = require('./events.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const pty = require('node-pty'); -const temptmp = require('temptmp').createTrackedSession('transfer_file'); -const paths = require('path'); -const fs = require('graceful-fs'); -const fse = require('fs-extra'); +const async = require('async'); +const _ = require('lodash'); +const pty = require('node-pty'); +const temptmp = require('temptmp').createTrackedSession('transfer_file'); +const paths = require('path'); +const fs = require('graceful-fs'); +const fse = require('fs-extra'); // some consts -const SYSTEM_EOL = require('os').EOL; -const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. +const SYSTEM_EOL = require('os').EOL; +const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. /* Notes @@ -44,9 +44,9 @@ 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 { @@ -59,56 +59,58 @@ exports.getModule = class TransferFileModule extends MenuModule { // Most options can be set via extraArgs or config block // const config = Config(); - if(options.extraArgs) { - if(options.extraArgs.protocol) { - this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol]; + if (options.extraArgs) { + if (options.extraArgs.protocol) { + this.protocolConfig = + config.fileTransferProtocols[options.extraArgs.protocol]; } - if(options.extraArgs.direction) { + if (options.extraArgs.direction) { this.direction = options.extraArgs.direction; } - if(options.extraArgs.sendQueue) { + if (options.extraArgs.sendQueue) { this.sendQueue = options.extraArgs.sendQueue; } - if(options.extraArgs.recvFileName) { + if (options.extraArgs.recvFileName) { this.recvFileName = options.extraArgs.recvFileName; } - if(options.extraArgs.recvDirectory) { + if (options.extraArgs.recvDirectory) { this.recvDirectory = options.extraArgs.recvDirectory; } } else { - if(this.config.protocol) { + if (this.config.protocol) { this.protocolConfig = config.fileTransferProtocols[this.config.protocol]; } - if(this.config.direction) { + if (this.config.direction) { this.direction = this.config.direction; } - if(this.config.sendQueue) { + if (this.config.sendQueue) { this.sendQueue = this.config.sendQueue; } - if(this.config.recvFileName) { + if (this.config.recvFileName) { this.recvFileName = this.config.recvFileName; } - if(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 }; + if (_.isString(item)) { + return { path: item }; } else { return item; } @@ -118,11 +120,11 @@ exports.getModule = class TransferFileModule extends MenuModule { } isSending() { - return ('send' === this.direction); + return 'send' === this.direction; } restorePipeAfterExternalProc() { - if(!this.pipeRestored) { + if (!this.pipeRestored) { this.pipeRestored = true; this.client.restoreDataHandler(); @@ -134,17 +136,22 @@ exports.getModule = class TransferFileModule extends MenuModule { // :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)' ); + 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)` ); + this.client.log.info( + { sentFiles: sentFiles }, + `Successfully sent ${sentFiles.length} file(s)` + ); } return cb(err); }); @@ -196,29 +203,32 @@ exports.getModule = class TransferFileModule extends MenuModule { // 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 dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); const dstFileSuffix = paths.basename(dst, dstFileExt); - let renameIndex = 0; - let movedOk = false; + let renameIndex = 0; + let movedOk = false; let tryDstPath; async.until( - (callback) => callback(null, movedOk), // until moved OK - (cb) => { - if(0 === renameIndex) { + callback => callback(null, movedOk), // until moved OK + cb => { + if (0 === renameIndex) { // try originally supplied path first tryDstPath = dst; } else { - tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); + tryDstPath = paths.join( + dstPath, + `${dstFileSuffix}(${renameIndex})${dstFileExt}` + ); } fse.move(src, tryDstPath, err => { - if(err) { - if('EEXIST' === err.code) { + if (err) { + if ('EEXIST' === err.code) { renameIndex += 1; - return cb(null); // keep trying + return cb(null); // keep trying } return cb(err); @@ -236,25 +246,27 @@ exports.getModule = class TransferFileModule extends MenuModule { recvFiles(cb) { this.executeExternalProtocolHandlerForRecv(err => { - if(err) { + if (err) { return cb(err); } this.recvFilePaths = []; - if(this.recvFileName) { + 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) { + 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); @@ -265,83 +277,96 @@ exports.getModule = class TransferFileModule extends MenuModule { // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already // fs.readdir(this.recvDirectory, (err, files) => { - if(err) { + if (err) { return cb(err); } // stat each to grab files only - async.each(files, (fileName, nextFile) => { - const recvFullPath = paths.join(this.recvDirectory, fileName); + 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)) { + if (path && paths.sep !== path.charAt(path.length - 1)) { path = path + paths.sep; } return path; } prepAndBuildSendArgs(filePaths, cb) { - const externalArgs = this.protocolConfig.external['sendArgs']; + const externalArgs = this.protocolConfig.external['sendArgs']; async.waterfall( [ function getTempFileListPath(callback) { - const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) ); - if(!hasFileList) { + 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 - } - - fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => { - if(err) { - return callback(err); + temptmp.open( + { prefix: TEMP_SUFFIX, suffix: '.txt' }, + (err, tempFileInfo) => { + if (err) { + return callback(err); // failed to create it } - fs.close(tempFileInfo.fd, err => { - return callback(err, tempFileInfo.path); + + fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => { + if (err) { + return callback(err); + } + fs.close(tempFileInfo.fd, err => { + return callback(err, tempFileInfo.path); + }); }); - }); - }); + } + ); }, 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 || '', - }); + return '{filePaths}' === arg + ? arg + : stringFormat(arg, { + fileListPath: tempFileListPath || '', + }); }); const filePathsPos = args.indexOf('{filePaths}'); - if(filePathsPos > -1) { + if (filePathsPos > -1) { // replace {filePaths} with 0:n individual entries in |args| - args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); + args.splice.apply(args, [filePathsPos, 1].concat(filePaths)); } return callback(null, args); - } + }, ], (err, args) => { return cb(err, args); @@ -350,47 +375,52 @@ exports.getModule = class TransferFileModule extends MenuModule { } prepAndBuildRecvArgs(cb) { - const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; - const externalArgs = this.protocolConfig.external[argsKey]; - const args = externalArgs.map(arg => stringFormat(arg, { - uploadDir : this.recvDirectory, - fileName : this.recvFileName || '', - })); + const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; + const externalArgs = this.protocolConfig.external[argsKey]; + const args = externalArgs.map(arg => + stringFormat(arg, { + uploadDir: this.recvDirectory, + fileName: this.recvFileName || '', + }) + ); return cb(null, args); } executeExternalProtocolHandler(args, cb) { - const external = this.protocolConfig.external; - const cmd = external[`${this.direction}Cmd`]; + const external = this.protocolConfig.external; + const cmd = external[`${this.direction}Cmd`]; // support for handlers that need IACs taken care of over Telnet/etc. - const processIACs = - external.processIACs || - external.escapeTelnet; // deprecated name + const processIACs = external.processIACs || external.escapeTelnet; // deprecated name // :TODO: we should only do this when over Telnet (or derived, such as WebSockets)? - const IAC = Buffer.from([255]); - const EscapedIAC = Buffer.from([255, 255]); + const IAC = Buffer.from([255]); + const EscapedIAC = Buffer.from([255, 255]); this.client.log.debug( - { cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, + { + 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! + 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); let dataHits = 0; const updateActivity = () => { - if (0 === (dataHits++ % 4)) { + if (0 === dataHits++ % 4) { this.client.explicitActivityTimeUpdate(); } }; @@ -399,7 +429,7 @@ exports.getModule = class TransferFileModule extends MenuModule { updateActivity(); // needed for things like sz/rz - if(processIACs) { + if (processIACs) { let iacPos = data.indexOf(EscapedIAC); if (-1 === iacPos) { return externalProc.write(data); @@ -430,7 +460,7 @@ exports.getModule = class TransferFileModule extends MenuModule { updateActivity(); // needed for things like sz/rz - if(processIACs) { + if (processIACs) { let iacPos = data.indexOf(IAC); if (-1 === iacPos) { return this.client.term.rawWrite(data); @@ -459,23 +489,33 @@ exports.getModule = class TransferFileModule extends MenuModule { 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(); - 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 ]; + if (!Array.isArray(filePaths)) { + filePaths = [filePaths]; } this.prepAndBuildSendArgs(filePaths, (err, args) => { - if(err) { + if (err) { return cb(err); } @@ -486,8 +526,8 @@ exports.getModule = class TransferFileModule extends MenuModule { } executeExternalProtocolHandlerForRecv(cb) { - this.prepAndBuildRecvArgs( (err, args) => { - if(err) { + this.prepAndBuildRecvArgs((err, args) => { + if (err) { return cb(err); } @@ -498,85 +538,115 @@ exports.getModule = class TransferFileModule extends MenuModule { } getMenuResult() { - if(this.isSending()) { - return { sentFileIds : this.sentFileIds }; + if (this.isSending()) { + return { sentFileIds: this.sentFileIds }; } else { - return { recvFilePaths : this.recvFilePaths }; + return { recvFilePaths: this.recvFilePaths }; } } updateSendStats(cb) { - let downloadBytes = 0; - let downloadCount = 0; - let fileIds = []; + let downloadBytes = 0; + let downloadCount = 0; + let fileIds = []; - async.each(this.sendQueue, (queueItem, next) => { - if(!queueItem.sent) { - return next(null); - } - - if(queueItem.fileId) { - fileIds.push(queueItem.fileId); - } - - 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; + async.each( + this.sendQueue, + (queueItem, next) => { + if (!queueItem.sent) { + return next(null); } - 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, UserProps.FileDlTotalCount, downloadCount); - StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalBytes, downloadBytes); + if (queueItem.fileId) { + fileIds.push(queueItem.fileId); + } - StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount); - StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes); + if (_.isNumber(queueItem.byteSize)) { + downloadCount += 1; + downloadBytes += queueItem.byteSize; + return next(null); + } - fileIds.forEach(fileId => { - FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); - }); + // 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 cb(null); - }); + 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, + UserProps.FileDlTotalCount, + downloadCount + ); + StatLog.incrementUserStat( + this.client.user, + UserProps.FileDlTotalBytes, + downloadBytes + ); + + StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount); + StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes); + + fileIds.forEach(fileId => { + FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); + }); + + return cb(null); + } + ); } 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, UserProps.FileUlTotalCount, uploadCount); - StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalBytes, uploadBytes); + return next(null); + }); + }, + () => { + StatLog.incrementUserStat( + this.client.user, + UserProps.FileUlTotalCount, + uploadCount + ); + StatLog.incrementUserStat( + this.client.user, + UserProps.FileUlTotalBytes, + uploadBytes + ); - StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount); - StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes); + StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount); + StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes); - return cb(null); - }); + return cb(null); + } + ); } initSequence() { @@ -587,41 +657,38 @@ exports.getModule = class TransferFileModule extends MenuModule { async.series( [ function validateConfig(callback) { - if(self.isSending()) { - if(!Array.isArray(self.sendQueue)) { - self.sendQueue = [ self.sendQueue ]; + 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) { + if (self.isSending()) { + self.sendFiles(err => { + if (err) { return callback(err); } const sentFileIds = []; self.sendQueue.forEach(queueItem => { - if(queueItem.sent && queueItem.fileId) { + if (queueItem.sent && queueItem.fileId) { sentFileIds.push(queueItem.fileId); } }); - if(sentFileIds.length > 0) { + 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 - } - ); + Events.emit(Events.getSystemEvents().UserDownload, { + user: self.client.user, + files: dlFileEntries, + }); self.sentFileIds = sentFileIds; } @@ -629,29 +696,32 @@ exports.getModule = class TransferFileModule extends MenuModule { return callback(null); }); } else { - self.recvFiles( err => { + self.recvFiles(err => { return callback(err); }); } }, function cleanupTempFiles(callback) { - temptmp.cleanup( paths => { - Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); + temptmp.cleanup(paths => { + Log.debug( + { paths: paths, sessionId: temptmp.sessionId }, + 'Temporary files cleaned up' + ); }); return callback(null); }, function updateUserAndSystemStats(callback) { - if(self.isSending()) { + 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'); + if (err) { + self.client.log.warn({ error: err.message }, 'File transfer error'); } return self.prevMenu(); diff --git a/core/file_transfer_protocol_select.js b/core/file_transfer_protocol_select.js index 13e30a74..91b6d474 100644 --- a/core/file_transfer_protocol_select.js +++ b/core/file_transfer_protocol_select.js @@ -2,84 +2,95 @@ 'use strict'; // enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const Config = require('./config.js').get; -const ViewController = require('./view_controller.js').ViewController; +const MenuModule = require('./menu_module.js').MenuModule; +const Config = require('./config.js').get; +const ViewController = require('./view_controller.js').ViewController; // deps -const async = require('async'); -const _ = require('lodash'); +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); this.config = this.menuConfig.config || {}; - if(options.extraArgs) { - if(options.extraArgs.direction) { + if (options.extraArgs) { + if (options.extraArgs.direction) { this.config.direction = options.extraArgs.direction; } } this.config.direction = this.config.direction || 'send'; - this.extraArgs = options.extraArgs; + this.extraArgs = options.extraArgs; - if(_.has(options, 'lastMenuResult.sentFileIds')) { + if (_.has(options, 'lastMenuResult.sentFileIds')) { this.sentFileIds = options.lastMenuResult.sentFileIds; } - if(_.has(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.menuMethods = { - selectProtocol : (formData, extraArgs, cb) => { - const protocol = this.protocols[formData.value.protocol]; + 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 ); + Object.assign( + finalExtraArgs, + { protocol: protocol.protocol, direction: this.config.direction }, + extraArgs + ); const modOpts = { - extraArgs : finalExtraArgs, + extraArgs: finalExtraArgs, }; - if('send' === this.config.direction) { - return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', 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); + return this.gotoMenu( + this.config.uploadFilesMenu || 'recvFilesFromUser', + modOpts, + cb + ); } }, }; } getMenuResult() { - if(this.sentFileIds) { - return { sentFileIds : this.sentFileIds }; + 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) { + if (this.sentFileIds || this.recvFilePaths) { // nothing to do here; move along (we're just falling through) this.prevMenu(); } else { @@ -89,19 +100,21 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(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 + callingMenu: self, + mciMap: mciData.menu, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -113,7 +126,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { protListView.redraw(); return callback(null); - } + }, ], err => { return cb(err); @@ -125,28 +138,32 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { loadAvailProtocols() { this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { return { - text : protInfo.name, // standard - protocol : protocol, - name : protInfo.name, - hasBatch : _.has(protInfo, 'external.recvArgs'), - hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), - sort : protInfo.sort, + text: protInfo.name, // standard + 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 ); + 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 ); + 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)) { + 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 } ); + return a.name.localeCompare(b.name, { + sensitivity: false, + numeric: true, + }); } }); } diff --git a/core/file_util.js b/core/file_util.js index 56d8d5ac..07a520f7 100644 --- a/core/file_util.js +++ b/core/file_util.js @@ -2,58 +2,61 @@ 'use strict'; // ENiGMA½ -const EnigAssert = require('./enigma_assert.js'); +const EnigAssert = require('./enigma_assert.js'); // deps -const fse = require('fs-extra'); -const paths = require('path'); -const async = require('async'); +const fse = require('fs-extra'); +const paths = require('path'); +const async = require('async'); -exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling; -exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling; -exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; +exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling; +exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling; +exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { - operation = operation || 'copy'; - const dstPath = paths.dirname(dst); - const dstFileExt = paths.extname(dst); + operation = operation || 'copy'; + const dstPath = paths.dirname(dst); + const dstFileExt = paths.extname(dst); const dstFileSuffix = paths.basename(dst, dstFileExt); EnigAssert('move' === operation || 'copy' === operation); - let renameIndex = 0; - let opOk = false; + let renameIndex = 0; + let opOk = false; let tryDstPath; function tryOperation(src, dst, callback) { - if('move' === operation) { + if ('move' === operation) { fse.move(src, tryDstPath, err => { return callback(err); }); - } else if('copy' === operation) { - fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => { + } else if ('copy' === operation) { + fse.copy(src, tryDstPath, { overwrite: false, errorOnExist: true }, err => { return callback(err); }); } } async.until( - (callback) => callback(null, opOk), // until moved OK - (cb) => { - if(0 === renameIndex) { + callback => callback(null, opOk), // until moved OK + cb => { + if (0 === renameIndex) { // try originally supplied path first tryDstPath = dst; } else { - tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); + tryDstPath = paths.join( + dstPath, + `${dstFileSuffix}(${renameIndex})${dstFileExt}` + ); } tryOperation(src, tryDstPath, err => { - if(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 || 'dest already exists.' === err.message) { + if ('EEXIST' === err.code || 'dest already exists.' === err.message) { renameIndex += 1; - return cb(null); // keep trying + return cb(null); // keep trying } return cb(err); @@ -82,7 +85,7 @@ function copyFileWithCollisionHandling(src, dst, cb) { } function pathWithTerminatingSeparator(path) { - if(path && paths.sep !== path.charAt(path.length - 1)) { + if (path && paths.sep !== path.charAt(path.length - 1)) { path = path + paths.sep; } return path; diff --git a/core/files_bbs_file.js b/core/files_bbs_file.js index 6e186ab4..8b95660a 100644 --- a/core/files_bbs_file.js +++ b/core/files_bbs_file.js @@ -1,12 +1,12 @@ /* jslint node: true */ 'use strict'; -const { Errors } = require('./enig_error.js'); +const { Errors } = require('./enig_error.js'); // deps -const fs = require('graceful-fs'); -const iconv = require('iconv-lite'); -const moment = require('moment'); +const fs = require('graceful-fs'); +const iconv = require('iconv-lite'); +const moment = require('moment'); // Descriptions found in the wild that mean "no description" /facepalm. const IgnoredDescriptions = [ @@ -25,14 +25,14 @@ module.exports = class FilesBBSFile { getDescription(fileName) { const entry = this.get(fileName); - if(entry) { + if (entry) { return entry.desc; } } static createFromFile(path, cb) { fs.readFile(path, (err, descData) => { - if(err) { + if (err) { return cb(err); } @@ -40,7 +40,7 @@ module.exports = class FilesBBSFile { const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); const filesBbs = new FilesBBSFile(); - const isBadDescription = (desc) => { + const isBadDescription = desc => { return IgnoredDescriptions.find(d => desc.startsWith(d)) ? true : false; }; @@ -59,9 +59,7 @@ module.exports = class FilesBBSFile { const detectDecoder = () => { // helpers const regExpTestUpTo = (n, re) => { - return lines - .slice(0, n) - .some(l => re.test(l)); + return lines.slice(0, n).some(l => re.test(l)); }; // @@ -70,36 +68,37 @@ module.exports = class FilesBBSFile { const decoders = [ { // I've been told this is what Syncrhonet uses - lineRegExp : /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/, - detect : function() { + lineRegExp: + /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/, + detect: function () { return regExpTestUpTo(10, this.lineRegExp); }, - extract : function() { - for(let i = 0; i < lines.length; ++i) { + extract: function () { + for (let i = 0; i < lines.length; ++i) { let line = lines[i]; const hdr = line.match(this.lineRegExp); - if(!hdr) { + if (!hdr) { continue; } const long = []; - for(let j = i + 1; j < lines.length; ++j) { + for (let j = i + 1; j < lines.length; ++j) { line = lines[j]; - if(!line.startsWith(' ')) { + if (!line.startsWith(' ')) { break; } long.push(line.trim()); ++i; } - const desc = long.join('\r\n') || hdr[3] || ''; - const fileName = hdr[1]; + const desc = long.join('\r\n') || hdr[3] || ''; + const fileName = hdr[1]; const timestamp = moment(hdr[2], 'MM/DD/YY'); - if(isBadDescription(desc) || !timestamp.isValid()) { + if (isBadDescription(desc) || !timestamp.isValid()) { continue; } - filesBbs.entries.set(fileName, { timestamp, desc } ); + filesBbs.entries.set(fileName, { timestamp, desc }); } - } + }, }, { @@ -107,37 +106,41 @@ module.exports = class FilesBBSFile { // Examples: // - Night Owl CD #7, 1992 // - lineRegExp : /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/, - detect : function() { + lineRegExp: /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/, + detect: function () { return regExpTestUpTo(10, this.lineRegExp); }, - extract : function() { - for(let i = 0; i < lines.length; ++i) { + extract: function () { + for (let i = 0; i < lines.length; ++i) { let line = lines[i]; const hdr = line.match(this.lineRegExp); - if(!hdr) { + if (!hdr) { continue; } - const long = [ hdr[2].trim() ]; - for(let j = i + 1; j < lines.length; ++j) { + const long = [hdr[2].trim()]; + for (let j = i + 1; j < lines.length; ++j) { line = lines[j]; // -------------------------------------------------v 32 - if(!line.startsWith(' | ')) { + if ( + !line.startsWith( + ' | ' + ) + ) { break; } long.push(line.substr(33)); ++i; } - const desc = long.join('\r\n'); - const fileName = hdr[1]; + const desc = long.join('\r\n'); + const fileName = hdr[1]; - if(isBadDescription(desc)) { + if (isBadDescription(desc)) { continue; } - filesBbs.entries.set(fileName, { desc } ); + filesBbs.entries.set(fileName, { desc }); } - } + }, }, { @@ -148,36 +151,36 @@ module.exports = class FilesBBSFile { // Examples // - GUS archive @ dk.toastednet.org // - lineRegExp : /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/, - detect : function() { + lineRegExp: /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/, + detect: function () { return regExpTestUpTo(10, this.lineRegExp); }, - extract : function() { - for(let i = 0; i < lines.length; ++i) { + extract: function () { + for (let i = 0; i < lines.length; ++i) { let line = lines[i]; const hdr = line.match(this.lineRegExp); - if(!hdr) { + if (!hdr) { continue; } - const long = [ hdr[2].trimRight() ]; - for(let j = i + 1; j < lines.length; ++j) { + const long = [hdr[2].trimRight()]; + for (let j = i + 1; j < lines.length; ++j) { line = lines[j]; - if(!line.startsWith('\t\t ')) { + if (!line.startsWith('\t\t ')) { break; } long.push(line.substr(4)); ++i; } - const desc = long.join('\r\n'); - const fileName = hdr[1]; + const desc = long.join('\r\n'); + const fileName = hdr[1]; - if(isBadDescription(desc)) { + if (isBadDescription(desc)) { continue; } - filesBbs.entries.set(fileName, { desc } ); + filesBbs.entries.set(fileName, { desc }); } - } + }, }, { @@ -187,41 +190,46 @@ module.exports = class FilesBBSFile { // Examples: // - Expanding Your BBS CD by David Wolfe, 1995 // - lineRegExp : /^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/, - detect : function() { + lineRegExp: + /^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/, + detect: function () { return regExpTestUpTo(10, this.lineRegExp); }, - extract : function() { - for(let i = 0; i < lines.length; ++i) { + extract: function () { + for (let i = 0; i < lines.length; ++i) { let line = lines[i]; const hdr = line.match(this.lineRegExp); - if(!hdr) { + if (!hdr) { continue; } const firstDescLine = hdr[4].trimRight(); - const long = [ firstDescLine ]; - for(let j = i + 1; j < lines.length; ++j) { + const long = [firstDescLine]; + for (let j = i + 1; j < lines.length; ++j) { line = lines[j]; - if(!line.startsWith(' '.repeat(34))) { + if (!line.startsWith(' '.repeat(34))) { break; } long.push(line.substr(34).trimRight()); ++i; } - const desc = long.join('\r\n'); - const fileName = hdr[1]; - const size = parseInt(hdr[2]); + const desc = long.join('\r\n'); + const fileName = hdr[1]; + const size = parseInt(hdr[2]); const timestamp = moment(hdr[3], 'MM-DD-YY'); - if(isBadDescription(desc) || isNaN(size) || !timestamp.isValid()) { + if ( + isBadDescription(desc) || + isNaN(size) || + !timestamp.isValid() + ) { continue; } filesBbs.entries.set(fileName, { desc, size, timestamp }); } - } + }, }, { @@ -235,25 +243,25 @@ module.exports = class FilesBBSFile { // // May contain headers, but we'll just skip 'em. // - lineRegExp : /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/, - detect : function() { + lineRegExp: /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/, + detect: function () { return regExpTestUpTo(10, this.lineRegExp); }, - extract : function() { + extract: function () { lines.forEach(line => { const hdr = line.match(this.lineRegExp); - if(!hdr) { + if (!hdr) { return; // forEach } - const fileName = hdr[1].trim(); - const desc = hdr[2].trim(); + const fileName = hdr[1].trim(); + const desc = hdr[2].trim(); - if(desc && !isBadDescription(desc)) { - filesBbs.entries.set(fileName, { desc } ); + if (desc && !isBadDescription(desc)) { + filesBbs.entries.set(fileName, { desc }); } }); - } + }, }, { @@ -261,31 +269,32 @@ module.exports = class FilesBBSFile { // Examples: // - AMINET CD's & similar // - lineRegExp : /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/, - detect : function() { + lineRegExp: /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/, + detect: function () { return regExpTestUpTo(10, this.lineRegExp); }, - extract : function() { + extract: function () { lines.forEach(line => { const hdr = line.match(this.tester); - if(!hdr) { + if (!hdr) { return; // forEach } - const fileName = hdr[1].trim(); - let size = parseInt(hdr[2]); - const desc = hdr[3].trim(); + const fileName = hdr[1].trim(); + let size = parseInt(hdr[2]); + const desc = hdr[3].trim(); - if(isNaN(size)) { + if (isNaN(size)) { return; // forEach } - size *= 1024; // K->bytes. + size *= 1024; // K->bytes. - if(desc) { // omit empty entries - filesBbs.entries.set(fileName, { size, desc } ); + if (desc) { + // omit empty entries + filesBbs.entries.set(fileName, { size, desc }); } }); - } + }, }, ]; @@ -294,18 +303,18 @@ module.exports = class FilesBBSFile { }; const decoder = detectDecoder(); - if(!decoder) { + if (!decoder) { return cb(Errors.Invalid('Invalid or unrecognized FILES.BBS format')); } decoder.extract(decoder); return cb( - filesBbs.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized FILES.BBS format'), + filesBbs.entries.size > 0 + ? null + : Errors.Invalid('Invalid or unrecognized FILES.BBS format'), filesBbs ); }); } - - }; diff --git a/core/fnv1a.js b/core/fnv1a.js index b85e4241..a8b7cdf3 100644 --- a/core/fnv1a.js +++ b/core/fnv1a.js @@ -3,36 +3,39 @@ const { Errors } = require('./enig_error.js'); -const _ = require('lodash'); +const _ = require('lodash'); // FNV-1a based on work here: https://github.com/wiedi/node-fnv module.exports = class FNV1a { constructor(data) { this.hash = 0x811c9dc5; - if(!_.isUndefined(data)) { + if (!_.isUndefined(data)) { this.update(data); } } update(data) { - if(_.isNumber(data)) { + if (_.isNumber(data)) { data = data.toString(); } - if(_.isString(data)) { + if (_.isString(data)) { data = Buffer.from(data); } - if(!Buffer.isBuffer(data)) { + if (!Buffer.isBuffer(data)) { throw Errors.Invalid('data must be String or Buffer!'); } - for(let b of data) { + for (let b of data) { this.hash = this.hash ^ b; this.hash += - (this.hash << 24) + (this.hash << 8) + (this.hash << 7) + - (this.hash << 4) + (this.hash << 1); + (this.hash << 24) + + (this.hash << 8) + + (this.hash << 7) + + (this.hash << 4) + + (this.hash << 1); } return this; @@ -49,4 +52,3 @@ module.exports = class FNV1a { return this.hash & 0xffffffff; } }; - diff --git a/core/fse.js b/core/fse.js index a0d8750b..b5997ca5 100644 --- a/core/fse.js +++ b/core/fse.js @@ -2,85 +2,75 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const { ViewController } = require('./view_controller.js'); -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const Message = require('./message.js'); -const { - updateMessageAreaLastReadId -} = require('./message_area.js'); -const { getMessageAreaByTag } = require('./message_area.js'); -const User = require('./user.js'); -const StatLog = require('./stat_log.js'); -const stringFormat = require('./string_format.js'); -const { - MessageAreaConfTempSwitcher -} = require('./mod_mixins.js'); -const { - isAnsi, stripAnsiControlCodes, - insert -} = require('./string_util.js'); -const { - stripMciColorCodes, - controlCodesToAnsi, -} = require('./color_codes.js'); -const Config = require('./config.js').get; -const { getAddressedToInfo } = require('./mail_util.js'); -const Events = require('./events.js'); -const UserProps = require('./user_property.js'); -const SysProps = require('./system_property.js'); -const FileArea = require('./file_base_area.js'); -const FileEntry = require('./file_entry.js'); -const DownloadQueue = require('./download_queue.js'); +const { MenuModule } = require('./menu_module.js'); +const { ViewController } = require('./view_controller.js'); +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const Message = require('./message.js'); +const { updateMessageAreaLastReadId } = require('./message_area.js'); +const { getMessageAreaByTag } = require('./message_area.js'); +const User = require('./user.js'); +const StatLog = require('./stat_log.js'); +const stringFormat = require('./string_format.js'); +const { MessageAreaConfTempSwitcher } = require('./mod_mixins.js'); +const { isAnsi, stripAnsiControlCodes, insert } = require('./string_util.js'); +const { stripMciColorCodes, controlCodesToAnsi } = require('./color_codes.js'); +const Config = require('./config.js').get; +const { getAddressedToInfo } = require('./mail_util.js'); +const Events = require('./events.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); +const FileArea = require('./file_base_area.js'); +const FileEntry = require('./file_entry.js'); +const DownloadQueue = require('./download_queue.js'); // deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const moment = require('moment'); -const fse = require('fs-extra'); -const fs = require('graceful-fs'); -const paths = require('path'); -const sanatizeFilename = require('sanitize-filename'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const moment = require('moment'); +const fse = require('fs-extra'); +const fs = require('graceful-fs'); +const paths = require('path'); +const sanatizeFilename = require('sanitize-filename'); 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: consolidate all footer MCI's - remove all magic #'s - ViewModeFooter : { - MsgNum : 6, - MsgTotal : 7, + ViewModeFooter: { + MsgNum: 6, + MsgTotal: 7, // :TODO: Just use custom ranges }, - quoteBuilder : { - quotedMsg : 1, + quoteBuilder: { + quotedMsg: 1, // 2 NYI - quoteLines : 3, - } + quoteLines: 3, + }, }; /* @@ -103,797 +93,941 @@ const MciViewIds = { // :TODO: convert code in this class to newer styles, conventions, etc. There is a lot of experimental stuff here that has better (DRY) alternatives -exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModule extends MessageAreaConfTempSwitcher(MenuModule) { +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; - - 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; - - // 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.isReady = false; - - 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 + // menuConfig.config: + // editorType : email | area + // editorMode : view | edit | quote // - viewValidationListener : function(err, cb) { - var errMsgView = self.viewControllers.header.getView(MciViewIds.header.errorMsg); - var newFocusViewId; - if(errMsgView) { - if(err) { - errMsgView.setText(err.message); + // menuConfig.config or extraArgs + // messageAreaTag + // messageIndex / messageTotal + // toUserId + // + this.editorType = config.editorType; + this.editorMode = config.editorMode; - 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(); - } + 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; + + // extraArgs can override some config + if (_.isObject(options.extraArgs)) { + if (options.extraArgs.messageAreaTag) { + this.messageAreaTag = options.extraArgs.messageAreaTag; } - 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 (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; + } + } - self.switchFooter(function next(err) { - if(err) { - return cb(err); + this.noUpdateLastReadId = + _.get( + options, + 'extraArgs.noUpdateLastReadId', + config.noUpdateLastReadId + ) || 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; + } + + 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'; + + 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; + + case 'editorMenu': + self.viewControllers.body.setFocus(false); + self.viewControllers.footerEditorMenu.switchFocus(1); + break; + + 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 + ); + + if (self.newQuoteBlock) { + self.newQuoteBlock = false; + + // :TODO: If replying to ANSI, add a blank sepration line here + + quoteMsgView.addText(self.getQuoteByHeader()); } - switch(self.footerMode) { - case 'editor' : - if(!_.isUndefined(self.viewControllers.footerEditorMenu)) { - self.viewControllers.footerEditorMenu.detachClientEvents(); - } - self.viewControllers.body.switchFocus(1); - self.observeEditorEvents(); - break; + const quoteListView = self.viewControllers.quoteBuilder.getView( + MciViewIds.quoteBuilder.quoteLines + ); + const quoteText = quoteListView.getItem(formData.value.quote); - case 'editorMenu' : - self.viewControllers.body.setFocus(false); - self.viewControllers.footerEditorMenu.switchFocus(1); - break; + quoteMsgView.addText(quoteText); - default : throw new Error('Unexpected mode'); + // + // 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(); } 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; - - // :TODO: If replying to ANSI, add a blank sepration line here - - quoteMsgView.addText(self.getQuoteByHeader()); - } - - const quoteListView = self.viewControllers.quoteBuilder.getView(MciViewIds.quoteBuilder.quoteLines); - const quoteText = quoteListView.getItem(formData.value.quote); - - 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 { + }, + quoteBuilderEscPressed: function (formData, extraArgs, cb) { self.quoteBuilderFinalize(); - } - - return cb(null); - }, - quoteBuilderEscPressed : function(formData, extraArgs, cb) { - self.quoteBuilderFinalize(); - return cb(null); - }, - /* + 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); - }, + 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); + }, - addToDownloadQueue : (formData, extraArgs, cb) => { - this.viewControllers.footerView.setFocus(false); - return this.addToDownloadQueue(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.message, '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; + addToDownloadQueue: (formData, extraArgs, cb) => { + this.viewControllers.footerView.setFocus(false); + return this.addToDownloadQueue(cb); + }, + }; } - } - buildMessage(cb) { - const headerValues = this.viewControllers.header.getFormData().value; - const area = getMessageAreaByTag(this.messageAreaTag); + isEditMode() { + return 'edit' === this.editorMode; + } - const getFromUserName = () => { - return (area && area.realNames) ? - this.client.user.getProperty(UserProps.RealName) || this.client.user.username : - this.client.user.username; - }; + isViewMode() { + return 'view' === this.editorMode; + } - let messageBody = this.viewControllers.body.getView(MciViewIds.body.message).getData( { forceLineTerms : this.replyIsAnsi } ); + isPrivateMail() { + return Message.WellKnownAreaTags.Private === this.messageAreaTag; + } - const msgOpts = { - areaTag : this.messageAreaTag, - toUserName : headerValues.to, - fromUserName : getFromUserName(), - subject : headerValues.subject, - }; + isReply() { + return !_.isUndefined(this.replyToMessage); + } - if(this.isReply()) { - msgOpts.replyToMsgId = this.replyToMessage.messageId; + getFooterName() { + return 'footer' + _.upperFirst(this.footerMode); // e.g. 'footerEditor', 'footerEditorMenu', ... + } - 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') } }; - messageBody = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto(1,1)}\r\n${ansi.up()}${messageBody}`; + 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.message, + '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; } } - // - // Append auto-signature, if enabled for the area & the user has one - // - if(false != area.autoSignatures) { - const sig = this.client.user.getProperty(UserProps.AutoSignature); - if(sig) { - messageBody += `\r\n-- \r\n${sig}`; + buildMessage(cb) { + const headerValues = this.viewControllers.header.getFormData().value; + const area = getMessageAreaByTag(this.messageAreaTag); + + const getFromUserName = () => { + return area && area.realNames + ? this.client.user.getProperty(UserProps.RealName) || + this.client.user.username + : this.client.user.username; + }; + + let messageBody = this.viewControllers.body + .getView(MciViewIds.body.message) + .getData({ forceLineTerms: this.replyIsAnsi }); + + const msgOpts = { + areaTag: this.messageAreaTag, + toUserName: headerValues.to, + fromUserName: getFromUserName(), + subject: headerValues.subject, + }; + + 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' + ), + }, + }; + messageBody = `${ansi.reset()}${ansi.eraseData(2)}${ansi.goto( + 1, + 1 + )}\r\n${ansi.up()}${messageBody}`; + } } - } - // finally, create the message - msgOpts.message = messageBody; - this.message = new Message(msgOpts); + // + // Append auto-signature, if enabled for the area & the user has one + // + if (false != area.autoSignatures) { + const sig = this.client.user.getProperty(UserProps.AutoSignature); + if (sig) { + messageBody += `\r\n-- \r\n${sig}`; + } + } - return cb(null); - } + // finally, create the message + msgOpts.message = messageBody; + this.message = new Message(msgOpts); - 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 = 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 { - msg = stripAnsiControlCodes(msg); // start clean - - const styleToArray = (style, len) => { - if (!Array.isArray(style)) { - style = [ style ]; - } - while (style.length < len) { - style.push(style[0]); - } - return style; - }; - - // - // In *View* mode, if enabled, do a little prep work so we can stylize: - // - Quote indicators - // - Tear lines - // - Origins - // - if (this.menuConfig.config.quoteStyleLevel1) { - // can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT - // Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed - const styleL1 = styleToArray(this.menuConfig.config.quoteStyleLevel1, 3); - - const QuoteRegex = /^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm; - msg = msg.replace(QuoteRegex, (m, spc1, initials, spc2, text) => { - return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`; - }); - } - - if (this.menuConfig.config.tearLineStyle) { - // '---' and TEXT - const style = styleToArray(this.menuConfig.config.tearLineStyle, 2); - - const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m; - msg = msg.replace(TearLineRegex, (m, text) => { - return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`; - }); - } - - if (this.menuConfig.config.originStyle) { - const style = styleToArray(this.menuConfig.config.originStyle, 3); - - const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m; - msg = msg.replace(OriginRegex, (m, spc, text) => { - return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`; - }); - } - - bodyMessageView.setText(controlCodesToAnsi(msg)); - } - } + updateLastReadId(cb) { + if (this.noUpdateLastReadId) { + return cb(null); } - }); - } - 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); - } - ); - } - - updateUserAndSystemStats(cb) { - if(Message.isPrivateAreaTag(this.message.areaTag)) { - Events.emit(Events.getSystemEvents().UserSendMail, { user : this.client.user }); - if(cb) { - cb(null); - } - return; // don't inc stats for private messages + return updateMessageAreaLastReadId( + this.client.user.userId, + this.messageAreaTag, + this.message.messageId, + cb + ); } - Events.emit(Events.getSystemEvents().UserPostMessage, { user : this.client.user, areaTag : this.message.areaTag }); + setMessage(message) { + this.message = message; - StatLog.incrementNonPersistentSystemStat(SysProps.MessageTotalCount, 1); - StatLog.incrementNonPersistentSystemStat(SysProps.MessagesToday, 1); - return StatLog.incrementUserStat(this.client.user, UserProps.MessagePostCount, 1, cb); - } + this.updateLastReadId(() => { + if (this.isReady) { + this.initHeaderViewMode(); + this.initFooterViewMode(); - 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, startRow: self.header.height + self.body.height }, - function displayed(err, artData) { - callback(err, artData); - } + const bodyMessageView = this.viewControllers.body.getView( + MciViewIds.body.message ); - } - ], - function complete(err, artData) { - cb(err, artData); - } - ); - } + let msg = this.message.message; - redrawScreen(cb) { - var comps = [ 'header', 'body' ]; - const self = this; - var art = self.menuConfig.config.art; + 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 = Message.getTearLinePosition(msg); - self.client.term.rawWrite(ansi.resetScreen()); - - async.series( - [ - function displayHeaderAndBody(callback) { - async.waterfall( - [ - function displayHeader(callback) { - theme.displayThemedAsset( - art['header'], - self.client, - { font : self.menuConfig.font }, - function displayed(err, artInfo) { - return callback(err, artInfo); - } + if (tearLinePos > -1) { + msg = insert( + msg, + tearLinePos, + bodyMessageView.getSGRFor('text') ); - }, - function displayBody(artInfo, callback) { - theme.displayThemedAsset( - art['header'], - self.client, - { font : self.menuConfig.font, startRow: artInfo.height + 1 }, - function displayed(err, artInfo) { - return callback(err, artInfo); + } + + bodyMessageView.setAnsi( + msg.replace(/\r?\n/g, '\r\n'), // messages are stored with CRLF -> LF + { + prepped: false, + forceLineTerm: true, + } + ); + } else { + msg = stripAnsiControlCodes(msg); // start clean + + const styleToArray = (style, len) => { + if (!Array.isArray(style)) { + style = [style]; + } + while (style.length < len) { + style.push(style[0]); + } + return style; + }; + + // + // In *View* mode, if enabled, do a little prep work so we can stylize: + // - Quote indicators + // - Tear lines + // - Origins + // + if (this.menuConfig.config.quoteStyleLevel1) { + // can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT + // Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed + const styleL1 = styleToArray( + this.menuConfig.config.quoteStyleLevel1, + 3 + ); + + const QuoteRegex = + /^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm; + msg = msg.replace( + QuoteRegex, + (m, spc1, initials, spc2, text) => { + return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`; } ); } - ], - function complete(err) { - //self.body.height = self.client.term.termHeight - self.header.height - 1; - callback(err); + + if (this.menuConfig.config.tearLineStyle) { + // '---' and TEXT + const style = styleToArray( + this.menuConfig.config.tearLineStyle, + 2 + ); + + const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m; + msg = msg.replace(TearLineRegex, (m, text) => { + return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + if (this.menuConfig.config.originStyle) { + const style = styleToArray( + this.menuConfig.config.originStyle, + 3 + ); + + const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m; + msg = msg.replace(OriginRegex, (m, spc, text) => { + return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + bodyMessageView.setText(controlCodesToAnsi(msg)); } - ); - }, - 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(); + getMessage(cb) { + const self = this; - this.redrawFooter( { footerName : footerName, clear : true }, (err, artData) => { - if(err) { - cb(err); - return; - } + async.series( + [ + function buildIfNecessary(callback) { + if (self.isEditMode()) { + return self.buildMessage(callback); // creates initial self.message + } - var formId = this.getFormId(footerName); + return callback(null); + }, + function populateLocalUserInfo(callback) { + self.message.setLocalFromUserId(self.client.user.userId); - if(_.isUndefined(this.viewControllers[footerName])) { - var menuLoadOpts = { - callingMenu : this, - formId : formId, - mciMap : artData.mciMap - }; + if (!self.isPrivateMail()) { + return callback(null); + } - this.addViewController( - footerName, - new ViewController( { client : this.client, formId : formId } ) - ).loadFromMenuConfig(menuLoadOpts, err => { - cb(err); + 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); + } + ); + } + + updateUserAndSystemStats(cb) { + if (Message.isPrivateAreaTag(this.message.areaTag)) { + Events.emit(Events.getSystemEvents().UserSendMail, { + user: this.client.user, }); - } else { - this.viewControllers[footerName].redrawAll(); - cb(null); + if (cb) { + cb(null); + } + return; // don't inc stats for private messages } - }); - } - initSequence() { - var mciData = { }; - const self = this; - var art = self.menuConfig.config.art; + Events.emit(Events.getSystemEvents().UserPostMessage, { + user: this.client.user, + areaTag: this.message.areaTag, + }); - assert(_.isObject(art)); + StatLog.incrementNonPersistentSystemStat(SysProps.MessageTotalCount, 1); + StatLog.incrementNonPersistentSystemStat(SysProps.MessagesToday, 1); + return StatLog.incrementUserStat( + this.client.user, + UserProps.MessagePostCount, + 1, + cb + ); + } - async.waterfall( - [ - function beforeDisplayArt(callback) { - self.beforeArt(callback); - }, - function displayHeader(callback) { - theme.displayThemedAsset( - art.header, - self.client, - { font : self.menuConfig.font }, - function displayed(err, artInfo) { - if(artInfo) { - mciData['header'] = artInfo; - self.header = {height: artInfo.height}; - } - return callback(err, artInfo); + 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)); } - ); - }, - function displayBody(artInfo, callback) { - theme.displayThemedAsset( - art.body, - self.client, - { font : self.menuConfig.font, startRow: artInfo.height + 1 }, - function displayed(err, artInfo) { - if(artInfo) { - mciData['body'] = artInfo; - self.body = {height: artInfo.height - self.header.height}; - } - return callback(err, artInfo); - }); - }, - function displayFooter(artInfo, callback) { - self.setInitialFooterMode(); + callback(null); + }, + function displayFooterArt(callback) { + const footerArt = self.menuConfig.config.art[options.footerName]; - 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) { - let from = self.viewControllers.header.getView(MciViewIds.header.from); - if (from) { - from.acceptsFocus = false; - } - - // :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(stripAnsiControlCodes(self.message.message)); - } - } - break; - - case 'edit' : + theme.displayThemedAsset( + footerArt, + self.client, { - const fromView = self.viewControllers.header.getView(MciViewIds.header.from); - const area = getMessageAreaByTag(self.messageAreaTag); - if(fromView !== undefined) { - if(area && area.realNames) { - fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username); - } else { - fromView.setText(self.client.user.username); + font: self.menuConfig.font, + startRow: self.header.height + self.body.height, + }, + 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.waterfall( + [ + function displayHeader(callback) { + theme.displayThemedAsset( + art['header'], + self.client, + { font: self.menuConfig.font }, + function displayed(err, artInfo) { + return callback(err, artInfo); + } + ); + }, + function displayBody(artInfo, callback) { + theme.displayThemedAsset( + art['header'], + self.client, + { + font: self.menuConfig.font, + startRow: artInfo.height + 1, + }, + function displayed(err, artInfo) { + return callback(err, artInfo); + } + ); + }, + ], + 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.waterfall( + [ + function beforeDisplayArt(callback) { + self.beforeArt(callback); + }, + function displayHeader(callback) { + theme.displayThemedAsset( + art.header, + self.client, + { font: self.menuConfig.font }, + function displayed(err, artInfo) { + if (artInfo) { + mciData['header'] = artInfo; + self.header = { height: artInfo.height }; + } + return callback(err, artInfo); + } + ); + }, + function displayBody(artInfo, callback) { + theme.displayThemedAsset( + art.body, + self.client, + { font: self.menuConfig.font, startRow: artInfo.height + 1 }, + function displayed(err, artInfo) { + if (artInfo) { + mciData['body'] = artInfo; + self.body = { + height: artInfo.height - self.header.height, + }; + } + return callback(err, artInfo); + } + ); + }, + function displayFooter(artInfo, 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) { + let from = self.viewControllers.header.getView( + MciViewIds.header.from + ); + if (from) { + from.acceptsFocus = false; + } + + // :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( + stripAnsiControlCodes(self.message.message) + ); } } + break; - if(self.replyToMessage) { - self.initHeaderReplyEditMode(); + case 'edit': + { + const fromView = self.viewControllers.header.getView( + MciViewIds.header.from + ); + const area = getMessageAreaByTag(self.messageAreaTag); + if (fromView !== undefined) { + if (area && area.realNames) { + fromView.setText( + self.client.user.properties[ + UserProps.RealName + ] || self.client.user.username + ); + } else { + fromView.setText(self.client.user.username); + } + } + + if (self.replyToMessage) { + self.initHeaderReplyEditMode(); + } } - } - break; - } + break; + } - callback(null); - }, - function setInitialFocus(callback) { + callback(null); + }, + function setInitialFocus(callback) { + switch (self.editorMode) { + case 'edit': + self.switchToHeader(); + break; - switch(self.editorMode) { - case 'edit' : - self.switchToHeader(); - break; + case 'view': + self.switchToFooter(); + //self.observeViewPosition(); + break; + } - case 'view' : - self.switchToFooter(); - //self.observeViewPosition(); - break; - } - - callback(null); + callback(null); + }, + ], + function complete(err) { + return cb(err); } - ], - function complete(err) { - return cb(err); - } - ); - } + ); + } - mciReadyHandler(mciData, cb) { + 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 - 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 @@ -907,280 +1041,346 @@ 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); - } - - initHeaderViewMode() { - // Only set header text for from view if it is on the form - if (this.viewControllers.header.getView(MciViewIds.header.from) !== undefined) { - 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.menuConfig.config.modTimestampFormat || this.client.currentTheme.helpers.getDateTimeFormat()) - ); - - this.setHeaderText(MciViewIds.header.msgNum, (this.messageIndex + 1).toString()); - this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); - - this.updateCustomViewTextsWithFilter('header', MciViewIds.header.customRangeStart, this.getHeaderFormatObj()); - - // if we changed conf/area we need to update any related standard MCI view - this.refreshPredefinedMciViewsByCode('header', [ 'MA', 'MC', 'ML', 'CM' ] ); - } - - initHeaderReplyEditMode() { - assert(_.isObject(this.replyToMessage)); - - 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}`; + setHeaderText(id, text) { + this.setViewText('header', id, text); } - this.setHeaderText(MciViewIds.header.subject, newSubj); - } + initHeaderViewMode() { + // Only set header text for from view if it is on the form + if ( + this.viewControllers.header.getView(MciViewIds.header.from) !== undefined + ) { + this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + } + this.setHeaderText(MciViewIds.header.to, this.message.toUserName); + this.setHeaderText(MciViewIds.header.subject, this.message.subject); - initFooterViewMode() { - this.setViewText('footerView', MciViewIds.ViewModeFooter.msgNum, (this.messageIndex + 1).toString() ); - this.setViewText('footerView', MciViewIds.ViewModeFooter.msgTotal, this.messageTotal.toString() ); - } + this.setHeaderText( + MciViewIds.header.modTimestamp, + moment(this.message.modTimestamp).format( + this.menuConfig.config.modTimestampFormat || + this.client.currentTheme.helpers.getDateTimeFormat() + ) + ); - displayHelp(cb) { - this.client.term.rawWrite(ansi.resetScreen()); + this.setHeaderText( + MciViewIds.header.msgNum, + (this.messageIndex + 1).toString() + ); + this.setHeaderText(MciViewIds.header.msgTotal, this.messageTotal.toString()); - 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); + this.updateCustomViewTextsWithFilter( + 'header', + MciViewIds.header.customRangeStart, + this.getHeaderFormatObj() + ); + + // if we changed conf/area we need to update any related standard MCI view + this.refreshPredefinedMciViewsByCode('header', ['MA', 'MC', 'ML', 'CM']); + } + + initHeaderReplyEditMode() { + assert(_.isObject(this.replyToMessage)); + + 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}`; + } + + 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() + ); + } + + 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); + }); }); - }); - } - ); - } + } + ); + } - addToDownloadQueue(cb) { - const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); - const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); + addToDownloadQueue(cb) { + const sysTempDownloadArea = FileArea.getFileAreaByTag( + FileArea.WellKnownAreaTags.TempDownloads + ); + const sysTempDownloadDir = + FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); - const msgInfo = this.getHeaderFormatObj(); + const msgInfo = this.getHeaderFormatObj(); - const outputFileName = paths.join( - sysTempDownloadDir, - sanatizeFilename( - `(${msgInfo.messageId}) ${msgInfo.subject}_(${this.message.modTimestamp.format('YYYY-MM-DD')}).txt`) - ); + const outputFileName = paths.join( + sysTempDownloadDir, + sanatizeFilename( + `(${msgInfo.messageId}) ${ + msgInfo.subject + }_(${this.message.modTimestamp.format('YYYY-MM-DD')}).txt` + ) + ); - async.waterfall( - [ - (callback) => { - const header = - `+${'-'.repeat(79)} + async.waterfall( + [ + callback => { + const header = `+${'-'.repeat(79)} | To : ${msgInfo.toUserName} | From : ${msgInfo.fromUserName} -| When : ${moment(this.message.modTimestamp).format('dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)')} +| When : ${moment(this.message.modTimestamp).format( + 'dddd, MMMM Do YYYY, h:mm:ss a (UTCZ)' + )} | Subject : ${msgInfo.subject} | ID : ${this.message.messageUuid} (${msgInfo.messageId}) +${'-'.repeat(79)} `; - const body = this.viewControllers.body - .getView(MciViewIds.body.message) - .getData( { forceLineTerms : true } ); + const body = this.viewControllers.body + .getView(MciViewIds.body.message) + .getData({ forceLineTerms: true }); - const cleanBody = stripMciColorCodes( - stripAnsiControlCodes(body, { all : true } ) - ); + const cleanBody = stripMciColorCodes( + stripAnsiControlCodes(body, { all: true }) + ); - const exportedMessage = `${header}\r\n${cleanBody}`; + const exportedMessage = `${header}\r\n${cleanBody}`; - fse.mkdirs(sysTempDownloadDir, err => { - return callback(err, exportedMessage); - }); - }, - (exportedMessage, callback) => { - return fs.writeFile(outputFileName, exportedMessage, 'utf8', callback); - }, - (callback) => { - fs.stat(outputFileName, (err, stats) => { - return callback(err, stats.size); - }); - }, - (fileSize, callback) => { - const newEntry = new FileEntry({ - areaTag : sysTempDownloadArea.areaTag, - fileName : paths.basename(outputFileName), - storageTag : sysTempDownloadArea.storageTags[0], - meta : { - upload_by_username : this.client.user.username, - upload_by_user_id : this.client.user.userId, - byte_size : fileSize, - session_temp_dl : 1, // download is valid until session is over - } - }); + fse.mkdirs(sysTempDownloadDir, err => { + return callback(err, exportedMessage); + }); + }, + (exportedMessage, callback) => { + return fs.writeFile( + outputFileName, + exportedMessage, + 'utf8', + callback + ); + }, + callback => { + fs.stat(outputFileName, (err, stats) => { + return callback(err, stats.size); + }); + }, + (fileSize, callback) => { + const newEntry = new FileEntry({ + areaTag: sysTempDownloadArea.areaTag, + fileName: paths.basename(outputFileName), + storageTag: sysTempDownloadArea.storageTags[0], + meta: { + upload_by_username: this.client.user.username, + upload_by_user_id: this.client.user.userId, + byte_size: fileSize, + session_temp_dl: 1, // download is valid until session is over + }, + }); - newEntry.desc = `${msgInfo.messageId} - ${msgInfo.subject}`; + newEntry.desc = `${msgInfo.messageId} - ${msgInfo.subject}`; - newEntry.persist(err => { - if(!err) { - // queue it! - DownloadQueue.get(this.client).addTemporaryDownload(newEntry); - } - return callback(err); - }); - }, - (callback) => { - const artSpec = this.menuConfig.config.art.expToDlQueue || - Buffer.from('Exported message has been added to your download queue!'); - this.displayAsset( - artSpec, - { clearScreen : true }, - () => { - this.client.waitForKeyPress( () => { - this.redrawScreen( () => { - this.viewControllers[this.getFooterName()].setFocus(true); + newEntry.persist(err => { + if (!err) { + // queue it! + DownloadQueue.get(this.client).addTemporaryDownload( + newEntry + ); + } + return callback(err); + }); + }, + callback => { + const artSpec = + this.menuConfig.config.art.expToDlQueue || + Buffer.from( + 'Exported message has been added to your download queue!' + ); + this.displayAsset(artSpec, { clearScreen: true }, () => { + this.client.waitForKeyPress(() => { + this.redrawScreen(() => { + this.viewControllers[this.getFooterName()].setFocus( + true + ); return callback(null); }); }); - } - ); - } - ], - err => { - return cb(err); - } - ); - } - - 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) + - 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'); - - 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.replyToMessage.getQuoteLines( - { - termWidth : self.client.term.termWidth, - termHeight : self.client.term.termHeight, - cols : quoteView.dimens.width, - startCol : quoteView.position.col, - ansiResetSgr : bodyView.styleSGR1, - ansiFocusPrefixSgr : quoteView.styleSGR2, - }, - (err, quoteLines, focusQuoteLines, replyIsAnsi) => { - if(err) { - return callback(err); - } - - self.replyIsAnsi = replyIsAnsi; - - quoteView.setItems(quoteLines); - quoteView.setFocusItems(focusQuoteLines); - - 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'); + }, + ], + err => { + return cb(err); } - } - ); - } + ); + } - observeEditorEvents() { - const bodyView = this.viewControllers.body.getView(MciViewIds.body.message); + displayQuoteBuilder() { + // + // Clear body area + // + this.newQuoteBlock = true; + const self = this; - bodyView.on('edit position', pos => { - this.updateEditModePosition(pos); - }); + 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 + ) + ); - bodyView.on('text edit mode', mode => { - this.updateTextEditMode(mode); - }); - } + 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, + }; + + 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.replyIsAnsi = replyIsAnsi; + + quoteView.setItems(quoteLines); + quoteView.setFocusItems(focusQuoteLines); + + self.viewControllers.quoteBuilder + .getView(MciViewIds.quoteBuilder.quotedMsg) + .setFocus(false); + self.viewControllers.quoteBuilder.switchFocus( + MciViewIds.quoteBuilder.quoteLines + ); + + return callback(null); + } + ); + }, + ], + function 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); + + bodyView.on('edit position', pos => { + this.updateEditModePosition(pos); + }); + + 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) @@ -1188,93 +1388,99 @@ 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')}`; + 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`); } - msgView.addText(`${quoteLines}\n\n`); + + quoteMsgView.setText(''); + + this.footerMode = 'editor'; + + this.switchFooter(() => { + this.switchFromQuoteBuilderToBody(); + }); } - quoteMsgView.setText(''); + getQuoteByHeader() { + let quoteFormat = this.menuConfig.config.quoteFormats; - this.footerMode = 'editor'; + if (Array.isArray(quoteFormat)) { + quoteFormat = quoteFormat[Math.floor(Math.random() * quoteFormat.length)]; + } else if (!_.isString(quoteFormat)) { + quoteFormat = 'On {dateTime} {userName} said...'; + } - this.switchFooter( () => { - this.switchFromQuoteBuilderToBody(); - }); - } - - 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...'; + 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 6751adb8..36ed3400 100644 --- a/core/ftn_address.js +++ b/core/ftn_address.js @@ -1,19 +1,20 @@ /* jslint node: true */ 'use strict'; -const _ = require('lodash'); +const _ = require('lodash'); const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i; -const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; +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)) { + if (addr) { + if (_.isObject(addr)) { Object.assign(this, addr); - } else if(_.isString(addr)) { + } else if (_.isString(addr)) { const temp = Address.fromString(addr); - if(temp) { + if (temp) { Object.assign(this, temp); } } @@ -30,7 +31,7 @@ module.exports = class Address { } isEqual(other) { - if(_.isString(other)) { + if (_.isString(other)) { other = Address.fromString(other); } @@ -45,46 +46,46 @@ module.exports = class Address { getMatchAddr(pattern) { const m = FTN_PATTERN_REGEXP.exec(pattern); - if(m) { - let addr = { }; + if (m) { + let addr = {}; - if(m[1]) { + if (m[1]) { addr.zone = m[1].slice(0, -1); - if('*' !== addr.zone) { + if ('*' !== addr.zone) { addr.zone = parseInt(addr.zone); } } else { addr.zone = '*'; } - if(m[2]) { + if (m[2]) { addr.net = m[2]; - if('*' !== addr.net) { + if ('*' !== addr.net) { addr.net = parseInt(addr.net); } } else { addr.net = '*'; } - if(m[3]) { + if (m[3]) { addr.node = m[3].substr(1); - if('*' !== addr.node) { + if ('*' !== addr.node) { addr.node = parseInt(addr.node); } } else { addr.node = '*'; } - if(m[4]) { + if (m[4]) { addr.point = m[4].substr(1); - if('*' !== addr.point) { + if ('*' !== addr.point) { addr.point = parseInt(addr.point); } } else { addr.point = '*'; } - if(m[5]) { + if (m[5]) { addr.domain = m[5].substr(1); } else { addr.domain = '*'; @@ -118,7 +119,7 @@ module.exports = class Address { isPatternMatch(pattern) { const addr = this.getMatchAddr(pattern); - if(addr) { + if (addr) { return ( ('*' === addr.net || this.net === addr.net) && ('*' === addr.node || this.node === addr.node) && @@ -134,25 +135,25 @@ module.exports = class Address { static fromString(addrStr) { const m = FTN_ADDRESS_REGEXP.exec(addrStr); - if(m) { + if (m) { // start with a 2D let addr = { - net : parseInt(m[2]), - node : parseInt(m[3].substr(1)), + net: parseInt(m[2]), + node: parseInt(m[3].substr(1)), }; // 3D: Addition of zone if present - if(m[1]) { + if (m[1]) { addr.zone = parseInt(m[1].slice(0, -1)); } // 4D if optional point is present - if(m[4]) { + if (m[4]) { addr.point = parseInt(m[4].substr(1)); } // 5D with @domain - if(m[5]) { + if (m[5]) { addr.domain = m[5].substr(1); } @@ -168,16 +169,16 @@ module.exports = class Address { // allow for e.g. '4D' or 5 const dim = parseInt(dimensions.toString()[0]); - if(dim >= 3) { + if (dim >= 3) { addrStr += `/${this.node}`; } // missing & .0 are equiv for point - if(dim >= 4 && this.point) { + if (dim >= 4 && this.point) { addrStr += `.${this.point}`; } - if(5 === dim && this.domain) { + if (5 === dim && this.domain) { addrStr += `@${this.domain.toLowerCase()}`; } @@ -185,19 +186,19 @@ module.exports = class Address { } static getComparator() { - return function(left, right) { + return function (left, right) { let c = (left.zone || 0) - (right.zone || 0); - if(0 !== c) { + if (0 !== c) { return c; } c = (left.net || 0) - (right.net || 0); - if(0 !== c) { + if (0 !== c) { return c; } c = (left.node || 0) - (right.node || 0); - if(0 !== c) { + if (0 !== c) { return c; } diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 973954c8..3ee667d6 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -1,29 +1,29 @@ /* jslint node: true */ 'use strict'; -const ftn = require('./ftn_util.js'); -const Message = require('./message.js'); -const sauce = require('./sauce.js'); -const Address = require('./ftn_address.js'); -const strUtil = require('./string_util.js'); -const Log = require('./logger.js').log; -const ansiPrep = require('./ansi_prep.js'); -const Errors = require('./enig_error.js').Errors; +const ftn = require('./ftn_util.js'); +const Message = require('./message.js'); +const sauce = require('./sauce.js'); +const Address = require('./ftn_address.js'); +const strUtil = require('./string_util.js'); +const Log = require('./logger.js').log; +const ansiPrep = require('./ansi_prep.js'); +const Errors = require('./enig_error.js').Errors; -const _ = require('lodash'); -const assert = require('assert'); -const { Parser } = require('binary-parser'); -const fs = require('graceful-fs'); -const async = require('async'); -const iconv = require('iconv-lite'); -const moment = require('moment'); +const _ = require('lodash'); +const assert = require('assert'); +const { Parser } = require('binary-parser'); +const fs = require('graceful-fs'); +const async = require('async'); +const iconv = require('iconv-lite'); +const moment = require('moment'); -exports.Packet = Packet; +exports.Packet = Packet; -const FTN_PACKET_HEADER_SIZE = 58; // fixed header size -const FTN_PACKET_HEADER_TYPE = 2; -const FTN_PACKET_MESSAGE_TYPE = 2; -const FTN_PACKET_BAUD_TYPE_2_2 = 2; +const FTN_PACKET_HEADER_SIZE = 58; // fixed header size +const FTN_PACKET_HEADER_TYPE = 2; +const FTN_PACKET_MESSAGE_TYPE = 2; +const FTN_PACKET_BAUD_TYPE_2_2 = 2; // SAUCE magic header + version ("00") const FTN_MESSAGE_SAUCE_HEADER = Buffer.from('SAUCE00'); @@ -33,50 +33,51 @@ const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; class PacketHeader { constructor(origAddr, destAddr, version, createdMoment) { const EMPTY_ADDRESS = { - node : 0, - net : 0, - zone : 0, - point : 0, + node: 0, + net: 0, + zone: 0, + point: 0, }; - this.version = version || '2+'; - this.origAddress = origAddr || EMPTY_ADDRESS; - this.destAddress = destAddr || EMPTY_ADDRESS; - this.created = createdMoment || moment(); + this.version = version || '2+'; + this.origAddress = origAddr || EMPTY_ADDRESS; + this.destAddress = destAddr || EMPTY_ADDRESS; + this.created = createdMoment || moment(); // uncommon to set the following explicitly - this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 - this.prodRevLo = 0; - this.baud = 0; - this.packetType = FTN_PACKET_HEADER_TYPE; - this.password = ''; - this.prodData = 0x47694e45; // "ENiG" + this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 + this.prodRevLo = 0; + this.baud = 0; + this.packetType = FTN_PACKET_HEADER_TYPE; + this.password = ''; + this.prodData = 0x47694e45; // "ENiG" - this.capWord = 0x0001; - this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap + this.capWord = 0x0001; + this.capWordValidate = + ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); // swap - this.prodCodeHi = 0xfe; // see above - this.prodRevHi = 0; + this.prodCodeHi = 0xfe; // see above + this.prodRevHi = 0; } get origAddress() { let addr = new Address({ - node : this.origNode, - zone : this.origZone, + node: this.origNode, + zone: this.origZone, }); - if(this.origPoint) { - addr.point = this.origPoint; - addr.net = this.auxNet; + if (this.origPoint) { + addr.point = this.origPoint; + addr.net = this.auxNet; } else { - addr.net = this.origNet; + addr.net = this.origNet; } return addr; } set origAddress(address) { - if(_.isString(address)) { + if (_.isString(address)) { address = Address.fromString(address); } @@ -92,22 +93,22 @@ 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, + node: this.destNode, + net: this.destNet, + zone: this.destZone, }); - if(this.destPoint) { + if (this.destPoint) { addr.point = this.destPoint; } @@ -115,37 +116,37 @@ class PacketHeader { } set destAddress(address) { - if(_.isString(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 + 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)) { + 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.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(); } @@ -179,8 +180,8 @@ const PacketHeaderParser = new Parser() .uint16le('origNet') .uint16le('destNet') .int8('prodCodeLo') - .int8('prodRevLo') // aka serialNo - .buffer('password', { length : 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 + .int8('prodRevLo') // aka serialNo + .buffer('password', { length: 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33 .uint16le('origZone') .uint16le('destZone') // @@ -212,25 +213,25 @@ const MessageHeaderParser = new Parser() // (https://github.com/keichi/binary-parser/issues/33) is fixed, this is cumbersome. // .array('modDateTime', { - type : 'uint8', - length : 20, // FTS-0001.016: 20 bytes + type: 'uint8', + length: 20, // FTS-0001.016: 20 bytes }) .array('toUserName', { - type : 'uint8', + type: 'uint8', // :TODO: array needs some soft of 'limit' field - readUntil : b => 0x00 === b, + readUntil: b => 0x00 === b, }) .array('fromUserName', { - type : 'uint8', - readUntil : b => 0x00 === b, + type: 'uint8', + readUntil: b => 0x00 === b, }) .array('subject', { - type : 'uint8', - readUntil : b => 0x00 === b, + type: 'uint8', + readUntil: b => 0x00 === b, }) .array('message', { - type : 'uint8', - readUntil : b => 0x00 === b, + type: 'uint8', + readUntil: b => 0x00 === b, }); function Packet(options) { @@ -238,33 +239,40 @@ function Packet(options) { this.options = options || {}; - this.parsePacketHeader = function(packetBuffer, cb) { + this.parsePacketHeader = function (packetBuffer, cb) { assert(Buffer.isBuffer(packetBuffer)); let packetHeader; try { packetHeader = PacketHeaderParser.parse(packetBuffer); - } catch(e) { + } 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'); + 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) { + if (FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { packetHeader.version = '2.2'; // See FSC-0045 - packetHeader.origPoint = packetHeader.year; - packetHeader.destPoint = packetHeader.month; + packetHeader.origPoint = packetHeader.year; + packetHeader.destPoint = packetHeader.month; packetHeader.destDomain = packetHeader.origZone2; packetHeader.origDomain = packetHeader.auxNet; @@ -276,14 +284,15 @@ function Packet(options) { ((packetHeader.capWordValidate & 0xff) << 8) | ((packetHeader.capWordValidate >> 8) & 0xff); - if(capWordValidateSwapped === packetHeader.capWord && + if ( + capWordValidateSwapped === packetHeader.capWord && 0 != packetHeader.capWord && - packetHeader.capWord & 0x0001) - { + packetHeader.capWord & 0x0001 + ) { packetHeader.version = '2+'; // See FSC-0048 - if(-1 === packetHeader.origNet) { + if (-1 === packetHeader.origNet) { packetHeader.origNet = packetHeader.auxNet; } } else { @@ -294,12 +303,12 @@ function Packet(options) { } packetHeader.created = moment({ - year : packetHeader.year, - month : packetHeader.month - 1, // moment uses 0 indexed months - date : packetHeader.day, - hour : packetHeader.hour, - minute : packetHeader.minute, - second : packetHeader.second + year: packetHeader.year, + month: packetHeader.month - 1, // moment uses 0 indexed months + date: packetHeader.day, + hour: packetHeader.hour, + minute: packetHeader.minute, + second: packetHeader.second, }); const ph = new PacketHeader(); @@ -308,7 +317,7 @@ function Packet(options) { return cb(null, ph); }; - this.getPacketHeaderBuffer = function(packetHeader) { + this.getPacketHeaderBuffer = function (packetHeader) { let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); @@ -322,7 +331,10 @@ function Packet(options) { buffer.writeUInt16LE(packetHeader.baud, 16); buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); - buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); + buffer.writeUInt16LE( + -1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, + 20 + ); buffer.writeUInt16LE(packetHeader.destNet, 22); buffer.writeUInt8(packetHeader.prodCodeLo, 24); buffer.writeUInt8(packetHeader.prodRevHi, 25); @@ -346,7 +358,7 @@ function Packet(options) { return buffer; }; - this.writePacketHeader = function(packetHeader, ws) { + this.writePacketHeader = function (packetHeader, ws) { let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE); buffer.writeUInt16LE(packetHeader.origNode, 0); @@ -360,7 +372,10 @@ function Packet(options) { buffer.writeUInt16LE(packetHeader.baud, 16); buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18); - buffer.writeUInt16LE(-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, 20); + buffer.writeUInt16LE( + -1 === packetHeader.origNet ? 0xffff : packetHeader.origNet, + 20 + ); buffer.writeUInt16LE(packetHeader.destNet, 22); buffer.writeUInt8(packetHeader.prodCodeLo, 24); buffer.writeUInt8(packetHeader.prodRevHi, 25); @@ -386,7 +401,7 @@ function Packet(options) { return buffer.length; }; - this.processMessageBody = function(messageBodyBuffer, cb) { + this.processMessageBody = function (messageBodyBuffer, cb) { // // From FTS-0001.16: // "Message text is unbounded and null terminated (note exception below). @@ -409,9 +424,9 @@ function Packet(options) { // decoding occurs // let messageBodyData = { - message : [], - kludgeLines : {}, // KLUDGE:[value1, value2, ...] map - seenBy : [], + message: [], + kludgeLines: {}, // KLUDGE:[value1, value2, ...] map + seenBy: [], }; function addKludgeLine(line) { @@ -421,21 +436,21 @@ function Packet(options) { // let key = line.substr(0, 4).trim(); let value; - if( ['INTL', 'TOPT', 'FMPT', 'Via' ].includes(key)) { + 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(); + 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] ]; + if (messageBodyData.kludgeLines[key]) { + if (!_.isArray(messageBodyData.kludgeLines[key])) { + messageBodyData.kludgeLines[key] = [messageBodyData.kludgeLines[key]]; } messageBodyData.kludgeLines[key].push(value); } else { @@ -451,19 +466,34 @@ function Packet(options) { // :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'); + 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 } - return callback(null); // failure to read SAUCE is OK - }); + ); } else { callback(null); } @@ -483,29 +513,38 @@ function Packet(options) { // Also according to the spec, the deprecated "CHARSET" value may be used // :TODO: Look into CHARSET more - should we bother supporting it? // :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam - const FTN_CHRS_PREFIX = Buffer.from( [ 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20 ] ); // "\x01CHRS:" - const FTN_CHRS_SUFFIX = Buffer.from( [ 0x0d ] ); + const FTN_CHRS_PREFIX = Buffer.from([ + 0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20, + ]); // "\x01CHRS:" + const FTN_CHRS_SUFFIX = Buffer.from([0x0d]); let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX); - if(chrsPrefixIndex < 0) { + if (chrsPrefixIndex < 0) { return callback(null); } chrsPrefixIndex += FTN_CHRS_PREFIX.length; - const chrsEndIndex = messageBodyBuffer.indexOf(FTN_CHRS_SUFFIX, chrsPrefixIndex); - if(chrsEndIndex < 0) { + const chrsEndIndex = messageBodyBuffer.indexOf( + FTN_CHRS_SUFFIX, + chrsPrefixIndex + ); + if (chrsEndIndex < 0) { return callback(null); } - let chrsContent = messageBodyBuffer.slice(chrsPrefixIndex, chrsEndIndex); - if(0 === chrsContent.length) { + 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) { + const chrsEncoding = + ftn.getEncodingFromCharacterSetIdentifier(chrsContent); + if (chrsEncoding) { encoding = chrsEncoding; } return callback(null); @@ -518,44 +557,54 @@ function Packet(options) { let decoded; try { decoded = iconv.decode(messageBodyBuffer, encoding); - } catch(e) { - Log.debug( { encoding : encoding, error : e.toString() }, 'Error decoding. Falling back to ASCII'); + } 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) { + if (0 === line.length) { messageBodyData.message.push(''); return; } - if(line.startsWith('AREA:')) { - messageBodyData.area = line.substring(line.indexOf(':') + 1).trim(); - } else if(line.startsWith('--- ')) { + if (line.startsWith('AREA:')) { + messageBodyData.area = line + .substring(line.indexOf(':') + 1) + .trim(); + } else if (line.startsWith('--- ')) { // Tear Lines are tracked allowing for specialized display/etc. messageBodyData.tearLine = line; - } else if(/^[ ]{1,2}\* Origin: /.test(line)) { // To spec is " * Origin: ..." + } else if (/^[ ]{1,2}\* Origin: /.test(line)) { + // To spec is " * Origin: ..." messageBodyData.originLine = line; - endOfMessage = true; // Anything past origin is not part of the message body - } else if(line.startsWith('SEEN-BY:')) { - endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body - messageBodyData.seenBy.push(line.substring(line.indexOf(':') + 1).trim()); - } else if(FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) { - if('PATH:' === line.slice(1, 6)) { - endOfMessage = true; // Anything pats the first PATH is not part of the message body + endOfMessage = true; // Anything 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) { + } else if (!endOfMessage) { // regular ol' message line messageBodyData.message.push(line); } }); return callback(null); - } + }, ], () => { messageBodyData.message = messageBodyData.message.join('\n'); @@ -564,14 +613,17 @@ function Packet(options) { ); }; - this.parsePacketMessages = function(header, packetBuffer, iterator, cb) { + 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) { + if (packetBuffer.length < 3) { const peek = packetBuffer.slice(0, 2); - if(peek.equals(Buffer.from([ 0x00 ])) || peek.equals(Buffer.from( [ 0x00, 0x00 ]))) { + if ( + peek.equals(Buffer.from([0x00])) || + peek.equals(Buffer.from([0x00, 0x00])) + ) { // end marker - no more messages return cb(null); } @@ -581,12 +633,14 @@ function Packet(options) { let msgData; try { msgData = MessageHeaderParser.parse(packetBuffer); - } catch(e) { + } 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}`) + ); } // @@ -603,21 +657,37 @@ function Packet(options) { // much more complex so we'll look for encoding kludges, detection, etc. // later on. // - if(msgData.modDateTime.length != 20) { - return cb(Errors.Invalid(`FTN packet DateTime field must be 20 bytes (got ${msgData.modDateTime.length})`)); + if (msgData.modDateTime.length != 20) { + return cb( + Errors.Invalid( + `FTN packet DateTime field must be 20 bytes (got ${msgData.modDateTime.length})` + ) + ); } - if(msgData.toUserName.length > 36) { - return cb(Errors.Invalid(`FTN packet toUserName field must be 36 bytes max (got ${msgData.toUserName.length})`)); + if (msgData.toUserName.length > 36) { + return cb( + Errors.Invalid( + `FTN packet toUserName field must be 36 bytes max (got ${msgData.toUserName.length})` + ) + ); } - if(msgData.fromUserName.length > 36) { - return cb(Errors.Invalid(`FTN packet fromUserName field must be 36 bytes max (got ${msgData.fromUserName.length})`)); + if (msgData.fromUserName.length > 36) { + return cb( + Errors.Invalid( + `FTN packet fromUserName field must be 36 bytes max (got ${msgData.fromUserName.length})` + ) + ); } - if(msgData.subject.length > 72) { - return cb(Errors.Invalid(`FTN packet subject field must be 72 bytes max (got ${msgData.subject.length})`)); + if (msgData.subject.length > 72) { + return cb( + Errors.Invalid( + `FTN packet subject field must be 72 bytes max (got ${msgData.subject.length})` + ) + ); } // Arrays of CP437 bytes -> String - [ 'modDateTime', 'toUserName', 'fromUserName', 'subject' ].forEach(k => { + ['modDateTime', 'toUserName', 'fromUserName', 'subject'].forEach(k => { msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437'); }); @@ -626,53 +696,53 @@ function Packet(options) { // 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), + const msg = new Message({ + toUserName: msgData.toUserName, + fromUserName: msgData.fromUserName, + subject: msgData.subject, + modTimestamp: ftn.getDateFromFtnDateTime(msgData.modDateTime), }); // :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further) msg.meta.FtnProperty = { - ftn_orig_node : header.origNode, - ftn_dest_node : header.destNode, - ftn_orig_network : header.origNet, - ftn_dest_network : header.destNet, + ftn_orig_node: header.origNode, + ftn_dest_node: header.destNode, + ftn_orig_network: header.origNet, + ftn_dest_network: header.destNet, - ftn_attr_flags : msgData.ftn_attr_flags, - ftn_cost : msgData.ftn_cost, + ftn_attr_flags: msgData.ftn_attr_flags, + ftn_cost: msgData.ftn_cost, - ftn_msg_orig_node : msgData.ftn_msg_orig_node, - ftn_msg_dest_node : msgData.ftn_msg_dest_node, - ftn_msg_orig_net : msgData.ftn_msg_orig_net, - ftn_msg_dest_net : msgData.ftn_msg_dest_net, + ftn_msg_orig_node: msgData.ftn_msg_orig_node, + ftn_msg_dest_node: msgData.ftn_msg_dest_node, + ftn_msg_orig_net: msgData.ftn_msg_orig_net, + ftn_msg_dest_net: msgData.ftn_msg_dest_net, }; self.processMessageBody(msgData.message, messageBodyData => { - msg.message = messageBodyData.message; - msg.meta.FtnKludge = messageBodyData.kludgeLines; + msg.message = messageBodyData.message; + msg.meta.FtnKludge = messageBodyData.kludgeLines; - if(messageBodyData.tearLine) { + if (messageBodyData.tearLine) { msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine; - if(self.options.keepTearAndOrigin) { + if (self.options.keepTearAndOrigin) { msg.message += `\r\n${messageBodyData.tearLine}\r\n`; } } - if(messageBodyData.seenBy.length > 0) { + if (messageBodyData.seenBy.length > 0) { msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy; } - if(messageBodyData.area) { + if (messageBodyData.area) { msg.meta.FtnProperty.ftn_area = messageBodyData.area; } - if(messageBodyData.originLine) { + if (messageBodyData.originLine) { msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; - if(self.options.keepTearAndOrigin) { + if (self.options.keepTearAndOrigin) { msg.message += `${messageBodyData.originLine}\r\n`; } } @@ -692,11 +762,11 @@ function Packet(options) { // - Positive offsets must not (to spec) proceed with '+', but // we'll allow it. // - const [, sign, hours, minutes ] = tzMatch; + const [, sign, hours, minutes] = tzMatch; // convert to a [+|-]hh:mm format. // example: 1300 -> +13:00 - const utcOffset = `${sign||'+'}${hours}:${minutes}`; + const utcOffset = `${sign || '+'}${hours}:${minutes}`; // finally, update our modTimestamp msg.modTimestamp = msg.modTimestamp.utcOffset(utcOffset); @@ -704,17 +774,21 @@ function Packet(options) { // :TODO: Parser should give is this info: const bytesRead = - 14 + // fixed header size - msgData.modDateTime.length + 1 + // +1 = NULL - msgData.toUserName.length + 1 + // +1 = NULL - msgData.fromUserName.length + 1 + // +1 = NULL - msgData.subject.length + 1 + // +1 = NULL - msgData.message.length; // includes NULL + 14 + // fixed header size + msgData.modDateTime.length + + 1 + // +1 = NULL + msgData.toUserName.length + + 1 + // +1 = NULL + msgData.fromUserName.length + + 1 + // +1 = NULL + msgData.subject.length + + 1 + // +1 = NULL + msgData.message.length; // includes NULL const nextBuf = packetBuffer.slice(bytesRead); - if(nextBuf.length > 0) { - const next = function(e) { - if(e) { + if (nextBuf.length > 0) { + const next = function (e) { + if (e) { cb(e); } else { self.parsePacketMessages(header, nextBuf, iterator, cb); @@ -728,7 +802,7 @@ function Packet(options) { }); }; - this.sanatizeFtnProperties = function(message) { + this.sanatizeFtnProperties = function (message) { [ Message.FtnPropertyNames.FtnOrigNode, Message.FtnPropertyNames.FtnDestNode, @@ -745,19 +819,24 @@ function Packet(options) { 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; + ].forEach(propName => { + if (message.meta.FtnProperty[propName]) { + message.meta.FtnProperty[propName] = + parseInt(message.meta.FtnProperty[propName]) || 0; } }); }; - this.writeMessageHeader = function(message, buf) { + 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); @@ -767,18 +846,19 @@ function Packet(options) { 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'); + const dateTimeBuffer = Buffer.from( + ftn.getDateTimeString(message.modTimestamp) + '\0' + ); dateTimeBuffer.copy(buf, 14); }; - this.getMessageEntryBuffer = function(message, options, cb) { - - function getAppendMeta(k, m, sepChar=':') { + this.getMessageEntryBuffer = function (message, options, cb) { + function getAppendMeta(k, m, sepChar = ':') { let append = ''; - if(m) { + if (m) { let a = m; - if(!_.isArray(a)) { - a = [ a ]; + if (!_.isArray(a)) { + a = [a]; } a.forEach(v => { append += `${k}${sepChar} ${v}\r`; @@ -796,9 +876,18 @@ function Packet(options) { // // To, from, and subject must be NULL term'd and have max lengths as per spec. // - const toUserNameBuf = strUtil.stringToNullTermBuffer(message.toUserName, { encoding : 'cp437', maxBufLen : 36 } ); - const fromUserNameBuf = strUtil.stringToNullTermBuffer(message.fromUserName, { encoding : 'cp437', maxBufLen : 36 } ); - const subjectBuf = strUtil.stringToNullTermBuffer(message.subject, { encoding : 'cp437', maxBufLen : 72 } ); + const toUserNameBuf = strUtil.stringToNullTermBuffer( + message.toUserName, + { encoding: 'cp437', maxBufLen: 36 } + ); + const fromUserNameBuf = strUtil.stringToNullTermBuffer( + message.fromUserName, + { encoding: 'cp437', maxBufLen: 36 } + ); + const subjectBuf = strUtil.stringToNullTermBuffer(message.subject, { + encoding: 'cp437', + maxBufLen: 72, + }); // // message: unbound length, NULL term'd @@ -813,64 +902,109 @@ function Packet(options) { // 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) + 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 + 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 + 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]); + default: + msgBody += getAppendMeta( + `\x01${k}`, + message.meta.FtnKludge[k] + ); break; } }); - return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody); + 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); + 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, + cols: 80, + rows: 'auto', + forceLineTerm: true, + exportMode: true, }, (err, preppedMsg) => { - return callback(null, basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, preppedMsg || message.message); + return callback( + null, + basicHeader, + toUserNameBuf, + fromUserNameBuf, + subjectBuf, + msgBody, + preppedMsg || message.message + ); } ); }, - function addMessageBody(basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, msgBody, preppedMsg, callback) { + 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) { + 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) { + if (message.meta.FtnProperty.ftn_origin) { msgBody += `${message.meta.FtnProperty.ftn_origin}\r`; } @@ -878,27 +1012,30 @@ function Packet(options) { // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // SEEN-BY and PATH should be the last lines of a message // - msgBody += getAppendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) + msgBody += getAppendMeta( + 'SEEN-BY', + message.meta.FtnProperty.ftn_seen_by + ); // note: no ^A (0x01) msgBody += getAppendMeta('\x01PATH', message.meta.FtnKludge['PATH']); let msgBodyEncoded; try { msgBodyEncoded = iconv.encode(msgBody + '\0', options.encoding); - } catch(e) { + } catch (e) { msgBodyEncoded = iconv.encode(msgBody + '\0', 'ascii'); } return callback( null, - Buffer.concat( [ + Buffer.concat([ basicHeader, toUserNameBuf, fromUserNameBuf, subjectBuf, - msgBodyEncoded + msgBodyEncoded, ]) ); - } + }, ], (err, msgEntryBuffer) => { return cb(err, msgEntryBuffer); @@ -906,7 +1043,7 @@ function Packet(options) { ); }; - this.writeMessage = function(message, ws, options) { + this.writeMessage = function (message, ws, options) { const basicHeader = Buffer.alloc(34); self.writeMessageHeader(message, basicHeader); @@ -915,16 +1052,16 @@ function Packet(options) { // toUserName & fromUserName: up to 36 bytes in length, NULL term'd // :TODO: DRY... let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); // subject: up to 72 bytes in length, NULL term'd encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72); - encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd + encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd ws.write(encBuf); // @@ -936,11 +1073,11 @@ function Packet(options) { // :TODO: Put this in it's own method let msgBody = ''; - function appendMeta(k, m, sepChar=':') { - if(m) { + function appendMeta(k, m, sepChar = ':') { + if (m) { let a = m; - if(!_.isArray(a)) { - a = [ a ]; + if (!_.isArray(a)) { + a = [a]; } a.forEach(v => { msgBody += `${k}${sepChar} ${v}\r`; @@ -953,20 +1090,25 @@ function Packet(options) { // 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) + 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 + 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; } }); @@ -976,14 +1118,14 @@ function Packet(options) { // 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) { + 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) { + if (message.meta.FtnProperty.ftn_origin) { msgBody += `${message.meta.FtnProperty.ftn_origin}\r`; } @@ -991,7 +1133,7 @@ function Packet(options) { // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 // SEEN-BY and PATH should be the last lines of a message // - appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) + appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); // note: no ^A (0x01) appendMeta('\x01PATH', message.meta.FtnKludge['PATH']); @@ -1000,16 +1142,16 @@ function Packet(options) { ws.write(iconv.encode(msgBody + '\0', options.encoding)); }; - this.parsePacketBuffer = function(packetBuffer, iterator, cb) { + this.parsePacketBuffer = function (packetBuffer, iterator, cb) { async.waterfall( [ function processHeader(callback) { self.parsePacketHeader(packetBuffer, (err, header) => { - if(err) { + if (err) { return callback(err); } - const next = function(e) { + const next = function (e) { return callback(e, header); }; @@ -1021,10 +1163,11 @@ function Packet(options) { header, packetBuffer.slice(FTN_PACKET_HEADER_SIZE), iterator, - callback); - } + callback + ); + }, ], - cb // complete + cb // complete ); }; } @@ -1037,32 +1180,32 @@ 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) { +Packet.prototype.read = function (pathOrBuffer, iterator, cb) { var self = this; async.series( [ function getBufferIfPath(callback) { - if(_.isString(pathOrBuffer)) { + if (_.isString(pathOrBuffer)) { fs.readFile(pathOrBuffer, (err, data) => { pathOrBuffer = data; callback(err); @@ -1075,7 +1218,7 @@ Packet.prototype.read = function(pathOrBuffer, iterator, cb) { self.parsePacketBuffer(pathOrBuffer, iterator, err => { callback(err); }); - } + }, ], err => { cb(err); @@ -1083,30 +1226,30 @@ Packet.prototype.read = function(pathOrBuffer, iterator, cb) { ); }; -Packet.prototype.writeHeader = function(ws, packetHeader) { +Packet.prototype.writeHeader = function (ws, packetHeader) { return this.writePacketHeader(packetHeader, ws); }; -Packet.prototype.writeMessageEntry = function(ws, msgEntry) { +Packet.prototype.writeMessageEntry = function (ws, msgEntry) { ws.write(msgEntry); return msgEntry.length; }; -Packet.prototype.writeTerminator = function(ws) { +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 + ws.write(Buffer.from([0x00, 0x00])); // final extra null term return 2; }; -Packet.prototype.writeStream = function(ws, messages, options) { - if(!_.isBoolean(options.terminatePacket)) { +Packet.prototype.writeStream = function (ws, messages, options) { + if (!_.isBoolean(options.terminatePacket)) { options.terminatePacket = true; } - if(_.isObject(options.packetHeader)) { + if (_.isObject(options.packetHeader)) { this.writePacketHeader(options.packetHeader, ws); } @@ -1116,21 +1259,21 @@ Packet.prototype.writeStream = function(ws, messages, options) { 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 ]; +Packet.prototype.write = function (path, packetHeader, messages, options) { + 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) + Object.assign({ packetHeader: packetHeader, terminatePacket: true }, options) ); }; diff --git a/core/ftn_util.js b/core/ftn_util.js index a788f2c7..25533381 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -1,40 +1,40 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').get; -const Address = require('./ftn_address.js'); -const FNV1a = require('./fnv1a.js'); +const Config = require('./config.js').get; +const Address = require('./ftn_address.js'); +const FNV1a = require('./fnv1a.js'); const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion; -const _ = require('lodash'); -const iconv = require('iconv-lite'); -const moment = require('moment'); -const os = require('os'); +const _ = require('lodash'); +const iconv = require('iconv-lite'); +const moment = require('moment'); +const os = require('os'); -const packageJson = require('../package.json'); +const packageJson = require('../package.json'); // :TODO: Remove "Ftn" from most of these -- it's implied in the module -exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; -exports.getMessageSerialNumber = getMessageSerialNumber; -exports.getDateFromFtnDateTime = getDateFromFtnDateTime; -exports.getDateTimeString = getDateTimeString; +exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; +exports.getMessageSerialNumber = getMessageSerialNumber; +exports.getDateFromFtnDateTime = getDateFromFtnDateTime; +exports.getDateTimeString = getDateTimeString; -exports.getMessageIdentifier = getMessageIdentifier; -exports.getProductIdentifier = getProductIdentifier; -exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; -exports.getOrigin = getOrigin; -exports.getTearLine = getTearLine; -exports.getVia = getVia; -exports.getIntl = getIntl; -exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; +exports.getMessageIdentifier = getMessageIdentifier; +exports.getProductIdentifier = getProductIdentifier; +exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; +exports.getOrigin = getOrigin; +exports.getTearLine = getTearLine; +exports.getVia = getVia; +exports.getIntl = getIntl; +exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList; -exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; -exports.getUpdatedPathEntries = getUpdatedPathEntries; +exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; +exports.getUpdatedPathEntries = getUpdatedPathEntries; -exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding; -exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier; +exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding; +exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier; -exports.getQuotePrefix = getQuotePrefix; +exports.getQuotePrefix = getQuotePrefix; // // Namespace for RFC-4122 name based UUIDs generated from @@ -45,9 +45,9 @@ 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) { + 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; @@ -65,7 +65,7 @@ function getDateFromFtnDateTime(dateTime) { // "27 Feb 15 00:00:03" // // :TODO: Use moment.js here - return moment(Date.parse(dateTime)); // Date.parse() allows funky formats + return moment(Date.parse(dateTime)); // Date.parse() allows funky formats } function getDateTimeString(m) { @@ -85,7 +85,7 @@ function getDateTimeString(m) { // MM = "00" | .. | "59" // SS = "00" | .. | "59" // - if(!moment.isMoment(m)) { + if (!moment.isMoment(m)) { m = moment(m); } @@ -93,8 +93,8 @@ function getDateTimeString(m) { } function getMessageSerialNumber(messageId) { - const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); - const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); + const 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,10 +143,13 @@ function getMessageSerialNumber(messageId) { // 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)}` - ; + return isNetMail + ? `${addrStr} ${getMessageSerialNumber(message.messageId)}` + : `${ + message.messageId + }.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber( + message.messageId + )}`; } // @@ -158,7 +161,7 @@ function getMessageIdentifier(message, address, isNetMail = false) { // function getProductIdentifier() { const version = getCleanEnigmaVersion(); - const nodeVer = process.version.substr(1); // remove 'v' prefix + const nodeVer = process.version.substr(1); // remove 'v' prefix return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } @@ -181,9 +184,12 @@ function getQuotePrefix(name) { let initials; const parts = name.split(' '); - if(parts.length > 1) { + if (parts.length > 1) { // First & Last initials - (Bryan Ashby -> BA) - initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase(); + 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)); @@ -198,17 +204,19 @@ function getQuotePrefix(name) { // function getOrigin(address) { const config = Config(); - const origin = _.has(config, 'messageNetworks.originLine') ? - config.messageNetworks.originLine : - config.general.boardName; + const origin = _.has(config, 'messageNetworks.originLine') + ? config.messageNetworks.originLine + : config.general.boardName; 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})`; } // @@ -222,9 +230,9 @@ function getVia(address) { ^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}`; } @@ -247,10 +255,10 @@ function getAbbreviatedNetNodeList(netNodes) { let abbrList = ''; let currNet; netNodes.forEach(netNode => { - if(_.isString(netNode)) { + if (_.isString(netNode)) { netNode = Address.fromString(netNode); } - if(currNet !== netNode.net) { + if (currNet !== netNode.net) { abbrList += `${netNode.net}/`; currNet = netNode.net; } @@ -268,12 +276,12 @@ function parseAbbreviatedNetNodeList(netNodes) { let net; let m; let results = []; - while(null !== (m = re.exec(netNodes))) { - if(m[1] && m[2]) { + 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]) } )); + results.push(new Address({ net: net, node: parseInt(m[2]) })); + } else if (net) { + results.push(new Address({ net: net, node: parseInt(m[3]) })); } } @@ -316,11 +324,11 @@ function getUpdatedSeenByEntries(existingEntries, additions) { programs." */ existingEntries = existingEntries || []; - if(!_.isArray(existingEntries)) { - existingEntries = [ existingEntries ]; + if (!_.isArray(existingEntries)) { + existingEntries = [existingEntries]; } - if(!_.isString(additions)) { + if (!_.isString(additions)) { additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); } @@ -338,12 +346,13 @@ function getUpdatedPathEntries(existingEntries, localAddress) { // :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 ]; + if (!_.isArray(existingEntries)) { + existingEntries = [existingEntries]; } - existingEntries.push(getAbbreviatedNetNodeList( - parseAbbreviatedNetNodeList(localAddress))); + existingEntries.push( + getAbbreviatedNetNodeList(parseAbbreviatedNetNodeList(localAddress)) + ); return existingEntries; } @@ -354,69 +363,68 @@ function getUpdatedPathEntries(existingEntries, localAddress) { // const ENCODING_TO_FTS_5003_001_CHARS = { // level 1 - generally should not be used - ascii : [ 'ASCII', 1 ], - 'us-ascii' : [ 'ASCII', 1 ], + ascii: ['ASCII', 1], + 'us-ascii': ['ASCII', 1], // level 2 - 8 bit, ASCII based - cp437 : [ 'CP437', 2 ], - cp850 : [ 'CP850', 2 ], + cp437: ['CP437', 2], + cp850: ['CP850', 2], // level 3 - reserved // level 4 - utf8 : [ 'UTF-8', 4 ], - 'utf-8' : [ 'UTF-8', 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 CHRSToEncodingTable = { - Level1 : { - 'ASCII' : 'ascii', // ISO-646-1 - 'DUTCH' : 'ascii', // ISO-646 - 'FINNISH' : 'ascii', // ISO-646-10 - 'FRENCH' : 'ascii', // ISO-646 - 'CANADIAN' : 'ascii', // ISO-646 - 'GERMAN' : 'ascii', // ISO-646 - 'ITALIAN' : 'ascii', // ISO-646 - 'NORWEIG' : 'ascii', // ISO-646 - 'PORTU' : 'ascii', // ISO-646 - 'SPANISH' : 'iso-656', - 'SWEDISH' : 'ascii', // ISO-646-10 - 'SWISS' : 'ascii', // ISO-646 - 'UK' : 'ascii', // ISO-646 - 'ISO-10' : 'ascii', // ISO-646-10 + Level1: { + ASCII: 'ascii', // ISO-646-1 + DUTCH: 'ascii', // ISO-646 + FINNISH: 'ascii', // ISO-646-10 + FRENCH: 'ascii', // ISO-646 + CANADIAN: 'ascii', // ISO-646 + GERMAN: 'ascii', // ISO-646 + ITALIAN: 'ascii', // ISO-646 + NORWEIG: 'ascii', // ISO-646 + PORTU: 'ascii', // ISO-646 + SPANISH: 'iso-656', + SWEDISH: 'ascii', // ISO-646-10 + SWISS: 'ascii', // ISO-646 + UK: 'ascii', // ISO-646 + 'ISO-10': 'ascii', // ISO-646-10 }, - Level2 : { - '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', + Level2: { + 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', }, - Level4 : { - 'UTF-8' : 'utf8', + Level4: { + 'UTF-8': 'utf8', }, - DeprecatedMisc : { - 'IBMPC' : 'cp1250', // :TODO: validate - '+7_FIDO' : 'cp866', - '+7' : 'cp866', - 'MAC' : 'macroman', // :TODO: validate - } + DeprecatedMisc: { + IBMPC: 'cp1250', // :TODO: validate + '+7_FIDO': 'cp866', + '+7': 'cp866', + MAC: 'macroman', // :TODO: validate + }, }; // Given 1:N CHRS kludge IDs, try to pick the best encoding we can @@ -424,7 +432,7 @@ const CHRSToEncodingTable = { // http://www.unicode.org/L2/L1999/99325-N.htm function getEncodingFromCharacterSetIdentifier(chrs) { if (!Array.isArray(chrs)) { - chrs = [ chrs ]; + chrs = [chrs]; } const encLevel = (ident, table, level) => { @@ -448,7 +456,7 @@ function getEncodingFromCharacterSetIdentifier(chrs) { } }); - mapping.sort( (l, r) => { + mapping.sort((l, r) => { return l.level - r.level; }); diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 212b4d15..b3bb2415 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -15,497 +15,513 @@ const _ = require('lodash'); exports.FullMenuView = FullMenuView; function FullMenuView(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); + // Initialize paging + this.pages = []; + this.currentPage = 0; + this.initDefaultWidth(); - // Initialize paging - this.pages = []; - this.currentPage = 0; - - this.initDefaultWidth(); - - // 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.autoAdjustHeightIfEnabled = () => { - if (this.autoAdjustHeight) { - this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); - this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); + // 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.positionCacheExpired = true; - }; - - this.autoAdjustHeightIfEnabled(); - - this.clearPage = () => { - let width = this.dimens.width; - if (this.oldDimens) { - if (this.oldDimens.width > width) { - width = this.oldDimens.width; - } - delete this.oldDimens; - } - - for (let i = 0; i < this.dimens.height; i++) { - const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; - this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); - } - } - - this.cachePositions = () => { - if (this.positionCacheExpired) { - // first, clear the page - this.clearPage(); - - - this.autoAdjustHeightIfEnabled(); - - this.pages = []; // reset - - // Calculate number of items visible per column - this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); - // handle case where one can fit at the end - if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { - this.itemsPerRow++; - } - - // Final check to make sure we don't try to display more than we have - if (this.itemsPerRow > this.items.length) { - this.itemsPerRow = this.items.length; - } - - let col = this.position.col; - let row = this.position.row; - const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar); - - let itemInRow = 0; - let itemInCol = 0; - - let pageStart = 0; - - for (let i = 0; i < this.items.length; ++i) { - itemInRow++; - this.items[i].row = row; - this.items[i].col = col; - this.items[i].itemInRow = itemInRow; - - row += this.itemSpacing + 1; - - // have to calculate the max length on the last entry - if (i == this.items.length - 1) { - let maxLength = 0; - for (let j = 0; j < this.itemsPerRow; j++) { - if (this.items[i - j].col != this.items[i].col) { - break; - } - const itemLength = this.items[i - j].text.length; - if (itemLength > maxLength) { - maxLength = itemLength; - } - } - - // set length on each item in the column - for (let j = 0; j < this.itemsPerRow; j++) { - if (this.items[i - j].col != this.items[i].col) { - break; - } - this.items[i - j].fixedLength = maxLength; - } - - - // Check if we have room for this column - // skip for column 0, we need at least one - if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { - // save previous page - this.pages.push({ start: pageStart, end: i - itemInRow }); - - // fix the last column processed - for (let j = 0; j < this.itemsPerRow; j++) { - if (this.items[i - j].col != col) { - break; - } - this.items[i - j].col = this.position.col; - pageStart = i - j; - } - - } - - // Since this is the last page, save the current page as well - this.pages.push({ start: pageStart, end: i }); - - } - // also handle going to next column - else if (itemInRow == this.itemsPerRow) { - itemInRow = 0; - - // restart row for next column - row = this.position.row; - let maxLength = 0; - for (let j = 0; j < this.itemsPerRow; j++) { - // TODO: handle complex items - let itemLength = this.items[i - j].text.length; - if (itemLength > maxLength) { - maxLength = itemLength; - } - } - - // set length on each item in the column - for (let j = 0; j < this.itemsPerRow; j++) { - this.items[i - j].fixedLength = maxLength; - } - - // Check if we have room for this column in the current page - // skip for first column, we need at least one - if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { - // save previous page - this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); - - // restart page start for next page - pageStart = i - this.itemsPerRow + 1; - - // reset - col = this.position.col; - itemInRow = 0; - - // fix the last column processed - for (let j = 0; j < this.itemsPerRow; j++) { - this.items[i - j].col = col; - } - - } - - // increment the column - col += maxLength + spacer.length; - itemInCol++; + this.autoAdjustHeightIfEnabled = () => { + if (this.autoAdjustHeight) { + this.dimens.height = + this.items.length * (this.itemSpacing + 1) - this.itemSpacing; + this.dimens.height = Math.min( + this.dimens.height, + this.client.term.termHeight - this.position.row + ); } + this.positionCacheExpired = true; + }; - // Set the current page if the current item is focused. - if (this.focusedItemIndex === i) { - this.currentPage = this.pages.length; + this.autoAdjustHeightIfEnabled(); + + this.clearPage = () => { + let width = this.dimens.width; + if (this.oldDimens) { + if (this.oldDimens.width > width) { + width = this.oldDimens.width; + } + delete this.oldDimens; } - } - } - this.positionCacheExpired = false; - }; + for (let i = 0; i < this.dimens.height; i++) { + const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + this.client.term.write( + `${ansi.goto( + this.position.row + i, + this.position.col + )}${this.getSGR()}${text}` + ); + } + }; - this.drawItem = (index) => { - const item = this.items[index]; - if (!item) { - return; - } + this.cachePositions = () => { + if (this.positionCacheExpired) { + // first, clear the page + this.clearPage(); - const cached = this.getRenderCacheItem(index, item.focused); - if (cached) { - return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); - } + this.autoAdjustHeightIfEnabled(); - let text; - let sgr; - if (item.focused && this.hasFocusItems()) { - const focusItem = this.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 === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); - } else { - text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle); - sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); - } + this.pages = []; // reset - let renderLength = strUtil.renderStringLength(text); - if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) { - text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; - } + // Calculate number of items visible per column + this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); + // handle case where one can fit at the end + if (this.dimens.height > this.itemsPerRow * (this.itemSpacing + 1)) { + this.itemsPerRow++; + } - let padLength = Math.min(item.fixedLength + 1, this.dimens.width); + // Final check to make sure we don't try to display more than we have + if (this.itemsPerRow > this.items.length) { + this.itemsPerRow = this.items.length; + } - text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; - this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); - this.setRenderCacheItem(index, text, item.focused); - }; + let col = this.position.col; + let row = this.position.row; + const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar); + + let itemInRow = 0; + let itemInCol = 0; + + let pageStart = 0; + + for (let i = 0; i < this.items.length; ++i) { + itemInRow++; + this.items[i].row = row; + this.items[i].col = col; + this.items[i].itemInRow = itemInRow; + + row += this.itemSpacing + 1; + + // have to calculate the max length on the last entry + if (i == this.items.length - 1) { + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + const itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (let j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + this.items[i - j].fixedLength = maxLength; + } + + // Check if we have room for this column + // skip for column 0, we need at least one + if (itemInCol != 0 && col + maxLength > this.dimens.width) { + // save previous page + this.pages.push({ start: pageStart, end: i - itemInRow }); + + // fix the last column processed + for (let j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != col) { + break; + } + this.items[i - j].col = this.position.col; + pageStart = i - j; + } + } + + // Since this is the last page, save the current page as well + this.pages.push({ start: pageStart, end: i }); + } + // also handle going to next column + else if (itemInRow == this.itemsPerRow) { + itemInRow = 0; + + // restart row for next column + row = this.position.row; + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { + // TODO: handle complex items + let itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (let j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].fixedLength = maxLength; + } + + // Check if we have room for this column in the current page + // skip for first column, we need at least one + if (itemInCol != 0 && col + maxLength > this.dimens.width) { + // save previous page + this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); + + // restart page start for next page + pageStart = i - this.itemsPerRow + 1; + + // reset + col = this.position.col; + itemInRow = 0; + + // fix the last column processed + for (let j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].col = col; + } + } + + // increment the column + col += maxLength + spacer.length; + itemInCol++; + } + + // Set the current page if the current item is focused. + if (this.focusedItemIndex === i) { + this.currentPage = this.pages.length; + } + } + } + + this.positionCacheExpired = false; + }; + + this.drawItem = index => { + const item = this.items[index]; + if (!item) { + return; + } + + const cached = this.getRenderCacheItem(index, item.focused); + if (cached) { + return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); + } + + let text; + let sgr; + if (item.focused && this.hasFocusItems()) { + const focusItem = this.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 === this.focusedItemIndex + ? this.getFocusSGR() + : this.getSGR(); + } else { + text = strUtil.stylizeString( + item.text, + item.focused ? this.focusTextStyle : this.textStyle + ); + sgr = index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR(); + } + + let renderLength = strUtil.renderStringLength(text); + if (this.hasTextOverflow() && item.col + renderLength > this.dimens.width) { + text = + strUtil.renderSubstr( + text, + 0, + this.dimens.width - (item.col + this.textOverflow.length) + ) + this.textOverflow; + } + + let padLength = Math.min(item.fixedLength + 1, this.dimens.width); + + text = `${sgr}${strUtil.pad( + text, + padLength, + this.fillChar, + this.justify + )}${this.getSGR()}`; + this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + this.setRenderCacheItem(index, text, item.focused); + }; } util.inherits(FullMenuView, MenuView); -FullMenuView.prototype.redraw = function() { - FullMenuView.super_.prototype.redraw.call(this); +FullMenuView.prototype.redraw = function () { + FullMenuView.super_.prototype.redraw.call(this); - this.cachePositions(); + this.cachePositions(); - if (this.items.length) { - for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) { - this.items[i].focused = this.focusedItemIndex === i; - this.drawItem(i); + if (this.items.length) { + for ( + let i = this.pages[this.currentPage].start; + i <= this.pages[this.currentPage].end; + ++i + ) { + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } } - } }; -FullMenuView.prototype.setHeight = function(height) { - this.oldDimens = Object.assign({}, this.dimens); - - FullMenuView.super_.prototype.setHeight.call(this, height); - - this.positionCacheExpired = true; - this.autoAdjustHeight = false; -}; - -FullMenuView.prototype.setWidth = function(width) { - this.oldDimens = Object.assign({}, this.dimens); - - FullMenuView.super_.prototype.setWidth.call(this, width); - - this.positionCacheExpired = true; -}; - -FullMenuView.prototype.setTextOverflow = function(overflow) { - FullMenuView.super_.prototype.setTextOverflow.call(this, overflow); - - this.positionCacheExpired = true; - -} - -FullMenuView.prototype.setPosition = function(pos) { - FullMenuView.super_.prototype.setPosition.call(this, pos); - - this.positionCacheExpired = true; -}; - -FullMenuView.prototype.setFocus = function(focused) { - FullMenuView.super_.prototype.setFocus.call(this, focused); - this.positionCacheExpired = true; - this.autoAdjustHeight = false; - - this.redraw(); -}; - -FullMenuView.prototype.setFocusItemIndex = function(index) { - FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex -}; - -FullMenuView.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('left', key.name)) { - this.focusPreviousColumn(); - } else if (this.isKeyMapped('right', key.name)) { - this.focusNextColumn(); - } 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(); - } - } - - FullMenuView.super_.prototype.onKeyPress.call(this, ch, key); -}; - -FullMenuView.prototype.getData = function() { - const item = this.getItem(this.focusedItemIndex); - return _.isString(item.data) ? item.data : this.focusedItemIndex; -}; - -FullMenuView.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) { +FullMenuView.prototype.setHeight = function (height) { this.oldDimens = Object.assign({}, this.dimens); - } - FullMenuView.super_.prototype.setItems.call(this, items); + FullMenuView.super_.prototype.setHeight.call(this, height); - this.positionCacheExpired = true; + this.positionCacheExpired = true; + this.autoAdjustHeight = false; }; -FullMenuView.prototype.removeItem = function(index) { - if (this.items && this.items.length) { +FullMenuView.prototype.setWidth = function (width) { this.oldDimens = Object.assign({}, this.dimens); - } - FullMenuView.super_.prototype.removeItem.call(this, index); - this.positionCacheExpired = true; + FullMenuView.super_.prototype.setWidth.call(this, width); + + this.positionCacheExpired = true; }; -FullMenuView.prototype.focusNext = function() { - if (this.items.length - 1 === this.focusedItemIndex) { - this.clearPage(); - this.focusedItemIndex = 0; - this.currentPage = 0; - } - else { - this.focusedItemIndex++; - if (this.focusedItemIndex > this.pages[this.currentPage].end) { - this.clearPage(); - this.currentPage++; - } - } +FullMenuView.prototype.setTextOverflow = function (overflow) { + FullMenuView.super_.prototype.setTextOverflow.call(this, overflow); - this.redraw(); - - FullMenuView.super_.prototype.focusNext.call(this); + this.positionCacheExpired = true; }; -FullMenuView.prototype.focusPrevious = function() { - if (0 === this.focusedItemIndex) { - this.clearPage(); - this.focusedItemIndex = this.items.length - 1; - this.currentPage = this.pages.length - 1; - } - else { - this.focusedItemIndex--; - if (this.focusedItemIndex < this.pages[this.currentPage].start) { - this.clearPage(); - this.currentPage--; - } - } +FullMenuView.prototype.setPosition = function (pos) { + FullMenuView.super_.prototype.setPosition.call(this, pos); - this.redraw(); - - FullMenuView.super_.prototype.focusPrevious.call(this); + this.positionCacheExpired = true; }; -FullMenuView.prototype.focusPreviousColumn = function() { +FullMenuView.prototype.setFocus = function (focused) { + FullMenuView.super_.prototype.setFocus.call(this, focused); + this.positionCacheExpired = true; + this.autoAdjustHeight = false; - const currentRow = this.items[this.focusedItemIndex].itemInRow; - this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; - if (this.focusedItemIndex < 0) { - this.clearPage(); - const lastItemRow = this.items[this.items.length - 1].itemInRow; - if (lastItemRow > currentRow) { - this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; - } - else { - // can't go to same column, so go to last item - this.focusedItemIndex = this.items.length - 1; - } - // set to last page - this.currentPage = this.pages.length - 1; - } - else { - if (this.focusedItemIndex < this.pages[this.currentPage].start) { - this.clearPage(); - this.currentPage--; - } - } - - this.redraw(); - - // TODO: This isn't specific to Previous, may want to replace in the future - FullMenuView.super_.prototype.focusPrevious.call(this); + this.redraw(); }; -FullMenuView.prototype.focusNextColumn = function() { +FullMenuView.prototype.setFocusItemIndex = function (index) { + FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex +}; - const currentRow = this.items[this.focusedItemIndex].itemInRow; - this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; - if (this.focusedItemIndex > this.items.length - 1) { - this.focusedItemIndex = currentRow - 1; - this.currentPage = 0; - this.clearPage(); - } - else if (this.focusedItemIndex > this.pages[this.currentPage].end) { +FullMenuView.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('left', key.name)) { + this.focusPreviousColumn(); + } else if (this.isKeyMapped('right', key.name)) { + this.focusNextColumn(); + } 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(); + } + } + + FullMenuView.super_.prototype.onKeyPress.call(this, ch, key); +}; + +FullMenuView.prototype.getData = function () { + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; +}; + +FullMenuView.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); + } + + FullMenuView.super_.prototype.setItems.call(this, items); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.removeItem = function (index) { + if (this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + FullMenuView.super_.prototype.removeItem.call(this, index); + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.focusNext = function () { + if (this.items.length - 1 === this.focusedItemIndex) { + this.clearPage(); + this.focusedItemIndex = 0; + this.currentPage = 0; + } else { + this.focusedItemIndex++; + if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } + } + + this.redraw(); + + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPrevious = function () { + if (0 === this.focusedItemIndex) { + this.clearPage(); + this.focusedItemIndex = this.items.length - 1; + this.currentPage = this.pages.length - 1; + } else { + this.focusedItemIndex--; + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); + this.currentPage--; + } + } + + this.redraw(); + + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusPreviousColumn = function () { + const currentRow = this.items[this.focusedItemIndex].itemInRow; + this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; + if (this.focusedItemIndex < 0) { + this.clearPage(); + const lastItemRow = this.items[this.items.length - 1].itemInRow; + if (lastItemRow > currentRow) { + this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; + } else { + // can't go to same column, so go to last item + this.focusedItemIndex = this.items.length - 1; + } + // set to last page + this.currentPage = this.pages.length - 1; + } else { + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); + this.currentPage--; + } + } + + this.redraw(); + + // TODO: This isn't specific to Previous, may want to replace in the future + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusNextColumn = function () { + const currentRow = this.items[this.focusedItemIndex].itemInRow; + this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; + if (this.focusedItemIndex > this.items.length - 1) { + this.focusedItemIndex = currentRow - 1; + this.currentPage = 0; + this.clearPage(); + } else if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } + + this.redraw(); + + // TODO: This isn't specific to Next, may want to replace in the future + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPreviousPageItem = function () { + // handle first page + if (this.currentPage == 0) { + // Do nothing, page up shouldn't go down on last page + return; + } + + this.currentPage--; + this.focusedItemIndex = this.pages[this.currentPage].start; this.clearPage(); + + this.redraw(); + + return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); +}; + +FullMenuView.prototype.focusNextPageItem = function () { + // handle last page + if (this.currentPage == this.pages.length - 1) { + // Do nothing, page up shouldn't go down on last page + return; + } + this.currentPage++; - } + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); - this.redraw(); + this.redraw(); - // TODO: This isn't specific to Next, may want to replace in the future - FullMenuView.super_.prototype.focusNext.call(this); + return FullMenuView.super_.prototype.focusNextPageItem.call(this); }; -FullMenuView.prototype.focusPreviousPageItem = function() { +FullMenuView.prototype.focusFirst = function () { + this.currentPage = 0; + this.focusedItemIndex = 0; + this.clearPage(); - // handle first page - if (this.currentPage == 0) { - // Do nothing, page up shouldn't go down on last page - return; - } - - this.currentPage--; - this.focusedItemIndex = this.pages[this.currentPage].start; - this.clearPage(); - - this.redraw(); - - return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); + this.redraw(); + return FullMenuView.super_.prototype.focusFirst.call(this); }; -FullMenuView.prototype.focusNextPageItem = function() { +FullMenuView.prototype.focusLast = function () { + this.currentPage = this.pages.length - 1; + this.focusedItemIndex = this.pages[this.currentPage].end; + this.clearPage(); - // handle last page - if (this.currentPage == this.pages.length - 1) { - // Do nothing, page up shouldn't go down on last page - return; - } - - this.currentPage++; - this.focusedItemIndex = this.pages[this.currentPage].start; - this.clearPage(); - - this.redraw(); - - return FullMenuView.super_.prototype.focusNextPageItem.call(this); + this.redraw(); + return FullMenuView.super_.prototype.focusLast.call(this); }; -FullMenuView.prototype.focusFirst = function() { +FullMenuView.prototype.setFocusItems = function (items) { + FullMenuView.super_.prototype.setFocusItems.call(this, items); - this.currentPage = 0; - this.focusedItemIndex = 0; - this.clearPage(); - - this.redraw(); - return FullMenuView.super_.prototype.focusFirst.call(this); + this.positionCacheExpired = true; }; -FullMenuView.prototype.focusLast = function() { +FullMenuView.prototype.setItemSpacing = function (itemSpacing) { + FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); - this.currentPage = this.pages.length - 1; - this.focusedItemIndex = this.pages[this.currentPage].end; - this.clearPage(); - - this.redraw(); - return FullMenuView.super_.prototype.focusLast.call(this); + this.positionCacheExpired = true; }; -FullMenuView.prototype.setFocusItems = function(items) { - FullMenuView.super_.prototype.setFocusItems.call(this, items); - - this.positionCacheExpired = true; +FullMenuView.prototype.setJustify = function (justify) { + FullMenuView.super_.prototype.setJustify.call(this, justify); + this.positionCacheExpired = true; }; -FullMenuView.prototype.setItemSpacing = function(itemSpacing) { - FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); +FullMenuView.prototype.setItemHorizSpacing = function (itemHorizSpacing) { + FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); - this.positionCacheExpired = true; -}; - -FullMenuView.prototype.setJustify = function(justify) { - FullMenuView.super_.prototype.setJustify.call(this, justify); - this.positionCacheExpired = true; -}; - - -FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); - - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; diff --git a/core/horizontal_menu_view.js b/core/horizontal_menu_view.js index 5c83eb16..dce01bb7 100644 --- a/core/horizontal_menu_view.js +++ b/core/horizontal_menu_view.js @@ -1,23 +1,23 @@ /* jslint node: true */ 'use strict'; -const MenuView = require('./menu_view.js').MenuView; -const strUtil = require('./string_util.js'); -const formatString = require('./string_format'); -const { pipeToAnsi } = require('./color_codes.js'); -const { goto } = require('./ansi_term.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const { pipeToAnsi } = require('./color_codes.js'); +const { goto } = require('./ansi_term.js'); -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); -exports.HorizontalMenuView = HorizontalMenuView; +exports.HorizontalMenuView = HorizontalMenuView; // :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView) function HorizontalMenuView(options) { - options.cursor = options.cursor || 'hide'; + options.cursor = options.cursor || 'hide'; - if(!_.isNumber(options.itemSpacing)) { + if (!_.isNumber(options.itemSpacing)) { options.itemSpacing = 1; } @@ -27,16 +27,16 @@ function HorizontalMenuView(options) { var self = this; - this.getSpacer = function() { + this.getSpacer = function () { return new Array(self.itemSpacing + 1).join(' '); }; - 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) { + for (var i = 0; i < self.items.length; ++i) { self.items[i].col = col; col += spacer.length + self.items[i].text.length + spacer.length; } @@ -45,75 +45,94 @@ function HorizontalMenuView(options) { this.positionCacheExpired = false; }; - this.drawItem = function(index) { + this.drawItem = function (index) { assert(!this.positionCacheExpired); const item = self.items[index]; - if(!item) { + if (!item) { return; } let text; let sgr; - if(item.focused && self.hasFocusItems()) { + 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 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 = 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')}` + `${goto(self.position.row, item.col)}${sgr}${strUtil.pad( + text, + drawWidth, + self.fillChar, + 'center' + )}` ); }; } require('util').inherits(HorizontalMenuView, MenuView); -HorizontalMenuView.prototype.setHeight = function(height) { +HorizontalMenuView.prototype.setHeight = function (height) { height = parseInt(height, 10); - assert(1 === height); // nothing else allowed here + assert(1 === height); // nothing else allowed here HorizontalMenuView.super_.prototype.setHeight(this, height); }; -HorizontalMenuView.prototype.redraw = function() { +HorizontalMenuView.prototype.redraw = function () { HorizontalMenuView.super_.prototype.redraw.call(this); this.cachePositions(); - for(var i = 0; i < this.items.length; ++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.prototype.setPosition = function (pos) { HorizontalMenuView.super_.prototype.setPosition.call(this, pos); this.positionCacheExpired = true; }; -HorizontalMenuView.prototype.setFocus = function(focused) { +HorizontalMenuView.prototype.setFocus = function (focused) { HorizontalMenuView.super_.prototype.setFocus.call(this, focused); this.redraw(); }; -HorizontalMenuView.prototype.setItems = function(items) { +HorizontalMenuView.prototype.setItems = function (items) { HorizontalMenuView.super_.prototype.setItems.call(this, items); this.positionCacheExpired = true; }; -HorizontalMenuView.prototype.focusNext = function() { - if(this.items.length - 1 === this.focusedItemIndex) { +HorizontalMenuView.prototype.focusNext = function () { + if (this.items.length - 1 === this.focusedItemIndex) { this.focusedItemIndex = 0; } else { this.focusedItemIndex++; @@ -125,9 +144,8 @@ HorizontalMenuView.prototype.focusNext = function() { HorizontalMenuView.super_.prototype.focusNext.call(this); }; -HorizontalMenuView.prototype.focusPrevious = function() { - - if(0 === this.focusedItemIndex) { +HorizontalMenuView.prototype.focusPrevious = function () { + if (0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; } else { this.focusedItemIndex--; @@ -139,11 +157,11 @@ HorizontalMenuView.prototype.focusPrevious = function() { HorizontalMenuView.super_.prototype.focusPrevious.call(this); }; -HorizontalMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('left', key.name)) { +HorizontalMenuView.prototype.onKeyPress = function (ch, key) { + if (key) { + if (this.isKeyMapped('left', key.name)) { this.focusPrevious(); - } else if(this.isKeyMapped('right', key.name)) { + } else if (this.isKeyMapped('right', key.name)) { this.focusNext(); } } @@ -151,7 +169,7 @@ HorizontalMenuView.prototype.onKeyPress = function(ch, key) { HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; -HorizontalMenuView.prototype.getData = function() { +HorizontalMenuView.prototype.getData = function () { 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 0bab0ad5..c7e871d0 100644 --- a/core/key_entry_view.js +++ b/core/key_entry_view.js @@ -1,12 +1,12 @@ /* jslint node: true */ 'use strict'; -const View = require('./view.js').View; -const valueWithDefault = require('./misc_util.js').valueWithDefault; -const isPrintable = require('./string_util.js').isPrintable; -const stylizeString = require('./string_util.js').stylizeString; +const View = require('./view.js').View; +const valueWithDefault = require('./misc_util.js').valueWithDefault; +const isPrintable = require('./string_util.js').isPrintable; +const stylizeString = require('./string_util.js').stylizeString; -const _ = require('lodash'); +const _ = require('lodash'); module.exports = class KeyEntryView extends View { constructor(options) { @@ -15,12 +15,12 @@ module.exports = class KeyEntryView extends View { super(options); - this.eatTabKey = options.eatTabKey || true; - this.caseInsensitive = options.caseInsensitive || true; + this.eatTabKey = options.eatTabKey || true; + this.caseInsensitive = options.caseInsensitive || true; - if(Array.isArray(options.keys)) { - if(this.caseInsensitive) { - this.keys = options.keys.map( k => k.toUpperCase() ); + if (Array.isArray(options.keys)) { + if (this.caseInsensitive) { + this.keys = options.keys.map(k => k.toUpperCase()); } else { this.keys = options.keys; } @@ -30,18 +30,22 @@ module.exports = class KeyEntryView extends View { onKeyPress(ch, key) { const drawKey = ch; - if(ch && this.caseInsensitive) { + if (ch && this.caseInsensitive) { ch = ch.toUpperCase(); } - if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { - this.redraw(); // sets position + 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; - if(key && 'tab' === key.name && !this.eatTabKey) { + if (key && 'tab' === key.name && !this.eatTabKey) { return this.emit('action', 'next', key); } @@ -50,21 +54,21 @@ module.exports = class KeyEntryView extends View { } setPropertyValue(propName, propValue) { - switch(propName) { - case 'eatTabKey' : - if(_.isBoolean(propValue)) { + switch (propName) { + case 'eatTabKey': + if (_.isBoolean(propValue)) { this.eatTabKey = propValue; } break; - case 'caseInsensitive' : - if(_.isBoolean(propValue)) { + case 'caseInsensitive': + if (_.isBoolean(propValue)) { this.caseInsensitive = propValue; } break; - case 'keys' : - if(Array.isArray(propValue)) { + case 'keys': + if (Array.isArray(propValue)) { this.keys = propValue; } break; @@ -73,5 +77,7 @@ module.exports = class KeyEntryView extends View { super.setPropertyValue(propName, propValue); } - getData() { return this.keyEntered; } -}; \ No newline at end of file + getData() { + return this.keyEntered; + } +}; diff --git a/core/last_callers.js b/core/last_callers.js index 9d875b2e..b469c02a 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -2,74 +2,90 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const StatLog = require('./stat_log.js'); -const User = require('./user.js'); -const sysDb = require('./database.js').dbs.system; -const { Errors } = require('./enig_error.js'); -const UserProps = require('./user_property.js'); -const SysLogKeys = require('./system_log.js'); +const { MenuModule } = require('./menu_module.js'); +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const sysDb = require('./database.js').dbs.system; +const { Errors } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); +const SysLogKeys = require('./system_log.js'); // deps -const moment = require('moment'); -const async = require('async'); -const _ = require('lodash'); +const moment = require('moment'); +const async = require('async'); +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 MciViewIds = { - callerList : 1, + callerList: 1, }; exports.getModule = class LastCallersModule extends MenuModule { constructor(options) { super(options); - this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {}); - this.actionIndicatorDefault = _.get(options, 'menuConfig.config.actionIndicatorDefault', '-'); + this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {}); + this.actionIndicatorDefault = _.get( + options, + 'menuConfig.config.actionIndicatorDefault', + '-' + ); } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.waterfall( [ - (callback) => { + callback => { this.prepViewController('callers', 0, mciData.menu, err => { return callback(err); }); }, - (callback) => { - this.fetchHistory( (err, loginHistory) => { + callback => { + this.fetchHistory((err, loginHistory) => { return callback(err, loginHistory); }); }, (loginHistory, callback) => { - this.loadUserForHistoryItems(loginHistory, (err, updatedHistory) => { - return callback(err, updatedHistory); - }); + this.loadUserForHistoryItems( + loginHistory, + (err, updatedHistory) => { + return callback(err, updatedHistory); + } + ); }, (loginHistory, callback) => { - const callersView = this.viewControllers.callers.getView(MciViewIds.callerList); - if(!callersView) { - return cb(Errors.MissingMci(`Missing caller list MCI ${MciViewIds.callerList}`)); + const callersView = this.viewControllers.callers.getView( + MciViewIds.callerList + ); + if (!callersView) { + return cb( + Errors.MissingMci( + `Missing caller list MCI ${MciViewIds.callerList}` + ) + ); } callersView.setItems(loginHistory); callersView.redraw(); return callback(null); - } + }, ], err => { - if(err) { - this.client.log.warn( { error : err.message }, 'Error loading last callers'); + if (err) { + this.client.log.warn( + { error: err.message }, + 'Error loading last callers' + ); } return cb(err); } @@ -79,65 +95,74 @@ exports.getModule = class LastCallersModule extends MenuModule { getCollapse(conf) { let collapse = _.get(this, conf); - collapse = collapse && collapse.match(/^([0-9]+)\s*(minutes?|seconds?|hours?|days?|months?)$/); - if(collapse) { + collapse = + collapse && + collapse.match(/^([0-9]+)\s*(minutes?|seconds?|hours?|days?|months?)$/); + if (collapse) { return moment.duration(parseInt(collapse[1]), collapse[2]); } } fetchHistory(cb) { const callersView = this.viewControllers.callers.getView(MciViewIds.callerList); - if(!callersView || 0 === callersView.dimens.height) { + if (!callersView || 0 === callersView.dimens.height) { return cb(null); } StatLog.getSystemLogEntries( SysLogKeys.UserLoginHistory, StatLog.Order.TimestampDesc, - 200, // max items to fetch - we need more than max displayed for filtering/etc. + 200, // max items to fetch - we need more than max displayed for filtering/etc. (err, loginHistory) => { - if(err) { + if (err) { return cb(err); } const dateTimeFormat = _.get( - this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateFormat('short')); + this, + 'menuConfig.config.dateTimeFormat', + this.client.currentTheme.helpers.getDateFormat('short') + ); loginHistory = loginHistory.map(item => { try { const historyItem = JSON.parse(item.log_value); - if(_.isObject(historyItem)) { - item.userId = historyItem.userId; - item.sessionId = historyItem.sessionId; + if (_.isObject(historyItem)) { + item.userId = historyItem.userId; + item.sessionId = historyItem.sessionId; } else { - item.userId = historyItem; // older format - item.sessionId = '-none-'; + item.userId = historyItem; // older format + item.sessionId = '-none-'; } - } catch(e) { - return null; // we'll filter this out + } catch (e) { + return null; // we'll filter this out } item.timestamp = moment(item.timestamp); - return Object.assign( - item, - { - ts : moment(item.timestamp).format(dateTimeFormat) - } - ); + return Object.assign(item, { + ts: moment(item.timestamp).format(dateTimeFormat), + }); }); - const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide'); - const sysOpCollapse = this.getCollapse('menuConfig.config.sysop.collapse'); + const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide'); + const sysOpCollapse = this.getCollapse( + 'menuConfig.config.sysop.collapse' + ); const collapseList = (withUserId, minAge) => { let lastUserId; let lastTimestamp; loginHistory = loginHistory.filter(item => { - const secApart = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).asSeconds() : 0; - const collapse = (null === withUserId ? true : withUserId === item.userId) && - (lastUserId === item.userId) && - (secApart < minAge); + const secApart = lastTimestamp + ? moment + .duration(lastTimestamp.diff(item.timestamp)) + .asSeconds() + : 0; + const collapse = + (null === withUserId ? true : withUserId === item.userId) && + lastUserId === item.userId && + secApart < minAge; lastUserId = item.userId; lastTimestamp = item.timestamp; @@ -146,20 +171,22 @@ exports.getModule = class LastCallersModule extends MenuModule { }); }; - if(hideSysOp) { - loginHistory = loginHistory.filter(item => false === User.isRootUserId(item.userId)); - } else if(sysOpCollapse) { + if (hideSysOp) { + loginHistory = loginHistory.filter( + item => false === User.isRootUserId(item.userId) + ); + } else if (sysOpCollapse) { collapseList(User.RootUserID, sysOpCollapse.asSeconds()); } const userCollapse = this.getCollapse('menuConfig.config.user.collapse'); - if(userCollapse) { + if (userCollapse) { collapseList(null, userCollapse.asSeconds()); } return cb( null, - loginHistory.slice(0, callersView.dimens.height) // trim the fat + loginHistory.slice(0, callersView.dimens.height) // trim the fat ); } ); @@ -167,57 +194,70 @@ exports.getModule = class LastCallersModule extends MenuModule { loadUserForHistoryItems(loginHistory, cb) { const getPropOpts = { - names : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations ] + names: [UserProps.RealName, UserProps.Location, UserProps.Affiliations], }; const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k); let indicatorSumsSql; - if(actionIndicatorNames.length > 0) { + if (actionIndicatorNames.length > 0) { indicatorSumsSql = actionIndicatorNames.map(i => { - return `SUM(CASE WHEN log_name='${_.snakeCase(i)}' THEN 1 ELSE 0 END) AS ${i}`; + return `SUM(CASE WHEN log_name='${_.snakeCase( + i + )}' THEN 1 ELSE 0 END) AS ${i}`; }); } - async.map(loginHistory, (item, nextHistoryItem) => { - User.getUserName(item.userId, (err, userName) => { - if(err) { - return nextHistoryItem(null, null); - } - - item.userName = item.text = userName; - - User.loadProperties(item.userId, getPropOpts, (err, props) => { - item.location = (props && props[UserProps.Location]) || ''; - item.affiliation = item.affils = (props && props[UserProps.Affiliations]) || ''; - item.realName = (props && props[UserProps.RealName]) || ''; - - if(!indicatorSumsSql) { - return nextHistoryItem(null, item); + async.map( + loginHistory, + (item, nextHistoryItem) => { + User.getUserName(item.userId, (err, userName) => { + if (err) { + return nextHistoryItem(null, null); } - sysDb.get( - `SELECT ${indicatorSumsSql.join(', ')} + item.userName = item.text = userName; + + User.loadProperties(item.userId, getPropOpts, (err, props) => { + item.location = (props && props[UserProps.Location]) || ''; + item.affiliation = item.affils = + (props && props[UserProps.Affiliations]) || ''; + item.realName = (props && props[UserProps.RealName]) || ''; + + if (!indicatorSumsSql) { + return nextHistoryItem(null, item); + } + + sysDb.get( + `SELECT ${indicatorSumsSql.join(', ')} FROM user_event_log WHERE user_id=? AND session_id=? LIMIT 1;`, - [ item.userId, item.sessionId ], - (err, results) => { - if(_.isObject(results)) { - item.actions = ''; - Object.keys(results).forEach(n => { - const indicator = results[n] > 0 ? this.actionIndicators[n] || this.actionIndicatorDefault : this.actionIndicatorDefault; - item[n] = indicator; - item.actions += indicator; - }); + [item.userId, item.sessionId], + (err, results) => { + if (_.isObject(results)) { + item.actions = ''; + Object.keys(results).forEach(n => { + const indicator = + results[n] > 0 + ? this.actionIndicators[n] || + this.actionIndicatorDefault + : this.actionIndicatorDefault; + item[n] = indicator; + item.actions += indicator; + }); + } + return nextHistoryItem(null, item); } - return nextHistoryItem(null, item); - } - ); + ); + }); }); - }); - }, - (err, mapped) => { - return cb(err, mapped.filter(item => item)); // remove deleted - }); + }, + (err, mapped) => { + return cb( + err, + mapped.filter(item => item) + ); // remove deleted + } + ); } }; diff --git a/core/listening_server.js b/core/listening_server.js index 7cb7405e..13670c8a 100644 --- a/core/listening_server.js +++ b/core/listening_server.js @@ -2,16 +2,16 @@ 'use strict'; // ENiGMA½ -const logger = require('./logger.js'); +const logger = require('./logger.js'); // deps -const async = require('async'); +const async = require('async'); -const listeningServers = {}; // packageName -> info +const listeningServers = {}; // packageName -> info -exports.startup = startup; -exports.shutdown = shutdown; -exports.getServer = getServer; +exports.startup = startup; +exports.shutdown = shutdown; +exports.getServer = getServer; function startup(cb) { return startListening(cb); @@ -28,36 +28,44 @@ function getServer(packageName) { function startListening(cb) { const moduleUtil = require('./module_util.js'); // late load so we get Config - async.each( [ 'login', 'content', 'chat' ], (category, next) => { - moduleUtil.loadModulesForCategory(`${category}Servers`, (module, nextModule) => { - const moduleInst = new module.getModule(); - try { - moduleInst.createServer(err => { - if(err) { - return nextModule(err); + async.each( + ['login', 'content', 'chat'], + (category, next) => { + moduleUtil.loadModulesForCategory( + `${category}Servers`, + (module, nextModule) => { + const moduleInst = new module.getModule(); + try { + moduleInst.createServer(err => { + if (err) { + return nextModule(err); + } + + moduleInst.listen(err => { + if (err) { + return nextModule(err); + } + + listeningServers[module.moduleInfo.packageName] = { + instance: moduleInst, + info: module.moduleInfo, + }; + + return nextModule(null); + }); + }); + } catch (e) { + logger.log.error(e, 'Exception caught creating server!'); + return nextModule(e); } - - moduleInst.listen( err => { - if(err) { - return nextModule(err); - } - - listeningServers[module.moduleInfo.packageName] = { - instance : moduleInst, - info : module.moduleInfo, - }; - - return nextModule(null); - }); - }); - } catch(e) { - logger.log.error(e, 'Exception caught creating server!'); - return nextModule(e); - } - }, err => { - return next(err); - }); - }, err => { - return cb(err); - }); + }, + err => { + return next(err); + } + ); + }, + err => { + return cb(err); + } + ); } diff --git a/core/logger.js b/core/logger.js index 9a4e8711..6d99ad81 100644 --- a/core/logger.js +++ b/core/logger.js @@ -2,54 +2,56 @@ 'use strict'; // deps -const bunyan = require('bunyan'); -const paths = require('path'); -const fs = require('graceful-fs'); -const _ = require('lodash'); +const bunyan = require('bunyan'); +const paths = require('path'); +const fs = require('graceful-fs'); +const _ = require('lodash'); module.exports = class Log { - static init() { - const Config = require('./config.js').get(); - const logPath = Config.paths.logs; + const Config = require('./config.js').get(); + const logPath = Config.paths.logs; const err = this.checkLogPath(logPath); - if(err) { + 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); + 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. + 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); + ['formData', 'formValue'].forEach(keyName => { + serializers[keyName] = fd => Log.hideSensitive(fd); }); this.log = bunyan.createLogger({ - name : 'ENiGMA½ BBS', - streams : logStreams, - serializers : serializers, + name: 'ENiGMA½ BBS', + streams: logStreams, + serializers: serializers, }); } static checkLogPath(logPath) { try { - if(!fs.statSync(logPath).isDirectory()) { + if (!fs.statSync(logPath).isDirectory()) { return new Error(`${logPath} is not a directory`); } return null; - } catch(e) { - if('ENOENT' === e.code) { + } catch (e) { + if ('ENOENT' === e.code) { return new Error(`${logPath} does not exist`); } return e; @@ -62,11 +64,14 @@ module.exports = class Log { // 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}":"********"`; - }) + JSON.stringify(obj).replace( + /"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, + (match, valueName) => { + return `"${valueName}":"********"`; + } + ) ); - } catch(e) { + } catch (e) { // be safe and return empty obj! return {}; } diff --git a/core/login_server_module.js b/core/login_server_module.js index 6a47f7c5..95eaebae 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -2,14 +2,14 @@ 'use strict'; // ENiGMA½ -const Config = require('./config').get; -const logger = require('./logger.js'); -const ServerModule = require('./server_module.js').ServerModule; -const clientConns = require('./client_connections.js'); -const UserProps = require('./user_property.js'); +const Config = require('./config').get; +const logger = require('./logger.js'); +const ServerModule = require('./server_module.js').ServerModule; +const clientConns = require('./client_connections.js'); +const UserProps = require('./user_property.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); module.exports = class LoginServerModule extends ServerModule { constructor() { @@ -19,7 +19,7 @@ module.exports = class LoginServerModule extends ServerModule { // :TODO: we need to max connections -- e.g. from config 'maxConnections' prepareClient(client, cb) { - if(client.user.isAuthenticated()) { + if (client.user.isAuthenticated()) { return cb(null); } @@ -29,7 +29,7 @@ module.exports = class LoginServerModule extends ServerModule { // Choose initial theme before we have user context // const preLoginTheme = _.get(Config(), 'theme.preLogin'); - if('*' === preLoginTheme) { + if ('*' === preLoginTheme) { client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || ''; } else { client.user.properties[UserProps.ThemeId] = preLoginTheme; @@ -41,24 +41,25 @@ module.exports = class LoginServerModule extends ServerModule { handleNewClient(client, clientSock, modInfo) { clientSock.on('error', err => { - logger.log.warn({ modInfo, error : err.message }, 'Client socket error'); + logger.log.warn({ modInfo, error: err.message }, 'Client socket error'); }); // // Start tracking the client. A session ID aka client ID // will be established in addNewClient() below. // - if(_.isUndefined(client.session)) { + 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); client.on('ready', readyOptions => { - client.startIdleMonitor(); // Go to module -- use default error handler @@ -72,12 +73,15 @@ module.exports = class LoginServerModule extends ServerModule { }); client.on('error', err => { - logger.log.info({ nodeId : client.node, error : err.message }, 'Connection error'); + logger.log.info( + { nodeId: client.node, error: err.message }, + 'Connection error' + ); }); client.on('close', err => { const logFunc = err ? logger.log.info : logger.log.debug; - logFunc( { nodeId : client.node }, 'Connection closed'); + logFunc({ nodeId: client.node }, 'Connection closed'); clientConns.removeClient(client); }); @@ -86,7 +90,7 @@ module.exports = class LoginServerModule extends ServerModule { client.log.info('User idle timeout expired'); client.menuStack.goto('idleLogoff', err => { - if(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 ce5b160a..71e48bdc 100644 --- a/core/mail_packet.js +++ b/core/mail_packet.js @@ -1,9 +1,9 @@ /* jslint node: true */ 'use strict'; -var events = require('events'); -var assert = require('assert'); -var _ = require('lodash'); +var events = require('events'); +var assert = require('assert'); +var _ = require('lodash'); module.exports = MailPacket; @@ -16,7 +16,7 @@ function MailPacket(options) { require('util').inherits(MailPacket, events.EventEmitter); -MailPacket.prototype.read = function(options) { +MailPacket.prototype.read = function (options) { // // options.packetPath | opts.packetBuffer: supplies a path-to-file // or a buffer containing packet data @@ -26,11 +26,11 @@ MailPacket.prototype.read = function(options) { assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer)); }; -MailPacket.prototype.write = function(options) { +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)); -}; \ No newline at end of file +}; diff --git a/core/mail_util.js b/core/mail_util.js index 6bd433d3..9b4bfefe 100644 --- a/core/mail_util.js +++ b/core/mail_util.js @@ -1,12 +1,13 @@ /* jslint node: true */ 'use strict'; -const Address = require('./ftn_address.js'); -const Message = require('./message.js'); +const Address = require('./ftn_address.js'); +const Message = require('./message.js'); -exports.getAddressedToInfo = getAddressedToInfo; +exports.getAddressedToInfo = getAddressedToInfo; -const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const EMAIL_REGEX = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /* Input Output @@ -26,56 +27,72 @@ function getAddressedToInfo(input) { const firstAtPos = input.indexOf('@'); - if(firstAtPos < 0) { + if (firstAtPos < 0) { let addr = Address.fromString(input); - if(Address.isValidAddress(addr)) { - return { flavor : Message.AddressFlavor.FTN, remote : 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 }; + if (lessThanPos < 0) { + return { name: input, flavor: Message.AddressFlavor.Local }; } const greaterThanPos = input.indexOf('>'); - if(greaterThanPos < lessThanPos) { - return { name : input, flavor : Message.AddressFlavor.Local }; + 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() }; + 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 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 }; + 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 }; + 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() }; + 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 c9163273..b9a68ced 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -1,16 +1,16 @@ /* jslint node: true */ 'use strict'; -var TextView = require('./text_view.js').TextView; -var miscUtil = require('./misc_util.js'); -var strUtil = require('./string_util.js'); -var ansi = require('./ansi_term.js'); +var TextView = require('./text_view.js').TextView; +var miscUtil = require('./misc_util.js'); +var strUtil = require('./string_util.js'); +var ansi = require('./ansi_term.js'); //var util = require('util'); -var assert = require('assert'); -var _ = require('lodash'); +var assert = require('assert'); +var _ = require('lodash'); -exports.MaskEditTextView = MaskEditTextView; +exports.MaskEditTextView = MaskEditTextView; // ##/##/#### <--styleSGR2 if fillChar // ^- styleSGR1 @@ -29,59 +29,71 @@ exports.MaskEditTextView = MaskEditTextView; // * There exists some sort of condition that allows pattern position to get out of sync function MaskEditTextView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); - options.resizable = false; + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); + options.resizable = false; TextView.call(this, options); this.initDefaultWidth(); - this.cursorPos = { x : 0 }; - this.patternArrayPos = 0; + this.cursorPos = { x: 0 }; + this.patternArrayPos = 0; var self = this; this.maskPattern = options.maskPattern || ''; - this.clientBackspace = function() { + this.clientBackspace = function () { var fillCharSGR = this.getStyleSGR(3) || this.getSGR(); - this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR()); + 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); // 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]); + 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) || ''); + 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++) { + 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]]); + if (self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) { + self.patternArray.push( + MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]] + ); ++self.maxLength; } else { self.patternArray.push(self.maskPattern[i]); @@ -89,53 +101,58 @@ function MaskEditTextView(options) { } }; - this.getEndOfTextColumn = function() { + this.getEndOfTextColumn = function () { return this.position.col + this.patternArrayPos; }; 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.prototype.setText = function (text) { MaskEditTextView.super_.prototype.setText.call(this, text); - if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() + if (this.patternArray) { + // :TODO: This is a hack - see TextView ctor note about setText() this.patternArrayPos = this.patternArray.length; } }; -MaskEditTextView.prototype.setMaskPattern = function(pattern) { +MaskEditTextView.prototype.setMaskPattern = function (pattern) { this.dimens.width = pattern.length; this.maskPattern = pattern; this.buildPattern(); }; -MaskEditTextView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('backspace', key.name)) { - if(this.text.length > 0) { +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(_.isRegExp(this.patternArray[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])) { + 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.client.term.write( + ansi.goto( + this.position.row, + this.getEndOfTextColumn() + 1 + ) + ); this.clientBackspace(); break; } @@ -145,62 +162,67 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { } return; - } else if(this.isKeyMapped('clearLine', key.name)) { - this.text = ''; - this.patternArrayPos = 0; - this.setFocus(true); // redraw + adjust cursor + } else if (this.isKeyMapped('clearLine', key.name)) { + this.text = ''; + this.patternArrayPos = 0; + this.setFocus(true); // redraw + adjust cursor return; } } - if(ch && strUtil.isPrintable(ch)) { - if(this.text.length < this.maxLength) { + if (ch && strUtil.isPrintable(ch)) { + if (this.text.length < this.maxLength) { ch = strUtil.stylizeString(ch, this.textStyle); - if(!ch.match(this.patternArray[this.patternArrayPos])) { + if (!ch.match(this.patternArray[this.patternArrayPos])) { return; } this.text += ch; this.patternArrayPos++; - while(this.patternArrayPos < this.patternArray.length && - !_.isRegExp(this.patternArray[this.patternArrayPos])) - { + while ( + this.patternArrayPos < this.patternArray.length && + !_.isRegExp(this.patternArray[this.patternArrayPos]) + ) { this.patternArrayPos++; } this.redraw(); - this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); + this.client.term.write( + ansi.goto(this.position.row, this.getEndOfTextColumn()) + ); } } MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key); }; -MaskEditTextView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'maskPattern' : this.setMaskPattern(value); break; +MaskEditTextView.prototype.setPropertyValue = function (propName, value) { + switch (propName) { + case 'maskPattern': + this.setMaskPattern(value); + break; } MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); }; -MaskEditTextView.prototype.getData = function() { +MaskEditTextView.prototype.getData = function () { var rawData = MaskEditTextView.super_.prototype.getData.call(this); - if(!rawData || 0 === rawData.length) { + if (!rawData || 0 === rawData.length) { return rawData; } - var data = ''; + var data = ''; assert(rawData.length <= this.patternArray.length); var p = 0; - for(var i = 0; i < this.patternArray.length; ++i) { - if(_.isRegExp(this.patternArray[i])) { + for (var i = 0; i < this.patternArray.length; ++i) { + if (_.isRegExp(this.patternArray[i])) { data += rawData[p++]; } else { data += this.patternArray[i]; diff --git a/core/mbf.js b/core/mbf.js index b051033c..20fbbb69 100644 --- a/core/mbf.js +++ b/core/mbf.js @@ -9,7 +9,7 @@ const { Errors } = require('./enig_error'); // // Number to 32bit MBF -const numToMbf32 = (v) => { +const numToMbf32 = v => { const mbf = Buffer.alloc(4); if (0 === v) { @@ -19,8 +19,8 @@ const numToMbf32 = (v) => { const ieee = Buffer.alloc(4); ieee.writeFloatLE(v, 0); - const sign = ieee[3] & 0x80; - let exp = (ieee[3] << 1) | (ieee[2] >> 7); + const sign = ieee[3] & 0x80; + let exp = (ieee[3] << 1) | (ieee[2] >> 7); if (exp === 0xfe) { throw Errors.Invalid(`${v} cannot be converted to mbf`); @@ -36,14 +36,14 @@ const numToMbf32 = (v) => { return mbf; }; -const mbf32ToNum = (mbf) => { +const mbf32ToNum = mbf => { if (0 === mbf[3]) { return 0.0; } - const ieee = Buffer.alloc(4); - const sign = mbf[2] & 0x80; - const exp = mbf[3] - 2; + const ieee = Buffer.alloc(4); + const sign = mbf[2] & 0x80; + const exp = mbf[3] - 2; ieee[3] = sign | (exp >> 1); ieee[2] = (exp << 7) | (mbf[2] & 0x7f); diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index d6c37865..b628b56e 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -2,33 +2,45 @@ 'use strict'; // ENiGMA½ -const TextView = require('./text_view.js').TextView; -const View = require('./view.js').View; -const EditTextView = require('./edit_text_view.js').EditTextView; -const ButtonView = require('./button_view.js').ButtonView; -const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; -const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; -const FullMenuView = require('./full_menu_view.js').FullMenuView; -const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; -const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; -const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; -const KeyEntryView = require('./key_entry_view.js'); -const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; +const TextView = require('./text_view.js').TextView; +const View = require('./view.js').View; +const EditTextView = require('./edit_text_view.js').EditTextView; +const ButtonView = require('./button_view.js').ButtonView; +const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; +const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const FullMenuView = require('./full_menu_view.js').FullMenuView; +const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; +const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; +const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; +const KeyEntryView = require('./key_entry_view.js'); +const MultiLineEditTextView = + require('./multi_line_edit_text_view.js').MultiLineEditTextView; const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; -const ansi = require('./ansi_term.js'); +const ansi = require('./ansi_term.js'); // deps -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MCIViewFactory = MCIViewFactory; +exports.MCIViewFactory = MCIViewFactory; function MCIViewFactory(client) { this.client = client; } MCIViewFactory.UserViewCodes = [ - 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE', + 'TL', + 'ET', + 'ME', + 'MT', + 'PL', + 'BT', + 'VM', + 'HM', + 'FM', + 'SM', + 'TM', + 'KE', // // XY is a special MCI code that allows finding positions @@ -38,34 +50,32 @@ MCIViewFactory.UserViewCodes = [ 'XY', ]; -MCIViewFactory.MovementCodes = [ - 'CF', 'CB', 'CU', 'CD', -]; +MCIViewFactory.MovementCodes = ['CF', 'CB', 'CU', 'CD']; -MCIViewFactory.prototype.createFromMCI = function(mci) { +MCIViewFactory.prototype.createFromMCI = function (mci) { 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] }, + 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) { + 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)) { + if (mci.args.length > pos && mci.args[pos].length > 0) { + if (!_.isObject(options.dimens)) { options.dimens = {}; } options.dimens.width = parseInt(mci.args[pos], 10); @@ -73,7 +83,11 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { } function setFocusOption(pos, name) { - if(mci.focusArgs && mci.focusArgs.length > pos && mci.focusArgs[pos].length > 0) { + if ( + mci.focusArgs && + mci.focusArgs.length > pos && + mci.focusArgs[pos].length > 0 + ) { options[name] = mci.focusArgs[pos]; } } @@ -81,46 +95,46 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { // // Note: Keep this in sync with UserViewCodes above! // - switch(mci.code) { + switch (mci.code) { // Text Label (Text View) - case 'TL' : - setOption(0, 'textStyle'); - setOption(1, 'justify'); + case 'TL': + setOption(0, 'textStyle'); + setOption(1, 'justify'); setWidth(2); view = new TextView(options); break; - // Edit Text - case 'ET' : + // Edit Text + case 'ET': setWidth(0); - setOption(1, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setOption(1, 'textStyle'); + setFocusOption(0, 'focusTextStyle'); view = new EditTextView(options); break; - // Masked Edit Text - case 'ME' : - setOption(0, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + // Masked Edit Text + case 'ME': + setOption(0, 'textStyle'); + setFocusOption(0, 'focusTextStyle'); view = new MaskEditTextView(options); break; - // Multi Line Edit Text - case 'MT' : + // 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) { + // 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) { + if (options.text) { setOption(1, 'textStyle'); setOption(2, 'justify'); setWidth(3); @@ -130,10 +144,10 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { } 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'); @@ -144,78 +158,78 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { 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; - // 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; - // Full Menu - case 'FM' : - setOption(0, 'itemSpacing'); - setOption(1, 'itemHorizSpacing'); - setOption(2, 'justify'); - setOption(3, 'textStyle'); + // Full Menu + case 'FM': + setOption(0, 'itemSpacing'); + setOption(1, 'itemHorizSpacing'); + setOption(2, 'justify'); + setOption(3, 'textStyle'); - setFocusOption(0, 'focusTextStyle'); + setFocusOption(0, 'focusTextStyle'); view = new FullMenuView(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; - case 'TM' : - if(mci.args.length > 0) { - var styleSG1 = { fg : parseInt(mci.args[0], 10) }; - if(mci.args.length > 1) { + 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; - case 'KE' : + case 'KE': view = new KeyEntryView(options); break; - case 'XY' : + case 'XY': view = new View(options); break; - default : - if(!MCIViewFactory.MovementCodes.includes(mci.code)) { + default: + if (!MCIViewFactory.MovementCodes.includes(mci.code)) { options.text = getPredefinedMCIValue(this.client, mci.code); - if(_.isString(options.text)) { + if (_.isString(options.text)) { setWidth(0); - setOption(1, 'textStyle'); - setOption(2, 'justify'); + setOption(1, 'textStyle'); + setOption(2, 'justify'); view = new TextView(options); } @@ -223,7 +237,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { break; } - if(view) { + if (view) { view.mciCode = mci.code; } diff --git a/core/menu_module.js b/core/menu_module.js index bfddbd18..d7c23dbb 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -1,47 +1,51 @@ /* jslint node: true */ 'use strict'; -const PluginModule = require('./plugin_module.js').PluginModule; -const theme = require('./theme.js'); -const ansi = require('./ansi_term.js'); -const ViewController = require('./view_controller.js').ViewController; -const menuUtil = require('./menu_util.js'); -const Config = require('./config.js').get; -const stringFormat = require('../core/string_format.js'); -const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; -const Errors = require('../core/enig_error.js').Errors; +const PluginModule = require('./plugin_module.js').PluginModule; +const theme = require('./theme.js'); +const ansi = require('./ansi_term.js'); +const ViewController = require('./view_controller.js').ViewController; +const menuUtil = require('./menu_util.js'); +const Config = require('./config.js').get; +const stringFormat = require('../core/string_format.js'); +const MultiLineEditTextView = + require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; +const Errors = require('../core/enig_error.js').Errors; const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); // deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const iconvDecode = require('iconv-lite').decode; +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const iconvDecode = require('iconv-lite').decode; exports.MenuModule = class MenuModule extends PluginModule { - constructor(options) { super(options); - this.menuName = options.menuName; - this.menuConfig = options.menuConfig; - this.client = options.client; - this.menuMethods = {}; // methods called from @method's - this.menuConfig.config = this.menuConfig.config || {}; - this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); - this.viewControllers = {}; - this.interrupt = (_.get(this.menuConfig.config, 'interrupt', MenuModule.InterruptTypes.Queued)).toLowerCase(); + this.menuName = options.menuName; + this.menuConfig = options.menuConfig; + this.client = options.client; + this.menuMethods = {}; // methods called from @method's + this.menuConfig.config = this.menuConfig.config || {}; + this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); + this.viewControllers = {}; + this.interrupt = _.get( + this.menuConfig.config, + 'interrupt', + MenuModule.InterruptTypes.Queued + ).toLowerCase(); - if(MenuModule.InterruptTypes.Realtime === this.interrupt) { - this.realTimeInterrupt = 'blocked'; + if (MenuModule.InterruptTypes.Realtime === this.interrupt) { + this.realTimeInterrupt = 'blocked'; } } static get InterruptTypes() { return { - Never : 'never', - Queued : 'queued', - Realtime : 'realtime', + Never: 'never', + Queued: 'queued', + Realtime: 'realtime', }; } @@ -54,13 +58,16 @@ exports.MenuModule = class MenuModule extends PluginModule { } initSequence() { - const self = this; - const mciData = {}; - let pausePosition = {row: 0, column: 0}; + const self = this; + const mciData = {}; + let pausePosition = { row: 0, column: 0 }; const hasArt = () => { - return _.isString(self.menuConfig.art) || - (Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs')); + return ( + _.isString(self.menuConfig.art) || + (Array.isArray(self.menuConfig.art) && + _.has(self.menuConfig.art[0], 'acs')) + ); }; async.waterfall( @@ -72,7 +79,7 @@ exports.MenuModule = class MenuModule extends PluginModule { return self.beforeArt(callback); }, function displayMenuArt(callback) { - if(!hasArt()) { + if (!hasArt()) { return callback(null, null); } @@ -80,32 +87,39 @@ exports.MenuModule = class MenuModule extends PluginModule { self.menuConfig.art, self.menuConfig.config, (err, artData) => { - if(err) { - self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } ); + if (err) { + self.client.log.trace('Could not display art', { + art: self.menuConfig.art, + reason: err.message, + }); } else { mciData.menu = artData.mciMap; } - if(artData) { + if (artData) { pausePosition.row = artData.height + 1; } - return callback(null, artData); // any errors are non-fatal + return callback(null, artData); // any errors are non-fatal } ); }, function displayPromptArt(artData, callback) { - if(!_.isString(self.menuConfig.prompt)) { + 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' + ) + ); } const options = Object.assign({}, self.menuConfig.config); - if(_.isNumber(artData?.height)) { + if (_.isNumber(artData?.height)) { options.startRow = artData.height + 1; } @@ -113,12 +127,12 @@ exports.MenuModule = class MenuModule extends PluginModule { self.menuConfig.promptConfig.art, options, (err, artData) => { - if(artData) { + if (artData) { mciData.prompt = artData.mciMap; pausePosition.row = artData.height + 1; } - return callback(err); // pass err here; prompts *must* have art + return callback(err); // pass err here; prompts *must* have art } ); }, @@ -126,11 +140,14 @@ exports.MenuModule = class MenuModule extends PluginModule { return self.mciReady(mciData, callback); }, function displayPauseIfRequested(callback) { - if(!self.shouldPause()) { + if (!self.shouldPause()) { return callback(null, null); } - if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) { + if ( + self.client.term.termHeight > 0 && + pausePosition.row > self.client.termHeight + ) { // If this scrolled, the prompt will go to the bottom of the screen pausePosition.row = self.client.termHeight; } @@ -141,25 +158,31 @@ exports.MenuModule = class MenuModule extends PluginModule { self.finishedLoading(); self.realTimeInterrupt = 'allowed'; return self.autoNextMenu(callback); - } + }, ], err => { - if(err) { - self.client.log.warn('Error during init sequence', { error : err.message } ); + 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.config.baudRate)) { + if (_.isNumber(this.menuConfig.config.baudRate)) { // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here - this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.config.baudRate)); + this.client.term.rawWrite( + ansi.setEmulatedBaudRate(this.menuConfig.config.baudRate) + ); } - if(this.cls) { + if (this.cls) { this.client.term.rawWrite(ansi.resetScreen()); } @@ -176,14 +199,14 @@ exports.MenuModule = class MenuModule extends PluginModule { } displayQueuedInterruptions(cb) { - if(MenuModule.InterruptTypes.Never === this.interrupt) { + if (MenuModule.InterruptTypes.Never === this.interrupt) { return cb(null); } - let opts = { cls : true }; // clear screen for first message + let opts = { cls: true }; // clear screen for first message async.whilst( - (callback) => callback(null, this.client.interruptQueue.hasItems()), + callback => callback(null, this.client.interruptQueue.hasItems()), next => { this.client.interruptQueue.displayNext(opts, err => { opts = {}; @@ -197,7 +220,10 @@ exports.MenuModule = class MenuModule extends PluginModule { } attemptInterruptNow(interruptItem, cb) { - if(this.realTimeInterrupt !== 'allowed' || MenuModule.InterruptTypes.Realtime !== this.interrupt) { + if ( + this.realTimeInterrupt !== 'allowed' || + MenuModule.InterruptTypes.Realtime !== this.interrupt + ) { return cb(null, false); // don't eat up the item; queue for later } @@ -212,15 +238,16 @@ exports.MenuModule = class MenuModule extends PluginModule { }; this.client.interruptQueue.displayWithItem( - Object.assign({}, interruptItem, { cls : true }), + Object.assign({}, interruptItem, { cls: true }), err => { - if(err) { + if (err) { return done(err, false); } this.reload(err => { return done(err, err ? false : true); }); - }); + } + ); } getSaveState() { @@ -237,17 +264,17 @@ exports.MenuModule = class MenuModule extends PluginModule { } nextMenu(cb) { - if(!this.haveNext()) { - return this.prevMenu(cb); // no next, go to prev + if (!this.haveNext()) { + return this.prevMenu(cb); // no next, go to prev } - this.displayQueuedInterruptions( () => { + this.displayQueuedInterruptions(() => { return this.client.menuStack.next(cb); }); } prevMenu(cb) { - this.displayQueuedInterruptions( () => { + this.displayQueuedInterruptions(() => { return this.client.menuStack.prev(cb); }); } @@ -258,8 +285,8 @@ exports.MenuModule = class MenuModule extends PluginModule { gotoMenuOrPrev(name, options, cb) { this.client.menuStack.goto(name, options, err => { - if(!err) { - if(cb) { + if (!err) { + if (cb) { return cb(null); } } @@ -269,7 +296,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } gotoMenuOrShowMessage(name, message, options, cb) { - if(!cb && _.isFunction(options)) { + if (!cb && _.isFunction(options)) { cb = options; options = {}; } @@ -277,18 +304,18 @@ exports.MenuModule = class MenuModule extends PluginModule { options = options || { clearScreen: true }; this.gotoMenu(name, options, err => { - if(err) { - if(options.clearScreen) { + if (err) { + if (options.clearScreen) { this.client.term.rawWrite(ansi.resetScreen()); } this.client.term.write(`${message}\n`); - return this.pausePrompt( () => { + return this.pausePrompt(() => { return this.prevMenu(cb); }); } - if(cb) { + if (cb) { return cb(null); } }); @@ -301,33 +328,39 @@ exports.MenuModule = class MenuModule extends PluginModule { } prevMenuOnTimeout(timeout, cb) { - setTimeout( () => { + setTimeout(() => { return this.prevMenu(cb); }, timeout); } addViewController(name, vc) { - assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); + assert( + !this.viewControllers[name], + `ViewController by the name of "${name}" already exists!` + ); this.viewControllers[name] = vc; return vc; } removeViewController(name) { - if(this.viewControllers[name]) { + if (this.viewControllers[name]) { this.viewControllers[name].detachClientEvents(); delete this.viewControllers[name]; } } detachViewControllers() { - Object.keys(this.viewControllers).forEach( name => { + Object.keys(this.viewControllers).forEach(name => { this.viewControllers[name].detachClientEvents(); }); } shouldPause() { - return ('end' === this.menuConfig.config.pause || true === this.menuConfig.config.pause); + return ( + 'end' === this.menuConfig.config.pause || + true === this.menuConfig.config.pause + ); } hasNextTimeout() { @@ -335,13 +368,13 @@ exports.MenuModule = class MenuModule extends PluginModule { } haveNext() { - return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); + return _.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next); } autoNextMenu(cb) { const gotoNextMenu = () => { - if(this.haveNext()) { - this.displayQueuedInterruptions( () => { + if (this.haveNext()) { + this.displayQueuedInterruptions(() => { return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb); }); } else { @@ -349,9 +382,12 @@ exports.MenuModule = class MenuModule extends PluginModule { } }; - if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { - if(this.hasNextTimeout()) { - setTimeout( () => { + if ( + _.has(this.menuConfig, 'runtime.autoNext') && + true === this.menuConfig.runtime.autoNext + ) { + if (this.hasNextTimeout()) { + setTimeout(() => { return gotoNextMenu(); }, this.menuConfig.config.nextTimeout); } else { @@ -374,20 +410,23 @@ exports.MenuModule = class MenuModule extends PluginModule { function addViewControllers(callback) { _.forEach(mciData, (mciMap, name) => { assert('menu' === name || 'prompt' === name); - self.addViewController(name, new ViewController( { client : self.client } ) ); + self.addViewController( + name, + new ViewController({ client: self.client }) + ); }); return callback(null); }, function createMenu(callback) { - if(!self.viewControllers.menu) { + if (!self.viewControllers.menu) { return callback(null); } const menuLoadOpts = { - mciMap : mciData.menu, - callingMenu : self, - withoutForm : _.isObject(mciData.prompt), + mciMap: mciData.menu, + callingMenu: self, + withoutForm: _.isObject(mciData.prompt), }; self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { @@ -395,19 +434,22 @@ exports.MenuModule = class MenuModule extends PluginModule { }); }, function createPrompt(callback) { - if(!self.viewControllers.prompt) { + if (!self.viewControllers.prompt) { return callback(null); } const promptLoadOpts = { - callingMenu : self, - mciMap : mciData.prompt, + callingMenu: self, + mciMap: mciData.prompt, }; - self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { - return callback(err); - }); - } + self.viewControllers.prompt.loadFromPromptConfig( + promptLoadOpts, + err => { + return callback(err); + } + ); + }, ], err => { return cb(err); @@ -416,28 +458,27 @@ exports.MenuModule = class MenuModule extends PluginModule { } displayAsset(nameOrData, options, cb) { - if(_.isFunction(options)) { + if (_.isFunction(options)) { cb = options; options = {}; } - if(options.clearScreen) { + if (options.clearScreen) { this.client.term.rawWrite(ansi.resetScreen()); } - options = Object.assign( { client : this.client, font : this.menuConfig.config.font }, options ); + options = Object.assign( + { client: this.client, font: this.menuConfig.config.font }, + options + ); - if(Buffer.isBuffer(nameOrData)) { + if (Buffer.isBuffer(nameOrData)) { const data = iconvDecode(nameOrData, options.encoding || 'cp437'); - return theme.displayPreparedArt( - options, - { data }, - (err, artData) => { - if(cb) { - return cb(err, artData); - } + return theme.displayPreparedArt(options, { data }, (err, artData) => { + if (cb) { + return cb(err, artData); } - ); + }); } return theme.displayThemedAsset( @@ -445,7 +486,7 @@ exports.MenuModule = class MenuModule extends PluginModule { this.client, options, (err, artData) => { - if(cb) { + if (cb) { return cb(err, artData); } } @@ -454,18 +495,18 @@ exports.MenuModule = class MenuModule extends PluginModule { prepViewController(name, formId, mciMap, cb) { const needsCreated = _.isUndefined(this.viewControllers[name]); - if(needsCreated) { + if (needsCreated) { const vcOpts = { - client : this.client, - formId : formId, + client: this.client, + formId: formId, }; const vc = this.addViewController(name, new ViewController(vcOpts)); const loadOpts = { - callingMenu : this, - mciMap : mciMap, - formId : formId, + callingMenu: this, + mciMap: mciMap, + formId: formId, }; return vc.loadFromMenuConfig(loadOpts, err => { @@ -479,21 +520,17 @@ exports.MenuModule = class MenuModule extends PluginModule { } 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); + this.displayAsset(this.menuConfig.config.art[name], options, (err, artData) => { + if (err) { + return cb(err); } - ); + + return this.prepViewController(name, formId, artData.mciMap, cb); + }); } optionalMoveToPosition(position) { - if(position) { + if (position) { position.x = position.row || position.x || 1; position.y = position.col || position.y || 1; @@ -502,47 +539,53 @@ exports.MenuModule = class MenuModule extends PluginModule { } pausePrompt(position, cb) { - if(!cb && _.isFunction(position)) { + if (!cb && _.isFunction(position)) { cb = position; position = null; } this.optionalMoveToPosition(position); - return theme.displayThemedPause(this.client, {position}, cb); + return theme.displayThemedPause(this.client, { position }, cb); } - promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) { - if(!cb && _.isFunction(options)) { + promptForInput( + { formName, formId, promptName, prevFormName, position } = {}, + options, + cb + ) { + if (!cb && _.isFunction(options)) { cb = options; options = {}; } options.viewController = this.addViewController( formName, - new ViewController( { client : this.client, formId } ) + new ViewController({ client: this.client, formId }) ); options.trailingLF = _.get(options, 'trailingLF', false); let prevVc; - if(prevFormName) { + if (prevFormName) { prevVc = this.viewControllers[prevFormName]; - if(prevVc) { + if (prevVc) { prevVc.setFocus(false); } } //let artHeight; options.submitNotify = () => { - if(prevVc) { + if (prevVc) { prevVc.setFocus(true); } this.removeViewController(formName); - if(options.clearAtSubmit) { + if (options.clearAtSubmit) { this.optionalMoveToPosition(position); - if(options.clearWidth) { - this.client.term.rawWrite(`${ansi.reset()}${' '.repeat(options.clearWidth)}`); + if (options.clearWidth) { + this.client.term.rawWrite( + `${ansi.reset()}${' '.repeat(options.clearWidth)}` + ); } else { // :TODO: handle multi-rows via artHeight this.client.term.rawWrite(ansi.eraseLine()); @@ -565,11 +608,11 @@ exports.MenuModule = class MenuModule extends PluginModule { setViewText(formName, mciId, text, appendMultiLine) { const view = this.getView(formName, mciId); - if(!view) { + if (!view) { return; } - if(appendMultiLine && (view instanceof MultiLineEditTextView)) { + if (appendMultiLine && view instanceof MultiLineEditTextView) { view.addText(text); } else { view.setText(text); @@ -586,17 +629,26 @@ exports.MenuModule = class MenuModule extends PluginModule { let textView; let customMciId = startId; - const config = this.menuConfig.config; - const endId = options.endId || 99; // we'll fail to get a view before 99 + const config = this.menuConfig.config; + const endId = options.endId || 99; // we'll fail to get a view before 99 - while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { - const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" - const format = config[key]; + 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))) { + if ( + format && + (!options.filter || options.filter.find(f => format.indexOf(f) > -1)) + ) { const text = stringFormat(format, fmtObj); - if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { + if ( + options.appendMultiLine && + textView instanceof MultiLineEditTextView + ) { textView.addText(text); } else { textView.setText(text); @@ -608,10 +660,10 @@ exports.MenuModule = class MenuModule extends PluginModule { } refreshPredefinedMciViewsByCode(formName, mciCodes) { - const form = _.get(this, [ 'viewControllers', formName] ); - if(form) { + const form = _.get(this, ['viewControllers', formName]); + if (form) { form.getViewsByMciCode(mciCodes).forEach(v => { - if(!v.setText) { + if (!v.setText) { return; } @@ -621,15 +673,15 @@ exports.MenuModule = class MenuModule extends PluginModule { } validateMCIByViewIds(formName, viewIds, cb) { - if(!Array.isArray(viewIds)) { - viewIds = [ viewIds ]; + if (!Array.isArray(viewIds)) { + viewIds = [viewIds]; } - const form = _.get(this, [ 'viewControllers', formName ] ); - if(!form) { + const form = _.get(this, ['viewControllers', formName]); + if (!form) { return cb(Errors.DoesNotExist(`Form does not exist: ${formName}`)); } - for(let i = 0; i < viewIds.length; ++i) { - if(!form.hasView(viewIds[i])) { + for (let i = 0; i < viewIds.length; ++i) { + if (!form.hasView(viewIds[i])) { return cb(Errors.MissingMci(`Missing MCI ${viewIds[i]}`)); } } @@ -641,7 +693,7 @@ exports.MenuModule = class MenuModule extends PluginModule { // fields is expected to be { key : type || validator(key, config) } // where |type| is 'string', 'array', object', 'number' // - if(!_.isObject(fields)) { + if (!_.isObject(fields)) { return cb(Errors.Invalid('Invalid validator!')); } @@ -649,10 +701,10 @@ exports.MenuModule = class MenuModule extends PluginModule { let firstBadKey; let badReason; const good = _.every(fields, (type, key) => { - if(_.isFunction(type)) { - if(!type(key, config)) { + if (_.isFunction(type)) { + if (!type(key, config)) { firstBadKey = key; - badReason = 'Validate failure'; + badReason = 'Validate failure'; return false; } return true; @@ -660,30 +712,44 @@ exports.MenuModule = class MenuModule extends PluginModule { const c = config[key]; let typeOk; - if(_.isUndefined(c)) { + if (_.isUndefined(c)) { typeOk = false; badReason = `Missing "${key}", expected ${type}`; } else { - switch(type) { - case 'string' : typeOk = _.isString(c); break; - case 'object' : typeOk = _.isObject(c); break; - case 'array' : typeOk = Array.isArray(c); break; - case 'number' : typeOk = !isNaN(parseInt(c)); break; - default : + switch (type) { + case 'string': + typeOk = _.isString(c); + break; + case 'object': + typeOk = _.isObject(c); + break; + case 'array': + typeOk = Array.isArray(c); + break; + case 'number': + typeOk = !isNaN(parseInt(c)); + break; + default: typeOk = false; badReason = `Don't know how to validate ${type}`; break; } } - if(!typeOk) { + if (!typeOk) { firstBadKey = key; - if(!badReason) { + if (!badReason) { badReason = `Expected ${type}`; } } return typeOk; }); - return cb(good ? null : Errors.Invalid(`Invalid or missing config option "${firstBadKey}" (${badReason})`)); + return cb( + good + ? null + : Errors.Invalid( + `Invalid or missing config option "${firstBadKey}" (${badReason})` + ) + ); } }; diff --git a/core/menu_stack.js b/core/menu_stack.js index 42cd6987..961fdc7e 100644 --- a/core/menu_stack.js +++ b/core/menu_stack.js @@ -2,25 +2,20 @@ 'use strict'; // ENiGMA½ -const loadMenu = require('./menu_util.js').loadMenu; -const { - Errors, - ErrorReasons -} = require('./enig_error.js'); -const { - getResolvedSpec -} = require('./menu_util.js'); +const loadMenu = require('./menu_util.js').loadMenu; +const { Errors, ErrorReasons } = require('./enig_error.js'); +const { getResolvedSpec } = require('./menu_util.js'); // deps -const _ = require('lodash'); -const assert = require('assert'); +const _ = require('lodash'); +const assert = require('assert'); // :TODO: Stack is backwards.... top should be most recent! :) module.exports = class MenuStack { constructor(client) { this.client = client; - this.stack = []; + this.stack = []; } push(moduleInfo) { @@ -32,13 +27,13 @@ module.exports = class MenuStack { } peekPrev() { - if(this.stackSize > 1) { + if (this.stackSize > 1) { return this.stack[this.stack.length - 2]; } } top() { - if(this.stackSize > 0) { + if (this.stackSize > 0) { return this.stack[this.stack.length - 1]; } } @@ -55,47 +50,61 @@ module.exports = class MenuStack { next(cb) { const currentModuleInfo = this.top(); - const menuConfig = currentModuleInfo.instance.menuConfig; - const nextMenu = getResolvedSpec(this.client, menuConfig.next, 'next'); - if(!nextMenu) { - return cb(Array.isArray(menuConfig.next) ? - Errors.MenuStack('No matching condition for "next"', ErrorReasons.NoConditionMatch) : - Errors.MenuStack('Invalid or missing "next" member in menu config', ErrorReasons.InvalidNextMenu) + const menuConfig = currentModuleInfo.instance.menuConfig; + const nextMenu = getResolvedSpec(this.client, menuConfig.next, 'next'); + if (!nextMenu) { + return cb( + Array.isArray(menuConfig.next) + ? Errors.MenuStack( + 'No matching condition for "next"', + ErrorReasons.NoConditionMatch + ) + : Errors.MenuStack( + 'Invalid or missing "next" member in menu config', + ErrorReasons.InvalidNextMenu + ) ); } - if(nextMenu === currentModuleInfo.name) { - return cb(Errors.MenuStack('Menu config "next" specifies current menu', ErrorReasons.AlreadyThere)); + if (nextMenu === currentModuleInfo.name) { + return cb( + Errors.MenuStack( + 'Menu config "next" specifies current menu', + ErrorReasons.AlreadyThere + ) + ); } - this.goto(nextMenu, { }, cb); + this.goto(nextMenu, {}, cb); } prev(cb) { const menuResult = this.top().instance.getMenuResult(); // :TODO: leave() should really take a cb... - this.pop().instance.leave(); // leave & remove current + this.pop().instance.leave(); // leave & remove current - const previousModuleInfo = this.pop(); // get previous + const previousModuleInfo = this.pop(); // get previous - if(previousModuleInfo) { + if (previousModuleInfo) { const opts = { - extraArgs : previousModuleInfo.extraArgs, - savedState : previousModuleInfo.savedState, - lastMenuResult : menuResult, + extraArgs: previousModuleInfo.extraArgs, + savedState: previousModuleInfo.savedState, + lastMenuResult: menuResult, }; return this.goto(previousModuleInfo.name, opts, cb); } - return cb(Errors.MenuStack('No previous menu available', ErrorReasons.NoPreviousMenu)); + return cb( + Errors.MenuStack('No previous menu available', ErrorReasons.NoPreviousMenu) + ); } goto(name, options, cb) { const currentModuleInfo = this.top(); - if(!cb && _.isFunction(options)) { + if (!cb && _.isFunction(options)) { cb = options; options = {}; } @@ -103,19 +112,24 @@ module.exports = class MenuStack { options = options || {}; const self = this; - if(currentModuleInfo && name === currentModuleInfo.name) { - if(cb) { - cb(Errors.MenuStack('Already at supplied menu', ErrorReasons.AlreadyThere)); + if (currentModuleInfo && name === currentModuleInfo.name) { + if (cb) { + cb( + Errors.MenuStack( + 'Already at supplied menu', + ErrorReasons.AlreadyThere + ) + ); } return; } const loadOpts = { - name : name, - client : self.client, + name: name, + client: self.client, }; - if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { + if (currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { loadOpts.extraArgs = currentModuleInfo.extraArgs; } else { loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); @@ -123,15 +137,15 @@ module.exports = class MenuStack { loadOpts.lastMenuResult = options.lastMenuResult; loadMenu(loadOpts, (err, modInst) => { - if(err) { + 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'); + self.client.log.debug({ menuName: name }, 'Goto menu module'); - if(!this.client.acs.hasMenuModuleAccess(modInst)) { - if(cb) { + if (!this.client.acs.hasMenuModuleAccess(modInst)) { + if (cb) { return cb(Errors.AccessDenied('No access to this menu')); } return; @@ -141,12 +155,15 @@ module.exports = class MenuStack { // Handle deprecated 'options' block by merging to config and warning user. // :TODO: Remove in 0.0.10+ // - if(modInst.menuConfig.options) { + if (modInst.menuConfig.options) { self.client.log.warn( - { options : modInst.menuConfig.options }, + { options: modInst.menuConfig.options }, 'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions' ); - Object.assign(modInst.menuConfig.config || {}, modInst.menuConfig.options); + Object.assign( + modInst.menuConfig.config || {}, + modInst.menuConfig.options + ); delete modInst.menuConfig.options; } @@ -155,57 +172,63 @@ module.exports = class MenuStack { // anything supplied in code. // let menuFlags; - if(0 === modInst.menuConfig.config.menuFlags.length) { + if (0 === modInst.menuConfig.config.menuFlags.length) { menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : []; } else { menuFlags = modInst.menuConfig.config.menuFlags; // in code we can ask to merge in - if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { + if ( + Array.isArray(options.menuFlags) && + options.menuFlags.includes('mergeFlags') + ) { menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); } } - if(currentModuleInfo) { + if (currentModuleInfo) { // save stack state - currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); + currentModuleInfo.savedState = + currentModuleInfo.instance.getSaveState(); currentModuleInfo.instance.leave(); - if(currentModuleInfo.menuFlags.includes('noHistory')) { + 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, + name: name, + instance: modInst, + extraArgs: loadOpts.extraArgs, + menuFlags: menuFlags, }); // restore previous state if requested - if(options.savedState) { + if (options.savedState) { modInst.restoreSavedState(options.savedState); } const stackEntries = self.stack.map(stackEntry => { let name = stackEntry.name; - if(stackEntry.instance.menuConfig.config.menuFlags.length > 0) { - name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(', ')})`; + if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) { + name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join( + ', ' + )})`; } return name; }); - self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' ); + self.client.log.trace({ stack: stackEntries }, 'Updated menu stack'); modInst.enter(); - if(cb) { + if (cb) { cb(null); } } diff --git a/core/menu_util.js b/core/menu_util.js index 0943fe22..bfc97373 100644 --- a/core/menu_util.js +++ b/core/menu_util.js @@ -2,29 +2,29 @@ 'use strict'; // ENiGMA½ -const moduleUtil = require('./module_util.js'); -const Log = require('./logger.js').log; -const Config = require('./config.js').get; -const asset = require('./asset.js'); -const { MCIViewFactory } = require('./mci_view_factory.js'); -const { Errors } = require('./enig_error.js'); +const moduleUtil = require('./module_util.js'); +const Log = require('./logger.js').log; +const Config = require('./config.js').get; +const asset = require('./asset.js'); +const { MCIViewFactory } = require('./mci_view_factory.js'); +const { Errors } = require('./enig_error.js'); // deps -const paths = require('path'); -const async = require('async'); -const _ = require('lodash'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); -exports.loadMenu = loadMenu; -exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; -exports.handleAction = handleAction; -exports.getResolvedSpec = getResolvedSpec; -exports.handleNext = handleNext; +exports.loadMenu = loadMenu; +exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; +exports.handleAction = handleAction; +exports.getResolvedSpec = getResolvedSpec; +exports.handleNext = handleNext; function getMenuConfig(client, name, cb) { async.waterfall( [ function locateMenuConfig(callback) { - const menuConfig = _.get(client.currentTheme, [ 'menus', name ]); + const menuConfig = _.get(client.currentTheme, ['menus', name]); if (menuConfig) { return callback(null, menuConfig); } @@ -32,15 +32,18 @@ function getMenuConfig(client, name, cb) { return callback(Errors.DoesNotExist(`No menu entry for "${name}"`)); }, function locatePromptConfig(menuConfig, callback) { - if(_.isString(menuConfig.prompt)) { - if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { - menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; + if (_.isString(menuConfig.prompt)) { + if (_.has(client.currentTheme, ['prompts', menuConfig.prompt])) { + menuConfig.promptConfig = + client.currentTheme.prompts[menuConfig.prompt]; return callback(null, menuConfig); } - return callback(Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`)); + return callback( + Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`) + ); } return callback(null, menuConfig); - } + }, ], (err, menuConfig) => { return cb(err, menuConfig); @@ -50,7 +53,7 @@ function getMenuConfig(client, name, cb) { // :TODO: name/client should not be part of options - they are required always function loadMenu(options, cb) { - if(!_.isString(options.name) || !_.isObject(options.client)) { + if (!_.isString(options.name) || !_.isObject(options.client)) { return cb(Errors.MissingParam('Missing required options')); } @@ -62,27 +65,30 @@ function loadMenu(options, cb) { }); }, function loadMenuModule(menuConfig, callback) { - menuConfig.config = menuConfig.config || {}; menuConfig.config.menuFlags = menuConfig.config.menuFlags || []; - if(!Array.isArray(menuConfig.config.menuFlags)) { - menuConfig.config.menuFlags = [ menuConfig.config.menuFlags ]; + if (!Array.isArray(menuConfig.config.menuFlags)) { + menuConfig.config.menuFlags = [menuConfig.config.menuFlags]; } - const modAsset = asset.getModuleAsset(menuConfig.module); - const modSupplied = null !== modAsset; + const modAsset = asset.getModuleAsset(menuConfig.module); + const modSupplied = null !== modAsset; const modLoadOpts = { - name : modSupplied ? modAsset.asset : 'standard_menu', - path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, - category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', + name: modSupplied ? modAsset.asset : 'standard_menu', + path: + !modSupplied || 'systemModule' === modAsset.type + ? __dirname + : Config().paths.mods, + category: + !modSupplied || 'systemModule' === modAsset.type ? null : 'mods', }; moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => { const modData = { - name : modLoadOpts.name, - config : menuConfig, - mod : mod, + name: modLoadOpts.name, + config: menuConfig, + mod: mod, }; return callback(err, modData); @@ -90,24 +96,30 @@ function loadMenu(options, cb) { }, function createModuleInstance(modData, callback) { Log.trace( - { moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, - 'Creating menu module instance'); + { + 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, + menuName: options.name, + menuConfig: modData.config, + extraArgs: options.extraArgs, + client: options.client, + lastMenuResult: options.lastMenuResult, }); - } catch(e) { + } catch (e) { return callback(e); } return callback(null, moduleInstance); - } + }, ], (err, modInst) => { return cb(err, modInst); @@ -116,82 +128,99 @@ function loadMenu(options, cb) { } function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { - if(!_.isObject(menuConfig.form)) { + if (!_.isObject(menuConfig.form)) { return cb(Errors.MissingParam('Invalid or missing "form" member for menu')); } - if(!_.isObject(menuConfig.form[formId])) { + if (!_.isObject(menuConfig.form[formId])) { return cb(Errors.DoesNotExist(`No form found for formId ${formId}`)); } const formForId = menuConfig.form[formId]; - const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => { + 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'); + if (_.isObject(formForId[mciReqKey])) { + Log.trace({ mciKey: mciReqKey }, 'Using exact configuration key match'); return cb(null, formForId[mciReqKey]); } // // Generic match // - if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { + if (_.has(formForId, 'mci') || _.has(formForId, 'submit')) { Log.trace('Using generic configuration'); return cb(null, formForId); } - return cb(Errors.DoesNotExist(`No matching form configuration found for key "${mciReqKey}"`)); + return cb( + Errors.DoesNotExist(`No matching form configuration found for key "${mciReqKey}"`) + ); } // :TODO: Most of this should be moved elsewhere .... DRY... function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) { - if('' === paths.extname(path)) { + if ('' === paths.extname(path)) { path += '.js'; } try { client.log.trace( - { path : path, methodName : asset.asset, formData : formData, extraArgs : extraArgs }, - 'Calling menu method'); + { + 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 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) { - if(!_.isObject(conf)) { + if (!_.isObject(conf)) { return cb(Errors.MissingParam('Missing config')); } - const action = getResolvedSpec(client, conf.action, 'action'); // random/conditionals/etc. + const action = getResolvedSpec(client, conf.action, 'action'); // random/conditionals/etc. const actionAsset = asset.parseAsset(action); - if(!_.isObject(actionAsset)) { + if (!_.isObject(actionAsset)) { return cb(Errors.Invalid('Unable to parse "conf.action"')); } - switch(actionAsset.type) { - case 'method' : - case 'systemMethod' : - if(_.isString(actionAsset.location)) { + 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) { + 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( @@ -200,21 +229,30 @@ function handleAction(client, formData, conf, cb) { paths.join(__dirname, 'system_menu_method.js'), formData, conf.extraArgs, - cb); + 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); + if (_.isFunction(currentModule.menuMethods[actionAsset.asset])) { + return currentModule.menuMethods[actionAsset.asset]( + formData, + conf.extraArgs, + cb + ); } const err = Errors.DoesNotExist('Method does not exist'); - client.log.warn( { method : actionAsset.asset }, err.message); + 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 + ); } } @@ -237,15 +275,15 @@ function getResolvedSpec(client, spec, memberName) { // (3) Simple array of strings. A random selection will be made: // next: [ "foo", "baz", "fizzbang" ] // - if(!Array.isArray(spec)) { - return spec; // (1) simple string, as-is + if (!Array.isArray(spec)) { + return spec; // (1) simple string, as-is } - if(_.isObject(spec[0])) { - return client.acs.getConditionalValue(spec, memberName); // (2) ACS conditionals + if (_.isObject(spec[0])) { + return client.acs.getConditionalValue(spec, memberName); // (2) ACS conditionals } - return spec[Math.floor(Math.random() * spec.length)]; // (3) random + return spec[Math.floor(Math.random() * spec.length)]; // (3) random } function handleNext(client, nextSpec, conf, cb) { @@ -257,32 +295,54 @@ function handleNext(client, nextSpec, conf, cb) { 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) { + 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); + 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 ); + if (_.isFunction(currentModule.menuMethods[nextAsset.asset])) { + const formData = {}; // we don't have any + return currentModule.menuMethods[nextAsset.asset]( + formData, + extraArgs, + cb + ); } const err = Errors.DoesNotExist('Method does not exist'); - client.log.warn( { method : nextAsset.asset }, err.message); + 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 = Errors.Invalid('Invalid asset type for "next"'); - client.log.error( { nextSpec : nextSpec }, err.message); + client.log.error({ nextSpec: nextSpec }, err.message); return cb(err); } diff --git a/core/menu_view.js b/core/menu_view.js index 9c750aba..f16f696a 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -2,16 +2,16 @@ 'use strict'; // ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuView = MenuView; +exports.MenuView = MenuView; function MenuView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); @@ -23,7 +23,7 @@ function MenuView(options) { const self = this; - if(options.items) { + if (options.items) { this.setItems(options.items); } else { this.items = []; @@ -31,54 +31,61 @@ function MenuView(options) { this.renderCache = {}; - this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault( + options.caseInsensitiveHotKeys, + true + ); this.setHotKeys(options.hotKeys); this.focusedItemIndex = options.focusedItemIndex || 0; - this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; + this.focusedItemIndex = + this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) + ? options.itemHorizSpacing + : 0; // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.hasFocusItems = function() { + 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)) { + 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() { + this.emitIndexUpdate = function () { self.emit('index update', self.focusedItemIndex); }; } util.inherits(MenuView, View); -MenuView.prototype.setTextOverflow = function(overflow) { - this.textOverflow = overflow; - this.invalidateRenderCache(); -} +MenuView.prototype.setTextOverflow = function (overflow) { + this.textOverflow = overflow; + this.invalidateRenderCache(); +}; -MenuView.prototype.hasTextOverflow = function() { - return this.textOverflow != undefined; -} +MenuView.prototype.hasTextOverflow = function () { + return this.textOverflow != undefined; +}; -MenuView.prototype.setItems = function(items) { - if(Array.isArray(items)) { +MenuView.prototype.setItems = function (items) { + if (Array.isArray(items)) { this.sorted = false; this.renderCache = {}; @@ -97,7 +104,7 @@ MenuView.prototype.setItems = function(items) { let stringItem; this.items = items.map(item => { stringItem = _.isString(item); - if(stringItem) { + if (stringItem) { text = item; } else { text = item.text || ''; @@ -105,10 +112,10 @@ MenuView.prototype.setItems = function(items) { } text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others }); - if(this.complexItems) { + if (this.complexItems) { this.itemFormat = this.itemFormat || '{text}'; } @@ -116,58 +123,58 @@ MenuView.prototype.setItems = function(items) { } }; -MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { +MenuView.prototype.getRenderCacheItem = function (index, focusItem = false) { const item = this.renderCache[index]; return item && item[focusItem ? 'focus' : 'standard']; }; -MenuView.prototype.removeRenderCacheItem = function(index) { +MenuView.prototype.removeRenderCacheItem = function (index) { delete this.renderCache[index]; }; -MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { +MenuView.prototype.setRenderCacheItem = function (index, rendered, focusItem = false) { this.renderCache[index] = this.renderCache[index] || {}; this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; -MenuView.prototype.invalidateRenderCache = function() { +MenuView.prototype.invalidateRenderCache = function () { this.renderCache = {}; }; -MenuView.prototype.setSort = function(sort) { - if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { +MenuView.prototype.setSort = function (sort) { + if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { return; } const key = true === sort ? 'text' : sort; - if('text' !== sort && !this.complexItems) { + if ('text' !== sort && !this.complexItems) { return; // need a valid sort key } - this.items.sort( (a, b) => { + this.items.sort((a, b) => { const a1 = a[key]; const b1 = b[key]; - if(!a1) { + if (!a1) { return -1; } - if(!b1) { + if (!b1) { return 1; } - return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); + return a1.localeCompare(b1, { sensitivity: false, numeric: true }); }); this.sorted = true; }; -MenuView.prototype.removeItem = function(index) { +MenuView.prototype.removeItem = function (index) { this.sorted = false; this.items.splice(index, 1); - if(this.focusItems) { + if (this.focusItems) { this.focusItems.splice(index, 1); } - if(this.focusedItemIndex >= index) { + if (this.focusedItemIndex >= index) { this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); } @@ -176,62 +183,62 @@ MenuView.prototype.removeItem = function(index) { this.positionCacheExpired = true; }; -MenuView.prototype.getCount = function() { +MenuView.prototype.getCount = function () { return this.items.length; }; -MenuView.prototype.getItems = function() { - if(this.complexItems) { +MenuView.prototype.getItems = function () { + if (this.complexItems) { return this.items; } - return this.items.map( item => { + return this.items.map(item => { return item.text; }); }; -MenuView.prototype.getItem = function(index) { - if(this.complexItems) { +MenuView.prototype.getItem = function (index) { + if (this.complexItems) { return this.items[index]; } return this.items[index].text; }; -MenuView.prototype.focusNext = function() { +MenuView.prototype.focusNext = function () { this.emitIndexUpdate(); }; -MenuView.prototype.focusPrevious = function() { +MenuView.prototype.focusPrevious = function () { this.emitIndexUpdate(); }; -MenuView.prototype.focusNextPageItem = function() { +MenuView.prototype.focusNextPageItem = function () { this.emitIndexUpdate(); }; -MenuView.prototype.focusPreviousPageItem = function() { +MenuView.prototype.focusPreviousPageItem = function () { this.emitIndexUpdate(); }; -MenuView.prototype.focusFirst = function() { +MenuView.prototype.focusFirst = function () { this.emitIndexUpdate(); }; -MenuView.prototype.focusLast = function() { +MenuView.prototype.focusLast = function () { this.emitIndexUpdate(); }; -MenuView.prototype.setFocusItemIndex = function(index) { +MenuView.prototype.setFocusItemIndex = function (index) { this.focusedItemIndex = index; }; -MenuView.prototype.onKeyPress = function(ch, key) { +MenuView.prototype.onKeyPress = function (ch, key) { const itemIndex = this.getHotKeyItemIndex(ch); - if(itemIndex >= 0) { + if (itemIndex >= 0) { this.setFocusItemIndex(itemIndex); - if(true === this.hotKeySubmit) { + if (true === this.hotKeySubmit) { this.emit('action', 'accept'); } } @@ -239,79 +246,99 @@ MenuView.prototype.onKeyPress = function(ch, key) { MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; -MenuView.prototype.setFocusItems = function(items) { +MenuView.prototype.setFocusItems = function (items) { const self = this; - if(items) { + if (items) { this.focusItems = []; - items.forEach( itemText => { - this.focusItems.push( - { - text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); + items.forEach(itemText => { + this.focusItems.push({ + text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client), + }); }); } }; -MenuView.prototype.setItemSpacing = function(itemSpacing) { +MenuView.prototype.setItemSpacing = function (itemSpacing) { itemSpacing = parseInt(itemSpacing); assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; -MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { +MenuView.prototype.setItemHorizSpacing = function (itemHorizSpacing) { itemHorizSpacing = parseInt(itemHorizSpacing); assert(_.isNumber(itemHorizSpacing)); - this.itemHorizSpacing = itemHorizSpacing; - this.positionCacheExpired = true; + this.itemHorizSpacing = itemHorizSpacing; + this.positionCacheExpired = true; }; -MenuView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'itemSpacing' : this.setItemSpacing(value); break; - case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; - case 'items' : this.setItems(value); break; - case 'focusItems' : this.setFocusItems(value); break; - case 'hotKeys' : this.setHotKeys(value); break; - case 'textOverflow' : this.setTextOverflow(value); break; - case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.setJustify(value); break; - case 'fillChar' : this.setFillChar(value); break; - case 'focusItemIndex' : this.focusedItemIndex = value; break; +MenuView.prototype.setPropertyValue = function (propName, value) { + switch (propName) { + case 'itemSpacing': + this.setItemSpacing(value); + break; + case 'itemHorizSpacing': + this.setItemHorizSpacing(value); + break; + case 'items': + this.setItems(value); + break; + case 'focusItems': + this.setFocusItems(value); + break; + case 'hotKeys': + this.setHotKeys(value); + break; + case 'textOverflow': + this.setTextOverflow(value); + break; + case 'hotKeySubmit': + this.hotKeySubmit = value; + break; + case 'justify': + this.setJustify(value); + break; + case 'fillChar': + this.setFillChar(value); + break; + case 'focusItemIndex': + this.focusedItemIndex = value; + break; - case 'itemFormat' : - case 'focusItemFormat' : + case 'itemFormat': + case 'focusItemFormat': this[propName] = value; // if there is a cache currently, invalidate it this.invalidateRenderCache(); break; - case 'sort' : this.setSort(value); break; + case 'sort': + this.setSort(value); + break; } MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; -MenuView.prototype.setFillChar = function(fillChar) { - this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); - this.invalidateRenderCache(); -} +MenuView.prototype.setFillChar = function (fillChar) { + this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); + this.invalidateRenderCache(); +}; -MenuView.prototype.setJustify = function(justify) { - this.justify = justify; - this.invalidateRenderCache(); - this.positionCacheExpired = true; -} +MenuView.prototype.setJustify = function (justify) { + this.justify = justify; + this.invalidateRenderCache(); + this.positionCacheExpired = true; +}; -MenuView.prototype.setHotKeys = function(hotKeys) { - if(_.isObject(hotKeys)) { - if(this.caseInsensitiveHotKeys) { +MenuView.prototype.setHotKeys = function (hotKeys) { + if (_.isObject(hotKeys)) { + if (this.caseInsensitiveHotKeys) { this.hotKeys = {}; - for(var key in hotKeys) { + for (var key in hotKeys) { this.hotKeys[key.toLowerCase()] = hotKeys[key]; } } else { @@ -319,4 +346,3 @@ MenuView.prototype.setHotKeys = function(hotKeys) { } } }; - diff --git a/core/message.js b/core/message.js index e98baec2..3c0b9c00 100644 --- a/core/message.js +++ b/core/message.js @@ -1,152 +1,153 @@ /* jslint node: true */ 'use strict'; -const msgDb = require('./database.js').dbs.message; -const wordWrapText = require('./word_wrap.js').wordWrapText; -const ftnUtil = require('./ftn_util.js'); -const createNamedUUID = require('./uuid_util.js').createNamedUUID; -const Errors = require('./enig_error.js').Errors; -const ANSI = require('./ansi_term.js'); -const { - sanitizeString, - getISOTimestampString } = require('./database.js'); +const msgDb = require('./database.js').dbs.message; +const wordWrapText = require('./word_wrap.js').wordWrapText; +const ftnUtil = require('./ftn_util.js'); +const createNamedUUID = require('./uuid_util.js').createNamedUUID; +const Errors = require('./enig_error.js').Errors; +const ANSI = require('./ansi_term.js'); +const { sanitizeString, getISOTimestampString } = require('./database.js'); const { isCP437Encodable } = require('./cp437util'); const { containsNonLatinCodepoints } = require('./string_util'); const { - isAnsi, isFormattedLine, + isAnsi, + isFormattedLine, splitTextAtTerms, - renderSubstr -} = require('./string_util.js'); + renderSubstr, +} = require('./string_util.js'); -const ansiPrep = require('./ansi_prep.js'); +const ansiPrep = require('./ansi_prep.js'); // deps -const uuidParse = require('uuid-parse'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); -const moment = require('moment'); -const iconvEncode = require('iconv-lite').encode; +const uuidParse = require('uuid-parse'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); +const moment = require('moment'); +const iconvEncode = require('iconv-lite').encode; -const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); +const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse( + '154506df-1df8-46b9-98f8-ebb5815baaf8' +); const WELL_KNOWN_AREA_TAGS = { - Invalid : '', - Private : 'private_mail', - Bulletin : 'local_bulletin', + Invalid: '', + Private: 'private_mail', + Bulletin: 'local_bulletin', }; const SYSTEM_META_NAMES = { - LocalToUserID : 'local_to_user_id', - LocalFromUserID : 'local_from_user_id', - StateFlags0 : 'state_flags0', // See Message.StateFlags0 - ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. - ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor - RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address - RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address + LocalToUserID: 'local_to_user_id', + LocalFromUserID: 'local_from_user_id', + StateFlags0: 'state_flags0', // See Message.StateFlags0 + ExplicitEncoding: 'explicit_encoding', // Explicitly set encoding when exporting/etc. + ExternalFlavor: 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor + RemoteToUser: 'remote_to_user', // Opaque value depends on external system, e.g. FTN address + RemoteFromUser: 'remote_from_user', // Opaque value depends on external system, e.g. FTN address }; // Types for Message.SystemMetaNames.ExternalFlavor meta const ADDRESS_FLAVOR = { - Local : 'local', // local / non-remote addressing - FTN : 'ftn', // FTN style - Email : 'email', // From email - QWK : 'qwk', // QWK packet + Local: 'local', // local / non-remote addressing + FTN: 'ftn', // FTN style + Email: 'email', // From email + QWK: 'qwk', // QWK packet }; 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', + 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', + 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', + 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 }; const QWKPropertyNames = { - MessageNumber : 'qwk_msg_num', - MessageStatus : 'qwk_msg_status', // See http://wiki.synchro.net/ref:qwk for a decent list - ConferenceNumber : 'qwk_conf_num', - InReplyToNum : 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available + MessageNumber: 'qwk_msg_num', + MessageStatus: 'qwk_msg_status', // See http://wiki.synchro.net/ref:qwk for a decent list + ConferenceNumber: 'qwk_conf_num', + InReplyToNum: 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available }; // :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.messageUuid = 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.messageUuid = uuid; + this.replyToMsgId = replyToMsgId; + this.toUserName = toUserName; + this.fromUserName = fromUserName; + this.subject = subject; + this.message = message; - if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { + if (_.isDate(modTimestamp) || _.isString(modTimestamp)) { modTimestamp = moment(modTimestamp); } this.modTimestamp = modTimestamp || moment(); this.meta = {}; - _.defaultsDeep(this.meta, { System : {} }, meta); + _.defaultsDeep(this.meta, { System: {} }, meta); - this.hashTags = hashTags; + this.hashTags = hashTags; } - get uuid() { // deprecated, will be removed in the near future + get uuid() { + // deprecated, will be removed in the near future return this.messageUuid; } - 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; @@ -161,17 +162,21 @@ module.exports = class Message { } isCP437Encodable() { - return isCP437Encodable(this.toUserName) && + return ( + isCP437Encodable(this.toUserName) && isCP437Encodable(this.fromUserName) && isCP437Encodable(this.subject) && - isCP437Encodable(this.message); + isCP437Encodable(this.message) + ); } containsNonLatinCodepoints() { - return containsNonLatinCodepoints(this.toUserName) || + return ( + containsNonLatinCodepoints(this.toUserName) || containsNonLatinCodepoints(this.fromUserName) || containsNonLatinCodepoints(this.subject) || - containsNonLatinCodepoints(this.message); + containsNonLatinCodepoints(this.message) + ); } /* @@ -196,7 +201,9 @@ module.exports = class Message { */ userHasDeleteRights(user) { - const messageLocalUserId = parseInt(this.meta.System[Message.SystemMetaNames.LocalToUserID]); + const messageLocalUserId = parseInt( + this.meta.System[Message.SystemMetaNames.LocalToUserID] + ); return (this.isPrivate() && user.userId === messageLocalUserId) || user.isSysOp(); } @@ -250,16 +257,24 @@ module.exports = class Message { assert(_.isString(subject)); assert(_.isString(body)); - if(!moment.isMoment(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) { @@ -308,31 +323,40 @@ module.exports = class Message { static findMessages(filter, cb) { filter = filter || {}; - filter.resultType = filter.resultType || 'id'; - filter.extraFields = filter.extraFields || []; - filter.operator = filter.operator || 'AND'; + filter.resultType = filter.resultType || 'id'; + filter.extraFields = filter.extraFields || []; + filter.operator = filter.operator || 'AND'; - 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'; - if(moment.isMoment(filter.newerThanTimestamp)) { + if (moment.isMoment(filter.newerThanTimestamp)) { filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); } let sql; - if('count' === filter.resultType) { - sql = - `SELECT COUNT() AS count + if ('count' === filter.resultType) { + sql = `SELECT COUNT() AS count FROM message m`; - } else { - sql = - `SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} + sql = `SELECT DISTINCT m.${field}${ + filter.extraFields.length > 0 + ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') + : '' + } FROM message m`; } @@ -341,7 +365,7 @@ module.exports = class Message { let sqlWhere = ''; function appendWhereClause(clause, op) { - if(sqlWhere) { + if (sqlWhere) { sqlWhere += ` ${op || filter.operator} `; } else { sqlWhere += ' WHERE '; @@ -350,40 +374,41 @@ module.exports = class Message { } // currently only avail sort - if('modTimestamp' === filter.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)) { + if (Array.isArray(filter.ids)) { appendWhereClause(`m.message_id IN (${filter.ids.join(', ')})`); } - if(Array.isArray(filter.uuids)) { + if (Array.isArray(filter.uuids)) { const uuidList = filter.uuids.map(u => `"${u}"`).join(', '); appendWhereClause(`m.message_id IN (${uuidList})`); } - - if(_.isNumber(filter.privateTagUserId)) { + 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 (filter.areaTag && filter.areaTag.length > 0) { if (!Array.isArray(filter.areaTag)) { - filter.areaTag = [ filter.areaTag ]; + filter.areaTag = [filter.areaTag]; } const areaList = filter.areaTag .filter(t => t !== Message.WellKnownAreaTags.Private) - .map(t => `"${t}"`).join(', '); - if(areaList.length > 0) { + .map(t => `"${t}"`) + .join(', '); + if (areaList.length > 0) { appendWhereClause(`m.area_tag IN(${areaList})`); } else { // nothing to do; no areas remain @@ -391,42 +416,59 @@ module.exports = class Message { } } else { // explicit exclude of Private - appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`, 'AND'); + appendWhereClause( + `m.area_tag != "${Message.WellKnownAreaTags.Private}"`, + 'AND' + ); } } - if(_.isNumber(filter.replyToMessageId)) { + if (_.isNumber(filter.replyToMessageId)) { appendWhereClause(`m.reply_to_message_id=${filter.replyToMessageId}`); } - [ 'toUserName', 'fromUserName' ].forEach(field => { + ['toUserName', 'fromUserName'].forEach(field => { let val = filter[field]; - if(!val) { + if (!val) { return; // next item } - if(_.isString(val)) { - val = [ val ]; + if (_.isString(val)) { + val = [val]; } - if(Array.isArray(val)) { - val = '(' + val.map(v => { - return `m.${_.snakeCase(field)} LIKE "${sanitizeString(v)}"`; - }).join(' OR ') + ')'; + if (Array.isArray(val)) { + val = + '(' + + val + .map(v => { + return `m.${_.snakeCase(field)} LIKE "${sanitizeString(v)}"`; + }) + .join(' OR ') + + ')'; appendWhereClause(val); } }); - if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { + if ( + _.isString(filter.newerThanTimestamp) && + filter.newerThanTimestamp.length > 0 + ) { // :TODO: should be using "localtime" here? - appendWhereClause(`DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); - } else if(moment.isMoment(filter.date)) { - appendWhereClause(`DATE(m.modified_timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`); + appendWhereClause( + `DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")` + ); + } else if (moment.isMoment(filter.date)) { + appendWhereClause( + `DATE(m.modified_timestamp, "localtime") = DATE("${filter.date.format( + 'YYYY-MM-DD' + )}")` + ); } - if(_.isNumber(filter.newerThanMessageId)) { + if (_.isNumber(filter.newerThanMessageId)) { appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); } - if(filter.terms && filter.terms.length > 0) { + 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 ( @@ -437,10 +479,14 @@ module.exports = class Message { ); } - if(Array.isArray(filter.metaTuples)) { + if (Array.isArray(filter.metaTuples)) { let sub = []; filter.metaTuples.forEach(mt => { - sub.push(`(meta_category = "${mt.category}" AND meta_name = "${mt.name}" AND meta_value = "${sanitizeString(mt.value)}")`); + sub.push( + `(meta_category = "${mt.category}" AND meta_name = "${ + mt.name + }" AND meta_value = "${sanitizeString(mt.value)}")` + ); }); sub = sub.join(` ${filter.operator} `); appendWhereClause( @@ -454,13 +500,13 @@ module.exports = class Message { sql += `${sqlWhere} ${sqlOrderBy}`; - if(_.isNumber(filter.limit)) { + if (_.isNumber(filter.limit)) { sql += ` LIMIT ${filter.limit}`; } sql += ';'; - if('count' === filter.resultType) { + if ('count' === filter.resultType) { msgDb.get(sql, (err, row) => { return cb(err, row ? row.count : 0); }); @@ -468,15 +514,22 @@ module.exports = class Message { 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]); + msgDb.each( + sql, + (err, row) => { + if (_.isObject(row)) { + matches.push(extra ? rowConv(row) : row[field]); + } + }, + err => { + return cb(err, matches); } - }, err => { - return cb(err, matches); - }); + ); } } @@ -487,13 +540,13 @@ module.exports = class Message { FROM message WHERE message_uuid = ? LIMIT 1;`, - [ uuid ], + [uuid], (err, row) => { - if(err) { + if (err) { return cb(err); } - const success = (row && row.message_id); + const success = row && row.message_id; return cb( success ? null : Errors.DoesNotExist(`No message for UUID ${uuid}`), success ? row.message_id : null @@ -508,37 +561,42 @@ module.exports = class Message { `SELECT message_id FROM message_meta WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, - [ category, name, value ], + [category, name, value], (err, rows) => { - if(err) { + if (err) { return cb(err); } - return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) + return cb( + null, + rows.map(r => parseInt(r.message_id)) + ); // return array of ID(s) } ); } static getMetaValuesByMessageId(messageId, category, name, cb) { - const sql = - `SELECT meta_value + 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) { + msgDb.all(sql, [messageId, category, name], (err, rows) => { + if (err) { return cb(err); } - if(0 === rows.length) { + if (0 === rows.length) { return cb(Errors.DoesNotExist('No value for category/name')); } // single values are returned without an array - if(1 === rows.length) { + 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 }); } @@ -551,10 +609,15 @@ module.exports = class Message { }); }, function getMetaValues(messageId, callback) { - Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { - return callback(err, values); - }); - } + Message.getMetaValuesByMessageId( + messageId, + category, + name, + (err, values) => { + return callback(err, values); + } + ); + }, ], (err, values) => { return cb(err, values); @@ -575,30 +638,36 @@ module.exports = class Message { } } */ - const sql = - `SELECT meta_category, meta_name, meta_value + 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])) { + 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(_.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] ]; - } + 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); + self.meta[row.meta_category][row.meta_name].push(row.meta_value); + } } + }, + err => { + return cb(err); } - }, err => { - return cb(err); - }); + ); } load(loadWith, cb) { @@ -616,27 +685,31 @@ module.exports = class Message { FROM message WHERE ${whereField} = ? LIMIT 1;`, - [ loadWith.uuid || loadWith.messageId ], + [loadWith.uuid || loadWith.messageId], (err, msgRow) => { - if(err) { + 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.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; // We use parseZone() to *preserve* the time zone information - self.modTimestamp = moment.parseZone(msgRow.modified_timestamp); + self.modTimestamp = moment.parseZone( + msgRow.modified_timestamp + ); return callback(err); } @@ -650,7 +723,7 @@ module.exports = class Message { function loadHashTags(callback) { // :TODO: return callback(null); - } + }, ], err => { return cb(err); @@ -659,32 +732,37 @@ module.exports = class Message { } persistMetaValue(category, name, value, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + if (!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; transOrDb = msgDb; } const metaStmt = transOrDb.prepare( `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) - VALUES (?, ?, ?, ?);`); + VALUES (?, ?, ?, ?);` + ); - if(!_.isArray(value)) { - value = [ value ]; + if (!_.isArray(value)) { + value = [value]; } const self = this; - async.each(value, (v, next) => { - metaStmt.run(self.messageId, category, name, v, err => { - return next(err); - }); - }, err => { - return cb(err); - }); + 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()) { + if (!this.isValid()) { return cb(Errors.Invalid('Cannot persist invalid message!')); } @@ -697,7 +775,7 @@ module.exports = class Message { }, function storeMessage(trans, callback) { // generate a UUID for this message if required (general case) - if(!self.messageUuid) { + if (!self.messageUuid) { self.messageUuid = Message.createMessageUUID( self.areaTag, self.modTimestamp, @@ -710,11 +788,18 @@ module.exports = class Message { `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.messageUuid, self.replyToMsgId, self.toUserName, - self.fromUserName, self.subject, self.message, getISOTimestampString(self.modTimestamp) + self.areaTag, + self.messageUuid, + self.replyToMsgId, + self.toUserName, + self.fromUserName, + self.subject, + self.message, + getISOTimestampString(self.modTimestamp), ], - function inserted(err) { // use non-arrow function for 'this' scope - if(!err) { + function inserted(err) { + // use non-arrow function for 'this' scope + if (!err) { self.messageId = this.lastID; } @@ -723,7 +808,7 @@ module.exports = class Message { ); }, function storeMeta(trans, callback) { - if(!self.meta) { + if (!self.meta) { return callback(null, trans); } /* @@ -738,26 +823,39 @@ 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); - }); - - }, err => { - return callback(err, trans); - }); + 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) { + if (trans) { trans[err ? 'rollback' : 'commit'](transErr => { return cb(err ? err : transErr, self.messageId); }); @@ -769,14 +867,16 @@ module.exports = class Message { } deleteMessage(requestingUser, cb) { - if(!this.userHasDeleteRights(requestingUser)) { - return cb(Errors.AccessDenied('User does not have rights to delete this message')); + if (!this.userHasDeleteRights(requestingUser)) { + return cb( + Errors.AccessDenied('User does not have rights to delete this message') + ); } msgDb.run( `DELETE FROM message WHERE message_uuid = ?;`, - [ this.messageUuid ], + [this.messageUuid], err => { return cb(err); } @@ -796,15 +896,19 @@ module.exports = class Message { } getQuoteLines(options, cb) { - if(!options.termWidth || !options.termHeight || !options.cols) { + 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 @@ -817,19 +921,23 @@ 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}` : ''; const wrapOpts = { - width : options.cols - (quotePrefix.length + extraPrefix.length), - tabHandling : 'expand', - tabWidth : 4, + width: options.cols - (quotePrefix.length + extraPrefix.length), + tabHandling: 'expand', + tabWidth: 4, }; - return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { - return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; + return wordWrapText(text, wrapOpts).wrapped.map((w, i) => { + return i === 0 + ? `${quotePrefix}${w}` + : `${quotePrefix}${extraPrefix}${w}`; }); } @@ -838,7 +946,7 @@ module.exports = class Message { let newLen; const total = line.length + quotePrefix.length; - if(total > options.cols) { + if (total > options.cols) { newLen = options.cols - total; } else { newLen = total; @@ -847,16 +955,16 @@ module.exports = class Message { return `${quotePrefix}${line.slice(0, newLen)}`; } - if(options.isAnsi) { + 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, + termWidth: options.termWidth, + termHeight: options.termHeight, + cols: options.cols, + rows: 'auto', + startCol: options.startCol, + forceLineTerm: true, }, (err, prepped) => { prepped = prepped || this.message; @@ -864,8 +972,8 @@ module.exports = class Message { let lastSgr = ''; const split = splitTextAtTerms(prepped); - const quoteLines = []; - const focusQuoteLines = []; + const quoteLines = []; + const focusQuoteLines = []; // // Do not include quote prefixes (e.g. XX> ) on ANSI replies (and therefor quote builder) @@ -876,8 +984,17 @@ module.exports = class Message { 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; @@ -886,95 +1003,104 @@ module.exports = class Message { } ); } else { - const QUOTE_RE = /^ ((?:[A-Za-z0-9]{1,2}> )+(?:[A-Za-z0-9]{1,2}>)*) */; - const quoted = []; - const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex + const QUOTE_RE = /^ ((?:[A-Za-z0-9]{1,2}> )+(?:[A-Za-z0-9]{1,2}>)*) */; + const quoted = []; + const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex // find *last* tearline let tearLinePos = Message.getTearLinePosition(input); - tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string + tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string - input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { - // - // For each paragraph, a state machine: - // - New line - line - // - New (pre)quoted line - quote_line - // - Continuation of new/quoted line - // - // 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 separation. - // - // 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); + if (quoted.length > 0) { + // + // Preserve paragraph separation. + // + // FSC-0032 states something about leaving blank lines fully blank + // (without a prefix) but it seems nicer (and more consistent with other systems) + // to put 'em in. + // + quoted.push(quotePrefix); } - quoteMatch = line.match(QUOTE_RE); + paragraph.split(/\r?\n/).forEach(line => { + if (0 === line.trim().length) { + // see blank line notes above + return quoted.push(quotePrefix); + } - switch(state) { - case 'line' : - if(quoteMatch) { - if(isFormattedLine(line)) { - quoted.push(getFormattedLine(line.replace(/\s/, ''))); + quoteMatch = line.match(QUOTE_RE); + + switch (state) { + case 'line': + if (quoteMatch) { + if (isFormattedLine(line)) { + quoted.push( + getFormattedLine(line.replace(/\s/, '')) + ); + } else { + quoted.push(...getWrapped(buf, quoteMatch[1])); + state = 'quote_line'; + buf = line; + } } else { - quoted.push(...getWrapped(buf, quoteMatch[1])); - state = 'quote_line'; + buf += ` ${line}`; + } + break; + + case 'quote_line': + if (quoteMatch) { + const rem = line.slice(quoteMatch[0].length); + if (!buf.startsWith(quoteMatch[0])) { + quoted.push(...getWrapped(buf, quoteMatch[1])); + buf = rem; + } else { + buf += ` ${rem}`; + } + } else { + quoted.push(...getWrapped(buf)); buf = line; + state = 'line'; } - } else { - buf += ` ${line}`; - } - break; + break; - case 'quote_line' : - if(quoteMatch) { - const rem = line.slice(quoteMatch[0].length); - if(!buf.startsWith(quoteMatch[0])) { - quoted.push(...getWrapped(buf, quoteMatch[1])); - buf = rem; + default: + if (isFormattedLine(line)) { + quoted.push(getFormattedLine(line)); } else { - buf += ` ${rem}`; + state = quoteMatch ? 'quote_line' : 'line'; + buf = + 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any } - } else { - quoted.push(...getWrapped(buf)); - buf = line; - state = 'line'; - } - break; + break; + } + }); - default : - if(isFormattedLine(line)) { - quoted.push(getFormattedLine(line)); - } else { - state = quoteMatch ? 'quote_line' : 'line'; - buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any - } - break; - } + quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); }); - quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); - }); - - input.slice(tearLinePos).split(/\r?\n/).forEach(l => { - quoted.push(...getWrapped(l)); - }); + input + .slice(tearLinePos) + .split(/\r?\n/) + .forEach(l => { + quoted.push(...getWrapped(l)); + }); return cb(null, quoted, null, false); } diff --git a/core/message_area.js b/core/message_area.js index 30ad14ed..76687aae 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -2,71 +2,80 @@ 'use strict'; // ENiGMA½ -const msgDb = require('./database.js').dbs.message; -const Config = require('./config.js').get; -const Message = require('./message.js'); -const Log = require('./logger.js').log; -const msgNetRecord = require('./msg_network.js').recordMessage; -const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; -const UserProps = require('./user_property.js'); -const StatLog = require('./stat_log.js'); -const SysProps = require('./system_property.js'); +const msgDb = require('./database.js').dbs.message; +const Config = require('./config.js').get; +const Message = require('./message.js'); +const Log = require('./logger.js').log; +const msgNetRecord = require('./msg_network.js').recordMessage; +const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; +const UserProps = require('./user_property.js'); +const StatLog = require('./stat_log.js'); +const SysProps = require('./system_property.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); -const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); +const moment = require('moment'); -exports.startup = startup; -exports.shutdown = shutdown; -exports.getAvailableMessageConferences = getAvailableMessageConferences; -exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; -exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; +exports.startup = startup; +exports.shutdown = shutdown; +exports.getAvailableMessageConferences = getAvailableMessageConferences; +exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; +exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag; -exports.getAllAvailableMessageAreaTags = getAllAvailableMessageAreaTags; -exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag; -exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag; -exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags; -exports.getMessageConferenceByTag = getMessageConferenceByTag; -exports.getMessageAreaByTag = getMessageAreaByTag; -exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag; -exports.changeMessageConference = changeMessageConference; -exports.changeMessageArea = changeMessageArea; -exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; -exports.hasMessageConfAndAreaWrite = hasMessageConfAndAreaWrite; -exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS; -exports.filterMessageListByReadACS = filterMessageListByReadACS; -exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; -exports.getMessageListForArea = getMessageListForArea; -exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; -exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; -exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; -exports.getMessageAreaLastReadId = getMessageAreaLastReadId; -exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; -exports.persistMessage = persistMessage; -exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; +exports.getAllAvailableMessageAreaTags = getAllAvailableMessageAreaTags; +exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag; +exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag; +exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags; +exports.getMessageConferenceByTag = getMessageConferenceByTag; +exports.getMessageAreaByTag = getMessageAreaByTag; +exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag; +exports.changeMessageConference = changeMessageConference; +exports.changeMessageArea = changeMessageArea; +exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; +exports.hasMessageConfAndAreaWrite = hasMessageConfAndAreaWrite; +exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS; +exports.filterMessageListByReadACS = filterMessageListByReadACS; +exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; +exports.getMessageListForArea = getMessageListForArea; +exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; +exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; +exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; +exports.getMessageAreaLastReadId = getMessageAreaLastReadId; +exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; +exports.persistMessage = persistMessage; +exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; function startup(cb) { // by default, private messages are NOT included async.series( [ - (callback) => { - Message.findMessages( { resultType : 'count' }, (err, count) => { - if(count) { - StatLog.setNonPersistentSystemStat(SysProps.MessageTotalCount, count); + callback => { + Message.findMessages({ resultType: 'count' }, (err, count) => { + if (count) { + StatLog.setNonPersistentSystemStat( + SysProps.MessageTotalCount, + count + ); } return callback(err); }); }, - (callback) => { - Message.findMessages( { resultType : 'count', date : moment() }, (err, count) => { - if(count) { - StatLog.setNonPersistentSystemStat(SysProps.MessagesToday, count); + callback => { + Message.findMessages( + { resultType: 'count', date: moment() }, + (err, count) => { + if (count) { + StatLog.setNonPersistentSystemStat( + SysProps.MessagesToday, + count + ); + } + return callback(err); } - return callback(err); - }); - } + ); + }, ], err => { return cb(err); @@ -79,13 +88,13 @@ function shutdown(cb) { } function getAvailableMessageConferences(client, options) { - options = options || { includeSystemInternal : false }; + options = options || { includeSystemInternal: false }; assert(client || true === options.noClient); // perform ACS check per conf & omit system_internal if desired return _.omitBy(Config().messageConferences, (conf, confTag) => { - if(!options.includeSystemInternal && 'system_internal' === confTag) { + if (!options.includeSystemInternal && 'system_internal' === confTag) { return true; } @@ -96,8 +105,8 @@ function getAvailableMessageConferences(client, options) { function getSortedAvailMessageConferences(client, options) { const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => { return { - confTag : k, - conf : v, + confTag: k, + conf: v, }; }); @@ -113,10 +122,10 @@ function getAvailableMessageAreasByConfTag(confTag, options) { // :TODO: confTag === "" then find default const config = Config(); - if(_.has(config.messageConferences, [ confTag, 'areas' ])) { + if (_.has(config.messageConferences, [confTag, 'areas'])) { const areas = config.messageConferences[confTag].areas; - if(!options.client || true === options.noAcsCheck) { + if (!options.client || true === options.noAcsCheck) { // everything - no ACS checks return areas; } else { @@ -130,9 +139,9 @@ function getAvailableMessageAreasByConfTag(confTag, options) { function getSortedAvailMessageAreasByConfTag(confTag, options) { const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => { - return { - areaTag : k, - area : v, + return { + areaTag: k, + area: v, }; }); @@ -145,11 +154,13 @@ function getAllAvailableMessageAreaTags(client, options) { const areaTags = []; // mask over older messy APIs for now - const confOpts = Object.assign({}, options, { noClient : client ? false : true }); + const confOpts = Object.assign({}, options, { noClient: client ? false : true }); const areaOpts = Object.assign({}, options, { client }); Object.keys(getAvailableMessageConferences(client, confOpts)).forEach(confTag => { - areaTags.push(...Object.keys(getAvailableMessageAreasByConfTag(confTag, areaOpts))); + areaTags.push( + ...Object.keys(getAvailableMessageAreasByConfTag(confTag, areaOpts)) + ); }); return areaTags; @@ -170,16 +181,19 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) { // const config = Config(); let defaultConf = _.findKey(config.messageConferences, o => o.default); - if(defaultConf) { + if (defaultConf) { const conf = config.messageConferences[defaultConf]; - if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { + if (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { return defaultConf; } } // just use anything we can defaultConf = _.findKey(config.messageConferences, (conf, confTag) => { - return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)); + return ( + 'system_internal' !== confTag && + (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) + ); }); return defaultConf; @@ -196,21 +210,21 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) { confTag = confTag || getDefaultMessageConferenceTag(client); const config = Config(); - if(confTag && _.has(config.messageConferences, [ confTag, 'areas' ])) { + if (confTag && _.has(config.messageConferences, [confTag, 'areas'])) { const areaPool = config.messageConferences[confTag].areas; let defaultArea = _.findKey(areaPool, o => o.default); - if(defaultArea) { + if (defaultArea) { const area = areaPool[defaultArea]; - if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) { + if (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) { return defaultArea; } } defaultArea = _.findKey(areaPool, (area, areaTag) => { - if(Message.isPrivateAreaTag(areaTag)) { + if (Message.isPrivateAreaTag(areaTag)) { return false; } - return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); + return true === disableAcsCheck || client.acs.hasMessageAreaRead(area); }); return defaultArea; @@ -229,26 +243,29 @@ function getSuitableMessageConfAndAreaTags(client) { // if we fail to find something. // let confTag = getDefaultMessageConferenceTag(client); - if(!confTag) { - return ['', '']; // can't have an area without a conf + if (!confTag) { + return ['', '']; // can't have an area without a conf } let areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); - if(!areaTag) { + if (!areaTag) { // OK, perhaps *any* area in *any* conf? _.forEach(Config().messageConferences, (conf, ct) => { - if(!client.acs.hasMessageConfRead(conf)) { + if (!client.acs.hasMessageConfRead(conf)) { return; } _.forEach(conf.areas, (area, at) => { - if(!_.includes(Message.WellKnownAreaTags, at) && client.acs.hasMessageAreaRead(area)) { + if ( + !_.includes(Message.WellKnownAreaTags, at) && + client.acs.hasMessageAreaRead(area) + ) { confTag = ct; areaTag = at; - return false; // stop inner iteration + return false; // stop inner iteration } }); - if(areaTag) { - return false; // stop iteration + if (areaTag) { + return false; // stop iteration } }); } @@ -262,8 +279,8 @@ function getMessageConferenceByTag(confTag) { function getMessageConfTagByAreaTag(areaTag) { const confs = Config().messageConferences; - return Object.keys(confs).find( (confTag) => { - return _.has(confs, [ confTag, 'areas', areaTag]); + return Object.keys(confs).find(confTag => { + return _.has(confs, [confTag, 'areas', areaTag]); }); } @@ -271,12 +288,12 @@ function getMessageAreaByTag(areaTag, optionalConfTag) { const confs = Config().messageConferences; // :TODO: this could be cached - if(_.isString(optionalConfTag)) { - if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { + if (_.isString(optionalConfTag)) { + if (_.has(confs, [optionalConfTag, 'areas', areaTag])) { return Object.assign( { areaTag, - confTag : optionalConfTag, + confTag: optionalConfTag, }, confs[optionalConfTag].areas[areaTag] ); @@ -287,9 +304,9 @@ function getMessageAreaByTag(areaTag, optionalConfTag) { // let area; _.forEach(confs, (conf, confTag) => { - if(_.has(conf, [ 'areas', areaTag ])) { + if (_.has(conf, ['areas', areaTag])) { area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]); - return false; // stop iteration + return false; // stop iteration } }); @@ -303,33 +320,38 @@ function changeMessageConference(client, confTag, cb) { function getConf(callback) { const conf = getMessageConferenceByTag(confTag); - if(conf) { + 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); + const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); + const area = getMessageAreaByTag(areaTag, confTag); - if(area) { - callback(null, conf, { areaTag : areaTag, area : area } ); + 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')); + 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 = { - [ UserProps.MessageConfTag ] : confTag, - [ UserProps.MessageAreaTag ] : areaInfo.areaTag, + [UserProps.MessageConfTag]: confTag, + [UserProps.MessageAreaTag]: areaInfo.areaTag, }; client.user.persistProperties(newProps, err => { callback(err, conf, areaInfo); @@ -337,10 +359,16 @@ function changeMessageConference(client, confTag, cb) { }, ], function complete(err, conf, areaInfo) { - if(!err) { - client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed'); + 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'); + client.log.warn( + { confTag: confTag, error: err.message }, + 'Could not change message conference' + ); } cb(err); } @@ -348,7 +376,7 @@ function changeMessageConference(client, confTag, cb) { } function changeMessageAreaWithOptions(client, areaTag, options, cb) { - options = options || {}; // :TODO: this is currently pointless... cb is required... + options = options || {}; // :TODO: this is currently pointless... cb is required... async.waterfall( [ @@ -360,28 +388,38 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) { // // Need at least *read* to access the area // - if(!client.acs.hasMessageAreaRead(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(UserProps.MessageAreaTag, areaTag, function persisted(err) { - return callback(err, area); - }); + if (true === options.persist) { + client.user.persistProperty( + UserProps.MessageAreaTag, + areaTag, + function persisted(err) { + return callback(err, area); + } + ); } else { client.user.properties[UserProps.MessageAreaTag] = areaTag; return callback(null, area); } - } + }, ], function complete(err, area) { - if(!err) { - client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed'); + 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'); + client.log.warn( + { areaTag: areaTag, area: area, error: err.message }, + 'Could not change message area' + ); } return cb(err); @@ -396,16 +434,16 @@ 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) { + if (!area || !confTag) { return false; } const conf = getMessageConferenceByTag(confTag); - if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { + if (!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { return false; } @@ -416,31 +454,35 @@ function tempChangeMessageConfAndArea(client, areaTag) { } function changeMessageArea(client, areaTag, cb) { - changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); + changeMessageAreaWithOptions(client, areaTag, { persist: true }, cb); } function hasMessageConfAndAreaRead(client, areaOrTag) { - if(_.isString(areaOrTag)) { + if (_.isString(areaOrTag)) { areaOrTag = getMessageAreaByTag(areaOrTag) || {}; } const conf = getMessageConferenceByTag(areaOrTag.confTag); - return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag); + return ( + client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag) + ); } function hasMessageConfAndAreaWrite(client, areaOrTag) { - if(_.isString(areaOrTag)) { + if (_.isString(areaOrTag)) { areaOrTag = getMessageAreaByTag(areaOrTag) || {}; } const conf = getMessageConferenceByTag(areaOrTag.confTag); - return client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag); + return ( + client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag) + ); } function filterMessageAreaTagsByReadACS(client, areaTags) { - if(!Array.isArray(areaTags)) { - areaTags = [ areaTags ]; + if (!Array.isArray(areaTags)) { + areaTags = [areaTags]; } - return areaTags.filter( areaTag => { + return areaTags.filter(areaTag => { const area = getMessageAreaByTag(areaTag); return hasMessageConfAndAreaRead(client, area); }); @@ -453,14 +495,14 @@ function filterMessageListByReadACS(client, messageList) { // // Keep a cache around for quick lookup. - const acsCache = new Map(); // areaTag:boolean + const acsCache = new Map(); // areaTag:boolean return messageList.filter(msg => { let cached = acsCache.get(msg.areaTag); - if(false === cached) { + if (false === cached) { return false; } - if(true === cached) { + if (true === cached) { return true; } cached = hasMessageConfAndAreaRead(client, msg.areaTag); @@ -475,11 +517,11 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) { const filter = { areaTag, - newerThanMessageId : lastMessageId, - resultType : 'count', + newerThanMessageId: lastMessageId, + resultType: 'count', }; - if(Message.isPrivateAreaTag(areaTag)) { + if (Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = userId; } @@ -495,13 +537,13 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { const filter = { areaTag, - resultType : 'messageList', - newerThanMessageId : lastMessageId, - sort : 'messageId', - order : 'ascending', + resultType: 'messageList', + newerThanMessageId: lastMessageId, + sort: 'messageId', + order: 'ascending', }; - if(Message.isPrivateAreaTag(areaTag)) { + if (Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = userId; } @@ -509,27 +551,26 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { }); } -function getMessageListForArea(client, areaTag, filter, cb) -{ - if(!cb && _.isFunction(filter)) { +function getMessageListForArea(client, areaTag, filter, cb) { + if (!cb && _.isFunction(filter)) { cb = filter; filter = { areaTag, - resultType : 'messageList', - sort : 'messageId', - order : 'ascending' + resultType: 'messageList', + sort: 'messageId', + order: 'ascending', }; } else { - Object.assign(filter, { areaTag } ); + Object.assign(filter, { areaTag }); } - if(client) { - if(!hasMessageConfAndAreaRead(client, areaTag)) { + if (client) { + if (!hasMessageConfAndAreaRead(client, areaTag)) { return cb(null, []); } } - if(Message.isPrivateAreaTag(areaTag)) { + if (Message.isPrivateAreaTag(areaTag)) { filter.privateTagUserId = client ? client.user.userId : 'INVALID_USER_ID'; } @@ -541,12 +582,12 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { { areaTag, newerThanTimestamp, - sort : 'modTimestamp', - order : 'ascending', - limit : 1, + sort: 'modTimestamp', + order: 'ascending', + limit: 1, }, (err, id) => { - if(err) { + if (err) { return cb(err); } return cb(null, id ? id[0] : null); @@ -556,10 +597,10 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) { function getMessageAreaLastReadId(userId, areaTag, cb) { msgDb.get( - 'SELECT message_id ' + - 'FROM user_message_area_last_read ' + - 'WHERE user_id = ? AND area_tag = ?;', - [ userId, areaTag.toLowerCase() ], + '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); } @@ -567,7 +608,7 @@ function getMessageAreaLastReadId(userId, areaTag, cb) { } function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) { - if(!cb && _.isFunction(allowOlder)) { + if (!cb && _.isFunction(allowOlder)) { cb = allowOlder; allowOlder = false; } @@ -582,30 +623,37 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) }); }, function update(lastId, callback) { - if(allowOlder || messageId > lastId) { + if (allowOlder || messageId > lastId) { msgDb.run( 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + - 'VALUES (?, ?, ?);', - [ userId, areaTag, messageId ], + 'VALUES (?, ?, ?);', + [userId, areaTag, messageId], function written(err) { - callback(err, true); // true=didUpdate + callback(err, true); // true=didUpdate } ); } else { callback(null); } - } + }, ], function complete(err, didUpdate) { - if(err) { + if (err) { Log.debug( - { error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, - 'Failed updating area last read ID'); + { + error: err.toString(), + userId: userId, + areaTag: areaTag, + messageId: messageId, + }, + 'Failed updating area last read ID' + ); } else { - if(true === didUpdate) { + if (true === didUpdate) { Log.trace( - { userId : userId, areaTag : areaTag, messageId : messageId }, - 'Area last read ID updated'); + { userId: userId, areaTag: areaTag, messageId: messageId }, + 'Area last read ID updated' + ); } } cb(err); @@ -621,7 +669,7 @@ function persistMessage(message, cb) { }, function recordToMessageNetworks(callback) { return msgNetRecord(message, callback); - } + }, ], cb ); @@ -629,9 +677,8 @@ function persistMessage(message, cb) { // method exposed for event scheduler function trimMessageAreasScheduledEvent(args, cb) { - function trimMessageAreaByMaxMessages(areaInfo, cb) { - if(0 === areaInfo.maxMessages) { + if (0 === areaInfo.maxMessages) { return cb(null); } @@ -644,12 +691,19 @@ 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'); + [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'); + Log.debug( + { areaInfo: areaInfo, type: 'maxMessages', count: this.changes }, + 'Area trimmed successfully' + ); } return cb(err); } @@ -657,19 +711,26 @@ function trimMessageAreasScheduledEvent(args, cb) { } function trimMessageAreaByMaxAgeDays(areaInfo, cb) { - if(0 === areaInfo.maxAgeDays) { + if (0 === areaInfo.maxAgeDays) { return cb(null); } 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'); + [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'); + Log.debug( + { areaInfo: areaInfo, type: 'maxAgeDays', count: this.changes }, + 'Area trimmed successfully' + ); } return cb(err); } @@ -688,12 +749,12 @@ function trimMessageAreasScheduledEvent(args, cb) { `SELECT DISTINCT area_tag FROM message;`, (err, row) => { - if(err) { + if (err) { return callback(err); } // We treat private mail special - if(!Message.isPrivateAreaTag(row.area_tag)) { + if (!Message.isPrivateAreaTag(row.area_tag)) { areaTags.push(row.area_tag); } }, @@ -708,21 +769,20 @@ function trimMessageAreasScheduledEvent(args, cb) { // determine maxMessages & maxAgeDays per area const config = Config(); areaTags.forEach(areaTag => { - let maxMessages = config.messageAreaDefaults.maxMessages; - let maxAgeDays = config.messageAreaDefaults.maxAgeDays; + let maxAgeDays = config.messageAreaDefaults.maxAgeDays; - const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here - if(area) { + const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here + if (area) { maxMessages = area.maxMessages || maxMessages; - maxAgeDays = area.maxAgeDays || maxAgeDays; + maxAgeDays = area.maxAgeDays || maxAgeDays; } - areaInfos.push( { - areaTag : areaTag, - maxMessages : maxMessages, - maxAgeDays : maxAgeDays, - } ); + areaInfos.push({ + areaTag: areaTag, + maxMessages: maxMessages, + maxAgeDays: maxAgeDays, + }); }); return callback(null, areaInfos); @@ -732,7 +792,7 @@ function trimMessageAreasScheduledEvent(args, cb) { areaInfos, (areaInfo, next) => { trimMessageAreaByMaxMessages(areaInfo, err => { - if(err) { + if (err) { return next(err); } @@ -773,20 +833,27 @@ 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'); + 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'); + Log.debug( + { count: this.changes }, + 'Private externally sent messages trimmed successfully' + ); } } ); return callback(null); - } + }, ], err => { return cb(err); } ); -} \ No newline at end of file +} diff --git a/core/message_base_qwk_export.js b/core/message_base_qwk_export.js index 2a86bda7..cb431eb5 100644 --- a/core/message_base_qwk_export.js +++ b/core/message_base_qwk_export.js @@ -22,44 +22,51 @@ const _ = require('lodash'); const fse = require('fs-extra'); const temptmp = require('temptmp'); const paths = require('path'); -const { v4 : UUIDv4 } = require('uuid'); +const { v4: UUIDv4 } = require('uuid'); const moment = require('moment'); const FormIds = { - main : 0, + main: 0, }; const MciViewIds = { - main : { - status : 1, - progressBar : 2, + main: { + status: 1, + progressBar: 2, - customRangeStart : 10, - } + customRangeStart: 10, + }, }; const UserProperties = { - ExportOptions : 'qwk_export_options', - ExportAreas : 'qwk_export_msg_areas', + ExportOptions: 'qwk_export_options', + ExportAreas: 'qwk_export_msg_areas', }; exports.moduleInfo = { - name : 'QWK Export', - desc : 'Exports a QWK Packet for download', - author : 'NuSkooler', + name: 'QWK Export', + desc: 'Exports a QWK Packet for download', + author: 'NuSkooler', }; exports.getModule = class MessageBaseQWKExport extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + this.config = Object.assign( + {}, + _.get(options, 'menuConfig.config'), + options.extraArgs + ); - this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); - this.config.bbsID = this.config.bbsID || _.get(Config(), 'messageNetworks.qwk.bbsID', 'ENIGMA'); + this.config.progBarChar = renderSubstr(this.config.progBarChar || '▒', 0, 1); + this.config.bbsID = + this.config.bbsID || _.get(Config(), 'messageNetworks.qwk.bbsID', 'ENIGMA'); this.tempName = `${UUIDv4().substr(-8).toUpperCase()}.QWK`; - this.sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); + this.sysTempDownloadArea = FileArea.getFileAreaByTag( + FileArea.WellKnownAreaTags.TempDownloads + ); } mciReady(mciData, cb) { @@ -70,27 +77,38 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { async.waterfall( [ - (callback) => { - this.prepViewController('main', FormIds.main, mciData.menu, err => { - return callback(err); - }); - }, - (callback) => { - this.temptmp = temptmp.createTrackedSession('qwkuserexp'); - this.temptmp.mkdir({ prefix : 'enigqwkwriter-'}, (err, tempDir) => { - if (err) { + callback => { + this.prepViewController( + 'main', + FormIds.main, + mciData.menu, + err => { return callback(err); } + ); + }, + callback => { + this.temptmp = temptmp.createTrackedSession('qwkuserexp'); + this.temptmp.mkdir( + { prefix: 'enigqwkwriter-' }, + (err, tempDir) => { + if (err) { + return callback(err); + } - this.tempPacketDir = tempDir; + this.tempPacketDir = tempDir; - const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(this.sysTempDownloadArea); + const sysTempDownloadDir = + FileArea.getAreaDefaultStorageDirectory( + this.sysTempDownloadArea + ); - // ensure dir exists - fse.mkdirs(sysTempDownloadDir, err => { - return callback(err, sysTempDownloadDir); - }); - }); + // ensure dir exists + fse.mkdirs(sysTempDownloadDir, err => { + return callback(err, sysTempDownloadDir); + }); + } + ); }, (sysTempDownloadDir, callback) => { this._performExport(sysTempDownloadDir, err => { @@ -104,7 +122,10 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { if (err) { // :TODO: doesn't do anything currently: if ('NORESULTS' === err.reasonCode) { - return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'qwkExportNoResults'); + return this.gotoMenu( + this.menuConfig.config.noResultsMenu || + 'qwkExportNoResults' + ); } return this.prevMenu(); @@ -123,12 +144,12 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { let qwkOptions = this.client.user.getProperty(UserProperties.ExportOptions); try { qwkOptions = JSON.parse(qwkOptions); - } catch(e) { + } catch (e) { qwkOptions = { - enableQWKE : true, - enableHeadersExtension : true, - enableAtKludges : true, - archiveFormat : 'application/zip', + enableQWKE: true, + enableHeadersExtension: true, + enableAtKludges: true, + archiveFormat: 'application/zip', }; } return qwkOptions; @@ -143,7 +164,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { } return exportArea; }); - } catch(e) { + } catch (e) { // default to all public and private without 'since' qwkExportAreas = getAllAvailableMessageAreaTags(this.client).map(areaTag => { return { areaTag }; @@ -151,7 +172,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { // Include user's private area qwkExportAreas.push({ - areaTag : Message.WellKnownAreaTags.Private, + areaTag: Message.WellKnownAreaTags.Private, }); } @@ -160,16 +181,18 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { _performExport(sysTempDownloadDir, cb) { const statusView = this.viewControllers.main.getView(MciViewIds.main.status); - const updateStatus = (status) => { + const updateStatus = status => { if (statusView) { statusView.setText(status); } }; - const progBarView = this.viewControllers.main.getView(MciViewIds.main.progressBar); + const progBarView = this.viewControllers.main.getView( + MciViewIds.main.progressBar + ); const updateProgressBar = (curr, total) => { if (progBarView) { - const prog = Math.floor( (curr / total) * progBarView.dimens.width ); + const prog = Math.floor((curr / total) * progBarView.dimens.width); progBarView.setText(this.config.progBarChar.repeat(prog)); } }; @@ -181,19 +204,27 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { // we can produce a TON of updates; only update progress at most every 3/4s if (Date.now() - lastProgUpdate > 750) { switch (state.step) { - case 'next_area' : + case 'next_area': updateStatus(state.status); updateProgressBar(0, 0); - this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state); + this.updateCustomViewTextsWithFilter( + 'main', + MciViewIds.main.customRangeStart, + state + ); break; - case 'message' : + case 'message': updateStatus(state.status); updateProgressBar(state.current, state.total); - this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state); + this.updateCustomViewTextsWithFilter( + 'main', + MciViewIds.main.customRangeStart, + state + ); break; - default : + default: break; } lastProgUpdate = Date.now(); @@ -203,7 +234,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { }; const keyPressHandler = (ch, key) => { - if('escape' === key.name) { + if ('escape' === key.name) { cancel = true; this.client.removeListener('key press', keyPressHandler); } @@ -217,54 +248,59 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { } let current = 1; - async.eachSeries(messageIds, (messageId, nextMessageId) => { - const message = new Message(); - message.load({ messageId }, err => { - if (err) { - return nextMessageId(err); - } - - const progress = { - message, - step : 'message', - total : ++totalExported, - areaCurrent : current, - areaCount : messageIds.length, - status : `${_.truncate(message.subject, { length : 25 })} (${current} / ${messageIds.length})`, - }; - - progressHandler(progress, err => { + async.eachSeries( + messageIds, + (messageId, nextMessageId) => { + const message = new Message(); + message.load({ messageId }, err => { if (err) { return nextMessageId(err); } - packetWriter.appendMessage(message); - current += 1; + const progress = { + message, + step: 'message', + total: ++totalExported, + areaCurrent: current, + areaCount: messageIds.length, + status: `${_.truncate(message.subject, { + length: 25, + })} (${current} / ${messageIds.length})`, + }; - return nextMessageId(null); + progressHandler(progress, err => { + if (err) { + return nextMessageId(err); + } + + packetWriter.appendMessage(message); + current += 1; + + return nextMessageId(null); + }); }); - }); - }, - err => { - return cb(err); - }); + }, + err => { + return cb(err); + } + ); }); }; const packetWriter = new QWKPacketWriter( Object.assign(this._getUserQWKExportOptions(), { - user : this.client.user, - bbsID : this.config.bbsID, + user: this.client.user, + bbsID: this.config.bbsID, }) ); packetWriter.on('warning', warning => { - this.client.log.warn( { warning }, 'QWK packet writer warning'); + this.client.log.warn({ warning }, 'QWK packet writer warning'); }); async.waterfall( [ - (callback) => { + callback => { // don't count idle monitor while processing this.client.stopIdleMonitor(); @@ -276,77 +312,91 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { }); packetWriter.once('error', err => { - this.client.log.error( { error : err.message }, 'QWK packet writer error'); + this.client.log.error( + { error: err.message }, + 'QWK packet writer error' + ); cancel = true; }); packetWriter.init(); }, - (callback) => { + callback => { // For each public area -> for each message const userExportAreas = this._getUserQWKExportAreas(); - const publicExportAreas = userExportAreas - .filter(exportArea => { - return exportArea.areaTag !== Message.WellKnownAreaTags.Private; - }); - async.eachSeries(publicExportAreas, (exportArea, nextExportArea) => { - const area = getMessageAreaByTag(exportArea.areaTag); - const conf = getMessageConferenceByTag(area.confTag); - if (!area || !conf) { - // :TODO: remove from user properties - this area does not exist - this.client.log.warn({ areaTag : exportArea.areaTag }, 'Cannot QWK export area as it does not exist'); - return nextExportArea(null); - } - - if (!hasMessageConfAndAreaRead(this.client, area)) { - this.client.log.warn({ areaTag : area.areaTag }, 'Cannot QWK export area due to ACS'); - return nextExportArea(null); - } - - const progress = { - conf, - area, - step : 'next_area', - status : `Gathering in ${conf.name} - ${area.name}...`, - }; - - progressHandler(progress, err => { - if (err) { - return nextExportArea(err); + const publicExportAreas = userExportAreas.filter(exportArea => { + return exportArea.areaTag !== Message.WellKnownAreaTags.Private; + }); + async.eachSeries( + publicExportAreas, + (exportArea, nextExportArea) => { + const area = getMessageAreaByTag(exportArea.areaTag); + const conf = getMessageConferenceByTag(area.confTag); + if (!area || !conf) { + // :TODO: remove from user properties - this area does not exist + this.client.log.warn( + { areaTag: exportArea.areaTag }, + 'Cannot QWK export area as it does not exist' + ); + return nextExportArea(null); } - const filter = { - resultType : 'id', - areaTag : exportArea.areaTag, - newerThanTimestamp : exportArea.newerThanTimestamp + if (!hasMessageConfAndAreaRead(this.client, area)) { + this.client.log.warn( + { areaTag: area.areaTag }, + 'Cannot QWK export area due to ACS' + ); + return nextExportArea(null); + } + + const progress = { + conf, + area, + step: 'next_area', + status: `Gathering in ${conf.name} - ${area.name}...`, }; - processMessagesWithFilter(filter, err => { - return nextExportArea(err); + progressHandler(progress, err => { + if (err) { + return nextExportArea(err); + } + + const filter = { + resultType: 'id', + areaTag: exportArea.areaTag, + newerThanTimestamp: exportArea.newerThanTimestamp, + }; + + processMessagesWithFilter(filter, err => { + return nextExportArea(err); + }); }); - }); - }, - err => { - return callback(err, userExportAreas); - }); + }, + err => { + return callback(err, userExportAreas); + } + ); }, (userExportAreas, callback) => { // Private messages to current user if the user has // elected to export private messages - const privateExportArea = userExportAreas.find(exportArea => exportArea.areaTag === Message.WellKnownAreaTags.Private); + const privateExportArea = userExportAreas.find( + exportArea => + exportArea.areaTag === Message.WellKnownAreaTags.Private + ); if (!privateExportArea) { return callback(null); } const filter = { - resultType : 'id', - privateTagUserId : this.client.user.userId, - newerThanTimestamp : privateExportArea.newerThanTimestamp, + resultType: 'id', + privateTagUserId: this.client.user.userId, + newerThanTimestamp: privateExportArea.newerThanTimestamp, }; return processMessagesWithFilter(filter, callback); }, - (callback) => { + callback => { let packetInfo; packetWriter.once('packet', info => { packetInfo = info; @@ -370,38 +420,40 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { }, (sysDownloadPath, packetInfo, callback) => { const newEntry = new FileEntry({ - areaTag : this.sysTempDownloadArea.areaTag, - fileName : paths.basename(sysDownloadPath), - storageTag : this.sysTempDownloadArea.storageTags[0], - meta : { - upload_by_username : this.client.user.username, - upload_by_user_id : this.client.user.userId, - byte_size : packetInfo.stats.size, - session_temp_dl : 1, // download is valid until session is over + areaTag: this.sysTempDownloadArea.areaTag, + fileName: paths.basename(sysDownloadPath), + storageTag: this.sysTempDownloadArea.storageTags[0], + meta: { + upload_by_username: this.client.user.username, + upload_by_user_id: this.client.user.userId, + byte_size: packetInfo.stats.size, + session_temp_dl: 1, // download is valid until session is over // :TODO: something like this: allow to override the displayed/downloaded as filename // separate from the actual on disk filename. E.g. we could always download as "ENIGMA.QWK" //visible_filename : paths.basename(packetInfo.path), - } + }, }); newEntry.desc = 'QWK Export'; newEntry.persist(err => { - if(!err) { + if (!err) { // queue it! DownloadQueue.get(this.client).addTemporaryDownload(newEntry); } return callback(err); }); }, - (callback) => { + callback => { // update user's export area dates; they can always change/reset them again - const updatedUserExportAreas = this._getUserQWKExportAreas().map(exportArea => { - return Object.assign(exportArea, { - newerThanTimestamp : getISOTimestampString(), - }); - }); + const updatedUserExportAreas = this._getUserQWKExportAreas().map( + exportArea => { + return Object.assign(exportArea, { + newerThanTimestamp: getISOTimestampString(), + }); + } + ); return this.client.user.persistProperty( UserProperties.ExportAreas, @@ -425,4 +477,4 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule { } ); } -}; \ No newline at end of file +}; diff --git a/core/message_base_search.js b/core/message_base_search.js index 859d2320..f98b0b89 100644 --- a/core/message_base_search.js +++ b/core/message_base_search.js @@ -2,36 +2,36 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; +const MenuModule = require('./menu_module.js').MenuModule; const { getSortedAvailMessageConferences, getAvailableMessageAreasByConfTag, getSortedAvailMessageAreasByConfTag, hasMessageConfAndAreaRead, filterMessageListByReadACS, -} = require('./message_area.js'); -const Errors = require('./enig_error.js').Errors; -const Message = require('./message.js'); +} = require('./message_area.js'); +const Errors = require('./enig_error.js').Errors; +const Message = require('./message.js'); // deps -const _ = require('lodash'); +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 { @@ -39,35 +39,37 @@ exports.getModule = class MessageBaseSearch extends MenuModule { super(options); this.menuMethods = { - search : (formData, extraArgs, cb) => { + search: (formData, extraArgs, cb) => { return this.searchNow(formData, cb); - } + }, }; } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } this.prepViewController('search', 0, mciData.menu, (err, vc) => { - if(err) { + 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) { + 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); @@ -76,9 +78,14 @@ exports.getModule = class MessageBaseSearch extends MenuModule { 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 } ) + 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); @@ -92,38 +99,40 @@ exports.getModule = class MessageBaseSearch extends MenuModule { } searchNow(formData, cb) { - const isAdvanced = formData.submitId === MciViewIds.search.advSearch; - const value = formData.value; + const isAdvanced = formData.submitId === MciViewIds.search.advSearch; + const value = formData.value; const filter = { - resultType : 'messageList', - sort : 'modTimestamp', - terms : value.searchTerms, + 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 + limit: 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned }; const returnNoResults = () => { return this.gotoMenu( this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', - { menuFlags : [ 'popParent' ] }, + { menuFlags: ['popParent'] }, cb ); }; - if(isAdvanced) { - filter.toUserName = value.toUserName; + if (isAdvanced) { + filter.toUserName = value.toUserName; filter.fromUserName = value.fromUserName; - if(value.confTag && !value.areaTag) { + 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 } ), + getAvailableMessageAreasByConfTag(value.confTag, { + client: this.client, + }), (area, areaTag) => areaTag ); - } else if(value.areaTag) { - if(hasMessageConfAndAreaRead(this.client, value.areaTag)) { + } else if (value.areaTag) { + if (hasMessageConfAndAreaRead(this.client, value.areaTag)) { filter.areaTag = value.areaTag; // specific conf + area } else { return returnNoResults(); @@ -132,26 +141,26 @@ exports.getModule = class MessageBaseSearch extends MenuModule { } Message.findMessages(filter, (err, messageList) => { - if(err) { + if (err) { return cb(err); } // don't include results without ACS -- if the user searched by // explicit conf/area tag, we should have already filtered (above) - if(!value.confTag && !value.areaTag) { + if (!value.confTag && !value.areaTag) { messageList = filterMessageListByReadACS(this.client, messageList); } - if(0 === messageList.length) { + if (0 === messageList.length) { return returnNoResults(); } const menuOpts = { - extraArgs : { + extraArgs: { messageList, - noUpdateLastReadId : true + noUpdateLastReadId: true, }, - menuFlags : [ 'popParent' ], + menuFlags: ['popParent'], }; return this.gotoMenu( diff --git a/core/mime_util.js b/core/mime_util.js index ddf44432..9a67aac3 100644 --- a/core/mime_util.js +++ b/core/mime_util.js @@ -2,31 +2,31 @@ 'use strict'; // deps -const _ = require('lodash'); +const _ = require('lodash'); const mimeTypes = require('mime-types'); -exports.startup = startup; -exports.resolveMimeType = resolveMimeType; +exports.startup = startup; +exports.resolveMimeType = resolveMimeType; function startup(cb) { // // Add in types (not yet) supported by mime-db -- and therefor, mime-types // const ADDITIONAL_EXT_MIMETYPES = { - ans : 'text/x-ansi', - gz : 'application/gzip', // not in mime-types 2.1.15 :( - lzx : 'application/x-lzx', // :TODO: submit to mime-types + ans: 'text/x-ansi', + gz: 'application/gzip', // not in mime-types 2.1.15 :( + lzx: 'application/x-lzx', // :TODO: submit to mime-types }; _.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => { // don't override any entries - if(!_.isString(mimeTypes.types[ext])) { + if (!_.isString(mimeTypes.types[ext])) { mimeTypes[ext] = mimeType; } - if(!mimeTypes.extensions[mimeType]) { - mimeTypes.extensions[mimeType] = [ ext ]; + if (!mimeTypes.extensions[mimeType]) { + mimeTypes.extensions[mimeType] = [ext]; } }); @@ -34,9 +34,9 @@ function startup(cb) { } function resolveMimeType(query) { - if(mimeTypes.extensions[query]) { - return query; // already a mime-type + if (mimeTypes.extensions[query]) { + return query; // already a mime-type } - return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined -} \ No newline at end of file + return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined +} diff --git a/core/misc_scheduled_events.js b/core/misc_scheduled_events.js index 1650b697..12fa19bb 100644 --- a/core/misc_scheduled_events.js +++ b/core/misc_scheduled_events.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const StatLog = require('./stat_log.js'); -const SysProps = require('./system_property.js'); +const StatLog = require('./stat_log.js'); +const SysProps = require('./system_property.js'); exports.dailyMaintenanceScheduledEvent = dailyMaintenanceScheduledEvent; @@ -10,7 +10,7 @@ function dailyMaintenanceScheduledEvent(args, cb) { // // Various stats need reset daily // - [ SysProps.LoginsToday, SysProps.MessagesToday ].forEach(prop => { + [SysProps.LoginsToday, SysProps.MessagesToday].forEach(prop => { StatLog.setNonPersistentSystemStat(prop, 0); }); diff --git a/core/misc_util.js b/core/misc_util.js index 78c76719..0b383f0e 100644 --- a/core/misc_util.js +++ b/core/misc_util.js @@ -2,18 +2,18 @@ 'use strict'; // deps -const paths = require('path'); -const os = require('os'); +const paths = require('path'); +const os = require('os'); -const packageJson = require('../package.json'); +const packageJson = require('../package.json'); -exports.isProduction = isProduction; -exports.isDevelopment = isDevelopment; -exports.valueWithDefault = valueWithDefault; -exports.resolvePath = resolvePath; -exports.getCleanEnigmaVersion = getCleanEnigmaVersion; -exports.getEnigmaUserAgent = getEnigmaUserAgent; -exports.valueAsArray = valueAsArray; +exports.isProduction = isProduction; +exports.isDevelopment = isDevelopment; +exports.valueWithDefault = valueWithDefault; +exports.resolvePath = resolvePath; +exports.getCleanEnigmaVersion = getCleanEnigmaVersion; +exports.getEnigmaUserAgent = getEnigmaUserAgent; +exports.valueAsArray = valueAsArray; function isProduction() { var env = process.env.NODE_ENV || 'dev'; @@ -21,17 +21,22 @@ function isProduction() { } 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) === '~/') { + 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); + path = + (process.env.HOME || + mswCombined || + process.env.HOMEPATH || + process.env.HOMEDIR || + process.cwd()) + path.substr(1); } return paths.resolve(path); } @@ -39,23 +44,22 @@ function resolvePath(path) { function getCleanEnigmaVersion() { return packageJson.version .replace(/-/g, '.') - .replace(/alpha/,'a') - .replace(/beta/,'b') - ; + .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 + const nodeVer = process.version.substr(1); // remove 'v' prefix return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; } function valueAsArray(value) { - if(!value) { + if (!value) { return []; } - return Array.isArray(value) ? value : [ value ]; + return Array.isArray(value) ? value : [value]; } diff --git a/core/mod_mixins.js b/core/mod_mixins.js index 22e49407..4998c5c8 100644 --- a/core/mod_mixins.js +++ b/core/mod_mixins.js @@ -1,36 +1,42 @@ /* jslint node: true */ 'use strict'; -const messageArea = require('../core/message_area.js'); -const UserProps = require('./user_property.js'); +const messageArea = require('../core/message_area.js'); +const UserProps = require('./user_property.js'); // deps -const { get } = require('lodash'); +const { get } = require('lodash'); -exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { +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[UserProps.MessageConfTag], + areaTag: this.client.user.properties[UserProps.MessageAreaTag], + }; + } + + if (!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) { + this.client.log.warn( + { messageAreaTag: messageArea }, + 'Failed to perform temporary message area/conf switch' + ); + } } - if(recordPrevious) { - this.prevMessageConfAndArea = { - confTag : this.client.user.properties[UserProps.MessageConfTag], - areaTag : this.client.user.properties[UserProps.MessageAreaTag], - }; + tempMessageConfAndAreaRestore() { + if (this.prevMessageConfAndArea) { + this.client.user.properties[UserProps.MessageConfTag] = + this.prevMessageConfAndArea.confTag; + this.client.user.properties[UserProps.MessageAreaTag] = + this.prevMessageConfAndArea.areaTag; + } } - - 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[UserProps.MessageConfTag] = this.prevMessageConfAndArea.confTag; - this.client.user.properties[UserProps.MessageAreaTag] = this.prevMessageConfAndArea.areaTag; - } - } -}; + }; diff --git a/core/module_util.js b/core/module_util.js index 033d6094..82453b1e 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -2,37 +2,41 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const Log = require('./logger.js').log; -const { - Errors, - ErrorReasons -} = require('./enig_error.js'); +const Config = require('./config.js').get; +const Log = require('./logger.js').log; +const { Errors, ErrorReasons } = require('./enig_error.js'); // deps -const fs = require('graceful-fs'); -const paths = require('path'); -const _ = require('lodash'); -const assert = require('assert'); -const async = require('async'); -const glob = require('glob'); +const fs = require('graceful-fs'); +const paths = require('path'); +const _ = require('lodash'); +const assert = require('assert'); +const async = require('async'); +const glob = require('glob'); // exports -exports.loadModuleEx = loadModuleEx; -exports.loadModule = loadModule; -exports.loadModulesForCategory = loadModulesForCategory; -exports.getModulePaths = getModulePaths; -exports.initializeModules = initializeModules; +exports.loadModuleEx = loadModuleEx; +exports.loadModule = loadModule; +exports.loadModulesForCategory = loadModulesForCategory; +exports.getModulePaths = getModulePaths; +exports.initializeModules = initializeModules; function loadModuleEx(options, cb) { 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) { - return cb(Errors.AccessDenied(`Module "${options.name}" is disabled`, ErrorReasons.Disabled)); + if (_.isObject(modConfig) && false === modConfig.enabled) { + return cb( + Errors.AccessDenied( + `Module "${options.name}" is disabled`, + ErrorReasons.Disabled + ) + ); } // @@ -41,15 +45,15 @@ function loadModuleEx(options, cb) { // to have their own containing folder, package.json & dependencies, etc. // let mod; - let modPath = paths.join(options.path, `${options.name}.js`); // general case first + let modPath = paths.join(options.path, `${options.name}.js`); // general case first try { mod = require(modPath); - } catch(e) { - if('MODULE_NOT_FOUND' === e.code) { + } catch (e) { + if ('MODULE_NOT_FOUND' === e.code) { modPath = paths.join(options.path, options.name, `${options.name}.js`); try { mod = require(modPath); - } catch(e) { + } catch (e) { return cb(e); } } else { @@ -57,12 +61,16 @@ function loadModuleEx(options, cb) { } } - if(!_.isObject(mod.moduleInfo)) { - return cb(Errors.Invalid(`No exported "moduleInfo" block for module ${modPath}!`)); + if (!_.isObject(mod.moduleInfo)) { + return cb( + Errors.Invalid(`No exported "moduleInfo" block for module ${modPath}!`) + ); } - if(!_.isFunction(mod.getModule)) { - return cb(Errors.Invalid(`No exported "getModule" method for module ${modPath}!`)); + if (!_.isFunction(mod.getModule)) { + return cb( + Errors.Invalid(`No exported "getModule" method for module ${modPath}!`) + ); } return cb(null, mod); @@ -71,19 +79,25 @@ function loadModuleEx(options, cb) { function loadModule(name, category, cb) { const path = Config().paths[category]; - if(!_.isString(path)) { - return cb(Errors.DoesNotExist(`Not sure where to look for module "${name}" of category "${category}"`)); + if (!_.isString(path)) { + return cb( + Errors.DoesNotExist( + `Not sure where to look for module "${name}" of category "${category}"` + ) + ); } - loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { - 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) { + if (err) { return iterator(err); } @@ -91,23 +105,27 @@ function loadModulesForCategory(category, iterator, complete) { return '.js' === paths.extname(file); }); - async.each(jsModules, (file, next) => { - loadModule(paths.basename(file, '.js'), category, (err, mod) => { - if(err) { - if(ErrorReasons.Disabled === err.reasonCode) { - Log.debug(err.message); - } else { - Log.info( { err : err }, 'Failed loading module'); + async.each( + jsModules, + (file, next) => { + loadModule(paths.basename(file, '.js'), category, (err, mod) => { + if (err) { + if (ErrorReasons.Disabled === err.reasonCode) { + Log.debug(err.message); + } else { + Log.info({ err: err }, 'Failed loading module'); + } + return next(null); // continue no matter what } - return next(null); // continue no matter what + return iterator(mod, next); + }); + }, + err => { + if (complete) { + return complete(err); } - return iterator(mod, next); - }); - }, err => { - if(complete) { - return complete(err); } - }); + ); }); } @@ -127,48 +145,63 @@ function initializeModules(cb) { const modulePaths = getModulePaths().concat(__dirname); - async.each(modulePaths, (modulePath, nextPath) => { - glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { - if(err) { - return nextPath(err); - } - - const ourPath = paths.join(__dirname, __filename); - - async.each(files, (moduleName, nextModule) => { - const fullModulePath = paths.join(modulePath, moduleName); - if(ourPath === fullModulePath) { - return nextModule(null); + async.each( + modulePaths, + (modulePath, nextPath) => { + glob('*{.js,/*.js}', { cwd: modulePath }, (err, files) => { + if (err) { + return nextPath(err); } - try { - const mod = require(fullModulePath); + const ourPath = paths.join(__dirname, __filename); - if(_.isFunction(mod.moduleInitialize)) { - const initInfo = { - events : Events, - }; - - mod.moduleInitialize(initInfo, err => { - if(err) { - Log.warn( { error : err.message, modulePath : fullModulePath }, 'Error during "moduleInitialize"'); - } + async.each( + files, + (moduleName, nextModule) => { + const fullModulePath = paths.join(modulePath, moduleName); + if (ourPath === fullModulePath) { return nextModule(null); - }); - } else { - return nextModule(null); + } + + try { + const mod = require(fullModulePath); + + if (_.isFunction(mod.moduleInitialize)) { + const initInfo = { + events: Events, + }; + + mod.moduleInitialize(initInfo, err => { + if (err) { + Log.warn( + { + error: err.message, + modulePath: fullModulePath, + }, + 'Error during "moduleInitialize"' + ); + } + return nextModule(null); + }); + } else { + return nextModule(null); + } + } catch (e) { + Log.warn( + { error: e.message, fullModulePath }, + 'Exception during "moduleInitialize"' + ); + return nextModule(null); + } + }, + err => { + return nextPath(err); } - } catch(e) { - Log.warn( { error : e.message, fullModulePath }, 'Exception during "moduleInitialize"'); - return nextModule(null); - } - }, - err => { - return nextPath(err); + ); }); - }); - }, - err => { - return cb(err); - }); + }, + err => { + return cb(err); + } + ); } diff --git a/core/mrc.js b/core/mrc.js index 28e0e3c3..c781826f 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -2,27 +2,24 @@ 'use strict'; // ENiGMA½ -const Log = require('./logger.js').log; -const { MenuModule } = require('./menu_module.js'); -const { - pipeToAnsi, - stripMciColorCodes -} = require('./color_codes.js'); -const stringFormat = require('./string_format.js'); -const StringUtil = require('./string_util.js'); -const Config = require('./config.js').get; +const Log = require('./logger.js').log; +const { MenuModule } = require('./menu_module.js'); +const { pipeToAnsi, stripMciColorCodes } = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); +const StringUtil = require('./string_util.js'); +const Config = require('./config.js').get; // deps -const _ = require('lodash'); -const async = require('async'); -const net = require('net'); -const moment = require('moment'); +const _ = require('lodash'); +const async = require('async'); +const net = require('net'); +const moment = require('moment'); exports.moduleInfo = { - name : 'MRC Client', - desc : 'Connects to an MRC chat server', - author : 'RiPuk', - packageName : 'codes.l33t.enigma.mrc.client', + name: 'MRC Client', + desc: 'Connects to an MRC chat server', + author: 'RiPuk', + packageName: 'codes.l33t.enigma.mrc.client', // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were // borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together. @@ -30,24 +27,22 @@ exports.moduleInfo = { }; const FormIds = { - mrcChat : 0, + mrcChat: 0, }; const MciViewIds = { - mrcChat : { - chatLog : 1, - inputArea : 2, - roomName : 3, - roomTopic : 4, - mrcUsers : 5, - mrcBbses : 6, + mrcChat: { + chatLog: 1, + inputArea: 2, + roomName: 3, + roomTopic: 4, + mrcUsers: 5, + mrcBbses: 6, - customRangeStart : 20, // 20+ = customs - } + customRangeStart: 20, // 20+ = customs + }, }; - - // TODO: this is a bit shit, could maybe do it with an ansi instead const helpText = ` |15General Chat|08: @@ -66,13 +61,14 @@ const helpText = ` |03/|11rainbow |03 |08- |07Crazy rainbow text `; - exports.getModule = class mrcModule extends MenuModule { constructor(options) { super(options); - this.log = Log.child( { module : 'MRC' } ); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + this.log = Log.child({ module: 'MRC' }); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { + extraArgs: options.extraArgs, + }); this.config.maxScrollbackLines = this.config.maxScrollbackLines || 500; @@ -82,27 +78,27 @@ exports.getModule = class mrcModule extends MenuModule { room: '', room_topic: '', nicks: [], - lastSentMsg : {}, // used for latency est. + lastSentMsg: {}, // used for latency est. }; this.customFormatObj = { - roomName : '', - roomTopic : '', - roomUserCount : 0, - userCount : 0, - boardCount : 0, - roomCount : 0, - latencyMs : 0, - activityLevel : 0, - activityLevelIndicator : ' ', + roomName: '', + roomTopic: '', + roomUserCount: 0, + userCount: 0, + boardCount: 0, + roomCount: 0, + latencyMs: 0, + activityLevel: 0, + activityLevelIndicator: ' ', }; this.menuMethods = { - - sendChatMessage : (formData, extraArgs, cb) => { - - const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); - const inputData = inputAreaView.getData(); + sendChatMessage: (formData, extraArgs, cb) => { + const inputAreaView = this.viewControllers.mrcChat.getView( + MciViewIds.mrcChat.inputArea + ); + const inputData = inputAreaView.getData(); this.processOutgoingMessage(inputData); inputAreaView.clearText(); @@ -110,13 +106,23 @@ exports.getModule = class mrcModule extends MenuModule { return cb(null); }, - movementKeyPressed : (formData, extraArgs, cb) => { - const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); - 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; + movementKeyPressed: (formData, extraArgs, cb) => { + const bodyView = this.viewControllers.mrcChat.getView( + MciViewIds.mrcChat.chatLog + ); + 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; } this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); @@ -124,35 +130,48 @@ exports.getModule = class mrcModule extends MenuModule { return cb(null); }, - quit : (formData, extraArgs, cb) => { + quit: (formData, extraArgs, cb) => { return this.prevMenu(cb); }, - clearMessages : (formData, extraArgs, cb) => { + clearMessages: (formData, extraArgs, cb) => { this.clearMessages(); return cb(null); - } + }, }; } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (callback) => { - return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback); + callback => { + return this.prepViewController( + 'mrcChat', + FormIds.mrcChat, + mciData.menu, + callback + ); }, - (callback) => { - return this.validateMCIByViewIds('mrcChat', [ MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea ], callback); + callback => { + return this.validateMCIByViewIds( + 'mrcChat', + [MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea], + callback + ); }, - (callback) => { + callback => { const connectOpts = { - port : _.get(Config(), 'chatServers.mrc.multiplexerPort', 5000), - host : 'localhost', + port: _.get( + Config(), + 'chatServers.mrc.multiplexerPort', + 5000 + ), + host: 'localhost', }; // connect to multiplexer @@ -167,18 +186,28 @@ exports.getModule = class mrcModule extends MenuModule { this.clientConnect(); // send register to central MRC and get stats every 60s - this.heartbeat = setInterval( () => { + this.heartbeat = setInterval(() => { this.sendHeartbeat(); this.sendServerMessage('STATS'); }, 60000); // override idle logout seconds if configured - const idleLogoutSeconds = parseInt(this.config.idleLogoutSeconds); - if(0 === idleLogoutSeconds) { - this.log.debug('Temporary disable idle monitor due to config'); + const idleLogoutSeconds = parseInt( + this.config.idleLogoutSeconds + ); + if (0 === idleLogoutSeconds) { + this.log.debug( + 'Temporary disable idle monitor due to config' + ); this.client.stopIdleMonitor(); - } else if (!isNaN(idleLogoutSeconds) && idleLogoutSeconds >= 60) { - this.log.debug( { idleLogoutSeconds }, 'Temporary override idle logout seconds due to config'); + } else if ( + !isNaN(idleLogoutSeconds) && + idleLogoutSeconds >= 60 + ) { + this.log.debug( + { idleLogoutSeconds }, + 'Temporary override idle logout seconds due to config' + ); this.client.overrideIdleLogoutSeconds(idleLogoutSeconds); } }); @@ -190,7 +219,10 @@ exports.getModule = class mrcModule extends MenuModule { }); this.state.socket.once('error', err => { - this.log.warn( { error : err.message }, 'MRC multiplexer socket error' ); + this.log.warn( + { error: err.message }, + 'MRC multiplexer socket error' + ); this.state.socket.destroy(); delete this.state.socket; @@ -198,8 +230,8 @@ exports.getModule = class mrcModule extends MenuModule { return callback(err); }); - return(callback); - } + return callback; + }, ], err => { return cb(err); @@ -222,7 +254,7 @@ exports.getModule = class mrcModule extends MenuModule { quitServer() { clearInterval(this.heartbeat); - if(this.state.socket) { + if (this.state.socket) { this.sendServerMessage('LOGOFF'); this.state.socket.destroy(); delete this.state.socket; @@ -233,12 +265,14 @@ exports.getModule = class mrcModule extends MenuModule { * Adds a message to the chat log on screen */ addMessageToChatLog(message) { - if(!Array.isArray(message)) { - message = [ message ]; + if (!Array.isArray(message)) { + message = [message]; } message.forEach(msg => { - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + const chatLogView = this.viewControllers.mrcChat.getView( + MciViewIds.mrcChat.chatLog + ); const messageLength = stripMciColorCodes(msg).length; const chatWidth = chatLogView.dimens.width; let padAmount = 0; @@ -255,7 +289,7 @@ exports.getModule = class mrcModule extends MenuModule { const padding = ' |00' + ' '.repeat(padAmount); chatLogView.addText(pipeToAnsi(msg + padding)); - if(chatLogView.getLineCount() > this.config.maxScrollbackLines) { + if (chatLogView.getLineCount() > this.config.maxScrollbackLines) { chatLogView.deleteLine(0); } }); @@ -265,8 +299,7 @@ exports.getModule = class mrcModule extends MenuModule { * Processes data received from the MRC multiplexer */ processReceivedMessage(blob) { - blob.split('\n').forEach( message => { - + blob.split('\n').forEach(message => { try { message = JSON.parse(message); } catch (e) { @@ -285,8 +318,8 @@ exports.getModule = class mrcModule extends MenuModule { this.setText(MciViewIds.mrcChat.roomName, `#${params[1]}`); this.setText(MciViewIds.mrcChat.roomTopic, params[2]); - this.customFormatObj.roomName = params[1]; - this.customFormatObj.roomTopic = params[2]; + this.customFormatObj.roomName = params[1]; + this.customFormatObj.roomTopic = params[2]; this.updateCustomViews(); this.state.room = params[1]; @@ -300,22 +333,19 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'STATS': { - const [ + const [boardCount, roomCount, userCount, activityLevel] = + params[1].split(' ').map(v => parseInt(v)); + + const activityLevelIndicator = + this.getActivityLevelIndicator(activityLevel); + + Object.assign(this.customFormatObj, { boardCount, roomCount, userCount, - activityLevel - ] = params[1].split(' ').map(v => parseInt(v)); - - const activityLevelIndicator = this.getActivityLevelIndicator(activityLevel); - - Object.assign( - this.customFormatObj, - { - boardCount, roomCount, userCount, - activityLevel, activityLevelIndicator - } - ); + activityLevel, + activityLevelIndicator, + }); this.setText(MciViewIds.mrcChat.mrcUsers, userCount); this.setText(MciViewIds.mrcChat.mrcBbses, boardCount); @@ -328,18 +358,22 @@ exports.getModule = class mrcModule extends MenuModule { this.addMessageToChatLog(message.body); break; } - } else { - if(message.body === this.state.lastSentMsg.msg) { - this.customFormatObj.latencyMs = - moment.duration(moment().diff(this.state.lastSentMsg.time)).asMilliseconds(); + if (message.body === this.state.lastSentMsg.msg) { + this.customFormatObj.latencyMs = moment + .duration(moment().diff(this.state.lastSentMsg.time)) + .asMilliseconds(); delete this.state.lastSentMsg.msg; } if (message.to_room == this.state.room) { // if we're here then we want to show it to the user - const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); - this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); + const currentTime = moment().format( + this.client.currentTheme.helpers.getTimeFormat() + ); + this.addMessageToChatLog( + '|08' + currentTime + '|00 ' + message.body + '|00' + ); } } @@ -349,8 +383,8 @@ exports.getModule = class mrcModule extends MenuModule { getActivityLevelIndicator(level) { let indicators = this.config.activityLevelIndicators; - if(!Array.isArray(indicators) || indicators.length < level + 1) { - indicators = [ ' ', '░', '▒', '▓' ]; + if (!Array.isArray(indicators) || indicators.length < level + 1) { + indicators = [' ', '░', '▒', '▓']; } return indicators[level]; } @@ -382,9 +416,9 @@ exports.getModule = class mrcModule extends MenuModule { // else just format and send const textFormatObj = { - fromUserName : this.state.alias, - toUserName : to_user, - message : message + fromUserName: this.state.alias, + toUserName: to_user, + message: message, }; const messageFormat = @@ -406,15 +440,19 @@ exports.getModule = class mrcModule extends MenuModule { try { this.state.lastSentMsg = { - msg : formattedMessage, - time : moment(), + msg: formattedMessage, + time: moment(), }; - this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); - } catch(e) { - this.client.log.warn( { error : e.message }, 'MRC error'); + this.sendMessageToMultiplexer( + to_user || '', + '', + this.state.room, + formattedMessage + ); + } catch (e) { + this.client.log.warn({ error: e.message }, 'MRC error'); } } - } /** @@ -432,24 +470,35 @@ exports.getModule = class mrcModule extends MenuModule { case 'rainbow': { // this is brutal, but i love it - const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { - const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); - a += `|${cc}${c}|00 `; - return a; - }, '').substr(0, 140).replace(/\\s\|\d*$/, ''); + const line = message + .replace(/^\/rainbow\s/, '') + .split(' ') + .reduce(function (a, c) { + const cc = Math.floor(Math.random() * 31 + 1) + .toString() + .padStart(2, '0'); + a += `|${cc}${c}|00 `; + return a; + }, '') + .substr(0, 140) + .replace(/\\s\|\d*$/, ''); this.processOutgoingMessage(line); break; } case 'l33t': - this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t')); + this.processOutgoingMessage( + StringUtil.stylizeString(message.substr(6), 'l33t') + ); break; case 'kewl': { - const text_modes = Array('f','v','V','i','M'); + const text_modes = Array('f', 'v', 'V', 'i', 'M'); const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; - this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode)); + this.processOutgoingMessage( + StringUtil.stylizeString(message.substr(6), mode) + ); break; } @@ -470,7 +519,9 @@ exports.getModule = class mrcModule extends MenuModule { break; case 'topic': - this.sendServerMessage(`NEWTOPIC:${this.state.room}:${message.substr(7)}`); + this.sendServerMessage( + `NEWTOPIC:${this.state.room}:${message.substr(7)}` + ); break; case 'info': @@ -489,7 +540,7 @@ exports.getModule = class mrcModule extends MenuModule { this.sendServerMessage('LIST'); break; - case 'quit' : + case 'quit': return this.prevMenu(); case 'clear': @@ -501,7 +552,6 @@ exports.getModule = class mrcModule extends MenuModule { break; default: - break; } @@ -511,7 +561,9 @@ exports.getModule = class mrcModule extends MenuModule { } clearMessages() { - const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); + const chatLogView = this.viewControllers.mrcChat.getView( + MciViewIds.mrcChat.chatLog + ); chatLogView.setText(''); } @@ -519,17 +571,16 @@ exports.getModule = class mrcModule extends MenuModule { * Creates a json object, stringifies it and sends it to the MRC multiplexer */ sendMessageToMultiplexer(to_user, to_site, to_room, body) { - const message = { to_user, to_site, to_room, body, - from_user : this.state.alias, - from_room : this.state.room, + from_user: this.state.alias, + from_room: this.state.room, }; - if(this.state.socket) { + if (this.state.socket) { this.state.socket.write(JSON.stringify(message) + '\n'); } } @@ -570,7 +621,3 @@ exports.getModule = class mrcModule extends MenuModule { this.sendHeartbeat(); } }; - - - - diff --git a/core/msg_area_list.js b/core/msg_area_list.js index 1d47f76c..43227a24 100644 --- a/core/msg_area_list.js +++ b/core/msg_area_list.js @@ -2,27 +2,27 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const messageArea = require('./message_area.js'); -const { Errors } = require('./enig_error.js'); -const UserProps = require('./user_property.js'); +const { MenuModule } = require('./menu_module.js'); +const messageArea = require('./message_area.js'); +const { Errors } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); // deps -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Area List', - desc : 'Module for listing / choosing message areas', - author : 'NuSkooler', + name: 'Message Area List', + desc: 'Module for listing / choosing message areas', + author: 'NuSkooler', }; // :TODO: Obv/2 others can show # of messages in area const MciViewIds = { - areaList : 1, - areaDesc : 2, // area desc updated @ index update - customRangeStart : 10, // updated @ index update + areaList: 1, + areaDesc: 2, // area desc updated @ index update + customRangeStart: 10, // updated @ index update }; exports.getModule = class MessageAreaListModule extends MenuModule { @@ -32,25 +32,32 @@ exports.getModule = class MessageAreaListModule extends MenuModule { this.initList(); this.menuMethods = { - changeArea : (formData, extraArgs, cb) => { - if(1 === formData.submitId) { + changeArea: (formData, extraArgs, cb) => { + if (1 === formData.submitId) { const area = this.messageAreas[formData.value.area]; messageArea.changeMessageArea(this.client, area.areaTag, err => { - if(err) { - this.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); + if (err) { + this.client.term.pipeWrite( + `\n|00Cannot change area: ${err.message}\n` + ); return this.prevMenuOnTimeout(1000, cb); } - if(area.hasArt) { + if (area.hasArt) { const menuOpts = { - extraArgs : { - areaTag : area.areaTag, + extraArgs: { + areaTag: area.areaTag, }, - menuFlags : [ 'popParent', 'noHistory' ] + menuFlags: ['popParent', 'noHistory'], }; - return this.gotoMenu(this.menuConfig.config.changeAreaPreArtMenu || 'changeMessageAreaPreArt', menuOpts, cb); + return this.gotoMenu( + this.menuConfig.config.changeAreaPreArtMenu || + 'changeMessageAreaPreArt', + menuOpts, + cb + ); } return this.prevMenu(cb); @@ -58,25 +65,31 @@ exports.getModule = class MessageAreaListModule extends MenuModule { } else { return cb(null); } - } + }, }; } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (next) => { + next => { return this.prepViewController('areaList', 0, mciData.menu, next); }, - (next) => { - const areaListView = this.viewControllers.areaList.getView(MciViewIds.areaList); - if(!areaListView) { - return cb(Errors.MissingMci(`Missing area list MCI ${MciViewIds.areaList}`)); + next => { + const areaListView = this.viewControllers.areaList.getView( + MciViewIds.areaList + ); + if (!areaListView) { + return cb( + Errors.MissingMci( + `Missing area list MCI ${MciViewIds.areaList}` + ) + ); } areaListView.on('index update', idx => { @@ -87,11 +100,14 @@ exports.getModule = class MessageAreaListModule extends MenuModule { areaListView.redraw(); this.selectionIndexUpdate(0); return next(null); - } + }, ], err => { - if(err) { - this.client.log.error( { error : err.message }, 'Failed loading message area list'); + if (err) { + this.client.log.error( + { error: err.message }, + 'Failed loading message area list' + ); } return cb(err); } @@ -101,27 +117,33 @@ exports.getModule = class MessageAreaListModule extends MenuModule { selectionIndexUpdate(idx) { const area = this.messageAreas[idx]; - if(!area) { + if (!area) { return; } this.setViewText('areaList', MciViewIds.areaDesc, area.desc); - this.updateCustomViewTextsWithFilter('areaList', MciViewIds.customRangeStart, area); + this.updateCustomViewTextsWithFilter( + 'areaList', + MciViewIds.customRangeStart, + area + ); } initList() { let index = 1; - this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( - this.client.user.properties[UserProps.MessageConfTag], - { client : this.client } - ).map(area => { - return { - index : index++, - areaTag : area.areaTag, - name : area.area.name, - text : area.area.name, // standard - desc : area.area.desc, - hasArt : _.isString(area.area.art), - }; - }); + this.messageAreas = messageArea + .getSortedAvailMessageAreasByConfTag( + this.client.user.properties[UserProps.MessageConfTag], + { client: this.client } + ) + .map(area => { + return { + index: index++, + areaTag: area.areaTag, + name: area.area.name, + text: area.area.name, // standard + desc: area.area.desc, + hasArt: _.isString(area.area.art), + }; + }); } }; diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 5f25fa19..cac2700a 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -1,19 +1,17 @@ /* jslint node: true */ 'use strict'; -const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; -const persistMessage = require('./message_area.js').persistMessage; -const UserProps = require('./user_property.js'); -const { - hasMessageConfAndAreaWrite, -} = require('./message_area.js'); +const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +const persistMessage = require('./message_area.js').persistMessage; +const UserProps = require('./user_property.js'); +const { hasMessageConfAndAreaWrite } = require('./message_area.js'); -const async = require('async'); +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 { @@ -25,8 +23,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { // 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( [ @@ -41,15 +38,19 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { }, function updateStats(callback) { self.updateUserAndSystemStats(callback); - } + }, ], function complete(err) { - if(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.messageUuid }, + { + to: msg.toUserName, + subject: msg.subject, + uuid: msg.messageUuid, + }, 'Message persisted' ); } @@ -62,14 +63,13 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { enter() { this.messageAreaTag = - this.messageAreaTag || - this.client.user.getProperty(UserProps.MessageAreaTag); + this.messageAreaTag || this.client.user.getProperty(UserProps.MessageAreaTag); super.enter(); } initSequence() { - if(!hasMessageConfAndAreaWrite(this.client, this.messageAreaTag)) { + if (!hasMessageConfAndAreaWrite(this.client, this.messageAreaTag)) { const noAcsMenu = this.menuConfig.config.messageBasePostMessageNoAccess || 'messageBasePostMessageNoAccess'; @@ -82,4 +82,4 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { super.initSequence(); } -}; \ No newline at end of file +}; diff --git a/core/msg_area_reply_fse.js b/core/msg_area_reply_fse.js index 11742865..9bb3afe7 100644 --- a/core/msg_area_reply_fse.js +++ b/core/msg_area_reply_fse.js @@ -1,14 +1,14 @@ /* jslint node: true */ 'use strict'; -var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; -exports.getModule = AreaReplyFSEModule; +exports.getModule = AreaReplyFSEModule; exports.moduleInfo = { - name : 'Message Area Reply', - desc : 'Module for replying to an area message', - author : 'NuSkooler', + name: 'Message Area Reply', + desc: 'Module for replying to an area message', + author: 'NuSkooler', }; function AreaReplyFSEModule(options) { diff --git a/core/msg_area_view_fse.js b/core/msg_area_view_fse.js index ed057438..4866092c 100644 --- a/core/msg_area_view_fse.js +++ b/core/msg_area_view_fse.js @@ -2,36 +2,36 @@ 'use strict'; // ENiGMA½ -const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; -const Message = require('./message.js'); +const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; +const Message = require('./message.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Message Area View', - desc : 'Module for viewing an area message', - author : 'NuSkooler', + name: 'Message Area View', + desc: 'Module for viewing an area message', + author: 'NuSkooler', }; exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { constructor(options) { super(options); - this.editorType = 'area'; - this.editorMode = 'view'; + this.editorType = 'area'; + this.editorMode = 'view'; - if(_.isObject(options.extraArgs)) { - this.messageList = options.extraArgs.messageList; - this.messageIndex = options.extraArgs.messageIndex; - this.lastMessageNextExit = options.extraArgs.lastMessageNextExit; + 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) { + if (this.messageList.length > 0) { this.messageAreaTag = this.messageList[this.messageIndex].areaTag; } @@ -39,18 +39,21 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { // assign *additional* menuMethods Object.assign(this.menuMethods, { - nextMessage : (formData, extraArgs, cb) => { - if(self.messageIndex + 1 < self.messageList.length) { + nextMessage: (formData, extraArgs, cb) => { + if (self.messageIndex + 1 < self.messageList.length) { self.messageIndex++; this.messageAreaTag = this.messageList[this.messageIndex].areaTag; - this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with + this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with - return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); + return self.loadMessageByUuid( + self.messageList[self.messageIndex].messageUuid, + cb + ); } // auto-exit if no more to go? - if(self.lastMessageNextExit) { + if (self.lastMessageNextExit) { self.lastMessageReached = true; return self.prevMenu(cb); } @@ -58,28 +61,39 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { return cb(null); }, - prevMessage : (formData, extraArgs, cb) => { - if(self.messageIndex > 0) { + 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.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); }, - 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; + 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 @@ -88,13 +102,13 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { return cb(null); }, - replyMessage : (formData, extraArgs, cb) => { - if(_.isString(extraArgs.menu)) { + replyMessage: (formData, extraArgs, cb) => { + if (_.isString(extraArgs.menu)) { const modOpts = { - extraArgs : { - messageAreaTag : self.messageAreaTag, - replyToMessage : self.message, - } + extraArgs: { + messageAreaTag: self.messageAreaTag, + replyToMessage: self.message, + }, }; return self.gotoMenu(extraArgs.menu, modOpts, cb); @@ -108,10 +122,10 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { loadMessageByUuid(uuid, cb) { const msg = new Message(); - msg.load( { uuid : uuid, user : this.client.user }, () => { + msg.load({ uuid: uuid, user: this.client.user }, () => { this.setMessage(msg); - if(cb) { + if (cb) { return cb(null); } }); @@ -123,22 +137,22 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { getSaveState() { return { - messageList : this.messageList, - messageIndex : this.messageIndex, - messageTotal : this.messageList.length, + messageList: this.messageList, + messageIndex: this.messageIndex, + messageTotal: this.messageList.length, }; } restoreSavedState(savedState) { - this.messageList = savedState.messageList; - this.messageIndex = savedState.messageIndex; - this.messageTotal = savedState.messageTotal; + this.messageList = savedState.messageList; + this.messageIndex = savedState.messageIndex; + this.messageTotal = savedState.messageTotal; } getMenuResult() { return { - messageIndex : this.messageIndex, - lastMessageReached : this.lastMessageReached, + messageIndex: this.messageIndex, + lastMessageReached: this.lastMessageReached, }; } }; diff --git a/core/msg_conf_list.js b/core/msg_conf_list.js index 7ba75376..74439be6 100644 --- a/core/msg_conf_list.js +++ b/core/msg_conf_list.js @@ -2,24 +2,24 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const messageArea = require('./message_area.js'); -const { Errors } = require('./enig_error.js'); +const { MenuModule } = require('./menu_module.js'); +const messageArea = require('./message_area.js'); +const { Errors } = require('./enig_error.js'); // deps -const async = require('async'); -const _ = require('lodash'); +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, - confDesc : 2, // description updated @ index update - customRangeStart : 10, // updated @ index update + confList: 1, + confDesc: 2, // description updated @ index update + customRangeStart: 10, // updated @ index update }; exports.getModule = class MessageConfListModule extends MenuModule { @@ -29,51 +29,68 @@ exports.getModule = class MessageConfListModule extends MenuModule { this.initList(); this.menuMethods = { - changeConference : (formData, extraArgs, cb) => { - if(1 === formData.submitId) { + changeConference: (formData, extraArgs, cb) => { + if (1 === formData.submitId) { const conf = this.messageConfs[formData.value.conf]; - messageArea.changeMessageConference(this.client, conf.confTag, err => { - if(err) { - this.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); - return this.prevMenuOnTimeout(1000, cb); + messageArea.changeMessageConference( + this.client, + conf.confTag, + err => { + if (err) { + this.client.term.pipeWrite( + `\n|00Cannot change conference: ${err.message}\n` + ); + return this.prevMenuOnTimeout(1000, cb); + } + + if (conf.hasArt) { + const menuOpts = { + extraArgs: { + confTag: conf.confTag, + }, + menuFlags: ['popParent', 'noHistory'], + }; + + return this.gotoMenu( + this.menuConfig.config.changeConfPreArtMenu || + 'changeMessageConfPreArt', + menuOpts, + cb + ); + } + + return this.prevMenu(cb); } - - if(conf.hasArt) { - const menuOpts = { - extraArgs : { - confTag : conf.confTag, - }, - menuFlags : [ 'popParent', 'noHistory' ] - }; - - return this.gotoMenu(this.menuConfig.config.changeConfPreArtMenu || 'changeMessageConfPreArt', menuOpts, cb); - } - - return this.prevMenu(cb); - }); + ); } else { return cb(null); } - } + }, }; } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (next) => { + next => { return this.prepViewController('confList', 0, mciData.menu, next); }, - (next) => { - const confListView = this.viewControllers.confList.getView(MciViewIds.confList); - if(!confListView) { - return next(Errors.MissingMci(`Missing conf list MCI ${MciViewIds.confList}`)); + next => { + const confListView = this.viewControllers.confList.getView( + MciViewIds.confList + ); + if (!confListView) { + return next( + Errors.MissingMci( + `Missing conf list MCI ${MciViewIds.confList}` + ) + ); } confListView.on('index update', idx => { @@ -84,11 +101,14 @@ exports.getModule = class MessageConfListModule extends MenuModule { confListView.redraw(); this.selectionIndexUpdate(0); return next(null); - } + }, ], err => { - if(err) { - this.client.log.error( { error : err.message }, 'Failed loading message conference list'); + if (err) { + this.client.log.error( + { error: err.message }, + 'Failed loading message conference list' + ); } } ); @@ -97,26 +117,31 @@ exports.getModule = class MessageConfListModule extends MenuModule { selectionIndexUpdate(idx) { const conf = this.messageConfs[idx]; - if(!conf) { + if (!conf) { return; } this.setViewText('confList', MciViewIds.confDesc, conf.desc); - this.updateCustomViewTextsWithFilter('confList', MciViewIds.customRangeStart, conf); + this.updateCustomViewTextsWithFilter( + 'confList', + MciViewIds.customRangeStart, + conf + ); } - initList() - { + initList() { let index = 1; - this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client).map(conf => { - return { - index : index++, - confTag : conf.confTag, - name : conf.conf.name, - text : conf.conf.name, - desc : conf.conf.desc, - areaCount : Object.keys(conf.conf.areas || {}).length, - hasArt : _.isString(conf.conf.art), - }; - }); + this.messageConfs = messageArea + .getSortedAvailMessageConferences(this.client) + .map(conf => { + return { + index: index++, + confTag: conf.confTag, + name: conf.conf.name, + text: conf.conf.name, + desc: conf.conf.desc, + areaCount: Object.keys(conf.conf.areas || {}).length, + hasArt: _.isString(conf.conf.art), + }; + }); } }; diff --git a/core/msg_list.js b/core/msg_list.js index 8b91585b..e94f1bae 100644 --- a/core/msg_list.js +++ b/core/msg_list.js @@ -2,18 +2,19 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const messageArea = require('./message_area.js'); -const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; -const Errors = require('./enig_error.js').Errors; -const Message = require('./message.js'); -const UserProps = require('./user_property.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const messageArea = require('./message_area.js'); +const MessageAreaConfTempSwitcher = + require('./mod_mixins.js').MessageAreaConfTempSwitcher; +const Errors = require('./enig_error.js').Errors; +const Message = require('./message.js'); +const UserProps = require('./user_property.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); /* Available itemFormat/focusItemFormat members for |msgList| @@ -26,54 +27,71 @@ const moment = require('moment'); newIndicator : New mark/indicator (config.newIndicator) */ 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 FormIds = { - allViews : 0, - delPrompt : 1, + allViews: 0, + delPrompt: 1, }; const MciViewIds = { - allViews : { - msgList : 1, // VM1 - see above - delPromptXy : 2, // %XY2, e.g: delete confirmation - customRangeStart : 10, // Everything |msgList| has plus { msgNumSelected, msgNumTotal } + allViews: { + msgList: 1, // VM1 - see above + delPromptXy: 2, // %XY2, e.g: delete confirmation + customRangeStart: 10, // Everything |msgList| has plus { msgNumSelected, msgNumTotal } }, delPrompt: { - prompt : 1, - } + prompt: 1, + }, }; -exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) { +exports.getModule = class MessageListModule extends ( + MessageAreaConfTempSwitcher(MenuModule) +) { constructor(options) { super(options); // :TODO: consider this pattern in base MenuModule - clean up code all over - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); + 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.allViews.msgList === formData.submitId) { + selectMessage: (formData, extraArgs, cb) => { + if (MciViewIds.allViews.msgList === formData.submitId) { // 'messageIndex' or older deprecated 'message' member - this.initialFocusIndex = _.get(formData, 'value.messageIndex', formData.value.message); + this.initialFocusIndex = _.get( + formData, + 'value.messageIndex', + formData.value.message + ); const modOpts = { - extraArgs : { - messageAreaTag : this.getSelectedAreaTag(this.initialFocusIndex), - messageList : this.config.messageList, - messageIndex : this.initialFocusIndex, - lastMessageNextExit : true, - } + extraArgs: { + messageAreaTag: this.getSelectedAreaTag( + this.initialFocusIndex + ), + messageList: this.config.messageList, + messageIndex: this.initialFocusIndex, + lastMessageNextExit: true, + }, }; - if(_.isBoolean(this.config.noUpdateLastReadId)) { - modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId; + if (_.isBoolean(this.config.noUpdateLastReadId)) { + modOpts.extraArgs.noUpdateLastReadId = + this.config.noUpdateLastReadId; } // @@ -81,72 +99,98 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // 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)); + 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, + messageAreaTag: this.messageAreaTag, + apprevMessageList: logMsgList, + messageCount: this.messageList.length, + messageIndex: this.messageIndex, }; }; - return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb); + return this.gotoMenu( + this.config.menuViewPost || 'messageAreaViewPost', + modOpts, + cb + ); } else { return cb(null); } }, - fullExit : (formData, extraArgs, cb) => { - this.menuResult = { fullExit : true }; + fullExit: (formData, extraArgs, cb) => { + this.menuResult = { fullExit: true }; return this.prevMenu(cb); }, - deleteSelected : (formData, extraArgs, cb) => { - if(MciViewIds.allViews.msgList != formData.submitId) { + deleteSelected: (formData, extraArgs, cb) => { + if (MciViewIds.allViews.msgList != formData.submitId) { return cb(null); } // newer 'messageIndex' or older deprecated value - const messageIndex = _.get(formData, 'value.messageIndex', formData.value.message); + const messageIndex = _.get( + formData, + 'value.messageIndex', + formData.value.message + ); return this.promptDeleteMessageConfirm(messageIndex, cb); }, - deleteMessageYes : (formData, extraArgs, cb) => { - const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); + deleteMessageYes: (formData, extraArgs, cb) => { + const msgListView = this.viewControllers.allViews.getView( + MciViewIds.allViews.msgList + ); this.enableMessageListIndexUpdates(msgListView); - if(this.selectedMessageForDelete) { + if (this.selectedMessageForDelete) { this.selectedMessageForDelete.deleteMessage(this.client.user, err => { - if(err) { - this.client.log.error(`Failed to delete message: ${this.selectedMessageForDelete.messageUuid}`); + if (err) { + this.client.log.error( + `Failed to delete message: ${this.selectedMessageForDelete.messageUuid}` + ); } else { - this.client.log.info(`User deleted message: ${this.selectedMessageForDelete.messageUuid}`); - this.config.messageList.splice(msgListView.focusedItemIndex, 1); - this.updateMessageNumbersAfterDelete(msgListView.focusedItemIndex); + this.client.log.info( + `User deleted message: ${this.selectedMessageForDelete.messageUuid}` + ); + this.config.messageList.splice( + msgListView.focusedItemIndex, + 1 + ); + this.updateMessageNumbersAfterDelete( + msgListView.focusedItemIndex + ); msgListView.setItems(this.config.messageList); } this.selectedMessageForDelete = null; msgListView.redraw(); - this.populateCustomLabelsForSelected(msgListView.focusedItemIndex); + this.populateCustomLabelsForSelected( + msgListView.focusedItemIndex + ); return cb(null); }); } else { return cb(null); } }, - deleteMessageNo : (formData, extraArgs, cb) => { - const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); + deleteMessageNo: (formData, extraArgs, cb) => { + const msgListView = this.viewControllers.allViews.getView( + MciViewIds.allViews.msgList + ); this.enableMessageListIndexUpdates(msgListView); return cb(null); }, - markAllRead : (formData, extraArgs, cb) => { - if(this.config.noUpdateLastReadId) { + markAllRead: (formData, extraArgs, cb) => { + if (this.config.noUpdateLastReadId) { return cb(null); } return this.markAllMessagesAsRead(cb); - } + }, }; } @@ -155,7 +199,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } enter() { - if(this.lastMessageReachedExit) { + if (this.lastMessageReachedExit) { return this.prevMenu(); } @@ -167,11 +211,12 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // 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) { + if (!Array.isArray(this.config.messageList)) { + if (this.config.messageAreaTag) { this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); } else { - this.config.messageAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; + this.config.messageAreaTag = + this.client.user.properties[UserProps.MessageAreaTag]; } } } @@ -184,30 +229,36 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( populateCustomLabelsForSelected(selectedIndex) { const formatObj = Object.assign( { - msgNumSelected : (selectedIndex + 1), - msgNumTotal : this.config.messageList.length, + msgNumSelected: selectedIndex + 1, + msgNumTotal: this.config.messageList.length, }, this.config.messageList[selectedIndex] // plus, all the selected message props ); - return this.updateCustomViewTextsWithFilter('allViews', MciViewIds.allViews.customRangeStart, formatObj); + return this.updateCustomViewTextsWithFilter( + 'allViews', + MciViewIds.allViews.customRangeStart, + formatObj + ); } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(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 configProvidedMessageList = false; async.series( [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu + callingMenu: self, + mciMap: mciData.menu, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -216,49 +267,70 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( // // Config can supply messages else we'll need to populate the list now // - if(_.isArray(self.config.messageList)) { + if (_.isArray(self.config.messageList)) { configProvidedMessageList = true; - return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); + 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); - }); + self.config.messageList = msgList; + return callback(err); + } + ); }, function getLastReadMessageId(callback) { // messageList entries can contain |isNew| if they want to be considered new - if(configProvidedMessageList) { + 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 - }); + 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 = ' '.repeat(newIndicator.length); // fill with space to avoid draw issues + const dateTimeFormat = + self.menuConfig.config.dateTimeFormat || + self.client.currentTheme.helpers.getDateTimeFormat(); + const newIndicator = self.menuConfig.config.newIndicator || '*'; + const regIndicator = ' '.repeat(newIndicator.length); // 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; + 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) { + if ( + _.isUndefined(self.initialFocusIndex) && + listItem.messageId > self.lastReadId + ) { self.initialFocusIndex = index; } - listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text + listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text }); return callback(null); }, @@ -267,7 +339,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( msgListView.setItems(self.config.messageList); self.enableMessageListIndexUpdates(msgListView); - if(self.initialFocusIndex > 0) { + if (self.initialFocusIndex > 0) { // note: causes redraw() msgListView.setFocusItemIndex(self.initialFocusIndex); } else { @@ -279,8 +351,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( }, ], err => { - if(err) { - self.client.log.error( { error : err.message }, 'Error loading message list'); + if (err) { + self.client.log.error( + { error: err.message }, + 'Error loading message list' + ); } return cb(err); } @@ -289,11 +364,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } getSaveState() { - return { initialFocusIndex : this.initialFocusIndex }; + return { initialFocusIndex: this.initialFocusIndex }; } restoreSavedState(savedState) { - if(savedState) { + if (savedState) { this.initialFocusIndex = savedState.initialFocusIndex; } } @@ -303,12 +378,12 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } enableMessageListIndexUpdates(msgListView) { - msgListView.on('index update', idx => this.populateCustomLabelsForSelected(idx) ); + msgListView.on('index update', idx => this.populateCustomLabelsForSelected(idx)); } markAllMessagesAsRead(cb) { - if(!this.config.messageList || this.config.messageList.length === 0) { - return cb(null); // nothing to do. + if (!this.config.messageList || this.config.messageList.length === 0) { + return cb(null); // nothing to do. } // @@ -320,8 +395,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( const areaHighestIds = {}; this.config.messageList.forEach(msg => { const highestId = areaHighestIds[msg.areaTag]; - if(highestId) { - if(msg.messageId > highestId) { + if (highestId) { + if (msg.messageId > highestId) { areaHighestIds[msg.areaTag] = msg.messageId; } } else { @@ -329,38 +404,52 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } }); - const regIndicator = ' '.repeat( (this.menuConfig.config.newIndicator || '*').length ); - async.forEachOf(areaHighestIds, (highestId, areaTag, nextArea) => { - messageArea.updateMessageAreaLastReadId( - this.client.user.userId, - areaTag, - highestId, - err => { - if(err) { - this.client.log.warn( { error : err.message }, 'Failed marking area as read'); - } else { - // update newIndicator on messages - this.config.messageList.forEach(msg => { - if(areaTag === msg.areaTag) { - msg.newIndicator = regIndicator; - } - }); - const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); - msgListView.setItems(this.config.messageList); - msgListView.redraw(); - this.client.log.info( { highestId, areaTag }, 'User marked area as read'); + const regIndicator = ' '.repeat( + (this.menuConfig.config.newIndicator || '*').length + ); + async.forEachOf( + areaHighestIds, + (highestId, areaTag, nextArea) => { + messageArea.updateMessageAreaLastReadId( + this.client.user.userId, + areaTag, + highestId, + err => { + if (err) { + this.client.log.warn( + { error: err.message }, + 'Failed marking area as read' + ); + } else { + // update newIndicator on messages + this.config.messageList.forEach(msg => { + if (areaTag === msg.areaTag) { + msg.newIndicator = regIndicator; + } + }); + const msgListView = this.viewControllers.allViews.getView( + MciViewIds.allViews.msgList + ); + msgListView.setItems(this.config.messageList); + msgListView.redraw(); + this.client.log.info( + { highestId, areaTag }, + 'User marked area as read' + ); + } + return nextArea(null); // always continue } - return nextArea(null); // always continue - } - ); - }, () => { - return cb(null); - }); + ); + }, + () => { + return cb(null); + } + ); } updateMessageNumbersAfterDelete(startIndex) { // all index -= 1 from this point on. - for(let i = startIndex; i < this.config.messageList.length; ++i) { + for (let i = startIndex; i < this.config.messageList.length; ++i) { const msgItem = this.config.messageList[i]; msgItem.msgNum -= 1; msgItem.text = `${msgItem.msgNum} - ${msgItem.subject} from ${msgItem.fromUserName}`; // default text @@ -369,21 +458,25 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( promptDeleteMessageConfirm(messageIndex, cb) { const messageInfo = this.config.messageList[messageIndex]; - if(!_.isObject(messageInfo)) { + if (!_.isObject(messageInfo)) { return cb(Errors.Invalid(`Invalid message index: ${messageIndex}`)); } // :TODO: create static userHasDeleteRights() that takes id || uuid that doesn't require full msg load this.selectedMessageForDelete = new Message(); - this.selectedMessageForDelete.load( { uuid : messageInfo.messageUuid }, err => { - if(err) { + this.selectedMessageForDelete.load({ uuid: messageInfo.messageUuid }, err => { + if (err) { this.selectedMessageForDelete = null; return cb(err); } - if(!this.selectedMessageForDelete.userHasDeleteRights(this.client.user)) { + if (!this.selectedMessageForDelete.userHasDeleteRights(this.client.user)) { this.selectedMessageForDelete = null; - return cb(Errors.AccessDenied('User does not have rights to delete this message')); + return cb( + Errors.AccessDenied( + 'User does not have rights to delete this message' + ) + ); } // user has rights to delete -- prompt/confirm then proceed @@ -392,25 +485,33 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher( } promptConfirmDelete(cb) { - const promptXyView = this.viewControllers.allViews.getView(MciViewIds.allViews.delPromptXy); - if(!promptXyView) { - return cb(Errors.MissingMci(`Missing prompt XY${MciViewIds.allViews.delPromptXy} MCI`)); + const promptXyView = this.viewControllers.allViews.getView( + MciViewIds.allViews.delPromptXy + ); + if (!promptXyView) { + return cb( + Errors.MissingMci( + `Missing prompt XY${MciViewIds.allViews.delPromptXy} MCI` + ) + ); } const promptOpts = { - clearAtSubmit : true, + clearAtSubmit: true, }; - if(promptXyView.dimens.width) { + if (promptXyView.dimens.width) { promptOpts.clearWidth = promptXyView.dimens.width; } return this.promptForInput( { - formName : 'delPrompt', - formId : FormIds.delPrompt, - promptName : this.config.deleteMessageFromListPrompt || 'deleteMessageFromListPrompt', - prevFormName : 'allViews', - position : promptXyView.position, + formName: 'delPrompt', + formId: FormIds.delPrompt, + promptName: + this.config.deleteMessageFromListPrompt || + 'deleteMessageFromListPrompt', + prevFormName: 'allViews', + position: promptXyView.position, }, promptOpts, err => { diff --git a/core/msg_network.js b/core/msg_network.js index e0018ece..e9df2e1e 100644 --- a/core/msg_network.js +++ b/core/msg_network.js @@ -2,14 +2,14 @@ 'use strict'; // ENiGMA½ -const loadModulesForCategory = require('./module_util.js').loadModulesForCategory; +const loadModulesForCategory = require('./module_util.js').loadModulesForCategory; // standard/deps -const async = require('async'); +const async = require('async'); -exports.startup = startup; -exports.shutdown = shutdown; -exports.recordMessage = recordMessage; +exports.startup = startup; +exports.shutdown = shutdown; +exports.recordMessage = recordMessage; let msgNetworkModules = []; @@ -17,19 +17,23 @@ function startup(cb) { async.series( [ function loadModules(callback) { - loadModulesForCategory('scannerTossers', (module, nextModule) => { - const modInst = new module.getModule(); + loadModulesForCategory( + 'scannerTossers', + (module, nextModule) => { + const modInst = new module.getModule(); - modInst.startup(err => { - if(!err) { - msgNetworkModules.push(modInst); - } - }); - return nextModule(null); - }, err => { - callback(err); - }); - } + modInst.startup(err => { + if (!err) { + msgNetworkModules.push(modInst); + } + }); + return nextModule(null); + }, + err => { + callback(err); + } + ); + }, ], cb ); @@ -39,7 +43,7 @@ function shutdown(cb) { async.each( msgNetworkModules, (msgNetModule, next) => { - msgNetModule.shutdown( () => { + msgNetModule.shutdown(() => { return next(); }); }, @@ -56,10 +60,14 @@ function recordMessage(message, cb) { // 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 + async.each( + msgNetworkModules, + (modInst, next) => { + modInst.record(message); + next(); + }, + err => { + cb(err); + } + ); +} diff --git a/core/msg_scan_toss_module.js b/core/msg_scan_toss_module.js index 59c94be0..999d4098 100644 --- a/core/msg_scan_toss_module.js +++ b/core/msg_scan_toss_module.js @@ -2,9 +2,9 @@ 'use strict'; // ENiGMA½ -var PluginModule = require('./plugin_module.js').PluginModule; +var PluginModule = require('./plugin_module.js').PluginModule; -exports.MessageScanTossModule = MessageScanTossModule; +exports.MessageScanTossModule = MessageScanTossModule; function MessageScanTossModule() { PluginModule.call(this); @@ -12,13 +12,12 @@ function MessageScanTossModule() { require('util').inherits(MessageScanTossModule, PluginModule); -MessageScanTossModule.prototype.startup = function(cb) { +MessageScanTossModule.prototype.startup = function (cb) { return cb(null); }; -MessageScanTossModule.prototype.shutdown = function(cb) { +MessageScanTossModule.prototype.shutdown = function (cb) { return cb(null); }; -MessageScanTossModule.prototype.record = function(/*message*/) { -}; \ No newline at end of file +MessageScanTossModule.prototype.record = function (/*message*/) {}; diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index ce6cf10b..025f14d4 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -1,14 +1,14 @@ /* jslint node: true */ 'use strict'; -const View = require('./view.js').View; -const strUtil = require('./string_util.js'); -const ansi = require('./ansi_term.js'); -const wordWrapText = require('./word_wrap.js').wordWrapText; -const ansiPrep = require('./ansi_prep.js'); +const View = require('./view.js').View; +const strUtil = require('./string_util.js'); +const ansi = require('./ansi_term.js'); +const wordWrapText = require('./word_wrap.js').wordWrapText; +const ansiPrep = require('./ansi_prep.js'); -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); // :TODO: Determine CTRL-* keys for various things // See http://www.bbsdocumentary.com/library/PROGRAMS/GRAPHICS/ANSI/bansi.txt @@ -61,37 +61,36 @@ const _ = require('lodash'); // * Add word delete (CTRL+????) // * - const SPECIAL_KEY_MAP_DEFAULT = { - 'line feed' : [ 'return' ], - exit : [ 'esc' ], - backspace : [ 'backspace', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ - delete : [ 'delete' ], - tab : [ 'tab' ], - up : [ 'up arrow' ], - down : [ 'down arrow' ], - end : [ 'end' ], - home : [ 'home' ], - left : [ 'left arrow' ], - right : [ 'right arrow' ], - 'delete line' : [ 'ctrl + y', 'ctrl + u' ], // https://en.wikipedia.org/wiki/Backspace - 'page up' : [ 'page up' ], - 'page down' : [ 'page down' ], - insert : [ 'insert', 'ctrl + v' ], + 'line feed': ['return'], + exit: ['esc'], + backspace: ['backspace', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ + delete: ['delete'], + tab: ['tab'], + up: ['up arrow'], + down: ['down arrow'], + end: ['end'], + home: ['home'], + left: ['left arrow'], + right: ['right arrow'], + 'delete line': ['ctrl + y', 'ctrl + u'], // https://en.wikipedia.org/wiki/Backspace + 'page up': ['page up'], + 'page down': ['page down'], + insert: ['insert', 'ctrl + v'], }; -exports.MultiLineEditTextView = MultiLineEditTextView; +exports.MultiLineEditTextView = MultiLineEditTextView; function MultiLineEditTextView(options) { - if(!_.isBoolean(options.acceptsFocus)) { + if (!_.isBoolean(options.acceptsFocus)) { options.acceptsFocus = true; } - if(!_.isBoolean(this.acceptsInput)) { + if (!_.isBoolean(this.acceptsInput)) { options.acceptsInput = true; } - if(!_.isObject(options.specialKeyMap)) { + if (!_.isObject(options.specialKeyMap)) { options.specialKeyMap = SPECIAL_KEY_MAP_DEFAULT; } @@ -109,11 +108,11 @@ function MultiLineEditTextView(options) { // This seems overkill though, so let's default to 4 :) // :TODO: what shoudl this really be? Maybe 8 is OK // - this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4; + this.tabWidth = _.isNumber(options.tabWidth) ? options.tabWidth : 4; - this.textLines = [ ]; - this.topVisibleIndex = 0; - this.mode = options.mode || 'edit'; // edit | preview | read-only + this.textLines = []; + this.topVisibleIndex = 0; + this.mode = options.mode || 'edit'; // edit | preview | read-only if ('preview' === this.mode) { this.autoScroll = options.autoScroll || true; @@ -126,89 +125,95 @@ function MultiLineEditTextView(options) { // cursorPos represents zero-based row, col positions // within the editor itself // - this.cursorPos = { col : 0, row : 0 }; + this.cursorPos = { col: 0, row: 0 }; - this.getSGRFor = function(sgrFor) { - return { - text : self.getSGR(), - }[sgrFor] || self.getSGR(); + this.getSGRFor = function (sgrFor) { + return ( + { + text: self.getSGR(), + }[sgrFor] || self.getSGR() + ); }; - this.isEditMode = function() { + this.isEditMode = function () { return 'edit' === self.mode; }; - this.isPreviewMode = function() { + 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)) { + 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)) { + 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) { + 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) { + 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) { + for (let i = startIndex; i < endIndex; ++i) { //${self.getSGRFor('text')} self.client.term.write( `${ansi.goto(absPos.row++, absPos.col)}${self.getRenderText(i)}`, - false // convertLineFeeds + false // convertLineFeeds ); } self.toggleTextCursor('show'); - return absPos.row - self.position.row; // row we ended on + return absPos.row - self.position.row; // row we ended on }; - this.eraseRows = function(startRow, endRow) { + 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) { + while (absPos.row < absPosEnd.row) { self.client.term.write( `${ansi.goto(absPos.row++, absPos.col)}${eraseFiller}`, - false // convertLineFeeds + false // convertLineFeeds ); } self.toggleTextCursor('show'); }; - this.redrawVisibleArea = function() { + this.redrawVisibleArea = function () { assert(self.topVisibleIndex <= self.textLines.length); const lastRow = self.redrawRows(0, self.dimens.height); @@ -227,72 +232,72 @@ function MultiLineEditTextView(options) { */ }; - this.getVisibleText = function(index) { - if(!_.isNumber(index)) { + 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)) { + 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)) { + 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)) { + this.getCharacter = function (index, col) { + if (!_.isNumber(col)) { col = self.cursorPos.col; } return self.getText(index).charAt(col); }; - this.isTab = function(index, col) { + this.isTab = function (index, col) { return '\t' === self.getCharacter(index, col); }; - this.getTextEndOfLineColumn = function(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 - strUtil.renderStringLength(text); + this.getRenderText = function (index) { + let text = self.getVisibleText(index); + const remain = self.dimens.width - strUtil.renderStringLength(text); - if(remain > 0) { - text += ' '.repeat(remain);// + 1); + if (remain > 0) { + text += ' '.repeat(remain); // + 1); } return text; }; - this.getTextLines = function(startIndex, endIndex) { + this.getTextLines = function (startIndex, endIndex) { var lines; - if(startIndex === endIndex) { - lines = [ self.textLines[startIndex] ]; + 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) { + this.getOutputText = function (startIndex, endIndex, eolMarker, options) { const lines = self.getTextLines(startIndex, endIndex); - let text = ''; - const re = new RegExp('\\t{1,' + (self.tabWidth) + '}', 'g'); + let text = ''; + const re = new RegExp('\\t{1,' + self.tabWidth + '}', 'g'); lines.forEach(line => { text += line.text.replace(re, '\t'); - if(options.forceLineTerms || (eolMarker && line.eol)) { + if (options.forceLineTerms || (eolMarker && line.eol)) { text += eolMarker; } }); @@ -300,21 +305,24 @@ function MultiLineEditTextView(options) { return text; }; - this.getContiguousText = function(startIndex, endIndex, includeEol) { + this.getContiguousText = function (startIndex, endIndex, includeEol) { var lines = self.getTextLines(startIndex, endIndex); var text = ''; - for(var i = 0; i < lines.length; ++i) { + for (var i = 0; i < lines.length; ++i) { text += lines[i].text; - if(includeEol && lines[i].eol) { + if (includeEol && lines[i].eol) { text += '\n'; } } return text; }; - this.replaceCharacterInText = function(c, index, col) { + this.replaceCharacterInText = function (c, index, col) { self.textLines[index].text = strUtil.replaceAt( - self.textLines[index].text, col, c); + self.textLines[index].text, + col, + c + ); }; /* @@ -336,22 +344,28 @@ 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; Array.prototype.splice.apply( self.textLines, - [ index, (nextEolIndex - index) + 1 ].concat(newLines)); + [index, nextEolIndex - index + 1].concat(newLines) + ); return wrapped.firstWrapRange; }; - this.removeCharactersFromText = function(index, col, operation, count) { - if('delete' === operation) { + 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); @@ -365,13 +379,13 @@ function MultiLineEditTextView(options) { self.textLines[index].text.slice(0, col - (count - 1)) + self.textLines[index].text.slice(col + 1); - self.cursorPos.col -= (count - 1); + self.cursorPos.col -= count - 1; self.updateTextWordWrap(index); self.redrawRows(self.cursorPos.row, self.dimens.height); self.moveClientCursorToCursorPos(); - } else if('delete line' === operation) { + } 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 @@ -379,19 +393,19 @@ function MultiLineEditTextView(options) { // treat all of these things using the physical approach, but this seems // a bit odd in this context. // - var isLastLine = (index === self.textLines.length - 1); - var hadEol = self.textLines[index].eol; + var isLastLine = index === self.textLines.length - 1; + var hadEol = self.textLines[index].eol; self.textLines.splice(index, 1); - if(hadEol && self.textLines.length > index && !self.textLines[index].eol) { + 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 } ]; + if (self.textLines.length < 1) { + self.textLines = [{ text: '', eol: true }]; isLastLine = false; // resetting } @@ -403,7 +417,7 @@ function MultiLineEditTextView(options) { // // If we just deleted the last line in the buffer, move up // - if(isLastLine) { + if (isLastLine) { self.cursorEndOfPreviousLine(); } else { self.moveClientCursorToCursorPos(); @@ -411,29 +425,29 @@ function MultiLineEditTextView(options) { } }; - this.insertCharactersInText = function(c, index, col) { - const prevTextLength = self.getTextLength(index); - let editingEol = self.cursorPos.col === prevTextLength; + 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) + self.textLines[index].text.slice(col), ].join(''); self.cursorPos.col += c.length; - if(self.getTextLength(index) > self.dimens.width) { + 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) { + const lastCol = self.cursorPos.col - c.length; + const firstWrapRange = self.updateTextWordWrap(index); + if (lastCol >= firstWrapRange.start && lastCol <= firstWrapRange.end) { cursorOffset = self.cursorPos.col - firstWrapRange.start; - editingEol = true; //override + editingEol = true; //override } else { cursorOffset = firstWrapRange.end; } @@ -443,135 +457,150 @@ function MultiLineEditTextView(options) { // 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) { + 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); + 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); + 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 + `${ansi.hideCursor()}${self.getSGRFor('text')}${renderText}${ansi.goto( + absPos.row, + absPos.col + )}${ansi.showCursor()}`, + false // convertLineFeeds ); } }; - this.getRemainingTabWidth = function(col) { - if(!_.isNumber(col)) { + this.getRemainingTabWidth = function (col) { + if (!_.isNumber(col)) { col = self.cursorPos.col; } return self.tabWidth - (col % self.tabWidth); }; - this.calculateTabStops = function() { - self.tabStops = [ 0 ]; + this.calculateTabStops = function () { + self.tabStops = [0]; var col = 0; - while(col < self.dimens.width) { + while (col < self.dimens.width) { col += self.getRemainingTabWidth(col); self.tabStops.push(col); } }; - this.getNextTabStop = function(col) { + this.getNextTabStop = function (col) { var i = self.tabStops.length; - while(self.tabStops[--i] > col); + while (self.tabStops[--i] > col); return self.tabStops[++i]; }; - this.getPrevTabStop = function(col) { + this.getPrevTabStop = function (col) { var i = self.tabStops.length; - while(self.tabStops[--i] >= col); + while (self.tabStops[--i] >= col); return self.tabStops[i]; }; - this.expandTab = function(col, expandChar) { + 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.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) )) { + 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 } ); + 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) { + 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 : '' }; } ) + ...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 } ); + 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 - ); + self.textLines.splice(index, 0, ...newLines); } }; - this.setAnsiWithOptions = function(ansi, options, cb) { - + 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 + self.setTextLines([line], index, true); // true=termWithEol index += 1; }); self.cursorStartOfDocument(); - if(cb) { + if (cb) { return cb(null); } } - if(options.prepped) { + if (options.prepped) { return setLines(ansi); } ansiPrep( ansi, { - termWidth : options.termWidth || this.client.term.termWidth, - termHeight : options.termHeight || this.client.term.termHeight, - cols : this.dimens.width, - rows : 'auto', - startCol : this.position.col, - forceLineTerm : options.forceLineTerm, + termWidth: options.termWidth || this.client.term.termWidth, + termHeight: options.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); @@ -579,7 +608,7 @@ function MultiLineEditTextView(options) { ); }; - this.insertRawText = function(text, index, col) { + this.insertRawText = function (text, index, col) { // // Perform the following on |text|: // * Normalize various line feed formats -> \n @@ -596,8 +625,8 @@ function MultiLineEditTextView(options) { // // :TODO: support index/col insertion point - if(_.isNumber(index)) { - if(_.isNumber(col)) { + if (_.isNumber(index)) { + if (_.isNumber(col)) { // // Modify text to have information from index // before and and after column @@ -617,25 +646,24 @@ function MultiLineEditTextView(options) { text.forEach(line => { wrapped = self.wordWrapSingleLine(line, 'expand').wrapped; - self.setTextLines(wrapped, index, true); // true=termWithEol + self.setTextLines(wrapped, index, true); // true=termWithEol index += wrapped.length; }); }; - this.getAbsolutePosition = function(row, col) { + this.getAbsolutePosition = function (row, col) { return { - row : self.position.row + row, - col : self.position.col + col, + row: self.position.row + row, + col: self.position.col + col, }; }; - this.moveClientCursorToCursorPos = function() { + 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) { + this.keyPressCharacter = function (c) { var index = self.getTextLinesIndex(); // @@ -647,7 +675,7 @@ function MultiLineEditTextView(options) { // // - if(self.overtypeMode) { + if (self.overtypeMode) { // :TODO: special handing for insert over eol mark? self.replaceCharacterInText(c, index, self.cursorPos.col); self.cursorPos.col++; @@ -659,12 +687,12 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressUp = function() { - if(self.cursorPos.row > 0) { + this.keyPressUp = function () { + if (self.cursorPos.row > 0) { self.cursorPos.row--; self.client.term.rawWrite(ansi.up()); - if(!self.adjustCursorToNextTab('up')) { + if (!self.adjustCursorToNextTab('up')) { self.adjustCursorIfPastEndOfLine(false); } } else { @@ -675,16 +703,16 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressDown = function() { - var lastVisibleRow = Math.min( - self.dimens.height, - (self.textLines.length - self.topVisibleIndex)) - 1; + this.keyPressDown = function () { + var lastVisibleRow = + Math.min(self.dimens.height, self.textLines.length - self.topVisibleIndex) - + 1; - if(self.cursorPos.row < lastVisibleRow) { + if (self.cursorPos.row < lastVisibleRow) { self.cursorPos.row++; self.client.term.rawWrite(ansi.down()); - if(!self.adjustCursorToNextTab('down')) { + if (!self.adjustCursorToNextTab('down')) { self.adjustCursorIfPastEndOfLine(false); } } else { @@ -695,14 +723,14 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressLeft = function() { - if(self.cursorPos.col > 0) { + this.keyPressLeft = function () { + if (self.cursorPos.col > 0) { var prevCharIsTab = self.isTab(); self.cursorPos.col--; self.client.term.rawWrite(ansi.left()); - if(prevCharIsTab) { + if (prevCharIsTab) { self.adjustCursorToNextTab('left'); } } else { @@ -712,15 +740,15 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressRight = function() { + this.keyPressRight = function () { var eolColumn = self.getTextEndOfLineColumn(); - if(self.cursorPos.col < eolColumn) { + if (self.cursorPos.col < eolColumn) { var prevCharIsTab = self.isTab(); self.cursorPos.col++; self.client.term.rawWrite(ansi.right()); - if(prevCharIsTab) { + if (prevCharIsTab) { self.adjustCursorToNextTab('right'); } } else { @@ -730,9 +758,9 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressHome = function() { + this.keyPressHome = function () { var firstNonWhitespace = self.getVisibleText().search(/\S/); - if(-1 !== firstNonWhitespace) { + if (-1 !== firstNonWhitespace) { self.cursorPos.col = firstNonWhitespace; } else { self.cursorPos.col = 0; @@ -742,14 +770,14 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressEnd = function() { + this.keyPressEnd = function () { self.cursorPos.col = self.getTextEndOfLineColumn(); self.moveClientCursorToCursorPos(); self.emitEditPosition(); }; - this.keyPressPageUp = function() { - if(self.topVisibleIndex > 0) { + this.keyPressPageUp = function () { + if (self.topVisibleIndex > 0) { self.topVisibleIndex = Math.max(0, self.topVisibleIndex - self.dimens.height); self.redraw(); self.adjustCursorIfPastEndOfLine(true); @@ -761,9 +789,9 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressPageDown = function() { + this.keyPressPageDown = function () { var linesBelow = self.getRemainingLinesBelowRow(); - if(linesBelow > 0) { + if (linesBelow > 0) { self.topVisibleIndex += Math.min(linesBelow, self.dimens.height); self.redraw(); self.adjustCursorIfPastEndOfLine(true); @@ -772,25 +800,29 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressLineFeed = function() { + 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; + 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.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)); + [index, nextEolIndex - index + 1].concat(newLines) + ); // redraw from current row to end of visible area self.redrawRows(self.cursorPos.row, self.dimens.height); @@ -799,19 +831,23 @@ function MultiLineEditTextView(options) { self.emitEditPosition(); }; - this.keyPressInsert = function() { + this.keyPressInsert = function () { self.toggleTextEditMode(); }; - this.keyPressTab = function() { + this.keyPressTab = function () { var index = self.getTextLinesIndex(); - self.insertCharactersInText(self.expandTab(self.cursorPos.col, '\t') + '\t', index, self.cursorPos.col); + self.insertCharactersInText( + self.expandTab(self.cursorPos.col, '\t') + '\t', + index, + self.cursorPos.col + ); self.emitEditPosition(); }; - this.keyPressBackspace = function() { - if(self.cursorPos.col >= 1) { + 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! @@ -821,26 +857,22 @@ function MultiLineEditTextView(options) { var index = self.getTextLinesIndex(); var count; - if(self.isTab()) { + if (self.isTab()) { var col = self.cursorPos.col; var prevTabStop = self.getPrevTabStop(self.cursorPos.col); - while(col >= prevTabStop) { - if(!self.isTab(index, col)) { + while (col >= prevTabStop) { + if (!self.isTab(index, col)) { break; } --col; } - count = (self.cursorPos.col - col); + count = self.cursorPos.col - col; } else { count = 1; } - self.removeCharactersFromText( - index, - self.cursorPos.col, - 'backspace', - count); + self.removeCharactersFromText(index, self.cursorPos.col, 'backspace', count); } else { // // Delete character at end of line previous. @@ -848,96 +880,91 @@ function MultiLineEditTextView(options) { // * 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.keyPressLeft(); // same as hitting left - jump to previous line //self.keyPressBackspace(); } self.emitEditPosition(); }; - this.keyPressDelete = function() { + this.keyPressDelete = function () { const lineIndex = self.getTextLinesIndex(); - if(0 === self.cursorPos.col && 0 === self.textLines[lineIndex].text.length && self.textLines.length > 0) { + 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' - ); + self.removeCharactersFromText(lineIndex, 0, 'delete line'); } else { - self.removeCharactersFromText( - lineIndex, - self.cursorPos.col, - 'delete', - 1 - ); + 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'); + this.keyPressDeleteLine = function () { + if (self.textLines.length > 0) { + self.removeCharactersFromText(self.getTextLinesIndex(), 0, 'delete line'); } self.emitEditPosition(); }; - this.adjustCursorIfPastEndOfLine = function(forceUpdate) { + this.adjustCursorIfPastEndOfLine = function (forceUpdate) { var eolColumn = self.getTextEndOfLineColumn(); - if(self.cursorPos.col > eolColumn) { + if (self.cursorPos.col > eolColumn) { self.cursorPos.col = eolColumn; forceUpdate = true; } - if(forceUpdate) { + if (forceUpdate) { self.moveClientCursorToCursorPos(); } }; - this.adjustCursorToNextTab = function(direction) { - if(self.isTab()) { + this.adjustCursorToNextTab = function (direction) { + if (self.isTab()) { var move; - switch(direction) { + switch (direction) { // // Next tabstop to the right // - case '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' : + // + // 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' : + 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); + return Math.abs(curr - self.cursorPos.col) < + Math.abs(prev - self.cursorPos.col) + ? curr + : prev; }); - if(newCol > self.cursorPos.col) { + 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) { + } else if (newCol < self.cursorPos.col) { move = self.cursorPos.col - newCol; self.cursorPos.col -= move; self.client.term.rawWrite(ansi.left(move)); @@ -947,53 +974,53 @@ function MultiLineEditTextView(options) { return true; } - return false; // did not fall on a tab + return false; // did not fall on a tab }; - this.cursorStartOfDocument = function() { - self.topVisibleIndex = 0; - self.cursorPos = { row : 0, col : 0 }; + 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(); + 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() { + 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) { + 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' + self.keyPressHome(); // same as pressing 'home' } }; - this.cursorEndOfPreviousLine = function() { + this.cursorEndOfPreviousLine = function () { // e.g. when scrolling left past start of line var moveToEnd; - if(self.cursorPos.row > 0) { + if (self.cursorPos.row > 0) { self.cursorPos.row--; moveToEnd = true; - } else if(self.topVisibleIndex > 0) { + } else if (self.topVisibleIndex > 0) { self.scrollDocumentDown(); moveToEnd = true; } - if(moveToEnd) { + if (moveToEnd) { self.keyPressEnd(); // same as pressing 'end' } }; @@ -1014,34 +1041,34 @@ function MultiLineEditTextView(options) { }; */ - this.scrollDocumentUp = function() { + this.scrollDocumentUp = function () { // // Note: We scroll *up* when the cursor goes *down* beyond // the visible area! // var linesBelow = self.getRemainingLinesBelowRow(); - if(linesBelow > 0) { + if (linesBelow > 0) { self.topVisibleIndex++; self.redraw(); } }; - this.scrollDocumentDown = function() { + this.scrollDocumentDown = function () { // // Note: We scroll *down* when the cursor goes *up* beyond // the visible area! // - if(self.topVisibleIndex > 0) { + 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() { + this.toggleTextEditMode = function () { self.overtypeMode = !self.overtypeMode; self.emit('text edit mode', self.getTextEditMode()); }; @@ -1051,27 +1078,30 @@ function MultiLineEditTextView(options) { require('util').inherits(MultiLineEditTextView, View); -MultiLineEditTextView.prototype.setWidth = function(width) { +MultiLineEditTextView.prototype.setWidth = function (width) { MultiLineEditTextView.super_.prototype.setWidth.call(this, width); this.calculateTabStops(); }; -MultiLineEditTextView.prototype.redraw = function() { +MultiLineEditTextView.prototype.redraw = function () { MultiLineEditTextView.super_.prototype.redraw.call(this); this.redrawVisibleArea(); }; -MultiLineEditTextView.prototype.setFocus = function(focused) { +MultiLineEditTextView.prototype.setFocus = function (focused) { this.client.term.rawWrite(this.getSGRFor('text')); this.moveClientCursorToCursorPos(); MultiLineEditTextView.super_.prototype.setFocus.call(this, focused); }; -MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode : 'default' } ) { - this.textLines = [ ]; +MultiLineEditTextView.prototype.setText = function ( + text, + options = { scrollMode: 'default' } +) { + this.textLines = []; this.addText(text, options); /*this.insertRawText(text); @@ -1082,53 +1112,60 @@ MultiLineEditTextView.prototype.setText = function(text, options = { scrollMode }*/ }; -MultiLineEditTextView.prototype.setAnsi = function(ansi, options = { prepped : false }, cb) { - this.textLines = [ ]; +MultiLineEditTextView.prototype.setAnsi = function ( + ansi, + options = { prepped: false }, + cb +) { + this.textLines = []; return this.setAnsiWithOptions(ansi, options, cb); }; -MultiLineEditTextView.prototype.addText = function(text, options = { scrollMode : 'default' }) { +MultiLineEditTextView.prototype.addText = function ( + text, + options = { scrollMode: 'default' } +) { this.insertRawText(text); - switch(options.scrollMode) { - case 'default' : - if(this.isEditMode() || this.autoScroll) { + switch (options.scrollMode) { + case 'default': + if (this.isEditMode() || this.autoScroll) { this.cursorEndOfDocument(); } else { this.cursorStartOfDocument(); } break; - case 'top' : - case 'start' : + case 'top': + case 'start': this.cursorStartOfDocument(); break; - case 'end' : - case 'bottom' : + case 'end': + case 'bottom': this.cursorEndOfDocument(); break; } }; -MultiLineEditTextView.prototype.getData = function(options = { forceLineTerms : false } ) { +MultiLineEditTextView.prototype.getData = function (options = { forceLineTerms: false }) { return this.getOutputText(0, this.textLines.length, '\r\n', options); }; -MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'mode' : +MultiLineEditTextView.prototype.setPropertyValue = function (propName, value) { + switch (propName) { + case 'mode': this.mode = value; - if('preview' === value && !this.specialKeyMap.next) { - this.specialKeyMap.next = [ 'tab' ]; + if ('preview' === value && !this.specialKeyMap.next) { + this.specialKeyMap.next = ['tab']; } break; - case 'autoScroll' : + case 'autoScroll': this.autoScroll = value; break; - case 'tabSwitchesView' : + case 'tabSwitchesView': this.tabSwitchesView = value; this.specialKeyMap.next = this.specialKeyMap.next || []; this.specialKeyMap.next.push('tab'); @@ -1139,33 +1176,39 @@ MultiLineEditTextView.prototype.setPropertyValue = function(propName, value) { }; const HANDLED_SPECIAL_KEYS = [ - 'up', 'down', 'left', 'right', - 'home', 'end', - 'page up', 'page down', + 'up', + 'down', + 'left', + 'right', + 'home', + 'end', + 'page up', + 'page down', 'line feed', 'insert', 'tab', - 'backspace', 'delete', + 'backspace', + 'delete', 'delete line', ]; -const PREVIEW_MODE_KEYS = [ - 'up', 'down', 'page up', 'page down' -]; +const PREVIEW_MODE_KEYS = ['up', 'down', 'page up', 'page down']; -MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { +MultiLineEditTextView.prototype.onKeyPress = function (ch, key) { const self = this; let handled; - if(key) { + if (key) { HANDLED_SPECIAL_KEYS.forEach(function aKey(specialKey) { - if(self.isKeyMapped(specialKey, key.name)) { - - if(self.isPreviewMode() && -1 === PREVIEW_MODE_KEYS.indexOf(specialKey)) { + if (self.isKeyMapped(specialKey, key.name)) { + if ( + self.isPreviewMode() && + -1 === PREVIEW_MODE_KEYS.indexOf(specialKey) + ) { return; } - if('tab' !== key.name || !self.tabSwitchesView) { + if ('tab' !== key.name || !self.tabSwitchesView) { self[_.camelCase('keyPress ' + specialKey)](); handled = true; } @@ -1173,42 +1216,42 @@ MultiLineEditTextView.prototype.onKeyPress = function(ch, key) { }); } - if(self.isEditMode() && ch && strUtil.isPrintable(ch)) { + if (self.isEditMode() && ch && strUtil.isPrintable(ch)) { this.keyPressCharacter(ch); } - if(!handled) { + if (!handled) { MultiLineEditTextView.super_.prototype.onKeyPress.call(this, ch, key); } }; -MultiLineEditTextView.prototype.scrollUp = function() { +MultiLineEditTextView.prototype.scrollUp = function () { this.scrollDocumentUp(); }; -MultiLineEditTextView.prototype.scrollDown = function() { +MultiLineEditTextView.prototype.scrollDown = function () { this.scrollDocumentDown(); }; -MultiLineEditTextView.prototype.deleteLine = function(line) { +MultiLineEditTextView.prototype.deleteLine = function (line) { this.textLines.splice(line, 1); }; -MultiLineEditTextView.prototype.getLineCount = function() { +MultiLineEditTextView.prototype.getLineCount = function () { return this.textLines.length; }; -MultiLineEditTextView.prototype.getTextEditMode = function() { +MultiLineEditTextView.prototype.getTextEditMode = function () { return this.overtypeMode ? 'overtype' : 'insert'; }; -MultiLineEditTextView.prototype.getEditPosition = function() { +MultiLineEditTextView.prototype.getEditPosition = function () { var currentIndex = this.getTextLinesIndex() + 1; return { - row : this.getTextLinesIndex(this.cursorPos.row), - col : this.cursorPos.col, - percent : Math.floor(((currentIndex / this.textLines.length) * 100)), - below : this.getRemainingLinesBelowRow(), + row: this.getTextLinesIndex(this.cursorPos.row), + col: this.cursorPos.col, + percent: Math.floor((currentIndex / this.textLines.length) * 100), + below: this.getRemainingLinesBelowRow(), }; }; diff --git a/core/my_messages.js b/core/my_messages.js index ed1d3fe1..50bba7ae 100644 --- a/core/my_messages.js +++ b/core/my_messages.js @@ -2,17 +2,15 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const Message = require('./message.js'); -const UserProps = require('./user_property.js'); -const { - filterMessageListByReadACS -} = require('./message_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const Message = require('./message.js'); +const UserProps = require('./user_property.js'); +const { filterMessageListByReadACS } = require('./message_area.js'); exports.moduleInfo = { - name : 'My Messages', - desc : 'Finds messages addressed to the current user.', - author : 'NuSkooler', + name: 'My Messages', + desc: 'Finds messages addressed to the current user.', + author: 'NuSkooler', }; exports.getModule = class MyMessagesModule extends MenuModule { @@ -22,15 +20,21 @@ exports.getModule = class MyMessagesModule extends MenuModule { initSequence() { const filter = { - toUserName : [ this.client.user.username, this.client.user.getProperty(UserProps.RealName) ], - sort : 'modTimestamp', - resultType : 'messageList', - limit : 1024 * 16, // we want some sort of limit... + toUserName: [ + this.client.user.username, + this.client.user.getProperty(UserProps.RealName), + ], + sort: 'modTimestamp', + resultType: 'messageList', + limit: 1024 * 16, // we want some sort of limit... }; Message.findMessages(filter, (err, messageList) => { - if(err) { - this.client.log.warn( { error : err.message }, 'Error finding messages addressed to current user'); + if (err) { + this.client.log.warn( + { error: err.message }, + 'Error finding messages addressed to current user' + ); return this.prevMenu(); } @@ -42,19 +46,19 @@ exports.getModule = class MyMessagesModule extends MenuModule { } finishedLoading() { - if(!this.messageList || 0 === this.messageList.length) { + if (!this.messageList || 0 === this.messageList.length) { return this.gotoMenu( this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', - { menuFlags : [ 'popParent' ] } + { menuFlags: ['popParent'] } ); } const menuOpts = { - extraArgs : { - messageList : this.messageList, - noUpdateLastReadId : true + extraArgs: { + messageList: this.messageList, + noUpdateLastReadId: true, }, - menuFlags : [ 'popParent' ], + menuFlags: ['popParent'], }; return this.gotoMenu( diff --git a/core/new_scan.js b/core/new_scan.js index ac741ab3..480f8738 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -2,24 +2,24 @@ 'use strict'; // ENiGMA½ -const msgArea = require('./message_area.js'); -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const stringFormat = require('./string_format.js'); -const FileEntry = require('./file_entry.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const Errors = require('./enig_error.js').Errors; -const { getAvailableFileAreaTags } = require('./file_base_area.js'); -const { valueAsArray } = require('./misc_util.js'); +const msgArea = require('./message_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const stringFormat = require('./string_format.js'); +const FileEntry = require('./file_entry.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const Errors = require('./enig_error.js').Errors; +const { getAvailableFileAreaTags } = require('./file_base_area.js'); +const { valueAsArray } = require('./misc_util.js'); // deps -const _ = require('lodash'); -const async = require('async'); +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', }; /* @@ -31,34 +31,33 @@ 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); + 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: // :TODO: Use newer custom info format - TL10+ - const config = this.menuConfig.config; - this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; - this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; - this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; - this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; + const config = this.menuConfig.config; + this.scanStartFmt = config.scanStartFmt || 'Scanning {confName} - {areaName}...'; + this.scanFinishNoneFmt = config.scanFinishNoneFmt || 'Nothing new'; + this.scanFinishNewFmt = config.scanFinishNewFmt || '{count} entries found'; + this.scanCompleteMsg = config.scanCompleteMsg || 'Finished newscan'; } updateScanStatus(statusText) { @@ -67,15 +66,18 @@ exports.getModule = class NewScanModule extends MenuModule { newScanMessageConference(cb) { // lazy init - if(!this.sortedMessageConfs) { - const getAvailOpts = { includeSystemInternal : true }; // find new private messages, bulletins, etc. + 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 @@ -83,10 +85,13 @@ exports.getModule = class NewScanModule extends MenuModule { // other conferences & areas // this.sortedMessageConfs.sort((a, b) => { - if('system_internal' === a.confTag) { + if ('system_internal' === a.confTag) { return -1; } else { - return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } ); + return a.conf.name.localeCompare(b.conf.name, { + sensitivity: false, + numeric: true, + }); } }); @@ -97,11 +102,11 @@ exports.getModule = class NewScanModule extends MenuModule { const currentConf = this.sortedMessageConfs[this.currentScanAux.conf]; this.newScanMessageArea(currentConf, () => { - if(this.sortedMessageConfs.length > this.currentScanAux.conf + 1) { + 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); @@ -111,10 +116,14 @@ exports.getModule = class NewScanModule extends MenuModule { newScanMessageArea(conf, cb) { // :TODO: it would be nice to cache this - must be done by conf! - const omitMessageAreaTags = valueAsArray(_.get(this, 'menuConfig.config.omitMessageAreaTags', [])); - const sortedAreas = msgArea.getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).filter(area => { - return !omitMessageAreaTags.includes(area.areaTag); - }); + const omitMessageAreaTags = valueAsArray( + _.get(this, 'menuConfig.config.omitMessageAreaTags', []) + ); + const sortedAreas = msgArea + .getSortedAvailMessageAreasByConfTag(conf.confTag, { client: this.client }) + .filter(area => { + return !omitMessageAreaTags.includes(area.areaTag); + }); const currentArea = sortedAreas[this.currentScanAux.area]; // @@ -126,43 +135,50 @@ exports.getModule = class NewScanModule extends MenuModule { [ function checkAndUpdateIndex(callback) { // Advance to next area if possible - if(sortedAreas.length >= self.currentScanAux.area + 1) { + if (sortedAreas.length >= self.currentScanAux.area + 1) { self.currentScanAux.area += 1; return callback(null); } else { self.updateScanStatus(self.scanCompleteMsg); - return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan + return callback(Errors.DoesNotExist('No more areas')); // this will stop our scan } }, function updateStatusScanStarted(callback) { - self.updateScanStatus(stringFormat(self.scanStartFmt, { - confName : conf.conf.name, - confDesc : conf.conf.desc, - areaName : currentArea.area.name, - areaDesc : currentArea.area.desc - })); + 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) => { + 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 + if (newMessageCount <= 0) { + return self.newScanMessageArea(conf, cb); // next area, if any } const nextModuleOpts = { extraArgs: { - messageAreaTag : currentArea.areaTag, - } + messageAreaTag: currentArea.areaTag, + }, }; - return self.gotoMenu(self.menuConfig.config.newScanMessageList || 'newScanMessageList', nextModuleOpts); - } + return self.gotoMenu( + self.menuConfig.config.newScanMessageList || 'newScanMessageList', + nextModuleOpts + ); + }, ], err => { return cb(err); @@ -172,78 +188,90 @@ exports.getModule = class NewScanModule extends MenuModule { newScanFileBase(cb) { // :TODO: add in steps - const omitFileAreaTags = valueAsArray(_.get(this, 'menuConfig.config.omitFileAreaTags', [])); + const omitFileAreaTags = valueAsArray( + _.get(this, 'menuConfig.config.omitFileAreaTags', []) + ); const filterCriteria = { - newerThanFileId : FileBaseFilters.getFileBaseLastViewedFileIdByUser(this.client.user), - areaTag : getAvailableFileAreaTags(this.client).filter(ft => !omitFileAreaTags.includes(ft)), - order : 'ascending', // oldest first + newerThanFileId: FileBaseFilters.getFileBaseLastViewedFileIdByUser( + this.client.user + ), + areaTag: getAvailableFileAreaTags(this.client).filter( + ft => !omitFileAreaTags.includes(ft) + ), + order: 'ascending', // oldest first }; - 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] ); - - const menuOpts = { - extraArgs : { - fileList : fileIds, - }, - }; - - return this.gotoMenu(this.menuConfig.config.newScanFileBaseList || 'newScanFileBaseList', menuOpts); + 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] + ); + + const menuOpts = { + extraArgs: { + fileList: fileIds, + }, + }; + + return this.gotoMenu( + this.menuConfig.config.newScanFileBaseList || 'newScanFileBaseList', + menuOpts + ); + }); } getSaveState() { return { - currentStep : this.currentStep, - currentScanAux : this.currentScanAux, + currentStep: this.currentStep, + currentScanAux: this.currentScanAux, }; } restoreSavedState(savedState) { - this.currentStep = savedState.currentStep; + this.currentStep = savedState.currentStep; this.currentScanAux = savedState.currentScanAux; } performScanCurrentStep(cb) { - switch(this.currentStep) { - case Steps.MessageConfs : - this.newScanMessageConference( () => { + switch (this.currentStep) { + case Steps.MessageConfs: + this.newScanMessageConference(() => { this.currentStep = Steps.FileBase; return this.performScanCurrentStep(cb); }); break; - case Steps.FileBase : - this.newScanFileBase( () => { + 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) { + if (this.newScanFullExit) { // user has canceled the entire scan @ message list view return cb(null); } super.mciReady(mciData, err => { - if(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. @@ -251,20 +279,23 @@ exports.getModule = class NewScanModule extends MenuModule { [ function loadFromConfig(callback) { const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, + callingMenu: self, + mciMap: mciData.menu, + noInput: true, }; vc.loadFromMenuConfig(loadOpts, callback); }, function performCurrentStepScan(callback) { return self.performScanCurrentStep(callback); - } + }, ], err => { - if(err) { - self.client.log.error( { error : err.toString() }, 'Error during new scan'); + if (err) { + self.client.log.error( + { error: err.toString() }, + 'Error during new scan' + ); } return cb(err); } diff --git a/core/node_msg.js b/core/node_msg.js index bb64757c..58585382 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -2,70 +2,75 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); +const { MenuModule } = require('./menu_module.js'); const { getActiveConnectionList, getConnectionByNodeId, -} = require('./client_connections.js'); -const UserInterruptQueue = require('./user_interrupt_queue.js'); -const { getThemeArt } = require('./theme.js'); -const { pipeToAnsi } = require('./color_codes.js'); -const stringFormat = require('./string_format.js'); -const { renderStringLength } = require('./string_util.js'); -const Events = require('./events.js'); +} = require('./client_connections.js'); +const UserInterruptQueue = require('./user_interrupt_queue.js'); +const { getThemeArt } = require('./theme.js'); +const { pipeToAnsi } = require('./color_codes.js'); +const stringFormat = require('./string_format.js'); +const { renderStringLength } = require('./string_util.js'); +const Events = require('./events.js'); // deps -const series = require('async/series'); -const _ = require('lodash'); -const async = require('async'); -const moment = require('moment'); +const series = require('async/series'); +const _ = require('lodash'); +const async = require('async'); +const moment = require('moment'); exports.moduleInfo = { - name : 'Node Message', - desc : 'Multi-node messaging', - author : 'NuSkooler', + name: 'Node Message', + desc: 'Multi-node messaging', + author: 'NuSkooler', }; const FormIds = { - sendMessage : 0, + sendMessage: 0, }; const MciViewIds = { - sendMessage : { - nodeSelect : 1, - message : 2, - preview : 3, + sendMessage: { + nodeSelect: 1, + message: 2, + preview: 3, - customRangeStart : 10, - } + customRangeStart: 10, + }, }; exports.getModule = class NodeMessageModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { + extraArgs: options.extraArgs, + }); this.menuMethods = { - sendMessage : (formData, extraArgs, cb) => { - const nodeId = this.nodeList[formData.value.node].node; // index from from -> node! - const message = _.get(formData.value, 'message', '').trim(); + sendMessage: (formData, extraArgs, cb) => { + const nodeId = this.nodeList[formData.value.node].node; // index from from -> node! + const message = _.get(formData.value, 'message', '').trim(); - if(0 === renderStringLength(message)) { + if (0 === renderStringLength(message)) { return this.prevMenu(cb); } this.createInterruptItem(message, (err, interruptItem) => { - if(-1 === nodeId) { + if (-1 === nodeId) { // ALL nodes - UserInterruptQueue.queue(interruptItem, { omit : this.client }); + UserInterruptQueue.queue(interruptItem, { omit: this.client }); } else { const conn = getConnectionByNodeId(nodeId); - if(conn) { - UserInterruptQueue.queue(interruptItem, { clients : conn } ); + if (conn) { + UserInterruptQueue.queue(interruptItem, { clients: conn }); } } - Events.emit(Events.getSystemEvents().UserSendNodeMsg, { user : this.client.user, global : -1 === nodeId } ); + Events.emit(Events.getSystemEvents().UserSendNodeMsg, { + user: this.client.user, + global: -1 === nodeId, + }); return this.prevMenu(cb); }); @@ -75,24 +80,34 @@ exports.getModule = class NodeMessageModule extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } series( [ - (callback) => { - return this.prepViewController('sendMessage', FormIds.sendMessage, mciData.menu, callback); - }, - (callback) => { - return this.validateMCIByViewIds( + callback => { + return this.prepViewController( 'sendMessage', - [ MciViewIds.sendMessage.nodeSelect, MciViewIds.sendMessage.message ], + FormIds.sendMessage, + mciData.menu, callback ); }, - (callback) => { - const nodeSelectView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.nodeSelect); + callback => { + return this.validateMCIByViewIds( + 'sendMessage', + [ + MciViewIds.sendMessage.nodeSelect, + MciViewIds.sendMessage.message, + ], + callback + ); + }, + callback => { + const nodeSelectView = this.viewControllers.sendMessage.getView( + MciViewIds.sendMessage.nodeSelect + ); this.prepareNodeList(); nodeSelectView.on('index update', idx => { @@ -104,23 +119,32 @@ exports.getModule = class NodeMessageModule extends MenuModule { this.nodeListSelectionIndexUpdate(0); return callback(null); }, - (callback) => { - const previewView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.preview); - if(!previewView) { - return callback(null); // preview is optional + callback => { + const previewView = this.viewControllers.sendMessage.getView( + MciViewIds.sendMessage.preview + ); + if (!previewView) { + return callback(null); // preview is optional } - const messageView = this.viewControllers.sendMessage.getView(MciViewIds.sendMessage.message); + const messageView = this.viewControllers.sendMessage.getView( + MciViewIds.sendMessage.message + ); let timerId; - messageView.on('key press', () => { - clearTimeout(timerId); - const focused = this.viewControllers.sendMessage.getFocusedView(); - if(focused === messageView) { - previewView.setText(messageView.getData()); - focused.setFocus(true); - } - }, 500); - } + messageView.on( + 'key press', + () => { + clearTimeout(timerId); + const focused = + this.viewControllers.sendMessage.getFocusedView(); + if (focused === messageView) { + previewView.setText(messageView.getData()); + focused.setFocus(true); + } + }, + 500 + ); + }, ], err => { return cb(err); @@ -130,14 +154,16 @@ exports.getModule = class NodeMessageModule extends MenuModule { } createInterruptItem(message, cb) { - const dateTimeFormat = this.config.dateTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat(); + const dateTimeFormat = + this.config.dateTimeFormat || + this.client.currentTheme.helpers.getDateTimeFormat(); const textFormatObj = { - fromUserName : this.client.user.username, - fromRealName : this.client.user.properties.real_name, - fromNodeId : this.client.node, - message : message, - timestamp : moment().format(dateTimeFormat), + fromUserName: this.client.user.username, + fromRealName: this.client.user.properties.real_name, + fromNodeId: this.client.node, + message: message, + timestamp: moment().format(dateTimeFormat), }; const messageFormat = @@ -145,19 +171,19 @@ exports.getModule = class NodeMessageModule extends MenuModule { 'Message from {fromUserName} on node {fromNodeId}:\r\n{message}'; const item = { - text : stringFormat(messageFormat, textFormatObj), - pause : true, + text: stringFormat(messageFormat, textFormatObj), + pause: true, }; const getArt = (name, callback) => { const spec = _.get(this.config, `art.${name}`); - if(!spec) { + if (!spec) { return callback(null); } const getArtOpts = { - name : spec, - client : this.client, - random : false, + name: spec, + client: this.client, + random: false, }; getThemeArt(getArtOpts, (err, artInfo) => { // ignore errors @@ -167,7 +193,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { async.waterfall( [ - (callback) => { + callback => { getArt('header', headerArt => { return callback(null, headerArt); }); @@ -178,11 +204,13 @@ exports.getModule = class NodeMessageModule extends MenuModule { }); }, (headerArt, footerArt, callback) => { - if(headerArt || footerArt) { - item.contents = `${headerArt || ''}\r\n${pipeToAnsi(item.text)}\r\n${footerArt || ''}`; + if (headerArt || footerArt) { + item.contents = `${headerArt || ''}\r\n${pipeToAnsi( + item.text + )}\r\n${footerArt || ''}`; } return callback(null); - } + }, ], err => { return cb(err, item); @@ -192,29 +220,41 @@ exports.getModule = class NodeMessageModule extends MenuModule { prepareNodeList() { // standard node list with {text} field added for compliance - this.nodeList = [{ - text : '-ALL-', - // dummy fields: - node : -1, - authenticated : false, - userId : 0, - action : 'N/A', - userName : 'Everyone', - realName : 'All Users', - location : 'N/A', - affils : 'N/A', - timeOn : 'N/A', - }].concat(getActiveConnectionList(true) - .map(node => Object.assign(node, { text : -1 == node.node ? '-ALL-' : node.node.toString() } )) - ).filter(node => node.node !== this.client.node); // remove our client's node - this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node + this.nodeList = [ + { + text: '-ALL-', + // dummy fields: + node: -1, + authenticated: false, + userId: 0, + action: 'N/A', + userName: 'Everyone', + realName: 'All Users', + location: 'N/A', + affils: 'N/A', + timeOn: 'N/A', + }, + ] + .concat( + getActiveConnectionList(true).map(node => + Object.assign(node, { + text: -1 == node.node ? '-ALL-' : node.node.toString(), + }) + ) + ) + .filter(node => node.node !== this.client.node); // remove our client's node + this.nodeList.sort((a, b) => a.node - b.node); // sort by node } nodeListSelectionIndexUpdate(idx) { const node = this.nodeList[idx]; - if(!node) { + if (!node) { return; } - this.updateCustomViewTextsWithFilter('sendMessage', MciViewIds.sendMessage.customRangeStart, node); + this.updateCustomViewTextsWithFilter( + 'sendMessage', + MciViewIds.sendMessage.customRangeStart, + node + ); } }; diff --git a/core/nua.js b/core/nua.js index 2cb4c26b..72cd6647 100644 --- a/core/nua.js +++ b/core/nua.js @@ -2,34 +2,31 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const User = require('./user.js'); -const theme = require('./theme.js'); -const login = require('./system_menu_method.js').login; -const Config = require('./config.js').get; -const messageArea = require('./message_area.js'); -const { - getISOTimestampString -} = require('./database.js'); -const UserProps = require('./user_property.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const User = require('./user.js'); +const theme = require('./theme.js'); +const login = require('./system_menu_method.js').login; +const Config = require('./config.js').get; +const messageArea = require('./message_area.js'); +const { getISOTimestampString } = require('./database.js'); +const UserProps = require('./user_property.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); 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); @@ -39,22 +36,30 @@ exports.getModule = class NewUserAppModule extends MenuModule { // // 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')); + 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) { + viewValidationListener: function (err, cb) { const errMsgView = self.viewControllers.menu.getView(MciViewIds.errMsg); let newFocusId; - if(err) { + if (err) { errMsgView.setText(err.message); err.view.clearText(); - if(err.view.getId() === MciViewIds.confirm) { + if (err.view.getId() === MciViewIds.confirm) { newFocusId = MciViewIds.password; - self.viewControllers.menu.getView(MciViewIds.password).clearText(); + self.viewControllers.menu + .getView(MciViewIds.password) + .clearText(); } } else { errMsgView.clearText(); @@ -63,11 +68,10 @@ exports.getModule = class NewUserAppModule extends MenuModule { return cb(newFocusId); }, - // // Submit handlers // - submitApplication : function(formData, extraArgs, cb) { + submitApplication: function (formData, extraArgs, cb) { const newUser = new User(); const config = Config(); @@ -76,35 +80,44 @@ exports.getModule = class NewUserAppModule extends MenuModule { // // We have to disable ACS checks for initial default areas as the user is not yet ready // - let confTag = messageArea.getDefaultMessageConferenceTag(self.client, true); // true=disableAcsCheck - let areaTag = messageArea.getDefaultMessageAreaTagByConfTag(self.client, confTag, true); // true=disableAcsCheck + let confTag = messageArea.getDefaultMessageConferenceTag( + self.client, + true + ); // true=disableAcsCheck + let areaTag = messageArea.getDefaultMessageAreaTagByConfTag( + self.client, + confTag, + true + ); // true=disableAcsCheck // can't store undefined! confTag = confTag || ''; areaTag = areaTag || ''; newUser.properties = { - [ UserProps.RealName ] : formData.value.realName, - [ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate), - [ UserProps.Sex ] : formData.value.sex, - [ UserProps.Location ] : formData.value.location, - [ UserProps.Affiliations ] : formData.value.affils, - [ UserProps.EmailAddress ] : formData.value.email, - [ UserProps.WebAddress ] : formData.value.web, - [ UserProps.AccountCreated ] : getISOTimestampString(), + [UserProps.RealName]: formData.value.realName, + [UserProps.Birthdate]: getISOTimestampString( + formData.value.birthdate + ), + [UserProps.Sex]: formData.value.sex, + [UserProps.Location]: formData.value.location, + [UserProps.Affiliations]: formData.value.affils, + [UserProps.EmailAddress]: formData.value.email, + [UserProps.WebAddress]: formData.value.web, + [UserProps.AccountCreated]: getISOTimestampString(), - [ UserProps.MessageConfTag ] : confTag, - [ UserProps.MessageAreaTag ] : areaTag, + [UserProps.MessageConfTag]: confTag, + [UserProps.MessageAreaTag]: areaTag, - [ UserProps.TermHeight ] : self.client.term.termHeight, - [ UserProps.TermWidth ] : self.client.term.termWidth, + [UserProps.TermHeight]: self.client.term.termHeight, + [UserProps.TermWidth]: self.client.term.termWidth, // :TODO: Other defaults // :TODO: should probably have a place to create defaults/etc. }; const defaultTheme = _.get(config, 'theme.default'); - if('*' === defaultTheme) { + if ('*' === defaultTheme) { newUser.properties[UserProps.ThemeId] = theme.getRandomTheme(); } else { newUser.properties[UserProps.ThemeId] = defaultTheme; @@ -112,32 +125,41 @@ exports.getModule = class NewUserAppModule extends MenuModule { // :TODO: User.create() should validate email uniqueness! const createUserInfo = { - password : formData.value.password, - sessionId : self.client.session.uniqueId, // used for events/etc. + password: formData.value.password, + sessionId: self.client.session.uniqueId, // used for events/etc. }; newUser.create(createUserInfo, err => { - if(err) { - self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); + if (err) { + self.client.log.info( + { error: err, username: formData.value.username }, + 'New user creation failed' + ); self.gotoMenu(extraArgs.error, err => { - if(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.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()) { + if (newUser.isSysOp()) { config.general.sysOp = { - username : formData.value.username, - properties : newUser.properties, + username: formData.value.username, + properties: newUser.properties, }; } - if(User.AccountStatus.inactive === self.client.user.properties[UserProps.AccountStatus]) { + if ( + User.AccountStatus.inactive === + self.client.user.properties[UserProps.AccountStatus] + ) { return self.gotoMenu(extraArgs.inactive, cb); } else { // diff --git a/core/onelinerz.js b/core/onelinerz.js index 679f5fec..5e7495be 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -2,18 +2,15 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; +const MenuModule = require('./menu_module.js').MenuModule; -const { - getModDatabasePath, - getTransactionDatabase -} = require('./database.js'); +const { getModDatabasePath, getTransactionDatabase } = require('./database.js'); // deps -const sqlite3 = require('sqlite3'); -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); +const sqlite3 = require('sqlite3'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); /* Module :TODO: @@ -21,27 +18,27 @@ 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 = { - view : { - entries : 1, - addPrompt : 2, + view: { + entries: 1, + addPrompt: 2, + }, + add: { + newEntry: 1, + entryPreview: 2, + addPrompt: 3, }, - add : { - newEntry : 1, - entryPreview : 2, - addPrompt : 3, - } }; const FormIds = { - view : 0, - add : 1, + view: 0, + add: 1, }; exports.getModule = class OnelinerzModule extends MenuModule { @@ -51,33 +48,38 @@ exports.getModule = class OnelinerzModule extends MenuModule { const self = this; this.menuMethods = { - viewAddScreen : function(formData, extraArgs, cb) { + 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'); + if (err) { + self.client.log.warn( + { error: err.message }, + 'Failed saving oneliner' + ); } self.clearAddForm(); - return self.displayViewScreen(true, cb); // true=cls + return self.displayViewScreen(true, cb); // true=cls }); - } else { // empty message - treat as if cancel was hit - return self.displayViewScreen(true, cb); // true=cls + return self.displayViewScreen(true, cb); // true=cls } }, - cancelAdd : function(formData, extraArgs, cb) { + cancelAdd: function (formData, extraArgs, cb) { self.clearAddForm(); - return self.displayViewScreen(true, cb); // true=cls - } + return self.displayViewScreen(true, cb); // true=cls + }, }; } @@ -90,10 +92,10 @@ exports.getModule = class OnelinerzModule extends MenuModule { }, function display(callback) { return self.displayViewScreen(false, callback); - } + }, ], err => { - if(err) { + if (err) { // :TODO: Handle me -- initSequence() should really take a completion callback } self.finishedLoading(); @@ -107,7 +109,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { async.waterfall( [ function prepArtAndViewController(callback) { - if(self.viewControllers.add) { + if (self.viewControllers.add) { self.viewControllers.add.setFocus(false); } @@ -116,19 +118,23 @@ exports.getModule = class OnelinerzModule extends MenuModule { FormIds.view, { clearScreen, - trailingLF : false + trailingLF: false, }, (err, artInfo, wasCreated) => { - if(!err && !wasCreated) { + if (!err && !wasCreated) { self.viewControllers.view.setFocus(true); - self.viewControllers.view.getView(MciViewIds.view.addPrompt).redraw(); + self.viewControllers.view + .getView(MciViewIds.view.addPrompt) + .redraw(); } return callback(err); } ); }, function fetchEntries(callback) { - const entriesView = self.viewControllers.view.getView(MciViewIds.view.entries); + const entriesView = self.viewControllers.view.getView( + MciViewIds.view.entries + ); const limit = entriesView.dimens.height; let entries = []; @@ -142,8 +148,8 @@ exports.getModule = class OnelinerzModule extends MenuModule { ) ORDER BY timestamp ASC;`, (err, row) => { - if(!err) { - row.timestamp = moment(row.timestamp); // convert -> moment + if (!err) { + row.timestamp = moment(row.timestamp); // convert -> moment entries.push(row); } }, @@ -155,30 +161,34 @@ exports.getModule = class OnelinerzModule extends MenuModule { function populateEntries(entriesView, entries, callback) { const tsFormat = self.menuConfig.config.dateTimeFormat || - self.menuConfig.config.timestampFormat || // deprecated + self.menuConfig.config.timestampFormat || // deprecated self.client.currentTheme.helpers.getDateFormat('short'); - entriesView.setItems(entries.map( e => { - return { - text : e.oneliner, // standard - userId : e.user_id, - userName : e.user_name, - oneliner : e.oneliner, - ts : e.timestamp.format(tsFormat), - }; - })); + entriesView.setItems( + entries.map(e => { + return { + text: e.oneliner, // standard + userId: e.user_id, + userName: e.user_name, + oneliner: e.oneliner, + ts: e.timestamp.format(tsFormat), + }; + }) + ); entriesView.redraw(); return callback(null); }, function finalPrep(callback) { - const promptView = self.viewControllers.view.getView(MciViewIds.view.addPrompt); - promptView.setFocusItemIndex(1); // default to NO + const promptView = self.viewControllers.view.getView( + MciViewIds.view.addPrompt + ); + promptView.setFocusItemIndex(1); // default to NO return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -197,29 +207,35 @@ exports.getModule = class OnelinerzModule extends MenuModule { 'add', FormIds.add, { - clearScreen : true, - trailingLF : false + clearScreen: true, + trailingLF: false, }, (err, artInfo, wasCreated) => { - if(!wasCreated) { + if (!wasCreated) { self.viewControllers.add.setFocus(true); self.viewControllers.add.redrawAll(); - self.viewControllers.add.switchFocus(MciViewIds.add.newEntry); + self.viewControllers.add.switchFocus( + MciViewIds.add.newEntry + ); } return callback(err); } ); }, function initPreviewUpdates(callback) { - const previewView = self.viewControllers.add.getView(MciViewIds.add.entryPreview); - const entryView = self.viewControllers.add.getView(MciViewIds.add.newEntry); - if(previewView) { + const previewView = self.viewControllers.add.getView( + MciViewIds.add.entryPreview + ); + const entryView = self.viewControllers.add.getView( + MciViewIds.add.newEntry + ); + if (previewView) { let timerId; entryView.on('key press', () => { clearTimeout(timerId); - timerId = setTimeout( () => { + timerId = setTimeout(() => { const focused = self.viewControllers.add.getFocusedView(); - if(focused === entryView) { + if (focused === entryView) { previewView.setText(entryView.getData()); focused.setFocus(true); } @@ -227,10 +243,10 @@ exports.getModule = class OnelinerzModule extends MenuModule { }); } return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -249,12 +265,14 @@ exports.getModule = class OnelinerzModule extends MenuModule { [ function openDatabase(callback) { const dbSuffix = self.menuConfig.config.dbSuffix; - self.db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(exports.moduleInfo, dbSuffix), - err => { - return callback(err); - } - )); + self.db = getTransactionDatabase( + new sqlite3.Database( + getModDatabasePath(exports.moduleInfo, dbSuffix), + err => { + return callback(err); + } + ) + ); }, function createTables(callback) { self.db.run( @@ -264,12 +282,12 @@ exports.getModule = class OnelinerzModule extends MenuModule { user_name VARCHAR NOT NULL, oneliner VARCHAR NOT NULL, timestamp DATETIME NOT NULL - );` - , + );`, err => { return callback(err); - }); - } + } + ); + }, ], err => { return cb(err); @@ -278,8 +296,8 @@ exports.getModule = class OnelinerzModule extends MenuModule { } storeNewOneliner(oneliner, cb) { - const self = this; - const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + const self = this; + const ts = moment().format('YYYY-MM-DDTHH:mm:ss.SSSZ'); async.series( [ @@ -287,7 +305,12 @@ exports.getModule = class OnelinerzModule extends MenuModule { self.db.run( `INSERT INTO onelinerz (user_id, user_name, oneliner, timestamp) VALUES (?, ?, ?, ?);`, - [ self.client.user.userId, self.client.user.username, oneliner, ts ], + [ + self.client.user.userId, + self.client.user.username, + oneliner, + ts, + ], callback ); }, @@ -304,7 +327,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { );`, callback ); - } + }, ], err => { return cb(err); diff --git a/core/oputil/oputil_common.js b/core/oputil/oputil_common.js index 72b6f14b..bbc6c500 100644 --- a/core/oputil/oputil_common.js +++ b/core/oputil/oputil_common.js @@ -2,59 +2,59 @@ /* eslint-disable no-console */ 'use strict'; -const config = require('../../core/config.js'); -const db = require('../../core/database.js'); +const config = require('../../core/config.js'); +const db = require('../../core/database.js'); -const _ = require('lodash'); -const async = require('async'); -const inq = require('inquirer'); -const fs = require('fs'); -const hjson = require('hjson'); +const _ = require('lodash'); +const async = require('async'); +const inq = require('inquirer'); +const fs = require('fs'); +const hjson = require('hjson'); -const packageJson = require('../../package.json'); +const packageJson = require('../../package.json'); -exports.printUsageAndSetExitCode = printUsageAndSetExitCode; -exports.getDefaultConfigPath = getDefaultConfigPath; -exports.getConfigPath = getConfigPath; -exports.initConfigAndDatabases = initConfigAndDatabases; -exports.getAreaAndStorage = getAreaAndStorage; -exports.looksLikePattern = looksLikePattern; -exports.getAnswers = getAnswers; -exports.writeConfig = writeConfig; +exports.printUsageAndSetExitCode = printUsageAndSetExitCode; +exports.getDefaultConfigPath = getDefaultConfigPath; +exports.getConfigPath = getConfigPath; +exports.initConfigAndDatabases = initConfigAndDatabases; +exports.getAreaAndStorage = getAreaAndStorage; +exports.looksLikePattern = looksLikePattern; +exports.getAnswers = getAnswers; +exports.writeConfig = writeConfig; -const HJSONStringifyCommonOpts = exports.HJSONStringifyCommonOpts = { - emitRootBraces : true, - bracesSameLine : true, - space : 4, - keepWsc : true, - quotes : 'min', - eol : '\n', -}; - -const exitCodes = exports.ExitCodes = { - SUCCESS : 0, - 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', - } +const HJSONStringifyCommonOpts = (exports.HJSONStringifyCommonOpts = { + emitRootBraces: true, + bracesSameLine: true, + space: 4, + keepWsc: true, + quotes: 'min', + eol: '\n', }); +const exitCodes = (exports.ExitCodes = { + 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', + }, +})); + function printUsageAndSetExitCode(errMsg, exitCode) { - if(_.isUndefined(exitCode)) { + if (_.isUndefined(exitCode)) { exitCode = exitCodes.ERROR; } process.exitCode = exitCode; - if(errMsg) { + if (errMsg) { console.error(errMsg); } } @@ -71,7 +71,7 @@ function getConfigPath() { function initConfig(cb) { const configPath = getConfigPath(); - config.Config.create(configPath, { keepWsc : true, hotReload : false }, cb); + config.Config.create(configPath, { keepWsc: true, hotReload: false }, cb); } function initConfigAndDatabases(cb) { @@ -85,9 +85,9 @@ function initConfigAndDatabases(cb) { }, function initArchiveUtil(callback) { // ensure we init ArchiveUtil without events - require('../../core/archive_util').getInstance(false); // false=hotReload + require('../../core/archive_util').getInstance(false); // false=hotReload return callback(null); - } + }, ], err => { return cb(err); @@ -99,10 +99,10 @@ function getAreaAndStorage(tags) { return tags.map(tag => { const parts = tag.toString().split('@'); const entry = { - areaTag : parts[0], + areaTag: parts[0], }; - entry.pattern = entry.areaTag; // handy - if(parts[1]) { + entry.pattern = entry.areaTag; // handy + if (parts[1]) { entry.storageTag = parts[1]; } return entry; @@ -111,7 +111,7 @@ function getAreaAndStorage(tags) { function looksLikePattern(tag) { // globs can start with @ - if(tag.indexOf('@') > 0) { + if (tag.indexOf('@') > 0) { return false; } @@ -119,20 +119,21 @@ function looksLikePattern(tag) { } function getAnswers(questions, cb) { - inq.prompt(questions).then( answers => { + inq.prompt(questions).then(answers => { return cb(answers); }); } function writeConfig(config, path) { - config = hjson.stringify(config, HJSONStringifyCommonOpts) - .replace(/%ENIG_VERSION%/g, packageJson.version) - .replace(/%HJSON_VERSION%/g, hjson.version); + config = hjson + .stringify(config, HJSONStringifyCommonOpts) + .replace(/%ENIG_VERSION%/g, packageJson.version) + .replace(/%HJSON_VERSION%/g, hjson.version); try { fs.writeFileSync(path, config, 'utf8'); return true; - } catch(e) { + } catch (e) { return false; } -} \ No newline at end of file +} diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index bcd6529f..d979f9b7 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -3,7 +3,7 @@ 'use strict'; // ENiGMA½ -const resolvePath = require('../../core/misc_util.js').resolvePath; +const resolvePath = require('../../core/misc_util.js').resolvePath; const { printUsageAndSetExitCode, getConfigPath, @@ -12,25 +12,28 @@ const { getAnswers, writeConfig, HJSONStringifyCommonOpts, -} = require('./oputil_common.js'); -const getHelpFor = require('./oputil_help.js').getHelpFor; +} = require('./oputil_common.js'); +const getHelpFor = require('./oputil_help.js').getHelpFor; // deps -const async = require('async'); -const inq = require('inquirer'); -const mkdirsSync = require('fs-extra').mkdirsSync; -const fs = require('graceful-fs'); -const hjson = require('hjson'); -const paths = require('path'); -const _ = require('lodash'); -const sanatizeFilename = require('sanitize-filename'); +const async = require('async'); +const inq = require('inquirer'); +const mkdirsSync = require('fs-extra').mkdirsSync; +const fs = require('graceful-fs'); +const hjson = require('hjson'); +const paths = require('path'); +const _ = require('lodash'); +const sanatizeFilename = require('sanitize-filename'); -exports.handleConfigCommand = handleConfigCommand; +exports.handleConfigCommand = handleConfigCommand; const ConfigIncludeKeys = [ 'theme', - 'users.preAuthIdleLogoutSeconds', 'users.idleLogoutSeconds', - 'users.newUserNames', 'users.failedLogin', 'users.unlockAtEmailPwReset', + 'users.preAuthIdleLogoutSeconds', + 'users.idleLogoutSeconds', + 'users.newUserNames', + 'users.failedLogin', + 'users.unlockAtEmailPwReset', 'paths.logs', 'loginServers', 'contentServers', @@ -39,71 +42,71 @@ const ConfigIncludeKeys = [ ]; const QUESTIONS = { - Intro : [ + Intro: [ { - name : 'createNewConfig', - message : 'Create a new configuration?', - type : 'confirm', - default : false, + name: 'createNewConfig', + message: 'Create a new configuration?', + type: 'confirm', + default: false, }, { - name : 'configPath', - message : 'Configuration path:', - default : getConfigPath(), - when : answers => answers.createNewConfig + name: 'configPath', + message: 'Configuration path:', + default: getConfigPath(), + when: answers => answers.createNewConfig, }, ], - OverwriteConfig : [ + OverwriteConfig: [ { - name : 'overwriteConfig', - message : 'Config file exists. Overwrite?', - type : 'confirm', - default : false, - } - ], - - Basic : [ - { - name : 'boardName', - message : 'BBS name:', - default : 'New ENiGMA½ BBS', + name: 'overwriteConfig', + message: 'Config file exists. Overwrite?', + type: 'confirm', + default: false, }, ], - Misc : [ + Basic: [ { - name : 'loggingLevel', - message : 'Logging level:', - type : 'list', - choices : [ 'Error', 'Warn', 'Info', 'Debug', 'Trace' ], - default : 2, - filter : s => s.toLowerCase(), + name: 'boardName', + message: 'BBS name:', + default: 'New ENiGMA½ BBS', }, ], - MessageConfAndArea : [ + Misc: [ { - name : 'msgConfName', - message : 'First message conference:', - default : 'Local', + name: 'loggingLevel', + message: 'Logging level:', + type: 'list', + choices: ['Error', 'Warn', 'Info', 'Debug', 'Trace'], + default: 2, + filter: s => s.toLowerCase(), + }, + ], + + MessageConfAndArea: [ + { + name: 'msgConfName', + message: 'First message conference:', + default: 'Local', }, { - name : 'msgConfDesc', - message : 'Conference description:', - default : 'Local Areas', + name: 'msgConfDesc', + message: 'Conference description:', + default: 'Local Areas', }, { - name : 'msgAreaName', - message : 'First area in message conference:', - default : 'General', + name: 'msgAreaName', + message: 'First area in message conference:', + default: 'General', }, { - name : 'msgAreaDesc', - message : 'Area description:', - default : 'General chit-chat', - } - ] + name: 'msgAreaDesc', + message: 'Area description:', + default: 'General chit-chat', + }, + ], }; function makeMsgConfAreaName(s) { @@ -111,7 +114,6 @@ function makeMsgConfAreaName(s) { } function askNewConfigQuestions(cb) { - const ui = new inq.ui.BottomBar(); let configPath; @@ -121,7 +123,7 @@ function askNewConfigQuestions(cb) { [ function intro(callback) { getAnswers(QUESTIONS.Intro, answers => { - if(!answers.createNewConfig) { + if (!answers.createNewConfig) { return callback('exit'); } @@ -135,21 +137,21 @@ function askNewConfigQuestions(cb) { // 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) { + if (err) { + if ('EACCES' === err.code) { ui.log.write(`${configPath} cannot be written to`); callback('exit'); - } else if('ENOENT' === err.code) { + } else if ('ENOENT' === err.code) { callback(null, false); } } else { - callback(null, true); // exists + writable + callback(null, true); // exists + writable } }); }); }, function promptOverwrite(needPrompt, callback) { - if(needPrompt) { + if (needPrompt) { getAnswers(QUESTIONS.OverwriteConfig, answers => { return callback(answers.overwriteConfig ? null : 'exit'); }); @@ -162,7 +164,12 @@ function askNewConfigQuestions(cb) { const defaultConfig = require('../../core/config_default')(); // start by plopping in values we want directly from config.js - const template = hjson.rt.parse(fs.readFileSync(paths.join(__dirname, '../../misc/config_template.in.hjson'), 'utf8')); + const template = hjson.rt.parse( + fs.readFileSync( + paths.join(__dirname, '../../misc/config_template.in.hjson'), + 'utf8' + ) + ); const direct = {}; _.each(ConfigIncludeKeys, keyPath => { @@ -179,22 +186,22 @@ function askNewConfigQuestions(cb) { }, function msgConfAndArea(callback) { getAnswers(QUESTIONS.MessageConfAndArea, answers => { - const confName = makeMsgConfAreaName(answers.msgConfName); - const areaName = makeMsgConfAreaName(answers.msgAreaName); + const confName = makeMsgConfAreaName(answers.msgConfName); + const areaName = makeMsgConfAreaName(answers.msgAreaName); config.messageConferences[confName] = { - name : answers.msgConfName, - desc : answers.msgConfDesc, - sort : 1, - default : true, + name: answers.msgConfName, + desc: answers.msgConfDesc, + sort: 1, + default: true, }; config.messageConferences[confName].areas = {}; config.messageConferences[confName].areas[areaName] = { - name : answers.msgAreaName, - desc : answers.msgAreaDesc, - sort : 1, - default : true, + name: answers.msgAreaName, + desc: answers.msgAreaDesc, + sort: 1, + default: true, }; return callback(null); @@ -206,7 +213,7 @@ function askNewConfigQuestions(cb) { return callback(null); }); - } + }, ], err => { return cb(err, configPath, config); @@ -217,14 +224,14 @@ function askNewConfigQuestions(cb) { const copyFileSyncSilent = (to, from, flags) => { try { fs.copyFileSync(to, from, flags); - } catch(e) { + } catch (e) { /* absorb! */ } }; function buildNewConfig() { - askNewConfigQuestions( (err, configPath, config) => { - if(err) { + askNewConfigQuestions((err, configPath, config) => { + if (err) { return err; } @@ -232,7 +239,7 @@ function buildNewConfig() { mkdirsSync(paths.join(__dirname, '../../config/menus')); const boardName = sanatizeFilename(config.general.boardName) - .replace(/[^a-z0-9_-]/ig, '_') + .replace(/[^a-z0-9_-]/gi, '_') .replace(/_+/g, '_') .toLowerCase(); @@ -258,8 +265,12 @@ function buildNewConfig() { }); // We really only need includes to be replaced - const mainTemplate = fs.readFileSync(paths.join(__dirname, '../../misc/menu_templates/main.in.hjson'), 'utf8') - .replace(/%INCLUDE_FILES%/g, includeFiles.join('\n\t\t')); // cheesy, but works! + const mainTemplate = fs + .readFileSync( + paths.join(__dirname, '../../misc/menu_templates/main.in.hjson'), + 'utf8' + ) + .replace(/%INCLUDE_FILES%/g, includeFiles.join('\n\t\t')); // cheesy, but works! const menuFile = `${boardName}-main.hjson`; fs.writeFileSync( @@ -270,7 +281,7 @@ function buildNewConfig() { config.general.menuFile = paths.join(__dirname, '../../config/menus/', menuFile); - if(writeConfig(config, configPath)) { + if (writeConfig(config, configPath)) { console.info('Configuration generated'); } else { console.error('Failed writing configuration'); @@ -280,15 +291,15 @@ function buildNewConfig() { function catCurrentConfig() { try { - const config = hjson.rt.parse(fs.readFileSync(getConfigPath(), 'utf8')); + const config = hjson.rt.parse(fs.readFileSync(getConfigPath(), 'utf8')); const hjsonOpts = Object.assign({}, HJSONStringifyCommonOpts, { - colors : false === argv.colors ? false : true, - keepWsc : false === argv.comments ? false : true, + colors: false === argv.colors ? false : true, + keepWsc: false === argv.comments ? false : true, }); console.log(hjson.stringify(config, hjsonOpts)); - } catch(e) { - if('ENOENT' == e.code) { + } catch (e) { + if ('ENOENT' == e.code) { console.error(`File not found: ${getConfigPath()}`); } else { console.error(e); @@ -297,16 +308,19 @@ function catCurrentConfig() { } function handleConfigCommand() { - if(true === argv.help) { + if (true === argv.help) { return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); } const action = argv._[1]; - switch(action) { - case 'new' : return buildNewConfig(); - case 'cat' : return catCurrentConfig(); + switch (action) { + case 'new': + return buildNewConfig(); + case 'cat': + return catCurrentConfig(); - 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 5531695d..b588c15c 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -2,32 +2,32 @@ /* eslint-disable no-console */ 'use strict'; -const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; -const ExitCodes = require('./oputil_common.js').ExitCodes; -const argv = require('./oputil_common.js').argv; -const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; -const getHelpFor = require('./oputil_help.js').getHelpFor; +const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; +const ExitCodes = require('./oputil_common.js').ExitCodes; +const argv = require('./oputil_common.js').argv; +const initConfigAndDatabases = require('./oputil_common.js').initConfigAndDatabases; +const getHelpFor = require('./oputil_help.js').getHelpFor; const { getAreaAndStorage, looksLikePattern, getConfigPath, getAnswers, - writeConfig -} = require('./oputil_common.js'); -const Errors = require('../enig_error.js').Errors; + writeConfig, +} = require('./oputil_common.js'); +const Errors = require('../enig_error.js').Errors; -const async = require('async'); -const fs = require('graceful-fs'); -const paths = require('path'); -const _ = require('lodash'); -const moment = require('moment'); -const inq = require('inquirer'); -const glob = require('glob'); -const sanatizeFilename = require('sanitize-filename'); -const hjson = require('hjson'); -const { mkdirs } = require('fs-extra'); +const async = require('async'); +const fs = require('graceful-fs'); +const paths = require('path'); +const _ = require('lodash'); +const moment = require('moment'); +const inq = require('inquirer'); +const glob = require('glob'); +const sanatizeFilename = require('sanitize-filename'); +const hjson = require('hjson'); +const { mkdirs } = require('fs-extra'); -exports.handleFileBaseCommand = handleFileBaseCommand; +exports.handleFileBaseCommand = handleFileBaseCommand; /* :TODO: @@ -42,49 +42,55 @@ exports.handleFileBaseCommand = handleFileBaseCommand; */ -let fileArea; // required during init +let fileArea; // required during init function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { async.series( [ function getDescFromHandlerIfNeeded(callback) { - if((fileEntry.desc && fileEntry.descSrc != 'fileName' && fileEntry.desc.length > 0 ) && !argv['desc-file']) { - return callback(null); // we have a desc already and are NOT overriding with desc file + if ( + fileEntry.desc && + fileEntry.descSrc != 'fileName' && + fileEntry.desc.length > 0 && + !argv['desc-file'] + ) { + return callback(null); // we have a desc already and are NOT overriding with desc file } - 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) { + if (desc) { fileEntry.desc = desc; } return callback(null); }, function getDescFromUserIfNeeded(callback) { - if(fileEntry.desc && fileEntry.desc.length > 0 ) { + 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) { + if (false === argv.prompt) { fileEntry.desc = descFromFile; return callback(null); } const questions = [ { - name : 'desc', - message : `Description for ${fileEntry.fileName}:`, - type : 'input', - default : descFromFile, - } + name: 'desc', + message: `Description for ${fileEntry.fileName}:`, + type: 'input', + default: descFromFile, + }, ]; - inq.prompt(questions).then( answers => { + inq.prompt(questions).then(answers => { fileEntry.desc = answers.desc; return callback(null); }); @@ -93,7 +99,7 @@ function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { fileEntry.persist(isUpdate, err => { return callback(err); }); - } + }, ], err => { return cb(err); @@ -101,20 +107,18 @@ function finalizeEntryAndPersist(isUpdate, fileEntry, descHandler, cb) { ); } -const SCAN_EXCLUDE_FILENAMES = [ - 'DESCRIPT.ION', - 'FILES.BBS', - 'ALLFILES.TXT', -]; +const SCAN_EXCLUDE_FILENAMES = ['DESCRIPT.ION', 'FILES.BBS', 'ALLFILES.TXT']; function loadDescHandler(path, cb) { const handlerClassFromFileName = { - 'descript.ion' : require('../../core/descript_ion_file.js'), - 'files.bbs' : require('../../core/files_bbs_file.js'), + 'descript.ion': require('../../core/descript_ion_file.js'), + 'files.bbs': require('../../core/files_bbs_file.js'), }[paths.basename(path).toLowerCase()]; - if(!handlerClassFromFileName) { - return cb(Errors.DoesNotExist(`No handlers registered for ${paths.basename(path)}`)); + if (!handlerClassFromFileName) { + return cb( + Errors.DoesNotExist(`No handlers registered for ${paths.basename(path)}`) + ); } handlerClassFromFileName.createFromFile(path, (err, descHandler) => { @@ -127,23 +131,25 @@ function loadDescHandler(path, cb) { // checking for common filenames. // function findSuitableDescHandler(basePath, cb) { - const commonFiles = [ 'FILES.BBS', 'DESCRIPT.ION' ]; + const commonFiles = ['FILES.BBS', 'DESCRIPT.ION']; - async.eachSeries(commonFiles, (fileName, nextFileName) => { - loadDescHandler(paths.join(basePath, fileName), (err, handler) => { - if(!err && handler) { - return cb(null, handler); - } - return nextFileName(null); - }); - }, - () => { - return cb(Errors.DoesNotExist('No suitable description handler available')); - }); + async.eachSeries( + commonFiles, + (fileName, nextFileName) => { + loadDescHandler(paths.join(basePath, fileName), (err, handler) => { + if (!err && handler) { + return cb(null, handler); + } + return nextFileName(null); + }); + }, + () => { + return cb(Errors.DoesNotExist('No suitable description handler available')); + } + ); } function scanFileAreaForChanges(areaInfo, options, cb) { - const storageLocations = fileArea.getAreaStorageLocations(areaInfo).filter(sl => { return options.areaAndStorageInfo.find(asi => { return !asi.storageTag || sl.storageTag === asi.storageTag; @@ -151,196 +157,287 @@ function scanFileAreaForChanges(areaInfo, options, cb) { }); function updateTags(fe) { - if(Array.isArray(options.tags)) { + if (Array.isArray(options.tags)) { fe.hashTags = new Set(options.tags); - } else if (areaInfo.hashTags) { // no explicit tags; merge in defaults, if any + } else if (areaInfo.hashTags) { + // no explicit tags; merge in defaults, if any fe.hashTags = areaInfo.hashTags; } } 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 - } - - findSuitableDescHandler(storageLoc.dir, (err, descHandler) => { - return callback(null, descHandler); - }); - }, - function scanPhysFiles(descHandler, callback) { - const physDir = storageLoc.dir; - - readDir(physDir, (err, files) => { - if(err) { - return callback(err); + 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(files, (fileName, nextFile) => { - const fullPath = paths.join(physDir, fileName); + findSuitableDescHandler(storageLoc.dir, (err, descHandler) => { + return callback(null, descHandler); + }); + }, + function scanPhysFiles(descHandler, callback) { + const physDir = storageLoc.dir; - if(SCAN_EXCLUDE_FILENAMES.includes(fileName.toUpperCase())) { - console.info(`Excluding ${fullPath}`); - return nextFile(null); + readDir(physDir, (err, files) => { + if (err) { + return callback(err); } - fs.stat(fullPath, (err, stats) => { - if(err) { - // :TODO: Log me! - return nextFile(null); // always try next file - } + async.eachSeries( + files, + (fileName, nextFile) => { + const fullPath = paths.join(physDir, fileName); - if(!stats.isFile()) { - return nextFile(null); - } + if ( + SCAN_EXCLUDE_FILENAMES.includes( + fileName.toUpperCase() + ) + ) { + console.info(`Excluding ${fullPath}`); + return nextFile(null); + } - process.stdout.write(`Scanning ${fullPath}... `); - - async.series( - [ - function quickCheck(next) { - if(options['full']) { - return next(null); - } - - FileEntry.quickCheckExistsByPath(fullPath, (err, exists) => { - if(exists) { - console.info('Dupe'); - return nextFile(null); - } - - return next(null); - }); - }, - function fullScan() { - fileArea.scanFile( - fullPath, - { - areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag, - hashTags : areaInfo.hashTags, - }, - (stepInfo, next) => { - if(argv.verbose) { - if(stepInfo.error) { - console.error(` error: ${stepInfo.error}`); - } else { - console.info(` processing: ${stepInfo.step}`); - } - } - return next(null); - }, - (err, fileEntry, dupeEntries) => { - if(err) { - console.info(`Error: ${err.message}`); - return nextFile(null); // try next anyway - } - - // - // We'll update the entry if the following conditions are met: - // * We have a single duplicate, and: - // * --update was passed or the existing entry's desc, - // longDesc, or est_release_year meta are blank/empty - // - if(argv.update && 1 === dupeEntries.length) { - const FileEntry = require('../../core/file_entry.js'); - const existingEntry = new FileEntry(); - - return existingEntry.load(dupeEntries[0].fileId, err => { - if(err) { - console.info('Dupe (cannot update)'); - return nextFile(null); - } - - // - // Update only if tags or desc changed - // - const optTags = Array.isArray(options.tags) ? new Set(options.tags) : existingEntry.hashTags; - const tagsEq = _.isEqual(optTags, existingEntry.hashTags); - - let descSauceCompare; - if(existingEntry.meta.desc_sauce) { - descSauceCompare = JSON.stringify(existingEntry.meta.desc_sauce); - } - - if( tagsEq && - fileEntry.desc === existingEntry.desc && - fileEntry.descLong === existingEntry.descLong && - fileEntry.meta.est_release_year === existingEntry.meta.est_release_year && - fileEntry.meta.desc_sauce === descSauceCompare - ) - { - console.info('Dupe'); - return nextFile(null); - } - - console.info('Dupe (updating)'); - - // don't allow overwrite of values if new version is blank - existingEntry.desc = fileEntry.desc || existingEntry.desc; - existingEntry.descLong = fileEntry.descLong || existingEntry.descLong; - - if(fileEntry.meta.est_release_year) { - existingEntry.meta.est_release_year = fileEntry.meta.est_release_year; - } - - if(fileEntry.meta.desc_sauce) { - existingEntry.meta.desc_sauce = fileEntry.meta.desc_sauce; - } - - updateTags(existingEntry); - - finalizeEntryAndPersist(true, existingEntry, descHandler, err => { - return nextFile(err); - }); - }); - } else if(dupeEntries.length > 0) { - console.info('Dupe'); - return nextFile(null); - } - - console.info('Done!'); - updateTags(fileEntry); - - finalizeEntryAndPersist(false, fileEntry, descHandler, err => { - return nextFile(err); - }); - } - ); + fs.stat(fullPath, (err, stats) => { + if (err) { + // :TODO: Log me! + return nextFile(null); // always try next file } - ] - ); - }); - }, err => { - return callback(err); + + if (!stats.isFile()) { + return nextFile(null); + } + + process.stdout.write(`Scanning ${fullPath}... `); + + async.series([ + function quickCheck(next) { + if (options['full']) { + return next(null); + } + + FileEntry.quickCheckExistsByPath( + fullPath, + (err, exists) => { + if (exists) { + console.info('Dupe'); + return nextFile(null); + } + + return next(null); + } + ); + }, + function fullScan() { + fileArea.scanFile( + fullPath, + { + areaTag: areaInfo.areaTag, + storageTag: storageLoc.storageTag, + hashTags: areaInfo.hashTags, + }, + (stepInfo, next) => { + if (argv.verbose) { + if (stepInfo.error) { + console.error( + ` error: ${stepInfo.error}` + ); + } else { + console.info( + ` processing: ${stepInfo.step}` + ); + } + } + return next(null); + }, + (err, fileEntry, dupeEntries) => { + if (err) { + console.info( + `Error: ${err.message}` + ); + return nextFile(null); // try next anyway + } + + // + // We'll update the entry if the following conditions are met: + // * We have a single duplicate, and: + // * --update was passed or the existing entry's desc, + // longDesc, or est_release_year meta are blank/empty + // + if ( + argv.update && + 1 === dupeEntries.length + ) { + const FileEntry = require('../../core/file_entry.js'); + const existingEntry = + new FileEntry(); + + return existingEntry.load( + dupeEntries[0].fileId, + err => { + if (err) { + console.info( + 'Dupe (cannot update)' + ); + return nextFile( + null + ); + } + + // + // Update only if tags or desc changed + // + const optTags = + Array.isArray( + options.tags + ) + ? new Set( + options.tags + ) + : existingEntry.hashTags; + const tagsEq = + _.isEqual( + optTags, + existingEntry.hashTags + ); + + let descSauceCompare; + if ( + existingEntry.meta + .desc_sauce + ) { + descSauceCompare = + JSON.stringify( + existingEntry + .meta + .desc_sauce + ); + } + + if ( + tagsEq && + fileEntry.desc === + existingEntry.desc && + fileEntry.descLong === + existingEntry.descLong && + fileEntry.meta + .est_release_year === + existingEntry + .meta + .est_release_year && + fileEntry.meta + .desc_sauce === + descSauceCompare + ) { + console.info( + 'Dupe' + ); + return nextFile( + null + ); + } + + console.info( + 'Dupe (updating)' + ); + + // don't allow overwrite of values if new version is blank + existingEntry.desc = + fileEntry.desc || + existingEntry.desc; + existingEntry.descLong = + fileEntry.descLong || + existingEntry.descLong; + + if ( + fileEntry.meta + .est_release_year + ) { + existingEntry.meta.est_release_year = + fileEntry.meta.est_release_year; + } + + if ( + fileEntry.meta + .desc_sauce + ) { + existingEntry.meta.desc_sauce = + fileEntry.meta.desc_sauce; + } + + updateTags( + existingEntry + ); + + finalizeEntryAndPersist( + true, + existingEntry, + descHandler, + err => { + return nextFile( + err + ); + } + ); + } + ); + } else if ( + dupeEntries.length > 0 + ) { + console.info('Dupe'); + return nextFile(null); + } + + console.info('Done!'); + updateTags(fileEntry); + + finalizeEntryAndPersist( + false, + fileEntry, + descHandler, + err => { + return nextFile(err); + } + ); + } + ); + }, + ]); + }); + }, + err => { + return callback(err); + } + ); }); - }); - }, - function scanDbEntries(callback) { - // :TODO: Look @ db entries for area that were *not* processed above - return callback(null); + }, + function scanDbEntries(callback) { + // :TODO: Look @ db entries for area that were *not* processed above + return callback(null); + }, + ], + err => { + return nextLocation(err); } - ], - err => { - return nextLocation(err); - } - ); - }, - err => { - return cb(err); - }); + ); + }, + err => { + return cb(err); + } + ); } function dumpAreaInfo(areaInfo, areaAndStorageInfo, cb) { @@ -364,31 +461,31 @@ function getFileEntries(pattern, cb) { [ function tryByFileId(callback) { const fileId = parseInt(pattern); - if(!/^[0-9]+$/.test(pattern) || isNaN(fileId)) { - return callback(null, null); // try SHA + 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 ] ); + return callback(null, err ? null : [fileEntry]); }); }, function tryByShaOrPartialSha(entries, callback) { - if(entries) { - return callback(null, entries); // already got it by FILE_ID + if (entries) { + return callback(null, entries); // already got it by FILE_ID } FileEntry.findBySha(pattern, (err, fileEntry) => { - return callback(null, fileEntry ? [ fileEntry ] : null ); + return callback(null, fileEntry ? [fileEntry] : null); }); }, function tryByFileNameWildcard(entries, callback) { - if(entries) { - return callback(null, entries); // already got by FILE_ID|SHA + if (entries) { + return callback(null, entries); // already got by FILE_ID|SHA } return FileEntry.findByFileNameWildcard(pattern, callback); - } + }, ], (err, entries) => { return cb(err, entries); @@ -401,7 +498,7 @@ function dumpFileInfo(shaOrFileId, cb) { [ function getEntry(callback) { getFileEntries(shaOrFileId, (err, entries) => { - if(err) { + if (err) { return callback(err); } @@ -409,7 +506,10 @@ function dumpFileInfo(shaOrFileId, cb) { }); }, function dumpInfo(fileEntry, callback) { - const fullPath = paths.join(fileArea.getAreaStorageDirectoryByTag(fileEntry.storageTag), fileEntry.fileName); + const fullPath = paths.join( + fileArea.getAreaStorageDirectoryByTag(fileEntry.storageTag), + fileEntry.fileName + ); console.info(`file_id: ${fileEntry.fileId}`); console.info(`sha_256: ${fileEntry.fileSha256}`); @@ -423,13 +523,13 @@ function dumpFileInfo(shaOrFileId, cb) { console.info(`${metaName}: ${metaValue}`); }); - if(argv['show-desc']) { + if (argv['show-desc']) { console.info(`${fileEntry.desc}`); } console.info(''); return callback(null); - } + }, ], err => { return cb(err); @@ -451,29 +551,37 @@ function displayFileOrAreaInfo() { 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); + if (!suppliedAreas || 0 === suppliedAreas.length) { + suppliedAreas = _.map( + sysConfig.fileBase.areas, + (areaInfo, areaTag) => areaTag + ); } const areaAndStorageInfo = getAreaAndStorage(suppliedAreas); 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); + 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 => { - return callback(err); - }); - } + ); + }, ], err => { - if(err) { + if (err) { process.exitCode = ExitCodes.ERROR; console.error(err.message); } @@ -485,17 +593,17 @@ function scanFileAreas() { const options = {}; const tags = argv.tags; - if(tags) { + if (tags) { options.tags = tags.split(','); } - options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH - options['full'] = argv.full; + options.descFile = argv['desc-file']; // --desc-file or --desc-file PATH + options['full'] = argv.full; options.areaAndStorageInfo = getAreaAndStorage(argv._.slice(2)); const last = argv._[argv._.length - 1]; - if(options.areaAndStorageInfo.length > 1 && looksLikePattern(last)) { + if (options.areaAndStorageInfo.length > 1 && looksLikePattern(last)) { options.glob = last; options.areaAndStorageInfo.length -= 1; } @@ -514,7 +622,7 @@ function scanFileAreas() { // the description handler now. Else, we'll attempt to look for a description // file in each storage location. // - if(!_.isString(options.descFile)) { + if (!_.isString(options.descFile)) { return callback(null); } @@ -530,11 +638,15 @@ function scanFileAreas() { let areaAndStorageInfoExpanded = []; options.areaAndStorageInfo.forEach(info => { if (info.areaTag.indexOf('*') > -1) { - const areas = fileArea.getFileAreasByTagWildcardRule(info.areaTag); + const areas = fileArea.getFileAreasByTagWildcardRule( + info.areaTag + ); areas.forEach(area => { - areaAndStorageInfoExpanded.push(Object.assign({}, info, { - areaTag : area.areaTag, - })); + areaAndStorageInfoExpanded.push( + Object.assign({}, info, { + areaTag: area.areaTag, + }) + ); }); } else { areaAndStorageInfoExpanded.push(info); @@ -543,24 +655,34 @@ function scanFileAreas() { options.areaAndStorageInfo = areaAndStorageInfoExpanded; - 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}":`); + + scanFileAreaForChanges(areaInfo, options, err => { + return nextAreaTag(err); + }); + }, + err => { + return callback(err); } - - console.info(`Processing area "${areaInfo.name}":`); - - scanFileAreaForChanges(areaInfo, options, err => { - return nextAreaTag(err); - }); - }, err => { - return callback(err); - }); - } + ); + }, ], err => { - if(err) { + if (err) { process.exitCode = ExitCodes.ERROR; console.error(err.message); } @@ -574,54 +696,59 @@ function expandFileTargets(targets, cb) { // 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; - } - - FileEntry.findFiles(findFilter, (err, fileIds) => { - if(err) { - return next(err); + if (areaAndStorage.storageTag) { + findFilter.storageTag = areaAndStorage.storageTag; } - async.each(fileIds, (fileId, nextFileId) => { - const fileEntry = new FileEntry(); - fileEntry.load(fileId, err => { - if(!err) { - entries.push(fileEntry); + 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); } - 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); - }); + entries = entries.concat(fileEntries); + return next(null); + }); + } + }, + err => { + return cb(err, entries); } - }, - err => { - return cb(err, entries); - }); + ); } function moveFiles() { @@ -631,7 +758,7 @@ function moveFiles() { // SRC: FILENAME_WC|FILE_ID|SHA|AREA_TAG[@STORAGE_TAG] // DST: AREA_TAG[@STORAGE_TAG] // - if(argv._.length < 4) { + if (argv._.length < 4) { return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); } @@ -644,8 +771,8 @@ function moveFiles() { async.waterfall( [ function init(callback) { - return initConfigAndDatabases( err => { - if(!err) { + return initConfigAndDatabases(err => { + if (!err) { fileArea = require('../../core/file_base_area.js'); } return callback(err); @@ -653,10 +780,12 @@ function moveFiles() { }, function validateAndExpandSourceAndDest(callback) { const areaInfo = fileArea.getFileAreaByTag(dst.areaTag); - if(areaInfo) { + if (areaInfo) { dst.areaInfo = areaInfo; } else { - return callback(Errors.DoesNotExist('Invalid or unknown destination area')); + return callback( + Errors.DoesNotExist('Invalid or unknown destination area') + ); } FileEntry = require('../../core/file_entry.js'); @@ -666,35 +795,37 @@ function moveFiles() { }); }, function moveEntries(srcEntries, callback) { - - if(!dst.storageTag) { + if (!dst.storageTag) { dst.storageTag = dst.areaInfo.storageTags[0]; } 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); - }); - } + 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) { + if (err) { process.exitCode = ExitCodes.ERROR; console.error(err.message); } @@ -713,19 +844,19 @@ function removeFiles() { // // --phys-file removes backing physical file(s) // - if(argv._.length < 3) { + if (argv._.length < 3) { return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); } 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) { + return initConfigAndDatabases(err => { + if (!err) { fileArea = require('../../core/file_base_area.js'); } return callback(err); @@ -741,26 +872,31 @@ function removeFiles() { 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); - }); - } + return nextEntry(err); + }); + }, + err => { + return callback(err); + } + ); + }, ], err => { - if(err) { + if (err) { process.exitCode = ExitCodes.ERROR; console.error(err.message); } @@ -769,7 +905,7 @@ function removeFiles() { } function getFileBaseImportType(path) { - if(argv.type) { + if (argv.type) { return argv.type.toLowerCase(); } @@ -785,12 +921,12 @@ function importFileAreas() { // http://wiki.mysticbbs.com/doku.php?id=mutil_import_filebone_na // const importPath = argv._[argv._.length - 1]; - if(argv._.length < 3 || !importPath || 0 === importPath.length) { + if (argv._.length < 3 || !importPath || 0 === importPath.length) { return printUsageAndSetExitCode(getHelpFor('FileBase'), ExitCodes.ERROR); } const importType = getFileBaseImportType(importPath); - if(!['zxx', 'na'].includes(importType)) { + if (!['zxx', 'na'].includes(importType)) { return console.error(`"${importType}" is not a recognized import file type`); } @@ -799,44 +935,46 @@ function importFileAreas() { async.waterfall( [ - (callback) => { + callback => { fs.readFile(importPath, 'utf8', (err, importData) => { - if(err) { + if (err) { return callback(err); } const importInfo = { - storageTags : {}, - areas : {}, - count : 0, + storageTags: {}, + areas: {}, + count: 0, }; const re = /Area\s+([^\s]+)\s+[0-9]\s+(?:!|\*&)\s+([^\r\n]+)/gm; let m; - while((m = re.exec(importData))) { - const dir = m[1].trim(); - const name = m[2].trim(); - const safeName = sanatizeFilename(name); + while ((m = re.exec(importData))) { + const dir = m[1].trim(); + const name = m[2].trim(); + const safeName = sanatizeFilename(name); - const stPrefix = _.snakeCase(sanatizeFilename(safeName)); - const storageTag = `${stPrefix}__${_.snakeCase(sanatizeFilename(dir))}`; - const areaTag = _.snakeCase(safeName); + const stPrefix = _.snakeCase(sanatizeFilename(safeName)); + const storageTag = `${stPrefix}__${_.snakeCase( + sanatizeFilename(dir) + )}`; + const areaTag = _.snakeCase(safeName); - if(!dir || !name || !storageTag || !areaTag) { + if (!dir || !name || !storageTag || !areaTag) { console.info(`Skipping entry: ${m[0]}`); continue; } importInfo.storageTags[storageTag] = dir; importInfo.areas[areaTag] = { - name : name, - desc : name, - storageTags : [ storageTag ], + name: name, + desc: name, + storageTags: [storageTag], }; ++importInfo.count; } - if(0 === importInfo.count) { + if (0 === importInfo.count) { return callback(new Error('Nothing to import')); } @@ -857,29 +995,31 @@ function importFileAreas() { console.info(` storage: ${area.storageTags[0]} => ${dir}`); }); - getAnswers([ - { - name : 'proceed', - message : 'Proceed?', - type : 'confirm', + getAnswers( + [ + { + name: 'proceed', + message: 'Proceed?', + type: 'confirm', + }, + ], + answers => { + if (answers.proceed) { + return callback(null, importInfo); + } + return callback(Errors.General('User canceled')); } - ], - answers => { - if(answers.proceed) { - return callback(null, importInfo); - } - return callback(Errors.General('User canceled')); - }); + ); }, (importInfo, callback) => { fs.readFile(getConfigPath(), 'utf8', (err, configData) => { - if(err) { + if (err) { return callback(err); } let config; try { config = hjson.rt.parse(configData); - } catch(e) { + } catch (e) { return callback(e); } return callback(null, importInfo, config); @@ -888,15 +1028,23 @@ function importFileAreas() { (importInfo, config, callback) => { const newStorageTagDirs = []; _.each(importInfo.areas, (area, areaTag) => { - const existingArea = _.get(config, [ 'fileBase', 'areas', areaTag ]); - if(existingArea) { - return console.info(`Skipping ${area.name}. Area tag "${areaTag}" already exists.`); + const existingArea = _.get(config, ['fileBase', 'areas', areaTag]); + if (existingArea) { + return console.info( + `Skipping ${area.name}. Area tag "${areaTag}" already exists.` + ); } const storageTag = area.storageTags[0]; - const existingStorageTag = _.get(config, [ 'fileBase', 'storageTags', storageTag ]); - if(existingStorageTag) { - return console.info(`Skipping ${area.name} (${areaTag}). Storage tag "${storageTag}" already exists`); + const existingStorageTag = _.get(config, [ + 'fileBase', + 'storageTags', + storageTag, + ]); + if (existingStorageTag) { + return console.info( + `Skipping ${area.name} (${areaTag}). Storage tag "${storageTag}" already exists` + ); } const dir = importInfo.storageTags[storageTag]; @@ -909,7 +1057,7 @@ function importFileAreas() { return callback(null, newStorageTagDirs, config); }, (newStorageTagDirs, config, callback) => { - if(!createDirs) { + if (!createDirs) { return callback(null, config); } @@ -917,29 +1065,32 @@ function importFileAreas() { // Create all directories // const prefixDir = config.fileBase.areaStoragePrefix; - async.eachSeries(newStorageTagDirs, (dir, nextDir) => { - const isAbs = paths.isAbsolute(dir); - if(!isAbs) { - dir = paths.join(prefixDir, dir); - } - mkdirs(dir, err => { - if(!err) { - console.log(`Created ${dir}`); + async.eachSeries( + newStorageTagDirs, + (dir, nextDir) => { + const isAbs = paths.isAbsolute(dir); + if (!isAbs) { + dir = paths.join(prefixDir, dir); } - return nextDir(err); - }); - }, - err => { - return callback(err, config); - }); + mkdirs(dir, err => { + if (!err) { + console.log(`Created ${dir}`); + } + return nextDir(err); + }); + }, + err => { + return callback(err, config); + } + ); }, (config, callback) => { const written = writeConfig(config, getConfigPath()); return callback(written ? null : new Error('Failed to write config!')); - } + }, ], err => { - if(err) { + if (err) { return console.error(err.reason ? err.reason : err.message); } @@ -956,25 +1107,25 @@ function setFileDescription() { // let fileCriteria; let desc; - if(argv._.length > 3) { - fileCriteria = argv._[argv._.length - 2]; - desc = argv._[argv._.length - 1]; + if (argv._.length > 3) { + fileCriteria = argv._[argv._.length - 2]; + desc = argv._[argv._.length - 1]; } else { fileCriteria = argv._[argv._.length - 1]; } async.waterfall( [ - (callback) => { + callback => { return initConfigAndDatabases(callback); }, - (callback) => { + callback => { getFileEntries(fileCriteria, (err, entries) => { - if(err) { + if (err) { return callback(err); } - if(entries.length > 1) { + if (entries.length > 1) { return callback(Errors.General('Criteria not specific enough.')); } @@ -982,33 +1133,36 @@ function setFileDescription() { }); }, (fileEntry, callback) => { - if(desc) { + if (desc) { return callback(null, fileEntry, desc); } - getAnswers([ - { - name : 'userDesc', - message : 'Description:', - type : 'editor', + getAnswers( + [ + { + name: 'userDesc', + message: 'Description:', + type: 'editor', + }, + ], + answers => { + if (!answers.userDesc) { + return callback(Errors.General('User canceled')); + } + return callback(null, fileEntry, answers.userDesc); } - ], - answers => { - if(!answers.userDesc) { - return callback(Errors.General('User canceled')); - } - return callback(null, fileEntry, answers.userDesc); - }); + ); }, (fileEntry, newDesc, callback) => { fileEntry.desc = newDesc; - fileEntry.persist(true, err => { // true=isUpdate + fileEntry.persist(true, err => { + // true=isUpdate return callback(err); }); - } + }, ], err => { - if(err) { + if (err) { process.exitCode = ExitCodes.ERROR; console.error(err.message); } else { @@ -1019,35 +1173,36 @@ function setFileDescription() { } function handleFileBaseCommand() { - - function errUsage() { + function errUsage() { return printUsageAndSetExitCode( getHelpFor('FileBase') + getHelpFor('FileOpsInfo'), ExitCodes.ERROR ); } - if(true === argv.help) { + if (true === argv.help) { return errUsage(); } const action = argv._[1]; - return ({ - info : displayFileOrAreaInfo, - scan : scanFileAreas, + return ( + { + info: displayFileOrAreaInfo, + scan: scanFileAreas, - mv : moveFiles, - move : moveFiles, + mv: moveFiles, + move: moveFiles, - rm : removeFiles, - remove : removeFiles, - del : removeFiles, - delete : removeFiles, + rm: removeFiles, + remove: removeFiles, + del: removeFiles, + delete: removeFiles, - 'import-areas' : importFileAreas, + 'import-areas': importFileAreas, - desc : setFileDescription, - description : setFileDescription, - }[action] || errUsage)(); -} \ No newline at end of file + desc: setFileDescription, + description: setFileDescription, + }[action] || errUsage + )(); +} diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 05025cfe..eab9d96c 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -2,13 +2,12 @@ /* eslint-disable no-console */ 'use strict'; -const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPath; +const getDefaultConfigPath = require('./oputil_common.js').getDefaultConfigPath; -exports.getHelpFor = getHelpFor; +exports.getHelpFor = getHelpFor; -const usageHelp = exports.USAGE_HELP = { - General : -`usage: oputil.js [--version] [--help] +const usageHelp = (exports.USAGE_HELP = { + General: `usage: oputil.js [--version] [--help] [] Global arguments: @@ -22,8 +21,7 @@ Commands: fb File base management mb Message base management `, - User : -`usage: oputil.js user [] + User: `usage: oputil.js user [] Actions: info USERNAME Display information about a user @@ -83,8 +81,7 @@ info arguments: --out PATH Path to write QR code to. defaults to stdout `, - Config : -`usage: oputil.js config [] + Config: `usage: oputil.js config [] Actions: new Generate a new / default configuration @@ -95,8 +92,7 @@ cat arguments: --no-color Disable color --no-comments Strip any comments `, - FileBase : -`usage: oputil.js fb [] + FileBase: `usage: oputil.js fb [] Actions: scan AREA_TAG[@STORAGE_TAG] Scan specified area @@ -160,8 +156,7 @@ import-areas arguments: --create-dirs Also create backing storage directories `, - FileOpsInfo : -` + FileOpsInfo: ` General Information: Generally an area tag can also include an optional storage tag. For example, the area of 'bbswarez' stored using 'bbswarez_main': bbswarez@bbswarez_main @@ -172,8 +167,7 @@ General Information: File ID's are those found in file.sqlite3. `, - MessageBase : -`usage: oputil.js mb [] + MessageBase: `usage: oputil.js mb [] Actions: areafix CMD1 CMD2 ... ADDR Sends an AreaFix NetMail @@ -202,8 +196,8 @@ qwk-export arguments: TIMESTAMP. --no-qwke Disable QWKE extensions. --no-synchronet Disable Synchronet style extensions. -` -}; +`, +}); function getHelpFor(command) { return usageHelp[command]; diff --git a/core/oputil/oputil_main.js b/core/oputil/oputil_main.js index aafc8ef1..9dcbc510 100644 --- a/core/oputil/oputil_main.js +++ b/core/oputil/oputil_main.js @@ -2,35 +2,37 @@ /* eslint-disable no-console */ 'use strict'; -const ExitCodes = require('./oputil_common.js').ExitCodes; -const argv = require('./oputil_common.js').argv; -const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; -const handleUserCommand = require('./oputil_user.js').handleUserCommand; -const handleFileBaseCommand = require('./oputil_file_base.js').handleFileBaseCommand; -const handleMessageBaseCommand = require('./oputil_message_base.js').handleMessageBaseCommand; -const handleConfigCommand = require('./oputil_config.js').handleConfigCommand; -const getHelpFor = require('./oputil_help.js').getHelpFor; - - -module.exports = function() { +const ExitCodes = require('./oputil_common.js').ExitCodes; +const argv = require('./oputil_common.js').argv; +const printUsageAndSetExitCode = require('./oputil_common.js').printUsageAndSetExitCode; +const handleUserCommand = require('./oputil_user.js').handleUserCommand; +const handleFileBaseCommand = require('./oputil_file_base.js').handleFileBaseCommand; +const handleMessageBaseCommand = + require('./oputil_message_base.js').handleMessageBaseCommand; +const handleConfigCommand = require('./oputil_config.js').handleConfigCommand; +const getHelpFor = require('./oputil_help.js').getHelpFor; +module.exports = function () { process.exitCode = ExitCodes.SUCCESS; - if(true === argv.version) { + if (true === argv.version) { return console.info(require('../../package.json').version); } - if(0 === argv._.length || - 'help' === argv._[0]) - { + if (0 === argv._.length || 'help' === argv._[0]) { 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 f01f8f9c..38f5e529 100644 --- a/core/oputil/oputil_message_base.js +++ b/core/oputil/oputil_message_base.js @@ -12,29 +12,26 @@ const { writeConfig, } = require('./oputil_common.js'); -const getHelpFor = require('./oputil_help.js').getHelpFor; -const Address = require('../ftn_address.js'); -const Errors = require('../enig_error.js').Errors; +const getHelpFor = require('./oputil_help.js').getHelpFor; +const Address = require('../ftn_address.js'); +const Errors = require('../enig_error.js').Errors; // deps -const async = require('async'); -const paths = require('path'); -const fs = require('fs'); -const hjson = require('hjson'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const paths = require('path'); +const fs = require('fs'); +const hjson = require('hjson'); +const _ = require('lodash'); +const moment = require('moment'); -exports.handleMessageBaseCommand = handleMessageBaseCommand; +exports.handleMessageBaseCommand = handleMessageBaseCommand; function areaFix() { // // oputil mb areafix CMD1 CMD2 ... ADDR [--password PASS] // - if(argv._.length < 3) { - return printUsageAndSetExitCode( - getHelpFor('MessageBase'), - ExitCodes.ERROR - ); + if (argv._.length < 3) { + return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR); } async.waterfall( @@ -46,8 +43,10 @@ function areaFix() { 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`) + ); } // @@ -65,9 +64,9 @@ function areaFix() { // const User = require('../user.js'); - if(argv.from) { + if (argv.from) { User.getUserIdAndNameByLookup(argv.from, (err, userId, fromName) => { - if(err) { + if (err) { return callback(null, ftnAddr, argv.from, 0); } @@ -76,7 +75,12 @@ function areaFix() { }); } else { User.getUserName(User.RootUserID, (err, fromName) => { - return callback(null, ftnAddr, fromName || 'SysOp', err ? 0 : User.RootUserID); + return callback( + null, + ftnAddr, + fromName || 'SysOp', + err ? 0 : User.RootUserID + ); }); } }, @@ -88,27 +92,31 @@ function areaFix() { // 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 messageBody = + argv._.slice(2, -1) + .map(arg => { + return arg.replace(/["']/g, ''); + }) + .join('\r\n') + '\n'; 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 - } - } + 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) { + if (0 !== fromUserId) { message.setLocalFromUserId(fromUserId); } @@ -116,15 +124,17 @@ function areaFix() { }, function persistMessage(message, callback) { message.persist(err => { - if(!err) { - console.log('AreaFix message persisted and will be exported at next scheduled scan'); + if (!err) { + console.log( + 'AreaFix message persisted and will be exported at next scheduled scan' + ); } return callback(err); }); - } + }, ], err => { - if(err) { + if (err) { process.exitCode = ExitCodes.ERROR; console.error(`${err.message}${err.reason ? ': ' + err.reason : ''}`); } @@ -142,7 +152,7 @@ function validateUplinks(uplinks) { } function getMsgAreaImportType(path) { - if(argv.type) { + if (argv.type) { return argv.type.toLowerCase(); } @@ -151,20 +161,20 @@ function getMsgAreaImportType(path) { function importAreas() { const importPath = argv._[argv._.length - 1]; - if(argv._.length < 3 || !importPath || 0 === importPath.length) { + if (argv._.length < 3 || !importPath || 0 === importPath.length) { return printUsageAndSetExitCode(getHelpFor('Config'), ExitCodes.ERROR); } const importType = getMsgAreaImportType(importPath); - if('na' !== importType && 'bbs' !== importType) { + 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) { + let confTag = argv.conf; + let networkName = argv.network; + let uplinks = argv.uplinks; + if (uplinks) { uplinks = uplinks.split(/[\s,]+/); } @@ -174,24 +184,24 @@ function importAreas() { [ function readImportFile(callback) { fs.readFile(importPath, 'utf8', (err, importData) => { - if(err) { + if (err) { return callback(err); } importEntries = getImportEntries(importType, importData); - if(0 === importEntries.length) { + 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)) { + 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 || [])) { + if (!validateUplinks(uplinks || [])) { return callback(Errors.Invalid('Invalid uplink(s)')); } } @@ -203,184 +213,222 @@ function importAreas() { return initConfigAndDatabases(callback); }, function validateAndCollectInput(callback) { - const msgArea = require('../../core/message_area.js'); - const sysConfig = require('../../core/config.js').get(); + 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, + 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`) + ); } - const existingNetworkNames = Object.keys(_.get(sysConfig, 'messageNetworks.ftn.networks', {})); + const existingNetworkNames = Object.keys( + _.get(sysConfig, 'messageNetworks.ftn.networks', {}) + ); - if(networkName && !existingNetworkNames.find(net => networkName === net)) { - return callback(Errors.DoesNotExist(`FTN style Network "${networkName}" does not exist`)); + if ( + networkName && + !existingNetworkNames.find(net => networkName === net) + ) { + return callback( + Errors.DoesNotExist( + `FTN style Network "${networkName}" does not exist` + ) + ); } // can't use --uplinks without a network - if(!networkName && 0 === existingNetworkNames.length && uplinks) { - return callback(Errors.Invalid('Cannot use --uplinks without an FTN network to import to')); + if (!networkName && 0 === existingNetworkNames.length && uplinks) { + return callback( + Errors.Invalid( + 'Cannot use --uplinks without an FTN network to import to' + ) + ); } - getAnswers([ - { - name : 'confTag', - message : 'Message conference:', - type : 'list', - choices : msgConfs, - pageSize : 10, - when : !confTag, - }, - { - name : 'networkName', - message : 'FTN network name:', - type : 'list', - choices : [ '-None-' ].concat(existingNetworkNames), - pageSize : 10, - when : !networkName && existingNetworkNames.length > 0, - filter : (choice) => { - return '-None-' === choice ? undefined : choice; - } - }, - ], - answers => { - confTag = confTag || answers.confTag; - networkName = networkName || answers.networkName; - uplinks = uplinks || answers.uplinks; + getAnswers( + [ + { + name: 'confTag', + message: 'Message conference:', + type: 'list', + choices: msgConfs, + pageSize: 10, + when: !confTag, + }, + { + name: 'networkName', + message: 'FTN network name:', + type: 'list', + choices: ['-None-'].concat(existingNetworkNames), + pageSize: 10, + when: !networkName && existingNetworkNames.length > 0, + filter: choice => { + return '-None-' === choice ? undefined : choice; + }, + }, + ], + answers => { + confTag = confTag || answers.confTag; + networkName = networkName || answers.networkName; + uplinks = uplinks || answers.uplinks; - importEntries.forEach(ie => { - ie.areaTag = ie.ftnTag.toLowerCase(); - }); + importEntries.forEach(ie => { + ie.areaTag = ie.ftnTag.toLowerCase(); + }); - return callback(null); - }); + return callback(null); + } + ); }, function collectUplinks(callback) { - if(!networkName || uplinks || 'bbs' === importType) { + if (!networkName || uplinks || 'bbs' === importType) { return callback(null); } - getAnswers([ - { - name : 'uplinks', - message : 'Uplink(s) (comma separated):', - type : 'input', - validate : (input) => { - const inputUplinks = input.split(/[\s,]+/); - return validateUplinks(inputUplinks) ? true : 'Invalid uplink(s)'; + getAnswers( + [ + { + name: 'uplinks', + message: 'Uplink(s) (comma separated):', + type: 'input', + validate: input => { + const inputUplinks = input.split(/[\s,]+/); + return validateUplinks(inputUplinks) + ? true + : 'Invalid uplink(s)'; + }, }, + ], + answers => { + uplinks = answers.uplinks; + return callback(null); } - ], - answers => { - uplinks = answers.uplinks; - return callback(null); - }); + ); }, function confirmWithUser(callback) { - const sysConfig = require('../../core/config.js').get(); + const sysConfig = require('../../core/config.js').get(); console.info(`Importing the following for "${confTag}"`); - console.info(`(${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})`); + console.info( + `(${sysConfig.messageConferences[confTag].name} - ${sysConfig.messageConferences[confTag].desc})` + ); console.info(''); importEntries.forEach(ie => { console.info(` ${ie.ftnTag} - ${ie.name}`); }); - if(networkName) { + if (networkName) { console.info(''); console.info(`For FTN network: ${networkName}`); console.info(`Uplinks: ${uplinks}`); console.info(''); - console.info('Importing will NOT create required FTN network configurations.'); - console.info('If you have not yet done this, you will need to complete additional steps after importing.'); + console.info( + 'Importing will NOT create required FTN network configurations.' + ); + console.info( + 'If you have not yet done this, you will need to complete additional steps after importing.' + ); console.info('See Message Networks docs for details.'); console.info(''); } - getAnswers([ - { - name : 'proceed', - message : 'Proceed?', - type : 'confirm', + getAnswers( + [ + { + name: 'proceed', + message: 'Proceed?', + type: 'confirm', + }, + ], + answers => { + return callback( + answers.proceed ? null : Errors.General('User canceled') + ); } - ], - answers => { - return callback(answers.proceed ? null : Errors.General('User canceled')); - }); - + ); }, function loadConfigHjson(callback) { const configPath = getConfigPath(); fs.readFile(configPath, 'utf8', (err, confData) => { - if(err) { + if (err) { return callback(err); } let config; try { - config = hjson.parse(confData, { keepWsc : true } ); - } catch(e) { + config = hjson.parse(confData, { keepWsc: true }); + } catch (e) { return callback(e); } return callback(null, config); - }); }, function performImport(config, callback) { - const confAreas = { messageConferences : {} }; - confAreas.messageConferences[confTag] = { areas : {} }; + const 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 + const specificUplinks = ie.uplinks || uplinks; // AREAS.BBS has specific uplinks per area confAreas.messageConferences[confTag].areas[ie.areaTag] = { - name : ie.name, - desc : ie.name, + name: ie.name, + desc: ie.name, }; - if(networkName) { + if (networkName) { msgNetworks.messageNetworks.ftn.areas[ie.areaTag] = { - network : networkName, - tag : ie.ftnTag, - uplinks : specificUplinks + 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')); + if (!writeConfig(newConfig, configPath)) { + return callback( + Errors.UnexpectedState('Failed writing configuration') + ); } return callback(null); - } + }, ], err => { - if(err) { + if (err) { console.error(err.reason ? err.reason : err.message); } else { const addFieldUpd = 'bbs' === importType ? '"name" and "desc"' : '"desc"'; console.info('Import complete.'); - console.info(`You may wish to validate changes made to ${getConfigPath()}`); + console.info( + `You may wish to validate changes made to ${getConfigPath()}` + ); console.info(`as well as update ${addFieldUpd} fields, sorting, etc.`); console.info(''); } @@ -391,7 +439,7 @@ function importAreas() { function getImportEntries(importType, importData) { let importEntries = []; - if('na' === importType) { + if ('na' === importType) { // // parse out // TAG DESC @@ -399,10 +447,10 @@ function getImportEntries(importType, importData) { const re = /^([^\s]+)\s+([^\r\n]+)/gm; let m; - while( (m = re.exec(importData) )) { + while ((m = re.exec(importData))) { importEntries.push({ - ftnTag : m[1].trim(), - name : m[2].trim(), + ftnTag: m[1].trim(), + name: m[2].trim(), }); } } else if ('bbs' === importType) { @@ -422,13 +470,13 @@ function getImportEntries(importType, importData) { // const re = /^[^\s]+\s+([^\s]+)\s+([^\n]+)$/gm; let m; - while ( (m = re.exec(importData) )) { + while ((m = re.exec(importData))) { const tag = m[1].trim(); importEntries.push({ - ftnTag : tag, - name : `Area: ${tag}`, - uplinks : m[2].trim().split(/[\s,]+/), + ftnTag: tag, + name: `Area: ${tag}`, + uplinks: m[2].trim().split(/[\s,]+/), }); } } @@ -438,16 +486,16 @@ function getImportEntries(importType, importData) { function dumpQWKPacket() { const packetPath = argv._[argv._.length - 1]; - if(argv._.length < 3 || !packetPath || 0 === packetPath.length) { + if (argv._.length < 3 || !packetPath || 0 === packetPath.length) { return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR); } async.waterfall( [ - (callback) => { + callback => { return initConfigAndDatabases(callback); }, - (callback) => { + callback => { const { QWKPacketReader } = require('../qwk_mail_packet'); const reader = new QWKPacketReader(packetPath); @@ -477,7 +525,7 @@ function dumpQWKPacket() { }); reader.read(); - } + }, ], err => { if (err) { @@ -489,7 +537,7 @@ function dumpQWKPacket() { function exportQWKPacket() { let packetPath = argv._[argv._.length - 1]; - if(argv._.length < 3 || !packetPath || 0 === packetPath.length) { + if (argv._.length < 3 || !packetPath || 0 === packetPath.length) { return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR); } @@ -524,19 +572,19 @@ function exportQWKPacket() { const userName = argv.user || '-'; const writerOptions = { - enableQWKE : !(false === argv.qwke), - enableHeadersExtension : !(false === argv.synchronet), - enableAtKludges : !(false === argv.synchronet), - archiveFormat : argv.format || 'application/zip' + enableQWKE: !(false === argv.qwke), + enableHeadersExtension: !(false === argv.synchronet), + enableAtKludges: !(false === argv.synchronet), + archiveFormat: argv.format || 'application/zip', }; let totalExported = 0; async.waterfall( [ - (callback) => { + callback => { return initConfigAndDatabases(callback); }, - (callback) => { + callback => { const User = require('../../core/user.js'); User.getUserIdAndName(userName, (err, userId) => { @@ -555,7 +603,7 @@ function exportQWKPacket() { // if they were not explicitly supplied if (!areaTags.length) { const { - getAllAvailableMessageAreaTags + getAllAvailableMessageAreaTags, } = require('../../core/message_area'); areaTags = getAllAvailableMessageAreaTags(); @@ -566,8 +614,8 @@ function exportQWKPacket() { const Message = require('../message'); const filter = { - resultType : 'id', - areaTag : areaTags, + resultType: 'id', + areaTag: areaTags, newerThanTimestamp, }; @@ -581,34 +629,46 @@ function exportQWKPacket() { filter.privateTagUserId = user.userId; Message.findMessages(filter, (err, privateMessageIds) => { - return callback(err, user, Message, privateMessageIds.concat(publicMessageIds)); + return callback( + err, + user, + Message, + privateMessageIds.concat(publicMessageIds) + ); }); }); }, (user, Message, messageIds, callback) => { const { QWKPacketWriter } = require('../qwk_mail_packet'); - const writer = new QWKPacketWriter(Object.assign(writerOptions, { - bbsID, - user, - })); + const writer = new QWKPacketWriter( + Object.assign(writerOptions, { + bbsID, + user, + }) + ); writer.on('ready', () => { - async.eachSeries(messageIds, (messageId, nextMessageId) => { - const message = new Message(); - message.load( { messageId }, err => { - if (!err) { - writer.appendMessage(message); - ++totalExported; + async.eachSeries( + messageIds, + (messageId, nextMessageId) => { + const message = new Message(); + message.load({ messageId }, err => { + if (!err) { + writer.appendMessage(message); + ++totalExported; + } + return nextMessageId(err); + }); + }, + err => { + writer.finish(packetPath); + if (err) { + console.error( + `Failed to write one or more messages: ${err.message}` + ); } - return nextMessageId(err); - }); - }, - (err) => { - writer.finish(packetPath); - if (err) { - console.error(`Failed to write one or more messages: ${err.message}`); } - }); + ); }); writer.on('warning', err => { @@ -620,10 +680,10 @@ function exportQWKPacket() { }); writer.init(); - } + }, ], err => { - if(err) { + if (err) { return console.error(err.reason ? err.reason : err.message); } @@ -633,24 +693,22 @@ function exportQWKPacket() { } function handleMessageBaseCommand() { - function errUsage() { - return printUsageAndSetExitCode( - getHelpFor('MessageBase'), - ExitCodes.ERROR - ); + return printUsageAndSetExitCode(getHelpFor('MessageBase'), ExitCodes.ERROR); } - if(true === argv.help) { + if (true === argv.help) { return errUsage(); } const action = argv._[1]; - return({ - areafix : areaFix, - 'import-areas' : importAreas, - 'qwk-dump' : dumpQWKPacket, - 'qwk-export' : exportQWKPacket, - }[action] || errUsage)(); -} \ No newline at end of file + return ( + { + areafix: areaFix, + 'import-areas': importAreas, + 'qwk-dump': dumpQWKPacket, + 'qwk-export': exportQWKPacket, + }[action] || errUsage + )(); +} diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index d3a13931..255c7792 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -7,18 +7,18 @@ const { getAnswers, ExitCodes, argv, - initConfigAndDatabases -} = require('./oputil_common.js'); -const getHelpFor = require('./oputil_help.js').getHelpFor; -const Errors = require('../enig_error.js').Errors; -const UserProps = require('../user_property.js'); + initConfigAndDatabases, +} = require('./oputil_common.js'); +const getHelpFor = require('./oputil_help.js').getHelpFor; +const Errors = require('../enig_error.js').Errors; +const UserProps = require('../user_property.js'); -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); -const fs = require('fs-extra'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); +const fs = require('fs-extra'); -exports.handleUserCommand = handleUserCommand; +exports.handleUserCommand = handleUserCommand; function initAndGetUser(userName, cb) { async.waterfall( @@ -29,7 +29,7 @@ function initAndGetUser(userName, cb) { function getUserObject(callback) { const User = require('../../core/user.js'); User.getUserIdAndName(userName, (err, userId) => { - if(err) { + if (err) { // try user ID if number was supplied if (_.isNumber(userName)) { return User.getUser(parseInt(userName), callback); @@ -38,7 +38,7 @@ function initAndGetUser(userName, cb) { } return User.getUser(userId, callback); }); - } + }, ], (err, user) => { return cb(err, user); @@ -47,36 +47,36 @@ function initAndGetUser(userName, cb) { } function setAccountStatus(user, status) { - if(argv._.length < 3) { + if (argv._.length < 3) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } const AccountStatus = require('../../core/user.js').AccountStatus; status = { - activate : AccountStatus.active, - deactivate : AccountStatus.inactive, - disable : AccountStatus.disabled, - lock : AccountStatus.locked, + activate: AccountStatus.active, + deactivate: AccountStatus.inactive, + disable: AccountStatus.disabled, + lock: AccountStatus.locked, }[status]; const statusDesc = _.invert(AccountStatus)[status]; async.series( [ - (callback) => { + callback => { return user.persistProperty(UserProps.AccountStatus, status, callback); }, - (callback) => { - if(AccountStatus.active !== status) { + callback => { + if (AccountStatus.active !== status) { return callback(null); } return user.unlockAccount(callback); - } + }, ], err => { - if(err) { + if (err) { process.exitCode = ExitCodes.ERROR; console.error(err.message); } else { @@ -87,7 +87,7 @@ function setAccountStatus(user, status) { } function setUserPassword(user) { - if(argv._.length < 4) { + if (argv._.length < 4) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } @@ -96,22 +96,22 @@ function setUserPassword(user) { function validate(callback) { // :TODO: prompt if no password provided (more secure, no history, etc.) const password = argv._[argv._.length - 1]; - if(0 === password.length) { + if (0 === password.length) { return callback(Errors.Invalid('Invalid password')); } return callback(null, password); }, function set(password, callback) { user.setNewAuthCredentials(password, err => { - if(err) { + if (err) { process.exitCode = ExitCodes.BAD_ARGS; } return callback(err); }); - } + }, ], err => { - if(err) { + if (err) { console.error(err.message); } else { console.info('New password set'); @@ -125,7 +125,7 @@ function removeUserRecordsFromDbAndTable(dbName, tableName, userId, col, cb) { db.run( `DELETE FROM ${tableName} WHERE ${col} = ?;`, - [ userId ], + [userId], err => { return cb(err); } @@ -135,96 +135,118 @@ function removeUserRecordsFromDbAndTable(dbName, tableName, userId, col, cb) { function removeUser(user) { async.series( [ - (callback) => { - if(user.isRoot()) { + callback => { + if (user.isRoot()) { return callback(Errors.Invalid('Cannot delete root/SysOp user!')); } return callback(null); }, - (callback) => { - if(false === argv.prompt) { + callback => { + if (false === argv.prompt) { return callback(null); } console.info('About to permanently delete the following user:'); console.info(`Username : ${user.username}`); - console.info(`Real name: ${user.properties[UserProps.RealName] || 'N/A'}`); + console.info( + `Real name: ${user.properties[UserProps.RealName] || 'N/A'}` + ); console.info(`User ID : ${user.userId}`); console.info('WARNING: This cannot be undone!'); - getAnswers([ - { - name : 'proceed', - message : `Proceed in deleting ${user.username}?`, - type : 'confirm', + getAnswers( + [ + { + name: 'proceed', + message: `Proceed in deleting ${user.username}?`, + type: 'confirm', + }, + ], + answers => { + if (answers.proceed) { + return callback(null); + } + return callback(Errors.General('User canceled')); } - ], - answers => { - if(answers.proceed) { - return callback(null); - } - return callback(Errors.General('User canceled')); - }); + ); }, - (callback) => { + callback => { // op has confirmed they are wanting ready to proceed (or passed --no-prompt) const DeleteFrom = { - message : [ 'user_message_area_last_read' ], - system : [ 'user_event_log', ], - user : [ 'user_group_member', 'user' ], - file : [ 'file_user_rating'] + message: ['user_message_area_last_read'], + system: ['user_event_log'], + user: ['user_group_member', 'user'], + file: ['file_user_rating'], }; - async.eachSeries(Object.keys(DeleteFrom), (dbName, nextDbName) => { - const tables = DeleteFrom[dbName]; - async.eachSeries(tables, (tableName, nextTableName) => { - const col = ('user' === dbName && 'user' === tableName) ? 'id' : 'user_id'; - removeUserRecordsFromDbAndTable(dbName, tableName, user.userId, col, err => { - return nextTableName(err); - }); - }, - err => { - return nextDbName(err); - }); - }, - err => { - return callback(err); - }); - }, - (callback) => { - // - // Clean up *private* messages *to* this user - // - const Message = require('../../core/message.js'); - const MsgDb = require('../../core/database.js').dbs.message; - - const filter = { - resultType : 'id', - privateTagUserId : user.userId, - }; - Message.findMessages(filter, (err, ids) => { - if(err) { - return callback(err); - } - - async.eachSeries(ids, (messageId, nextMessageId) => { - MsgDb.run( - `DELETE FROM message - WHERE message_id = ?;`, - [ messageId ], + async.eachSeries( + Object.keys(DeleteFrom), + (dbName, nextDbName) => { + const tables = DeleteFrom[dbName]; + async.eachSeries( + tables, + (tableName, nextTableName) => { + const col = + 'user' === dbName && 'user' === tableName + ? 'id' + : 'user_id'; + removeUserRecordsFromDbAndTable( + dbName, + tableName, + user.userId, + col, + err => { + return nextTableName(err); + } + ); + }, err => { - return nextMessageId(err); + return nextDbName(err); } ); }, err => { return callback(err); - }); + } + ); + }, + callback => { + // + // Clean up *private* messages *to* this user + // + const Message = require('../../core/message.js'); + const MsgDb = require('../../core/database.js').dbs.message; + + const filter = { + resultType: 'id', + privateTagUserId: user.userId, + }; + Message.findMessages(filter, (err, ids) => { + if (err) { + return callback(err); + } + + async.eachSeries( + ids, + (messageId, nextMessageId) => { + MsgDb.run( + `DELETE FROM message + WHERE message_id = ?;`, + [messageId], + err => { + return nextMessageId(err); + } + ); + }, + err => { + return callback(err); + } + ); }); - } + }, ], err => { - if(err) { + if (err) { return console.error(err.reason ? err.reason : err.message); } @@ -234,7 +256,7 @@ function removeUser(user) { } function renameUser(user) { - if(argv._.length < 3) { + if (argv._.length < 3) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } @@ -242,25 +264,27 @@ function renameUser(user) { async.series( [ - (callback) => { - const { validateUserNameAvail } = require('../../core/system_view_validate.js'); + callback => { + const { + validateUserNameAvail, + } = require('../../core/system_view_validate.js'); return validateUserNameAvail(newUserName, callback); }, - (callback) => { + callback => { const userDb = require('../../core/database.js').dbs.user; userDb.run( `UPDATE user SET user_name = ? WHERE id = ?;`, - [ newUserName, user.userId, ], + [newUserName, user.userId], err => { return callback(err); } ); - } + }, ], err => { - if(err) { + if (err) { return console.error(err.reason ? err.reason : err.message); } return console.info(`User "${user.username}" renamed to "${newUserName}"`); @@ -269,33 +293,33 @@ function renameUser(user) { } function modUserGroups(user) { - if(argv._.length < 3) { + 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 || '~' === action) { + if ('-' === action || '+' === action || '~' === action) { groupName = groupName.substr(1); } action = action || '+'; - if(0 === groupName.length) { + if (0 === groupName.length) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } // // Groups are currently arbitrary, so do a slight validation // - if(!/[A-Za-z0-9]+/.test(groupName)) { + if (!/[A-Za-z0-9]+/.test(groupName)) { process.exitCode = ExitCodes.BAD_ARGS; return console.error('Bad group name'); } function done(err) { - if(err) { + if (err) { process.exitCode = ExitCodes.BAD_ARGS; console.error(err.message); } else { @@ -304,7 +328,7 @@ function modUserGroups(user) { } const UserGroup = require('../../core/user_group.js'); - if('-' === action || '~' === action) { + if ('-' === action || '~' === action) { UserGroup.removeUserFromGroup(user.userId, groupName, done); } else { UserGroup.addUserToGroup(user.userId, groupName, done); @@ -312,7 +336,6 @@ function modUserGroups(user) { } function showUserInfo(user) { - const User = require('../../core/user.js'); const statusDesc = () => { @@ -352,14 +375,15 @@ Email : ${propOrNA(UserProps.EmailAddress)} Location : ${propOrNA(UserProps.Location)} Affiliations : ${propOrNA(UserProps.Affiliations)}`; let secInfo = ''; - if(argv.security) { + if (argv.security) { const otp = user.getProperty(UserProps.AuthFactor2OTP); - if(otp) { + if (otp) { const backupCodesOrNa = () => { - try - { - return JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)).join(', '); - } catch(e) { + try { + return JSON.parse( + user.getProperty(UserProps.AuthFactor2OTPBackupCodes) + ).join(', '); + } catch (e) { return 'N/A'; } }; @@ -373,7 +397,7 @@ OTP Backup : ${backupCodesOrNa()}`; } function twoFactorAuthOTP(user) { - if(argv._.length < 4) { + if (argv._.length < 4) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } @@ -386,14 +410,14 @@ function twoFactorAuthOTP(user) { let otpType = argv._[argv._.length - 1]; // shortcut for removal - if('disable' === otpType) { + if ('disable' === otpType) { const props = [ UserProps.AuthFactor2OTP, UserProps.AuthFactor2OTPSecret, UserProps.AuthFactor2OTPBackupCodes, ]; return user.removeProperties(props, err => { - if(err) { + if (err) { console.error(err.message); } else { console.info(`2FA OTP disabled for ${user.username}`); @@ -406,30 +430,37 @@ function twoFactorAuthOTP(user) { function validate(callback) { // :TODO: Prompt for if not supplied // allow aliases for OTP types - otpType = { - google : OTPTypes.GoogleAuthenticator, - hotp : OTPTypes.RFC4266_HOTP, - totp : OTPTypes.RFC6238_TOTP, - }[otpType] || otpType; + otpType = + { + google: OTPTypes.GoogleAuthenticator, + hotp: OTPTypes.RFC4266_HOTP, + totp: OTPTypes.RFC6238_TOTP, + }[otpType] || otpType; otpType = _.find(OTPTypes, t => { return t.toLowerCase() === otpType.toLowerCase(); }); - if(!otpType) { + if (!otpType) { return callback(Errors.Invalid('Invalid OTP type')); } return callback(null, otpType); }, function prepare(otpType, callback) { const otpOpts = { - username : user.username, - qrType : argv['qr-type'] || 'ascii', + username: user.username, + qrType: argv['qr-type'] || 'ascii', }; prepareOTP(otpType, otpOpts, (err, otpInfo) => { - return callback(err, Object.assign(otpInfo, { otpType, backupCodes : createBackupCodes() })); + return callback( + err, + Object.assign(otpInfo, { + otpType, + backupCodes: createBackupCodes(), + }) + ); }); }, function storeOrDisplayQR(otpInfo, callback) { - if(!argv.out || !otpInfo.qr) { + if (!argv.out || !otpInfo.qr) { return callback(null, otpInfo); } @@ -439,25 +470,27 @@ function twoFactorAuthOTP(user) { }, function persist(otpInfo, callback) { const props = { - [ UserProps.AuthFactor2OTP ] : otpInfo.otpType, - [ UserProps.AuthFactor2OTPSecret ] : otpInfo.secret, - [ UserProps.AuthFactor2OTPBackupCodes ] : JSON.stringify(otpInfo.backupCodes), + [UserProps.AuthFactor2OTP]: otpInfo.otpType, + [UserProps.AuthFactor2OTPSecret]: otpInfo.secret, + [UserProps.AuthFactor2OTPBackupCodes]: JSON.stringify( + otpInfo.backupCodes + ), }; user.persistProperties(props, err => { return callback(err, otpInfo); }); - } + }, ], (err, otpInfo) => { - if(err) { + if (err) { console.error(err.message); } else { console.info(`OTP enabled for : ${user.username}`); console.info(`Secret : ${otpInfo.secret}`); console.info(`Backup codes : ${otpInfo.backupCodes.join(', ')}`); - if(otpInfo.qr) { - if(!argv.out) { + if (otpInfo.qr) { + if (!argv.out) { console.info('--- Begin QR ---'); console.info(otpInfo.qr); console.info('--- End QR ---'); @@ -484,19 +517,17 @@ function listUsers() { } const User = require('../../core/user'); - if (![ 'all' ].concat(Object.keys(User.AccountStatus)).includes(listWhat)) { + if (!['all'].concat(Object.keys(User.AccountStatus)).includes(listWhat)) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } async.waterfall( [ - (callback) => { + callback => { const UserProps = require('../../core/user_property'); const userListOpts = { - properties : [ - UserProps.AccountStatus, - ], + properties: [UserProps.AccountStatus], }; User.getUserList(userListOpts, (err, userList) => { @@ -510,9 +541,12 @@ function listUsers() { const accountStatusFilter = User.AccountStatus[listWhat].toString(); - return callback(null, userList.filter(user => { - return user[UserProps.AccountStatus] === accountStatusFilter; - })); + return callback( + null, + userList.filter(user => { + return user[UserProps.AccountStatus] === accountStatusFilter; + }) + ); }); }, (userList, callback) => { @@ -524,7 +558,7 @@ function listUsers() { }, ], err => { - if(err) { + if (err) { return console.error(err.reason ? err.reason : err.message); } } @@ -532,63 +566,72 @@ function listUsers() { } function handleUserCommand() { - function errUsage() { + function errUsage() { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); } - if(true === argv.help) { + if (true === argv.help) { return errUsage(); } const action = argv._[1]; - const userRequired = ![ 'list' ].includes(action); + const userRequired = !['list'].includes(action); let userName; if (userRequired) { const usernameIdx = [ - 'pw', 'pass', 'passwd', 'password', + 'pw', + 'pass', + 'passwd', + 'password', 'group', - 'mv', 'rename', - '2fa-otp', 'otp' - ].includes(action) ? argv._.length - 2 : argv._.length - 1; + 'mv', + 'rename', + '2fa-otp', + 'otp', + ].includes(action) + ? argv._.length - 2 + : argv._.length - 1; userName = argv._[usernameIdx]; } - if(!userName && userRequired) { + if (!userName && userRequired) { return errUsage(); } initAndGetUser(userName, (err, user) => { - if(userName && err) { + if (userName && err) { process.exitCode = ExitCodes.ERROR; return console.error(err.message); } - return ({ - pw : setUserPassword, - passwd : setUserPassword, - password : setUserPassword, + return ( + { + pw: setUserPassword, + passwd: setUserPassword, + password: setUserPassword, - rm : removeUser, - remove : removeUser, - del : removeUser, - delete : removeUser, + rm: removeUser, + remove: removeUser, + del: removeUser, + delete: removeUser, - mv : renameUser, - rename : renameUser, + mv: renameUser, + rename: renameUser, - activate : setAccountStatus, - deactivate : setAccountStatus, - disable : setAccountStatus, - lock : setAccountStatus, + activate: setAccountStatus, + deactivate: setAccountStatus, + disable: setAccountStatus, + lock: setAccountStatus, - group : modUserGroups, + group: modUserGroups, - info : showUserInfo, + info: showUserInfo, - '2fa-otp' : twoFactorAuthOTP, - otp : twoFactorAuthOTP, - list : listUsers, - }[action] || errUsage)(user, action); + '2fa-otp': twoFactorAuthOTP, + otp: twoFactorAuthOTP, + list: listUsers, + }[action] || errUsage + )(user, action); }); -} \ No newline at end of file +} diff --git a/core/plugin_module.js b/core/plugin_module.js index 60b878aa..9abfffad 100644 --- a/core/plugin_module.js +++ b/core/plugin_module.js @@ -1,7 +1,6 @@ /* jslint node: true */ 'use strict'; -exports.PluginModule = PluginModule; +exports.PluginModule = PluginModule; -function PluginModule(/*options*/) { -} +function PluginModule(/*options*/) {} diff --git a/core/predefined_mci.js b/core/predefined_mci.js index a1182a79..8ce1dcee 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -2,47 +2,47 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const Log = require('./logger.js').log; -const { - getMessageAreaByTag, - getMessageConferenceByTag -} = require('./message_area.js'); -const clientConnections = require('./client_connections.js'); -const StatLog = require('./stat_log.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const { - formatByteSize, -} = require('./string_util.js'); -const ANSI = require('./ansi_term.js'); -const UserProps = require('./user_property.js'); -const SysProps = require('./system_property.js'); -const SysLogKeys = require('./system_log.js'); +const Config = require('./config.js').get; +const Log = require('./logger.js').log; +const { getMessageAreaByTag, getMessageConferenceByTag } = require('./message_area.js'); +const clientConnections = require('./client_connections.js'); +const StatLog = require('./stat_log.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const { formatByteSize } = require('./string_util.js'); +const ANSI = require('./ansi_term.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); +const SysLogKeys = require('./system_log.js'); // deps -const packageJson = require('../package.json'); -const os = require('os'); -const _ = require('lodash'); -const moment = require('moment'); +const packageJson = require('../package.json'); +const os = require('os'); +const _ = require('lodash'); +const moment = require('moment'); -exports.getPredefinedMCIValue = getPredefinedMCIValue; -exports.init = init; +exports.getPredefinedMCIValue = getPredefinedMCIValue; +exports.init = init; function init(cb) { setNextRandomRumor(cb); } function setNextRandomRumor(cb) { - StatLog.getSystemLogEntries(SysLogKeys.UserAddedRumorz, StatLog.Order.Random, 1, (err, entry) => { - if(entry) { - entry = entry[0]; + StatLog.getSystemLogEntries( + SysLogKeys.UserAddedRumorz, + StatLog.Order.Random, + 1, + (err, entry) => { + if (entry) { + entry = entry[0]; + } + const randRumor = entry && entry.log_value ? entry.log_value : ''; + StatLog.setNonPersistentSystemStat(SysProps.NextRandomRumor, randRumor); + if (cb) { + return cb(null); + } } - const randRumor = entry && entry.log_value ? entry.log_value : ''; - StatLog.setNonPersistentSystemStat(SysProps.NextRandomRumor, randRumor); - if(cb) { - return cb(null); - } - }); + ); } function getUserRatio(client, propA, propB) { @@ -73,105 +73,202 @@ const PREDEFINED_MCI_GENERATORS = { // // Board // - BN : function boardName() { return Config().general.boardName; }, + BN: function boardName() { + return Config().general.boardName; + }, // ENiGMA - VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, - VN : function version() { return packageJson.version; }, + VL: function versionLabel() { + return 'ENiGMA½ v' + packageJson.version; + }, + VN: function version() { + return packageJson.version; + }, // +op info - SN : function opUserName() { return StatLog.getSystemStat(SysProps.SysOpUsername); }, - SR : function opRealName() { return StatLog.getSystemStat(SysProps.SysOpRealName); }, - SL : function opLocation() { return StatLog.getSystemStat(SysProps.SysOpLocation); }, - SA : function opAffils() { return StatLog.getSystemStat(SysProps.SysOpAffiliations); }, - SS : function opSex() { return StatLog.getSystemStat(SysProps.SysOpSex); }, - SE : function opEmail() { return StatLog.getSystemStat(SysProps.SysOpEmailAddress); }, + SN: function opUserName() { + return StatLog.getSystemStat(SysProps.SysOpUsername); + }, + SR: function opRealName() { + return StatLog.getSystemStat(SysProps.SysOpRealName); + }, + SL: function opLocation() { + return StatLog.getSystemStat(SysProps.SysOpLocation); + }, + SA: function opAffils() { + return StatLog.getSystemStat(SysProps.SysOpAffiliations); + }, + SS: function opSex() { + return StatLog.getSystemStat(SysProps.SysOpSex); + }, + SE: function opEmail() { + return StatLog.getSystemStat(SysProps.SysOpEmailAddress); + }, // :TODO: op age, web, ????? // // 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, UserProps.RealName, ''); }, - LO : function location(client) { return userStatAsString(client, UserProps.Location, ''); }, - UA : function age(client) { return client.user.getAge().toString(); }, - BD : function birthdate(client) { // iNiQUiTY - return moment(client.user.properties[UserProps.Birthdate]).format(client.currentTheme.helpers.getDateFormat()); + UN: function userName(client) { + return client.user.username; }, - US : function sex(client) { return userStatAsString(client, UserProps.Sex, ''); }, - UE : function emailAddress(client) { return userStatAsString(client, UserProps.EmailAddress, ''); }, - UW : function webAddress(client) { return userStatAsString(client, UserProps.WebAddress, ''); }, - UF : function affils(client) { return userStatAsString(client, UserProps.Affiliations, ''); }, - UT : function themeName(client) { - return _.get(client, 'currentTheme.info.name', userStatAsString(client, UserProps.ThemeId, '')); + UI: function userId(client) { + return client.user.userId.toString(); }, - UD : function themeId(client) { return userStatAsString(client, UserProps.ThemeId, ''); }, - UC : function loginCount(client) { return userStatAsCountString(client, UserProps.LoginCount, 0); }, - ND : function connectedNode(client) { return client.node.toString(); }, - IP : function clientIpAddress(client) { return client.remoteAddress.replace(/^::ffff:/, ''); }, // convert any :ffff: IPv4's to 32bit version - ST : function serverName(client) { return client.session.serverName; }, - FN : function activeFileBaseFilterName(client) { + UG: function groups(client) { + return _.values(client.user.groups).join(', '); + }, + UR: function realName(client) { + return userStatAsString(client, UserProps.RealName, ''); + }, + LO: function location(client) { + return userStatAsString(client, UserProps.Location, ''); + }, + UA: function age(client) { + return client.user.getAge().toString(); + }, + BD: function birthdate(client) { + // iNiQUiTY + return moment(client.user.properties[UserProps.Birthdate]).format( + client.currentTheme.helpers.getDateFormat() + ); + }, + US: function sex(client) { + return userStatAsString(client, UserProps.Sex, ''); + }, + UE: function emailAddress(client) { + return userStatAsString(client, UserProps.EmailAddress, ''); + }, + UW: function webAddress(client) { + return userStatAsString(client, UserProps.WebAddress, ''); + }, + UF: function affils(client) { + return userStatAsString(client, UserProps.Affiliations, ''); + }, + UT: function themeName(client) { + return _.get( + client, + 'currentTheme.info.name', + userStatAsString(client, UserProps.ThemeId, '') + ); + }, + UD: function themeId(client) { + return userStatAsString(client, UserProps.ThemeId, ''); + }, + UC: function loginCount(client) { + return userStatAsCountString(client, UserProps.LoginCount, 0); + }, + ND: function connectedNode(client) { + return client.node.toString(); + }, + IP: function clientIpAddress(client) { + return client.remoteAddress.replace(/^::ffff:/, ''); + }, // convert any :ffff: IPv4's to 32bit version + ST: function serverName(client) { + return client.session.serverName; + }, + FN: function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); return activeFilter ? activeFilter.name : '(Unknown)'; }, - DN : function userNumDownloads(client) { return userStatAsCountString(client, UserProps.FileDlTotalCount, 0); }, // Obv/2 - DK : function userByteDownload(client) { // Obv/2 uses DK=downloaded Kbytes + DN: function userNumDownloads(client) { + return userStatAsCountString(client, UserProps.FileDlTotalCount, 0); + }, // Obv/2 + DK: function userByteDownload(client) { + // Obv/2 uses DK=downloaded Kbytes const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileDlTotalBytes); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - UP : function userNumUploads(client) { return userStatAsCountString(client, UserProps.FileUlTotalCount, 0); }, // Obv/2 - UK : function userByteUpload(client) { // Obv/2 uses UK=uploaded Kbytes + UP: function userNumUploads(client) { + return userStatAsCountString(client, UserProps.FileUlTotalCount, 0); + }, // Obv/2 + UK: function userByteUpload(client) { + // Obv/2 uses UK=uploaded Kbytes const byteSize = StatLog.getUserStatNum(client.user, UserProps.FileUlTotalBytes); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - NR : function userUpDownRatio(client) { // Obv/2 - return getUserRatio(client, UserProps.FileUlTotalCount, UserProps.FileDlTotalCount); + NR: function userUpDownRatio(client) { + // Obv/2 + return getUserRatio( + client, + UserProps.FileUlTotalCount, + UserProps.FileDlTotalCount + ); }, - KR : function userUpDownByteRatio(client) { // Obv/2 uses KR=upload/download Kbyte ratio - return getUserRatio(client, UserProps.FileUlTotalBytes, UserProps.FileDlTotalBytes); + KR: function userUpDownByteRatio(client) { + // Obv/2 uses KR=upload/download Kbyte ratio + return getUserRatio( + client, + UserProps.FileUlTotalBytes, + UserProps.FileDlTotalBytes + ); }, - MS : function accountCreated(client) { - return moment(client.user.properties[UserProps.AccountCreated]).format(client.currentTheme.helpers.getDateFormat()); + MS: function accountCreated(client) { + return moment(client.user.properties[UserProps.AccountCreated]).format( + client.currentTheme.helpers.getDateFormat() + ); }, - PS : function userPostCount(client) { return userStatAsCountString(client, UserProps.MessagePostCount, 0); }, - PC : function userPostCallRatio(client) { return getUserRatio(client, UserProps.MessagePostCount, UserProps.LoginCount); }, - - MD : function currentMenuDescription(client) { - return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; + PS: function userPostCount(client) { + return userStatAsCountString(client, UserProps.MessagePostCount, 0); + }, + PC: function userPostCallRatio(client) { + return getUserRatio(client, UserProps.MessagePostCount, UserProps.LoginCount); }, - MA : function messageAreaName(client) { - const area = getMessageAreaByTag(client.user.properties[UserProps.MessageAreaTag]); + MD: function currentMenuDescription(client) { + return _.has(client, 'currentMenuModule.menuConfig.desc') + ? client.currentMenuModule.menuConfig.desc + : ''; + }, + + MA: function messageAreaName(client) { + const area = getMessageAreaByTag( + client.user.properties[UserProps.MessageAreaTag] + ); return area ? area.name : ''; }, - MC : function messageConfName(client) { - const conf = getMessageConferenceByTag(client.user.properties[UserProps.MessageConfTag]); + MC: function messageConfName(client) { + const conf = getMessageConferenceByTag( + client.user.properties[UserProps.MessageConfTag] + ); return conf ? conf.name : ''; }, - ML : function messageAreaDescription(client) { - const area = getMessageAreaByTag(client.user.properties[UserProps.MessageAreaTag]); + ML: function messageAreaDescription(client) { + const area = getMessageAreaByTag( + client.user.properties[UserProps.MessageAreaTag] + ); return area ? area.desc : ''; }, - CM : function messageConfDescription(client) { - const conf = getMessageConferenceByTag(client.user.properties[UserProps.MessageConfTag]); + CM: function messageConfDescription(client) { + const conf = getMessageConferenceByTag( + client.user.properties[UserProps.MessageConfTag] + ); 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(); + }, - AC : function achievementCount(client) { return userStatAsCountString(client, UserProps.AchievementTotalCount, 0); }, - AP : function achievementPoints(client) { return userStatAsCountString(client, UserProps.AchievementTotalPoints, 0); }, + AC: function achievementCount(client) { + return userStatAsCountString(client, UserProps.AchievementTotalCount, 0); + }, + AP: function achievementPoints(client) { + return userStatAsCountString(client, UserProps.AchievementTotalPoints, 0); + }, - DR : function doorRuns(client) { return userStatAsCountString(client, UserProps.DoorRunTotalCount, 0); }, - DM : function doorFriendlyRunTime(client) { + DR: function doorRuns(client) { + return userStatAsCountString(client, UserProps.DoorRunTotalCount, 0); + }, + DM: function doorFriendlyRunTime(client) { const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0; return moment.duration(minutes, 'minutes').humanize(); }, - TO : function friendlyTotalTimeOnSystem(client) { + TO: function friendlyTotalTimeOnSystem(client) { const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0; return moment.duration(minutes, 'minutes').humanize(); }, @@ -179,35 +276,44 @@ const PREDEFINED_MCI_GENERATORS = { // // Date/Time // - DT : function date(client) { return moment().format(client.currentTheme.helpers.getDateFormat()); }, - CT : function time(client) { return moment().format(client.currentTheme.helpers.getTimeFormat()) ;}, + DT: function date(client) { + return moment().format(client.currentTheme.helpers.getDateFormat()); + }, + CT: function time(client) { + return moment().format(client.currentTheme.helpers.getTimeFormat()); + }, // // OS/System Info // // https://github.com/nodejs/node-v0.x-archive/issues/25769 // - OS : function operatingSystem() { - return { - linux : 'Linux', - darwin : 'OS X', - win32 : 'Windows', - sunos : 'SunOS', - freebsd : 'FreeBSD', - android : 'Android', - openbsd : 'OpenBSD', - aix : 'IBM AIX', - }[os.platform()] || os.type(); + OS: function operatingSystem() { + return ( + { + linux: 'Linux', + darwin: 'OS X', + win32: 'Windows', + sunos: 'SunOS', + freebsd: 'FreeBSD', + android: 'Android', + openbsd: 'OpenBSD', + aix: 'IBM AIX', + }[os.platform()] || os.type() + ); }, - OA : function systemArchitecture() { return os.arch(); }, + OA: function systemArchitecture() { + return os.arch(); + }, - SC : function systemCpuModel() { + SC: function systemCpuModel() { // // Clean up CPU strings a bit for better display // - return os.cpus()[0].model - .replace(/\(R\)|\(TM\)|processor|CPU/ig, '') + return os + .cpus()[0] + .model.replace(/\(R\)|\(TM\)|processor|CPU/gi, '') .replace(/\s+(?= )/g, '') .trim(); }, @@ -215,16 +321,22 @@ const PREDEFINED_MCI_GENERATORS = { // :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; }, + 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(SysProps.LoginCount).toLocaleString(); }, - TT : function totalCallsToday() { + TC: function totalCalls() { + return StatLog.getSystemStat(SysProps.LoginCount).toLocaleString(); + }, + TT: function totalCallsToday() { return StatLog.getSystemStat(SysProps.LoginsToday).toLocaleString(); }, - RR : function randomRumor() { + RR: function randomRumor() { // start the process of picking another random one setNextRandomRumor(); @@ -236,29 +348,35 @@ const PREDEFINED_MCI_GENERATORS = { // // :TODO: DD - Today's # of downloads (iNiQUiTY) // - SD : function systemNumDownloads() { return sysStatAsString(SysProps.FileDlTotalCount, 0); }, - SO : function systemByteDownload() { + SD: function systemNumDownloads() { + return sysStatAsString(SysProps.FileDlTotalCount, 0); + }, + SO: function systemByteDownload() { const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTotalBytes); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - SU : function systemNumUploads() { return sysStatAsString(SysProps.FileUlTotalCount, 0); }, - SP : function systemByteUpload() { + SU: function systemNumUploads() { + return sysStatAsString(SysProps.FileUlTotalCount, 0); + }, + SP: function systemByteUpload() { const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTotalBytes); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - TF : function totalFilesOnSystem() { + TF: function totalFilesOnSystem() { const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); return _.get(areaStats, 'totalFiles', 0).toLocaleString(); }, - TB : function totalBytesOnSystem() { - const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); - const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); - return formatByteSize(totalBytes, true); // true=withAbbr + TB: function totalBytesOnSystem() { + const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); + const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); + return formatByteSize(totalBytes, true); // true=withAbbr }, - PT : function messagesPostedToday() { // Obv/2 + PT: function messagesPostedToday() { + // Obv/2 return sysStatAsString(SysProps.MessagesToday, 0); }, - TP : function totalMessagesOnSystem() { // Obv/2 + TP: function totalMessagesOnSystem() { + // Obv/2 return sysStatAsString(SysProps.MessageTotalCount, 0); }, @@ -268,35 +386,46 @@ const PREDEFINED_MCI_GENERATORS = { // :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 */ }, + XY: function xyHack() { + return; /* nothing */ + }, // // Various movement by N // - CF : function cursorForwardBy(client, n = 1) { return ANSI.forward(n); }, - CB : function cursorBackBy(client, n = 1) { return ANSI.back(n); }, - CU : function cursorUpBy(client, n = 1) { return ANSI.up(n); }, - CD : function cursorDownBy(client, n = 1) { return ANSI.down(n); }, + CF: function cursorForwardBy(client, n = 1) { + return ANSI.forward(n); + }, + CB: function cursorBackBy(client, n = 1) { + return ANSI.back(n); + }, + CU: function cursorUpBy(client, n = 1) { + return ANSI.up(n); + }, + CD: function cursorDownBy(client, n = 1) { + return ANSI.down(n); + }, }; function getPredefinedMCIValue(client, code, extra) { - - if(!client || !code) { + if (!client || !code) { return; } const generator = PREDEFINED_MCI_GENERATORS[code]; - if(generator) { + if (generator) { let value; try { value = generator(client, extra); - } catch(e) { - Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' ); + } catch (e) { + Log.error( + { code: code, exception: e.message }, + 'Exception caught generating predefined MCI value' + ); } return value; diff --git a/core/qwk_mail_packet.js b/core/qwk_mail_packet.js index 26d7bef2..a22be749 100644 --- a/core/qwk_mail_packet.js +++ b/core/qwk_mail_packet.js @@ -32,82 +32,82 @@ const enigmaVersion = require('../package.json').version; // see https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h const SMBTZToUTCOffset = { // US Standard - '40F0' : '-04:00', // Atlantic - '412C' : '-05:00', // Eastern - '4168' : '-06:00', // Central - '41A4' : '-07:00', // Mountain - '41E0' : '-08:00', // Pacific - '421C' : '-09:00', // Yukon - '4258' : '-10:00', // Hawaii/Alaska - '4294' : '-11:00', // Bering + '40F0': '-04:00', // Atlantic + '412C': '-05:00', // Eastern + 4168: '-06:00', // Central + '41A4': '-07:00', // Mountain + '41E0': '-08:00', // Pacific + '421C': '-09:00', // Yukon + 4258: '-10:00', // Hawaii/Alaska + 4294: '-11:00', // Bering // US Daylight - 'C0F0' : '-03:00', // Atlantic - 'C12C' : '-04:00', // Eastern - 'C168' : '-05:00', // Central - 'C1A4' : '-06:00', // Mountain - 'C1E0' : '-07:00', // Pacific - 'C21C' : '-08:00', // Yukon - 'C258' : '-09:00', // Hawaii/Alaska - 'C294' : '-10:00', // Bering + C0F0: '-03:00', // Atlantic + C12C: '-04:00', // Eastern + C168: '-05:00', // Central + C1A4: '-06:00', // Mountain + C1E0: '-07:00', // Pacific + C21C: '-08:00', // Yukon + C258: '-09:00', // Hawaii/Alaska + C294: '-10:00', // Bering // "Non-Standard" - '2294' : '-11:00', // Midway - '21E0' : '-08:00', // Vancouver - '21A4' : '-07:00', // Edmonton - '2168' : '-06:00', // Winnipeg - '212C' : '-05:00', // Bogota - '20F0' : '-04:00', // Caracas - '20B4' : '-03:00', // Rio de Janeiro - '2078' : '-02:00', // Fernando de Noronha - '203C' : '-01:00', // Azores - '1000' : '+00:00', // London - '103C' : '+01:00', // Berlin - '1078' : '+02:00', // Athens - '10B4' : '+03:00', // Moscow - '10F0' : '+04:00', // Dubai - '110E' : '+04:30', // Kabul - '112C' : '+05:00', // Karachi - '114A' : '+05:30', // Bombay - '1159' : '+05:45', // Kathmandu - '1168' : '+06:00', // Dhaka - '11A4' : '+07:00', // Bangkok - '11E0' : '+08:00', // Hong Kong - '121C' : '+09:00', // Tokyo - '1258' : '+10:00', // Sydney - '1294' : '+11:00', // Noumea - '12D0' : '+12:00', // Wellington + 2294: '-11:00', // Midway + '21E0': '-08:00', // Vancouver + '21A4': '-07:00', // Edmonton + 2168: '-06:00', // Winnipeg + '212C': '-05:00', // Bogota + '20F0': '-04:00', // Caracas + '20B4': '-03:00', // Rio de Janeiro + 2078: '-02:00', // Fernando de Noronha + '203C': '-01:00', // Azores + 1000: '+00:00', // London + '103C': '+01:00', // Berlin + 1078: '+02:00', // Athens + '10B4': '+03:00', // Moscow + '10F0': '+04:00', // Dubai + '110E': '+04:30', // Kabul + '112C': '+05:00', // Karachi + '114A': '+05:30', // Bombay + 1159: '+05:45', // Kathmandu + 1168: '+06:00', // Dhaka + '11A4': '+07:00', // Bangkok + '11E0': '+08:00', // Hong Kong + '121C': '+09:00', // Tokyo + 1258: '+10:00', // Sydney + 1294: '+11:00', // Noumea + '12D0': '+12:00', // Wellington }; -const UTCOffsetToSMBTZ = _.invert(SMBTZToUTCOffset); +const UTCOffsetToSMBTZ = _.invert(SMBTZToUTCOffset); -const QWKMessageBlockSize = 128; -const QWKHeaderTimestampFormat = 'MM-DD-YYHH:mm'; -const QWKLF = 0xe3; +const QWKMessageBlockSize = 128; +const QWKHeaderTimestampFormat = 'MM-DD-YYHH:mm'; +const QWKLF = 0xe3; const QWKMessageStatusCodes = { - UnreadPublic : ' ', - ReadPublic : '-', - UnreadPrivate : '+', - ReadPrivate : '*', - UnreadCommentToSysOp : '~', - ReadCommentToSysOp : '`', - UnreadSenderPWProtected : '%', - ReadSenderPWProtected : '^', - UnreadGroupPWProtected : '!', - ReadGroupPWProtected : '#', - PWProtectedToAll : '$', - Vote : 'V', + UnreadPublic: ' ', + ReadPublic: '-', + UnreadPrivate: '+', + ReadPrivate: '*', + UnreadCommentToSysOp: '~', + ReadCommentToSysOp: '`', + UnreadSenderPWProtected: '%', + ReadSenderPWProtected: '^', + UnreadGroupPWProtected: '!', + ReadGroupPWProtected: '#', + PWProtectedToAll: '$', + Vote: 'V', }; const QWKMessageActiveStatus = { - Active : 255, - Deleted : 226, + Active: 255, + Deleted: 226, }; const QWKNetworkTagIndicator = { - Present : '*', - NotPresent : ' ', + Present: '*', + NotPresent: ' ', }; // See the following: @@ -117,50 +117,51 @@ const QWKNetworkTagIndicator = { const MessageHeaderParser = new Parser() .endianess('little') .string('status', { - encoding : 'ascii', - length : 1, + encoding: 'ascii', + length: 1, }) - .string('num', { // message num or conf num for REP's - encoding : 'ascii', - length : 7, - formatter : n => { + .string('num', { + // message num or conf num for REP's + encoding: 'ascii', + length: 7, + formatter: n => { return parseInt(n); - } + }, }) .string('timestamp', { - encoding : 'ascii', - length : 13, + encoding: 'ascii', + length: 13, }) // these fields may be encoded in something other than ascii/CP437 .array('toName', { - type : 'uint8', - length : 25, + type: 'uint8', + length: 25, }) .array('fromName', { - type : 'uint8', - length : 25, + type: 'uint8', + length: 25, }) .array('subject', { - type : 'uint8', - length : 25, + type: 'uint8', + length: 25, }) .string('password', { - encoding : 'ascii', - length : 12, + encoding: 'ascii', + length: 12, }) .string('replyToNum', { - encoding : 'ascii', - length : 8, - formatter : n => { + encoding: 'ascii', + length: 8, + formatter: n => { return parseInt(n); - } + }, }) .string('numBlocks', { - encoding : 'ascii', - length : 6, - formatter : n => { + encoding: 'ascii', + length: 6, + formatter: n => { return parseInt(n); - } + }, }) .uint8('status2') .uint16('confNum') @@ -183,20 +184,23 @@ const replaceCharInBuffer = (buffer, search, replace) => { class QWKPacketReader extends EventEmitter { constructor( packetPath, - { mode = QWKPacketReader.Modes.Guess, keepTearAndOrigin = true } = { mode : QWKPacketReader.Modes.Guess, keepTearAndOrigin : true }) - { + { mode = QWKPacketReader.Modes.Guess, keepTearAndOrigin = true } = { + mode: QWKPacketReader.Modes.Guess, + keepTearAndOrigin: true, + } + ) { super(); this.packetPath = packetPath; - this.options = { mode, keepTearAndOrigin }; - this.temptmp = temptmp.createTrackedSession('qwkpacketreader'); + this.options = { mode, keepTearAndOrigin }; + this.temptmp = temptmp.createTrackedSession('qwkpacketreader'); } static get Modes() { return { - Guess : 'guess', // try to guess - QWK : 'qwk', // standard incoming packet - REP : 'rep', // a reply packet + Guess: 'guess', // try to guess + QWK: 'qwk', // standard incoming packet + REP: 'rep', // a reply packet }; } @@ -212,7 +216,7 @@ class QWKPacketReader extends EventEmitter { async.waterfall( [ // determine packet archive type - (callback) => { + callback => { const archiveUtil = ArchiveUtil.getInstance(); archiveUtil.detectType(this.packetPath, (err, archiveType) => { if (err) { @@ -224,7 +228,7 @@ class QWKPacketReader extends EventEmitter { }, // create a temporary location to do processing (archiveType, callback) => { - this.temptmp.mkdir( { prefix : 'enigqwkreader-'}, (err, tempDir) => { + this.temptmp.mkdir({ prefix: 'enigqwkreader-' }, (err, tempDir) => { if (err) { return callback(err); } @@ -258,68 +262,83 @@ class QWKPacketReader extends EventEmitter { const key = filename.toUpperCase(); switch (key) { - case 'MESSAGES.DAT' : // QWK - if (this.options.mode === QWKPacketReader.Modes.Guess) { + case 'MESSAGES.DAT': // QWK + if ( + this.options.mode === + QWKPacketReader.Modes.Guess + ) { this.options.mode = QWKPacketReader.Modes.QWK; } - if (this.options.mode === QWKPacketReader.Modes.QWK) { + if ( + this.options.mode === + QWKPacketReader.Modes.QWK + ) { out.messages = { filename }; } break; - case 'ID.MSG' : - if (this.options.mode === QWKPacketReader.Modes.Guess) { + case 'ID.MSG': + if ( + this.options.mode === + QWKPacketReader.Modes.Guess + ) { this.options.mode = QWKPacketReader.Modes.REP; } - if (this.options.mode === QWKPacketReader.Modes.REP) { + if ( + this.options.mode === + QWKPacketReader.Modes.REP + ) { out.messages = { filename }; } break; - case 'HEADERS.DAT' : // Synchronet + case 'HEADERS.DAT': // Synchronet out.headers = { filename }; break; - case 'VOTING.DAT' : // Synchronet + case 'VOTING.DAT': // Synchronet out.voting = { filename }; break; - case 'CONTROL.DAT' : // QWK + case 'CONTROL.DAT': // QWK out.control = { filename }; break; - case 'DOOR.ID' : // QWK + case 'DOOR.ID': // QWK out.door = { filename }; break; - case 'NETFLAGS.DAT' : // QWK + case 'NETFLAGS.DAT': // QWK out.netflags = { filename }; break; - case 'NEWFILES.DAT' : // QWK + case 'NEWFILES.DAT': // QWK out.newfiles = { filename }; break; - case 'PERSONAL.NDX' : // QWK + case 'PERSONAL.NDX': // QWK out.personal = { filename }; break; - case '000.NDX' : // QWK + case '000.NDX': // QWK out.inbox = { filename }; break; - case 'TOREADER.EXT' : // QWKE + case 'TOREADER.EXT': // QWKE out.toreader = { filename }; break; - case 'QLR.DAT' : + case 'QLR.DAT': out.qlr = { filename }; break; - default : - if (/[0-9]+\.NDX/.test(key)) { // QWK - out.pointers = out.pointers || { filenames: [] }; + default: + if (/[0-9]+\.NDX/.test(key)) { + // QWK + out.pointers = out.pointers || { + filenames: [], + }; out.pointers.filenames.push(filename); } else { out[key] = { filename }; @@ -330,19 +349,15 @@ class QWKPacketReader extends EventEmitter { return next(null, out); }, (err, packetFileInfo) => { - this.packetInfo = Object.assign( - {}, - packetFileInfo, - { - tempDir, - } - ); + this.packetInfo = Object.assign({}, packetFileInfo, { + tempDir, + }); return callback(null); } ); }); }, - (callback) => { + callback => { return this.processPacketFiles(callback); }, ], @@ -361,15 +376,15 @@ class QWKPacketReader extends EventEmitter { processPacketFiles(cb) { async.series( [ - (callback) => { + callback => { return this.readControl(callback); }, - (callback) => { + callback => { return this.readHeadersExtension(callback); }, - (callback) => { + callback => { return this.readMessages(callback); - } + }, ], err => { return cb(err); @@ -389,12 +404,15 @@ class QWKPacketReader extends EventEmitter { return cb(Errors.DoesNotExist('No control file found within QWK packet')); } - const path = paths.join(this.packetInfo.tempDir, this.packetInfo.control.filename); + const path = paths.join( + this.packetInfo.tempDir, + this.packetInfo.control.filename + ); // note that we read as UTF-8. Legacy says it should be CP437/ASCII // but this seems safer for now so conference names and the like // can be non-English for example. - fs.readFile(path, { encoding : 'utf8' }, (err, controlLines) => { + fs.readFile(path, { encoding: 'utf8' }, (err, controlLines) => { if (err) { return cb(err); } @@ -402,30 +420,47 @@ class QWKPacketReader extends EventEmitter { controlLines = splitTextAtTerms(controlLines); let state = 'header'; - const control = { confMap : {} }; + const control = { confMap: {} }; let currConfNumber; for (let lineNumber = 0; lineNumber < controlLines.length; ++lineNumber) { const line = controlLines[lineNumber].trim(); switch (lineNumber) { // first set of lines is header info - case 0 : control.bbsName = line; break; - case 1 : control.bbsLocation = line; break; - case 2 : control.bbsPhone = line; break; - case 3 : control.bbsSysOp = line; break; - case 4 : control.doorRegAndBoardID = line; break; - case 5 : control.packetCreationTime = line; break; - case 6 : control.toUser = line; break; - case 7 : break; // Qmail menu - case 8 : break; // unknown, always 0? - case 9 : break; // total messages in packet (often set to 0) - case 10 : - control.totalMessages = (parseInt(line) + 1); + case 0: + control.bbsName = line; + break; + case 1: + control.bbsLocation = line; + break; + case 2: + control.bbsPhone = line; + break; + case 3: + control.bbsSysOp = line; + break; + case 4: + control.doorRegAndBoardID = line; + break; + case 5: + control.packetCreationTime = line; + break; + case 6: + control.toUser = line; + break; + case 7: + break; // Qmail menu + case 8: + break; // unknown, always 0? + case 9: + break; // total messages in packet (often set to 0) + case 10: + control.totalMessages = parseInt(line) + 1; state = 'confNumber'; break; - default : + default: switch (state) { - case 'confNumber' : + case 'confNumber': currConfNumber = parseInt(line); if (isNaN(currConfNumber)) { state = 'news'; @@ -436,22 +471,22 @@ class QWKPacketReader extends EventEmitter { } break; - case 'confName' : + case 'confName': control.confMap[currConfNumber] = line; state = 'confNumber'; break; - case 'news' : + case 'news': control.newsFile = line; state = 'logoff'; break; - case 'logoff' : + case 'logoff': control.logoffFile = line; state = 'footer'; break; - case 'footer' : + case 'footer': // some systems append additional info; we don't care. break; } @@ -464,25 +499,37 @@ class QWKPacketReader extends EventEmitter { readHeadersExtension(cb) { if (!this.packetInfo.headers) { - return cb(null); // nothing to do + return cb(null); // nothing to do } - const path = paths.join(this.packetInfo.tempDir, this.packetInfo.headers.filename); - fs.readFile(path, { encoding : 'utf8' }, (err, iniData) => { + const path = paths.join( + this.packetInfo.tempDir, + this.packetInfo.headers.filename + ); + fs.readFile(path, { encoding: 'utf8' }, (err, iniData) => { if (err) { - this.emit('warning', Errors.Invalid(`Problem reading HEADERS.DAT: ${err.message}`)); - return cb(null); // non-fatal + this.emit( + 'warning', + Errors.Invalid(`Problem reading HEADERS.DAT: ${err.message}`) + ); + return cb(null); // non-fatal } try { const parserOptions = { - lineComment : false, // no line comments; consume full lines - nativeType : false, // just keep everything as strings - dotKey : false, // 'a.b.c = value' stays 'a.b.c = value' + lineComment: false, // no line comments; consume full lines + nativeType: false, // just keep everything as strings + dotKey: false, // 'a.b.c = value' stays 'a.b.c = value' }; - this.packetInfo.headers.ini = IniConfigParser.parse(iniData, parserOptions); + this.packetInfo.headers.ini = IniConfigParser.parse( + iniData, + parserOptions + ); } catch (e) { - this.emit('warning', Errors.Invalid(`HEADERS.DAT file appears to be invalid: ${e.message}`)); + this.emit( + 'warning', + Errors.Invalid(`HEADERS.DAT file appears to be invalid: ${e.message}`) + ); } return cb(null); @@ -497,7 +544,10 @@ class QWKPacketReader extends EventEmitter { const encodingToSpec = 'cp437'; let encoding; - const path = paths.join(this.packetInfo.tempDir, this.packetInfo.messages.filename); + const path = paths.join( + this.packetInfo.tempDir, + this.packetInfo.messages.filename + ); fs.open(path, 'r', (err, fd) => { if (err) { return cb(err); @@ -506,18 +556,18 @@ class QWKPacketReader extends EventEmitter { // Some mappings/etc. used in loops below.... // Sync sets these in HEADERS.DAT: http://wiki.synchro.net/ref:qwk const FTNPropertyMapping = { - 'X-FTN-AREA' : Message.FtnPropertyNames.FtnArea, - 'X-FTN-SEEN-BY' : Message.FtnPropertyNames.FtnSeenBy, + 'X-FTN-AREA': Message.FtnPropertyNames.FtnArea, + 'X-FTN-SEEN-BY': Message.FtnPropertyNames.FtnSeenBy, }; const FTNKludgeMapping = { - 'X-FTN-PATH' : 'PATH', - 'X-FTN-MSGID' : 'MSGID', - 'X-FTN-REPLY' : 'REPLY', - 'X-FTN-PID' : 'PID', - 'X-FTN-FLAGS' : 'FLAGS', - 'X-FTN-TID' : 'TID', - 'X-FTN-CHRS' : 'CHRS', + 'X-FTN-PATH': 'PATH', + 'X-FTN-MSGID': 'MSGID', + 'X-FTN-REPLY': 'REPLY', + 'X-FTN-PID': 'PID', + 'X-FTN-FLAGS': 'FLAGS', + 'X-FTN-TID': 'TID', + 'X-FTN-CHRS': 'CHRS', // :TODO: X-FTN-KLUDGE - not sure what this is? }; @@ -529,16 +579,16 @@ class QWKPacketReader extends EventEmitter { // const Kludges = { // QWKE - To : 'To:', - From : 'From:', - Subject : 'Subject:', + To: 'To:', + From: 'From:', + Subject: 'Subject:', // Synchronet - Via : '@VIA:', - MsgID : '@MSGID:', - Reply : '@REPLY:', - TZ : '@TZ:', // https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h - ReplyTo : '@REPLYTO:', + Via: '@VIA:', + MsgID: '@MSGID:', + Reply: '@REPLY:', + TZ: '@TZ:', // https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h + ReplyTo: '@REPLYTO:', // :TODO: Look into other non-standards // https://github.com/wmcbrine/MultiMail/blob/master/mmail/qwk.cc @@ -546,7 +596,7 @@ class QWKPacketReader extends EventEmitter { }; let blockCount = 0; - let currMessage = { }; + let currMessage = {}; let state; let messageBlocksRemain; const buffer = Buffer.alloc(QWKMessageBlockSize); @@ -565,7 +615,11 @@ class QWKPacketReader extends EventEmitter { } if (QWKMessageBlockSize !== read) { - return cb(Errors.Invalid(`Invalid QWK message block size. Expected ${QWKMessageBlockSize} got ${read}`)); + return cb( + Errors.Invalid( + `Invalid QWK message block size. Expected ${QWKMessageBlockSize} got ${read}` + ) + ); } if (0 === blockCount) { @@ -575,37 +629,49 @@ class QWKPacketReader extends EventEmitter { state = 'header'; } else { switch (state) { - case 'header' : + case 'header': { const header = MessageHeaderParser.parse(buffer); - encoding = encodingToSpec; // reset per message + encoding = encodingToSpec; // reset per message // massage into something a little more sane (things we can't quite do in the parser directly) ['toName', 'fromName', 'subject'].forEach(field => { // note: always use to-spec encoding here - header[field] = iconv.decode(header[field], encodingToSpec).trim(); + header[field] = iconv + .decode(header[field], encodingToSpec) + .trim(); }); - header.timestamp = moment(header.timestamp, QWKHeaderTimestampFormat); + header.timestamp = moment( + header.timestamp, + QWKHeaderTimestampFormat + ); currMessage = { header, // these may be overridden - toName : header.toName, - fromName : header.fromName, - subject : header.subject, + toName: header.toName, + fromName: header.fromName, + subject: header.subject, }; if (_.has(this.packetInfo, 'headers.ini')) { // Sections for a message in HEADERS.DAT are by current byte offset. // 128 = first message header = 0x80 = section [80] - const headersSectionId = (blockCount * QWKMessageBlockSize).toString(16); - currMessage.headersExtension = this.packetInfo.headers.ini[headersSectionId]; + const headersSectionId = ( + blockCount * QWKMessageBlockSize + ).toString(16); + currMessage.headersExtension = + this.packetInfo.headers.ini[headersSectionId]; } // if we have HEADERS.DAT with a 'Utf8' override for this message, // the overridden to/from/subject/message fields are UTF-8 - if (currMessage.headersExtension && 'true' === currMessage.headersExtension.Utf8.toLowerCase()) { + if ( + currMessage.headersExtension && + 'true' === + currMessage.headersExtension.Utf8.toLowerCase() + ) { encoding = 'utf8'; } @@ -615,11 +681,14 @@ class QWKPacketReader extends EventEmitter { } break; - case 'message' : + case 'message': if (!currMessage.body) { currMessage.body = Buffer.from(buffer); } else { - currMessage.body = Buffer.concat([currMessage.body, buffer]); + currMessage.body = Buffer.concat([ + currMessage.body, + buffer, + ]); } messageBlocksRemain -= 1; @@ -628,7 +697,11 @@ class QWKPacketReader extends EventEmitter { // First, replace QWK style line feeds (0xe3) unless the message is UTF-8. // If the message is UTF-8, we assume it's using standard line feeds. if (encoding !== 'utf8') { - replaceCharInBuffer(currMessage.body, QWKLF, 0x0a); + replaceCharInBuffer( + currMessage.body, + QWKLF, + 0x0a + ); } // @@ -636,7 +709,9 @@ class QWKPacketReader extends EventEmitter { // into lines so we can extract various bits such as QWKE headers, origin, tear // lines, etc. // - const messageLines = splitTextAtTerms(iconv.decode(currMessage.body, encoding).trimEnd()); + const messageLines = splitTextAtTerms( + iconv.decode(currMessage.body, encoding).trimEnd() + ); const bodyLines = []; let bodyState = 'kludge'; @@ -645,8 +720,8 @@ class QWKPacketReader extends EventEmitter { // While technically FTN oriented, these can come from any network // (though we'll be processing a lot of messages that routed through FTN // at some point) - Origin : /^[ ]{1,2}\* Origin: /, - Tear : /^--- /, + Origin: /^[ ]{1,2}\* Origin: /, + Tear: /^--- /, }; const qwkKludge = {}; @@ -659,36 +734,62 @@ class QWKPacketReader extends EventEmitter { } switch (bodyState) { - case 'kludge' : + case 'kludge': // :TODO: Update these to use the well known consts: if (line.startsWith(Kludges.To)) { - currMessage.toName = line.substring(Kludges.To.length).trim(); - } else if (line.startsWith(Kludges.From)) { - currMessage.fromName = line.substring(Kludges.From.length).trim(); - } else if (line.startsWith(Kludges.Subject)) { - currMessage.subject = line.substring(Kludges.Subject.length).trim(); + currMessage.toName = line + .substring(Kludges.To.length) + .trim(); + } else if ( + line.startsWith(Kludges.From) + ) { + currMessage.fromName = line + .substring(Kludges.From.length) + .trim(); + } else if ( + line.startsWith(Kludges.Subject) + ) { + currMessage.subject = line + .substring(Kludges.Subject.length) + .trim(); } else if (line.startsWith(Kludges.Via)) { qwkKludge['@VIA'] = line; - } else if (line.startsWith(Kludges.MsgID)) { - qwkKludge['@MSGID'] = line.substring(Kludges.MsgID.length).trim(); - } else if (line.startsWith(Kludges.Reply)) { - qwkKludge['@REPLY'] = line.substring(Kludges.Reply.length).trim(); + } else if ( + line.startsWith(Kludges.MsgID) + ) { + qwkKludge['@MSGID'] = line + .substring(Kludges.MsgID.length) + .trim(); + } else if ( + line.startsWith(Kludges.Reply) + ) { + qwkKludge['@REPLY'] = line + .substring(Kludges.Reply.length) + .trim(); } else if (line.startsWith(Kludges.TZ)) { - qwkKludge['@TZ'] = line.substring(Kludges.TZ.length).trim(); - } else if (line.startsWith(Kludges.ReplyTo)) { - qwkKludge['@REPLYTO'] = line.substring(Kludges.ReplyTo.length).trim(); + qwkKludge['@TZ'] = line + .substring(Kludges.TZ.length) + .trim(); + } else if ( + line.startsWith(Kludges.ReplyTo) + ) { + qwkKludge['@REPLYTO'] = line + .substring(Kludges.ReplyTo.length) + .trim(); } else { bodyState = 'body'; // past this point and up to any tear/origin/etc., is the real message body bodyLines.push(line); } break; - case 'body' : - case 'trailers' : + case 'body': + case 'trailers': if (MessageTrailers.Origin.test(line)) { ftnProperty.ftn_origin = line; bodyState = 'trailers'; - } else if (MessageTrailers.Tear.test(line)) { + } else if ( + MessageTrailers.Tear.test(line) + ) { ftnProperty.ftn_tear_line = line; bodyState = 'trailers'; } else if ('body' === bodyState) { @@ -705,19 +806,25 @@ class QWKPacketReader extends EventEmitter { const ext = currMessage.headersExtension; // to and subject can be overridden yet again if entries are present - currMessage.toName = ext.To || currMessage.toName; - currMessage.subject = ext.Subject || currMessage.subject; - currMessage.from = ext.Sender || currMessage.fromName; // why not From? Who the fuck knows. + currMessage.toName = ext.To || currMessage.toName; + currMessage.subject = + ext.Subject || currMessage.subject; + currMessage.from = + ext.Sender || currMessage.fromName; // why not From? Who the fuck knows. // possibly override message ID kludge - qwkKludge['@MSGID'] = ext['Message-ID'] || qwkKludge['@MSGID']; + qwkKludge['@MSGID'] = + ext['Message-ID'] || qwkKludge['@MSGID']; // WhenWritten contains a ISO-8601-ish timestamp and a Synchronet/SMB style TZ offset: // 20180101174837-0600 4168 // We can use this to get a very slightly better precision on the timestamp (addition of seconds) // over the headers value. Why not milliseconds? Who the fuck knows. if (ext.WhenWritten) { - const whenWritten = moment(ext.WhenWritten, 'YYYYMMDDHHmmssZ'); + const whenWritten = moment( + ext.WhenWritten, + 'YYYYMMDDHHmmssZ' + ); if (whenWritten.isValid()) { messageTimestamp = whenWritten; useTZKludge = false; @@ -725,18 +832,23 @@ class QWKPacketReader extends EventEmitter { } if (ext.Tags) { - currMessage.hashTags = (ext.Tags).toString().split(' '); + currMessage.hashTags = + ext.Tags.toString().split(' '); } // FTN style properties/kludges represented as X-FTN-XXXX - for (let [extName, propName] of Object.entries(FTNPropertyMapping)) { + for (let [extName, propName] of Object.entries( + FTNPropertyMapping + )) { const v = ext[extName]; if (v) { ftnProperty[propName] = v; } } - for (let [extName, kludgeName] of Object.entries(FTNKludgeMapping)) { + for (let [extName, kludgeName] of Object.entries( + FTNKludgeMapping + )) { const v = ext[extName]; if (v) { ftnKludge[kludgeName] = v; @@ -745,16 +857,18 @@ class QWKPacketReader extends EventEmitter { } const message = new Message({ - toUserName : currMessage.toName, - fromUserName : currMessage.fromName, - subject : currMessage.subject, - modTimestamp : messageTimestamp, - message : bodyLines.join('\n'), - hashTags : currMessage.hashTags, + toUserName: currMessage.toName, + fromUserName: currMessage.fromName, + subject: currMessage.subject, + modTimestamp: messageTimestamp, + message: bodyLines.join('\n'), + hashTags: currMessage.hashTags, }); // Indicate this message was imported from a QWK packet - message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.QWK; + message.meta.System[ + Message.SystemMetaNames.ExternalFlavor + ] = Message.AddressFlavor.QWK; if (!_.isEmpty(qwkKludge)) { message.meta.QwkKludge = qwkKludge; @@ -781,28 +895,36 @@ class QWKPacketReader extends EventEmitter { // Update the timestamp if we have a valid TZ if (useTZKludge && qwkKludge['@TZ']) { - const tzOffset = SMBTZToUTCOffset[qwkKludge['@TZ']]; + const tzOffset = + SMBTZToUTCOffset[qwkKludge['@TZ']]; if (tzOffset) { message.modTimestamp.utcOffset(tzOffset); } } message.meta.QwkProperty = { - qwk_msg_status : currMessage.header.status, - qwk_in_reply_to_num : currMessage.header.replyToNum, + qwk_msg_status: currMessage.header.status, + qwk_in_reply_to_num: + currMessage.header.replyToNum, }; if (this.options.mode === QWKPacketReader.Modes.QWK) { - message.meta.QwkProperty.qwk_msg_num = currMessage.header.num; - message.meta.QwkProperty.qwk_conf_num = currMessage.header.confNum; + message.meta.QwkProperty.qwk_msg_num = + currMessage.header.num; + message.meta.QwkProperty.qwk_conf_num = + currMessage.header.confNum; } else { // For REP's, prefer the larger field. - message.meta.QwkProperty.qwk_conf_num = currMessage.header.num || currMessage.header.confNum; + message.meta.QwkProperty.qwk_conf_num = + currMessage.header.num || + currMessage.header.confNum; } // Another quick HEADERS.DAT fix-up if (currMessage.headersExtension) { - message.meta.QwkProperty.qwk_conf_num = currMessage.headersExtension.Conference || message.meta.QwkProperty.qwk_conf_num; + message.meta.QwkProperty.qwk_conf_num = + currMessage.headersExtension.Conference || + message.meta.QwkProperty.qwk_conf_num; } this.emit('message', message); @@ -835,8 +957,8 @@ class QWKPacketWriter extends EventEmitter { user = null, archiveFormat = 'application/zip', forceEncoding = null, - } = QWKPacketWriter.DefaultOptions) - { + } = QWKPacketWriter.DefaultOptions + ) { super(); this.options = { @@ -848,7 +970,7 @@ class QWKPacketWriter extends EventEmitter { bbsID, user, archiveFormat, - forceEncoding : forceEncoding ? forceEncoding.toLowerCase() : null, + forceEncoding: forceEncoding ? forceEncoding.toLowerCase() : null, }; this.temptmp = temptmp.createTrackedSession('qwkpacketwriter'); @@ -858,38 +980,38 @@ class QWKPacketWriter extends EventEmitter { static get DefaultOptions() { return { - mode : QWKPacketWriter.Modes.User, - enableQWKE : true, - enableHeadersExtension : true, - enableAtKludges : true, - systemDomain : 'enigma-bbs', - bbsID : 'ENIGMA', - user : null, - archiveFormat :'application/zip', - forceEncoding : null, + mode: QWKPacketWriter.Modes.User, + enableQWKE: true, + enableHeadersExtension: true, + enableAtKludges: true, + systemDomain: 'enigma-bbs', + bbsID: 'ENIGMA', + user: null, + archiveFormat: 'application/zip', + forceEncoding: null, }; } static get Modes() { return { - User : 'user', // creation of a packet for a user (non-network); non-mapped confs allowed - Network : 'network', // creation of a packet for QWK network + User: 'user', // creation of a packet for a user (non-network); non-mapped confs allowed + Network: 'network', // creation of a packet for QWK network }; } init() { async.series( [ - (callback) => { + callback => { return StatLog.init(callback); }, - (callback) => { - this.temptmp.mkdir( { prefix : 'enigqwkwriter-'}, (err, workDir) => { + callback => { + this.temptmp.mkdir({ prefix: 'enigqwkwriter-' }, (err, workDir) => { this.workDir = workDir; return callback(err); }); }, - (callback) => { + callback => { // // Prepare areaTag -> conference number mapping: // - In User mode, areaTags's that are not explicitly configured @@ -911,19 +1033,28 @@ class QWKPacketWriter extends EventEmitter { // All the rest // Start at 1000 to work around what seems to be a bug with some readers let confNumber = 1000; - const usedConfNumbers = new Set(Object.values(this.areaTagConfMap)); + const usedConfNumbers = new Set( + Object.values(this.areaTagConfMap) + ); getAllAvailableMessageAreaTags().forEach(areaTag => { if (this.areaTagConfMap[areaTag]) { return; } - while (confNumber < 10001 && usedConfNumbers.has(confNumber)) { + while ( + confNumber < 10001 && + usedConfNumbers.has(confNumber) + ) { ++confNumber; } // we can go up to 65535 for some things, but NDX files are limited to 9999 - if (confNumber === 10000) { // sanity... - this.emit('warning', Errors.General('To many conferences (over 9999)')); + if (confNumber === 10000) { + // sanity... + this.emit( + 'warning', + Errors.General('To many conferences (over 9999)') + ); } else { this.areaTagConfMap[areaTag] = confNumber; ++confNumber; @@ -933,22 +1064,29 @@ class QWKPacketWriter extends EventEmitter { return callback(null); }, - (callback) => { - this.messagesStream = fs.createWriteStream(paths.join(this.workDir, 'messages.dat')); + callback => { + this.messagesStream = fs.createWriteStream( + paths.join(this.workDir, 'messages.dat') + ); if (this.options.enableHeadersExtension) { - this.headersDatStream = fs.createWriteStream(paths.join(this.workDir, 'headers.dat')); + this.headersDatStream = fs.createWriteStream( + paths.join(this.workDir, 'headers.dat') + ); } // First block is a space padded ID const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2022 Bryan Ashby`; - this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii'); + this.messagesStream.write( + id.padEnd(QWKMessageBlockSize, ' '), + 'ascii' + ); this.currentMessageOffset = QWKMessageBlockSize; this.totalMessages = 0; this.areaTagsSeen = new Set(); - this.personalIndex = []; // messages addressed to 'user' - this.inboxIndex = []; // private messages for 'user' + this.personalIndex = []; // messages addressed to 'user' + this.inboxIndex = []; // private messages for 'user' this.publicIndex = new Map(); return callback(null); @@ -972,7 +1110,12 @@ class QWKPacketWriter extends EventEmitter { try { return iconv.encode(s, encoding); } catch (e) { - this.emit('warning', Errors.General(`Failed to encode buffer using ${encoding}; Falling back to 'ascii'`)); + this.emit( + 'warning', + Errors.General( + `Failed to encode buffer using ${encoding}; Falling back to 'ascii'` + ) + ); return iconv.encode(s, 'ascii'); } } @@ -1008,7 +1151,10 @@ class QWKPacketWriter extends EventEmitter { if (this.options.enableAtKludges) { // Add in original kludges (perhaps in a different order) if // they were originally imported - if (Message.AddressFlavor.QWK == message.meta.System[Message.SystemMetaNames.ExternalFlavor]) { + if ( + Message.AddressFlavor.QWK == + message.meta.System[Message.SystemMetaNames.ExternalFlavor] + ) { if (message.meta.QwkKludge) { for (let [kludge, value] of Object.entries(message.meta.QwkKludge)) { fullMessageBody += `${kludge}: ${value}\n`; @@ -1041,16 +1187,13 @@ class QWKPacketWriter extends EventEmitter { // Messages must comprise of multiples of 128 bit blocks with the last // block padded by spaces or nulls (we use nulls) - const fullBlocks = Math.trunc(encodedMessage.length / QWKMessageBlockSize); - const remainBytes = QWKMessageBlockSize - (encodedMessage.length % QWKMessageBlockSize); - const totalBlocks = fullBlocks + 1 + (remainBytes ? 1 : 0); + const fullBlocks = Math.trunc(encodedMessage.length / QWKMessageBlockSize); + const remainBytes = + QWKMessageBlockSize - (encodedMessage.length % QWKMessageBlockSize); + const totalBlocks = fullBlocks + 1 + (remainBytes ? 1 : 0); // The first block is always a header - if (!this._writeMessageHeader( - message, - totalBlocks - )) - { + if (!this._writeMessageHeader(message, totalBlocks)) { // we can't write this message return; } @@ -1109,9 +1252,7 @@ class QWKPacketWriter extends EventEmitter { _messageAddressedToUser(message) { if (_.isUndefined(this.cachedCompareNames)) { if (this.options.user) { - this.cachedCompareNames = [ - this.options.user.username.toLowerCase() - ]; + this.cachedCompareNames = [this.options.user.username.toLowerCase()]; const realName = this.options.user.getProperty(UserProps.RealName); if (realName) { this.cachedCompareNames.push(realName.toLowerCase()); @@ -1126,7 +1267,7 @@ class QWKPacketWriter extends EventEmitter { _updateIndexTracking(message) { // index points at start of *message* not the header for... reasons? - const index = (this.currentMessageOffset / QWKMessageBlockSize) + 1; + const index = this.currentMessageOffset / QWKMessageBlockSize + 1; if (message.isPrivate()) { this.inboxIndex.push(index); } else { @@ -1144,20 +1285,18 @@ class QWKPacketWriter extends EventEmitter { } } - appendNewFile() { - - } + appendNewFile() {} finish(packetDirectory) { async.series( [ - (callback) => { + callback => { this.messagesStream.on('close', () => { return callback(null); }); this.messagesStream.end(); }, - (callback) => { + callback => { if (!this.headersDatStream) { return callback(null); } @@ -1166,15 +1305,15 @@ class QWKPacketWriter extends EventEmitter { }); this.headersDatStream.end(); }, - (callback) => { + callback => { return this._createControlData(callback); }, - (callback) => { + callback => { return this._createIndexes(callback); }, - (callback) => { + callback => { return this._producePacketArchive(packetDirectory, callback); - } + }, ], err => { this.temptmp.cleanup(); @@ -1194,35 +1333,41 @@ class QWKPacketWriter extends EventEmitter { // start with .QWK -> .QW1 ... .QW9 -> .Q10 ... .Q99 // let digits = 0; - async.doWhilst( callback => { - let ext; - if (0 === digits) { - ext = 'QWK'; - } else if (digits < 10) { - ext = `QW${digits}`; - } else if (digits < 100) { - ext = `Q${digits}`; - } else { - return callback(Errors.UnexpectedState('Unable to choose a valid QWK output filename')); - } - - ++digits; - - const filename = `${this.options.bbsID}.${ext}`; - fs.stat(paths.join(packetDirectory, filename), err => { - if (err && 'ENOENT' === err.code) { - return callback(null, filename); + async.doWhilst( + callback => { + let ext; + if (0 === digits) { + ext = 'QWK'; + } else if (digits < 10) { + ext = `QW${digits}`; + } else if (digits < 100) { + ext = `Q${digits}`; } else { - return callback(null, null); + return callback( + Errors.UnexpectedState( + 'Unable to choose a valid QWK output filename' + ) + ); } - }); - }, - (filename, callback) => { - return callback(null, filename ? false : true); - }, - (err, filename) => { - return cb(err, filename); - }); + + ++digits; + + const filename = `${this.options.bbsID}.${ext}`; + fs.stat(paths.join(packetDirectory, filename), err => { + if (err && 'ENOENT' === err.code) { + return callback(null, filename); + } else { + return callback(null, null); + } + }); + }, + (filename, callback) => { + return callback(null, filename ? false : true); + }, + (err, filename) => { + return cb(err, filename); + } + ); } _producePacketArchive(packetDirectory, cb) { @@ -1247,7 +1392,7 @@ class QWKPacketWriter extends EventEmitter { () => { fs.stat(packetPath, (err, stats) => { if (stats) { - this.emit('packet', { stats, path : packetPath } ); + this.emit('packet', { stats, path: packetPath }); } return cb(err); }); @@ -1285,26 +1430,38 @@ class QWKPacketWriter extends EventEmitter { return false; } - const conferenceNumber = this._getMessageConferenceNumberByAreaTag(message.areaTag); + const conferenceNumber = this._getMessageConferenceNumberByAreaTag( + message.areaTag + ); if (isNaN(conferenceNumber)) { - this.emit('warning', Errors.MissingConfig(`No QWK conference mapping for areaTag ${message.areaTag}`)); + this.emit( + 'warning', + Errors.MissingConfig( + `No QWK conference mapping for areaTag ${message.areaTag}` + ) + ); return false; } const header = Buffer.alloc(QWKMessageBlockSize, ' '); header.write(this._qwkMessageStatus(message), 0, 1, 'ascii'); header.write(asciiNum(message.messageId), 1, 'ascii'); - header.write(message.modTimestamp.format(QWKHeaderTimestampFormat), 8, 13, 'ascii'); + header.write( + message.modTimestamp.format(QWKHeaderTimestampFormat), + 8, + 13, + 'ascii' + ); header.write(message.toUserName.substr(0, 25), 21, 'ascii'); header.write(message.fromUserName.substr(0, 25), 46, 'ascii'); header.write(message.subject.substr(0, 25), 71, 'ascii'); - header.write(' '.repeat(12), 96, 'ascii'); // we don't use the password field + header.write(' '.repeat(12), 96, 'ascii'); // we don't use the password field header.write(asciiNum(message.replyToMsgId), 108, 'ascii'); header.write(asciiTotalBlocks, 116, 'ascii'); header.writeUInt8(QWKMessageActiveStatus.Active, 122); header.writeUInt16LE(conferenceNumber, 123); header.writeUInt16LE(this.totalMessages + 1, 125); - header.write(QWKNetworkTagIndicator.NotPresent, 127, 1, 'ascii'); // :TODO: Present if for network output? + header.write(QWKNetworkTagIndicator.NotPresent, 127, 1, 'ascii'); // :TODO: Present if for network output? this.messagesStream.write(header); @@ -1331,15 +1488,17 @@ class QWKPacketWriter extends EventEmitter { const areas = Array.from(this.areaTagsSeen).map(areaTag => { if (Message.isPrivateAreaTag(areaTag)) { return { - areaTag : Message.WellKnownAreaTags.Private, - name : 'Private', - desc : 'Private Messages', + areaTag: Message.WellKnownAreaTags.Private, + name: 'Private', + desc: 'Private Messages', }; } return getMessageAreaByTag(areaTag); }); - const controlStream = fs.createWriteStream(paths.join(this.workDir, 'control.dat')); + const controlStream = fs.createWriteStream( + paths.join(this.workDir, 'control.dat') + ); controlStream.setDefaultEncoding('ascii'); controlStream.on('close', () => { @@ -1358,8 +1517,8 @@ class QWKPacketWriter extends EventEmitter { `0000,${this.options.bbsID}`, moment().format('MM-DD-YYYY,HH:mm:ss'), this._getExportForUsername(), - '', // name of Qmail menu - '0', // uh, OK + '', // name of Qmail menu + '0', // uh, OK this.totalMessages.toString(), // this next line is total conferences - 1: // We have areaTag <> conference mapping, so the number should work out @@ -1372,7 +1531,9 @@ class QWKPacketWriter extends EventEmitter { // map areas as conf #\r\nDescription\r\n pairs areas.forEach(area => { - const conferenceNumber = this._getMessageConferenceNumberByAreaTag(area.areaTag); + const conferenceNumber = this._getMessageConferenceNumberByAreaTag( + area.areaTag + ); const conf = getMessageConferenceByTag(area.confTag); const desc = `${conf.name} - ${area.name}`; @@ -1400,14 +1561,18 @@ class QWKPacketWriter extends EventEmitter { async.series( [ - (callback) => { + callback => { // Create PERSONAL.NDX if (!this.personalIndex.length) { return callback(null); } - const indexStream = fs.createWriteStream(paths.join(this.workDir, 'personal.ndx')); - this.personalIndex.forEach(offset => appendIndexData(indexStream, offset)); + const indexStream = fs.createWriteStream( + paths.join(this.workDir, 'personal.ndx') + ); + this.personalIndex.forEach(offset => + appendIndexData(indexStream, offset) + ); indexStream.on('close', err => { return callback(err); @@ -1415,14 +1580,18 @@ class QWKPacketWriter extends EventEmitter { indexStream.end(); }, - (callback) => { + callback => { // 000.NDX of private mails if (!this.inboxIndex.length) { return callback(null); } - const indexStream = fs.createWriteStream(paths.join(this.workDir, '000.ndx')); - this.inboxIndex.forEach(offset => appendIndexData(indexStream, offset)); + const indexStream = fs.createWriteStream( + paths.join(this.workDir, '000.ndx') + ); + this.inboxIndex.forEach(offset => + appendIndexData(indexStream, offset) + ); indexStream.on('close', err => { return callback(err); @@ -1430,24 +1599,35 @@ class QWKPacketWriter extends EventEmitter { indexStream.end(); }, - (callback) => { + callback => { // ####.NDX - async.eachSeries(this.publicIndex.keys(), (areaTag, nextArea) => { - const offsets = this.publicIndex.get(areaTag); - const conferenceNumber = this._getMessageConferenceNumberByAreaTag(areaTag); - const indexStream = fs.createWriteStream(paths.join(this.workDir, `${conferenceNumber.toString().padStart(4, '0')}.ndx`)); - offsets.forEach(offset => appendIndexData(indexStream, offset)); + async.eachSeries( + this.publicIndex.keys(), + (areaTag, nextArea) => { + const offsets = this.publicIndex.get(areaTag); + const conferenceNumber = + this._getMessageConferenceNumberByAreaTag(areaTag); + const indexStream = fs.createWriteStream( + paths.join( + this.workDir, + `${conferenceNumber.toString().padStart(4, '0')}.ndx` + ) + ); + offsets.forEach(offset => + appendIndexData(indexStream, offset) + ); - indexStream.on('close', err => { - return nextArea(err); - }); + indexStream.on('close', err => { + return nextArea(err); + }); - indexStream.end(); - }, - err => { - return callback(err); - }); - } + indexStream.end(); + }, + err => { + return callback(err); + } + ); + }, ], err => { return cb(err); @@ -1457,76 +1637,87 @@ class QWKPacketWriter extends EventEmitter { _makeSynchronetTimestamp(ts) { const syncTimestamp = ts.format('YYYYMMDDHHmmssZZ'); - const syncTZ = UTCOffsetToSMBTZ[ts.format('Z')] || '0000'; // :TODO: what if we don't have a map? + const syncTZ = UTCOffsetToSMBTZ[ts.format('Z')] || '0000'; // :TODO: what if we don't have a map? return `${syncTimestamp} ${syncTZ}`; } _appendHeadersExtensionData(message, encoding) { const messageData = { // Synchronet style - Utf8 : ('utf8' === encoding ? 'true' : 'false'), - 'Message-ID' : this.makeMessageIdentifier(message), + Utf8: 'utf8' === encoding ? 'true' : 'false', + 'Message-ID': this.makeMessageIdentifier(message), - WhenWritten : this._makeSynchronetTimestamp(message.modTimestamp), + WhenWritten: this._makeSynchronetTimestamp(message.modTimestamp), // WhenImported : '', // :TODO: only if we have a imported time from another external system? - ExportedFrom : `${this.options.systemID} ${message.areaTag} ${message.messageId}`, - Sender : message.fromUserName, + ExportedFrom: `${this.options.systemID} ${message.areaTag} ${message.messageId}`, + Sender: message.fromUserName, // :TODO: if exporting for QWK-Net style/etc. //SenderNetAddr - SenderIpAddr : '127.0.0.1', // no sir, that's private. - SenderHostName : this.options.systemDomain, + SenderIpAddr: '127.0.0.1', // no sir, that's private. + SenderHostName: this.options.systemDomain, // :TODO: if exported: //SenderProtocol - Organization : 'BBS', + Organization: 'BBS', //'Reply-To' : :TODO: "address to direct replies".... ?! - Subject : message.subject, - To : message.toUserName, + Subject: message.subject, + To: message.toUserName, //ToNetAddr : :TODO: net addr to?! // :TODO: Only set if not imported: - Tags : message.hashTags.join(' '), + Tags: message.hashTags.join(' '), // :TODO: Needs tested with Sync/etc.; Sync wants Conference *numbers* - Conference : message.isPrivate() ? '0' : getMessageConfTagByAreaTag(message.areaTag), + Conference: message.isPrivate() + ? '0' + : getMessageConfTagByAreaTag(message.areaTag), // ENiGMA Headers - MessageUUID : message.messageUuid, - ModTimestamp : message.modTimestamp.format('YYYY-MM-DDTHH:mm:ss.SSSZ'), - AreaTag : message.areaTag, + MessageUUID: message.messageUuid, + ModTimestamp: message.modTimestamp.format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + AreaTag: message.areaTag, }; - const externalFlavor = message.meta.System[Message.SystemMetaNames.ExternalFlavor]; + const externalFlavor = + message.meta.System[Message.SystemMetaNames.ExternalFlavor]; if (externalFlavor === Message.AddressFlavor.FTN) { // Add FTN properties if it came from such an origin if (message.meta.FtnProperty) { const ftnProp = message.meta.FtnProperty; - messageData['X-FTN-AREA'] = ftnProp[Message.FtnPropertyNames.FtnArea]; - messageData['X-FTN-SEEN-BY'] = ftnProp[Message.FtnPropertyNames.FtnSeenBy]; + messageData['X-FTN-AREA'] = ftnProp[Message.FtnPropertyNames.FtnArea]; + messageData['X-FTN-SEEN-BY'] = + ftnProp[Message.FtnPropertyNames.FtnSeenBy]; } if (message.meta.FtnKludge) { const ftnKludge = message.meta.FtnKludge; - messageData['X-FTN-PATH'] = ftnKludge.PATH; - messageData['X-FTN-MSGID'] = ftnKludge.MSGID; - messageData['X-FTN-REPLY'] = ftnKludge.REPLY; - messageData['X-FTN-PID'] = ftnKludge.PID; - messageData['X-FTN-FLAGS'] = ftnKludge.FLAGS; - messageData['X-FTN-TID'] = ftnKludge.TID; - messageData['X-FTN-CHRS'] = ftnKludge.CHRS; + messageData['X-FTN-PATH'] = ftnKludge.PATH; + messageData['X-FTN-MSGID'] = ftnKludge.MSGID; + messageData['X-FTN-REPLY'] = ftnKludge.REPLY; + messageData['X-FTN-PID'] = ftnKludge.PID; + messageData['X-FTN-FLAGS'] = ftnKludge.FLAGS; + messageData['X-FTN-TID'] = ftnKludge.TID; + messageData['X-FTN-CHRS'] = ftnKludge.CHRS; } } else { - messageData.WhenExported = this._makeSynchronetTimestamp(moment()); - messageData.Editor = `ENiGMA 1/2 BBS FSE v${enigmaVersion}`; + messageData.WhenExported = this._makeSynchronetTimestamp(moment()); + messageData.Editor = `ENiGMA 1/2 BBS FSE v${enigmaVersion}`; } - this.headersDatStream.write(this._encodeWithFallback(`[${this.currentMessageOffset.toString(16)}]\r\n`, encoding)); + this.headersDatStream.write( + this._encodeWithFallback( + `[${this.currentMessageOffset.toString(16)}]\r\n`, + encoding + ) + ); for (let [name, value] of Object.entries(messageData)) { if (value) { - this.headersDatStream.write(this._encodeWithFallback(`${name}: ${value}\r\n`, encoding)); + this.headersDatStream.write( + this._encodeWithFallback(`${name}: ${value}\r\n`, encoding) + ); } } diff --git a/core/rumorz.js b/core/rumorz.js index 51e58d53..65ebc251 100644 --- a/core/rumorz.js +++ b/core/rumorz.js @@ -2,54 +2,57 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const theme = require('./theme.js'); -const resetScreen = require('./ansi_term.js').resetScreen; -const StatLog = require('./stat_log.js'); -const renderStringLength = require('./string_util.js').renderStringLength; -const SystemLogKeys = require('./system_log.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const resetScreen = require('./ansi_term.js').resetScreen; +const StatLog = require('./stat_log.js'); +const renderStringLength = require('./string_util.js').renderStringLength; +const SystemLogKeys = require('./system_log.js'); // deps -const async = require('async'); -const _ = require('lodash'); +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 FormIds = { - View : 0, - Add : 1, + View: 0, + Add: 1, }; const MciCodeIds = { - ViewForm : { - Entries : 1, - AddPrompt : 2, + ViewForm: { + Entries: 1, + AddPrompt: 2, + }, + AddForm: { + NewEntry: 1, + EntryPreview: 2, + AddPrompt: 3, }, - AddForm : { - NewEntry : 1, - EntryPreview : 2, - AddPrompt : 3, - } }; exports.getModule = class RumorzModule extends MenuModule { constructor(options) { super(options); - this.menuMethods = { - viewAddScreen : (formData, extraArgs, 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( SystemLogKeys.UserAddedRumorz, @@ -58,32 +61,38 @@ exports.getModule = class RumorzModule extends MenuModule { StatLog.KeepType.Forever, () => { this.clearAddForm(); - return this.displayViewScreen(true, cb); // true=cls + return this.displayViewScreen(true, cb); // true=cls } ); } else { // empty message - treat as if cancel was hit - return this.displayViewScreen(true, cb); // true=cls + return this.displayViewScreen(true, cb); // true=cls } }, - cancelAdd : (formData, extraArgs, cb) => { + cancelAdd: (formData, extraArgs, cb) => { this.clearAddForm(); - return this.displayViewScreen(true, cb); // true=cls - } + 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); + const newEntryView = this.viewControllers.add.getView( + MciCodeIds.AddForm.NewEntry + ); + const previewView = this.viewControllers.add.getView( + MciCodeIds.AddForm.EntryPreview + ); newEntryView.setText(''); // preview is optional - if(previewView) { + if (previewView) { previewView.setText(''); } } @@ -98,10 +107,10 @@ exports.getModule = class RumorzModule extends MenuModule { }, function display(callback) { self.displayViewScreen(false, callback); - } + }, ], err => { - if(err) { + if (err) { // :TODO: Handle me -- initSequence() should really take a completion callback } self.finishedLoading(); @@ -114,70 +123,85 @@ exports.getModule = class RumorzModule extends MenuModule { async.waterfall( [ function clearAndDisplayArt(callback) { - if(self.viewControllers.add) { + if (self.viewControllers.add) { self.viewControllers.add.setFocus(false); } - if(clearScreen) { + if (clearScreen) { self.client.term.rawWrite(resetScreen()); } theme.displayThemedAsset( self.config.art.entries, self.client, - { font : self.menuConfig.font, trailingLF : false }, + { font: self.menuConfig.font, trailingLF: false }, (err, artData) => { return callback(err, artData); } ); }, function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { + if (_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( 'view', - new ViewController( { client : self.client, formId : FormIds.View } ) + new ViewController({ + client: self.client, + formId: FormIds.View, + }) ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.View, + 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(); + self.viewControllers.view + .getView(MciCodeIds.ViewForm.AddPrompt) + .redraw(); return callback(null); } }, function fetchEntries(callback) { - const entriesView = self.viewControllers.view.getView(MciCodeIds.ViewForm.Entries); + const entriesView = self.viewControllers.view.getView( + MciCodeIds.ViewForm.Entries + ); - StatLog.getSystemLogEntries(SystemLogKeys.UserAddedRumorz, StatLog.Order.Timestamp, (err, entries) => { - return callback(err, entriesView, entries); - }); + StatLog.getSystemLogEntries( + SystemLogKeys.UserAddedRumorz, + StatLog.Order.Timestamp, + (err, entries) => { + return callback(err, entriesView, entries); + } + ); }, function populateEntries(entriesView, entries, callback) { - entriesView.setItems(entries.map(e => { - return { - text : e.log_value, // standard - rumor : e.log_value, - }; - })); + entriesView.setItems( + entries.map(e => { + return { + text: e.log_value, // standard + 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 + const promptView = self.viewControllers.view.getView( + MciCodeIds.ViewForm.AddPrompt + ); + promptView.setFocusItemIndex(1); // default to NO return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -196,23 +220,26 @@ exports.getModule = class RumorzModule extends MenuModule { theme.displayThemedAsset( self.config.art.add, self.client, - { font : self.menuConfig.font }, + { font: self.menuConfig.font }, (err, artData) => { return callback(err, artData); } ); }, function initOrRedrawViewController(artData, callback) { - if(_.isUndefined(self.viewControllers.add)) { + if (_.isUndefined(self.viewControllers.add)) { const vc = self.addViewController( 'add', - new ViewController( { client : self.client, formId : FormIds.Add } ) + new ViewController({ + client: self.client, + formId: FormIds.Add, + }) ); const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds.Add, + callingMenu: self, + mciMap: artData.mciMap, + formId: FormIds.Add, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -224,15 +251,19 @@ exports.getModule = class RumorzModule extends MenuModule { } }, function initPreviewUpdates(callback) { - const previewView = self.viewControllers.add.getView(MciCodeIds.AddForm.EntryPreview); - const entryView = self.viewControllers.add.getView(MciCodeIds.AddForm.NewEntry); - if(previewView) { + 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( () => { + timerId = setTimeout(() => { const focused = self.viewControllers.add.getFocusedView(); - if(focused === entryView) { + if (focused === entryView) { previewView.setText(entryView.getData()); focused.setFocus(true); } @@ -240,10 +271,10 @@ exports.getModule = class RumorzModule extends MenuModule { }); } return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } diff --git a/core/sauce.js b/core/sauce.js index 40434861..2fc654c6 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -const Errors = require('./enig_error.js').Errors; +const Errors = require('./enig_error.js').Errors; // deps -const iconv = require('iconv-lite'); -const { Parser } = require('binary-parser'); +const iconv = require('iconv-lite'); +const { Parser } = require('binary-parser'); -exports.readSAUCE = readSAUCE; +exports.readSAUCE = readSAUCE; -const SAUCE_SIZE = 128; -const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' +const SAUCE_SIZE = 128; +const SAUCE_ID = Buffer.from([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' // :TODO read comments //const COMNT_ID = Buffer.from([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' -exports.SAUCE_SIZE = SAUCE_SIZE; +exports.SAUCE_SIZE = SAUCE_SIZE; // :TODO: SAUCE should be a class // - with getFontName() // - ...other methods @@ -24,15 +24,15 @@ exports.SAUCE_SIZE = SAUCE_SIZE; // See // http://www.acid.org/info/sauce/sauce.htm // -const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; +const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8]; const SAUCEParser = 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 } ) + .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') @@ -43,55 +43,55 @@ const SAUCEParser = new Parser() .int8('numComments') .int8('flags') // :TODO: does this need to be optional? - .buffer('tinfos', { length: 22 } ); // SAUCE 00.5 + .buffer('tinfos', { length: 22 }); // SAUCE 00.5 function readSAUCE(data, cb) { - if(data.length < SAUCE_SIZE) { + if (data.length < SAUCE_SIZE) { return cb(Errors.DoesNotExist('No SAUCE record present')); } let sauceRec; try { sauceRec = SAUCEParser.parse(data.slice(data.length - SAUCE_SIZE)); - } catch(e) { + } catch (e) { return cb(Errors.Invalid('Invalid SAUCE record')); } - if(!SAUCE_ID.equals(sauceRec.id)) { + if (!SAUCE_ID.equals(sauceRec.id)) { return cb(Errors.DoesNotExist('No SAUCE record present')); } const ver = iconv.decode(sauceRec.version, 'cp437'); - if('00' !== ver) { + if ('00' !== ver) { return cb(Errors.Invalid(`Unsupported SAUCE version: ${ver}`)); } - if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(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, + 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) { + if (dt && dt.parser) { sauce[dt.name] = dt.parser(sauce); } @@ -100,27 +100,27 @@ function readSAUCE(data, cb) { // :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,32 +129,49 @@ 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' -].forEach( page => { + '437', + '720', + '737', + '775', + '819', + '850', + '852', + '855', + '857', + '858', + '860', + '861', + '862', + '863', + '864', + '865', + '866', + '869', + '872', +].forEach(page => { const codec = 'cp' + page; - SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; - SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM EGA43 ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM EGA ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA25g ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA50 ' + page] = codec; + SAUCE_FONT_TO_ENCODING_HINT['IBM VGA ' + page] = codec; }); function parseCharacterSAUCE(sauce) { @@ -162,23 +179,23 @@ function parseCharacterSAUCE(sauce) { result.fileType = SAUCE_CHARACTER_FILE_TYPES[sauce.fileType] || 'Unknown'; - if(sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { + if (sauce.fileType === 0 || sauce.fileType === 1 || sauce.fileType === 2) { // convenience: create ansiFlags sauce.ansiFlags = sauce.flags; let i = 0; - while(i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { + while (i < sauce.tinfos.length && sauce.tinfos[i] !== 0x00) { ++i; } const fontName = iconv.decode(sauce.tinfos.slice(0, i), 'cp437'); - if(fontName.length > 0) { + if (fontName.length > 0) { result.fontName = fontName; } const setDimen = (v, field) => { const i = parseInt(v, 10); - if(!isNaN(i)) { + if (!isNaN(i)) { result[field] = i; } }; @@ -188,4 +205,4 @@ function parseCharacterSAUCE(sauce) { } 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 068ff111..30d29267 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -2,46 +2,48 @@ 'use strict'; // ENiGMA½ -const MessageScanTossModule = require('../msg_scan_toss_module.js').MessageScanTossModule; -const Config = require('../config.js').get; -const ftnMailPacket = require('../ftn_mail_packet.js'); -const ftnUtil = require('../ftn_util.js'); -const Address = require('../ftn_address.js'); -const Log = require('../logger.js').log; -const ArchiveUtil = require('../archive_util.js'); -const msgDb = require('../database.js').dbs.message; -const Message = require('../message.js'); -const TicFileInfo = require('../tic_file_info.js'); -const Errors = require('../enig_error.js').Errors; -const FileEntry = require('../file_entry.js'); -const scanFile = require('../file_base_area.js').scanFile; -const getFileAreaByTag = require('../file_base_area.js').getFileAreaByTag; -const getDescFromFileName = require('../file_base_area.js').getDescFromFileName; -const copyFileWithCollisionHandling = require('../file_util.js').copyFileWithCollisionHandling; -const getAreaStorageDirectoryByTag = require('../file_base_area.js').getAreaStorageDirectoryByTag; -const isValidStorageTag = require('../file_base_area.js').isValidStorageTag; -const User = require('../user.js'); -const StatLog = require('../stat_log.js'); -const SysProps = require('../system_property.js'); +const MessageScanTossModule = require('../msg_scan_toss_module.js').MessageScanTossModule; +const Config = require('../config.js').get; +const ftnMailPacket = require('../ftn_mail_packet.js'); +const ftnUtil = require('../ftn_util.js'); +const Address = require('../ftn_address.js'); +const Log = require('../logger.js').log; +const ArchiveUtil = require('../archive_util.js'); +const msgDb = require('../database.js').dbs.message; +const Message = require('../message.js'); +const TicFileInfo = require('../tic_file_info.js'); +const Errors = require('../enig_error.js').Errors; +const FileEntry = require('../file_entry.js'); +const scanFile = require('../file_base_area.js').scanFile; +const getFileAreaByTag = require('../file_base_area.js').getFileAreaByTag; +const getDescFromFileName = require('../file_base_area.js').getDescFromFileName; +const copyFileWithCollisionHandling = + require('../file_util.js').copyFileWithCollisionHandling; +const getAreaStorageDirectoryByTag = + require('../file_base_area.js').getAreaStorageDirectoryByTag; +const isValidStorageTag = require('../file_base_area.js').isValidStorageTag; +const User = require('../user.js'); +const StatLog = require('../stat_log.js'); +const SysProps = require('../system_property.js'); // deps -const moment = require('moment'); -const _ = require('lodash'); -const paths = require('path'); -const async = require('async'); -const fs = require('graceful-fs'); -const later = require('@breejs/later'); -const temptmp = require('temptmp').createTrackedSession('ftn_bso'); -const assert = require('assert'); -const sane = require('sane'); -const fse = require('fs-extra'); -const iconv = require('iconv-lite'); -const { v4 : UUIDv4 } = require('uuid'); +const moment = require('moment'); +const _ = require('lodash'); +const paths = require('path'); +const async = require('async'); +const fs = require('graceful-fs'); +const later = require('@breejs/later'); +const temptmp = require('temptmp').createTrackedSession('ftn_bso'); +const assert = require('assert'); +const sane = require('sane'); +const fse = require('fs-extra'); +const iconv = require('iconv-lite'); +const { v4: UUIDv4 } = require('uuid'); 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', }; /* @@ -53,7 +55,7 @@ exports.moduleInfo = { exports.getModule = FTNMessageScanTossModule; -const SCHEDULE_REGEXP = /(?:^|or )?(@watch:|@immediate)([^\0]+)?$/; +const SCHEDULE_REGEXP = /(?:^|or )?(@watch:|@immediate)([^\0]+)?$/; function FTNMessageScanTossModule() { MessageScanTossModule.call(this); @@ -63,30 +65,31 @@ function FTNMessageScanTossModule() { this.archUtil = ArchiveUtil.getInstance(); const config = Config(); - if(_.has(config, 'scannerTossers.ftn_bso')) { + if (_.has(config, 'scannerTossers.ftn_bso')) { this.moduleConfig = config.scannerTossers.ftn_bso; } - this.getDefaultNetworkName = function() { - if(this.moduleConfig.defaultNetwork) { + this.getDefaultNetworkName = function () { + if (this.moduleConfig.defaultNetwork) { return this.moduleConfig.defaultNetwork.toLowerCase(); } const networkNames = Object.keys(config.messageNetworks.ftn.networks); - if(1 === networkNames.length) { + if (1 === networkNames.length) { return networkNames[0].toLowerCase(); } }; - this.getDefaultZone = function(networkName) { + this.getDefaultZone = function (networkName) { const config = Config(); - if(_.isNumber(config.messageNetworks.ftn.networks[networkName].defaultZone)) { + 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 networkLocalAddress = + config.messageNetworks.ftn.networks[networkName].localAddress; + if (networkLocalAddress) { const addr = Address.fromString(networkLocalAddress); return addr.zone; } @@ -99,29 +102,34 @@ function FTNMessageScanTossModule() { }; */ - this.getNetworkNameByAddress = function(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) { + this.getNetworkNameByAddressPattern = function (remoteAddressPattern) { return _.findKey(Config().messageNetworks.ftn.networks, network => { const localAddress = Address.fromString(network.localAddress); - return !_.isUndefined(localAddress) && localAddress.isPatternMatch(remoteAddressPattern); + return ( + !_.isUndefined(localAddress) && + localAddress.isPatternMatch(remoteAddressPattern) + ); }); }; - this.getLocalAreaTagByFtnAreaTag = function(ftnAreaTag) { - ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper + this.getLocalAreaTagByFtnAreaTag = function (ftnAreaTag) { + ftnAreaTag = ftnAreaTag.toUpperCase(); // always compare upper return _.findKey(Config().messageNetworks.ftn.areas, areaConf => { return _.isString(areaConf.tag) && 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'; }; /* @@ -138,8 +146,10 @@ 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 + ); }; /* @@ -153,22 +163,22 @@ function FTNMessageScanTossModule() { }; */ - this.getOutgoingEchoMailPacketDir = function(networkName, destAddress) { + this.getOutgoingEchoMailPacketDir = function (networkName, destAddress) { networkName = networkName.toLowerCase(); let dir = this.moduleConfig.paths.outbound; - const defaultNetworkName = this.getDefaultNetworkName(); - const defaultZone = this.getDefaultZone(networkName); + const defaultNetworkName = this.getDefaultNetworkName(); + const defaultZone = this.getDefaultZone(networkName); let zoneExt; - if(defaultZone !== destAddress.zone) { + if (defaultZone !== destAddress.zone) { zoneExt = '.' + `000${destAddress.zone.toString(16)}`.substr(-3); } else { zoneExt = ''; } - if(defaultNetworkName === networkName) { + if (defaultNetworkName === networkName) { dir = paths.join(dir, `outbound${zoneExt}`); } else { dir = paths.join(dir, `${networkName}${zoneExt}`); @@ -177,7 +187,7 @@ function FTNMessageScanTossModule() { return dir; }; - this.getOutgoingPacketFileName = function(basePath, messageId, isTemp, fileCase) { + 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 @@ -195,36 +205,57 @@ function FTNMessageScanTossModule() { // * We already have a system for 8-character serial number gernation that is // used for e.g. in FTS-0009.001 MSGIDs... let's use that! // - const name = ftnUtil.getMessageSerialNumber(messageId); - const ext = (true === isTemp) ? 'pk_' : 'pkt'; + const name = ftnUtil.getMessageSerialNumber(messageId); + const ext = true === isTemp ? 'pk_' : 'pkt'; let fileName = `${name}.${ext}`; - if('upper' === fileCase) { + if ('upper' === fileCase) { fileName = fileName.toUpperCase(); } return paths.join(basePath, fileName); }; - this.getOutgoingFlowFileExtension = function(destAddress, flowType, exportType, fileCase) { + 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) { + if ('upper' === fileCase) { ext = ext.toUpperCase(); } return ext; }; - this.getOutgoingFlowFileName = function(basePath, destAddress, flowType, exportType, fileCase) { + this.getOutgoingFlowFileName = function ( + basePath, + destAddress, + flowType, + exportType, + fileCase + ) { // // Refs // * http://ftsc.org/docs/fts-5005.003 @@ -240,12 +271,12 @@ function FTNMessageScanTossModule() { fileCase ); - const netComponent = `0000${destAddress.net.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) { + 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`; + pointDir = `${netComponent}${nodeComponent}.pnt`; controlFileBaseName = `00000000${destAddress.point.toString(16)}`.substr(-8); } else { pointDir = ''; @@ -261,23 +292,24 @@ function FTNMessageScanTossModule() { // From FTS-5005.003: "Lower case filenames are prefered if supported by the file system." // ...but we let the user override. // - if('upper' === fileCase) { + if ('upper' === fileCase) { controlFileBaseName = controlFileBaseName.toUpperCase(); - pointDir = pointDir.toUpperCase(); + pointDir = pointDir.toUpperCase(); } return paths.join(basePath, pointDir, `${controlFileBaseName}.${ext}`); }; - this.flowFileAppendRefs = function(filePath, fileRefs, directive, cb) { + 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) => { + fse.mkdirs(flowFileDir, () => { + // note not checking err; let's try appendFile + const appendLines = fileRefs.reduce((content, ref) => { return content + `${directive}${ref}\n`; }, ''); @@ -287,7 +319,7 @@ function FTNMessageScanTossModule() { }); }; - this.getOutgoingBundleFileName = function(basePath, sourceAddress, destAddress, cb) { + 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 @@ -299,13 +331,17 @@ function FTNMessageScanTossModule() { // Extension is dd? where dd is Su...Mo and ? is 0...Z as collisions arise // let basename; - if(destAddress.point) { + 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); + `0000${Math.abs(sourceAddress.net - destAddress.net).toString( + 16 + )}`.substr(-4) + + `0000${Math.abs(sourceAddress.node - destAddress.node).toString( + 16 + )}`.substr(-4); } // @@ -314,21 +350,25 @@ function FTNMessageScanTossModule() { // 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)); - } + 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) { + this.prepareMessage = function (message, options) { // // Set various FTN kludges/etc. // @@ -338,36 +378,36 @@ function FTNMessageScanTossModule() { message.meta.FtnProperty = message.meta.FtnProperty || {}; message.meta.FtnKludge = message.meta.FtnKludge || {}; - message.meta.FtnProperty.ftn_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_orig_network = localAddress.net; - message.meta.FtnProperty.ftn_cost = 0; - message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; - message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; + message.meta.FtnProperty.ftn_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_orig_network = localAddress.net; + message.meta.FtnProperty.ftn_cost = 0; + message.meta.FtnProperty.ftn_msg_orig_node = localAddress.node; + message.meta.FtnProperty.ftn_msg_orig_net = localAddress.net; const destAddress = options.routeAddress || options.destAddress; - message.meta.FtnProperty.ftn_dest_node = destAddress.node; - message.meta.FtnProperty.ftn_dest_network = destAddress.net; + message.meta.FtnProperty.ftn_dest_node = destAddress.node; + message.meta.FtnProperty.ftn_dest_network = destAddress.net; - if(destAddress.zone) { - message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; + if (destAddress.zone) { + message.meta.FtnProperty.ftn_dest_zone = destAddress.zone; } - if(destAddress.point) { + 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); + 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)) { + 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; + 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; @@ -388,43 +428,57 @@ function FTNMessageScanTossModule() { // 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); + message.meta.FtnKludge.INTL = ftnUtil.getIntl( + options.destAddress, + localAddress + ); - if(_.isNumber(localAddress.point) && localAddress.point > 0) { + if (_.isNumber(localAddress.point) && localAddress.point > 0) { message.meta.FtnKludge.FMPT = localAddress.point; } - if(_.isNumber(options.destAddress.point) && options.destAddress.point > 0) { + 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; + 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; + message.meta.FtnProperty.ftn_area = + config.messageNetworks.ftn.areas[message.areaTag].tag; // // When exporting messages, we should create/update SEEN-BY // with remote address(s) we are exporting to. // - const seenByAdditions = - [ `${localAddress.net}/${localAddress.node}` ].concat(config.messageNetworks.ftn.areas[message.areaTag].uplinks); - message.meta.FtnProperty.ftn_seen_by = - ftnUtil.getUpdatedSeenByEntries(message.meta.FtnProperty.ftn_seen_by, seenByAdditions); + const seenByAdditions = [`${localAddress.net}/${localAddress.node}`].concat( + config.messageNetworks.ftn.areas[message.areaTag].uplinks + ); + message.meta.FtnProperty.ftn_seen_by = ftnUtil.getUpdatedSeenByEntries( + message.meta.FtnProperty.ftn_seen_by, + seenByAdditions + ); // // And create/update PATH for ourself // - message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries(message.meta.FtnKludge.PATH, localAddress); + message.meta.FtnKludge.PATH = ftnUtil.getUpdatedPathEntries( + message.meta.FtnKludge.PATH, + localAddress + ); } message.meta.FtnProperty.ftn_attr_flags = ftnAttribute; @@ -435,7 +489,7 @@ function FTNMessageScanTossModule() { // 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) { + if (!message.meta.FtnKludge.MSGID) { message.meta.FtnKludge.MSGID = ftnUtil.getMessageIdentifier( message, localAddress, @@ -458,13 +512,18 @@ function FTNMessageScanTossModule() { // Determine CHRS and actual internal encoding name. If the message has an // explicit encoding set, use it. Otherwise, try to preserve any CHRS/encoding already set. // - let encoding = options.nodeConfig.encoding || config.scannerTossers.ftn_bso.packetMsgEncoding || 'utf8'; + let encoding = + options.nodeConfig.encoding || + config.scannerTossers.ftn_bso.packetMsgEncoding || + 'utf8'; const explicitEncoding = _.get(message.meta, 'System.explicit_encoding'); - if(explicitEncoding) { + if (explicitEncoding) { encoding = explicitEncoding; - } else if(message.meta.FtnKludge.CHRS) { - const encFromChars = ftnUtil.getEncodingFromCharacterSetIdentifier(message.meta.FtnKludge.CHRS); - if(encFromChars) { + } else if (message.meta.FtnKludge.CHRS) { + const encFromChars = ftnUtil.getEncodingFromCharacterSetIdentifier( + message.meta.FtnKludge.CHRS + ); + if (encFromChars) { encoding = encFromChars; } } @@ -472,61 +531,77 @@ function FTNMessageScanTossModule() { // // 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'); + 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) { + 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)) { + this.isAreaConfigValid = function (areaConfig) { + if ( + !areaConfig || + !_.isString(areaConfig.tag) || + !_.isString(areaConfig.network) + ) { return false; } - if(_.isString(areaConfig.uplinks)) { + if (_.isString(areaConfig.uplinks)) { areaConfig.uplinks = areaConfig.uplinks.split(' '); } - return (_.isArray(areaConfig.uplinks)); + return _.isArray(areaConfig.uplinks); }; - - this.hasValidConfiguration = function({shouldLog = false} = {}) { + this.hasValidConfiguration = function ({ shouldLog = false } = {}) { const hasNodes = _.has(this, 'moduleConfig.nodes'); const hasAreas = _.has(Config(), 'messageNetworks.ftn.areas'); - if(!hasNodes && !hasAreas) { + if (!hasNodes && !hasAreas) { if (shouldLog) { Log.warn( { - 'scannerTossers.ftn_bso.nodes' : hasNodes, - 'messageNetworks.ftn.areas' : hasAreas, + 'scannerTossers.ftn_bso.nodes': hasNodes, + 'messageNetworks.ftn.areas': hasAreas, }, 'Missing one or more required configuration blocks' ); @@ -539,60 +614,58 @@ function FTNMessageScanTossModule() { return true; }; - this.parseScheduleString = function(schedStr) { - if(!schedStr) { + this.parseScheduleString = function (schedStr) { + if (!schedStr) { return; // nothing to parse! } let schedule = {}; const m = SCHEDULE_REGEXP.exec(schedStr); - if(m) { + if (m) { schedStr = schedStr.substr(0, m.index).trim(); - if('@watch:' === m[1]) { + if ('@watch:' === m[1]) { schedule.watchFile = m[2]; - } else if('@immediate' === m[1]) { + } else if ('@immediate' === m[1]) { schedule.immediate = true; } } - if(schedStr.length > 0) { + if (schedStr.length > 0) { const sched = later.parse.text(schedStr); - if(-1 === sched.error) { + if (-1 === sched.error) { schedule.sched = sched; } } // return undefined if we couldn't parse out anything useful - if(!_.isEmpty(schedule)) { + if (!_.isEmpty(schedule)) { return schedule; } }; - this.getAreaLastScanId = function(areaTag, cb) { - const sql = - `SELECT area_tag, message_id + 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) => { + msgDb.get(sql, [areaTag], (err, row) => { return cb(err, row ? row.message_id : 0); }); }; - this.setAreaLastScanId = function(areaTag, lastScanId, cb) { - const sql = - `REPLACE INTO message_area_last_scan (scan_toss, area_tag, message_id) + 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 => { + msgDb.run(sql, [areaTag, lastScanId], err => { return cb(err); }); }; - this.getNodeConfigByAddress = function(addr) { + this.getNodeConfigByAddress = function (addr) { addr = _.isString(addr) ? Address.fromString(addr) : addr; // :TODO: sort wildcard nodes{} entries by most->least explicit according to FTN hierarchy @@ -601,7 +674,7 @@ function FTNMessageScanTossModule() { }); }; - this.exportNetMailMessagePacket = function(message, exportOpts, cb) { + this.exportNetMailMessagePacket = function (message, exportOpts, cb) { // // For NetMail, we always create a *single* packet per message. // @@ -627,7 +700,7 @@ function FTNMessageScanTossModule() { exportOpts.pktFileName = self.getOutgoingPacketFileName( self.exportTempDir, message.messageId, - false, // createTempPacket=false + false, // createTempPacket=false exportOpts.fileCase ); @@ -636,7 +709,7 @@ function FTNMessageScanTossModule() { packet.writeHeader(ws, packetHeader); packet.getMessageEntryBuffer(message, exportOpts, (err, msgBuf) => { - if(err) { + if (err) { return callback(err); } @@ -649,7 +722,7 @@ function FTNMessageScanTossModule() { return callback(null); }); }); - } + }, ], err => { return cb(err); @@ -657,20 +730,22 @@ function FTNMessageScanTossModule() { ); }; - this.exportMessagesByUuid = function(messageUuids, exportOpts, cb) { + 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 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; + const createTempPacket = + !_.isString(exportOpts.nodeConfig.archiveType) || + 0 === exportOpts.nodeConfig.archiveType.length; function finalizePacket(cb) { packet.writeTerminator(ws); @@ -680,165 +755,198 @@ function FTNMessageScanTossModule() { }); } - async.each(messageUuids, (msgUuid, nextUuid) => { - let message = new Message(); + 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) { + function finalizePrevious(callback) { + if ( + packet && + currPacketSize >= self.moduleConfig.packetTargetByteSize + ) { return finalizePacket(callback); } else { callback(null); } }, - function writeRemainPacket(callback) { - if(remainMessageBuf) { - // :TODO: DRY this with the code above -- they are basically identical + 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); + exportOpts.nodeConfig.packetType + ); - packetHeader.password = exportOpts.nodeConfig.packetPassword || ''; + packetHeader.password = + exportOpts.nodeConfig.packetPassword || ''; // use current message ID for filename seed const pktFileName = self.getOutgoingPacketFileName( self.exportTempDir, - remainMessageId, + message.messageId, createTempPacket, - exportOpts.filleCase + exportOpts.fileCase ); exportedFiles.push(pktFileName); ws = fs.createWriteStream(pktFileName); - packet.writeHeader(ws, packetHeader); - ws.write(remainMessageBuf); - return finalizePacket(callback); + 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 => { - cb(err, exportedFiles); + 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) { + this.getNetMailRoute = function (dstAddr) { // // Route full|wildcard -> full adddress/network lookup // const routes = _.get(Config(), 'scannerTossers.ftn_bso.netMail.routes'); - if(!routes) { + if (!routes) { return; } @@ -847,7 +955,7 @@ function FTNMessageScanTossModule() { }); }; - this.getNetMailRouteInfoFromAddress = function(destAddress, cb) { + this.getNetMailRouteInfoFromAddress = function (destAddress, cb) { // // Attempt to find route information for |destAddress|: // @@ -864,282 +972,388 @@ function FTNMessageScanTossModule() { let routeAddress; let networkName; let isRouted; - if(route) { - routeAddress = Address.fromString(route.address); - networkName = route.network; - isRouted = true; + if (route) { + routeAddress = Address.fromString(route.address); + networkName = route.network; + isRouted = true; } else { - routeAddress = destAddress; - isRouted = false; + routeAddress = destAddress; + isRouted = false; } networkName = networkName || this.getNetworkNameByAddress(routeAddress); const config = _.find(this.moduleConfig.nodes, (node, nodeAddrWildcard) => { return routeAddress.isPatternMatch(nodeAddrWildcard); - }) || { packetType : '2+', encoding : Config().scannerTossers.ftn_bso.packetMsgEncoding }; + }) || { + packetType: '2+', + encoding: Config().scannerTossers.ftn_bso.packetMsgEncoding, + }; // we should never be failing here; we may just be using defaults. return cb( - networkName ? null : Errors.DoesNotExist(`No NetMail route for ${destAddress.toString()}`), + networkName + ? null + : Errors.DoesNotExist(`No NetMail route for ${destAddress.toString()}`), { destAddress, routeAddress, networkName, config, isRouted } ); }; - this.exportNetMailMessagesToUplinks = function(messagesOrMessageUuids, cb) { + this.exportNetMailMessagesToUplinks = function (messagesOrMessageUuids, cb) { // for each message/UUID, find where to send the thing - async.each(messagesOrMessageUuids, (msgOrUuid, nextMessageOrUuid) => { + async.each( + messagesOrMessageUuids, + (msgOrUuid, nextMessageOrUuid) => { + const exportOpts = {}; + const message = new Message(); - 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); + 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] + ); - 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); + self.getNetMailRouteInfoFromAddress( + dstAddr, + (err, routeInfo) => { + if (err) { + return callback(err); + } - if(!exportOpts.network) { - return callback(Errors.DoesNotExist(`No configuration found for network ${routeInfo.networkName}`)); + exportOpts.nodeConfig = routeInfo.config; + exportOpts.destAddress = dstAddr; + exportOpts.routeAddress = routeInfo.routeAddress; + exportOpts.fileCase = + routeInfo.config.fileCase || 'lower'; + exportOpts.network = + Config().messageNetworks.ftn.networks[ + routeInfo.networkName + ]; + exportOpts.networkName = routeInfo.networkName; + exportOpts.outgoingDir = + self.getOutgoingEchoMailPacketDir( + exportOpts.networkName, + exportOpts.destAddress + ); + exportOpts.exportType = self.getExportType( + 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); - }); - }, - 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); + }, + ], + err => { + if (err) { + Log.warn({ error: err.message }, 'Error exporting message'); } - - return callback(null); + return nextMessageOrUuid(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'); } - ); - }, err => { - if(err) { - Log.warn( { error : err.message }, 'Error(s) during NetMail export'); + return cb(err); } - return cb(err); - }); + ); }; - this.exportEchoMailMessagesToUplinks = function(messageUuids, areaConfig, cb) { + 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(); + async.each( + areaConfig.uplinks, + (uplink, nextUplink) => { + const nodeConfig = self.getNodeConfigByAddress(uplink); + if (!nodeConfig) { + return nextUplink(); } - ); - }, cb); // complete + + 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) { + 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)) { + 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!'); + 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(); } - cb(); - }); + ); }; - this.getLocalUserNameFromAlias = function(lookup) { + this.getLocalUserNameFromAlias = function (lookup) { lookup = lookup.toLowerCase(); const aliases = _.get(Config(), 'messageNetworks.ftn.netMail.aliases'); - if(!aliases) { - return lookup; // keep orig + if (!aliases) { + return lookup; // keep orig } const alias = _.find(aliases, (localName, alias) => { @@ -1149,71 +1363,95 @@ function FTNMessageScanTossModule() { return alias || lookup; }; - this.getAddressesFromNetMailMessage = function(message) { + this.getAddressesFromNetMailMessage = function (message) { const intlKludge = _.get(message, 'meta.FtnKludge.INTL'); - if(!intlKludge) { + if (!intlKludge) { return {}; } - let [ to, from ] = intlKludge.split(' '); - if(!to || !from) { + let [to, from] = intlKludge.split(' '); + if (!to || !from) { return {}; } const fromPoint = _.get(message, 'meta.FtnKludge.FMPT'); - const toPoint = _.get(message, 'meta.FtnKludge.TOPT'); + const toPoint = _.get(message, 'meta.FtnKludge.TOPT'); - if(fromPoint) { + if (fromPoint) { from += `.${fromPoint}`; } - if(toPoint) { + if (toPoint) { to += `.${toPoint}`; } - return { to : Address.fromString(to), from : Address.fromString(from) }; + return { to: Address.fromString(to), from: Address.fromString(from) }; }; - this.importMailToArea = function(config, header, message, cb) { + 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); + const localNetworkName = + self.getNetworkNameByAddressPattern(localNetworkPattern); - return callback(_.isString(localNetworkName) ? null : new Error('Packet destination is not us')); + 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')) { + 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); - } + 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); - }); + 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; + 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)) { + if ( + true === + _.get( + Config(), + [ + 'messageNetworks', + 'ftn', + 'areas', + config.localAreaTag, + 'allowDupes', + ], + false + ) + ) { // just generate a UUID & therefor always allow for dupes message.messageUuid = UUIDv4(); } @@ -1229,7 +1467,7 @@ function FTNMessageScanTossModule() { // // If this is a private message (e.g. NetMail) we set the local user ID // - if(Message.WellKnownAreaTags.Private !== config.localAreaTag) { + if (Message.WellKnownAreaTags.Private !== config.localAreaTag) { return callback(null); } @@ -1239,65 +1477,101 @@ function FTNMessageScanTossModule() { // const { from } = self.getAddressesFromNetMailMessage(message); - if(!from) { - return callback(Errors.Invalid('Cannot import FTN NetMail without valid INTL line')); + if (!from) { + return callback( + Errors.Invalid( + 'Cannot import FTN NetMail without valid INTL line' + ) + ); } - message.meta.System[Message.SystemMetaNames.RemoteFromUser] = from.toString(); + 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 && 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.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 && + 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')); - } + 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}`)); + 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 preserved above - if(lookupName !== message.toUserName) { - message.toUserName = localUserName; - } + // we do this after such that error cases can be preserved above + if (lookupName !== message.toUserName) { + message.toUserName = localUserName; + } - // set the meta information - used elsewhere for retrieval - message.meta.System[Message.SystemMetaNames.LocalToUserID] = localToUserId; - return callback(null); - }); + // set the meta information - used elsewhere 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(); + message.meta.System.state_flags0 = + Message.StateFlags0.Imported.toString(); // save to disc message.persist(err => { - if(!message.isPrivate()) { - StatLog.incrementNonPersistentSystemStat(SysProps.MessageTotalCount, 1); - StatLog.incrementNonPersistentSystemStat(SysProps.MessagesToday, 1); + if (!message.isPrivate()) { + StatLog.incrementNonPersistentSystemStat( + SysProps.MessageTotalCount, + 1 + ); + StatLog.incrementNonPersistentSystemStat( + SysProps.MessagesToday, + 1 + ); } return callback(err); }); - } + }, ], err => { cb(err); @@ -1305,12 +1579,12 @@ function FTNMessageScanTossModule() { ); }; - this.appendTearAndOrigin = function(message) { - if(message.meta.FtnProperty.ftn_tear_line) { + 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) { + if (message.meta.FtnProperty.ftn_origin) { message.message += `${message.meta.FtnProperty.ftn_origin}\r\n`; } }; @@ -1320,117 +1594,147 @@ function FTNMessageScanTossModule() { // * 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) { + this.importMessagesFromPacketFile = function (packetPath, password, cb) { let packetHeader; - const packetOpts = { keepTearAndOrigin : false }; // needed so we can calc message UUID without these; we'll add later + const packetOpts = { keepTearAndOrigin: false }; // needed so we can calc message UUID without these; we'll add later let importStats = { - areaSuccess : {}, // areaTag->count - areaFail : {}, // areaTag->count - otherFail : 0, + areaSuccess: {}, // areaTag->count + areaFail: {}, // areaTag->count + otherFail: 0, }; - new ftnMailPacket.Packet(packetOpts).read(packetPath, (entryType, entryData, next) => { - if('header' === entryType) { - packetHeader = entryData; + 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; + 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 { - Log.warn('Non-private message without area tag'); - importStats.otherFail += 1; + // :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; - message.messageUuid = Message.createMessageUUID( - localAreaTag, - message.modTimestamp, - message.subject, - message.message); + let localAreaTag; + if (areaTag) { + localAreaTag = self.getLocalAreaTagByFtnAreaTag(areaTag); - self.appendTearAndOrigin(message); + 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!' + ); - 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.messageUuid, MSGID : msgId }, - 'Not importing non-unique message'); + // bump generic failure + importStats.otherFail += 1; return next(null); } } else { - // bump area success - importStats.areaSuccess[localAreaTag] = (importStats.areaSuccess[localAreaTag] || 0) + 1; + // + // 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); + } } - 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 } ); + message.messageUuid = 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.messageUuid, + 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'); } - Log.warn(finalStats, 'Import completed with error(s)'); - } else { - Log.info(finalStats, 'Import complete'); + cb(err); } - - cb(err); - }); + ); }; - this.maybeArchiveImportFile = function(origPath, type, status, cb) { + this.maybeArchiveImportFile = function (origPath, type, status, cb) { // // type : pkt|tic|bundle // status : good|reject @@ -1442,76 +1746,113 @@ function FTNMessageScanTossModule() { 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)) { + 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}`); + 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 + return cb(null); // don't archive non-good/pkt files } - Log.debug( { origPath : origPath, archivePath : archivePath, type : type, status : status }, 'Archiving import file'); + 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'); + 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 + return cb(null); // never fatal }); }; - this.importPacketFilesFromDirectory = function(importDir, password, cb) { + this.importPacketFilesFromDirectory = function (importDir, password, cb) { async.waterfall( [ function getPacketFiles(callback) { fs.readdir(importDir, (err, files) => { - if(err) { + if (err) { return callback(err); } - callback(null, files.filter(f => '.pkt' === paths.extname(f).toLowerCase())); + 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'); + 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); - }); + 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); - }); - } + 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); @@ -1519,7 +1860,7 @@ function FTNMessageScanTossModule() { ); }; - this.importFromDirectory = function(inboundType, importDir, cb) { + this.importFromDirectory = function (inboundType, importDir, cb) { async.waterfall( [ // start with .pkt files @@ -1537,86 +1878,113 @@ function FTNMessageScanTossModule() { 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); - }); + 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'); + async.each( + bundleFiles, + (bundleFile, nextFile) => { + if (_.isUndefined(bundleFile.archName)) { + Log.warn( + { fileName: bundleFile.path }, + 'Unknown bundle archive type' + ); - rejects.push(bundleFile.path); + 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(); + return nextFile(); // unknown archive type } - ); - }, err => { - if(err) { - return callback(err); - } - // - // All extracted - import .pkt's - // - self.importPacketFilesFromDirectory(self.importTempDir, '', () => { - // :TODO: handle |err| - callback(null, bundleFiles, rejects); - }); - }); + 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, + '', + () => { + // :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); - }); + 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); @@ -1624,15 +1992,15 @@ function FTNMessageScanTossModule() { ); }; - this.createTempDirectories = function(cb) { - temptmp.mkdir( { prefix : 'enigftnexport-' }, (err, tempDir) => { - if(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) => { + temptmp.mkdir({ prefix: 'enigftnimport-' }, (err, tempDir) => { self.importTempDir = tempDir; cb(err); @@ -1641,8 +2009,8 @@ function FTNMessageScanTossModule() { }; // Starts an export block - returns true if we can proceed - this.exportingStart = function() { - if(!this.exportRunning) { + this.exportingStart = function () { + if (!this.exportRunning) { this.exportRunning = true; return true; } @@ -1651,17 +2019,17 @@ function FTNMessageScanTossModule() { }; // ends an export block - this.exportingEnd = function(cb) { + this.exportingEnd = function (cb) { this.exportRunning = false; - if(cb) { + if (cb) { return cb(null); } }; - this.copyTicAttachment = function(src, dst, isUpdate, cb) { - if(isUpdate) { - fse.copy(src, dst, { overwrite : true }, err => { + this.copyTicAttachment = function (src, dst, isUpdate, cb) { + if (isUpdate) { + fse.copy(src, dst, { overwrite: true }, err => { return cb(err, dst); }); } else { @@ -1671,39 +2039,48 @@ function FTNMessageScanTossModule() { } }; - this.getLocalAreaTagsForTic = function() { + this.getLocalAreaTagsForTic = function () { const config = Config(); - return _.union(Object.keys(config.scannerTossers.ftn_bso.ticAreas || {} ), Object.keys(config.fileBase.areas)); + 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'); + 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(), + nodes: sysConfig.scannerTossers.ftn_bso.nodes, + defaultPassword: sysConfig.scannerTossers.ftn_bso.tic.password, + localAreaTags: self.getLocalAreaTagsForTic(), }; ticFileInfo.validate(config, (err, localInfo) => { - if(err) { - Log.trace( { reason : err.message }, 'Validation failure'); + 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 ]); + 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)) { + 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; } } @@ -1723,68 +2100,95 @@ function FTNMessageScanTossModule() { // Lastly, we will only replace if the item is in the same/specified area // and that come from the same origin as a previous entry. // - const allowReplace = _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'allowReplace' ], Config().scannerTossers.ftn_bso.tic.allowReplace); - const replaces = ticFileInfo.getAsString('Replaces'); + const allowReplace = _.get( + Config().scannerTossers.ftn_bso.nodes, + [localInfo.node, 'tic', 'allowReplace'], + Config().scannerTossers.ftn_bso.tic.allowReplace + ); + const replaces = ticFileInfo.getAsString('Replaces'); - if(!allowReplace || !replaces) { + 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: 'short_file_name', + value: replaces.toUpperCase(), // we store upper as well + wildcards: true, // value may contain wildcards }, { - name : 'tic_origin', - value : ticFileInfo.getAsString('Origin'), - } + name: 'tic_origin', + value: ticFileInfo.getAsString('Origin'), + }, ]; - FileEntry.findFiles( { metaPairs : metaPairs, areaTag : localInfo.areaTag }, (err, fileIds) => { - if(err) { - return callback(err); + 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.length > 1) { + return callback( + Errors.General( + `More than one existing entry for TIC in ${ + localInfo.areaTag + } ([${fileIds.join(', ')}])` + ) + ); + } else { + return callback(null, localInfo); + } } - - // 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.length > 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 : { + 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), - } + 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) { + if (ldesc) { scanOpts.meta.tic_ldesc = ldesc; } @@ -1793,46 +2197,52 @@ function FTNMessageScanTossModule() { // const hashTags = localInfo.hashTags || - _.get(Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'hashTags' ] ); // catch-all*/ + _.get(Config().scannerTossers.ftn_bso.nodes, [ + localInfo.node, + 'tic', + 'hashTags', + ]); // catch-all*/ - if(hashTags) { + 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'); - } - - localInfo.fileEntry = fileEntry; - return callback(err, localInfo); + 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}`)); + 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}`)); + 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. @@ -1841,82 +2251,124 @@ function FTNMessageScanTossModule() { // We will still fallback as needed from -> -> // const descPriority = _.get( - Config().scannerTossers.ftn_bso.nodes, [ localInfo.node, 'tic', 'descPriority' ], + Config().scannerTossers.ftn_bso.nodes, + [localInfo.node, 'tic', 'descPriority'], Config().scannerTossers.ftn_bso.tic.descPriority ); - if('tic' === descPriority) { + if ('tic' === descPriority) { const origDesc = localInfo.fileEntry.desc; - localInfo.fileEntry.desc = ticFileInfo.getAsString('Ldesc') || origDesc || getDescFromFileName(ticFileInfo.filePath); + 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); + 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}`)); + if (!areaStorageDir) { + return callback( + Errors.UnexpectedState( + `Could not get storage directory for tag ${localInfo.areaTag}` + ) + ); } const isUpdate = localInfo.existingFileId ? true : false; - if(isUpdate) { + if (isUpdate) { // we need to *update* an existing record/file localInfo.fileEntry.fileId = localInfo.existingFileId; } 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); + } + + localInfo.newPath = dst; + + localInfo.fileEntry.persist(isUpdate, err => { + return callback(err, localInfo); + }); } - - if(dst !== finalPath) { - localInfo.fileEntry.fileName = paths.basename(finalPath); - } - - localInfo.newPath = dst; - - 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) { + 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); // if we updated a file in place, don't delete it! if (localInfo.newPath === oldPath) { - Log.trace({path : oldPath}, 'TIC file replaced in place. Nothing to remove.'); + Log.trace( + { path: oldPath }, + 'TIC file replaced in place. Nothing to remove.' + ); return callback(null, localInfo); } fs.unlink(oldPath, err => { - if(err) { - Log.warn( { error : err.message, oldPath : oldPath }, 'Failed removing old physical file during TIC replacement'); + if (err) { + Log.warn( + { error: err.message, oldPath: oldPath }, + 'Failed removing old physical file during TIC replacement' + ); } else { - Log.trace( { oldPath : oldPath }, 'Removed old physical file during TIC replacement'); + Log.trace( + { oldPath: oldPath }, + 'Removed old physical file during TIC replacement' + ); } - return callback(null, localInfo); // continue even if err + return callback(null, localInfo); // continue even if err }); }, ], (err, localInfo) => { - if(err) { - Log.error( { error : err.message, reason : err.reason, tic : ticFileInfo.filePath }, 'Failed to import/update TIC' ); + if (err) { + Log.error( + { + error: err.message, + reason: err.reason, + tic: ticFileInfo.filePath, + }, + 'Failed to import/update TIC' + ); } else { Log.info( - { tic : ticFileInfo.path, file : ticFileInfo.filePath, area : localInfo.areaTag }, + { + tic: ticFileInfo.path, + file: ticFileInfo.filePath, + area: localInfo.areaTag, + }, 'TIC imported successfully' ); } @@ -1925,21 +2377,28 @@ function FTNMessageScanTossModule() { ); }; - this.removeAssocTicFiles = function(ticFileInfo, cb) { - async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { - fs.unlink(path, err => { - if(err && 'ENOENT' !== err.code) { // don't log when the file doesn't exist - 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) { + 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 @@ -1947,78 +2406,95 @@ function FTNMessageScanTossModule() { // // NOTE: If StateFlags0 starts to use additional bits, we'll likely need to check them here! // - const getNewUuidsSql = - `SELECT message_id, message_uuid + const getNewUuidsSql = `SELECT message_id, message_uuid FROM message m WHERE area_tag = ? AND message_id > ? AND (SELECT COUNT(message_id) FROM message_meta WHERE message_id = m.message_id AND meta_category = 'System' AND meta_name = 'state_flags0') = 0 - ORDER BY message_id;` - ; - + 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); + 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(); - } - - // - // 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'); - - callback(err, newLastScanId); - }); - }, - function updateLastScanId(newLastScanId, callback) { - self.setAreaLastScanId(areaTag, newLastScanId, callback); - } - ], - () => { + async.each( + areaTags, + (areaTag, nextArea) => { + const areaConfig = config.messageNetworks.ftn.areas[areaTag]; + if (!this.isAreaConfigValid(areaConfig)) { return nextArea(); } - ); - }, - err => { - return cb(err); - }); + + // + // 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' + ); + + callback(err, newLastScanId); + } + ); + }, + function updateLastScanId(newLastScanId, callback) { + self.setAreaLastScanId(areaTag, newLastScanId, callback); + }, + ], + () => { + return nextArea(); + } + ); + }, + err => { + return cb(err); + } + ); }; - this.performNetMailExport = function(cb) { + 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. @@ -2029,8 +2505,7 @@ function FTNMessageScanTossModule() { // // :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 + const getNewUuidsSql = `SELECT message_id, message_uuid FROM message m WHERE area_tag = '${Message.WellKnownAreaTags.Private}' AND message_id > ? AND (SELECT COUNT(message_id) @@ -2053,16 +2528,19 @@ function FTNMessageScanTossModule() { async.waterfall( [ function getLastScanId(callback) { - return self.getAreaLastScanId(Message.WellKnownAreaTags.Private, callback); + return self.getAreaLastScanId( + Message.WellKnownAreaTags.Private, + callback + ); }, function getNewUuids(lastScanId, callback) { - msgDb.all(getNewUuidsSql, [ lastScanId ], (err, rows) => { - if(err) { + 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); @@ -2071,7 +2549,7 @@ function FTNMessageScanTossModule() { function exportMessages(rows, callback) { const messageUuids = rows.map(r => r.message_uuid); return self.exportNetMailMessagesToUplinks(messageUuids, callback); - } + }, ], err => { return cb(err); @@ -2079,10 +2557,13 @@ function FTNMessageScanTossModule() { ); }; - 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); + Message.AddressFlavor.FTN === + _.get(message, 'meta.System.external_flavor', null) + ); }; } @@ -2090,7 +2571,7 @@ 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) { +FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function (importDir, cb) { // :TODO: pass in 'inbound' vs 'secInbound' -- pass along to processSingleTicFile() where password will be checked const self = this; @@ -2098,69 +2579,86 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD [ function findTicFiles(callback) { fs.readdir(importDir, (err, files) => { - if(err) { + if (err) { return callback(err); } - return callback(null, files.filter(f => '.tic' === paths.extname(f).toLowerCase())); + 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); - }); + return nextFile(null); + }); + }, + err => { + return callback(err, ticFilesInfo); + } + ); }, function process(ticFilesInfo, callback) { - async.eachSeries(ticFilesInfo, (ticFileInfo, nextTicInfo) => { - self.processSingleTicFile(ticFileInfo, err => { - if(err) { - // :TODO: If ENOENT -OR- failed due to CRC mismatch: create a pending state & try again later; the "attached" file may not yet be ready. + async.eachSeries( + ticFilesInfo, + (ticFileInfo, nextTicInfo) => { + self.processSingleTicFile(ticFileInfo, err => { + if (err) { + // :TODO: If ENOENT -OR- failed due to CRC mismatch: create a pending state & try again later; the "attached" file may not yet be ready. - // archive rejected TIC stuff (.TIC + attach) - async.each( [ ticFileInfo.path, ticFileInfo.filePath ], (path, nextPath) => { - if(!path) { // possibly rejected due to "File" not existing/etc. - return nextPath(null); - } + // 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', + self.maybeArchiveImportFile( + path, + 'tic', + 'reject', + () => { + return nextPath(null); + } + ); + }, () => { - return nextPath(null); + self.removeAssocTicFiles(ticFileInfo, () => { + return nextTicInfo(null); + }); } ); - }, - () => { + } else { self.removeAssocTicFiles(ticFileInfo, () => { return nextTicInfo(null); }); - }); - } else { - self.removeAssocTicFiles(ticFileInfo, () => { - return nextTicInfo(null); - }); - } - }); - }, err => { - return callback(err); - }); - } + } + }); + }, + err => { + return callback(err); + } + ); + }, ], err => { return cb(err); @@ -2168,94 +2666,117 @@ FTNMessageScanTossModule.prototype.processTicFilesInDirectory = function(importD ); }; -FTNMessageScanTossModule.prototype.startup = function(cb) { +FTNMessageScanTossModule.prototype.startup = function (cb) { Log.info(`${exports.moduleInfo.name} Scanner/Tosser starting up`); - this.hasValidConfiguration({ shouldLog : true }); // just check and log + this.hasValidConfiguration({ shouldLog: true }); // just check and log let importing = false; let self = this; function tryImportNow(reasonDesc, extraInfo) { - if(!importing) { + 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( () => { + self.performImport(() => { importing = false; }); } } this.createTempDirectories(err => { - if(err) { - Log.warn( { error : err.toStrong() }, 'Failed creating temporary directories!'); + 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) { + if (_.isObject(this.moduleConfig.schedule)) { + const exportSchedule = this.parseScheduleString( + this.moduleConfig.schedule.export + ); + if (exportSchedule) { Log.debug( { - schedule : this.moduleConfig.schedule.export, - schedOK : -1 === _.get(exportSchedule, 'sched.error'), - next : exportSchedule.sched ? moment(later.schedule(exportSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', - immediate : exportSchedule.immediate ? true : false, + schedule: this.moduleConfig.schedule.export, + schedOK: -1 === _.get(exportSchedule, 'sched.error'), + next: exportSchedule.sched + ? moment(later.schedule(exportSchedule.sched).next(1)).format( + 'ddd, MMM Do, YYYY @ h:m:ss a' + ) + : 'N/A', + immediate: exportSchedule.immediate ? true : false, }, 'Export schedule loaded' ); - 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.performExport(() => { this.exportingEnd(); }); } }, exportSchedule.sched); } - if(_.isBoolean(exportSchedule.immediate)) { + if (_.isBoolean(exportSchedule.immediate)) { this.exportImmediate = exportSchedule.immediate; } } - const importSchedule = this.parseScheduleString(this.moduleConfig.schedule.import); - if(importSchedule) { + const importSchedule = this.parseScheduleString( + this.moduleConfig.schedule.import + ); + if (importSchedule) { Log.debug( { - schedule : this.moduleConfig.schedule.import, - schedOK : -1 === _.get(importSchedule, 'sched.error'), - next : importSchedule.sched ? moment(later.schedule(importSchedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', - watchFile : _.isString(importSchedule.watchFile) ? importSchedule.watchFile : 'None', + schedule: this.moduleConfig.schedule.import, + schedOK: -1 === _.get(importSchedule, 'sched.error'), + next: importSchedule.sched + ? moment(later.schedule(importSchedule.sched).next(1)).format( + 'ddd, MMM Do, YYYY @ h:m:ss a' + ) + : 'N/A', + watchFile: _.isString(importSchedule.watchFile) + ? importSchedule.watchFile + : 'None', }, 'Import schedule loaded' ); - if(importSchedule.sched) { - this.importTimer = later.setInterval( () => { + 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 => { + ['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 ( + paths.join(fileRoot, fileName) === + importSchedule.watchFile + ) { + tryImportNow('Performing import/toss due to @watch', { + eventPath, + event, + }); } }); }); @@ -2265,8 +2786,11 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { // 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 (exists) { + tryImportNow('Performing import/toss due to @watch', { + eventPath: importSchedule.watchFile, + event: 'initial exists', + }); } }); } @@ -2277,26 +2801,26 @@ FTNMessageScanTossModule.prototype.startup = function(cb) { }); }; -FTNMessageScanTossModule.prototype.shutdown = function(cb) { +FTNMessageScanTossModule.prototype.shutdown = function (cb) { Log.info('FidoNet Scanner/Tosser shutting down'); - if(this.exportTimer) { + if (this.exportTimer) { this.exportTimer.clear(); } - if(this.importTimer) { + if (this.importTimer) { this.importTimer.clear(); } // // Clean up temp dir/files we created // - temptmp.cleanup( paths => { + temptmp.cleanup(paths => { const fullStats = { - exportDir : this.exportTempDir, - importTemp : this.importTempDir, - paths : paths, - sessionId : temptmp.sessionId, + exportDir: this.exportTempDir, + importTemp: this.importTempDir, + paths: paths, + sessionId: temptmp.sessionId, }; Log.trace(fullStats, 'Temporary directories cleaned up'); @@ -2307,86 +2831,101 @@ FTNMessageScanTossModule.prototype.shutdown = function(cb) { FTNMessageScanTossModule.super_.prototype.shutdown.call(this, cb); }; -FTNMessageScanTossModule.prototype.performImport = function(cb) { - if(!this.hasValidConfiguration()) { +FTNMessageScanTossModule.prototype.performImport = function (cb) { + if (!this.hasValidConfiguration()) { return cb(Errors.MissingConfig('Invalid or missing configuration')); } const self = this; - async.each( [ 'inbound', 'secInbound' ], (inboundType, nextDir) => { - const importDir = self.moduleConfig.paths[inboundType]; - self.importFromDirectory(inboundType, importDir, err => { - if (err) { - Log.trace({ importDir, error : err.message }, 'Cannot perform FTN import for directory'); - } + async.each( + ['inbound', 'secInbound'], + (inboundType, nextDir) => { + const importDir = self.moduleConfig.paths[inboundType]; + self.importFromDirectory(inboundType, importDir, err => { + if (err) { + Log.trace( + { importDir, error: err.message }, + 'Cannot perform FTN import for directory' + ); + } - return nextDir(null); - }); - }, cb); + return nextDir(null); + }); + }, + cb + ); }; -FTNMessageScanTossModule.prototype.performExport = function(cb) { +FTNMessageScanTossModule.prototype.performExport = function (cb) { // // We're only concerned with areas related to FTN. For each area, loop though // and let's find out what messages need exported. // - if(!this.hasValidConfiguration()) { + if (!this.hasValidConfiguration()) { return cb(Errors.MissingConfig('Invalid or missing configuration')); } const self = this; - async.eachSeries( [ 'EchoMail', 'NetMail' ], (type, nextType) => { - self[`perform${type}Export`]( err => { - if(err) { - Log.warn( { type, error : err.message }, '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({ type, error: err.message }, 'Error(s) during export'); + } + return nextType(null); // try next, always + }); + }, + () => { + return cb(null); + } + ); }; -FTNMessageScanTossModule.prototype.record = function(message) { +FTNMessageScanTossModule.prototype.record = function (message) { // // This module works off schedules, but we do support @immediate for export // - if(true !== this.exportImmediate || !this.hasValidConfiguration()) { + if (true !== this.exportImmediate || !this.hasValidConfiguration()) { return; } - const info = { uuid : message.messageUuid, subject : message.subject }; + const info = { uuid: message.messageUuid, subject: message.subject }; function exportLog(err) { - if(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.messageUuid ], err => { - this.exportingEnd( () => exportLog(err) ); + if (this.exportingStart()) { + this.exportNetMailMessagesToUplinks([message.messageUuid], err => { + this.exportingEnd(() => exportLog(err)); }); } - } else if(message.areaTag) { - Object.assign(info, { type : 'EchoMail' } ); + } else if (message.areaTag) { + Object.assign(info, { type: 'EchoMail' }); const areaConfig = Config().messageNetworks.ftn.areas[message.areaTag]; - if(!this.isAreaConfigValid(areaConfig)) { + if (!this.isAreaConfigValid(areaConfig)) { return; } - if(this.exportingStart()) { - this.exportEchoMailMessagesToUplinks( [ message.messageUuid ], areaConfig, err => { - this.exportingEnd( () => exportLog(err) ); - }); + if (this.exportingStart()) { + this.exportEchoMailMessagesToUplinks( + [message.messageUuid], + areaConfig, + err => { + this.exportingEnd(() => exportLog(err)); + } + ); } } }; diff --git a/core/servers/chat/mrc_multiplexer.js b/core/servers/chat/mrc_multiplexer.js index 306e73c8..68cf64d7 100644 --- a/core/servers/chat/mrc_multiplexer.js +++ b/core/servers/chat/mrc_multiplexer.js @@ -2,30 +2,29 @@ 'use strict'; // ENiGMA½ -const Log = require('../../logger.js').log; -const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').get; -const { Errors } = require('../../enig_error.js'); -const SysProps = require('../../system_property.js'); -const StatLog = require('../../stat_log.js'); +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); +const SysProps = require('../../system_property.js'); +const StatLog = require('../../stat_log.js'); // deps -const net = require('net'); -const _ = require('lodash'); -const os = require('os'); - +const net = require('net'); +const _ = require('lodash'); +const os = require('os'); // MRC -const protocolVersion = '1.2.9'; -const lineDelimiter = new RegExp('\r\n|\r|\n'); // eslint-disable-line no-control-regex +const protocolVersion = '1.2.9'; +const lineDelimiter = new RegExp('\r\n|\r|\n'); // eslint-disable-line no-control-regex -const ModuleInfo = exports.moduleInfo = { - name : 'MRC', - desc : 'An MRC Chat Multiplexer', - author : 'RiPuk', - packageName : 'codes.l33t.enigma.mrc.server', - notes : 'https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform', -}; +const ModuleInfo = (exports.moduleInfo = { + name: 'MRC', + desc: 'An MRC Chat Multiplexer', + author: 'RiPuk', + packageName: 'codes.l33t.enigma.mrc.server', + notes: 'https://bbswiki.bottomlessabyss.net/index.php?title=MRC_Chat_platform', +}); const connectedSockets = new Set(); @@ -33,29 +32,30 @@ exports.getModule = class MrcModule extends ServerModule { constructor() { super(); - this.log = Log.child( { server : 'MRC' } ); + this.log = Log.child({ server: 'MRC' }); - const config = Config(); - this.boardName = config.general.prettyBoardName || config.general.boardName; + const config = Config(); + this.boardName = config.general.prettyBoardName || config.general.boardName; this.mrcConnectOpts = { - host : config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', - port : config.chatServers.mrc.serverPort || 5000, - retryDelay : config.chatServers.mrc.retryDelay || 10000 + host: config.chatServers.mrc.serverHostname || 'mrc.bottomlessabyss.net', + port: config.chatServers.mrc.serverPort || 5000, + retryDelay: config.chatServers.mrc.retryDelay || 10000, }; } _connectionHandler() { const enigmaVersion = 'ENiGMA½-BBS_' + require('../../../package.json').version; - const handshake = `${this.boardName}~${enigmaVersion}/${os.platform()}.${os.arch()}/${protocolVersion}`; - this.log.debug({ handshake : handshake }, 'Handshaking with MRC server'); + const handshake = `${ + this.boardName + }~${enigmaVersion}/${os.platform()}.${os.arch()}/${protocolVersion}`; + this.log.debug({ handshake: handshake }, 'Handshaking with MRC server'); this.sendRaw(handshake); this.log.info(this.mrcConnectOpts, 'Connected to MRC server'); } createServer(cb) { - if (!this.enabled) { return cb(null); } @@ -74,11 +74,19 @@ exports.getModule = class MrcModule extends ServerModule { const config = Config(); const port = parseInt(config.chatServers.mrc.multiplexerPort); - if(isNaN(port)) { - this.log.warn( { port : config.chatServers.mrc.multiplexerPort, server : ModuleInfo.name }, 'Invalid port' ); - return cb(Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`)); + if (isNaN(port)) { + this.log.warn( + { port: config.chatServers.mrc.multiplexerPort, server: ModuleInfo.name }, + 'Invalid port' + ); + return cb( + Errors.Invalid(`Invalid port: ${config.chatServers.mrc.multiplexerPort}`) + ); } - Log.info( { server : ModuleInfo.name, port : config.chatServers.mrc.multiplexerPort }, 'MRC multiplexer starting up'); + Log.info( + { server: ModuleInfo.name, port: config.chatServers.mrc.multiplexerPort }, + 'MRC multiplexer starting up' + ); return this.server.listen(port, cb); } @@ -89,7 +97,10 @@ exports.getModule = class MrcModule extends ServerModule { const self = this; // create connection to MRC server - this.mrcClient = net.createConnection(this.mrcConnectOpts, self._connectionHandler.bind(self)); + this.mrcClient = net.createConnection( + this.mrcConnectOpts, + self._connectionHandler.bind(self) + ); this.mrcClient.requestedDisconnect = false; @@ -97,7 +108,7 @@ exports.getModule = class MrcModule extends ServerModule { let buffer = new Buffer.from(''); function handleData(chunk) { - if(_.isString(chunk)) { + if (_.isString(chunk)) { buffer += chunk; } else { buffer = Buffer.concat([buffer, chunk]); @@ -113,7 +124,7 @@ exports.getModule = class MrcModule extends ServerModule { buffer = new Buffer.from(''); } - lines.forEach( line => { + lines.forEach(line => { if (line.length) { let message = self.parseMessage(line); if (message) { @@ -123,7 +134,7 @@ exports.getModule = class MrcModule extends ServerModule { }); } - this.mrcClient.on('data', (data) => { + this.mrcClient.on('data', data => { handleData(data); }); @@ -132,52 +143,61 @@ exports.getModule = class MrcModule extends ServerModule { }); this.mrcClient.on('close', () => { + if (this.mrcClient && this.mrcClient.requestedDisconnect) return; - if (this.mrcClient && this.mrcClient.requestedDisconnect) - return; + this.log.info( + this.mrcConnectOpts, + 'Disconnected from MRC server, reconnecting' + ); + this.log.debug( + 'Waiting ' + this.mrcConnectOpts.retryDelay + 'ms before retrying' + ); - this.log.info(this.mrcConnectOpts, 'Disconnected from MRC server, reconnecting'); - this.log.debug('Waiting ' + this.mrcConnectOpts.retryDelay + 'ms before retrying'); - - setTimeout(function() { + setTimeout(function () { self.connectToMrc(); }, this.mrcConnectOpts.retryDelay); }); this.mrcClient.on('error', err => { - this.log.info( { error : err.message }, 'MRC server error'); + this.log.info({ error: err.message }, 'MRC server error'); }); } createLocalListener() { // start a local server for clients to connect to - this.server = net.createServer( socket => { + this.server = net.createServer(socket => { socket.setEncoding('ascii'); socket.on('data', data => { // split on \n to deal with getting messages in batches - data.toString().split(lineDelimiter).forEach( item => { - if (item == '') return; + data.toString() + .split(lineDelimiter) + .forEach(item => { + if (item == '') return; - // save username with socket - if(item.startsWith('--DUDE-ITS--')) { - connectedSockets.add(socket); - socket.username = item.split('|')[1]; - Log.debug( { server : 'MRC', user: socket.username } , 'User connected'); - } else { - this.receiveFromClient(socket.username, item); - } - }); + // save username with socket + if (item.startsWith('--DUDE-ITS--')) { + connectedSockets.add(socket); + socket.username = item.split('|')[1]; + Log.debug( + { server: 'MRC', user: socket.username }, + 'User connected' + ); + } else { + this.receiveFromClient(socket.username, item); + } + }); }); - socket.on('end', function() { + socket.on('end', function () { connectedSockets.delete(socket); }); socket.on('error', err => { - if('ECONNRESET' !== err.code) { // normal - this.log.error( { error: err.message }, 'MRC error' ); + if ('ECONNRESET' !== err.code) { + // normal + this.log.error({ error: err.message }, 'MRC error'); } }); }); @@ -196,8 +216,14 @@ exports.getModule = class MrcModule extends ServerModule { * Sends received messages to local clients */ sendToClient(message) { - connectedSockets.forEach( (client) => { - if (message.to_user == '' || message.to_user == client.username || message.to_user == 'CLIENT' || message.from_user == client.username || message.to_user == 'NOTME' ) { + connectedSockets.forEach(client => { + if ( + message.to_user == '' || + message.to_user == client.username || + message.to_user == 'CLIENT' || + message.from_user == client.username || + message.to_user == 'NOTME' + ) { // this.log.debug({ server : 'MRC', username : client.username, message : message }, 'Forwarding message to connected user'); client.write(JSON.stringify(message) + '\n'); } @@ -212,16 +238,59 @@ exports.getModule = class MrcModule extends ServerModule { if (message.from_user == 'SERVER' && message.body == 'HELLO') { // reply with extra bbs info - this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}`); - this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOWEB:${config.general.website}`); - this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOTEL:${config.general.telnetHostname}`); - this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFOSSH:${config.general.sshHostname}`); - this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `INFODSC:${config.general.description}`); - - } else if (message.from_user == 'SERVER' && message.body.toUpperCase() == 'PING') { + this.sendToMrcServer( + 'CLIENT', + '', + 'SERVER', + 'ALL', + '', + `INFOSYS:${StatLog.getSystemStat(SysProps.SysOpUsername)}` + ); + this.sendToMrcServer( + 'CLIENT', + '', + 'SERVER', + 'ALL', + '', + `INFOWEB:${config.general.website}` + ); + this.sendToMrcServer( + 'CLIENT', + '', + 'SERVER', + 'ALL', + '', + `INFOTEL:${config.general.telnetHostname}` + ); + this.sendToMrcServer( + 'CLIENT', + '', + 'SERVER', + 'ALL', + '', + `INFOSSH:${config.general.sshHostname}` + ); + this.sendToMrcServer( + 'CLIENT', + '', + 'SERVER', + 'ALL', + '', + `INFODSC:${config.general.description}` + ); + } else if ( + message.from_user == 'SERVER' && + message.body.toUpperCase() == 'PING' + ) { // reply to heartbeat - this.sendToMrcServer('CLIENT', '', 'SERVER', 'ALL', '', `IMALIVE:${this.boardName}`); - + this.sendToMrcServer( + 'CLIENT', + '', + 'SERVER', + 'ALL', + '', + `IMALIVE:${this.boardName}` + ); } else { // if not a heartbeat, and we have clients then we need to send something to them this.sendToClient(message); @@ -232,8 +301,8 @@ exports.getModule = class MrcModule extends ServerModule { * Takes an MRC message and parses it into something usable */ parseMessage(line) { - - const [from_user, from_site, from_room, to_user, to_site, to_room, body ] = line.split('~'); + const [from_user, from_site, from_room, to_user, to_site, to_room, body] = + line.split('~'); // const msg = line.split('~'); // if (msg.length < 7) { @@ -249,9 +318,19 @@ exports.getModule = class MrcModule extends ServerModule { receiveFromClient(username, message) { try { message = JSON.parse(message); - this.sendToMrcServer(message.from_user, message.from_room, message.to_user, message.to_site, message.to_room, message.body); + this.sendToMrcServer( + message.from_user, + message.from_room, + message.to_user, + message.to_site, + message.to_room, + message.body + ); } catch (e) { - Log.debug({ server : 'MRC', user : username, message : message }, 'Dodgy message received from client'); + Log.debug( + { server: 'MRC', user: username, message: message }, + 'Dodgy message received from client' + ); } } @@ -259,16 +338,16 @@ exports.getModule = class MrcModule extends ServerModule { * Converts a message back into the MRC format and sends it to the central MRC server */ sendToMrcServer(fromUser, fromRoom, toUser, toSite, toRoom, messageBody) { - - const line = [ - fromUser, - this.boardName, - sanitiseRoomName(fromRoom || ''), - sanitiseName(toUser || ''), - sanitiseName(toSite || ''), - sanitiseRoomName(toRoom || ''), - sanitiseMessage(messageBody || '') - ].join('~') + '~'; + const line = + [ + fromUser, + this.boardName, + sanitiseRoomName(fromRoom || ''), + sanitiseName(toUser || ''), + sanitiseName(toSite || ''), + sanitiseRoomName(toRoom || ''), + sanitiseMessage(messageBody || ''), + ].join('~') + '~'; // Log.debug({ server : 'MRC', data : line }, 'Sending data'); this.sendRaw(line); @@ -284,13 +363,13 @@ exports.getModule = class MrcModule extends ServerModule { * User / site name must be ASCII 33-125, no MCI, 30 chars max, underscores */ function sanitiseName(str) { - return str.replace( - /\s/g, '_' - ).replace( - /[^\x21-\x7D]|(\|\w\w)/g, '' // Non-printable & MCI - ).substr( - 0, 30 - ); + return str + .replace(/\s/g, '_') + .replace( + /[^\x21-\x7D]|(\|\w\w)/g, + '' // Non-printable & MCI + ) + .substr(0, 30); } function sanitiseRoomName(message) { @@ -300,4 +379,3 @@ function sanitiseRoomName(message) { function sanitiseMessage(message) { return message.replace(/[^\x20-\x7D]/g, ''); } - diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index a817bd22..3ade127d 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -2,89 +2,91 @@ 'use strict'; // ENiGMA½ -const Log = require('../../logger.js').log; -const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').get; -const { Errors } = require('../../enig_error.js'); +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); const { splitTextAtTerms, isAnsi, - stripAnsiControlCodes -} = require('../../string_util.js'); + stripAnsiControlCodes, +} = require('../../string_util.js'); const { getMessageConferenceByTag, getMessageAreaByTag, getMessageListForArea, -} = require('../../message_area.js'); -const { sortAreasOrConfs } = require('../../conf_area_util.js'); -const AnsiPrep = require('../../ansi_prep.js'); -const { wordWrapText } = require('../../word_wrap.js'); -const { stripMciColorCodes } = require('../../color_codes.js'); +} = require('../../message_area.js'); +const { sortAreasOrConfs } = require('../../conf_area_util.js'); +const AnsiPrep = require('../../ansi_prep.js'); +const { wordWrapText } = require('../../word_wrap.js'); +const { stripMciColorCodes } = require('../../color_codes.js'); // deps -const net = require('net'); -const _ = require('lodash'); -const fs = require('graceful-fs'); -const paths = require('path'); -const moment = require('moment'); +const net = require('net'); +const _ = require('lodash'); +const fs = require('graceful-fs'); +const paths = require('path'); +const moment = require('moment'); -const ModuleInfo = exports.moduleInfo = { - name : 'Gopher', - desc : 'A RFC-1436-ish Gopher Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.gopher.server', - notes : 'https://tools.ietf.org/html/rfc1436', -}; +const ModuleInfo = (exports.moduleInfo = { + name: 'Gopher', + desc: 'A RFC-1436-ish Gopher Server', + author: 'NuSkooler', + packageName: 'codes.l33t.enigma.gopher.server', + notes: 'https://tools.ietf.org/html/rfc1436', +}); -const Message = require('../../message.js'); +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', + 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', + HtmlFile: 'h', + InfoMessage: 'i', + SoundFile: 's', }; exports.getModule = class GopherModule extends ServerModule { - 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(cb) { - if(!this.enabled) { + if (!this.enabled) { return cb(null); } const config = Config(); this.publicHostname = config.contentServers.gopher.publicHostname; - this.publicPort = config.contentServers.gopher.publicPort; + this.publicPort = config.contentServers.gopher.publicPort; - 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( + /^\/?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(/^(\/?[^\t\r\n]*)\r\n$/, this.staticGenerator); - this.server = net.createServer( socket => { + this.server = net.createServer(socket => { socket.setEncoding('ascii'); socket.on('data', data => { @@ -100,8 +102,9 @@ exports.getModule = class GopherModule extends ServerModule { }); socket.on('error', err => { - if('ECONNRESET' !== err.code) { // normal - this.log.trace( { error : err.message }, 'Socket error'); + if ('ECONNRESET' !== err.code) { + // normal + this.log.trace({ error: err.message }, 'Socket error'); } }); }); @@ -110,37 +113,46 @@ exports.getModule = class GopherModule extends ServerModule { } listen(cb) { - if(!this.enabled) { + if (!this.enabled) { return cb(null); } const config = Config(); const port = parseInt(config.contentServers.gopher.port); - if(isNaN(port)) { - this.log.warn( { port : config.contentServers.gopher.port, server : ModuleInfo.name }, 'Invalid port' ); - return cb(Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`)); + if (isNaN(port)) { + this.log.warn( + { port: config.contentServers.gopher.port, server: ModuleInfo.name }, + 'Invalid port' + ); + return cb( + Errors.Invalid(`Invalid port: ${config.contentServers.gopher.port}`) + ); } return this.server.listen(port, config.contentServers.gopher.address, cb); } get enabled() { - return _.get(Config(), 'contentServers.gopher.enabled', false) && this.isConfigured(); + return ( + _.get(Config(), 'contentServers.gopher.enabled', false) && this.isConfigured() + ); } isConfigured() { // public hostname & port must be set; responses contain them! const config = Config(); - return _.isString(_.get(config, 'contentServers.gopher.publicHostname')) && - _.isNumber(_.get(config, 'contentServers.gopher.publicPort')); + return ( + _.isString(_.get(config, 'contentServers.gopher.publicHostname')) && + _.isNumber(_.get(config, 'contentServers.gopher.publicPort')) + ); } addRoute(selectorRegExp, generatorHandler) { - if(_.isString(selectorRegExp)) { + if (_.isString(selectorRegExp)) { try { selectorRegExp = new RegExp(`${selectorRegExp}\r\n`); - } catch(e) { - this.log.warn( { pattern : selectorRegExp }, 'Invalid RegExp for selector' ); + } catch (e) { + this.log.warn({ pattern: selectorRegExp }, 'Invalid RegExp for selector'); return false; } } @@ -149,9 +161,9 @@ exports.getModule = class GopherModule extends ServerModule { routeRequest(selector, socket) { let match; - for(let [regex, gen] of this.routes) { + for (let [regex, gen] of this.routes) { match = selector.match(regex); - if(match) { + if (match) { return gen(match, res => { return socket.end(`${res}`); }); @@ -163,14 +175,17 @@ exports.getModule = class GopherModule extends ServerModule { } makeItem(itemType, text, selector, hostname, port) { - selector = selector || ''; // e.g. for info + selector = selector || ''; // e.g. for info hostname = hostname || this.publicHostname; port = port || this.publicPort; return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; } staticGenerator(selectorMatch, cb) { - this.log.debug( { selector : selectorMatch[1] || '(gophermap)' }, 'Serving static content'); + this.log.debug( + { selector: selectorMatch[1] || '(gophermap)' }, + 'Serving static content' + ); const requestedPath = selectorMatch[1]; let path = this.resolveContentPath(requestedPath); @@ -192,7 +207,11 @@ exports.getModule = class GopherModule extends ServerModule { fs.readFile(path, isGopherMap ? 'utf8' : null, (err, content) => { if (err) { let content = 'You have reached an ENiGMA½ Gopher server!\r\n'; - content += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); + content += this.makeItem( + ItemTypes.SubMenu, + 'Public Message Area', + '/msgarea' + ); return cb(content); } @@ -220,12 +239,17 @@ exports.getModule = class GopherModule extends ServerModule { } notFoundGenerator(selector, cb) { - this.log.debug( { selector }, 'Serving not found content'); + this.log.debug({ selector }, 'Serving not found content'); return cb('Not found'); } isAreaAndConfExposed(confTag, areaTag) { - const conf = _.get(Config(), [ 'contentServers', 'gopher', 'messageConferences', confTag ]); + const conf = _.get(Config(), [ + 'contentServers', + 'gopher', + 'messageConferences', + confTag, + ]); return Array.isArray(conf) && conf.includes(areaTag); } @@ -248,14 +272,14 @@ exports.getModule = class GopherModule extends ServerModule { // to follow the KISS principle: Wrap at 79. // const WordWrapColumn = 79; - if(isAnsi(body)) { + if (isAnsi(body)) { AnsiPrep( body, { - cols : WordWrapColumn, // See notes above - forceLineTerm : true, // Ensure each line is term'd - asciiMode : true, // Export to ASCII - fillLines : false, // Don't fill up to |cols| + cols: WordWrapColumn, // See notes above + forceLineTerm: true, // Ensure each line is term'd + asciiMode: true, // Export to ASCII + fillLines: false, // Don't fill up to |cols| }, (err, prepped) => { return cb(prepped || body); @@ -263,23 +287,24 @@ exports.getModule = class GopherModule extends ServerModule { ); } else { const cleaned = stripMciColorCodes( - stripAnsiControlCodes(body, { all : true } ) + stripAnsiControlCodes(body, { all: true }) ); - const prepped = - splitTextAtTerms(cleaned) - .map(l => (wordWrapText(l, { width : WordWrapColumn } ).wrapped || []).join('\n')) - .join('\n'); + const prepped = splitTextAtTerms(cleaned) + .map(l => + (wordWrapText(l, { width: WordWrapColumn }).wrapped || []).join('\n') + ) + .join('\n'); return cb(prepped); } } shortenSubject(subject) { - return _.truncate(subject, { length : 30 } ); + return _.truncate(subject, { length: 30 }); } messageAreaGenerator(selectorMatch, cb) { - this.log.debug( { selector : selectorMatch[0] }, 'Serving message area content'); + this.log.debug({ selector: selectorMatch[0] }, 'Serving message area content'); // // Selector should be: // /msgarea - list confs @@ -288,28 +313,40 @@ exports.getModule = class GopherModule extends ServerModule { // /msgarea/conftag/areatag/ - message as text // /msgarea/conftag/areatag/_raw - full message as text + headers // - if(selectorMatch[3] || selectorMatch[4]) { + 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(); + 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-existent message UUID!'); + return message.load({ uuid: msgUuid }, err => { + if (err) { + this.log.debug( + { uuid: msgUuid }, + 'Attempted access to non-existent 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!'); + 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!'); + if (Message.isPrivateAreaTag(areaTag)) { + this.log.warn( + { areaTag }, + 'Attempted access to message in private area!' + ); return this.notFoundGenerator(selectorMatch, cb); } @@ -326,26 +363,29 @@ ${msgBody} return cb(response); }); }); - } else if(selectorMatch[2]) { + } 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); + 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!'); + 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!'); + 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); } const filter = { - resultType : 'messageList', - sort : 'messageId', - order : 'descending', // we want newest messages first for Gopher + resultType: 'messageList', + sort: 'messageId', + order: 'descending', // we want newest messages first for Gopher }; return getMessageListForArea(null, areaTag, filter, (err, msgList) => { @@ -354,30 +394,48 @@ ${msgBody} this.makeItem(ItemTypes.InfoMessage, `Messages in ${area.name}`), this.makeItem(ItemTypes.InfoMessage, '(newest first)'), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - ...msgList.map(msg => this.makeItem( - ItemTypes.TextFile, - `${moment(msg.modTimestamp).format('YYYY-MM-DD hh:mma')}: ${this.shortenSubject(msg.subject)} (${msg.fromUserName} to ${msg.toUserName})`, - `/msgarea/${confTag}/${areaTag}/${msg.messageUuid}` - )) + ...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]) { + } 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) { + 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))) + 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); @@ -386,18 +444,33 @@ ${msgBody} this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), this.makeItem(ItemTypes.InfoMessage, `Message areas in ${conf.name}`), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), - ...areas.map(area => this.makeItem(ItemTypes.SubMenu, `${area.name} ${area.desc ? '- ' + area.desc : ''}`, `/msgarea/${confTag}/${area.areaTag}`)) + ...areas.map(area => + this.makeItem( + ItemTypes.SubMenu, + `${area.name} ${area.desc ? '- ' + area.desc : ''}`, + `/msgarea/${confTag}/${area.areaTag}` + ) + ), ].join(''); return cb(response); } 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 + 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); @@ -407,10 +480,16 @@ ${msgBody} this.makeItem(ItemTypes.InfoMessage, 'Available Message Conferences'), this.makeItem(ItemTypes.InfoMessage, '-'.repeat(70)), this.makeItem(ItemTypes.InfoMessage, ''), - ...confs.map(conf => this.makeItem(ItemTypes.SubMenu, `${conf.name} ${conf.desc ? '- ' + conf.desc : ''}`, `/msgarea/${conf.confTag}`)) + ...confs.map(conf => + this.makeItem( + ItemTypes.SubMenu, + `${conf.name} ${conf.desc ? '- ' + conf.desc : ''}`, + `/msgarea/${conf.confTag}` + ) + ), ].join(''); return cb(response); } } -}; \ No newline at end of file +}; diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 7c1a263b..95ed9326 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -2,43 +2,38 @@ 'use strict'; // ENiGMA½ -const Log = require('../../logger.js').log; -const { ServerModule } = require('../../server_module.js'); -const Config = require('../../config.js').get; -const { - getTransactionDatabase, - getModDatabasePath -} = require('../../database.js'); +const Log = require('../../logger.js').log; +const { ServerModule } = require('../../server_module.js'); +const Config = require('../../config.js').get; +const { getTransactionDatabase, getModDatabasePath } = require('../../database.js'); const { getMessageAreaByTag, getMessageConferenceByTag, -} = require('../../message_area.js'); -const User = require('../../user.js'); -const Errors = require('../../enig_error.js').Errors; -const Message = require('../../message.js'); -const FTNAddress = require('../../ftn_address.js'); +} = require('../../message_area.js'); +const User = require('../../user.js'); +const Errors = require('../../enig_error.js').Errors; +const Message = require('../../message.js'); +const FTNAddress = require('../../ftn_address.js'); const { isAnsi, stripAnsiControlCodes, splitTextAtTerms, -} = require('../../string_util.js'); -const AnsiPrep = require('../../ansi_prep.js'); -const { - stripMciColorCodes -} = require('../../color_codes.js'); +} = require('../../string_util.js'); +const AnsiPrep = require('../../ansi_prep.js'); +const { stripMciColorCodes } = require('../../color_codes.js'); // deps -const NNTPServerBase = require('nntp-server'); -const _ = require('lodash'); -const fs = require('fs-extra'); -const forEachSeries = require('async/forEachSeries'); -const asyncReduce = require('async/reduce'); -const asyncMap = require('async/map'); -const asyncSeries = require('async/series'); -const asyncWaterfall = require('async/waterfall'); -const LRU = require('lru-cache'); -const sqlite3 = require('sqlite3'); -const paths = require('path'); +const NNTPServerBase = require('nntp-server'); +const _ = require('lodash'); +const fs = require('fs-extra'); +const forEachSeries = require('async/forEachSeries'); +const asyncReduce = require('async/reduce'); +const asyncMap = require('async/map'); +const asyncSeries = require('async/series'); +const asyncWaterfall = require('async/waterfall'); +const LRU = require('lru-cache'); +const sqlite3 = require('sqlite3'); +const paths = require('path'); // // Network News Transfer Protocol (NNTP) @@ -51,13 +46,13 @@ const paths = require('path'); // exports.moduleInfo = { - name : 'NNTP', - desc : 'Network News Transfer Protocol (NNTP) Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.nntp.server', + name: 'NNTP', + desc: 'Network News Transfer Protocol (NNTP) Server', + author: 'NuSkooler', + packageName: 'codes.l33t.enigma.nntp.server', }; -exports.performMaintenanceTask = performMaintenanceTask; +exports.performMaintenanceTask = performMaintenanceTask; /* General TODO @@ -69,24 +64,24 @@ exports.performMaintenanceTask = performMaintenanceTask; // simple DB maps NNTP Message-ID's which are // sequential per group -> ENiG messages // A single instance is shared across NNTP and/or NNTPS -class NNTPDatabase -{ - constructor() { - } +class NNTPDatabase { + constructor() {} init(cb) { asyncSeries( [ - (callback) => { - this.db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(exports.moduleInfo), - err => { - return callback(err); - } - )); + callback => { + this.db = getTransactionDatabase( + new sqlite3.Database( + getModDatabasePath(exports.moduleInfo), + err => { + return callback(err); + } + ) + ); }, - (callback) => { - this.db.serialize( () => { + callback => { + this.db.serialize(() => { this.db.run( `CREATE TABLE IF NOT EXISTS nntp_area_message ( nntp_message_id INTEGER NOT NULL, @@ -105,7 +100,7 @@ class NNTPDatabase return callback(null); }); - } + }, ], err => { return cb(err); @@ -120,12 +115,12 @@ class NNTPServer extends NNTPServerBase { constructor(options, serverName) { super(options); - this.log = Log.child( { server : serverName } ); + this.log = Log.child({ server: serverName }); const config = Config(); this.groupCache = new LRU({ - max : _.get(config, 'contentServers.nntp.cache.maxItems', 200), - ttl : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s + max: _.get(config, 'contentServers.nntp.cache.maxItems', 200), + ttl: _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s }); } @@ -134,25 +129,31 @@ class NNTPServer extends NNTPServerBase { } _authenticate(session) { - const username = session.authinfo_user; - const password = session.authinfo_pass; + const username = session.authinfo_user; + const password = session.authinfo_pass; - this.log.trace( { username }, 'Authentication request'); + this.log.trace({ username }, 'Authentication request'); - return new Promise( resolve => { + return new Promise(resolve => { const user = new User(); - user.authenticateFactor1({ type : User.AuthFactor1Types.Password, username, password }, err => { - if(err) { - // :TODO: Log IP address - this.log.debug( { username, reason : err.message }, 'Authentication failure'); - return resolve(false); + user.authenticateFactor1( + { type: User.AuthFactor1Types.Password, username, password }, + err => { + if (err) { + // :TODO: Log IP address + this.log.debug( + { username, reason: err.message }, + 'Authentication failure' + ); + return resolve(false); + } + + session.authUser = user; + + this.log.debug({ username }, 'User authenticated successfully'); + return resolve(true); } - - session.authUser = user; - - this.log.debug( { username }, 'User authenticated successfully'); - return resolve(true); - }); + ); }); } @@ -170,24 +171,31 @@ class NNTPServer extends NNTPServerBase { // some sort of compliance. We also extend up to 5D addressing. // - If we have an email address, then it's ready to go. // - const remoteFrom = _.get(message.meta, [ 'System', Message.SystemMetaNames.RemoteFromUser ]); + const remoteFrom = _.get(message.meta, [ + 'System', + Message.SystemMetaNames.RemoteFromUser, + ]); let jamStyleFrom; - if(remoteFrom) { - const flavor = _.get(message.meta, [ 'System', Message.SystemMetaNames.ExternalFlavor ]); - switch(flavor) { - case [ Message.AddressFlavor.FTN ] : + if (remoteFrom) { + const flavor = _.get(message.meta, [ + 'System', + Message.SystemMetaNames.ExternalFlavor, + ]); + switch (flavor) { + case [Message.AddressFlavor.FTN]: { let ftnAddr = FTNAddress.fromString(remoteFrom); - if(ftnAddr && ftnAddr.isValid()) { + if (ftnAddr && ftnAddr.isValid()) { // In general, addresses are in point, node, net, zone, domain order - if(ftnAddr.domain) { // 5D + if (ftnAddr.domain) { + // 5D // point.node.net.zone@domain or node.net.zone@domain jamStyleFrom = `${ftnAddr.node}.${ftnAddr.net}.${ftnAddr.zone}@${ftnAddr.domain}`; - if(ftnAddr.point) { + if (ftnAddr.point) { jamStyleFrom = `${ftnAddr.point}.` + jamStyleFrom; } } else { - if(ftnAddr.point) { + if (ftnAddr.point) { jamStyleFrom = `${ftnAddr.point}@${ftnAddr.node}.${ftnAddr.net}.${ftnAddr.zone}`; } else { jamStyleFrom = `0@${ftnAddr.node}.${ftnAddr.net}.${ftnAddr.zone}`; @@ -197,13 +205,13 @@ class NNTPServer extends NNTPServerBase { } break; - case [ Message.AddressFlavor.Email ] : + case [Message.AddressFlavor.Email]: jamStyleFrom = `${fromName} <${remoteFrom}>`; break; } } - if(!jamStyleFrom) { + if (!jamStyleFrom) { jamStyleFrom = fromName; } @@ -218,42 +226,45 @@ class NNTPServer extends NNTPServerBase { // - https://tools.ietf.org/html/rfc5536#section-3.1 // - https://github.com/ftnapps/jamnntpd/blob/master/src/nntpserv.c#L962 // - const toName = this.getMessageTo(message); - const fromName = this.getMessageFrom(message); + const toName = this.getMessageTo(message); + const fromName = this.getMessageFrom(message); message.nntpHeaders = { - From : this.getJAMStyleFrom(message, fromName), - 'X-Comment-To' : toName, - Newsgroups : session.group.name, - Subject : message.subject, - Date : this.getMessageDate(message), - 'Message-ID' : this.getMessageIdentifier(message), - Path : 'ENiGMA1/2!not-for-mail', - 'Content-Type' : 'text/plain; charset=utf-8', + From: this.getJAMStyleFrom(message, fromName), + 'X-Comment-To': toName, + Newsgroups: session.group.name, + Subject: message.subject, + Date: this.getMessageDate(message), + 'Message-ID': this.getMessageIdentifier(message), + Path: 'ENiGMA1/2!not-for-mail', + 'Content-Type': 'text/plain; charset=utf-8', }; - const externalFlavor = _.get(message.meta.System, [ Message.SystemMetaNames.ExternalFlavor ]); - if(externalFlavor) { + const externalFlavor = _.get(message.meta.System, [ + Message.SystemMetaNames.ExternalFlavor, + ]); + if (externalFlavor) { message.nntpHeaders['X-ENiG-MessageFlavor'] = externalFlavor; } // Any FTN properties -> X-FTN-* _.each(message.meta.FtnProperty, (v, k) => { const suffix = { - [ Message.FtnPropertyNames.FtnTearLine ] : 'Tearline', - [ Message.FtnPropertyNames.FtnOrigin ] : 'Origin', - [ Message.FtnPropertyNames.FtnArea ] : 'AREA', - [ Message.FtnPropertyNames.FtnSeenBy ] : 'SEEN-BY', + [Message.FtnPropertyNames.FtnTearLine]: 'Tearline', + [Message.FtnPropertyNames.FtnOrigin]: 'Origin', + [Message.FtnPropertyNames.FtnArea]: 'AREA', + [Message.FtnPropertyNames.FtnSeenBy]: 'SEEN-BY', }[k]; - if(suffix) { + if (suffix) { // some special treatment. - if('Tearline' === suffix) { + if ('Tearline' === suffix) { v = v.replace(/^--- /, ''); - } else if('Origin' === suffix) { + } else if ('Origin' === suffix) { v = v.replace(/^[ ]{1,2}\* Origin: /, ''); } - if(Array.isArray(v)) { // ie: SEEN-BY[] -> one big list + if (Array.isArray(v)) { + // ie: SEEN-BY[] -> one big list v = v.join(' '); } message.nntpHeaders[`X-FTN-${suffix}`] = v.trim(); @@ -262,8 +273,8 @@ class NNTPServer extends NNTPServerBase { // Other FTN kludges _.each(message.meta.FtnKludge, (v, k) => { - if(Array.isArray(v)) { - v = v.join(' '); // same as above + if (Array.isArray(v)) { + v = v.join(' '); // same as above } message.nntpHeaders[`X-FTN-${k.toUpperCase()}`] = v.toString().trim(); }); @@ -273,24 +284,35 @@ class NNTPServer extends NNTPServerBase { // - If remote to/from : joeuser // - Without remote : joeuser // - const remoteFrom = _.get(message.meta, [ 'System', Message.SystemMetaNames.RemoteFromUser ]); - message.nntpHeaders['X-FTN-From'] = remoteFrom ? `${fromName} <${remoteFrom}>` : fromName; - const remoteTo = _.get(message.meta, [ 'System', Message.SystemMetaNames.RemoteToUser ]); + const remoteFrom = _.get(message.meta, [ + 'System', + Message.SystemMetaNames.RemoteFromUser, + ]); + message.nntpHeaders['X-FTN-From'] = remoteFrom + ? `${fromName} <${remoteFrom}>` + : fromName; + const remoteTo = _.get(message.meta, [ + 'System', + Message.SystemMetaNames.RemoteToUser, + ]); message.nntpHeaders['X-FTN-To'] = remoteTo ? `${toName} <${remoteTo}>` : toName; - if(!message.replyToMsgId) { + if (!message.replyToMsgId) { return cb(null); } // replyToMessageId -> Message-ID formatted ID const filter = { - resultType : 'uuid', - ids : [ parseInt(message.replyToMsgId) ], - limit : 1, + resultType: 'uuid', + ids: [parseInt(message.replyToMsgId)], + limit: 1, }; Message.findMessages(filter, (err, uuids) => { - if(!err && Array.isArray(uuids)) { - message.nntpHeaders.References = this.makeMessageIdentifier(message.replyToMsgId, uuids[0]); + if (!err && Array.isArray(uuids)) { + message.nntpHeaders.References = this.makeMessageIdentifier( + message.replyToMsgId, + uuids[0] + ); } return cb(null); }); @@ -300,14 +322,17 @@ class NNTPServer extends NNTPServerBase { let messageUuid; // Direct ID request - if((_.isString(messageId) && '<' !== messageId.charAt(0)) || _.isNumber(messageId)) { + if ( + (_.isString(messageId) && '<' !== messageId.charAt(0)) || + _.isNumber(messageId) + ) { // group must be in session - if(!this.isGroupSelected(session)) { + if (!this.isGroupSelected(session)) { return null; } messageId = parseInt(messageId); - if(isNaN(messageId)) { + if (isNaN(messageId)) { return null; } @@ -318,10 +343,10 @@ class NNTPServer extends NNTPServerBase { messageUuid = msg && msg.messageUuid; } else { // request - [ , messageUuid ] = this.getMessageIdentifierParts(messageId); + [, messageUuid] = this.getMessageIdentifierParts(messageId); } - if(!_.isString(messageUuid)) { + if (!_.isString(messageUuid)) { return null; } @@ -329,53 +354,74 @@ class NNTPServer extends NNTPServerBase { } _getArticle(session, messageId) { - return new Promise( resolve => { - this.log.trace( { messageId }, 'Get article'); + return new Promise(resolve => { + this.log.trace({ messageId }, 'Get article'); const messageUuid = this.getMessageUUIDFromMessageID(session, messageId); - if(!messageUuid) { - this.log.debug( { messageId }, 'Unable to retrieve message UUID for article request'); + if (!messageUuid) { + this.log.debug( + { messageId }, + 'Unable to retrieve message UUID for article request' + ); return resolve(null); } const message = new Message(); asyncSeries( [ - (callback) => { - return message.load( { uuid : messageUuid }, callback); + callback => { + return message.load({ uuid: messageUuid }, callback); }, - (callback) => { - if(!_.has(session, 'groupInfo.areaTag')) { + callback => { + if (!_.has(session, 'groupInfo.areaTag')) { // :TODO: if this is needed, how to validate properly? - this.log.warn( { messageUuid, messageId }, 'Get article request without group selection'); + this.log.warn( + { messageUuid, messageId }, + 'Get article request without group selection' + ); return resolve(null); } - if(session.groupInfo.areaTag !== message.areaTag) { + if (session.groupInfo.areaTag !== message.areaTag) { return resolve(null); } - if(!this.hasConfAndAreaReadAccess(session, session.groupInfo.confTag, session.groupInfo.areaTag)) { - this.log.info( { messageUuid, messageId}, 'Access denied for message'); + if ( + !this.hasConfAndAreaReadAccess( + session, + session.groupInfo.confTag, + session.groupInfo.areaTag + ) + ) { + this.log.info( + { messageUuid, messageId }, + 'Access denied for message' + ); return resolve(null); } return callback(null); }, - (callback) => { + callback => { return this.populateNNTPHeaders(session, message, callback); }, - (callback) => { + callback => { return this.prepareMessageBody(message, callback); - } + }, ], err => { - if(err) { - this.log.error( { error : err.message, messageUuid }, 'Failed to load article'); + if (err) { + this.log.error( + { error: err.message, messageUuid }, + 'Failed to load article' + ); return resolve(null); } - this.log.info( { messageUuid, messageId, areaTag : message.areaTag }, 'Serving article'); + this.log.info( + { messageUuid, messageId, areaTag: message.areaTag }, + 'Serving article' + ); return resolve(message); } ); @@ -389,63 +435,68 @@ class NNTPServer extends NNTPServerBase { // be used with the various _build* methods. // // :TODO: Handle |options| - if(!this.isGroupSelected(session)) { + if (!this.isGroupSelected(session)) { return resolve(null); } - const uuids = session.groupInfo.messageList.filter(m => { - if(m.areaTag !== session.groupInfo.areaTag) { - return false; - } - if(m.index < first || m.index > last) { - return false; - } - return true; - }).map(m => { - return { uuid : m.messageUuid, index : m.index }; - }); - - asyncMap(uuids, (msgInfo, nextMessageUuid) => { - const message = new Message(); - message.load( { uuid : msgInfo.uuid }, err => { - if(err) { - return nextMessageUuid(err); + const uuids = session.groupInfo.messageList + .filter(m => { + if (m.areaTag !== session.groupInfo.areaTag) { + return false; } + if (m.index < first || m.index > last) { + return false; + } + return true; + }) + .map(m => { + return { uuid: m.messageUuid, index: m.index }; + }); - message.index = msgInfo.index; + asyncMap( + uuids, + (msgInfo, nextMessageUuid) => { + const message = new Message(); + message.load({ uuid: msgInfo.uuid }, err => { + if (err) { + return nextMessageUuid(err); + } - this.populateNNTPHeaders(session, message, () => { - this.prepareMessageBody(message, () => { - return nextMessageUuid(null, message); + message.index = msgInfo.index; + + this.populateNNTPHeaders(session, message, () => { + this.prepareMessageBody(message, () => { + return nextMessageUuid(null, message); + }); }); }); - }); - }, - (err, messages) => { - return resolve(err ? null : messages); - }); + }, + (err, messages) => { + return resolve(err ? null : messages); + } + ); }); } - _selectGroup (session, groupName) { - this.log.trace( { groupName }, 'Select group request'); + _selectGroup(session, groupName) { + this.log.trace({ groupName }, 'Select group request'); - return new Promise( resolve => { + return new Promise(resolve => { this.getGroup(session, groupName, (err, group) => { - if(err) { + if (err) { return resolve(false); } session.group = Object.assign( {}, // start clean { - description : group.friendlyDesc || group.friendlyName, - current_article : group.nntp.total ? group.nntp.min_index : 0, + description: group.friendlyDesc || group.friendlyName, + current_article: group.nntp.total ? group.nntp.min_index : 0, }, group.nntp ); - session.groupInfo = group; // full set of info + session.groupInfo = group; // full set of info return resolve(true); }); @@ -453,82 +504,98 @@ class NNTPServer extends NNTPServerBase { } _getGroups(session, time, wildmat) { - this.log.trace( { time, wildmat }, 'Get groups request'); + this.log.trace({ time, wildmat }, 'Get groups request'); // :TODO: handle time - probably use as caching mechanism - must consider user/auth/rights // :TODO: handle |time| if possible. - return new Promise( (resolve, reject) => { + return new Promise((resolve, reject) => { const config = Config(); // :TODO: merge confs avail to authenticated user - const publicConfs = _.get(config, 'contentServers.nntp.publicMessageConferences', {}); + const publicConfs = _.get( + config, + 'contentServers.nntp.publicMessageConferences', + {} + ); - asyncReduce(Object.keys(publicConfs), [], (groups, confTag, nextConfTag) => { - const areaTags = publicConfs[confTag]; - // :TODO: merge area tags available to authenticated user - asyncMap(areaTags, (areaTag, nextAreaTag) => { - const groupName = this.getGroupName(confTag, areaTag); + asyncReduce( + Object.keys(publicConfs), + [], + (groups, confTag, nextConfTag) => { + const areaTags = publicConfs[confTag]; + // :TODO: merge area tags available to authenticated user + asyncMap( + areaTags, + (areaTag, nextAreaTag) => { + const groupName = this.getGroupName(confTag, areaTag); - // filter on |wildmat| if supplied. We will remove - // empty areas below in the final results. - if(wildmat && !wildmat.test(groupName)) { - return nextAreaTag(null, null); - } + // filter on |wildmat| if supplied. We will remove + // empty areas below in the final results. + if (wildmat && !wildmat.test(groupName)) { + return nextAreaTag(null, null); + } - this.getGroup(session, groupName, (err, group) => { - if(err) { - return nextAreaTag(null, null); // try others + this.getGroup(session, groupName, (err, group) => { + if (err) { + return nextAreaTag(null, null); // try others + } + return nextAreaTag(null, group.nntp); + }); + }, + (err, areas) => { + if (err) { + return nextConfTag(err); + } + + areas = areas.filter(a => a && Object.keys(a).length > 0); // remove empty + groups.push(...areas); + + return nextConfTag(null, groups); } - return nextAreaTag(null, group.nntp); - }); + ); }, - (err, areas) => { - if(err) { - return nextConfTag(err); + (err, groups) => { + if (err) { + return reject(err); } - - areas = areas.filter(a => a && Object.keys(a).length > 0); // remove empty - groups.push(...areas); - - return nextConfTag(null, groups); - }); - }, - (err, groups) => { - if(err) { - return reject(err); + return resolve(groups); } - return resolve(groups); - }); + ); }); } isConfAndAreaPubliclyExposed(confTag, areaTag) { - const publicAreaTags = _.get(Config(), [ 'contentServers', 'nntp', 'publicMessageConferences', confTag ] ); + const publicAreaTags = _.get(Config(), [ + 'contentServers', + 'nntp', + 'publicMessageConferences', + confTag, + ]); return Array.isArray(publicAreaTags) && publicAreaTags.includes(areaTag); } hasConfAndAreaReadAccess(session, confTag, areaTag) { - if(Message.isPrivateAreaTag(areaTag)) { + if (Message.isPrivateAreaTag(areaTag)) { return false; } - if(this.isConfAndAreaPubliclyExposed(confTag, areaTag)) { + if (this.isConfAndAreaPubliclyExposed(confTag, areaTag)) { return true; } // further checks require an authenticated user & ACS - if(!session || !session.authUser) { + if (!session || !session.authUser) { return false; } const conf = getMessageConferenceByTag(confTag); - if(!conf) { + if (!conf) { return false; } // :TODO: validate ACS const area = getMessageAreaByTag(areaTag, confTag); - if(!area) { + if (!area) { return false; } // :TODO: validate ACS @@ -538,47 +605,55 @@ class NNTPServer extends NNTPServerBase { getGroup(session, groupName, cb) { let group = this.groupCache.get(groupName); - if(group) { + if (group) { return cb(null, group); } - const [ confTag, areaTag ] = groupName.split('.'); - if(!confTag || !areaTag) { + const [confTag, areaTag] = groupName.split('.'); + if (!confTag || !areaTag) { return cb(Errors.UnexpectedState(`Invalid NNTP group name: ${groupName}`)); } - if(!this.hasConfAndAreaReadAccess(session, confTag, areaTag)) { - return cb(Errors.AccessDenied(`No access to conference ${confTag} and/or area ${areaTag}`)); + if (!this.hasConfAndAreaReadAccess(session, confTag, areaTag)) { + return cb( + Errors.AccessDenied( + `No access to conference ${confTag} and/or area ${areaTag}` + ) + ); } const area = getMessageAreaByTag(areaTag, confTag); - if(!area) { - return cb(Errors.DoesNotExist(`No area for areaTag "${areaTag}" / confTag "${confTag}"`)); + if (!area) { + return cb( + Errors.DoesNotExist( + `No area for areaTag "${areaTag}" / confTag "${confTag}"` + ) + ); } this.getMappedMessageListForArea(areaTag, (err, messageList) => { - if(err) { + if (err) { return cb(err); } - if(0 === messageList.length) { + if (0 === messageList.length) { // // Handle empty group // See https://tools.ietf.org/html/rfc3977#section-6.1.1.2 // return cb(null, { - messageList : [], + messageList: [], confTag, areaTag, - friendlyName : area.name, - friendlyDesc : area.desc, - nntp : { - name : groupName, - description : area.desc, - min_index : 0, - max_index : 0, - total : 0, - } + friendlyName: area.name, + friendlyDesc: area.desc, + nntp: { + name: groupName, + description: area.desc, + min_index: 0, + max_index: 0, + total: 0, + }, }); } @@ -586,13 +661,13 @@ class NNTPServer extends NNTPServerBase { messageList, confTag, areaTag, - friendlyName : area.name, - friendlyDesc : area.desc, - nntp : { - name : groupName, - min_index : messageList[0].index, - max_index : messageList[messageList.length - 1].index, - total : messageList.length, + friendlyName: area.name, + friendlyDesc: area.desc, + nntp: { + name: groupName, + min_index: messageList[0].index, + max_index: messageList[messageList.length - 1].index, + total: messageList.length, }, }; @@ -611,28 +686,29 @@ class NNTPServer extends NNTPServerBase { // :TODO: introduce caching asyncWaterfall( [ - (callback) => { + callback => { nntpDatabase.db.all( `SELECT nntp_message_id, message_id, message_uuid FROM nntp_area_message WHERE message_area_tag = ? ORDER BY nntp_message_id;`, - [ areaTag ], + [areaTag], (err, rows) => { - if(err) { + if (err) { return callback(err); } let messageList; - const lastMessageId = rows.length > 0 ? rows[rows.length - 1].message_id : 0; - if(!lastMessageId) { + const lastMessageId = + rows.length > 0 ? rows[rows.length - 1].message_id : 0; + if (!lastMessageId) { messageList = []; } else { messageList = rows.map(r => { return { areaTag, - index : r.nntp_message_id, // node-nntp wants this name - messageUuid : r.message_uuid, + index: r.nntp_message_id, // node-nntp wants this name + messageUuid: r.message_uuid, }; }); } @@ -645,65 +721,76 @@ class NNTPServer extends NNTPServerBase { // Find any new entries const filter = { areaTag, - newerThanMessageId : lastMessageId, - sort : 'messageId', - order : 'ascending', - resultType : 'messageList', + newerThanMessageId: lastMessageId, + sort: 'messageId', + order: 'ascending', + resultType: 'messageList', }; Message.findMessages(filter, (err, newMessageList) => { - if(err) { + if (err) { return callback(err); } - let index = messageList.length > 0 ? - messageList[messageList.length - 1].index + 1 - : 1; + let index = + messageList.length > 0 + ? messageList[messageList.length - 1].index + 1 + : 1; newMessageList = newMessageList.map(m => { - return Object.assign(m, { index : index++ } ); + return Object.assign(m, { index: index++ }); }); - if(0 === newMessageList.length) { + if (0 === newMessageList.length) { return callback(null, messageList); } // populate mapping DB with any new entries - nntpDatabase.db.beginTransaction( (err, trans) => { - if(err) { + nntpDatabase.db.beginTransaction((err, trans) => { + if (err) { return callback(err); } - forEachSeries(newMessageList, (newMessage, nextNewMessage) => { - trans.run( - `INSERT INTO nntp_area_message (nntp_message_id, message_id, message_area_tag, message_uuid) + forEachSeries( + newMessageList, + (newMessage, nextNewMessage) => { + trans.run( + `INSERT INTO nntp_area_message (nntp_message_id, message_id, message_area_tag, message_uuid) VALUES (?, ?, ?, ?);`, - [ newMessage.index, newMessage.messageId, areaTag, newMessage.messageUuid ], - err => { - return nextNewMessage(err); + [ + newMessage.index, + newMessage.messageId, + areaTag, + newMessage.messageUuid, + ], + err => { + return nextNewMessage(err); + } + ); + }, + err => { + if (err) { + return trans.rollback(() => { + return callback(err); + }); } - ); - }, - err => { - if(err) { - return trans.rollback( () => { - return callback(err); + + trans.commit(() => { + messageList.push( + ...newMessageList.map(m => { + return { + areaTag, + index: m.nntpMessageId, + messageUuid: m.messageUuid, + }; + }) + ); + + return callback(null, messageList); }); } - - trans.commit( () => { - messageList.push(...newMessageList.map(m => { - return { - areaTag, - index : m.nntpMessageId, - messageUuid : m.messageUuid, - }; - })); - - return callback(null, messageList); - }); - }); + ); }); }); - } + }, ], (err, messageList) => { return cb(err, messageList); @@ -721,20 +808,21 @@ class NNTPServer extends NNTPServerBase { _buildHeaderField(session, message, field) { const body = message.preparedBody || message.message; - const value = { - ':bytes' : Buffer.byteLength(body).toString(), - ':lines' : splitTextAtTerms(body).length.toString(), - }[field] - || _.find(message.nntpHeaders, (v, k) => { + const value = + { + ':bytes': Buffer.byteLength(body).toString(), + ':lines': splitTextAtTerms(body).length.toString(), + }[field] || + _.find(message.nntpHeaders, (v, k) => { return k.toLowerCase() === field; }); - if(!value) { + if (!value) { // // Clients will check some headers just to see if they exist. // Don't spam logs with these. For others, it's good to know. // - if(!['references', 'xref'].includes(field)) { + if (!['references', 'xref'].includes(field)) { this.log.trace(`No value for requested header field "${field}"`); } } @@ -748,7 +836,10 @@ class NNTPServer extends NNTPServerBase { _getNewNews(session, time, wildmat) { // Currently seems pointless to implement. No semi-modern clients seem to use it anyway. - this.log.debug( { time, wildmat }, 'Request made using unsupported NEWNEWS command'); + this.log.debug( + { time, wildmat }, + 'Request made using unsupported NEWNEWS command' + ); throw new Errors.Invalid('NEWNEWS is not enabled on this server'); } @@ -771,9 +862,11 @@ class NNTPServer extends NNTPServerBase { } getMessageIdentifierParts(messageId) { - const m = messageId.match(/<([0-9]+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@enigma-bbs>/); - if(m) { - return [ m[1], m[2] ]; + const m = messageId.match( + /<([0-9]+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@enigma-bbs>/ + ); + if (m) { + return [m[1], m[2]]; } return []; } @@ -789,15 +882,15 @@ class NNTPServer extends NNTPServerBase { } prepareMessageBody(message, cb) { - if(isAnsi(message.message)) { + if (isAnsi(message.message)) { AnsiPrep( message.message, { - rows : 'auto', - cols : 79, - forceLineTerm : true, - asciiMode : true, - fillLines : false, + rows: 'auto', + cols: 79, + forceLineTerm: true, + asciiMode: true, + fillLines: false, }, (err, prepped) => { message.preparedBody = prepped || message.message; @@ -805,7 +898,9 @@ class NNTPServer extends NNTPServerBase { } ); } else { - message.preparedBody = stripMciColorCodes(stripAnsiControlCodes(message.message, { all : true })); + message.preparedBody = stripMciColorCodes( + stripAnsiControlCodes(message.message, { all: true }) + ); return cb(null); } } @@ -820,7 +915,9 @@ class NNTPServer extends NNTPServerBase { // tags such that we *only* have a period separator // between the two for a group name! // - return `${_.snakeCase(confTag).replace(/\./g, '_')}.${_.snakeCase(areaTag).replace(/\./g, '_')}`; + return `${_.snakeCase(confTag).replace(/\./g, '_')}.${_.snakeCase( + areaTag + ).replace(/\./g, '_')}`; } } @@ -847,29 +944,33 @@ exports.getModule = class NNTPServerModule extends ServerModule { // // Any conf/areas exposed? // - const publicConfs = _.get(config, 'contentServers.nntp.publicMessageConferences', {}); + const publicConfs = _.get( + config, + 'contentServers.nntp.publicMessageConferences', + {} + ); const areasExposed = _.some(publicConfs, areas => { return Array.isArray(areas) && areas.length > 0; }); - if(!areasExposed) { + if (!areasExposed) { return false; } const nntp = _.get(config, 'contentServers.nntp.nntp'); - if(nntp && this.enableNntp) { - if(isNaN(nntp.port)) { + if (nntp && this.enableNntp) { + if (isNaN(nntp.port)) { return false; } } const nntps = _.get(config, 'contentServers.nntp.nntps'); - if(nntps && this.enableNttps) { - if(isNaN(nntps.port)) { + if (nntps && this.enableNttps) { + if (isNaN(nntps.port)) { return false; } - if(!_.isString(nntps.certPem) || !_.isString(nntps.keyPem)) { + if (!_.isString(nntps.certPem) || !_.isString(nntps.keyPem)) { return false; } } @@ -878,7 +979,7 @@ exports.getModule = class NNTPServerModule extends ServerModule { } createServer(cb) { - if(!this.isEnabled() || !this.isConfigured()) { + if (!this.isEnabled() || !this.isConfigured()) { return cb(null); } @@ -889,23 +990,25 @@ exports.getModule = class NNTPServerModule extends ServerModule { // :TODO: override |session| - use our own debug to Bunyan, etc. }; - if(this.enableNntp) { + if (this.enableNntp) { this.nntpServer = new NNTPServer( // :TODO: according to docs: if connection is non-tls, but behind proxy (assuming TLS termination?!!) then set this to true - Object.assign( { secure : false }, commonOptions), + Object.assign({ secure: false }, commonOptions), 'NNTP' ); } - if(this.enableNttps) { - this.nntpsServer = new NNTPServer( + if (this.enableNttps) { + this.nntpsServer = new NNTPServer( Object.assign( { - secure : true, - tls : { - cert : fs.readFileSync(config.contentServers.nntp.nntps.certPem), - key : fs.readFileSync(config.contentServers.nntp.nntps.keyPem), - } + secure: true, + tls: { + cert: fs.readFileSync( + config.contentServers.nntp.nntps.certPem + ), + key: fs.readFileSync(config.contentServers.nntp.nntps.keyPem), + }, }, commonOptions ), @@ -921,24 +1024,32 @@ exports.getModule = class NNTPServerModule extends ServerModule { listen(cb) { const config = Config(); - forEachSeries([ 'nntp', 'nntps' ], (service, nextService) => { - const server = this[`${service}Server`]; - if(server) { - const port = config.contentServers.nntp[service].port; - server.listen(this.listenURI(port, service)) - .catch(e => { - Log.warn( { error : e.message, port }, `${service.toUpperCase()} failed to listen`); - return nextService(null); // try next anyway - }).then( () => { - return nextService(null); - }); - } else { - return nextService(null); + forEachSeries( + ['nntp', 'nntps'], + (service, nextService) => { + const server = this[`${service}Server`]; + if (server) { + const port = config.contentServers.nntp[service].port; + server + .listen(this.listenURI(port, service)) + .catch(e => { + Log.warn( + { error: e.message, port }, + `${service.toUpperCase()} failed to listen` + ); + return nextService(null); // try next anyway + }) + .then(() => { + return nextService(null); + }); + } else { + return nextService(null); + } + }, + err => { + return cb(err); } - }, - err => { - return cb(err); - }); + ); } listenURI(port, service = 'nntp') { @@ -951,7 +1062,7 @@ function performMaintenanceTask(args, cb) { // Delete any message mapping that no longer have // an actual message associated with them. // - if(!nntpDatabase) { + if (!nntpDatabase) { Log.trace('Cannot perform NNTP maintenance without NNTP database initialized'); return cb(null); } @@ -959,7 +1070,7 @@ function performMaintenanceTask(args, cb) { let attached = false; asyncSeries( [ - (callback) => { + callback => { const messageDbPath = paths.join(Config().paths.db, 'message.sqlite3'); nntpDatabase.db.run( `ATTACH DATABASE "${messageDbPath}" AS msgdb;`, @@ -969,29 +1080,36 @@ function performMaintenanceTask(args, cb) { } ); }, - (callback) => { + callback => { nntpDatabase.db.run( `DELETE FROM nntp_area_message WHERE message_uuid NOT IN ( SELECT message_uuid FROM msgdb.message );`, - function result(err) { // no arrow func; need |this.changes| - if(err) { - Log.warn( { error : err.message }, 'Failed to delete from NNTP database'); + function result(err) { + // no arrow func; need |this.changes| + if (err) { + Log.warn( + { error: err.message }, + 'Failed to delete from NNTP database' + ); } else { - Log.debug( { count : this.changes }, 'Deleted mapped message IDs from NNTP database'); + Log.debug( + { count: this.changes }, + 'Deleted mapped message IDs from NNTP database' + ); } return callback(err); } ); - } + }, ], err => { - if(attached) { + if (attached) { nntpDatabase.db.run('DETACH DATABASE msgdb;'); } return cb(err); } ); -} \ No newline at end of file +} diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 70ede1c1..68f47917 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -2,46 +2,56 @@ 'use strict'; // ENiGMA½ -const Log = require('../../logger.js').log; -const ServerModule = require('../../server_module.js').ServerModule; -const Config = require('../../config.js').get; -const { Errors } = require('../../enig_error.js'); +const Log = require('../../logger.js').log; +const ServerModule = require('../../server_module.js').ServerModule; +const Config = require('../../config.js').get; +const { Errors } = require('../../enig_error.js'); // deps -const http = require('http'); -const https = require('https'); -const _ = require('lodash'); -const fs = require('graceful-fs'); -const paths = require('path'); -const mimeTypes = require('mime-types'); +const http = require('http'); +const https = require('https'); +const _ = require('lodash'); +const fs = require('graceful-fs'); +const paths = require('path'); +const mimeTypes = require('mime-types'); const forEachSeries = require('async/forEachSeries'); -const ModuleInfo = exports.moduleInfo = { - name : 'Web', - desc : 'Web Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.web.server', -}; +const ModuleInfo = (exports.moduleInfo = { + name: 'Web', + desc: 'Web Server', + author: 'NuSkooler', + packageName: 'codes.l33t.enigma.web.server', +}); class Route { constructor(route) { Object.assign(this, route); - if(this.method) { + if (this.method) { this.method = this.method.toUpperCase(); } try { this.pathRegExp = new RegExp(this.path); - } catch(e) { - Log.debug( { route : route }, 'Invalid regular expression for route 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) ) || + (this.pathRegExp instanceof RegExp && + -1 !== + [ + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'CONNECT', + 'OPTIONS', + 'TRACE', + ].indexOf(this.method)) || !_.isFunction(this.handler) ); } @@ -50,24 +60,26 @@ class Route { 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(); - const config = Config(); - this.enableHttp = config.contentServers.web.http.enabled || false; - this.enableHttps = config.contentServers.web.https.enabled || false; + const config = Config(); + this.enableHttp = config.contentServers.web.http.enabled || false; + this.enableHttps = config.contentServers.web.https.enabled || false; this.routes = {}; - if(this.isEnabled() && config.contentServers.web.staticRoot) { + if (this.isEnabled() && config.contentServers.web.staticRoot) { this.addRoute({ - method : 'GET', - path : '/static/.*$', - handler : this.routeStaticFile.bind(this), + method: 'GET', + path: '/static/.*$', + handler: this.routeStaticFile.bind(this), }); } } @@ -81,22 +93,24 @@ exports.getModule = class WebServerModule extends ServerModule { // only if non-standard. Allow users to override full prefix in config. // const config = Config(); - if(_.isString(config.contentServers.web.overrideUrlPrefix)) { + 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}`; + 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}`; + schema = 'http://'; + port = + 80 === config.contentServers.web.http.port + ? '' + : `:${config.contentServers.web.http.port}`; } return `${schema}${config.contentServers.web.domain}${port}${pathAndQuery}`; @@ -107,21 +121,25 @@ exports.getModule = class WebServerModule extends ServerModule { } createServer(cb) { - if(this.enableHttp) { - this.httpServer = http.createServer( (req, resp) => this.routeRequest(req, resp) ); + if (this.enableHttp) { + this.httpServer = http.createServer((req, resp) => + this.routeRequest(req, resp) + ); } const config = Config(); - if(this.enableHttps) { + if (this.enableHttps) { const options = { - cert : fs.readFileSync(config.contentServers.web.https.certPem), - key : fs.readFileSync(config.contentServers.web.https.keyPem), + cert: fs.readFileSync(config.contentServers.web.https.certPem), + key: fs.readFileSync(config.contentServers.web.https.keyPem), }; // additional options - Object.assign(options, config.contentServers.web.https.options || {} ); + Object.assign(options, config.contentServers.web.https.options || {}); - this.httpsServer = https.createServer(options, (req, resp) => this.routeRequest(req, resp) ); + this.httpsServer = https.createServer(options, (req, resp) => + this.routeRequest(req, resp) + ); } return cb(null); @@ -129,38 +147,61 @@ exports.getModule = class WebServerModule extends ServerModule { listen(cb) { const config = Config(); - forEachSeries([ 'http', 'https' ], (service, nextService) => { - const name = `${service}Server`; - if(this[name]) { - const port = parseInt(config.contentServers.web[service].port); - if(isNaN(port)) { - Log.warn( { port : config.contentServers.web[service].port, server : ModuleInfo.name }, `Invalid port (${service})` ); - return nextService(Errors.Invalid(`Invalid port: ${config.contentServers.web[service].port}`)); - } + forEachSeries( + ['http', 'https'], + (service, nextService) => { + const name = `${service}Server`; + if (this[name]) { + const port = parseInt(config.contentServers.web[service].port); + if (isNaN(port)) { + Log.warn( + { + port: config.contentServers.web[service].port, + server: ModuleInfo.name, + }, + `Invalid port (${service})` + ); + return nextService( + Errors.Invalid( + `Invalid port: ${config.contentServers.web[service].port}` + ) + ); + } - this[name].listen(port, config.contentServers.web[service].address, err => { - return nextService(err); - }); - } else { - return nextService(null); + this[name].listen( + port, + config.contentServers.web[service].address, + err => { + return nextService(err); + } + ); + } else { + return nextService(null); + } + }, + err => { + return cb(err); } - }, - err => { - return cb(err); - }); + ); } addRoute(route) { route = new Route(route); - if(!route.isValid()) { - Log.warn( { route : route }, 'Cannot add route: missing or invalid required members' ); + if (!route.isValid()) { + Log.warn( + { route: route }, + 'Cannot add route: missing or invalid required members' + ); return false; } const routeKey = route.getRouteKey(); - if(routeKey in this.routes) { - Log.warn( { route : route, routeKey : routeKey }, 'Cannot add route: duplicate method/path combination exists' ); + if (routeKey in this.routes) { + Log.warn( + { route: route, routeKey: routeKey }, + 'Cannot add route: duplicate method/path combination exists' + ); return false; } @@ -169,9 +210,9 @@ exports.getModule = class WebServerModule extends ServerModule { } routeRequest(req, resp) { - const route = _.find(this.routes, r => r.matchesRequest(req) ); + const route = _.find(this.routes, r => r.matchesRequest(req)); - if(!route && '/' === req.url) { + if (!route && '/' === req.url) { return this.routeIndex(req, resp); } @@ -179,12 +220,15 @@ exports.getModule = class WebServerModule extends ServerModule { } respondWithError(resp, code, bodyText, title) { - const customErrorPage = paths.join(Config().contentServers.web.staticRoot, `${code}.html`); + const customErrorPage = paths.join( + Config().contentServers.web.staticRoot, + `${code}.html` + ); fs.readFile(customErrorPage, 'utf8', (err, data) => { - resp.writeHead(code, { 'Content-Type' : 'text/html' } ); + resp.writeHead(code, { 'Content-Type': 'text/html' }); - if(err) { + if (err) { return resp.end(` @@ -197,8 +241,7 @@ exports.getModule = class WebServerModule extends ServerModule {

${bodyText}

- ` - ); + `); } return resp.end(data); @@ -232,13 +275,15 @@ exports.getModule = class WebServerModule extends ServerModule { } fs.stat(filePath, (err, stats) => { - if(err || !stats.isFile()) { + if (err || !stats.isFile()) { return self.fileNotFound(resp); } const headers = { - 'Content-Type' : mimeTypes.contentType(paths.basename(filePath)) || mimeTypes.contentType('.bin'), - 'Content-Length' : stats.size, + 'Content-Type': + mimeTypes.contentType(paths.basename(filePath)) || + mimeTypes.contentType('.bin'), + 'Content-Length': stats.size, }; const readStream = fs.createReadStream(filePath); @@ -259,18 +304,23 @@ exports.getModule = class WebServerModule extends ServerModule { const self = this; fs.readFile(templatePath, 'utf8', (err, templateData) => { - if(err) { + 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'); + 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, + 'Content-Type': contentType || mimeTypes.contentType('.html'), + 'Content-Length': finalPage.length, }; resp.writeHead(200, headers); diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index 44e6588a..6acd99e9 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -2,35 +2,32 @@ 'use strict'; // ENiGMA½ -const Config = require('../../config.js').get; -const baseClient = require('../../client.js'); -const Log = require('../../logger.js').log; +const Config = require('../../config.js').get; +const baseClient = require('../../client.js'); +const Log = require('../../logger.js').log; const LoginServerModule = require('../../login_server_module.js'); -const userLogin = require('../../user_login.js').userLogin; -const enigVersion = require('../../../package.json').version; -const theme = require('../../theme.js'); -const stringFormat = require('../../string_format.js'); -const { - Errors, - ErrorReasons -} = require('../../enig_error.js'); -const User = require('../../user.js'); -const UserProps = require('../../user_property.js'); +const userLogin = require('../../user_login.js').userLogin; +const enigVersion = require('../../../package.json').version; +const theme = require('../../theme.js'); +const stringFormat = require('../../string_format.js'); +const { Errors, ErrorReasons } = require('../../enig_error.js'); +const User = require('../../user.js'); +const UserProps = require('../../user_property.js'); // deps -const ssh2 = require('ssh2'); -const fs = require('graceful-fs'); -const util = require('util'); -const _ = require('lodash'); -const assert = require('assert'); +const ssh2 = require('ssh2'); +const fs = require('graceful-fs'); +const util = require('util'); +const _ = require('lodash'); +const assert = require('assert'); -const ModuleInfo = exports.moduleInfo = { - name : 'SSH', - desc : 'SSH Server', - author : 'NuSkooler', - isSecure : true, - packageName : 'codes.l33t.enigma.ssh.server', -}; +const ModuleInfo = (exports.moduleInfo = { + name: 'SSH', + desc: 'SSH Server', + author: 'NuSkooler', + isSecure: true, + packageName: 'codes.l33t.enigma.ssh.server', +}); function SSHClient(clientConn) { baseClient.Client.apply(this, arguments); @@ -43,16 +40,19 @@ function SSHClient(clientConn) { const self = this; clientConn.on('authentication', function authAttempt(ctx) { - const username = ctx.username || ''; - const config = Config(); - self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; + const username = ctx.username || ''; + 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' + ); - const safeContextReject = (param) => { + const safeContextReject = param => { try { return ctx.reject(param); - } catch(e) { + } catch (e) { return; } }; @@ -64,58 +64,79 @@ function SSHClient(clientConn) { // slow version to thwart brute force attacks const slowTerminateConnection = () => { - setTimeout( () => { + setTimeout(() => { return terminateConnection(); }, 2000); }; const promptAndTerm = (msg, method = 'standard') => { - if('keyboard-interactive' === ctx.method) { + if ('keyboard-interactive' === ctx.method) { ctx.prompt(msg); } return 'slow' === method ? slowTerminateConnection() : terminateConnection(); }; - const accountAlreadyLoggedIn = (username) => { - return promptAndTerm(`${username} is already connected to the system. Terminating connection.\n(Press any key to continue)`); + const accountAlreadyLoggedIn = username => { + return promptAndTerm( + `${username} is already connected to the system. Terminating connection.\n(Press any key to continue)` + ); }; - const accountDisabled = (username) => { + const accountDisabled = username => { return promptAndTerm(`${username} is disabled.\n(Press any key to continue)`); }; - const accountInactive = (username) => { - return promptAndTerm(`${username} is waiting for +op activation.\n(Press any key to continue)`); + const accountInactive = username => { + return promptAndTerm( + `${username} is waiting for +op activation.\n(Press any key to continue)` + ); }; - const accountLocked = (username) => { - return promptAndTerm(`${username} is locked.\n(Press any key to continue)`, 'slow'); + const accountLocked = username => { + return promptAndTerm( + `${username} is locked.\n(Press any key to continue)`, + 'slow' + ); }; - const isSpecialHandleError = (err) => { - return [ ErrorReasons.AlreadyLoggedIn, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked ].includes(err.reasonCode); + const isSpecialHandleError = err => { + return [ + ErrorReasons.AlreadyLoggedIn, + ErrorReasons.Disabled, + ErrorReasons.Inactive, + ErrorReasons.Locked, + ].includes(err.reasonCode); }; const handleSpecialError = (err, username) => { - switch(err.reasonCode) { - case ErrorReasons.AlreadyLoggedIn : return accountAlreadyLoggedIn(username); - case ErrorReasons.Inactive : return accountInactive(username); - case ErrorReasons.Disabled : return accountDisabled(username); - case ErrorReasons.Locked : return accountLocked(username); - default : return terminateConnection(); + switch (err.reasonCode) { + case ErrorReasons.AlreadyLoggedIn: + return accountAlreadyLoggedIn(username); + case ErrorReasons.Inactive: + return accountInactive(username); + case ErrorReasons.Disabled: + return accountDisabled(username); + case ErrorReasons.Locked: + return accountLocked(username); + default: + return terminateConnection(); } }; - const authWithPasswordOrPubKey = (authType) => { - if(User.AuthFactor1Types.SSHPubKey !== authType || !self.user.isAuthenticated() || !ctx.signature) { + const authWithPasswordOrPubKey = authType => { + if ( + User.AuthFactor1Types.SSHPubKey !== authType || + !self.user.isAuthenticated() || + !ctx.signature + ) { // step 1: login/auth using PubKey - userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => { - if(err) { - if(isSpecialHandleError(err)) { + userLogin(self, ctx.username, ctx.password, { authType, ctx }, err => { + if (err) { + if (isSpecialHandleError(err)) { return handleSpecialError(err, username); } - if(Errors.BadLogin().code === err.code) { + if (Errors.BadLogin().code === err.code) { return slowTerminateConnection(); } @@ -126,8 +147,10 @@ function SSHClient(clientConn) { }); } else { // step 2: verify signature - const pubKeyActual = ssh2.utils.parseKey(self.user.getProperty(UserProps.AuthPubKey)); - if(!pubKeyActual || !pubKeyActual.verify(ctx.blob, ctx.signature)) { + const pubKeyActual = ssh2.utils.parseKey( + self.user.getProperty(UserProps.AuthPubKey) + ); + if (!pubKeyActual || !pubKeyActual.verify(ctx.blob, ctx.signature)) { return slowTerminateConnection(); } return ctx.accept(); @@ -135,38 +158,48 @@ function SSHClient(clientConn) { }; const authKeyboardInteractive = () => { - if(0 === username.length) { + if (0 === username.length) { return safeContextReject(); } - const interactivePrompt = { prompt : `${ctx.username}'s password: `, echo : false }; + const interactivePrompt = { + prompt: `${ctx.username}'s password: `, + echo: false, + }; ctx.prompt(interactivePrompt, function retryPrompt(answers) { - userLogin(self, username, (answers[0] || ''), err => { - if(err) { - if(isSpecialHandleError(err)) { + userLogin(self, username, answers[0] || '', err => { + if (err) { + if (isSpecialHandleError(err)) { return handleSpecialError(err, username); } - if(Errors.BadLogin().code === err.code) { + if (Errors.BadLogin().code === err.code) { return slowTerminateConnection(); } const artOpts = { - client : self, - name : 'SSHPMPT.ASC', - readSauce : false, + client: self, + name: 'SSHPMPT.ASC', + readSauce: false, }; theme.getThemeArt(artOpts, (err, artInfo) => { - if(err) { + 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!)'; + 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:`; + interactivePrompt.prompt = `Access denied\n${stringFormat( + artInfo.data, + { newUserNames: newUserNameList } + )}\n${ctx.username}'s password:`; } return ctx.prompt(interactivePrompt, retryPrompt); }); @@ -181,32 +214,32 @@ function SSHClient(clientConn) { // If the system is open and |isNewUser| is true, the login // sequence is hijacked in order to start the application process. // - if(false === config.general.closedSystem && self.isNewUser) { + if (false === config.general.closedSystem && self.isNewUser) { return ctx.accept(); } - switch(ctx.method) { - case 'password' : + switch (ctx.method) { + case 'password': return authWithPasswordOrPubKey(User.AuthFactor1Types.Password); - //return authWithPassword(); + //return authWithPassword(); - case 'publickey' : + case 'publickey': return authWithPasswordOrPubKey(User.AuthFactor1Types.SSHPubKey); - //return authWithPubKey(); + //return authWithPubKey(); - case 'keyboard-interactive' : + case 'keyboard-interactive': return authKeyboardInteractive(); - default : + default: return safeContextReject(SSHClient.ValidAuthMethods); } }); - this.dataHandler = function(data) { + this.dataHandler = function (data) { self.emit('data', data); }; - this.updateTermInfo = function(info) { + this.updateTermInfo = function (info) { // // From ssh2 docs: // "rows and cols override width and height when rows and cols are non-zero." @@ -214,12 +247,12 @@ function SSHClient(clientConn) { 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)); @@ -228,12 +261,16 @@ function SSHClient(clientConn) { // 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) { + if (termHeight > 0 && termWidth > 0) { self.term.termHeight = termHeight; self.term.termWidth = termWidth; } - if(_.isString(info.term) && info.term.length > 0 && 'unknown' === self.term.termType) { + if ( + _.isString(info.term) && + info.term.length > 0 && + 'unknown' === self.term.termType + ) { self.setTermType(info.term); } }; @@ -242,17 +279,17 @@ function SSHClient(clientConn) { self.log.info('SSH authentication success'); clientConn.on('session', accept => { - const session = accept(); session.on('pty', function pty(accept, reject, info) { self.log.debug(info, 'SSH pty event'); - if(_.isFunction(accept)) { + if (_.isFunction(accept)) { accept(); } - if(self.input) { // do we have I/O? + if (self.input) { + // do we have I/O? self.updateTermInfo(info); } else { self.cachedTermInfo = info; @@ -262,7 +299,7 @@ function SSHClient(clientConn) { session.on('env', (accept, reject, info) => { self.log.debug(info, 'SSH env event'); - if(_.isFunction(accept)) { + if (_.isFunction(accept)) { accept(); } }); @@ -276,49 +313,46 @@ function SSHClient(clientConn) { channel.stdin.on('data', self.dataHandler); - if(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 } ); + 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'); - if(self.input) { + if (self.input) { self.updateTermInfo(info); } else { self.cachedTermInfo = info; } }); - }); }); clientConn.once('end', () => { - return self.emit('end'); // remove client connection/tracking + return self.emit('end'); // remove client connection/tracking }); clientConn.on('error', err => { - self.log.warn( { error : err.message, code : err.code }, 'SSH connection error'); + self.log.warn({ error: err.message, code: err.code }, 'SSH connection error'); }); - this.disconnect = function() { + this.disconnect = function () { return clientConn.end(); }; } util.inherits(SSHClient, baseClient.Client); -SSHClient.ValidAuthMethods = [ - 'password', - 'keyboard-interactive', - 'publickey', -]; +SSHClient.ValidAuthMethods = ['password', 'keyboard-interactive', 'publickey']; exports.getModule = class SSHServerModule extends LoginServerModule { constructor() { @@ -327,27 +361,27 @@ exports.getModule = class SSHServerModule extends LoginServerModule { createServer(cb) { const config = Config(); - if(true != config.loginServers.ssh.enabled) { + if (true != config.loginServers.ssh.enabled) { return cb(null); } const serverConf = { - hostKeys : [ + hostKeys: [ { - key : fs.readFileSync(config.loginServers.ssh.privateKeyPem), - passphrase : config.loginServers.ssh.privateKeyPass, - } + key: fs.readFileSync(config.loginServers.ssh.privateKeyPem), + passphrase: config.loginServers.ssh.privateKeyPass, + }, ], - ident : 'enigma-bbs-' + enigVersion + '-srv', + ident: 'enigma-bbs-' + enigVersion + '-srv', // Note that sending 'banner' breaks at least EtherTerm! - debug : (sshDebugLine) => { - if(true === config.loginServers.ssh.traceConnections) { + debug: sshDebugLine => { + if (true === config.loginServers.ssh.traceConnections) { Log.trace(`SSH: ${sshDebugLine}`); } }, - algorithms : config.loginServers.ssh.algorithms, + algorithms: config.loginServers.ssh.algorithms, }; // @@ -370,19 +404,25 @@ exports.getModule = class SSHServerModule extends LoginServerModule { listen(cb) { const config = Config(); - if(true != config.loginServers.ssh.enabled) { + if (true != config.loginServers.ssh.enabled) { return cb(null); } const port = parseInt(config.loginServers.ssh.port); - if(isNaN(port)) { - Log.error( { server : ModuleInfo.name, port : config.loginServers.ssh.port }, 'Cannot load server (invalid port)' ); + if (isNaN(port)) { + Log.error( + { server: ModuleInfo.name, port: config.loginServers.ssh.port }, + 'Cannot load server (invalid port)' + ); return cb(Errors.Invalid(`Invalid port: ${config.loginServers.ssh.port}`)); } this.server.listen(port, config.loginServers.ssh.address, err => { - if(!err) { - Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + if (!err) { + Log.info( + { server: ModuleInfo.name, port: port }, + 'Listening for connections' + ); } return cb(err); }); diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index a9f5f745..e5b3167f 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -9,17 +9,17 @@ const { Errors } = require('../../enig_error'); const net = require('net'); const { TelnetSocket, - TelnetSpec: { Options, Commands } + TelnetSpec: { Options, Commands }, } = require('telnet-socket'); const { inherits } = require('util'); -const ModuleInfo = exports.moduleInfo = { - name : 'Telnet', - desc : 'Telnet Server v2', - author : 'NuSkooler', - isSecure : false, - packageName : 'codes.l33t.enigma.telnet.server.v2', -}; +const ModuleInfo = (exports.moduleInfo = { + name: 'Telnet', + desc: 'Telnet Server v2', + author: 'NuSkooler', + isSecure: false, + packageName: 'codes.l33t.enigma.telnet.server.v2', +}); class TelnetClient { constructor(socket) { @@ -36,14 +36,14 @@ class TelnetClient { this._clientReady(); }, 3000); - this.dataHandler = function(data) { + this.dataHandler = function (data) { this.emit('data', data); }.bind(this); this.socket.on('data', this.dataHandler); this.socket.on('error', err => { - this._logDebug({ error : err.message }, 'Socket error'); + this._logDebug({ error: err.message }, 'Socket error'); return this.emit('end'); }); @@ -52,7 +52,7 @@ class TelnetClient { }); this.socket.on('command error', (command, err) => { - this._logDebug({ command, error : err.message }, 'Command error'); + this._logDebug({ command, error: err.message }, 'Command error'); }); this.socket.on('DO', command => { @@ -61,12 +61,12 @@ class TelnetClient { // the banner - some terminals will ask over and over // if we respond to a DO with a WILL, so just don't // do anything... - case Options.SGA : - case Options.ECHO : - case Options.TRANSMIT_BINARY : + case Options.SGA: + case Options.ECHO: + case Options.TRANSMIT_BINARY: break; - default : + default: return this.socket.command(Commands.WONT, command.option); } }); @@ -77,15 +77,18 @@ class TelnetClient { this.socket.on('WILL', command => { switch (command.option) { - case Options.TTYPE : + case Options.TTYPE: return this.socket.sb.send.ttype(); - case Options.NEW_ENVIRON : - return this.socket.sb.send.new_environ( - [ 'ROWS', 'COLUMNS', 'TERM', 'TERM_PROGRAM' ] - ); + case Options.NEW_ENVIRON: + return this.socket.sb.send.new_environ([ + 'ROWS', + 'COLUMNS', + 'TERM', + 'TERM_PROGRAM', + ]); - default : + default: break; } }); @@ -96,23 +99,29 @@ class TelnetClient { this.socket.on('SB', command => { switch (command.option) { - case Options.TTYPE : + case Options.TTYPE: this.setTermType(command.optionData.ttype); return this._clientReady(); - case Options.NEW_ENVIRON : + case Options.NEW_ENVIRON: { this._logDebug( - { vars : command.optionData.vars, uservars : command.optionData.uservars }, + { + vars: command.optionData.vars, + uservars: command.optionData.uservars, + }, 'New environment received' ); // get a value from vars with fallback of user vars - const getValue = (name) => { - return command.optionData.vars && + const getValue = name => { + return ( + command.optionData.vars && (command.optionData.vars.find(nv => nv.name === name) || - command.optionData.uservars.find(nv => nv.name === name) - ); + command.optionData.uservars.find( + nv => nv.name === name + )) + ); }; if ('unknown' === this.term.termType) { @@ -124,13 +133,15 @@ class TelnetClient { } if (0 === this.term.termHeight || 0 === this.term.termWidth) { - const updateTermSize = (what) => { + const updateTermSize = what => { const value = parseInt(getValue(what)); if (value) { - this.term[what === 'ROWS' ? 'termHeight' : 'termWidth'] = value; + this.term[ + what === 'ROWS' ? 'termHeight' : 'termWidth' + ] = value; this._logDebug( - { [ what ] : value, source : 'NEW-ENVIRON' }, + { [what]: value, source: 'NEW-ENVIRON' }, 'Window size updated' ); } @@ -142,12 +153,12 @@ class TelnetClient { } break; - case Options.NAWS : + case Options.NAWS: { const { width, height } = command.optionData; - this.term.termWidth = width; - this.term.termHeight = height; + this.term.termWidth = width; + this.term.termHeight = height; if (width) { this.term.env.COLUMNS = width; @@ -158,13 +169,13 @@ class TelnetClient { } this._logDebug( - { width, height, source : 'NAWS' }, + { width, height, source: 'NAWS' }, 'Windows size updated' ); } break; - default : + default: return this._logTrace(command, 'SB'); } }); @@ -197,8 +208,8 @@ class TelnetClient { } banner() { - this.socket.dont.echo(); // don't echo characters - this.socket.will.echo(); // ...we'll echo them back + this.socket.dont.echo(); // don't echo characters + this.socket.will.echo(); // ...we'll echo them back this.socket.will.sga(); this.socket.do.sga(); @@ -229,7 +240,7 @@ class TelnetClient { } this.clientReadyHandled = true; - this.emit('ready', { firstMenu : Config().loginServers.telnet.firstMenu } ); + this.emit('ready', { firstMenu: Config().loginServers.telnet.firstMenu }); } } @@ -241,14 +252,14 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { } createServer(cb) { - this.server = net.createServer( socket => { + this.server = net.createServer(socket => { const client = new TelnetClient(socket); - client.banner(); // start negotiations + client.banner(); // start negotiations this.handleNewClient(client, socket, ModuleInfo); }); this.server.on('error', err => { - Log.info( { error : err.message }, 'Telnet server error'); + Log.info({ error: err.message }, 'Telnet server error'); }); return cb(null); @@ -257,18 +268,24 @@ exports.getModule = class TelnetServerModule extends LoginServerModule { listen(cb) { const config = Config(); const port = parseInt(config.loginServers.telnet.port); - if(isNaN(port)) { - Log.error( { server : ModuleInfo.name, port : config.loginServers.telnet.port }, 'Cannot load server (invalid port)' ); + if (isNaN(port)) { + Log.error( + { server: ModuleInfo.name, port: config.loginServers.telnet.port }, + 'Cannot load server (invalid port)' + ); return cb(Errors.Invalid(`Invalid port: ${config.loginServers.telnet.port}`)); } this.server.listen(port, config.loginServers.telnet.address, err => { - if(!err) { - Log.info( { server : ModuleInfo.name, port : port }, 'Listening for connections' ); + if (!err) { + Log.info( + { server: ModuleInfo.name, port: port }, + 'Listening for connections' + ); } return cb(err); }); } }; -exports.TelnetClient = TelnetClient; // WebSockets is a wrapper on top of this +exports.TelnetClient = TelnetClient; // WebSockets is a wrapper on top of this diff --git a/core/servers/login/websocket.js b/core/servers/login/websocket.js index ce89c547..de9e09aa 100644 --- a/core/servers/login/websocket.js +++ b/core/servers/login/websocket.js @@ -2,32 +2,32 @@ 'use strict'; // ENiGMA½ -const Config = require('../../config.js').get; -const TelnetClient = require('./telnet.js').TelnetClient; -const Log = require('../../logger.js').log; +const Config = require('../../config.js').get; +const TelnetClient = require('./telnet.js').TelnetClient; +const Log = require('../../logger.js').log; const LoginServerModule = require('../../login_server_module.js'); -const { Errors } = require('../../enig_error.js'); +const { Errors } = require('../../enig_error.js'); // deps -const _ = require('lodash'); -const WebSocketServer = require('ws').Server; -const http = require('http'); -const https = require('https'); -const fs = require('graceful-fs'); +const _ = require('lodash'); +const WebSocketServer = require('ws').Server; +const http = require('http'); +const https = require('https'); +const fs = require('graceful-fs'); const { Duplex } = require('stream'); -const forEachSeries = require('async/forEachSeries'); +const forEachSeries = require('async/forEachSeries'); -const ModuleInfo = exports.moduleInfo = { - name : 'WebSocket', - desc : 'WebSocket Server', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.websocket.server', -}; +const ModuleInfo = (exports.moduleInfo = { + name: 'WebSocket', + desc: 'WebSocket Server', + author: 'NuSkooler', + packageName: 'codes.l33t.enigma.websocket.server', +}); class WebSocketClient extends TelnetClient { constructor(ws, req, serverType) { // allow WebSocket to act like a Duplex (socket) - const wsDuplex = new class WebSocketDuplex extends Duplex { + const wsDuplex = new (class WebSocketDuplex extends Duplex { constructor(ws) { super(); this.ws = ws; @@ -42,17 +42,23 @@ class WebSocketClient extends TelnetClient { // Support X-Forwarded-For and X-Real-IP headers for proxied connections this.resolvedRemoteAddress = - (this.client.proxied && (httpRequest.headers['x-forwarded-for'] || httpRequest.headers['x-real-ip'])) || + (this.client.proxied && + (httpRequest.headers['x-forwarded-for'] || + httpRequest.headers['x-real-ip'])) || httpRequest.connection.remoteAddress; } get remoteAddress() { - return this.resolvedRemoteAddress; + return this.resolvedRemoteAddress; } _write(data, encoding, cb) { - cb = cb || ( () => { /* eat it up */} ); // handle data writes after close - return this.ws.send(data, { binary : true }, cb); + cb = + cb || + (() => { + /* eat it up */ + }); // handle data writes after close + return this.ws.send(data, { binary: true }, cb); } _read() { @@ -62,7 +68,7 @@ class WebSocketClient extends TelnetClient { _data(data) { this.push(data); } - }(ws); + })(ws); super(wsDuplex); wsDuplex.setClient(this, req); @@ -85,16 +91,19 @@ class WebSocketClient extends TelnetClient { ws.isConnectionAlive = true; }); - 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') && - 'https' === req.headers['x-forwarded-proto']) - { - Log.debug(`Assuming secure connection due to X-Forwarded-Proto of "${req.headers['x-forwarded-proto']}"`); + 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; @@ -105,11 +114,11 @@ class WebSocketClient extends TelnetClient { } get isSecure() { - return ('secure' === this.serverType || true === this.proxied) ? true : false; + return 'secure' === this.serverType || true === this.proxied ? true : false; } } -const WSS_SERVER_TYPES = [ 'insecure', 'secure' ]; +const WSS_SERVER_TYPES = ['insecure', 'secure']; exports.getModule = class WebSocketLoginServer extends LoginServerModule { constructor() { @@ -123,35 +132,39 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { // * secure (tls) websocket (wss://) // const config = _.get(Config(), 'loginServers.webSocket'); - if(!_.isObject(config)) { + if (!_.isObject(config)) { return cb(null); } - 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) => { + 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 } ), + httpServer: httpServer, + wsServer: new WebSocketServer({ server: httpServer }), }; } - if(_.isObject(config, 'wss') && true === _.get(config, 'wss.enabled') && _.isNumber(wssPort)) { + if ( + _.isObject(config, 'wss') && + true === _.get(config, 'wss.enabled') && + _.isNumber(wssPort) + ) { const httpServer = https.createServer({ - key : fs.readFileSync(config.wss.keyPem), - cert : fs.readFileSync(config.wss.certPem), + key: fs.readFileSync(config.wss.keyPem), + cert: fs.readFileSync(config.wss.certPem), }); this.secure = { - httpServer : httpServer, - wsServer : new WebSocketServer( { server : httpServer } ), + httpServer: httpServer, + wsServer: new WebSocketServer({ server: httpServer }), }; } @@ -162,21 +175,24 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { // // Send pings every 30s // - setInterval( () => { + setInterval(() => { WSS_SERVER_TYPES.forEach(serverType => { - if(this[serverType]) { + if (this[serverType]) { this[serverType].wsServer.clients.forEach(ws => { - if(false === ws.isConnectionAlive) { - Log.debug('WebSocket connection seems inactive. Terminating.'); + 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'); try { - ws.ping('', false); // false=don't mask - } catch(e) { // don't barf on closing state + ws.ping('', false); // false=don't mask + } catch (e) { + // don't barf on closing state /* nothing */ } }); @@ -184,38 +200,55 @@ exports.getModule = class WebSocketLoginServer extends LoginServerModule { }); }, 30000); - forEachSeries(WSS_SERVER_TYPES, (serverType, nextServerType) => { - const server = this[serverType]; - if(!server) { - return nextServerType(null); - } - - const serverName = `${ModuleInfo.name} (${serverType})`; - const conf = _.get(Config(), [ 'loginServers', 'webSocket', 'secure' === serverType ? 'wss' : 'ws' ] ); - const confPort = conf.port; - const port = parseInt(confPort); - - if(isNaN(port)) { - Log.error( { server : serverName, port : confPort }, 'Cannot load server (invalid port)' ); - return nextServerType(Errors.Invalid(`Invalid port: ${confPort}`)); - } - - server.httpServer.listen(port, conf.address, err => { - if(err) { - return nextServerType(err); + forEachSeries( + WSS_SERVER_TYPES, + (serverType, nextServerType) => { + const server = this[serverType]; + if (!server) { + return nextServerType(null); } - server.wsServer.on('connection', (ws, req) => { - const webSocketClient = new WebSocketClient(ws, req, serverType); - this.handleNewClient(webSocketClient, webSocketClient.socket, ModuleInfo); - }); + const serverName = `${ModuleInfo.name} (${serverType})`; + const conf = _.get(Config(), [ + 'loginServers', + 'webSocket', + 'secure' === serverType ? 'wss' : 'ws', + ]); + const confPort = conf.port; + const port = parseInt(confPort); - Log.info( { server : serverName, port : port }, 'Listening for connections' ); - return nextServerType(null); - }); - }, - err => { - cb(err); - }); + if (isNaN(port)) { + Log.error( + { server: serverName, port: confPort }, + 'Cannot load server (invalid port)' + ); + return nextServerType(Errors.Invalid(`Invalid port: ${confPort}`)); + } + + server.httpServer.listen(port, conf.address, err => { + if (err) { + return nextServerType(err); + } + + server.wsServer.on('connection', (ws, req) => { + const webSocketClient = new WebSocketClient(ws, req, serverType); + this.handleNewClient( + webSocketClient, + webSocketClient.socket, + ModuleInfo + ); + }); + + Log.info( + { server: serverName, port: port }, + 'Listening for connections' + ); + return nextServerType(null); + }); + }, + err => { + cb(err); + } + ); } }; diff --git a/core/set_newscan_date.js b/core/set_newscan_date.js index 7713f647..427119eb 100644 --- a/core/set_newscan_date.js +++ b/core/set_newscan_date.js @@ -2,36 +2,36 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const Errors = require('./enig_error.js').Errors; -const FileEntry = require('./file_entry.js'); -const FileBaseFilters = require('./file_base_filter.js'); -const { getAvailableFileAreaTags } = require('./file_base_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const Errors = require('./enig_error.js').Errors; +const FileEntry = require('./file_entry.js'); +const FileBaseFilters = require('./file_base_filter.js'); +const { getAvailableFileAreaTags } = require('./file_base_area.js'); const { getSortedAvailMessageConferences, getSortedAvailMessageAreasByConfTag, updateMessageAreaLastReadId, - getMessageIdNewerThanTimestampByArea -} = require('./message_area.js'); -const UserProps = require('./user_property.js'); + getMessageIdNewerThanTimestampByArea, +} = require('./message_area.js'); +const UserProps = require('./user_property.js'); // deps -const async = require('async'); -const moment = require('moment'); -const _ = require('lodash'); +const async = require('async'); +const moment = require('moment'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'Set New Scan Date', - desc : 'Sets new scan date for applicable scans', - author : 'NuSkooler', + name: 'Set New Scan Date', + desc: 'Sets new scan date for applicable scans', + author: 'NuSkooler', }; const MciViewIds = { - main : { - scanDate : 1, - targetSelection : 2, - } + main: { + scanDate: 1, + targetSelection: 2, + }, }; // :TODO: for messages, we could insert "conf - all areas" into targets, and allow such @@ -42,69 +42,87 @@ exports.getModule = class SetNewScanDate extends MenuModule { const config = this.menuConfig.config; - this.target = config.target || 'message'; + this.target = config.target || 'message'; this.scanDateFormat = config.scanDateFormat || 'YYYYMMDD'; this.menuMethods = { - scanDateSubmit : (formData, extraArgs, cb) => { + scanDateSubmit: (formData, extraArgs, cb) => { let scanDate = _.get(formData, 'value.scanDate'); - if(!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`)); + 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')); + 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) { + if ('' === target.area.areaTag) { updateAreaTags = this.targetSelections - .map( targetSelection => targetSelection.area.areaTag ) - .filter( areaTag => areaTag ); // remove the blank 'all' entry + .map(targetSelection => targetSelection.area.areaTag) + .filter(areaTag => areaTag); // remove the blank 'all' entry } else { - updateAreaTags = [ target.area.areaTag ]; + updateAreaTags = [target.area.areaTag]; } - async.each(updateAreaTags, (areaTag, nextAreaTag) => { - getMessageIdNewerThanTimestampByArea(areaTag, scanDate, (err, messageId) => { - if(err) { - return nextAreaTag(err); - } - - if(!messageId) { - return nextAreaTag(null); // nothing to do - } - - messageId = Math.max(messageId - 1, 0); - - return updateMessageAreaLastReadId( - this.client.user.userId, + async.each( + updateAreaTags, + (areaTag, nextAreaTag) => { + getMessageIdNewerThanTimestampByArea( areaTag, - messageId, - true, // allowOlder - nextAreaTag + scanDate, + (err, messageId) => { + if (err) { + return nextAreaTag(err); + } + + if (!messageId) { + return nextAreaTag(null); // nothing to do + } + + messageId = Math.max(messageId - 1, 0); + + return updateMessageAreaLastReadId( + this.client.user.userId, + areaTag, + messageId, + true, // allowOlder + nextAreaTag + ); + } ); - }); - }, err => { - return cb(err); - }); + }, + err => { + return cb(err); + } + ); } setNewScanDateForFileBase(targetSelection, scanDate, cb) { @@ -114,19 +132,19 @@ exports.getModule = class SetNewScanDate extends MenuModule { // to the user. // const filterCriteria = { - areaTag : getAvailableFileAreaTags(this.client), - newerThanTimestamp : scanDate, - limit : 1, - orderBy : 'upload_timestamp', - order : 'ascending', + areaTag: getAvailableFileAreaTags(this.client), + newerThanTimestamp: scanDate, + limit: 1, + orderBy: 'upload_timestamp', + order: 'ascending', }; FileEntry.findFiles(filterCriteria, (err, fileIds) => { - if(err) { + if (err) { return cb(err); } - if(!fileIds || 0 === fileIds.length) { + if (!fileIds || 0 === fileIds.length) { // nothing to do return cb(null); } @@ -136,7 +154,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { return FileBaseFilters.setFileBaseLastViewedFileIdForUser( this.client.user, pointerFileId, - true, // allowOlder + true, // allowOlder cb ); }); @@ -149,48 +167,53 @@ exports.getModule = class SetNewScanDate extends MenuModule { // const selections = []; getSortedAvailMessageConferences(this.client).forEach(conf => { - getSortedAvailMessageAreasByConfTag(conf.confTag, { client : this.client } ).forEach(area => { + getSortedAvailMessageAreasByConfTag(conf.confTag, { + client: this.client, + }).forEach(area => { selections.push({ - conf : { - confTag : conf.confTag, - text : conf.conf.name, // standard - name : conf.conf.name, - desc : conf.conf.desc, + conf: { + confTag: conf.confTag, + text: conf.conf.name, // standard + name: conf.conf.name, + desc: conf.conf.desc, + }, + area: { + areaTag: area.areaTag, + text: area.area.name, // standard + name: area.area.name, + desc: area.area.desc, }, - area : { - areaTag : area.areaTag, - text : area.area.name, // standard - name : area.area.name, - desc : area.area.desc, - } }); }); }); selections.unshift({ - conf : { - confTag : '', - text : 'All conferences', - name : 'All conferences', - desc : 'All conferences', + conf: { + confTag: '', + text: 'All conferences', + name: 'All conferences', + desc: 'All conferences', + }, + area: { + areaTag: '', + text: 'All areas', + name: 'All areas', + desc: 'All areas', }, - area : { - areaTag : '', - text : 'All areas', - name : 'All areas', - desc : 'All areas', - } }); // Find current conf/area & move it directly under "All" - const currConfTag = this.client.user.properties[UserProps.MessageConfTag]; - const currAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; - if(currConfTag && currAreaTag) { - const confAreaIndex = selections.findIndex( confArea => { - return confArea.conf.confTag === currConfTag && confArea.area.areaTag === currAreaTag; + const currConfTag = this.client.user.properties[UserProps.MessageConfTag]; + const currAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; + if (currConfTag && currAreaTag) { + const confAreaIndex = selections.findIndex(confArea => { + return ( + confArea.conf.confTag === currConfTag && + confArea.area.areaTag === currAreaTag + ); }); - if(confAreaIndex > -1) { + if (confAreaIndex > -1) { selections.splice(1, 0, selections.splice(confAreaIndex, 1)[0]); } } @@ -202,31 +225,41 @@ exports.getModule = class SetNewScanDate extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(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}`)); + 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); + return vc.loadFromMenuConfig( + { callingMenu: self, mciMap: mciData.menu }, + callback + ); }, function loadAvailSelections(callback) { - switch(self.target) { - case 'message' : + switch (self.target) { + case 'message': return self.loadAvailMessageBaseSelections(callback); - default : + default: return callback(null); } }, @@ -236,11 +269,16 @@ exports.getModule = class SetNewScanDate extends MenuModule { const scanDateView = vc.getView(MciViewIds.main.scanDate); // :TODO: MaskTextEditView needs some love: If setText() with input that matches the mask, we should ignore the non-mask chars! Hack in place for now - const scanDateFormat = self.scanDateFormat.replace(/[/\-. ]/g, ''); + const scanDateFormat = self.scanDateFormat.replace( + /[/\-. ]/g, + '' + ); scanDateView.setText(today.format(scanDateFormat)); - if('message' === self.target) { - const targetSelectionView = vc.getView(MciViewIds.main.targetSelection); + if ('message' === self.target) { + const targetSelectionView = vc.getView( + MciViewIds.main.targetSelection + ); targetSelectionView.setItems(self.targetSelections); targetSelectionView.setFocusItemIndex(0); @@ -249,7 +287,7 @@ exports.getModule = class SetNewScanDate extends MenuModule { 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 4236760c..2359081c 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -2,28 +2,28 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const Errors = require('../core/enig_error.js').Errors; -const ANSI = require('./ansi_term.js'); -const Config = require('./config.js').get; -const { - getMessageAreaByTag -} = require('./message_area.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const Errors = require('../core/enig_error.js').Errors; +const ANSI = require('./ansi_term.js'); +const Config = require('./config.js').get; +const { getMessageAreaByTag } = require('./message_area.js'); // deps -const async = require('async'); -const _ = require('lodash'); +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 }); + 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); @@ -41,49 +41,56 @@ exports.getModule = class ShowArtModule extends MenuModule { // // How we show art depends on our configuration // - let handler = { - extraArgs : self.showByExtraArgs, - sequence : self.showBySequence, - random : self.showByRandom, - fileBaseArea : self.showByFileBaseArea, - messageConf : self.showByMessageConf, - messageArea : self.showByMessageArea, - }[self.config.method] || self.showRandomArt; + let handler = + { + extraArgs: self.showByExtraArgs, + sequence: self.showBySequence, + random: self.showByRandom, + fileBaseArea: self.showByFileBaseArea, + messageConf: self.showByMessageConf, + messageArea: self.showByMessageArea, + }[self.config.method] || self.showRandomArt; handler = handler.bind(self); return handler(callback); - } + }, ], err => { - if(err && !self.config.optional) { - self.client.log.warn('Error during init sequence', { error : err.message } ); - return self.prevMenu( () => { /* dummy */ } ); + 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 */ } ); + return self.autoNextMenu(() => { + /* dummy */ + }); } ); } showByExtraArgs(cb) { const artData = _.get(this.config, 'extraArgs.artData'); - if(Buffer.isBuffer(artData)) { + if (Buffer.isBuffer(artData)) { const options = { - pause : this.shouldPause(), - desc : 'extraArgs', + pause: this.shouldPause(), + desc: 'extraArgs', }; return this.displaySingleArtWithOptions(artData, options, cb); } this.getArtKeyValue(this.config.key, (err, artSpec) => { - if(err) { + if (err) { return cb(err); } const options = { - pause : this.shouldPause(), - desc : 'extraArgs', + pause: this.shouldPause(), + desc: 'extraArgs', }; return this.displaySingleArtWithOptions(artSpec, options, cb); }); @@ -99,58 +106,68 @@ exports.getModule = class ShowArtModule extends MenuModule { showByFileBaseArea(cb) { this.getArtKeyValue('areaTag', (err, key) => { - if(err) { + if (err) { return cb(err); } - return this.displaySingleArtByConfigPath( [ 'fileBase', 'areas', key, 'art' ], cb); + return this.displaySingleArtByConfigPath( + ['fileBase', 'areas', key, 'art'], + cb + ); }); } showByMessageConf(cb) { this.getArtKeyValue('confTag', (err, key) => { - if(err) { + if (err) { return cb(err); } - return this.displaySingleArtByConfigPath( [ 'messageConferences', key, 'art' ], cb); + return this.displaySingleArtByConfigPath( + ['messageConferences', key, 'art'], + cb + ); }); } showByMessageArea(cb) { this.getArtKeyValue('areaTag', (err, key) => { - if(err) { + if (err) { return cb(err); } const area = getMessageAreaByTag(key); - if(!area) { + if (!area) { return cb(Errors.DoesNotExist(`No area by areaTag ${key} found`)); } - return cb(null); // :TODO: REMOVE ME --- currently NYI + return cb(null); // :TODO: REMOVE ME --- currently NYI }); } displaySingleArtByConfigPath(configPath, cb) { const desc = configPath.join('.'); const artSpec = _.get(Config(), configPath); - if(!artSpec) { + if (!artSpec) { return cb(Errors.MissingConfig(`No art defined at path ${desc}`)); } const options = { desc, - pause : this.shouldPause(), + pause: this.shouldPause(), }; return this.displaySingleArtWithOptions(artSpec, options, cb); } getArtKeyValue(defaultKey, cb) { const key = this.config.key || defaultKey; - if(!_.isString(key)) { - return cb(Errors.MissingConfig('Config option "key" is required for method "extraArgs"')); + 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)) { + const artKey = _.get(this.config, ['extraArgs'].concat(path)); + if (!_.isString(artKey)) { return cb(Errors.MissingParam(`Invalid or missing "extraArgs.${key}" value`)); } @@ -163,22 +180,21 @@ exports.getModule = class ShowArtModule extends MenuModule { [ 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.config, - (err, artData) => { - if(err) { - return callback(err); - } - const mciData = { menu : artData.mciMap }; - if(self.client.term.termHeight > 0 && artData.height > self.client.term.termHeight) { - // We must have scrolled, adjust the positioning for pause - artData.height = self.client.term.termHeight; - } - const pausePosition = { row: artData.height + 1, col: 1}; - return callback(null, mciData, pausePosition); + self.displayAsset(artSpec, self.menuConfig.config, (err, artData) => { + if (err) { + return callback(err); } - ); + const mciData = { menu: artData.mciMap }; + if ( + self.client.term.termHeight > 0 && + artData.height > self.client.term.termHeight + ) { + // We must have scrolled, adjust the positioning for pause + artData.height = self.client.term.termHeight; + } + const pausePosition = { row: artData.height + 1, col: 1 }; + return callback(null, mciData, pausePosition); + }); }, function afterArtDisplayed(mciData, pausePosition, callback) { self.mciReady(mciData, err => { @@ -186,15 +202,18 @@ exports.getModule = class ShowArtModule extends MenuModule { }); }, function displayPauseIfRequested(pausePosition, callback) { - if(!options.pause) { + 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`); + 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 9547c9b8..6954f27f 100644 --- a/core/spinner_menu_view.js +++ b/core/spinner_menu_view.js @@ -1,21 +1,21 @@ /* jslint node: true */ 'use strict'; -const MenuView = require('./menu_view.js').MenuView; -const ansi = require('./ansi_term.js'); -const strUtil = require('./string_util.js'); -const { pipeToAnsi } = require('./color_codes.js'); -const formatString = require('./string_format'); +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); +const { pipeToAnsi } = require('./color_codes.js'); +const formatString = require('./string_format'); -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); exports.SpinnerMenuView = SpinnerMenuView; function SpinnerMenuView(options) { options.justify = options.justify || 'left'; - options.cursor = options.cursor || 'hide'; + options.cursor = options.cursor || 'hide'; MenuView.call(this, options); @@ -29,7 +29,7 @@ function SpinnerMenuView(options) { }; */ - this.updateSelection = function() { + this.updateSelection = function () { //assert(!self.positionCacheExpired); assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); @@ -38,54 +38,77 @@ function SpinnerMenuView(options) { this.emit('index update', this.focusedItemIndex); }; - this.drawItem = function(index) { + this.drawItem = function (index) { const item = this.items[index]; - if(!item) { + if (!item) { return; } const cached = this.getRenderCacheItem(index, this.hasFocus); - if(cached) { - return this.client.term.write(`${ansi.goto(this.position.row, this.position.col)}${cached}`); + if (cached) { + return this.client.term.write( + `${ansi.goto(this.position.row, this.position.col)}${cached}` + ); } let text; let sgr; - if(this.complexItems) { - text = pipeToAnsi(formatString(this.hasFocus && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); - sgr = this.focusItemFormat ? '' : (this.hasFocus ? this.getFocusSGR() : self.getSGR()); + if (this.complexItems) { + text = pipeToAnsi( + formatString( + this.hasFocus && this.focusItemFormat + ? this.focusItemFormat + : this.itemFormat, + item + ) + ); + sgr = this.focusItemFormat + ? '' + : this.hasFocus + ? this.getFocusSGR() + : self.getSGR(); } else { - text = strUtil.stylizeString(item.text, this.hasFocus ? self.focusTextStyle : self.textStyle); - sgr = this.hasFocus ? this.getFocusSGR() : this.getSGR(); + text = strUtil.stylizeString( + item.text, + this.hasFocus ? self.focusTextStyle : self.textStyle + ); + sgr = this.hasFocus ? this.getFocusSGR() : this.getSGR(); } - text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; - this.client.term.write(`${ansi.goto(this.position.row, this.position.col)}${text}`); + text = `${sgr}${strUtil.pad( + text, + this.dimens.width, + this.fillChar, + this.justify + )}`; + this.client.term.write( + `${ansi.goto(this.position.row, this.position.col)}${text}` + ); this.setRenderCacheItem(index, text, this.hasFocus); }; } util.inherits(SpinnerMenuView, MenuView); -SpinnerMenuView.prototype.redraw = function() { +SpinnerMenuView.prototype.redraw = function () { SpinnerMenuView.super_.prototype.redraw.call(this); this.drawItem(this.focusedItemIndex); }; -SpinnerMenuView.prototype.setFocus = function(focused) { +SpinnerMenuView.prototype.setFocus = function (focused) { SpinnerMenuView.super_.prototype.setFocus.call(this, focused); this.redraw(); }; -SpinnerMenuView.prototype.setFocusItemIndex = function(index) { - SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex +SpinnerMenuView.prototype.setFocusItemIndex = function (index) { + SpinnerMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex this.updateSelection(); // will redraw }; -SpinnerMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('up', key.name)) { - if(0 === this.focusedItemIndex) { +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--; @@ -93,8 +116,8 @@ SpinnerMenuView.prototype.onKeyPress = function(ch, key) { this.updateSelection(); return; - } else if(this.isKeyMapped('down', key.name)) { - if(this.items.length - 1 === this.focusedItemIndex) { + } else if (this.isKeyMapped('down', key.name)) { + if (this.items.length - 1 === this.focusedItemIndex) { this.focusedItemIndex = 0; } else { this.focusedItemIndex++; @@ -108,7 +131,7 @@ SpinnerMenuView.prototype.onKeyPress = function(ch, key) { SpinnerMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; -SpinnerMenuView.prototype.getData = function() { +SpinnerMenuView.prototype.getData = function () { const item = this.getItem(this.focusedItemIndex); return _.isString(item.data) ? item.data : this.focusedItemIndex; }; diff --git a/core/standard_menu.js b/core/standard_menu.js index a4dacb95..eaa73f82 100644 --- a/core/standard_menu.js +++ b/core/standard_menu.js @@ -1,12 +1,12 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; +const MenuModule = require('./menu_module.js').MenuModule; exports.moduleInfo = { - name : 'Standard Menu Module', - desc : 'A Menu Module capable of handing standard configurations', - author : 'NuSkooler', + name: 'Standard Menu Module', + desc: 'A Menu Module capable of handing standard configurations', + author: 'NuSkooler', }; exports.getModule = class StandardMenuModule extends MenuModule { @@ -16,7 +16,7 @@ exports.getModule = class StandardMenuModule extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } diff --git a/core/stat_log.js b/core/stat_log.js index af88ff57..aa921c9b 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -1,15 +1,13 @@ /* jslint node: true */ 'use strict'; -const sysDb = require('./database.js').dbs.system; -const { - getISOTimestampString -} = require('./database.js'); -const Errors = require('./enig_error.js'); +const sysDb = require('./database.js').dbs.system; +const { getISOTimestampString } = require('./database.js'); +const Errors = require('./enig_error.js'); // deps -const _ = require('lodash'); -const moment = require('moment'); +const _ = require('lodash'); +const moment = require('moment'); /* System Event Log & Stats @@ -38,7 +36,7 @@ class StatLog { `SELECT stat_name, stat_value FROM system_stat;`, (err, row) => { - if(row) { + if (row) { self.systemStats[row.stat_name] = row.stat_value; } }, @@ -50,25 +48,25 @@ class StatLog { get KeepDays() { return { - Forever : -1, + Forever: -1, }; } get KeepType() { return { - Forever : 'forever', - Days : 'days', - Max : 'max', - Count : 'max', + Forever: 'forever', + Days: 'days', + Max: 'max', + Count: 'max', }; } get Order() { return { - Timestamp : 'timestamp_asc', - TimestampAsc : 'timestamp_asc', - TimestampDesc : 'timestamp_desc', - Random : 'random', + Timestamp: 'timestamp_asc', + TimestampAsc: 'timestamp_asc', + TimestampDesc: 'timestamp_desc', + Random: 'random', }; } @@ -80,7 +78,7 @@ class StatLog { incrementBy = incrementBy || 1; let newValue = parseInt(this.systemStats[statName]); - if(!isNaN(newValue)) { + if (!isNaN(newValue)) { newValue += incrementBy; } else { newValue = incrementBy; @@ -97,17 +95,19 @@ class StatLog { sysDb.run( `REPLACE INTO system_stat (stat_name, stat_value) VALUES (?, ?);`, - [ statName, statValue ], + [statName, statValue], err => { // cb optional - callers may fire & forget - if(cb) { + 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; @@ -126,9 +126,13 @@ class StatLog { // note: cb is optional in PersistUserProperty user.persistProperty(statName, statValue, cb); - if(!options.noEvent) { - const Events = require('./events.js'); // we need to late load currently - Events.emit(Events.getSystemEvents().UserStatSet, { user, statName, statValue } ); + if (!options.noEvent) { + const Events = require('./events.js'); // we need to late load currently + Events.emit(Events.getSystemEvents().UserStatSet, { + user, + statName, + statValue, + }); } } @@ -150,31 +154,22 @@ class StatLog { const oldValue = user.getPropertyAsNumber(statName) || 0; const newValue = oldValue + incrementBy; - this.setUserStatWithOptions( - user, - statName, - newValue, - { noEvent : true }, - err => { - if(!err) { - const Events = require('./events.js'); // we need to late load currently - Events.emit( - Events.getSystemEvents().UserStatIncrement, - { - user, - statName, - oldValue, - statIncrementBy : incrementBy, - statValue : newValue - } - ); - } - - if(cb) { - return cb(err); - } + this.setUserStatWithOptions(user, statName, newValue, { noEvent: true }, err => { + if (!err) { + const Events = require('./events.js'); // we need to late load currently + Events.emit(Events.getSystemEvents().UserStatIncrement, { + user, + statName, + oldValue, + statIncrementBy: incrementBy, + statValue: newValue, + }); } - ); + + if (cb) { + return cb(err); + } + }); } // the time "now" in the ISO format we use and love :) @@ -186,28 +181,28 @@ class StatLog { sysDb.run( `INSERT INTO system_event_log (timestamp, log_name, log_value) VALUES (?, ?, ?);`, - [ this.now, logName, logValue ], + [this.now, logName, logValue], () => { // // Handle keep // - if(-1 === keep) { - if(cb) { + if (-1 === keep) { + if (cb) { return cb(null); } return; } - switch(keepType) { + switch (keepType) { // keep # of days - case 'days' : + case 'days': sysDb.run( `DELETE FROM system_event_log WHERE log_name = ? AND timestamp <= DATETIME("now", "-${keep} day");`, - [ logName ], + [logName], err => { // cb optional - callers may fire & forget - if(cb) { + if (cb) { return cb(err); } } @@ -215,7 +210,7 @@ class StatLog { break; case 'count': - case 'max' : + case 'max': // keep max of N/count sysDb.run( `DELETE FROM system_event_log @@ -226,17 +221,17 @@ class StatLog { ORDER BY id DESC LIMIT -1 OFFSET ${keep} );`, - [ logName ], + [logName], err => { - if(cb) { + if (cb) { return cb(err); } } ); break; - case 'forever' : - default : + case 'forever': + default: // nop break; } @@ -256,69 +251,69 @@ class StatLog { */ findSystemLogEntries(filter, cb) { filter = filter || {}; - if(!_.isString(filter.logName)) { + if (!_.isString(filter.logName)) { return cb(Errors.MissingParam('filter.logName is required')); } - filter.resultType = filter.resultType || 'obj'; - filter.order = filter.order || 'timestamp'; + filter.resultType = filter.resultType || 'obj'; + filter.order = filter.order || 'timestamp'; let sql; - if('count' === filter.resultType) { - sql = - `SELECT COUNT() AS count + if ('count' === filter.resultType) { + sql = `SELECT COUNT() AS count FROM system_event_log`; } else { - sql = - `SELECT timestamp, log_value + sql = `SELECT timestamp, log_value FROM system_event_log`; } sql += ' WHERE log_name = ?'; - if(filter.date) { + if (filter.date) { filter.date = moment(filter.date); - sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`; + sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format( + 'YYYY-MM-DD' + )}")`; } - if('count' !== filter.resultType) { - switch(filter.order) { - case 'timestamp' : - case 'timestamp_asc' : + if ('count' !== filter.resultType) { + switch (filter.order) { + case 'timestamp': + case 'timestamp_asc': sql += ' ORDER BY timestamp ASC'; break; - case 'timestamp_desc' : + case 'timestamp_desc': sql += ' ORDER BY timestamp DESC'; break; - case 'random' : + case 'random': sql += ' ORDER BY RANDOM()'; break; } } - if(_.isNumber(filter.limit) && 0 !== filter.limit) { + if (_.isNumber(filter.limit) && 0 !== filter.limit) { sql += ` LIMIT ${filter.limit}`; } sql += ';'; - if('count' === filter.resultType) { - sysDb.get(sql, [ filter.logName ], (err, row) => { + if ('count' === filter.resultType) { + sysDb.get(sql, [filter.logName], (err, row) => { return cb(err, row ? row.count : 0); }); } else { - sysDb.all(sql, [ filter.logName ], (err, rows) => { + sysDb.all(sql, [filter.logName], (err, rows) => { return cb(err, rows); }); } } getSystemLogEntries(logName, order, limit, cb) { - if(!cb && _.isFunction(limit)) { - cb = limit; - limit = 0; + if (!cb && _.isFunction(limit)) { + cb = limit; + limit = 0; } else { limit = limit || 0; } @@ -335,10 +330,10 @@ class StatLog { sysDb.run( `INSERT INTO user_event_log (timestamp, user_id, session_id, log_name, log_value) VALUES (?, ?, ?, ?, ?);`, - [ this.now, user.userId, user.sessionId, logName, logValue ], + [this.now, user.userId, user.sessionId, logName, logValue], err => { - if(err) { - if(cb) { + if (err) { + if (cb) { cb(err); } return; @@ -346,8 +341,8 @@ class StatLog { // // Handle keepDays // - if(-1 === keepDays) { - if(cb) { + if (-1 === keepDays) { + if (cb) { return cb(null); } return; @@ -356,10 +351,10 @@ class StatLog { sysDb.run( `DELETE FROM user_event_log WHERE user_id = ? AND log_name = ? AND timestamp <= DATETIME("now", "-${keepDays} day");`, - [ user.userId, logName ], + [user.userId, logName], err => { // cb optional - callers may fire & forget - if(cb) { + if (cb) { return cb(err); } } diff --git a/core/string_format.js b/core/string_format.js index 4a5b110c..4f3f4ee9 100644 --- a/core/string_format.js +++ b/core/string_format.js @@ -1,20 +1,22 @@ /* jslint node: true */ 'use strict'; -const EnigError = require('./enig_error.js').EnigError; +const EnigError = require('./enig_error.js').EnigError; const { pad, stylizeString, renderStringLength, renderSubstr, - formatByteSize, formatByteSizeAbbr, - formatCount, formatCountAbbr, -} = require('./string_util.js'); + formatByteSize, + formatByteSizeAbbr, + formatCount, + formatCountAbbr, +} = require('./string_util.js'); // deps -const _ = require('lodash'); -const moment = require('moment'); +const _ = require('lodash'); +const moment = require('moment'); /* String formatting HEAVILY inspired by David Chambers string-format library @@ -25,27 +27,27 @@ const moment = require('moment'); and ANSI escape sequences. */ -class ValueError extends EnigError { } -class KeyError extends EnigError { } +class ValueError extends EnigError {} +class KeyError extends EnigError {} const SpecRegExp = { - FillAlign : /^(.)?([<>=^])/, - Sign : /^[ +-]/, - Width : /^\d*/, - Precision : /^\d+/, + FillAlign: /^(.)?([<>=^])/, + Sign: /^[ +-]/, + Width: /^\d*/, + Precision: /^\d+/, }; function tokenizeFormatSpec(spec) { const tokens = { - fill : '', - align : '', - sign : '', - '#' : false, - '0' : false, - width : '', - ',' : false, - precision : '', - type : '', + fill: '', + align: '', + sign: '', + '#': false, + 0: false, + width: '', + ',': false, + precision: '', + type: '', }; let index = 0; @@ -56,8 +58,8 @@ function tokenizeFormatSpec(spec) { } match = SpecRegExp.FillAlign.exec(spec); - if(match) { - if(match[1]) { + if (match) { + if (match[1]) { tokens.fill = match[1]; } tokens.align = match[2]; @@ -65,17 +67,17 @@ function tokenizeFormatSpec(spec) { } match = SpecRegExp.Sign.exec(spec.slice(index)); - if(match) { + if (match) { tokens.sign = match[0]; incIndexByMatch(); } - if('#' === spec.charAt(index)) { + if ('#' === spec.charAt(index)) { tokens['#'] = true; ++index; } - if('0' === spec.charAt(index)) { + if ('0' === spec.charAt(index)) { tokens['0'] = true; ++index; } @@ -84,16 +86,16 @@ function tokenizeFormatSpec(spec) { tokens.width = match[0]; incIndexByMatch(); - if(',' === spec.charAt(index)) { + if (',' === spec.charAt(index)) { tokens[','] = true; ++index; } - if('.' === spec.charAt(index)) { + if ('.' === spec.charAt(index)) { ++index; match = SpecRegExp.Precision.exec(spec.slice(index)); - if(!match) { + if (!match) { throw new ValueError('Format specifier missing precision'); } @@ -101,17 +103,17 @@ function tokenizeFormatSpec(spec) { incIndexByMatch(); } - if(index < spec.length) { + if (index < spec.length) { tokens.type = spec.charAt(index); ++index; } - if(index < spec.length) { + 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; @@ -122,120 +124,142 @@ function quote(s) { } 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 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) { + 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) { + if (tokens.sign) { throw new ValueError('Sign not allowed in string format specifier'); } - if(tokens['#']) { + if (tokens['#']) { throw new ValueError('Alternate form (#) not allowed in string format specifier'); } - if('=' === align) { + 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)) { + 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' : + 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) { + if ('' !== tokens.sign && 'c' !== type) { throw new ValueError(`Sign not allowed with integer format specifier 'c'`); // eslint-disable-line quotes } - if(tokens[','] && 'd' !== type) { + if (tokens[','] && 'd' !== type) { throw new ValueError(`Cannot specify ',' with '${type}'`); } - if('' !== tokens.precision) { + 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'); + } 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); + if (tokens[',']) { + const match = /^(\d*)(.*)$/.exec(s); const separated = match[1].replace(/.(?=(...)+$)/g, '$&,') + match[2]; - if('=' !== align) { + if ('=' !== align) { return pad(sign + separated, width, fill, getPadAlign(align)); } - if('0' === fill) { + if ('0' === fill) { const shortfall = Math.max(0, width - sign.length - separated.length); - const digits = /^\d*/.exec(separated)[0].length; - let padding = ''; + const digits = /^\d*/.exec(separated)[0].length; + let padding = ''; // :TODO: do this differntly... - for(let n = 0; n < shortfall; n++) { + for (let n = 0; n < shortfall; n++) { padding = ((digits + n) % 4 === 3 ? ',' : '0') + padding; } @@ -245,12 +269,16 @@ function formatNumber(value, tokens) { return sign + pad(separated, width - sign.length, fill, getPadAlign('>')); } - if(0 === width) { + 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)); @@ -258,51 +286,51 @@ function formatNumber(value, tokens) { const transformers = { // String standard - toUpperCase : String.prototype.toUpperCase, - toLowerCase : String.prototype.toLowerCase, + 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'), + 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(), - 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), - durationHours : (h) => moment.duration(h, 'hours').humanize(), - durationMinutes : (m) => moment.duration(m, 'minutes').humanize(), - durationSeconds : (s) => moment.duration(s, 'seconds').humanize(), + durationHours: h => moment.duration(h, 'hours').humanize(), + durationMinutes: m => moment.duration(m, 'minutes').humanize(), + durationSeconds: s => moment.duration(s, 'seconds').humanize(), }; function transformValue(transformerName, value) { - if(transformerName in transformers) { + if (transformerName in transformers) { const transformer = transformers[transformerName]; - value = transformer.apply(value, [ value ] ); + value = transformer.apply(value, [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; +const REGEXP_BASIC_FORMAT = /{([^.!:}]+(?:\.[^.!:}]+)*)(?:!([^:}]+))?(?::([^}]+))?}/g; function getValue(obj, path) { const value = _.get(obj, path); - if(!_.isUndefined(value)) { + if (!_.isUndefined(value)) { return _.isFunction(value) ? value() : value; } @@ -310,58 +338,56 @@ function getValue(obj, path) { } module.exports = function format(fmt, obj) { - const re = REGEXP_BASIC_FORMAT; - re.lastIndex = 0; // reset from prev + re.lastIndex = 0; // reset from prev let match; let pos; let out = ''; - let objPath ; + let objPath; let transformer; let formatSpec; let value; let tokens; do { - pos = re.lastIndex; - match = re.exec(fmt); + pos = re.lastIndex; + match = re.exec(fmt); - if(match) { - if(match.index > pos) { + if (match) { + if (match.index > pos) { out += fmt.slice(pos, match.index); } - objPath = match[1]; + objPath = match[1]; transformer = match[2]; - formatSpec = match[3]; + formatSpec = match[3]; try { - value = getValue(obj, objPath); - if(transformer) { + value = getValue(obj, objPath); + if (transformer) { value = transformValue(transformer, value); } tokens = tokenizeFormatSpec(formatSpec || ''); - if(_.isNumber(value)) { + if (_.isNumber(value)) { out += formatNumber(value, tokens); } else { out += formatString(value, tokens); } - } catch(e) { - if(e instanceof KeyError) { - out += match[0]; // preserve full thing - } else if(e instanceof ValueError) { + } catch (e) { + if (e instanceof KeyError) { + out += match[0]; // preserve full thing + } else if (e instanceof ValueError) { out += value.toString(); } } } - - } while(0 !== re.lastIndex); + } while (0 !== re.lastIndex); // remainder - if(pos < fmt.length) { + if (pos < fmt.length) { out += fmt.slice(pos); } diff --git a/core/string_util.js b/core/string_util.js index 407c06c5..fc60f561 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -2,49 +2,46 @@ 'use strict'; // ENiGMA½ -const ANSI = require('./ansi_term.js'); +const ANSI = require('./ansi_term.js'); // deps -const iconv = require('iconv-lite'); -const _ = require('lodash'); +const iconv = require('iconv-lite'); +const _ = require('lodash'); -exports.stylizeString = stylizeString; -exports.pad = pad; -exports.insert = insert; -exports.replaceAt = replaceAt; -exports.isPrintable = isPrintable; -exports.containsNonLatinCodepoints = containsNonLatinCodepoints; -exports.stripAllLineFeeds = stripAllLineFeeds; -exports.debugEscapedString = debugEscapedString; -exports.stringFromNullTermBuffer = stringFromNullTermBuffer; -exports.stringToNullTermBuffer = stringToNullTermBuffer; -exports.renderSubstr = renderSubstr; -exports.renderStringLength = renderStringLength; -exports.ansiRenderStringLength = ansiRenderStringLength; -exports.formatByteSizeAbbr = formatByteSizeAbbr; -exports.formatByteSize = formatByteSize; -exports.formatCountAbbr = formatCountAbbr; -exports.formatCount = formatCount; -exports.stripAnsiControlCodes = stripAnsiControlCodes; -exports.isAnsi = isAnsi; -exports.isAnsiLine = isAnsiLine; -exports.isFormattedLine = isFormattedLine; -exports.splitTextAtTerms = splitTextAtTerms; -exports.wildcardMatch = wildcardMatch; +exports.stylizeString = stylizeString; +exports.pad = pad; +exports.insert = insert; +exports.replaceAt = replaceAt; +exports.isPrintable = isPrintable; +exports.containsNonLatinCodepoints = containsNonLatinCodepoints; +exports.stripAllLineFeeds = stripAllLineFeeds; +exports.debugEscapedString = debugEscapedString; +exports.stringFromNullTermBuffer = stringFromNullTermBuffer; +exports.stringToNullTermBuffer = stringToNullTermBuffer; +exports.renderSubstr = renderSubstr; +exports.renderStringLength = renderStringLength; +exports.ansiRenderStringLength = ansiRenderStringLength; +exports.formatByteSizeAbbr = formatByteSizeAbbr; +exports.formatByteSize = formatByteSize; +exports.formatCountAbbr = formatCountAbbr; +exports.formatCount = formatCount; +exports.stripAnsiControlCodes = stripAnsiControlCodes; +exports.isAnsi = isAnsi; +exports.isAnsiLine = isAnsiLine; +exports.isFormattedLine = isFormattedLine; +exports.splitTextAtTerms = splitTextAtTerms; +exports.wildcardMatch = wildcardMatch; // :TODO: create Unicode version of this -const VOWELS = [ - 'a', 'e', 'i', 'o', 'u', - 'A', 'E', 'I', 'O', 'U', -]; +const VOWELS = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U']; 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) { @@ -53,42 +50,42 @@ function stylizeString(s, style) { var i; var stylized = ''; - switch(style) { + switch (style) { // None/normal - case 'normal' : - case 'N' : + case 'normal': + case 'N': return s; - // UPPERCASE - case 'upper' : - case 'U' : + // UPPERCASE + case 'upper': + case 'U': return s.toUpperCase(); - // lowercase - case 'lower' : - case 'l' : + // lowercase + case 'lower': + case 'l': return s.toLowerCase(); - // Title Case - case 'title' : - case 'T' : + // 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' : + // 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) { + // SMaLL VoWeLS + case 'small vowels': + case 'v': + for (i = 0; i < len; ++i) { c = s[i]; - if(-1 !== VOWELS.indexOf(c)) { + if (-1 !== VOWELS.indexOf(c)) { stylized += c.toLowerCase(); } else { stylized += c.toUpperCase(); @@ -96,12 +93,12 @@ function stylizeString(s, style) { } return stylized; - // bIg vOwELS - case 'big vowels' : - case 'V' : - for(i = 0; i < len; ++i) { + // bIg vOwELS + case 'big vowels': + case 'V': + for (i = 0; i < len; ++i) { c = s[i]; - if(-1 !== VOWELS.indexOf(c)) { + if (-1 !== VOWELS.indexOf(c)) { stylized += c.toUpperCase(); } else { stylized += c.toLowerCase(); @@ -109,16 +106,16 @@ function stylizeString(s, style) { } return stylized; - // Small i's: DEMENTiA - case 'small i' : - case '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) { + // 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(); @@ -126,10 +123,10 @@ function stylizeString(s, style) { } return stylized; - // l337 5p34k - case 'l33t' : - case '3' : - for(i = 0; i < len; ++i) { + // l337 5p34k + case 'l33t': + case '3': + for (i = 0; i < len; ++i) { c = SIMPLE_ELITE_MAP[s[i].toLowerCase()]; stylized += c || s[i]; } @@ -140,38 +137,41 @@ function stylizeString(s, style) { } function pad(s, len, padChar, justify, stringSGR, padSGR, useRenderLen) { - len = len || 0; - padChar = padChar || ' '; - justify = justify || 'left'; - stringSGR = stringSGR || ''; - padSGR = padSGR || ''; - useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; + len = len || 0; + padChar = padChar || ' '; + justify = justify || 'left'; + stringSGR = stringSGR || ''; + padSGR = padSGR || ''; + useRenderLen = _.isUndefined(useRenderLen) ? true : useRenderLen; const renderLen = useRenderLen ? renderStringLength(s) : s.length; - const padlen = len >= renderLen ? len - renderLen : 0; + const padlen = len >= renderLen ? len - renderLen : 0; - switch(justify) { - case 'L' : - case 'left' : + switch (justify) { + case 'L': + case 'left': s = `${stringSGR}${s}${padSGR}${Array(padlen).join(padChar)}`; break; - case 'C' : - case 'center' : - case 'both' : + 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)}`; + const left = padlen - right; + s = `${padSGR}${Array(left + 1).join( + padChar + )}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; } break; - case 'R' : - case 'right' : + case 'R': + case 'right': s = `${padSGR}${Array(padlen).join(padChar)}${stringSGR}${s}`; break; - default : break; + default: + break; } return stringSGR + s; @@ -186,7 +186,7 @@ function replaceAt(s, n, t) { } const RE_NON_PRINTABLE = - /[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/; // eslint-disable-line no-control-regex, no-misleading-character-class + /[\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, no-misleading-character-class function isPrintable(s) { // @@ -199,7 +199,7 @@ function isPrintable(s) { return !RE_NON_PRINTABLE.test(s); } -const NonLatinCodePointsRegExp = /[^\u0000-\u00ff]/; // eslint-disable-line no-control-regex +const NonLatinCodePointsRegExp = /[^\u0000-\u00ff]/; // eslint-disable-line no-control-regex function containsNonLatinCodepoints(s) { if (!s.length) { @@ -222,37 +222,40 @@ function debugEscapedString(s) { } function stringFromNullTermBuffer(buf, encoding) { - let nullPos = buf.indexOf( 0x00 ); - if(-1 === nullPos) { + let nullPos = buf.indexOf(0x00); + if (-1 === nullPos) { nullPos = buf.length; } 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); +function stringToNullTermBuffer(s, options = { encoding: 'utf8', maxBufLen: -1 }) { + let buf = iconv.encode(`${s}\0`, options.encoding).slice(0, options.maxBufLen); buf[buf.length - 1] = '\0'; // make abs sure we null term even if truncated return buf; } -const PIPE_REGEXP = /(\|[A-Z\d]{2})/g; -const ANSI_OR_PIPE_REGEXP = new RegExp(PIPE_REGEXP.source + '|' + ANSI.getFullMatchRegExp().source, 'g'); +const PIPE_REGEXP = /(\|[A-Z\d]{2})/g; +const ANSI_OR_PIPE_REGEXP = new RegExp( + PIPE_REGEXP.source + '|' + ANSI.getFullMatchRegExp().source, + 'g' +); // // Similar to substr() but works with ANSI/Pipe code strings // function renderSubstr(str, start, length) { // shortcut for empty strings - if(0 === str.length) { + if (0 === str.length) { return str; } - start = start || 0; - length = length || str.length - start; + start = start || 0; + length = length || str.length - start; const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the obj; must reset! + re.lastIndex = 0; // we recycle the obj; must reset! let pos = 0; let match; @@ -260,24 +263,27 @@ function renderSubstr(str, start, length) { let renderLen = 0; let s; do { - pos = re.lastIndex; - match = re.exec(str); + pos = re.lastIndex; + match = re.exec(str); - if(match) { - if(match.index > pos) { - s = str.slice(pos + start, Math.min(match.index, pos + (length - renderLen))); - start = 0; // start offset applies only once - out += s; - renderLen += s.length; + 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); + } while (renderLen < length && 0 !== re.lastIndex); // remainder - if(pos + start < str.length && renderLen < length) { - out += str.slice(pos + start, (pos + start + (length - renderLen))); + 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))); } @@ -298,7 +304,7 @@ function renderStringLength(s) { let len = 0; const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the regex; reset + re.lastIndex = 0; // we recycle the regex; reset // // Loop counting only literal (non-control) sequences @@ -306,20 +312,21 @@ function renderStringLength(s) { // do { pos = re.lastIndex; - m = re.exec(s); + m = re.exec(s); - if(m) { - if(m.index > pos) { + if (m) { + if (m.index > pos) { len += s.slice(pos, m.index).length; } - if('C' === m[3]) { // ESC[C is forward/right + if ('C' === m[3]) { + // ESC[C is forward/right len += parseInt(m[2], 10) || 0; } } - } while(0 !== re.lastIndex); + } while (0 !== re.lastIndex); - if(pos < s.length) { + if (pos < s.length) { len += s.slice(pos).length; } @@ -340,49 +347,50 @@ function ansiRenderStringLength(s) { // do { pos = re.lastIndex; - m = re.exec(s); + m = re.exec(s); - if(m) { - if(m.index > pos) { + if (m) { + if (m.index > pos) { len += s.slice(pos, m.index).length; } - if('C' === m[3]) { // ESC[C is forward/right + if ('C' === m[3]) { + // ESC[C is forward/right len += parseInt(m[2], 10) || 0; } } - } while(0 !== re.lastIndex); + } while (0 !== re.lastIndex); - if(pos < s.length) { + if (pos < s.length) { len += s.slice(pos).length; } return len; } -const BYTE_SIZE_ABBRS = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; // :) +const BYTE_SIZE_ABBRS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; // :) function formatByteSizeAbbr(byteSize) { - if(0 === byteSize) { - return BYTE_SIZE_ABBRS[0]; // B + if (0 === byteSize) { + return BYTE_SIZE_ABBRS[0]; // B } return BYTE_SIZE_ABBRS[Math.floor(Math.log(byteSize) / Math.log(1024))]; } function formatByteSize(byteSize, withAbbr = false, decimals = 2) { - const i = 0 === byteSize ? byteSize : Math.floor(Math.log(byteSize) / Math.log(1024)); - let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)); - if(withAbbr) { + 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' ]; +const COUNT_ABBRS = ['', 'K', 'M', 'B', 'T', 'P', 'E', 'Z', 'Y']; function formatCountAbbr(count) { - if(count < 1000) { + if (count < 1000) { return ''; } @@ -390,22 +398,21 @@ function formatCountAbbr(count) { } function formatCount(count, withAbbr = false, decimals = 2) { - const i = 0 === count ? count : Math.floor(Math.log(count) / Math.log(1000)); - let result = parseFloat((count / Math.pow(1000, i)).toFixed(decimals)); - if(withAbbr) { + 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; } - // :TODO: See notes in word_wrap.js about need to consolidate the various ANSI related RegExp's //const REGEXP_ANSI_CONTROL_CODES = /(\x1b\x5b)([\?=;0-9]*?)([0-9A-ORZcf-npsu=><])/g; -const REGEXP_ANSI_CONTROL_CODES = /(?:\x1b\x5b)([?=;0-9]*?)([A-ORZcf-npsu=><])/g; // eslint-disable-line no-control-regex -const ANSI_OPCODES_ALLOWED_CLEAN = [ +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 + 'm', // color ]; function stripAnsiControlCodes(input, options) { @@ -421,26 +428,25 @@ function stripAnsiControlCodes(input, options) { // do { pos = REGEXP_ANSI_CONTROL_CODES.lastIndex; - m = REGEXP_ANSI_CONTROL_CODES.exec(input); + m = REGEXP_ANSI_CONTROL_CODES.exec(input); - if(m) { - if(m.index > pos) { + if (m) { + if (m.index > pos) { cleaned += input.slice(pos, m.index); } - if(options.all) { + if (options.all) { continue; } - if(ANSI_OPCODES_ALLOWED_CLEAN.indexOf(m[2].charAt(0)) > -1) { + 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) { + if (pos < input.length) { cleaned += input.slice(pos); } @@ -448,7 +454,7 @@ function stripAnsiControlCodes(input, options) { } function isAnsiLine(line) { - return isAnsi(line);// || renderStringLength(line) < line.length; + return isAnsi(line); // || renderStringLength(line) < line.length; } // @@ -461,15 +467,16 @@ 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 + if (line.match(/[\t\x00-\x1f\x80-\xff]/)) { + // eslint-disable-line no-control-regex return true; } - if(_.trimEnd(line).match(/[ ]{3,}/)) { + if (_.trimEnd(line).match(/[ ]{3,}/)) { return true; } @@ -478,7 +485,7 @@ function isFormattedLine(line) { // :TODO: rename to containsAnsi() function isAnsi(input) { - if(!input || 0 === input.length) { + if (!input || 0 === input.length) { return false; } @@ -504,7 +511,7 @@ 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 + return m.length >= 4; // :TODO: do this reasonably, e.g. a percent or soemthing } function splitTextAtTerms(s) { @@ -512,6 +519,8 @@ function splitTextAtTerms(s) { } function wildcardMatch(input, rule) { - const escapeRegex = (s) => s.replace(/([.*+?^=!:${}()|[]\/\\])/g, '\\$1'); - return new RegExp('^' + rule.split('*').map(escapeRegex).join('.*') + '$').test(input); -} \ No newline at end of file + const escapeRegex = s => s.replace(/([.*+?^=!:${}()|[]\/\\])/g, '\\$1'); + return new RegExp('^' + rule.split('*').map(escapeRegex).join('.*') + '$').test( + input + ); +} diff --git a/core/sys_event_user_log.js b/core/sys_event_user_log.js index 63ae0e55..586bc6a6 100644 --- a/core/sys_event_user_log.js +++ b/core/sys_event_user_log.js @@ -1,8 +1,8 @@ /* jslint node: true */ 'use strict'; -const Events = require('./events.js'); -const LogNames = require('./user_log_name.js'); +const Events = require('./events.js'); +const LogNames = require('./user_log_name.js'); const DefaultKeepForDays = 365; @@ -11,10 +11,14 @@ module.exports = function systemEventUserLogInit(statLog) { const interestedEvents = [ systemEvents.NewUser, - systemEvents.UserLogin, systemEvents.UserLogoff, - systemEvents.UserUpload, systemEvents.UserDownload, - systemEvents.UserPostMessage, systemEvents.UserSendMail, - systemEvents.UserRunDoor, systemEvents.UserSendNodeMsg, + systemEvents.UserLogin, + systemEvents.UserLogoff, + systemEvents.UserUpload, + systemEvents.UserDownload, + systemEvents.UserPostMessage, + systemEvents.UserSendMail, + systemEvents.UserRunDoor, + systemEvents.UserSendNodeMsg, systemEvents.UserAchievementEarned, ]; @@ -24,49 +28,56 @@ module.exports = function systemEventUserLogInit(statLog) { Events.addMultipleEventListener(interestedEvents, (event, eventName) => { const detailHandler = { - [ systemEvents.NewUser ] : (e) => { + [systemEvents.NewUser]: e => { append(e, LogNames.NewUser, 1); }, - [ systemEvents.UserLogin ] : (e) => { + [systemEvents.UserLogin]: e => { append(e, LogNames.Login, 1); }, - [ systemEvents.UserLogoff ] : (e) => { + [systemEvents.UserLogoff]: e => { append(e, LogNames.Logoff, e.minutesOnline); }, - [ systemEvents.UserUpload ] : (e) => { - if(e.files.length) { // we can get here for dupe uploads + [systemEvents.UserUpload]: e => { + if (e.files.length) { + // we can get here for dupe uploads append(e, LogNames.UlFiles, e.files.length); - const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.meta.byte_size, 0); + const totalBytes = e.files.reduce( + (bytes, fileEntry) => bytes + fileEntry.meta.byte_size, + 0 + ); append(e, LogNames.UlFileBytes, totalBytes); } }, - [ systemEvents.UserDownload ] : (e) => { - if(e.files.length) { + [systemEvents.UserDownload]: e => { + if (e.files.length) { append(e, LogNames.DlFiles, e.files.length); - const totalBytes = e.files.reduce( (bytes, fileEntry) => bytes + fileEntry.byteSize, 0); + const totalBytes = e.files.reduce( + (bytes, fileEntry) => bytes + fileEntry.byteSize, + 0 + ); append(e, LogNames.DlFileBytes, totalBytes); } }, - [ systemEvents.UserPostMessage ] : (e) => { + [systemEvents.UserPostMessage]: e => { append(e, LogNames.PostMessage, e.areaTag); }, - [ systemEvents.UserSendMail ] : (e) => { + [systemEvents.UserSendMail]: e => { append(e, LogNames.SendMail, 1); }, - [ systemEvents.UserRunDoor ] : (e) => { + [systemEvents.UserRunDoor]: e => { append(e, LogNames.RunDoor, e.doorTag); append(e, LogNames.RunDoorMinutes, e.runTimeMinutes); }, - [ systemEvents.UserSendNodeMsg ] : (e) => { + [systemEvents.UserSendNodeMsg]: e => { append(e, LogNames.SendNodeMsg, e.global ? 'global' : 'direct'); }, - [ systemEvents.UserAchievementEarned ] : (e) => { + [systemEvents.UserAchievementEarned]: e => { append(e, LogNames.AchievementEarned, e.achievementTag); append(e, LogNames.AchievementPointsEarned, e.points); - } + }, }[eventName]; - if(detailHandler) { + if (detailHandler) { detailHandler(event); } }); diff --git a/core/system_events.js b/core/system_events.js index 00f242ca..c5969b95 100644 --- a/core/system_events.js +++ b/core/system_events.js @@ -2,25 +2,25 @@ 'use strict'; module.exports = { - ClientConnected : 'codes.l33t.enigma.system.connected', // { client, connectionCount } - ClientDisconnected : 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } - TermDetected : 'codes.l33t.enigma.system.term_detected', // { client } + ClientConnected: 'codes.l33t.enigma.system.connected', // { client, connectionCount } + ClientDisconnected: 'codes.l33t.enigma.system.disconnected', // { client, connectionCount } + TermDetected: 'codes.l33t.enigma.system.term_detected', // { client } - ThemeChanged : 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } - ConfigChanged : 'codes.l33t.enigma.system.config_changed', // (config.hjson) - MenusChanged : 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) + ThemeChanged: 'codes.l33t.enigma.system.theme_changed', // (theme.hjson): { themeId } + ConfigChanged: 'codes.l33t.enigma.system.config_changed', // (config.hjson) + MenusChanged: 'codes.l33t.enigma.system.menus_changed', // (menu.hjson) // User - includes { user, ...} - NewUser : 'codes.l33t.enigma.system.user_new', // { ... } - UserLogin : 'codes.l33t.enigma.system.user_login', // { ... } - UserLogoff : 'codes.l33t.enigma.system.user_logoff', // { ... } - UserUpload : 'codes.l33t.enigma.system.user_upload', // { ..., files[ fileEntry, ...] } - UserDownload : 'codes.l33t.enigma.system.user_download', // { ..., files[ fileEntry, ...] } - UserPostMessage : 'codes.l33t.enigma.system.user_post_msg', // { ..., areaTag } - UserSendMail : 'codes.l33t.enigma.system.user_send_mail', // { ... } - UserRunDoor : 'codes.l33t.enigma.system.user_run_door', // { ..., runTimeMinutes, doorTag|unknown } - UserSendNodeMsg : 'codes.l33t.enigma.system.user_send_node_msg', // { ..., global } - UserStatSet : 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } - UserStatIncrement : 'codes.l33t.enigma.system.user_stat_increment', // { ..., statName, statIncrementBy, statValue } - UserAchievementEarned : 'codes.l33t.enigma.system.user_achievement_earned', // { ..., achievementTag, points, title, text } + NewUser: 'codes.l33t.enigma.system.user_new', // { ... } + UserLogin: 'codes.l33t.enigma.system.user_login', // { ... } + UserLogoff: 'codes.l33t.enigma.system.user_logoff', // { ... } + UserUpload: 'codes.l33t.enigma.system.user_upload', // { ..., files[ fileEntry, ...] } + UserDownload: 'codes.l33t.enigma.system.user_download', // { ..., files[ fileEntry, ...] } + UserPostMessage: 'codes.l33t.enigma.system.user_post_msg', // { ..., areaTag } + UserSendMail: 'codes.l33t.enigma.system.user_send_mail', // { ... } + UserRunDoor: 'codes.l33t.enigma.system.user_run_door', // { ..., runTimeMinutes, doorTag|unknown } + UserSendNodeMsg: 'codes.l33t.enigma.system.user_send_node_msg', // { ..., global } + UserStatSet: 'codes.l33t.enigma.system.user_stat_set', // { ..., statName, statValue } + UserStatIncrement: 'codes.l33t.enigma.system.user_stat_increment', // { ..., statName, statIncrementBy, statValue } + UserAchievementEarned: 'codes.l33t.enigma.system.user_achievement_earned', // { ..., achievementTag, points, title, text } }; diff --git a/core/system_log.js b/core/system_log.js index e753c68b..21c310ed 100644 --- a/core/system_log.js +++ b/core/system_log.js @@ -5,7 +5,6 @@ // Common SYSTEM/global log keys // module.exports = { - UserAddedRumorz : 'system_rumorz', - UserLoginHistory : 'user_login_history', + UserAddedRumorz: 'system_rumorz', + UserLoginHistory: 'user_login_history', }; - diff --git a/core/system_menu_method.js b/core/system_menu_method.js index 75e14dae..2e78f964 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -2,50 +2,56 @@ 'use strict'; // ENiGMA½ -const { removeClient } = require('./client_connections.js'); -const ansiNormal = require('./ansi_term.js').normal; -const { userLogin } = require('./user_login.js'); -const messageArea = require('./message_area.js'); -const { ErrorReasons } = require('./enig_error.js'); -const UserProps = require('./user_property.js'); -const { - loginFactor2_OTP -} = require('./user_2fa_otp.js'); +const { removeClient } = require('./client_connections.js'); +const ansiNormal = require('./ansi_term.js').normal; +const { userLogin } = require('./user_login.js'); +const messageArea = require('./message_area.js'); +const { ErrorReasons } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); +const { loginFactor2_OTP } = require('./user_2fa_otp.js'); // deps -const _ = require('lodash'); -const iconv = require('iconv-lite'); +const _ = require('lodash'); +const iconv = require('iconv-lite'); -exports.login = login; -exports.login2FA_OTP = login2FA_OTP; -exports.logoff = logoff; -exports.prevMenu = prevMenu; -exports.nextMenu = nextMenu; -exports.prevConf = prevConf; -exports.nextConf = nextConf; -exports.prevArea = prevArea; -exports.nextArea = nextArea; +exports.login = login; +exports.login2FA_OTP = login2FA_OTP; +exports.logoff = logoff; +exports.prevMenu = prevMenu; +exports.nextMenu = nextMenu; +exports.prevConf = prevConf; +exports.nextConf = nextConf; +exports.prevArea = prevArea; +exports.nextArea = nextArea; exports.sendForgotPasswordEmail = sendForgotPasswordEmail; -exports.optimizeDatabases = optimizeDatabases; +exports.optimizeDatabases = optimizeDatabases; const handleAuthFailures = (callingMenu, err, cb) => { // already logged in with this user? - if(ErrorReasons.AlreadyLoggedIn === err.reasonCode && - _.has(callingMenu, 'menuConfig.config.tooNodeMenu')) - { + if ( + ErrorReasons.AlreadyLoggedIn === err.reasonCode && + _.has(callingMenu, 'menuConfig.config.tooNodeMenu') + ) { return callingMenu.gotoMenu(callingMenu.menuConfig.config.tooNodeMenu, cb); } // banned username results in disconnect - if(ErrorReasons.NotAllowed === err.reasonCode) { + if (ErrorReasons.NotAllowed === err.reasonCode) { return logoff(callingMenu, {}, {}, cb); } const ReasonsMenus = [ - ErrorReasons.TooMany, ErrorReasons.Disabled, ErrorReasons.Inactive, ErrorReasons.Locked + ErrorReasons.TooMany, + ErrorReasons.Disabled, + ErrorReasons.Inactive, + ErrorReasons.Locked, ]; - if(ReasonsMenus.includes(err.reasonCode)) { - const menu = _.get(callingMenu, [ 'menuConfig', 'config', err.reasonCode.toLowerCase() ]); + if (ReasonsMenus.includes(err.reasonCode)) { + const menu = _.get(callingMenu, [ + 'menuConfig', + 'config', + err.reasonCode.toLowerCase(), + ]); return menu ? callingMenu.gotoMenu(menu, cb) : logoff(callingMenu, {}, {}, cb); } @@ -54,20 +60,24 @@ const handleAuthFailures = (callingMenu, err, cb) => { }; function login(callingMenu, formData, extraArgs, cb) { + userLogin( + callingMenu.client, + formData.value.username, + formData.value.password, + err => { + if (err) { + return handleAuthFailures(callingMenu, err, cb); + } - userLogin(callingMenu.client, formData.value.username, formData.value.password, err => { - if(err) { - return handleAuthFailures(callingMenu, err, cb); + // success! + return callingMenu.nextMenu(cb); } - - // success! - return callingMenu.nextMenu(cb); - }); + ); } function login2FA_OTP(callingMenu, formData, extraArgs, cb) { loginFactor2_OTP(callingMenu.client, formData.value.token, err => { - if(err) { + if (err) { return handleAuthFailures(callingMenu, err, cb); } @@ -83,15 +93,20 @@ function logoff(callingMenu, formData, extraArgs, cb) { // const client = callingMenu.client; - setTimeout( () => { + 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, () => { - + 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); @@ -101,24 +116,29 @@ function logoff(callingMenu, formData, extraArgs, cb) { } function prevMenu(callingMenu, formData, extraArgs, cb) { - // :TODO: this is a pretty big hack -- need the whole key map concep there like other places - if(formData.key && 'return' === formData.key.name) { + 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!'); + 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!'); + callingMenu.nextMenu(err => { + if (err) { + callingMenu.client.log.error( + { error: err.message }, + 'Error attempting to go to next menu!' + ); } return cb(err); }); @@ -130,63 +150,95 @@ function reloadMenu(menu, cb) { } function prevConf(callingMenu, formData, extraArgs, cb) { - const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); - const currIndex = confs.findIndex( e => e.confTag === callingMenu.client.user.properties[UserProps.MessageConfTag]) || confs.length; + const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); + const currIndex = + confs.findIndex( + e => + e.confTag === callingMenu.client.user.properties[UserProps.MessageConfTag] + ) || confs.length; - messageArea.changeMessageConference(callingMenu.client, confs[currIndex - 1].confTag, err => { - if(err) { - 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[UserProps.MessageConfTag]); + const confs = messageArea.getSortedAvailMessageConferences(callingMenu.client); + let currIndex = confs.findIndex( + e => e.confTag === callingMenu.client.user.properties[UserProps.MessageConfTag] + ); - if(currIndex === confs.length - 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[UserProps.MessageConfTag]); - const currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]) || areas.length; + const areas = messageArea.getSortedAvailMessageAreasByConfTag( + callingMenu.client.user.properties[UserProps.MessageConfTag] + ); + const currIndex = + areas.findIndex( + e => + e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag] + ) || areas.length; - messageArea.changeMessageArea(callingMenu.client, areas[currIndex - 1].areaTag, err => { - if(err) { - 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[UserProps.MessageConfTag]); - let currIndex = areas.findIndex( e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag]); + const areas = messageArea.getSortedAvailMessageAreasByConfTag( + callingMenu.client.user.properties[UserProps.MessageConfTag] + ); + let currIndex = areas.findIndex( + e => e.areaTag === callingMenu.client.user.properties[UserProps.MessageAreaTag] + ); - if(currIndex === areas.length - 1) { + 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) { @@ -195,11 +247,14 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { 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'); + if (err) { + callingMenu.client.log.warn( + { err: err.message }, + 'Failed sending forgot password email' + ); } - if(extraArgs.next) { + if (extraArgs.next) { return callingMenu.gotoMenu(extraArgs.next, cb); } @@ -221,10 +276,13 @@ function optimizeDatabases(callingMenu, formData, extraArgs, cb) { // https://www.sqlite.org/pragma.html#pragma_optimize dbs[dbName].run('PRAGMA optimize;', err => { if (err) { - client.log.error({ error : err, dbName }, 'Error attempting to optimize database'); + client.log.error( + { error: err, dbName }, + 'Error attempting to optimize database' + ); } }); }); return callingMenu.prevMenu(cb); -} \ No newline at end of file +} diff --git a/core/system_property.js b/core/system_property.js index ca3cf7cd..239a987a 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -8,26 +8,26 @@ // their own! // module.exports = { - LoginCount : 'login_count', - LoginsToday : 'logins_today', // non-persistent + LoginCount: 'login_count', + LoginsToday: 'logins_today', // non-persistent - FileBaseAreaStats : 'file_base_area_stats', // object - see file_base_area.js::getAreaStats - FileUlTotalCount : 'ul_total_count', - FileUlTotalBytes : 'ul_total_bytes', - FileDlTotalCount : 'dl_total_count', - FileDlTotalBytes : 'dl_total_bytes', + FileBaseAreaStats: 'file_base_area_stats', // object - see file_base_area.js::getAreaStats + FileUlTotalCount: 'ul_total_count', + FileUlTotalBytes: 'ul_total_bytes', + FileDlTotalCount: 'dl_total_count', + FileDlTotalBytes: 'dl_total_bytes', - MessageTotalCount : 'message_post_total_count', // total non-private messages on the system; non-persistent - MessagesToday : 'message_post_today', // non-private messages posted/imported today; non-persistent + MessageTotalCount: 'message_post_total_count', // total non-private messages on the system; non-persistent + MessagesToday: 'message_post_today', // non-private messages posted/imported today; non-persistent // begin +op non-persistent... - SysOpUsername : 'sysop_username', - SysOpRealName : 'sysop_real_name', - SysOpLocation : 'sysop_location', - SysOpAffiliations : 'sysop_affiliation', - SysOpSex : 'sysop_sex', - SysOpEmailAddress : 'sysop_email_address', + SysOpUsername: 'sysop_username', + SysOpRealName: 'sysop_real_name', + SysOpLocation: 'sysop_location', + SysOpAffiliations: 'sysop_affiliation', + SysOpSex: 'sysop_sex', + SysOpEmailAddress: 'sysop_email_address', // end +op non-persistent - NextRandomRumor : 'random_rumor', + NextRandomRumor: 'random_rumor', }; diff --git a/core/system_view_validate.js b/core/system_view_validate.js index 5eb0fc4a..61c52a52 100644 --- a/core/system_view_validate.js +++ b/core/system_view_validate.js @@ -2,24 +2,24 @@ 'use strict'; // ENiGMA½ -const User = require('./user.js'); -const Config = require('./config.js').get; -const Log = require('./logger.js').log; -const { getAddressedToInfo } = require('./mail_util.js'); -const Message = require('./message.js'); +const User = require('./user.js'); +const Config = require('./config.js').get; +const Log = require('./logger.js').log; +const { getAddressedToInfo } = require('./mail_util.js'); +const Message = require('./message.js'); // deps -const fs = require('graceful-fs'); +const fs = require('graceful-fs'); -exports.validateNonEmpty = validateNonEmpty; -exports.validateMessageSubject = validateMessageSubject; -exports.validateUserNameAvail = validateUserNameAvail; -exports.validateUserNameExists = validateUserNameExists; -exports.validateUserNameOrRealNameExists = validateUserNameOrRealNameExists; -exports.validateGeneralMailAddressedTo = validateGeneralMailAddressedTo; -exports.validateEmailAvail = validateEmailAvail; -exports.validateBirthdate = validateBirthdate; -exports.validatePasswordSpec = validatePasswordSpec; +exports.validateNonEmpty = validateNonEmpty; +exports.validateMessageSubject = validateMessageSubject; +exports.validateUserNameAvail = validateUserNameAvail; +exports.validateUserNameExists = validateUserNameExists; +exports.validateUserNameOrRealNameExists = validateUserNameOrRealNameExists; +exports.validateGeneralMailAddressedTo = validateGeneralMailAddressedTo; +exports.validateEmailAvail = validateEmailAvail; +exports.validateBirthdate = validateBirthdate; +exports.validatePasswordSpec = validatePasswordSpec; function validateNonEmpty(data, cb) { return cb(data && data.length > 0 ? null : new Error('Field cannot be empty')); @@ -31,25 +31,26 @@ function validateMessageSubject(data, cb) { function validateUserNameAvail(data, cb) { const config = Config(); - if(!data || data.length < config.users.usernameMin) { + if (!data || data.length < config.users.usernameMin) { cb(new Error('Username too short')); - } else if(data.length > config.users.usernameMax) { + } else if (data.length > config.users.usernameMax) { // generally should be unreached due to view restraints return cb(new Error('Username too long')); } else { - const usernameRegExp = new RegExp(config.users.usernamePattern); - const invalidNames = config.users.newUserNames + config.users.badUserNames; + const usernameRegExp = new RegExp(config.users.usernamePattern); + const invalidNames = config.users.newUserNames + config.users.badUserNames; - if(!usernameRegExp.test(data)) { + if (!usernameRegExp.test(data)) { return cb(new Error('Username contains invalid characters')); - } else if(invalidNames.indexOf(data.toLowerCase()) > -1) { + } else if (invalidNames.indexOf(data.toLowerCase()) > -1) { return cb(new Error('Username is blacklisted')); - } else if(/^[0-9]+$/.test(data)) { + } 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 + if (!err) { + // err is null if we succeeded -- meaning this user exists already return cb(new Error('Username unavailable')); } @@ -62,17 +63,17 @@ function validateUserNameAvail(data, cb) { const invalidUserNameError = () => new Error('Invalid username'); function validateUserNameExists(data, cb) { - if(0 === data.length) { + if (0 === data.length) { return cb(invalidUserNameError()); } - User.getUserIdAndName(data, (err) => { + User.getUserIdAndName(data, err => { return cb(err ? invalidUserNameError() : null); }); } function validateUserNameOrRealNameExists(data, cb) { - if(0 === data.length) { + if (0 === data.length) { return cb(invalidUserNameError()); } @@ -90,7 +91,7 @@ function validateGeneralMailAddressedTo(data, cb) { // :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) { + if (Message.AddressFlavor.FTN === addressedToInfo.flavor) { return cb(null); } @@ -101,7 +102,7 @@ function validateEmailAvail(data, cb) { // // This particular method allows empty data - e.g. no email entered // - if(!data || 0 === data.length) { + if (!data || 0 === data.length) { return cb(null); } @@ -112,22 +113,25 @@ function validateEmailAvail(data, cb) { // See http://stackoverflow.com/questions/7786058/find-the-regex-used-by-html5-forms-for-validation // const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; - if(!emailRegExp.test(data)) { + 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); @@ -135,19 +139,19 @@ function validateBirthdate(data, cb) { function validatePasswordSpec(data, cb) { const config = Config(); - if(!data || data.length < config.users.passwordMin) { + 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'); + 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)) { + if (passwords.includes(data)) { return cb(new Error('Password is too common')); } diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index b7ff09e1..f8c5a8d3 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -2,24 +2,19 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const resetScreen = require('./ansi_term.js').resetScreen; -const setSyncTermFontWithAlias = require('./ansi_term.js').setSyncTermFontWithAlias; +const MenuModule = require('./menu_module.js').MenuModule; +const resetScreen = require('./ansi_term.js').resetScreen; +const setSyncTermFontWithAlias = require('./ansi_term.js').setSyncTermFontWithAlias; // deps -const async = require('async'); -const _ = require('lodash'); -const net = require('net'); -const EventEmitter = require('events'); +const async = require('async'); +const _ = require('lodash'); +const net = require('net'); +const EventEmitter = require('events'); const { TelnetSocket, - TelnetSpec : - { - Commands, - Options, - SubNegotiationCommands, - }, + TelnetSpec: { Commands, Options, SubNegotiationCommands }, } = require('telnet-socket'); /* @@ -37,15 +32,12 @@ const { // :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 = TelnetSocket.commandBuffer( - Commands.DO, - Options.TTYPE -); +const IAC_DO_TERM_TYPE = TelnetSocket.commandBuffer(Commands.DO, Options.TTYPE); class TelnetClientConnection extends EventEmitter { constructor(client) { @@ -57,20 +49,20 @@ class TelnetClientConnection extends EventEmitter { } updateActivity() { - if (0 === (this.dataHits++ % 4)) { + if (0 === this.dataHits++ % 4) { this.client.explicitActivityTimeUpdate(); } } restorePipe() { - if(!this.pipeRestored) { + if (!this.pipeRestored) { this.pipeRestored = true; this.client.restoreDataHandler(); // client may have bailed - if(null !== _.get(this, 'client.term.output', null)) { - if(this.bridgeConnection) { + if (null !== _.get(this, 'client.term.output', null)) { + if (this.bridgeConnection) { this.client.term.output.unpipe(this.bridgeConnection); } this.client.term.output.resume(); @@ -99,7 +91,7 @@ class TelnetClientConnection extends EventEmitter { // 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) { + if (!this.termSent && data.indexOf(IAC_DO_TERM_TYPE) > -1) { this.termSent = true; this.bridgeConnection.write(this.getTermTypeNegotiationBuffer()); } @@ -117,13 +109,13 @@ class TelnetClientConnection extends EventEmitter { } disconnect() { - if(this.bridgeConnection) { + if (this.bridgeConnection) { this.bridgeConnection.end(); } } destroy() { - if(this.bridgeConnection) { + if (this.bridgeConnection) { this.bridgeConnection.destroy(); this.bridgeConnection.removeAllListeners(); this.restorePipe(); @@ -136,16 +128,12 @@ class TelnetClientConnection extends EventEmitter { // Create a TERMINAL-TYPE sub negotiation buffer using the // actual/current terminal type. // - const sendTermType = TelnetSocket.commandBuffer( - Commands.SB, - Options.TTYPE, - [ - SubNegotiationCommands.IS, - ...Buffer.from(this.client.term.termType), // e.g. "ansi" - Commands.IAC, - Commands.SE, - ] - ); + const sendTermType = TelnetSocket.commandBuffer(Commands.SB, Options.TTYPE, [ + SubNegotiationCommands.IS, + ...Buffer.from(this.client.term.termType), // e.g. "ansi" + Commands.IAC, + Commands.SE, + ]); return sendTermType; } } @@ -154,8 +142,12 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); - this.config.port = this.config.port || 23; + this.config = Object.assign( + {}, + _.get(options, 'menuConfig.config'), + options.extraArgs + ); + this.config.port = this.config.port || 23; } initSequence() { @@ -165,18 +157,18 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { async.series( [ function validateConfig(callback) { - if(_.isString(self.config.host) && - _.isNumber(self.config.port)) - { + if (_.isString(self.config.host) && _.isNumber(self.config.port)) { callback(null); } else { - callback(new Error('Configuration is missing required option(s)')); + callback( + new Error('Configuration is missing required option(s)') + ); } }, function createTelnetBridge(callback) { const connectOpts = { - port : self.config.port, - host : self.config.host, + port: self.config.port, + host: self.config.host, }; self.client.term.write(resetScreen()); @@ -187,8 +179,11 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { const telnetConnection = new TelnetClientConnection(self.client); const connectionKeyPressHandler = (ch, key) => { - if('escape' === key.name) { - self.client.removeListener('key press', connectionKeyPressHandler); + if ('escape' === key.name) { + self.client.removeListener( + 'key press', + connectionKeyPressHandler + ); telnetConnection.destroy(); } }; @@ -196,40 +191,62 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { self.client.on('key press', connectionKeyPressHandler); telnetConnection.on('connected', () => { - self.client.removeListener('key press', connectionKeyPressHandler); - self.client.log.info(connectOpts, 'Telnet bridge connection established'); + self.client.removeListener( + 'key press', + connectionKeyPressHandler + ); + self.client.log.info( + connectOpts, + 'Telnet bridge connection established' + ); // put the font back how it was prior, if fonts are enabled - if(self.client.term.syncTermFontsEnabled && self.config.font) { - self.client.term.rawWrite(setSyncTermFontWithAlias(self.config.font)); + if (self.client.term.syncTermFontsEnabled && self.config.font) { + self.client.term.rawWrite( + setSyncTermFontWithAlias(self.config.font) + ); } self.client.once('end', () => { - self.client.log.info('Connection ended. Terminating connection'); + self.client.log.info( + 'Connection ended. Terminating connection' + ); clientTerminated = true; telnetConnection.disconnect(); }); }); telnetConnection.on('end', err => { - self.client.removeListener('key press', connectionKeyPressHandler); + 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'); + if (err) { + self.client.log.warn( + { error: err.message }, + 'Telnet connection error' + ); } - if(!clientTerminated) { + if (!clientTerminated) { self.prevMenu(); } } diff --git a/core/text_view.js b/core/text_view.js index cbecb54f..dcd14c7c 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -2,75 +2,84 @@ 'use strict'; // ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const ansi = require('./ansi_term.js'); -const padStr = require('./string_util.js').pad; -const stylizeString = require('./string_util.js').stylizeString; -const renderSubstr = require('./string_util.js').renderSubstr; -const renderStringLength = require('./string_util.js').renderStringLength; -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; -const stripAllLineFeeds = require('./string_util.js').stripAllLineFeeds; +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const ansi = require('./ansi_term.js'); +const padStr = require('./string_util.js').pad; +const stylizeString = require('./string_util.js').stylizeString; +const renderSubstr = require('./string_util.js').renderSubstr; +const renderStringLength = require('./string_util.js').renderStringLength; +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const stripAllLineFeeds = require('./string_util.js').stripAllLineFeeds; // deps -const util = require('util'); -const _ = require('lodash'); +const util = require('util'); +const _ = require('lodash'); -exports.TextView = TextView; +exports.TextView = TextView; function TextView(options) { - if(options.dimens) { - options.dimens.height = 1; // force height of 1 for TextView's & sub classes + if (options.dimens) { + options.dimens.height = 1; // force height of 1 for TextView's & sub classes } View.call(this, options); - if(options.maxLength) { + 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)) { + if (_.isString(options.textOverflow)) { this.textOverflow = options.textOverflow; } - if(_.isString(options.textMaskChar) && 1 === options.textMaskChar.length) { + if (_.isString(options.textMaskChar) && 1 === options.textMaskChar.length) { this.textMaskChar = options.textMaskChar; } - 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: + 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); - if(renderLength >= this.dimens.width) { - if(this.hasFocus) { - if(this.horizScroll) { - textToDraw = renderSubstr(textToDraw, renderLength - this.dimens.width, renderLength); + if (renderLength >= this.dimens.width) { + if (this.hasFocus) { + if (this.horizScroll) { + textToDraw = renderSubstr( + textToDraw, + renderLength - this.dimens.width, + renderLength + ); } } else { - if(this.textOverflow && + if ( + this.textOverflow && this.dimens.width > this.textOverflow.length && - renderLength - this.textOverflow.length >= this.textOverflow.length) - { - textToDraw = renderSubstr(textToDraw, 0, this.dimens.width - this.textOverflow.length) + this.textOverflow; + 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); } @@ -87,31 +96,30 @@ function TextView(options) { this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), this.getStyleSGR(1) || this.getSGR(), - true // use render len + true // use render len ), - false // no converting CRLF needed + false // no converting CRLF needed ); }; - - this.getEndOfTextColumn = function() { + 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() { +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)) { + if (!this.hasDrawnOnce) { + if (_.isUndefined(this.text)) { return; } } @@ -119,12 +127,12 @@ TextView.prototype.redraw = function() { TextView.super_.prototype.redraw.call(this); - if(_.isString(this.text)) { + if (_.isString(this.text)) { this.drawText(this.text); } }; -TextView.prototype.setFocus = function(focused) { +TextView.prototype.setFocus = function (focused) { TextView.super_.prototype.setFocus.call(this, focused); this.redraw(); @@ -133,46 +141,55 @@ TextView.prototype.setFocus = function(focused) { this.client.term.write(this.getFocusSGR()); }; -TextView.prototype.getData = function() { +TextView.prototype.getData = function () { return this.text; }; -TextView.prototype.setText = function(text, redraw) { +TextView.prototype.setText = function (text, redraw) { redraw = _.isBoolean(redraw) ? redraw : true; - if(!_.isString(text)) { // allow |text| to be numbers/etc. + if (!_.isString(text)) { + // allow |text| to be numbers/etc. text = text.toString(); } - this.text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. - if(this.maxLength > 0) { + this.text = pipeToAnsi(stripAllLineFeeds(text), this.client); // expand MCI/etc. + if (this.maxLength > 0) { this.text = renderSubstr(this.text, 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); + this.text = stylizeString( + this.text, + this.hasFocus ? this.focusTextStyle : this.textStyle + ); - if(redraw) { + if (redraw) { this.redraw(); } }; -TextView.prototype.clearText = function() { +TextView.prototype.clearText = function () { 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) { +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; } - TextView.super_.prototype.setPropertyValue.call(this, propName, value); }; diff --git a/core/theme.js b/core/theme.js index a511ba33..81fba949 100644 --- a/core/theme.js +++ b/core/theme.js @@ -1,36 +1,36 @@ /* jslint node: true */ 'use strict'; -const Config = require('./config.js').get; -const art = require('./art.js'); -const ansi = require('./ansi_term.js'); -const Log = require('./logger.js').log; -const asset = require('./asset.js'); -const ViewController = require('./view_controller.js').ViewController; -const Errors = require('./enig_error.js').Errors; -const Events = require('./events.js'); -const AnsiPrep = require('./ansi_prep.js'); -const UserProps = require('./user_property.js'); +const Config = require('./config.js').get; +const art = require('./art.js'); +const ansi = require('./ansi_term.js'); +const Log = require('./logger.js').log; +const asset = require('./asset.js'); +const ViewController = require('./view_controller.js').ViewController; +const Errors = require('./enig_error.js').Errors; +const Events = require('./events.js'); +const AnsiPrep = require('./ansi_prep.js'); +const UserProps = require('./user_property.js'); -const ConfigLoader = require('./config_loader'); +const ConfigLoader = require('./config_loader'); const { getConfigPath } = require('./config_util'); // deps -const fs = require('graceful-fs'); -const paths = require('path'); -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); +const fs = require('graceful-fs'); +const paths = require('path'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); -exports.getThemeArt = getThemeArt; -exports.getAvailableThemes = getAvailableThemes; -exports.getRandomTheme = getRandomTheme; -exports.setClientTheme = setClientTheme; -exports.displayPreparedArt = displayPreparedArt; -exports.displayThemeArt = displayThemeArt; -exports.displayThemedPause = displayThemedPause; -exports.displayThemedPrompt = displayThemedPrompt; -exports.displayThemedAsset = displayThemedAsset; +exports.getThemeArt = getThemeArt; +exports.getAvailableThemes = getAvailableThemes; +exports.getRandomTheme = getRandomTheme; +exports.setClientTheme = setClientTheme; +exports.displayPreparedArt = displayPreparedArt; +exports.displayThemeArt = displayThemeArt; +exports.displayThemedPause = displayThemedPause; +exports.displayThemedPrompt = displayThemedPrompt; +exports.displayThemedAsset = displayThemedAsset; // global instance of ThemeManager; see ThemeManager.create() let themeManagerInstance; @@ -44,13 +44,15 @@ exports.ThemeManager = class ThemeManager { themeManagerInstance = new ThemeManager(); themeManagerInstance.init(err => { if (!err) { - themeManagerInstance.getAvailableThemes().forEach( (themeConfig, themeId) => { - const { name, author, group } = themeConfig.get().info; - Log.info( - { themeId, themeName : name, author, group }, - 'Theme loaded' - ); - }); + themeManagerInstance + .getAvailableThemes() + .forEach((themeConfig, themeId) => { + const { name, author, group } = themeConfig.get().info; + Log.info( + { themeId, themeName: name, author, group }, + 'Theme loaded' + ); + }); } return cb(err); @@ -63,7 +65,7 @@ exports.ThemeManager = class ThemeManager { init(cb) { this.menuConfig = new ConfigLoader({ - onReload : err => { + onReload: err => { if (!err) { // menu.hjson/includes have changed; this could affect // all themes, so they must be reloaded @@ -91,46 +93,51 @@ exports.ThemeManager = class ThemeManager { return cb(err); } - async.filter(files, (filename, nextFilename) => { - const fullPath = paths.join(themeDir, filename); - fs.stat(fullPath, (err, stats) => { + async.filter( + files, + (filename, nextFilename) => { + const fullPath = paths.join(themeDir, filename); + fs.stat(fullPath, (err, stats) => { + if (err) { + return nextFilename(err); + } + + return nextFilename(null, stats.isDirectory()); + }); + }, + (err, themeIds) => { if (err) { - return nextFilename(err); + return cb(err); } - return nextFilename(null, stats.isDirectory()); - }); - }, - (err, themeIds) => { - if (err) { - return cb(err); + async.each( + themeIds, + (themeId, nextThemeId) => { + return this._loadTheme(themeId, nextThemeId); + }, + err => { + return cb(err); + } + ); } - - async.each(themeIds, (themeId, nextThemeId) => { - return this._loadTheme(themeId, nextThemeId); - }, - err => { - return cb(err); - }); - }); + ); }); } _loadTheme(themeId, cb) { const themeConfig = new ConfigLoader({ - onReload : err => { + onReload: err => { if (!err) { // this particular theme has changed this._themeLoaded(themeId, themeConfig, err => { if (!err) { - Events.emit( - Events.getSystemEvents().ThemeChanged, - { themeId } - ); + Events.emit(Events.getSystemEvents().ThemeChanged, { + themeId, + }); } }); } - } + }, }); const themeConfigPath = paths.join(Config().paths.themes, themeId, 'theme.hjson'); @@ -150,14 +157,18 @@ exports.ThemeManager = class ThemeManager { // do some basic validation // :TODO: schema validation here - if(!_.isObject(theme.info) || + if ( + !_.isObject(theme.info) || !_.isString(theme.info.name) || - !_.isString(theme.info.author)) - { - return Log.warn({ themeId }, 'Theme contains invalid or missing "info" section'); + !_.isString(theme.info.author) + ) { + return Log.warn( + { themeId }, + 'Theme contains invalid or missing "info" section' + ); } - if(false === _.get(theme, 'info.enabled')) { + if (false === _.get(theme, 'info.enabled')) { Log.info({ themeId }, 'Theme is disabled'); return this.availableThemes.delete(themeId); } @@ -168,10 +179,7 @@ exports.ThemeManager = class ThemeManager { // Theme is valid and enabled; update it in available themes this.availableThemes.set(themeId, themeConfig); - Events.emit( - Events.getSystemEvents().ThemeChanged, - { themeId } - ); + Events.emit(Events.getSystemEvents().ThemeChanged, { themeId }); } _finalizeTheme(themeId, themeConfig) { @@ -185,22 +193,20 @@ exports.ThemeManager = class ThemeManager { const theme = themeConfig.get(); // some data brought directly over - mergedTheme.info = Object.assign({}, theme.info, { themeId }); - mergedTheme.achievements = _.get(theme, 'customization.achievements'); + mergedTheme.info = Object.assign({}, theme.info, { themeId }); + mergedTheme.achievements = _.get(theme, 'customization.achievements'); // Create some helpers for this theme this._setThemeHelpers(mergedTheme); // merge customizer to disallow immutable MCI properties - const ImmutableMCIProperties = [ - 'maxLength', 'argName', 'submit', 'validate' - ]; + const ImmutableMCIProperties = ['maxLength', 'argName', 'submit', 'validate']; const mciCustomizer = (objVal, srcVal, key) => { return ImmutableMCIProperties.indexOf(key) > -1 ? objVal : srcVal; }; - const getFormKeys = (obj) => { + const getFormKeys = obj => { // remove all non-numbers return _.remove(Object.keys(obj), k => !isNaN(k)); }; @@ -217,9 +223,9 @@ exports.ThemeManager = class ThemeManager { }; const applyThemeMciBlock = (dst, src, formKey) => { - if(_.isObject(src.mci)) { + if (_.isObject(src.mci)) { mergeMciProperties(dst, src.mci); - } else if (_.has(src, [ formKey, 'mci' ])) { + } else if (_.has(src, [formKey, 'mci'])) { mergeMciProperties(dst, src[formKey].mci); } }; @@ -252,49 +258,59 @@ exports.ThemeManager = class ThemeManager { applyThemeMciBlock(form.mci, menuTheme, formKey); } else { // remove anything not uppercase - const menuMciCodeKeys = _.remove(_.keys(form), k => k === k.toUpperCase()); + const menuMciCodeKeys = _.remove( + _.keys(form), + k => k === k.toUpperCase() + ); menuMciCodeKeys.forEach(mciKey => { - const src = _.has(menuTheme, [ mciKey, 'mci' ]) ? - menuTheme[mciKey] : - menuTheme; + const src = _.has(menuTheme, [mciKey, 'mci']) + ? menuTheme[mciKey] + : menuTheme; applyThemeMciBlock(form[mciKey].mci, src, formKey); }); } }; - [ 'menus', 'prompts'].forEach(sectionName => { + ['menus', 'prompts'].forEach(sectionName => { if (!_.isObject(mergedTheme[sectionName])) { - return Log.error({sectionName}, 'Merged theme is missing section'); + return Log.error({ sectionName }, 'Merged theme is missing section'); } Object.keys(mergedTheme[sectionName]).forEach(entryName => { let createdFormSection = false; const mergedThemeMenu = mergedTheme[sectionName][entryName]; - const menuTheme = _.get(theme, [ 'customization', sectionName, entryName ]); + const menuTheme = _.get(theme, ['customization', sectionName, entryName]); if (menuTheme) { if (menuTheme.config) { // :TODO: should this be _.merge() ? - mergedThemeMenu.config = _.assign(mergedThemeMenu.config || {}, menuTheme.config); + mergedThemeMenu.config = _.assign( + mergedThemeMenu.config || {}, + menuTheme.config + ); } - if('menus' === sectionName) { - if(_.isObject(mergedThemeMenu.form)) { + if ('menus' === sectionName) { + if (_.isObject(mergedThemeMenu.form)) { getFormKeys(mergedThemeMenu.form).forEach(formKey => { - applyToForm(mergedThemeMenu.form[formKey], menuTheme, formKey); + applyToForm( + mergedThemeMenu.form[formKey], + menuTheme, + formKey + ); }); - } else if(_.isObject(menuTheme.mci)) { + } 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 : { } } }; + mergedThemeMenu.form = { 0: { mci: {} } }; mergeMciProperties(mergedThemeMenu.form[0], menuTheme); createdFormSection = true; } - } else if('prompts' === sectionName) { + } else if ('prompts' === sectionName) { // no 'form' or form keys for prompts -- direct to mci applyToForm(mergedThemeMenu, menuTheme); } @@ -308,11 +324,14 @@ exports.ThemeManager = class ThemeManager { // * There is/was no explicit 'form' section // * There is no 'prompt' specified // - if('menus' === sectionName && + if ( + 'menus' === sectionName && !_.isString(mergedThemeMenu.prompt) && - (createdFormSection || !_.isObject(mergedThemeMenu.form))) - { - mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { autoNext : true } ); + (createdFormSection || !_.isObject(mergedThemeMenu.form)) + ) { + mergedThemeMenu.runtime = _.merge(mergedThemeMenu.runtime || {}, { + autoNext: true, + }); } }); }); @@ -322,43 +341,48 @@ exports.ThemeManager = class ThemeManager { _setThemeHelpers(theme) { theme.helpers = { - getPasswordChar : function() { + getPasswordChar: function () { let pwChar = _.get( theme, 'customization.defaults.passwordChar', Config().theme.passwordChar ); - if(_.isString(pwChar)) { + if (_.isString(pwChar)) { pwChar = pwChar.substr(0, 1); - } else if(_.isNumber(pwChar)) { + } else if (_.isNumber(pwChar)) { pwChar = String.fromCharCode(pwChar); } return pwChar; }, - getDateFormat : function(style = 'short') { + getDateFormat: function (style = 'short') { const format = Config().theme.dateFormat[style] || 'MM/DD/YYYY'; return _.get(theme, `customization.defaults.dateFormat.${style}`, format); }, - getTimeFormat : function(style = 'short') { + getTimeFormat: function (style = 'short') { const format = Config().theme.timeFormat[style] || 'h:mm a'; return _.get(theme, `customization.defaults.timeFormat.${style}`, format); }, - getDateTimeFormat : function(style = 'short') { - const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; - return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); - } + getDateTimeFormat: function (style = 'short') { + const format = + Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; + return _.get( + theme, + `customization.defaults.dateTimeFormat.${style}`, + format + ); + }, }; } _reloadAllThemes() { - async.each([ ...this.availableThemes.keys() ], (themeId, nextThemeId) => { + async.each([...this.availableThemes.keys()], (themeId, nextThemeId) => { this._loadTheme(themeId, err => { if (!err) { Log.info({ themeId }, 'Theme reloaded'); } - return nextThemeId(null); // always proceed + return nextThemeId(null); // always proceed }); }); } @@ -370,8 +394,8 @@ function getAvailableThemes() { function getRandomTheme() { const avail = getAvailableThemes(); - if(avail.size > 0) { - const themeIds = [ ...avail.keys() ]; + if (avail.size > 0) { + const themeIds = [...avail.keys()]; return themeIds[Math.floor(Math.random() * themeIds.length)]; } } @@ -382,19 +406,23 @@ function setClientTheme(client, themeId) { let msg; let setThemeId; const config = Config(); - if(availThemes.has(themeId)) { + if (availThemes.has(themeId)) { msg = 'Set client theme'; setThemeId = themeId; - } else if(availThemes.has(config.theme.default)) { + } else if (availThemes.has(config.theme.default)) { msg = 'Failed setting theme by supplied ID; Using default'; setThemeId = config.theme.default; } else { - msg = 'Failed setting theme by system default ID; Using the first one we can find'; + msg = + 'Failed setting theme by system default ID; Using the first one we can find'; setThemeId = availThemes.keys().next().value; } client.currentTheme = availThemes.get(setThemeId); - client.log.debug( { setThemeId, requestedThemeId : themeId, info : client.currentTheme.info }, msg); + client.log.debug( + { setThemeId, requestedThemeId: themeId, info: client.currentTheme.info }, + msg + ); } function getThemeArt(options, cb) { @@ -410,7 +438,10 @@ function getThemeArt(options, cb) { // random // const config = Config(); - if(!options.themeId && _.has(options, [ 'client', 'user', 'properties', UserProps.ThemeId ])) { + if ( + !options.themeId && + _.has(options, ['client', 'user', 'properties', UserProps.ThemeId]) + ) { options.themeId = options.client.user.properties[UserProps.ThemeId]; } @@ -418,9 +449,9 @@ function getThemeArt(options, cb) { // :TODO: replace asAnsi stuff with something like retrieveAs = 'ansi' | 'pipe' | ... // :TODO: Some of these options should only be set if not provided! - options.asAnsi = true; // always convert to ANSI - options.readSauce = _.get(options, 'readSauce', true); // read SAUCE, if avail - options.random = _.get(options, 'random', true); // FILENAME.EXT support + options.asAnsi = true; // always convert to ANSI + options.readSauce = _.get(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: @@ -435,12 +466,16 @@ function getThemeArt(options, cb) { // // We allow relative (to enigma-bbs) or full paths // - if('/' === options.name.charAt(0)) { + if ('/' === options.name.charAt(0)) { // just take the path as-is options.basePath = paths.dirname(options.name); - } else if(options.name.indexOf('/') > -1) { + } else if (options.name.indexOf('/') > -1) { // make relative to base BBS dir - options.basePath = paths.join(__dirname, '../', paths.dirname(options.name)); + options.basePath = paths.join( + __dirname, + '../', + paths.dirname(options.name) + ); } else { return callback(null, null); } @@ -450,7 +485,7 @@ function getThemeArt(options, cb) { }); }, function fromSuppliedTheme(artInfo, callback) { - if(artInfo) { + if (artInfo) { return callback(null, artInfo); } @@ -460,7 +495,7 @@ function getThemeArt(options, cb) { }); }, function fromDefaultTheme(artInfo, callback) { - if(artInfo || config.theme.default === options.themeId) { + if (artInfo || config.theme.default === options.themeId) { return callback(null, artInfo); } @@ -470,7 +505,7 @@ function getThemeArt(options, cb) { }); }, function fromGeneralArtDir(artInfo, callback) { - if(artInfo) { + if (artInfo) { return callback(null, artInfo); } @@ -478,12 +513,12 @@ function getThemeArt(options, cb) { art.getArt(options.name, options, (err, artInfo) => { return callback(err, artInfo); }); - } + }, ], function complete(err, artInfo) { - if(err) { + if (err) { const logger = _.get(options, 'client.log') || Log; - logger.debug( { reason : err.message }, 'Cannot find theme art'); + logger.debug({ reason: err.message }, 'Cannot find theme art'); } return cb(err, artInfo); } @@ -492,13 +527,13 @@ function getThemeArt(options, cb) { function displayPreparedArt(options, artInfo, cb) { const displayOpts = { - sauce : artInfo.sauce, - font : options.font, - trailingLF : options.trailingLF, - startRow : options.startRow, + sauce: artInfo.sauce, + font: options.font, + trailingLF: options.trailingLF, + startRow: options.startRow, }; art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { - return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } ); + return cb(err, { mciMap: mciMap, artInfo: artInfo, extraInfo: extraInfo }); }); } @@ -513,24 +548,20 @@ function displayThemeArt(options, cb) { return getThemeArt(options, callback); }, function prepWork(artInfo, callback) { - if(_.isObject(options.ansiPrepOptions)) { - AnsiPrep( - artInfo.data, - options.ansiPrepOptions, - (err, prepped) => { - if(!err && prepped) { - artInfo.data = prepped; - return callback(null, artInfo); - } + if (_.isObject(options.ansiPrepOptions)) { + AnsiPrep(artInfo.data, options.ansiPrepOptions, (err, prepped) => { + if (!err && prepped) { + artInfo.data = prepped; + return callback(null, artInfo); } - ); + }); } else { return callback(null, artInfo); } }, function disp(artInfo, callback) { return displayPreparedArt(options, artInfo, callback); - } + }, ], (err, artData) => { return cb(err, artData); @@ -539,20 +570,21 @@ function displayThemeArt(options, cb) { } function displayThemedPrompt(name, client, options, cb) { - const usingTempViewController = _.isUndefined(options.viewController); async.waterfall( [ function display(callback) { const promptConfig = client.currentTheme.prompts[name]; - if(!promptConfig) { - return callback(Errors.DoesNotExist(`Missing "${name}" prompt configuration!`)); + if (!promptConfig) { + return callback( + Errors.DoesNotExist(`Missing "${name}" prompt configuration!`) + ); } - if(options.clearScreen) { + if (options.clearScreen) { client.term.rawWrite(ansi.resetScreen()); - options.position = {row: 1, column: 1}; + options.position = { row: 1, column: 1 }; } // @@ -560,7 +592,7 @@ function displayThemedPrompt(name, client, options, cb) { // doing so messes things up -- most terminals that support font // changing can only display a single font at at time. // - const dispOptions = Object.assign( {}, options, promptConfig.config ); + const dispOptions = Object.assign({}, options, promptConfig.config); // :TODO: We can use term detection to do nifty things like avoid this kind of kludge: // if(!options.clearScreen) { // dispOptions.font = 'not_really_a_font!'; // kludge :) @@ -571,7 +603,7 @@ function displayThemedPrompt(name, client, options, cb) { client, dispOptions, (err, artInfo) => { - if(err) { + if (err) { return callback(err); } @@ -580,14 +612,17 @@ function displayThemedPrompt(name, client, options, cb) { ); }, function discoverCursorPosition(promptConfig, artInfo, callback) { - if(!options.clearPrompt) { + if (!options.clearPrompt) { // no need to query cursor - we're not gonna use it return callback(null, promptConfig, artInfo); } - if(_.isNumber(options?.position?.row)) { + if (_.isNumber(options?.position?.row)) { artInfo.startRow = options.position.row; - if(client.term.termHeight > 0 && artInfo.startRow + artInfo.height > client.term.termHeight) { + if ( + client.term.termHeight > 0 && + artInfo.startRow + artInfo.height > client.term.termHeight + ) { // in this case, we will have scrolled artInfo.startRow = client.term.termHeight - artInfo.height; } @@ -596,13 +631,15 @@ function displayThemedPrompt(name, client, options, cb) { return callback(null, promptConfig, artInfo); }, function createMCIViews(promptConfig, artInfo, callback) { - const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController; + const assocViewController = usingTempViewController + ? new ViewController({ client: client }) + : options.viewController; const loadOpts = { - promptName : name, - mciMap : artInfo.mciMap, - config : promptConfig, - submitNotify : options.submitNotify, + promptName: name, + mciMap: artInfo.mciMap, + config: promptConfig, + submitNotify: options.submitNotify, }; assocViewController.loadFromPromptConfig(loadOpts, () => { @@ -610,19 +647,19 @@ function displayThemedPrompt(name, client, options, cb) { }); }, function pauseForUserInput(artInfo, assocViewController, callback) { - if(!options.pause) { + if (!options.pause) { return callback(null, artInfo, assocViewController); } - client.waitForKeyPress( () => { + client.waitForKeyPress(() => { return callback(null, artInfo, assocViewController); }); }, function clearPauseArt(artInfo, assocViewController, callback) { // Only clear with height if clearPrompt is true and if we were able // to determine the row - if(options.clearPrompt && artInfo.startRow) { - if(artInfo.startRow && artInfo.height) { + if (options.clearPrompt && artInfo.startRow) { + if (artInfo.startRow && artInfo.height) { client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); // Note: Does not work properly in NetRunner < 2.0b17: @@ -633,14 +670,17 @@ function displayThemedPrompt(name, client, options, cb) { } return callback(null, assocViewController, artInfo); - } + }, ], (err, assocViewController, artInfo) => { - if(err) { - client.log.warn( { error : err.message }, `Failed displaying "${name}" prompt` ); + if (err) { + client.log.warn( + { error: err.message }, + `Failed displaying "${name}" prompt` + ); } - if(assocViewController && usingTempViewController) { + if (assocViewController && usingTempViewController) { assocViewController.detachClientEvents(); } @@ -653,17 +693,16 @@ function displayThemedPrompt(name, client, options, cb) { // Pause prompts are a special prompt by the name 'pause'. // function displayThemedPause(client, options, cb) { - - if(!cb && _.isFunction(options)) { + if (!cb && _.isFunction(options)) { cb = options; options = {}; } - if(!_.isBoolean(options.clearPrompt)) { + if (!_.isBoolean(options.clearPrompt)) { options.clearPrompt = true; } - const promptOptions = Object.assign( {}, options, { pause : true } ); + const promptOptions = Object.assign({}, options, { pause: true }); return displayThemedPrompt('pause', client, promptOptions, cb); } @@ -671,40 +710,45 @@ function displayThemedAsset(assetSpec, client, options, cb) { assert(_.isObject(client)); // options are... optional - if(3 === arguments.length) { + if (3 === arguments.length) { cb = options; options = {}; } - if(Array.isArray(assetSpec)) { + if (Array.isArray(assetSpec)) { const acsCondMember = options.acsCondMember || 'art'; assetSpec = client.acs.getConditionalValue(assetSpec, acsCondMember); } const artAsset = asset.getArtAsset(assetSpec); - if(!artAsset) { + if (!artAsset) { return cb(new Error('Asset not found: ' + assetSpec)); } - const dispOpts = Object.assign( {}, options, { client, name : artAsset.asset } ); - switch(artAsset.type) { - case 'art' : + const dispOpts = Object.assign({}, options, { client, name: artAsset.asset }); + switch (artAsset.type) { + case 'art': displayThemeArt(dispOpts, function displayed(err, artData) { - return cb(err, err ? null : { mciMap : artData.mciMap, height : artData.extraInfo.height } ); + return cb( + err, + err + ? null + : { mciMap: artData.mciMap, height: artData.extraInfo.height } + ); }); break; - case 'method' : + case 'method': // :TODO: fetch & render via method break; - case 'inline ' : + 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 : + 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 fd3c7572..b91bd563 100644 --- a/core/tic_file_info.js +++ b/core/tic_file_info.js @@ -2,17 +2,17 @@ 'use strict'; // ENiGMA½ -const Address = require('./ftn_address.js'); -const Errors = require('./enig_error.js').Errors; -const EnigAssert = require('./enigma_assert.js'); +const Address = require('./ftn_address.js'); +const Errors = require('./enig_error.js').Errors; +const EnigAssert = require('./enigma_assert.js'); // deps -const fs = require('graceful-fs'); -const CRC32 = require('./crc.js').CRC32; -const _ = require('lodash'); -const async = require('async'); -const paths = require('path'); -const crypto = require('crypto'); +const fs = require('graceful-fs'); +const CRC32 = require('./crc.js').CRC32; +const _ = require('lodash'); +const async = require('async'); +const paths = require('path'); +const crypto = require('crypto'); // // Class to read and hold information from a TIC file @@ -28,7 +28,11 @@ module.exports = class TicFileInfo { static get requiredFields() { return [ - 'Area', 'Origin', 'From', 'File', 'Crc', + 'Area', + 'Origin', + 'From', + 'File', + 'Crc', // :TODO: validate this: //'Path', 'Seenby' // these two are questionable; some systems don't send them? ]; @@ -40,13 +44,13 @@ module.exports = class TicFileInfo { getAsString(key, joinWith) { const value = this.get(key); - if(value) { + 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); + if (Array.isArray(value)) { + return value.map(v => v.toString()).join(joinWith); } return value.toString(); @@ -58,12 +62,16 @@ module.exports = class TicFileInfo { } get longFileName() { - return this.getAsString('Lfile') || this.getAsString('Fullname') || this.getAsString('File'); + return ( + this.getAsString('Lfile') || + this.getAsString('Fullname') || + this.getAsString('File') + ); } hasRequiredFields() { const req = TicFileInfo.requiredFields; - return req.every( f => this.get(f) ); + return req.every(f => this.get(f)); } validate(config, cb) { @@ -77,63 +85,77 @@ module.exports = class TicFileInfo { async.waterfall( [ function initial(callback) { - if(!self.hasRequiredFields()) { - return callback(Errors.Invalid('One or more required fields missing from TIC')); + if (!self.hasRequiredFields()) { + return callback( + Errors.Invalid('One or more required fields missing from TIC') + ); } const area = self.getAsString('Area').toUpperCase(); const localInfo = { - areaTag : config.localAreaTags.find( areaTag => areaTag.toUpperCase() === area ), + areaTag: config.localAreaTags.find( + areaTag => areaTag.toUpperCase() === area + ), }; - if(!localInfo.areaTag) { - 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')}`)); + 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) ); + localInfo.node = Object.keys(config.nodes).find(nodeAddrWildcard => + from.isPatternMatch(nodeAddrWildcard) + ); - if(!localInfo.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 + 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) { + 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; + const crcTic = self.get('Crc'); + const stream = fs.createReadStream(self.filePath); + const crc = new CRC32(); + let sizeActual = 0; - let sha256Tic = self.getAsString('Sha256'); + let sha256Tic = self.getAsString('Sha256'); let sha256; - if(sha256Tic) { - sha256Tic = sha256Tic.toLowerCase(); - sha256 = crypto.createHash('sha256'); + if (sha256Tic) { + sha256Tic = sha256Tic.toLowerCase(); + sha256 = crypto.createHash('sha256'); } stream.on('data', data => { sizeActual += data.length; // sha256 if possible, else crc32 - if(sha256) { + if (sha256) { sha256.update(data); } else { crc.update(data); @@ -142,28 +164,40 @@ module.exports = class TicFileInfo { stream.on('end', () => { // again, use sha256 if possible - if(sha256) { + 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}`)); + 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}`)); + 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)) { + 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); @@ -172,7 +206,7 @@ module.exports = class TicFileInfo { stream.on('error', err => { return callback(err); }); - } + }, ], (err, localInfo) => { return cb(err, localInfo); @@ -199,7 +233,7 @@ module.exports = class TicFileInfo { // const to = this.get('To'); - if(!to) { + if (!to) { return allowNonExplicit; } @@ -208,12 +242,12 @@ module.exports = class TicFileInfo { static createFromFile(path, cb) { fs.readFile(path, 'utf8', (err, ticData) => { - if(err) { + 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) @@ -226,51 +260,51 @@ module.exports = class TicFileInfo { let entry; lines.forEach(line => { - keyEnd = line.search(/\s/); + keyEnd = line.search(/\s/); - if(keyEnd < 0) { + if (keyEnd < 0) { keyEnd = line.length; } key = line.substr(0, keyEnd).toLowerCase(); - if(0 === key.length) { + if (0 === key.length) { return; } value = line.substr(keyEnd + 1); // don't trim Ldesc; may mess with FILE_ID.DIZ type descriptions - if('ldesc' !== key) { + 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' : + switch (key) { + case 'origin': + case 'from': + case 'seenby': + case 'to': value = Address.fromString(value); break; - case 'crc' : + case 'crc': value = parseInt(value, 16); break; - case 'size' : + case 'size': value = parseInt(value, 10); break; - default : + default: break; } entry = ticFileInfo.entries.get(key); - if(entry) { - if(!Array.isArray(entry)) { - entry = [ entry ]; + if (entry) { + if (!Array.isArray(entry)) { + entry = [entry]; ticFileInfo.entries.set(key, entry); } entry.push(value); diff --git a/core/toggle_menu_view.js b/core/toggle_menu_view.js index c06297b2..280056c6 100644 --- a/core/toggle_menu_view.js +++ b/core/toggle_menu_view.js @@ -1,15 +1,15 @@ /* jslint node: true */ 'use strict'; -const MenuView = require('./menu_view.js').MenuView; -const strUtil = require('./string_util.js'); +const MenuView = require('./menu_view.js').MenuView; +const strUtil = require('./string_util.js'); -const util = require('util'); -const assert = require('assert'); +const util = require('util'); +const assert = require('assert'); -exports.ToggleMenuView = ToggleMenuView; +exports.ToggleMenuView = ToggleMenuView; -function ToggleMenuView (options) { +function ToggleMenuView(options) { options.cursor = options.cursor || 'hide'; MenuView.call(this, options); @@ -24,7 +24,7 @@ function ToggleMenuView (options) { }; */ - this.updateSelection = function() { + this.updateSelection = function () { assert(this.focusedItemIndex >= 0 && this.focusedItemIndex <= self.items.length); self.redraw(); }; @@ -32,10 +32,10 @@ function ToggleMenuView (options) { util.inherits(ToggleMenuView, MenuView); -ToggleMenuView.prototype.redraw = function() { +ToggleMenuView.prototype.redraw = function () { ToggleMenuView.super_.prototype.redraw.call(this); - if(0 === this.items.length) { + if (0 === this.items.length) { return; } @@ -45,12 +45,16 @@ ToggleMenuView.prototype.redraw = function() { assert(this.items.length === 2, 'ToggleMenuView must contain exactly (2) items'); - for(var i = 0; i < 2; i++) { + 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); + item.text, + i === this.focusedItemIndex && this.hasFocus + ? this.focusTextStyle + : this.textStyle + ); - if(1 === i) { + if (1 === i) { //console.log(this.styleColor1) //var sepColor = this.getANSIColor(this.styleColor1 || this.getColor()); //console.log(sepColor.substr(1)) @@ -60,25 +64,27 @@ ToggleMenuView.prototype.redraw = function() { //this.client.term.write(sepColor + ' / '); } - this.client.term.write(i === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); + 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.prototype.setFocusItemIndex = function (index) { + ToggleMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex this.updateSelection(); }; -ToggleMenuView.prototype.setFocus = function(focused) { +ToggleMenuView.prototype.setFocus = function (focused) { ToggleMenuView.super_.prototype.setFocus.call(this, focused); this.redraw(); }; -ToggleMenuView.prototype.focusNext = function() { - if(this.items.length - 1 === this.focusedItemIndex) { +ToggleMenuView.prototype.focusNext = function () { + if (this.items.length - 1 === this.focusedItemIndex) { this.focusedItemIndex = 0; } else { this.focusedItemIndex++; @@ -89,9 +95,8 @@ ToggleMenuView.prototype.focusNext = function() { ToggleMenuView.super_.prototype.focusNext.call(this); }; -ToggleMenuView.prototype.focusPrevious = function() { - - if(0 === this.focusedItemIndex) { +ToggleMenuView.prototype.focusPrevious = function () { + if (0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; } else { this.focusedItemIndex--; @@ -102,12 +107,14 @@ ToggleMenuView.prototype.focusPrevious = function() { 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)) { +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.name)) { + } else if ( + this.isKeyMapped('left', key.name) || + this.isKeyMapped('up', key.name) + ) { this.focusPrevious(); } } @@ -115,14 +122,14 @@ ToggleMenuView.prototype.onKeyPress = function(ch, key) { ToggleMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; -ToggleMenuView.prototype.getData = function() { +ToggleMenuView.prototype.getData = function () { return this.focusedItemIndex; }; -ToggleMenuView.prototype.setItems = function(items) { - items = items.slice(0, 2); // switch/toggle only works with two elements +ToggleMenuView.prototype.setItems = function (items) { + items = items.slice(0, 2); // switch/toggle only works with two elements ToggleMenuView.super_.prototype.setItems.call(this, items); - this.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/top_x.js b/core/top_x.js index 2403c380..e93a3128 100644 --- a/core/top_x.js +++ b/core/top_x.js @@ -2,111 +2,128 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const UserProps = require('./user_property.js'); -const UserLogNames = require('./user_log_name.js'); -const { Errors } = require('./enig_error.js'); -const UserDb = require('./database.js').dbs.user; -const SysDb = require('./database.js').dbs.system; -const User = require('./user.js'); +const { MenuModule } = require('./menu_module.js'); +const UserProps = require('./user_property.js'); +const UserLogNames = require('./user_log_name.js'); +const { Errors } = require('./enig_error.js'); +const UserDb = require('./database.js').dbs.user; +const SysDb = require('./database.js').dbs.system; +const User = require('./user.js'); // deps -const _ = require('lodash'); -const async = require('async'); +const _ = require('lodash'); +const async = require('async'); exports.moduleInfo = { - name : 'TopX', - desc : 'Displays users top X stats', - author : 'NuSkooler', - packageName : 'codes.l33t.enigma.topx', + name: 'TopX', + desc: 'Displays users top X stats', + author: 'NuSkooler', + packageName: 'codes.l33t.enigma.topx', }; const FormIds = { - menu : 0, + menu: 0, }; exports.getModule = class TopXModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { + extraArgs: options.extraArgs, + }); } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (callback) => { - const userPropValues = _.values(UserProps); - const userLogValues = _.values(UserLogNames); + callback => { + const userPropValues = _.values(UserProps); + const userLogValues = _.values(UserLogNames); const hasMci = (c, t) => { - if(!Array.isArray(t)) { - t = [ t ]; + if (!Array.isArray(t)) { + t = [t]; } - return t.some(t => _.isObject(mciData, [ 'menu', `${t}${c}` ])); + return t.some(t => _.isObject(mciData, ['menu', `${t}${c}`])); }; return this.validateConfigFields( { - mciMap : (key, config) => { - const mciCodes = Object.keys(config.mciMap).map(mci => { - return parseInt(mci); - }).filter(mci => !isNaN(mci)); - if(0 === mciCodes.length) { + mciMap: (key, config) => { + const mciCodes = Object.keys(config.mciMap) + .map(mci => { + return parseInt(mci); + }) + .filter(mci => !isNaN(mci)); + if (0 === mciCodes.length) { return false; } return mciCodes.every(mci => { const o = config.mciMap[mci]; - if(!_.isObject(o)) { + if (!_.isObject(o)) { return false; } const type = o.type; - switch(type) { - case 'userProp' : - if(!userPropValues.includes(o.value)) { + switch (type) { + case 'userProp': + if (!userPropValues.includes(o.value)) { return false; } // VM# must exist for this mci - if(!_.isObject(mciData, [ 'menu', `VM${mci}` ])) { + if ( + !_.isObject(mciData, [ + 'menu', + `VM${mci}`, + ]) + ) { return false; } break; - case 'userEventLog' : - if(!userLogValues.includes(o.value)) { + case 'userEventLog': + if (!userLogValues.includes(o.value)) { return false; } // VM# must exist for this mci - if(!hasMci(mci, ['VM'])) { + if (!hasMci(mci, ['VM'])) { return false; } break; - default : + default: return false; } return true; }); - } + }, }, callback ); }, - (callback) => { - return this.prepViewController('menu', FormIds.menu, mciData.menu, callback); + callback => { + return this.prepViewController( + 'menu', + FormIds.menu, + mciData.menu, + callback + ); + }, + callback => { + async.forEachSeries( + Object.keys(this.config.mciMap), + (mciCode, nextMciCode) => { + return this.populateTopXList(mciCode, nextMciCode); + }, + err => { + return callback(err); + } + ); }, - (callback) => { - async.forEachSeries(Object.keys(this.config.mciMap), (mciCode, nextMciCode) => { - return this.populateTopXList(mciCode, nextMciCode); - }, - err => { - return callback(err); - }); - } ], err => { return cb(err); @@ -117,43 +134,57 @@ exports.getModule = class TopXModule extends MenuModule { populateTopXList(mciCode, cb) { const listView = this.viewControllers.menu.getView(mciCode); - if(!listView) { + if (!listView) { return cb(Errors.UnexpectedState(`Failed to get view for MCI ${mciCode}`)); } const type = this.config.mciMap[mciCode].type; - switch(type) { - case 'userProp' : return this.populateTopXUserProp(listView, mciCode, cb); - case 'userEventLog' : return this.populateTopXUserEventLog(listView, mciCode, cb); + switch (type) { + case 'userProp': + return this.populateTopXUserProp(listView, mciCode, cb); + case 'userEventLog': + return this.populateTopXUserEventLog(listView, mciCode, cb); // we should not hit here; validation happens up front - default : return cb(Errors.UnexpectedState(`Unexpected type: ${type}`)); + default: + return cb(Errors.UnexpectedState(`Unexpected type: ${type}`)); } } rowsToItems(rows, cb) { let position = 1; - async.mapSeries(rows, (row, nextRow) => { - this.loadUserInfo(row.user_id, (err, userInfo) => { - if(err) { - return nextRow(err); - } - return nextRow(null, Object.assign(userInfo, { position : position++, value : row.value })); - }); - }, - (err, items) => { - return cb(err, items); - }); + async.mapSeries( + rows, + (row, nextRow) => { + this.loadUserInfo(row.user_id, (err, userInfo) => { + if (err) { + return nextRow(err); + } + return nextRow( + null, + Object.assign(userInfo, { + position: position++, + value: row.value, + }) + ); + }); + }, + (err, items) => { + return cb(err, items); + } + ); } populateTopXUserEventLog(listView, mciCode, cb) { - const mciMap = this.config.mciMap[mciCode]; - const count = listView.dimens.height || 1; - const daysBack = mciMap.daysBack; + const mciMap = this.config.mciMap[mciCode]; + const count = listView.dimens.height || 1; + const daysBack = mciMap.daysBack; const shouldSum = _.get(mciMap, 'sum', true); - const valueSql = shouldSum ? 'SUM(CAST(log_value AS INTEGER))' : 'COUNT()'; - const dateSql = daysBack ? `AND DATETIME(timestamp) >= DATETIME('now', '-${daysBack} days')` : ''; + const valueSql = shouldSum ? 'SUM(CAST(log_value AS INTEGER))' : 'COUNT()'; + const dateSql = daysBack + ? `AND DATETIME(timestamp) >= DATETIME('now', '-${daysBack} days')` + : ''; SysDb.all( `SELECT user_id, ${valueSql} AS value @@ -162,14 +193,14 @@ exports.getModule = class TopXModule extends MenuModule { GROUP BY user_id ORDER BY value DESC LIMIT ${count};`, - [ mciMap.value ], + [mciMap.value], (err, rows) => { - if(err) { + if (err) { return cb(err); } this.rowsToItems(rows, (err, items) => { - if(err) { + if (err) { return cb(err); } listView.setItems(items); @@ -188,14 +219,14 @@ exports.getModule = class TopXModule extends MenuModule { WHERE prop_name = ? ORDER BY value DESC LIMIT ${count};`, - [ this.config.mciMap[mciCode].value ], + [this.config.mciMap[mciCode].value], (err, rows) => { - if(err) { + if (err) { return cb(err); } this.rowsToItems(rows, (err, items) => { - if(err) { + if (err) { return cb(err); } listView.setItems(items); @@ -208,25 +239,26 @@ exports.getModule = class TopXModule extends MenuModule { loadUserInfo(userId, cb) { const getPropOpts = { - names : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations ] + names: [UserProps.RealName, UserProps.Location, UserProps.Affiliations], }; const userInfo = { userId }; User.getUserName(userId, (err, userName) => { - if(err) { + if (err) { return cb(err); } userInfo.userName = userName; User.loadProperties(userId, getPropOpts, (err, props) => { - if(err) { + if (err) { return cb(err); } - userInfo.location = props[UserProps.Location] || ''; - userInfo.affils = userInfo.affiliation = props[UserProps.Affiliations] || ''; - userInfo.realName = props[UserProps.RealName] || ''; + userInfo.location = props[UserProps.Location] || ''; + userInfo.affils = userInfo.affiliation = + props[UserProps.Affiliations] || ''; + userInfo.realName = props[UserProps.RealName] || ''; return cb(null, userInfo); }); diff --git a/core/upload.js b/core/upload.js index 7ab79c48..beb0cc71 100644 --- a/core/upload.js +++ b/core/upload.js @@ -2,118 +2,124 @@ 'use strict'; // enigma-bbs -const MenuModule = require('./menu_module.js').MenuModule; -const stringFormat = require('./string_format.js'); -const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; -const getAreaDefaultStorageDirectory = require('./file_base_area.js').getAreaDefaultStorageDirectory; -const scanFile = require('./file_base_area.js').scanFile; -const getFileAreaByTag = require('./file_base_area.js').getFileAreaByTag; -const getDescFromFileName = require('./file_base_area.js').getDescFromFileName; -const ansiGoto = require('./ansi_term.js').goto; -const moveFileWithCollisionHandling = require('./file_util.js').moveFileWithCollisionHandling; -const pathWithTerminatingSeparator = require('./file_util.js').pathWithTerminatingSeparator; -const Log = require('./logger.js').log; -const Errors = require('./enig_error.js').Errors; -const FileEntry = require('./file_entry.js'); -const isAnsi = require('./string_util.js').isAnsi; -const Events = require('./events.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const stringFormat = require('./string_format.js'); +const getSortedAvailableFileAreas = + require('./file_base_area.js').getSortedAvailableFileAreas; +const getAreaDefaultStorageDirectory = + require('./file_base_area.js').getAreaDefaultStorageDirectory; +const scanFile = require('./file_base_area.js').scanFile; +const getFileAreaByTag = require('./file_base_area.js').getFileAreaByTag; +const getDescFromFileName = require('./file_base_area.js').getDescFromFileName; +const ansiGoto = require('./ansi_term.js').goto; +const moveFileWithCollisionHandling = + require('./file_util.js').moveFileWithCollisionHandling; +const pathWithTerminatingSeparator = + require('./file_util.js').pathWithTerminatingSeparator; +const Log = require('./logger.js').log; +const Errors = require('./enig_error.js').Errors; +const FileEntry = require('./file_entry.js'); +const isAnsi = require('./string_util.js').isAnsi; +const Events = require('./events.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const temptmp = require('temptmp').createTrackedSession('upload'); -const paths = require('path'); -const sanatizeFilename = require('sanitize-filename'); +const async = require('async'); +const _ = require('lodash'); +const temptmp = require('temptmp').createTrackedSession('upload'); +const paths = require('path'); +const sanatizeFilename = require('sanitize-filename'); exports.moduleInfo = { - name : 'Upload', - desc : 'Module for classic file uploads', - author : 'NuSkooler', + name: 'Upload', + desc: 'Module for classic file uploads', + author: 'NuSkooler', }; const FormIds = { - options : 0, - processing : 1, - fileDetails : 2, - dupes : 3, + options: 0, + processing: 1, + fileDetails: 2, + dupes: 3, }; const MciViewIds = { - options : { - area : 1, // area selection - uploadType : 2, // blind vs specify filename - fileName : 3, // for non-blind; not editable for blind - navMenu : 4, // next/cancel/etc. - errMsg : 5, // errors (e.g. filename cannot be blank) + 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); this.interrupt = MenuModule.InterruptTypes.Never; - if(_.has(options, 'lastMenuResult.recvFilePaths')) { + if (_.has(options, 'lastMenuResult.recvFilePaths')) { this.recvFilePaths = options.lastMenuResult.recvFilePaths; } - this.availAreas = getSortedAvailableFileAreas(this.client, { writeAcs : true } ); + this.availAreas = getSortedAvailableFileAreas(this.client, { writeAcs: true }); this.menuMethods = { - optionsNavContinue : (formData, extraArgs, cb) => { + optionsNavContinue: (formData, extraArgs, cb) => { return this.performUpload(cb); }, - fileDetailsContinue : (formData, extraArgs, 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 + return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any }, // validation - validateNonBlindFileName : (fileName, cb) => { - if(0 === fileName.length) { + validateNonBlindFileName: (fileName, cb) => { + if (0 === fileName.length) { return cb(new Error('Filename cannot be empty')); } - fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. - if(0 === fileName.length) { // sanatize nuked everything? + fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc. + if (0 === fileName.length) { + // sanatize nuked everything? return cb(new Error('Invalid filename')); } // At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused ;-( - if(/^[0-9].*$/.test(fileName)) { + 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) { + viewValidationListener: (err, cb) => { + const errView = this.viewControllers.options.getView( + MciViewIds.options.errMsg + ); + if (errView) { + if (err) { errView.setText(err.message); } else { errView.clearText(); @@ -121,38 +127,50 @@ exports.getModule = class UploadModule extends MenuModule { } 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) { + if (this.availAreas.length > 0) { return { - uploadType : this.uploadType, - tempRecvDirectory : this.tempRecvDirectory, - areaInfo : this.availAreas[ this.viewControllers.options.getView(MciViewIds.options.area).getData() ], + uploadType: this.uploadType, + tempRecvDirectory: this.tempRecvDirectory, + areaInfo: + this.availAreas[ + this.viewControllers.options + .getView(MciViewIds.options.area) + .getData() + ], }; } } restoreSavedState(savedState) { - if(savedState.areaInfo) { - this.uploadType = savedState.uploadType; - this.areaInfo = savedState.areaInfo; - this.tempRecvDirectory = savedState.tempRecvDirectory; + 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); } + isBlindUpload() { + return 'blind' === this.uploadType; + } + isFileTransferComplete() { + return !_.isUndefined(this.recvFilePaths); + } initSequence() { const self = this; - if(0 === this.availAreas.length) { + if (0 === this.availAreas.length) { // - return this.gotoMenu(this.menuConfig.config.noUploadAreasAvailMenu || 'fileBaseNoUploadAreasAvail'); + return this.gotoMenu( + this.menuConfig.config.noUploadAreasAvailMenu || + 'fileBaseNoUploadAreasAvail' + ); } async.series( @@ -161,12 +179,12 @@ exports.getModule = class UploadModule extends MenuModule { return self.beforeArt(callback); }, function display(callback) { - if(self.isFileTransferComplete()) { + if (self.isFileTransferComplete()) { return self.displayProcessingPage(callback); } else { return self.displayOptionsPage(callback); } - } + }, ], () => { return self.finishedLoading(); @@ -175,14 +193,14 @@ exports.getModule = class UploadModule extends MenuModule { } finishedLoading() { - if(this.isFileTransferComplete()) { + if (this.isFileTransferComplete()) { return this.processUploadedFiles(); } } performUpload(cb) { - temptmp.mkdir( { prefix : 'enigul-' }, (err, tempRecvDirectory) => { - if(err) { + temptmp.mkdir({ prefix: 'enigul-' }, (err, tempRecvDirectory) => { + if (err) { return cb(err); } @@ -190,15 +208,17 @@ exports.getModule = class UploadModule extends MenuModule { 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', - } + extraArgs: { + recvDirectory: this.tempRecvDirectory, // we'll move files from here to their area container once processed/confirmed + direction: 'recv', + }, }; - if(!this.isBlindUpload()) { + if (!this.isBlindUpload()) { // data has been sanatized at this point - modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData(); + modOpts.extraArgs.recvFileName = this.viewControllers.options + .getView(MciViewIds.options.fileName) + .getData(); } // @@ -206,7 +226,8 @@ exports.getModule = class UploadModule extends MenuModule { // Upon completion, we'll re-enter the module with some file paths handed to us // return this.gotoMenu( - this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', + this.menuConfig.config.fileTransferProtocolSelection || + 'fileTransferProtocolSelection', modOpts, cb ); @@ -220,88 +241,112 @@ exports.getModule = class UploadModule extends MenuModule { updateScanStepInfoViews(stepInfo) { // :TODO: add some blinking (e.g. toggle items) indicators - see OBV.DOC - const fmtObj = Object.assign( {}, stepInfo); + const fmtObj = Object.assign({}, stepInfo); let stepIndicatorFmt = ''; let logStepFmt; const fmtConfig = this.menuConfig.config; - const indicatorStates = fmtConfig.indicatorStates || [ '|', '/', '-', '\\' ]; + const indicatorStates = fmtConfig.indicatorStates || ['|', '/', '-', '\\']; const indicatorFinished = fmtConfig.indicatorFinished || '√'; - const indicator = { }; + const indicator = {}; const self = this; function updateIndicator(mci, isFinished) { indicator.mci = mci; - if(isFinished) { + if (isFinished) { indicator.text = indicatorFinished; } else { self.scanStatus.indicatorPos += 1; - if(self.scanStatus.indicatorPos >= indicatorStates.length) { + 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}'; + switch (stepInfo.step) { + case 'start': + logStepFmt = stepIndicatorFmt = + fmtConfig.scanningStartFormat || 'Scanning {fileName}'; break; - case 'hash_update' : - stepIndicatorFmt = fmtConfig.calcHashFormat || 'Calculating hash/checksums: {calcHashPercent}%'; + 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'; + 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'; + case 'archive_list_start': + stepIndicatorFmt = + fmtConfig.extractArchiveListFormat || 'Extracting archive list'; updateIndicator(MciViewIds.processing.archiveListIndicator); break; - case 'archive_list_finish' : + case 'archive_list_finish': fmtObj.archivedFileCount = stepInfo.archiveEntries.length; - stepIndicatorFmt = fmtConfig.extractArchiveListFinishFormat || 'Archive list extracted ({archivedFileCount} files)'; + 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'; + case 'archive_list_failed': + stepIndicatorFmt = + fmtConfig.extractArchiveListFailedFormat || + 'Archive list extraction failed'; break; - case 'desc_files_start' : - stepIndicatorFmt = fmtConfig.processingDescFilesFormat || 'Processing description files'; + 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'; + case 'desc_files_finish': + stepIndicatorFmt = + fmtConfig.processingDescFilesFinishFormat || + 'Finished processing description files'; updateIndicator(MciViewIds.processing.descFileIndicator, true); break; - case 'finished' : - logStepFmt = stepIndicatorFmt = fmtConfig.scanningStartFormat || 'Finished'; + 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 (this.hasProcessingArt) { + this.updateCustomViewTextsWithFilter( + 'processing', + MciViewIds.processing.customRangeStart, + fmtObj, + { appendMultiLine: true } + ); - if(indicator.mci && indicator.text) { + 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 } ); + if (logStepFmt) { + this.setViewText( + 'processing', + MciViewIds.processing.logStep, + stringFormat(logStepFmt, fmtObj), + { appendMultiLine: true } + ); } } else { this.client.term.pipeWrite(fmtObj.stepIndicatorText); @@ -312,136 +357,162 @@ exports.getModule = class UploadModule extends MenuModule { const self = this; const results = { - newEntries : [], - dupes : [], + newEntries: [], + dupes: [], }; - self.client.log.debug('Scanning upload(s)', { paths : this.recvFilePaths } ); + 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 + async.eachSeries( + this.recvFilePaths, + (filePath, nextFilePath) => { + // :TODO: virus scanning/etc. should occur around here - currentFileNum += 1; + currentFileNum += 1; - self.scanStatus = { - indicatorPos : 0, - }; + self.scanStatus = { + indicatorPos: 0, + }; - const scanOpts = { - areaTag : self.areaInfo.areaTag, - storageTag : self.areaInfo.storageTags[0], - hashTags : self.areaInfo.hashTags, - }; + const scanOpts = { + areaTag: self.areaInfo.areaTag, + storageTag: self.areaInfo.storageTags[0], + hashTags: self.areaInfo.hashTags, + }; - function handleScanStep(stepInfo, nextScanStep) { - stepInfo.totalFileNum = self.recvFilePaths.length; - stepInfo.currentFileNum = currentFileNum; + function handleScanStep(stepInfo, nextScanStep) { + stepInfo.totalFileNum = self.recvFilePaths.length; + stepInfo.currentFileNum = currentFileNum; - self.updateScanStepInfoViews(stepInfo); - return nextScanStep(null); + 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); } - - 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' ); + 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); + 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 } - ); - return nextEntry(null); // still try next file - } else if(dst !== finalPath) - { - // name changed; adjust before persist - newEntry.fileName = paths.basename(finalPath); - } - - 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 } ); + 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, + }); + return nextEntry(null); // still try next file + } else if (dst !== finalPath) { + // name changed; adjust 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(); - }); + }, + () => { + // + // 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; + 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); + } - 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); - }); + }); + }, + err => { + delete this.fileDetailsCurrentEntrySubmitCallback; + return cb(err, scanResults); + } + ); } displayDupesPage(dupes, cb) { @@ -457,54 +528,71 @@ exports.getModule = class UploadModule extends MenuModule { self.prepViewControllerWithArt( 'dupes', FormIds.dupes, - { clearScreen : true, trailingLF : false }, + { clearScreen: true, trailingLF: false }, err => { - if(err) { - self.client.term.pipeWrite('|00|07Duplicate upload(s) found:\n'); + 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); + 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); - } + 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); - }); + 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}'; + const dupeInfoFormat = + self.menuConfig.config.dupeInfoFormat || + '{fileName} @ {areaName}'; - if(dupeListView) { - dupeListView.setItems(dupes.map(dupe => stringFormat(dupeInfoFormat, dupe) ) ); + if (dupeListView) { + dupeListView.setItems( + dupes.map(dupe => stringFormat(dupeInfoFormat, dupe)) + ); dupeListView.redraw(); } else { dupes.forEach(dupe => { - self.client.term.pipeWrite(`${stringFormat(dupeInfoFormat, dupe)}\n`); + self.client.term.pipeWrite( + `${stringFormat(dupeInfoFormat, dupe)}\n` + ); }); } return callback(null); }, function pause(callback) { - return self.pausePrompt( { row : self.client.term.termHeight }, callback); - } + return self.pausePrompt( + { row: self.client.term.termHeight }, + callback + ); + }, ], err => { return cb(err); @@ -521,7 +609,7 @@ exports.getModule = class UploadModule extends MenuModule { async.waterfall( [ function prepNonBlind(callback) { - if(self.isBlindUpload()) { + if (self.isBlindUpload()) { return callback(null); } @@ -529,9 +617,16 @@ exports.getModule = class UploadModule extends MenuModule { // For non-blind uploads, batch is not supported, we expect a single file // in |recvFilePaths|. If not, it's an error (we don't want to process the wrong thing) // - 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}`)); + 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); @@ -540,18 +635,20 @@ exports.getModule = class UploadModule extends MenuModule { return self.scanFiles(callback); }, function pause(scanResults, callback) { - if(self.hasProcessingArt) { - self.client.term.rawWrite(ansiGoto(self.client.term.termHeight, 1)); + if (self.hasProcessingArt) { + self.client.term.rawWrite( + ansiGoto(self.client.term.termHeight, 1) + ); } else { self.client.term.write('\n'); } - self.pausePrompt( () => { + self.pausePrompt(() => { return callback(null, scanResults); }); }, function displayDupes(scanResults, callback) { - if(0 === scanResults.dupes.length) { + if (0 === scanResults.dupes.length) { return callback(null, scanResults); } @@ -572,20 +669,19 @@ exports.getModule = class UploadModule extends MenuModule { return callback(null, scanResults.newEntries); }, function sendEvent(uploadedEntries, callback) { - Events.emit( - Events.getSystemEvents().UserUpload, - { - user : self.client.user, - files : uploadedEntries, - } - ); + 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. + if (err) { + self.client.log.warn('File upload error encountered', { + error: err.message, + }); + self.cleanupTempFiles(); // normally called after moveAndPersistUploadsToDatabase() is completed. } return self.prevMenu(); @@ -602,26 +698,36 @@ exports.getModule = class UploadModule extends MenuModule { return self.prepViewControllerWithArt( 'options', FormIds.options, - { clearScreen : true, trailingLF : false }, + { 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 areaSelectView = self.viewControllers.options.getView( + MciViewIds.options.area + ); + areaSelectView.setItems( + self.availAreas.map(areaInfo => areaInfo.name) + ); - const uploadTypeView = self.viewControllers.options.getView(MciViewIds.options.uploadType); - const fileNameView = self.viewControllers.options.getView(MciViewIds.options.fileName); + const uploadTypeView = self.viewControllers.options.getView( + MciViewIds.options.uploadType + ); + const fileNameView = self.viewControllers.options.getView( + MciViewIds.options.fileName + ); - const blindFileNameText = self.menuConfig.config.blindFileNameText || '(blind - filename ignored)'; + const blindFileNameText = + self.menuConfig.config.blindFileNameText || + '(blind - filename ignored)'; uploadTypeView.on('index update', idx => { - self.uploadType = (0 === idx) ? 'blind' : 'non-blind'; + self.uploadType = 0 === idx ? 'blind' : 'non-blind'; - if(self.isBlindUpload()) { + if (self.isBlindUpload()) { fileNameView.setText(blindFileNameText); fileNameView.acceptsFocus = false; - } else { + } else { fileNameView.clearText(); fileNameView.acceptsFocus = true; } @@ -629,21 +735,23 @@ exports.getModule = class UploadModule extends MenuModule { // 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())); + if (prevView.id === MciViewIds.options.fileName) { + fileNameView.setText( + sanatizeFilename(fileNameView.getData()) + ); } }); self.uploadType = 'blind'; - uploadTypeView.setFocusItemIndex(0); // default to blind + uploadTypeView.setFocusItemIndex(0); // default to blind fileNameView.setText(blindFileNameText); areaSelectView.redraw(); return callback(null); - } + }, ], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -654,7 +762,7 @@ exports.getModule = class UploadModule extends MenuModule { return this.prepViewControllerWithArt( 'processing', FormIds.processing, - { clearScreen : true, trailingLF : false }, + { clearScreen: true, trailingLF: false }, err => { // note: this art is not required this.hasProcessingArt = !err; @@ -665,7 +773,7 @@ exports.getModule = class UploadModule extends MenuModule { } fileEntryHasDetectedDesc(fileEntry) { - return (fileEntry.desc && fileEntry.desc.length > 0); + return fileEntry.desc && fileEntry.desc.length > 0; } displayFileDetailsPageForUploadEntry(fileEntry, cb) { @@ -677,50 +785,74 @@ exports.getModule = class UploadModule extends MenuModule { return self.prepViewControllerWithArt( 'fileDetails', FormIds.fileDetails, - { clearScreen : true, trailingLF : false }, + { 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); + 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 ); + self.updateCustomViewTextsWithFilter( + 'fileDetails', + MciViewIds.fileDetails.customRangeStart, + fileEntry + ); - tagsView.setText( Array.from(fileEntry.hashTags).join(',') ); // :TODO: optional 'hashTagsSep' like file list/browse + tagsView.setText(Array.from(fileEntry.hashTags).join(',')); // :TODO: optional 'hashTagsSep' like file list/browse yearView.setText(fileEntry.meta.est_release_year || ''); - if(isAnsi(fileEntry.desc)) { + if (isAnsi(fileEntry.desc)) { fileEntry.descIsAnsi = true; return descView.setAnsi( fileEntry.desc, { - prepped : false, - forceLineTerm : true, + prepped: false, + forceLineTerm: true, }, () => { - return callback(null, descView, 'preview', MciViewIds.fileDetails.tags); + 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 + 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 ); - return callback(null, descView, 'edit', hasDesc ? MciViewIds.fileDetails.tags : MciViewIds.fileDetails.desc); } }, function finalizeViews(descView, descViewMode, focusId, callback) { descView.setPropertyValue('mode', descViewMode); - descView.acceptsFocus = 'preview' === descViewMode ? false : true; + descView.acceptsFocus = 'preview' === descViewMode ? false : true; self.viewControllers.fileDetails.switchFocus(focusId); return callback(null); - } + }, ], err => { // @@ -728,11 +860,11 @@ exports.getModule = class UploadModule extends MenuModule { // 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) { + if (err) { return cb(err); } - self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue + self.fileDetailsCurrentEntrySubmitCallback = cb; // stash for moduleMethods.fileDetailsContinue } ); } diff --git a/core/user.js b/core/user.js index 7c017e2c..ec6dcb73 100644 --- a/core/user.js +++ b/core/user.js @@ -2,36 +2,35 @@ 'use strict'; // ENiGMA½ -const userDb = require('./database.js').dbs.user; -const Config = require('./config.js').get; -const userGroup = require('./user_group.js'); -const { - Errors, - ErrorReasons -} = require('./enig_error.js'); -const Events = require('./events.js'); -const UserProps = require('./user_property.js'); -const Log = require('./logger.js').log; -const StatLog = require('./stat_log.js'); +const userDb = require('./database.js').dbs.user; +const Config = require('./config.js').get; +const userGroup = require('./user_group.js'); +const { Errors, ErrorReasons } = require('./enig_error.js'); +const Events = require('./events.js'); +const UserProps = require('./user_property.js'); +const Log = require('./logger.js').log; +const StatLog = require('./stat_log.js'); // deps -const crypto = require('crypto'); -const assert = require('assert'); -const async = require('async'); -const _ = require('lodash'); -const moment = require('moment'); -const sanatizeFilename = require('sanitize-filename'); -const ssh2 = require('ssh2'); +const crypto = require('crypto'); +const assert = require('assert'); +const async = require('async'); +const _ = require('lodash'); +const moment = require('moment'); +const sanatizeFilename = require('sanitize-filename'); +const ssh2 = require('ssh2'); -exports.isRootUserId = function(id) { return 1 === id; }; +exports.isRootUserId = function (id) { + return 1 === id; +}; module.exports = class User { constructor() { - this.userId = 0; - this.username = ''; - this.properties = {}; // name:value - this.groups = []; // group membership(s) - this.authFactor = User.AuthFactors.None; + this.userId = 0; + this.username = ''; + this.properties = {}; // name:value + this.groups = []; // group membership(s) + this.authFactor = User.AuthFactors.None; } // static property accessors @@ -41,24 +40,25 @@ module.exports = class User { static get AuthFactors() { return { - None : 0, // Not yet authenticated in any way - Factor1 : 1, // username + password/pubkey/etc. checked out - Factor2 : 2, // validated with 2FA of some sort such as OTP + None: 0, // Not yet authenticated in any way + Factor1: 1, // username + password/pubkey/etc. checked out + Factor2: 2, // validated with 2FA of some sort such as OTP }; } static get PBKDF2() { return { - iterations : 1000, - keyLen : 128, - saltLen : 32, + iterations: 1000, + keyLen: 128, + saltLen: 32, }; } static get StandardPropertyGroups() { return { - auth : [ - UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk, + auth: [ + UserProps.PassPbkdf2Salt, + UserProps.PassPbkdf2Dk, UserProps.AuthPubKey, ], }; @@ -66,10 +66,10 @@ module.exports = class User { static get AccountStatus() { return { - disabled : 0, // +op disabled - inactive : 1, // inactive, aka requires +op approval/activation - active : 2, // standard, active - locked : 3, // locked out (too many bad login attempts, etc.) + disabled: 0, // +op disabled + inactive: 1, // inactive, aka requires +op approval/activation + active: 2, // standard, active + locked: 3, // locked out (too many bad login attempts, etc.) }; } @@ -78,7 +78,7 @@ module.exports = class User { } isValid() { - if(this.userId <= 0 || this.username.length < Config().users.usernameMin) { + if (this.userId <= 0 || this.username.length < Config().users.usernameMin) { return false; } @@ -86,13 +86,15 @@ module.exports = class User { } hasValidPasswordProperties() { - const salt = this.getProperty(UserProps.PassPbkdf2Salt); - const dk = this.getProperty(UserProps.PassPbkdf2Dk); + const salt = this.getProperty(UserProps.PassPbkdf2Salt); + const dk = this.getProperty(UserProps.PassPbkdf2Dk); - if(!salt || !dk || - (salt.length !== User.PBKDF2.saltLen * 2) || - (dk.length !== User.PBKDF2.keyLen * 2)) - { + if ( + !salt || + !dk || + salt.length !== User.PBKDF2.saltLen * 2 || + dk.length !== User.PBKDF2.keyLen * 2 + ) { return false; } @@ -103,40 +105,42 @@ module.exports = class User { return User.isRootUserId(this.userId); } - isSysOp() { // alias to isRoot() + isSysOp() { + // alias to isRoot() return this.isRoot(); } isGroupMember(groupNames) { - if(_.isString(groupNames)) { - groupNames = [ groupNames ]; + if (_.isString(groupNames)) { + groupNames = [groupNames]; } - const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn))); + const isMember = groupNames.some(gn => -1 !== this.groups.indexOf(gn)); return isMember; } - getSanitizedName(type='username') { - const name = 'real' === type ? this.getProperty(UserProps.RealName) : this.username; + getSanitizedName(type = 'username') { + const name = + 'real' === type ? this.getProperty(UserProps.RealName) : this.username; return sanatizeFilename(name) || `user${this.userId.toString()}`; } getLegacySecurityLevel() { - if(this.isRoot() || this.isGroupMember('sysops')) { + if (this.isRoot() || this.isGroupMember('sysops')) { return 100; } - if(this.isGroupMember('users')) { + if (this.isGroupMember('users')) { return 30; } - return 10; // :TODO: Is this what we want? + return 10; // :TODO: Is this what we want? } processFailedLogin(userId, cb) { async.waterfall( [ - (callback) => { + callback => { return User.getUser(userId, callback); }, (tempUser, callback) => { @@ -151,20 +155,26 @@ module.exports = class User { }, (tempUser, failedAttempts, callback) => { const lockAccount = _.get(Config(), 'users.failedLogin.lockAccount'); - if(lockAccount > 0 && failedAttempts >= lockAccount) { + if (lockAccount > 0 && failedAttempts >= lockAccount) { const props = { - [ UserProps.AccountStatus ] : User.AccountStatus.locked, - [ UserProps.AccountLockedTs ] : StatLog.now, + [UserProps.AccountStatus]: User.AccountStatus.locked, + [UserProps.AccountLockedTs]: StatLog.now, }; - if(!_.has(tempUser.properties, UserProps.AccountLockedPrevStatus)) { - props[UserProps.AccountLockedPrevStatus] = tempUser.getProperty(UserProps.AccountStatus); + if ( + !_.has(tempUser.properties, UserProps.AccountLockedPrevStatus) + ) { + props[UserProps.AccountLockedPrevStatus] = + tempUser.getProperty(UserProps.AccountStatus); } - Log.info( { userId, failedAttempts }, '(Re)setting account to locked due to failed logins'); + Log.info( + { userId, failedAttempts }, + '(Re)setting account to locked due to failed logins' + ); return tempUser.persistProperties(props, callback); } return cb(null); - } + }, ], err => { return cb(err); @@ -174,24 +184,27 @@ module.exports = class User { unlockAccount(cb) { const prevStatus = this.getProperty(UserProps.AccountLockedPrevStatus); - if(!prevStatus) { - return cb(null); // nothing to do + if (!prevStatus) { + return cb(null); // nothing to do } this.persistProperty(UserProps.AccountStatus, prevStatus, err => { - if(err) { + if (err) { return cb(err); } - return this.removeProperties( [ UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs ], cb); + return this.removeProperties( + [UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs], + cb + ); }); } static get AuthFactor1Types() { return { - SSHPubKey : 'sshPubKey', - Password : 'password', - TLSClient : 'tlsClientAuth', + SSHPubKey: 'sshPubKey', + Password: 'password', + TLSClient: 'tlsClientAuth', }; } @@ -201,33 +214,42 @@ module.exports = class User { const tempAuthInfo = {}; const validatePassword = (props, callback) => { - User.generatePasswordDerivedKey(authInfo.password, props[UserProps.PassPbkdf2Salt], (err, dk) => { - if(err) { - return callback(err); + User.generatePasswordDerivedKey( + authInfo.password, + props[UserProps.PassPbkdf2Salt], + (err, dk) => { + if (err) { + return callback(err); + } + + // + // Use constant time comparison here for security feel-goods + // + const passDkBuf = Buffer.from(dk, 'hex'); + const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex'); + + return callback( + crypto.timingSafeEqual(passDkBuf, propsDkBuf) + ? null + : Errors.AccessDenied('Invalid password') + ); } - - // - // Use constant time comparison here for security feel-goods - // - const passDkBuf = Buffer.from(dk, 'hex'); - const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex'); - - return callback(crypto.timingSafeEqual(passDkBuf, propsDkBuf) ? - null : - Errors.AccessDenied('Invalid password') - ); - }); + ); }; const validatePubKey = (props, callback) => { const pubKeyActual = ssh2.utils.parseKey(props[UserProps.AuthPubKey]); - if(!pubKeyActual) { + if (!pubKeyActual) { return callback(Errors.AccessDenied('Invalid public key')); } - if(authInfo.pubKey.key.algo != pubKeyActual.type || - !crypto.timingSafeEqual(authInfo.pubKey.key.data, pubKeyActual.getPublicSSH())) - { + if ( + authInfo.pubKey.key.algo != pubKeyActual.type || + !crypto.timingSafeEqual( + authInfo.pubKey.key.data, + pubKeyActual.getPublicSSH() + ) + ) { return callback(Errors.AccessDenied('Invalid public key')); } @@ -239,7 +261,7 @@ module.exports = class User { function fetchUserId(callback) { // get user ID User.getUserIdAndName(username, (err, uid, un) => { - tempAuthInfo.userId = uid; + tempAuthInfo.userId = uid; tempAuthInfo.username = un; return callback(err); @@ -247,19 +269,23 @@ module.exports = class User { }, function getRequiredAuthProperties(callback) { // fetch properties required for authentication - User.loadProperties(tempAuthInfo.userId, { names : User.StandardPropertyGroups.auth }, (err, props) => { - return callback(err, props); - }); + User.loadProperties( + tempAuthInfo.userId, + { names: User.StandardPropertyGroups.auth }, + (err, props) => { + return callback(err, props); + } + ); }, function validatePassOrPubKey(props, callback) { - if(User.AuthFactor1Types.SSHPubKey === authInfo.type) { + if (User.AuthFactor1Types.SSHPubKey === authInfo.type) { return validatePubKey(props, callback); } return validatePassword(props, callback); }, function initProps(callback) { User.loadProperties(tempAuthInfo.userId, (err, allProps) => { - if(!err) { + if (!err) { tempAuthInfo.properties = allProps; } @@ -267,33 +293,51 @@ module.exports = class User { }); }, function checkAccountStatus(callback) { - const accountStatus = parseInt(tempAuthInfo.properties[UserProps.AccountStatus], 10); - if(User.AccountStatus.disabled === accountStatus) { - return callback(Errors.AccessDenied('Account disabled', ErrorReasons.Disabled)); + const accountStatus = parseInt( + tempAuthInfo.properties[UserProps.AccountStatus], + 10 + ); + if (User.AccountStatus.disabled === accountStatus) { + return callback( + Errors.AccessDenied('Account disabled', ErrorReasons.Disabled) + ); } - if(User.AccountStatus.inactive === accountStatus) { - return callback(Errors.AccessDenied('Account inactive', ErrorReasons.Inactive)); + if (User.AccountStatus.inactive === accountStatus) { + return callback( + Errors.AccessDenied('Account inactive', ErrorReasons.Inactive) + ); } - if(User.AccountStatus.locked === accountStatus) { - const autoUnlockMinutes = _.get(Config(), 'users.failedLogin.autoUnlockMinutes'); - const lockedTs = moment(tempAuthInfo.properties[UserProps.AccountLockedTs]); - if(autoUnlockMinutes && lockedTs.isValid()) { + if (User.AccountStatus.locked === accountStatus) { + const autoUnlockMinutes = _.get( + Config(), + 'users.failedLogin.autoUnlockMinutes' + ); + const lockedTs = moment( + tempAuthInfo.properties[UserProps.AccountLockedTs] + ); + if (autoUnlockMinutes && lockedTs.isValid()) { const minutesSinceLocked = moment().diff(lockedTs, 'minutes'); - if(minutesSinceLocked >= autoUnlockMinutes) { + if (minutesSinceLocked >= autoUnlockMinutes) { // allow the login - we will clear any lock there Log.info( - { username, userId : tempAuthInfo.userId, lockedAt : lockedTs.format() }, + { + username, + userId: tempAuthInfo.userId, + lockedAt: lockedTs.format(), + }, 'Locked account will now be unlocked due to auto-unlock minutes policy' ); return callback(null); } } - return callback(Errors.AccessDenied('Account is locked', ErrorReasons.Locked)); + return callback( + Errors.AccessDenied('Account is locked', ErrorReasons.Locked) + ); } // anything else besides active is still not allowed - if(User.AccountStatus.active !== accountStatus) { + if (User.AccountStatus.active !== accountStatus) { return callback(Errors.AccessDenied('Account is not active')); } @@ -301,26 +345,34 @@ module.exports = class User { }, function initGroups(callback) { userGroup.getGroupsForUser(tempAuthInfo.userId, (err, groups) => { - if(!err) { + if (!err) { tempAuthInfo.groups = groups; } return callback(err); }); - } + }, ], err => { - if(err) { + if (err) { // // If we failed login due to something besides an inactive or disabled account, // we need to update failure status and possibly lock the account. // // If locked already, update the lock timestamp -- ie, extend the lockout period. // - if(![ErrorReasons.Disabled, ErrorReasons.Inactive].includes(err.reasonCode) && tempAuthInfo.userId) { + if ( + ![ErrorReasons.Disabled, ErrorReasons.Inactive].includes( + err.reasonCode + ) && + tempAuthInfo.userId + ) { self.processFailedLogin(tempAuthInfo.userId, persistErr => { - if(persistErr) { - Log.warn( { error : persistErr.message }, 'Failed to persist failed login information'); + if (persistErr) { + Log.warn( + { error: persistErr.message }, + 'Failed to persist failed login information' + ); } return cb(err); // pass along original error }); @@ -329,16 +381,18 @@ module.exports = class User { } } else { // everything checks out - load up info - self.userId = tempAuthInfo.userId; - self.username = tempAuthInfo.username; - self.properties = tempAuthInfo.properties; - self.groups = tempAuthInfo.groups; - self.authFactor = User.AuthFactors.Factor1; + self.userId = tempAuthInfo.userId; + self.username = tempAuthInfo.username; + self.properties = tempAuthInfo.properties; + self.groups = tempAuthInfo.groups; + self.authFactor = User.AuthFactors.Factor1; // // If 2FA/OTP is required, this user is not quite authenticated yet. // - self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP) ? true : false); + self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP) + ? true + : false); self.removeProperty(UserProps.FailedLoginAttempts); @@ -347,8 +401,11 @@ module.exports = class User { // the user's previous status & clean up props. // self.unlockAccount(unlockErr => { - if(unlockErr) { - Log.warn( { error : unlockErr.message }, 'Failed to unlock account'); + if (unlockErr) { + Log.warn( + { error: unlockErr.message }, + 'Failed to unlock account' + ); } return cb(null); }); @@ -357,18 +414,23 @@ module.exports = class User { ); } - create(createUserInfo , cb) { + create(createUserInfo, cb) { assert(0 === this.userId); const config = Config(); - if(this.username.length < config.users.usernameMin || this.username.length > config.users.usernameMax) { + if ( + this.username.length < config.users.usernameMin || + this.username.length > config.users.usernameMax + ) { return cb(Errors.Invalid('Invalid username length')); } const self = this; // :TODO: set various defaults, e.g. default activation status, etc. - self.properties[UserProps.AccountStatus] = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active; + self.properties[UserProps.AccountStatus] = config.users.requireActivation + ? User.AccountStatus.inactive + : User.AccountStatus.active; async.waterfall( [ @@ -379,17 +441,19 @@ module.exports = class User { trans.run( `INSERT INTO user (user_name) VALUES (?);`, - [ self.username ], - function inserted(err) { // use classic function for |this| - if(err) { + [self.username], + function inserted(err) { + // use classic function for |this| + if (err) { return callback(err); } self.userId = this.lastID; // Do not require activation for userId 1 (root/admin) - if(User.RootUserID === self.userId) { - self.properties[UserProps.AccountStatus] = User.AccountStatus.active; + if (User.RootUserID === self.userId) { + self.properties[UserProps.AccountStatus] = + User.AccountStatus.active; } return callback(null, trans); @@ -397,21 +461,25 @@ module.exports = class User { ); }, function genAuthCredentials(trans, callback) { - User.generatePasswordDerivedKeyAndSalt(createUserInfo.password, (err, info) => { - if(err) { - return callback(err); - } + User.generatePasswordDerivedKeyAndSalt( + createUserInfo.password, + (err, info) => { + if (err) { + return callback(err); + } - self.properties[UserProps.PassPbkdf2Salt] = info.salt; - self.properties[UserProps.PassPbkdf2Dk] = info.dk; - return callback(null, trans); - }); + self.properties[UserProps.PassPbkdf2Salt] = info.salt; + self.properties[UserProps.PassPbkdf2Dk] = info.dk; + return callback(null, trans); + } + ); }, function setInitialGroupMembership(trans, callback) { // Assign initial groups. Must perform a clone: #235 - All users are sysops (and I can't un-sysop them) self.groups = [...config.users.defaultGroups]; - if(User.RootUserID === self.userId) { // root/SysOp? + if (User.RootUserID === self.userId) { + // root/SysOp? self.groups.push('sysops'); } @@ -423,17 +491,16 @@ module.exports = class User { }); }, function sendEvent(trans, callback) { - Events.emit( - Events.getSystemEvents().NewUser, - { - user : Object.assign({}, self, { sessionId : createUserInfo.sessionId } ) - } - ); + Events.emit(Events.getSystemEvents().NewUser, { + user: Object.assign({}, self, { + sessionId: createUserInfo.sessionId, + }), + }); return callback(null, trans); - } + }, ], (err, trans) => { - if(trans) { + if (trans) { trans[err ? 'rollback' : 'commit'](transErr => { return cb(err ? err : transErr); }); @@ -460,7 +527,7 @@ module.exports = class User { userGroup.addUserToGroups(self.userId, self.groups, trans, err => { return callback(err); }); - } + }, ], err => { return cb(err); @@ -472,9 +539,9 @@ module.exports = class User { userDb.run( `REPLACE INTO user_property (user_id, prop_name, prop_value) VALUES (?, ?, ?);`, - [ userId, propName, propValue ], + [userId, propName, propValue], err => { - if(cb) { + if (cb) { return cb(err, propValue); } } @@ -488,7 +555,7 @@ module.exports = class User { incrementProperty(propName, incrementBy) { incrementBy = incrementBy || 1; let newValue = parseInt(this.getProperty(propName)); - if(newValue) { + if (newValue) { newValue += incrementBy; } else { newValue = incrementBy; @@ -519,9 +586,9 @@ module.exports = class User { userDb.run( `DELETE FROM user_property WHERE user_id = ? AND prop_name = ?;`, - [ this.userId, propName ], + [this.userId, propName], err => { - if(cb) { + if (cb) { return cb(err); } } @@ -529,18 +596,21 @@ module.exports = class User { } removeProperties(propNames, cb) { - async.each(propNames, (name, next) => { - return this.removeProperty(name, next); - }, - err => { - if(cb) { - return cb(err); + async.each( + propNames, + (name, next) => { + return this.removeProperty(name, next); + }, + err => { + if (cb) { + return cb(err); + } } - }); + ); } persistProperties(properties, transOrDb, cb) { - if(!_.isFunction(cb) && _.isFunction(transOrDb)) { + if (!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; transOrDb = userDb; } @@ -555,31 +625,34 @@ module.exports = class User { 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) { + if (err) { return cb(err); } const newProperties = { - [ UserProps.PassPbkdf2Salt ] : info.salt, - [ UserProps.PassPbkdf2Dk ] : info.dk, + [UserProps.PassPbkdf2Salt]: info.salt, + [UserProps.PassPbkdf2Dk]: info.dk, }; this.persistProperties(newProperties, err => { @@ -590,7 +663,7 @@ module.exports = class User { getAge() { const birthdate = this.getProperty(UserProps.Birthdate); - if(birthdate) { + if (birthdate) { return moment().diff(birthdate, 'years'); } } @@ -612,18 +685,18 @@ module.exports = class User { 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.userId = userId; + user.username = userName; + user.properties = properties; + user.groups = groups; // explicitly NOT an authenticated user! - user.authenticated = false; - user.authFactor = User.AuthFactors.None; + user.authenticated = false; + user.authFactor = User.AuthFactors.None; return cb(err, user); } @@ -631,7 +704,7 @@ module.exports = class User { } static isRootUserId(userId) { - return (User.RootUserID === userId); + return User.RootUserID === userId; } static getUserIdAndName(username, cb) { @@ -639,13 +712,13 @@ module.exports = class User { `SELECT id, user_name FROM user WHERE user_name LIKE ?;`, - [ username ], + [username], (err, row) => { - if(err) { + if (err) { return cb(err); } - if(row) { + if (row) { return cb(null, row.id, row.user_name); } @@ -663,13 +736,13 @@ module.exports = class User { FROM user_property WHERE prop_name='${UserProps.RealName}' AND prop_value LIKE ? );`, - [ realName ], + [realName], (err, row) => { - if(err) { + if (err) { return cb(err); } - if(row) { + if (row) { return cb(null, row.id, row.user_name); } @@ -680,7 +753,7 @@ module.exports = class User { static getUserIdAndNameByLookup(lookup, cb) { User.getUserIdAndName(lookup, (err, userId, userName) => { - if(err) { + if (err) { User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => { return cb(err, userId, userName); }); @@ -695,13 +768,13 @@ module.exports = class User { `SELECT user_name FROM user WHERE id = ?;`, - [ userId ], + [userId], (err, row) => { - if(err) { + if (err) { return cb(err); } - if(row) { + if (row) { return cb(null, row.user_name); } @@ -711,31 +784,35 @@ module.exports = class User { } static loadProperties(userId, options, cb) { - if(!cb && _.isFunction(options)) { + if (!cb && _.isFunction(options)) { cb = options; options = {}; } - let sql = - `SELECT prop_name, prop_value + let sql = `SELECT prop_name, prop_value FROM user_property WHERE user_id = ?`; - if(options.names) { + 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); + 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); } - 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. @@ -746,9 +823,9 @@ module.exports = class User { `SELECT user_id FROM user_property WHERE prop_name = ? AND prop_value = ?;`, - [ propName, propValue ], + [propName, propValue], (err, row) => { - if(row) { + if (row) { userIds.push(row.user_id); } }, @@ -761,7 +838,7 @@ module.exports = class User { static getUserList(options, cb) { const userList = []; - options.properties = options.properties || [ UserProps.RealName ]; + options.properties = options.properties || [UserProps.RealName]; const asList = []; const joinList = []; @@ -769,7 +846,9 @@ module.exports = class User { const dbProp = options.properties[i]; const propName = options.propsCamelCase ? _.camelCase(dbProp) : dbProp; asList.push(`p${i}.prop_value AS ${propName}`); - joinList.push(`LEFT OUTER JOIN user_property p${i} ON p${i}.user_id = u.id AND p${i}.prop_name = '${dbProp}'`); + joinList.push( + `LEFT OUTER JOIN user_property p${i} ON p${i}.user_id = u.id AND p${i}.prop_name = '${dbProp}'` + ); } userDb.each( @@ -792,7 +871,7 @@ module.exports = class User { async.waterfall( [ function getSalt(callback) { - User.generatePasswordDerivedKeySalt( (err, salt) => { + User.generatePasswordDerivedKeySalt((err, salt) => { return callback(err, salt); }); }, @@ -800,17 +879,17 @@ module.exports = class User { User.generatePasswordDerivedKey(password, salt, (err, dk) => { return callback(err, salt, dk); }); - } + }, ], (err, salt, dk) => { - return cb(err, { salt : salt, dk : dk } ); + return cb(err, { salt: salt, dk: dk }); } ); } static generatePasswordDerivedKeySalt(cb) { crypto.randomBytes(User.PBKDF2.saltLen, (err, salt) => { - if(err) { + if (err) { return cb(err); } return cb(null, salt.toString('hex')); @@ -820,12 +899,19 @@ module.exports = class User { 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_2fa_otp.js b/core/user_2fa_otp.js index c51d1282..2da28e6f 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -2,67 +2,61 @@ 'use strict'; // ENiGMA½ -const UserProps = require('./user_property.js'); -const { - Errors, - ErrorReasons, -} = require('./enig_error.js'); -const User = require('./user.js'); -const { - recordLogin, - transformLoginError, -} = require('./user_login.js'); -const Config = require('./config.js').get; +const UserProps = require('./user_property.js'); +const { Errors, ErrorReasons } = require('./enig_error.js'); +const User = require('./user.js'); +const { recordLogin, transformLoginError } = require('./user_login.js'); +const Config = require('./config.js').get; // deps -const _ = require('lodash'); -const crypto = require('crypto'); -const qrGen = require('qrcode-generator'); +const _ = require('lodash'); +const crypto = require('crypto'); +const qrGen = require('qrcode-generator'); -exports.prepareOTP = prepareOTP; -exports.createBackupCodes = createBackupCodes; -exports.createQRCode = createQRCode; -exports.otpFromType = otpFromType; -exports.loginFactor2_OTP = loginFactor2_OTP; +exports.prepareOTP = prepareOTP; +exports.createBackupCodes = createBackupCodes; +exports.createQRCode = createQRCode; +exports.otpFromType = otpFromType; +exports.loginFactor2_OTP = loginFactor2_OTP; -const OTPTypes = exports.OTPTypes = { - RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512 - RFC4266_HOTP : 'rfc4266_HOTP', // HMAC-Based, SHA-512 - GoogleAuthenticator : 'googleAuth', // Google Authenticator is basically TOTP + quirks -}; +const OTPTypes = (exports.OTPTypes = { + RFC6238_TOTP: 'rfc6238_TOTP', // Time-Based, SHA-512 + RFC4266_HOTP: 'rfc4266_HOTP', // HMAC-Based, SHA-512 + GoogleAuthenticator: 'googleAuth', // Google Authenticator is basically TOTP + quirks +}); function otpFromType(otpType) { try { return { - [ OTPTypes.RFC6238_TOTP ] : () => { + [OTPTypes.RFC6238_TOTP]: () => { const totp = require('otplib/totp'); - totp.options = { crypto, algorithm : 'sha256' }; + totp.options = { crypto, algorithm: 'sha256' }; return totp; }, - [ OTPTypes.RFC4266_HOTP ] : () => { + [OTPTypes.RFC4266_HOTP]: () => { const hotp = require('otplib/hotp'); - hotp.options = { crypto, algorithm : 'sha256' }; + hotp.options = { crypto, algorithm: 'sha256' }; return hotp; }, - [ OTPTypes.GoogleAuthenticator ] : () => { + [OTPTypes.GoogleAuthenticator]: () => { const googleAuth = require('otplib/authenticator'); googleAuth.options = { crypto }; return googleAuth; }, }[otpType](); - } catch(e) { + } catch (e) { // nothing } } function generateOTPBackupCode() { const consonants = 'bdfghjklmnprstvz'.split(''); - const vowels = 'aiou'.split(''); + const vowels = 'aiou'.split(''); const bits = []; const rng = crypto.randomBytes(4); - for(let i = 0; i < rng.length / 2; ++i) { + for (let i = 0; i < rng.length / 2; ++i) { const n = rng.readUInt16BE(i * 2); const c1 = n & 0x0f; @@ -71,13 +65,11 @@ function generateOTPBackupCode() { const v2 = (n >> 10) & 0x03; const c3 = (n >> 12) & 0x0f; - bits.push([ - consonants[c1], - vowels[v1], - consonants[c2], - vowels[v2], - consonants[c3], - ].join('')); + bits.push( + [consonants[c1], vowels[v1], consonants[c2], vowels[v2], consonants[c3]].join( + '' + ) + ); } return bits.join('-'); @@ -89,12 +81,15 @@ function createBackupCodes() { } function validateAndConsumeBackupCode(user, token, cb) { - try - { - let validCodes = JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)); + try { + let validCodes = JSON.parse( + user.getProperty(UserProps.AuthFactor2OTPBackupCodes) + ); const matchingCode = validCodes.find(c => c === token); - if(!matchingCode) { - return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA)); + if (!matchingCode) { + return cb( + Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA) + ); } // We're consuming a match - remove it from available backup codes @@ -103,68 +98,73 @@ function validateAndConsumeBackupCode(user, token, cb) { user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => { return cb(err); }); - } catch(e) { + } catch (e) { return cb(e); } } function createQRCode(otp, options, secret) { try { - const uri = otp.keyuri(options.username || 'user', Config().general.boardName, secret); + const uri = otp.keyuri( + options.username || 'user', + Config().general.boardName, + secret + ); const qrCode = qrGen(0, 'L'); qrCode.addData(uri); qrCode.make(); options.qrType = options.qrType || 'ascii'; return { - ascii : qrCode.createASCII, - data : qrCode.createDataURL, - img : qrCode.createImgTag, - svg : qrCode.createSvgTag, + ascii: qrCode.createASCII, + data: qrCode.createDataURL, + img: qrCode.createImgTag, + svg: qrCode.createSvgTag, }[options.qrType](options.cellSize); - } catch(e) { + } catch (e) { return ''; } } function prepareOTP(otpType, options, cb) { - if(!_.isFunction(cb)) { + if (!_.isFunction(cb)) { cb = options; options = {}; } const otp = otpFromType(otpType); - if(!otp) { + if (!otp) { return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`)); } - const secret = OTPTypes.GoogleAuthenticator === otpType ? - otp.generateSecret() : - crypto.randomBytes(64).toString('base64').substr(0, 32); + const secret = + OTPTypes.GoogleAuthenticator === otpType + ? otp.generateSecret() + : crypto.randomBytes(64).toString('base64').substr(0, 32); const qr = createQRCode(otp, options, secret); - return cb(null, { secret, qr } ); + return cb(null, { secret, qr }); } function loginFactor2_OTP(client, token, cb) { - if(client.user.authFactor < User.AuthFactors.Factor1) { + if (client.user.authFactor < User.AuthFactors.Factor1) { return cb(Errors.AccessDenied('OTP requires prior authentication factor 1')); } const otpType = client.user.getProperty(UserProps.AuthFactor2OTP); - const otp = otpFromType(otpType); + const otp = otpFromType(otpType); - if(!otp) { + if (!otp) { return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`)); } const secret = client.user.getProperty(UserProps.AuthFactor2OTPSecret); - if(!secret) { + if (!secret) { return cb(Errors.Invalid('Missing OTP secret')); } - const valid = otp.verify( { token, secret } ); + const valid = otp.verify({ token, secret }); const allowLogin = () => { client.user.authFactor = User.AuthFactors.Factor2; @@ -172,13 +172,13 @@ function loginFactor2_OTP(client, token, cb) { return recordLogin(client, cb); }; - if(valid) { + if (valid) { return allowLogin(); } // maybe they punched in a backup code? validateAndConsumeBackupCode(client.user, token, err => { - if(err) { + if (err) { return cb(transformLoginError(err, client, client.user.username)); } diff --git a/core/user_2fa_otp_config.js b/core/user_2fa_otp_config.js index 1a7bf0c6..127d98eb 100644 --- a/core/user_2fa_otp_config.js +++ b/core/user_2fa_otp_config.js @@ -2,107 +2,122 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const UserProps = require('./user_property.js'); +const { MenuModule } = require('./menu_module.js'); +const UserProps = require('./user_property.js'); const { OTPTypes, otpFromType, createQRCode, createBackupCodes, -} = require('./user_2fa_otp.js'); -const { Errors } = require('./enig_error.js'); -const { getServer } = require('./listening_server.js'); -const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const WebRegister = require('./user_2fa_otp_web_register.js'); +} = require('./user_2fa_otp.js'); +const { Errors } = require('./enig_error.js'); +const { getServer } = require('./listening_server.js'); +const WebServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const WebRegister = require('./user_2fa_otp_web_register.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const iconv = require('iconv-lite'); +const async = require('async'); +const _ = require('lodash'); +const iconv = require('iconv-lite'); exports.moduleInfo = { - name : 'User 2FA/OTP Configuration', - desc : 'Module for user 2FA/OTP configuration', - author : 'NuSkooler', + name: 'User 2FA/OTP Configuration', + desc: 'Module for user 2FA/OTP configuration', + author: 'NuSkooler', }; const FormIds = { - menu : 0, + menu: 0, }; const MciViewIds = { - enableToggle : 1, - otpType : 2, - submit : 3, - infoText : 4, + enableToggle: 1, + otpType: 2, + submit: 3, + infoText: 4, - customRangeStart : 10, // 10+ = customs + customRangeStart: 10, // 10+ = customs }; const DefaultMsg = { infoText: { - disabled : 'Enabling 2-factor authentication can greatly increase account security.', - enabled : 'A valid email address set in user config is required to enable 2-Factor Authentication.', - rfc6238_TOTP : 'Time-Based One-Time-Password (TOTP, RFC-6238).', - rfc4266_HOTP : 'HMAC-Based One-Time-Password (HOTP, RFC-4266).', - googleAuth : 'Google Authenticator.', + disabled: + 'Enabling 2-factor authentication can greatly increase account security.', + enabled: + 'A valid email address set in user config is required to enable 2-Factor Authentication.', + rfc6238_TOTP: 'Time-Based One-Time-Password (TOTP, RFC-6238).', + rfc4266_HOTP: 'HMAC-Based One-Time-Password (HOTP, RFC-4266).', + googleAuth: 'Google Authenticator.', + }, + statusText: { + otpNotEnabled: '2FA/OTP is not currently enabled for this account.', + noBackupCodes: 'No backup codes remaining or set.', + saveDisabled: '2FA/OTP is now disabled for this account.', + saveEmailSent: + 'An 2FA/OTP registration email has been sent with further instructions.', + saveError: 'Failed to send email. Please contact the system operator.', + qrNotAvail: 'QR code not available for this OTP type.', + emailRequired: + 'Your account must have a valid email address set to use this feature.', }, - statusText : { - otpNotEnabled : '2FA/OTP is not currently enabled for this account.', - noBackupCodes : 'No backup codes remaining or set.', - saveDisabled : '2FA/OTP is now disabled for this account.', - saveEmailSent : 'An 2FA/OTP registration email has been sent with further instructions.', - saveError : 'Failed to send email. Please contact the system operator.', - qrNotAvail : 'QR code not available for this OTP type.', - emailRequired : 'Your account must have a valid email address set to use this feature.', - } }; exports.getModule = class User2FA_OTPConfigModule extends MenuModule { constructor(options) { super(options); - this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { + extraArgs: options.extraArgs, + }); this.menuMethods = { - showQRCode : (formData, extraArgs, cb) => { + showQRCode: (formData, extraArgs, cb) => { return this.showQRCode(cb); }, - showSecret : (formData, extraArgs, cb) => { + showSecret: (formData, extraArgs, cb) => { return this.showSecret(cb); }, - showBackupCodes : (formData, extraArgs, cb) => { + showBackupCodes: (formData, extraArgs, cb) => { return this.showBackupCodes(cb); }, - generateNewBackupCodes : (formData, extraArgs, cb) => { + generateNewBackupCodes: (formData, extraArgs, cb) => { return this.generateNewBackupCodes(cb); }, - saveChanges : (formData, extraArgs, cb) => { + saveChanges: (formData, extraArgs, cb) => { return this.saveChanges(formData, cb); - } + }, }; } initSequence() { const webServer = getServer(WebServerPackageName); - if(!webServer || !webServer.instance.isEnabled()) { - this.client.log.warn('User 2FA/OTP configuration requires the web server to be enabled!'); - return this.prevMenu( () => { /* dummy */ } ); + if (!webServer || !webServer.instance.isEnabled()) { + this.client.log.warn( + 'User 2FA/OTP configuration requires the web server to be enabled!' + ); + return this.prevMenu(() => { + /* dummy */ + }); } return super.initSequence(); } mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (callback) => { - return this.prepViewController('menu', FormIds.menu, mciData.menu, callback); + callback => { + return this.prepViewController( + 'menu', + FormIds.menu, + mciData.menu, + callback + ); }, - (callback) => { + callback => { const requiredCodes = [ MciViewIds.enableToggle, MciViewIds.otpType, @@ -110,8 +125,11 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { ]; return this.validateMCIByViewIds('menu', requiredCodes, callback); }, - (callback) => { - const enableToggleView = this.getView('menu', MciViewIds.enableToggle); + callback => { + const enableToggleView = this.getView( + 'menu', + MciViewIds.enableToggle + ); let initialIndex = this.isOTPEnabledForUser() ? 1 : 0; enableToggleView.setFocusItemIndex(initialIndex); this.enableToggleUpdate(initialIndex); @@ -129,15 +147,17 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { }); this.viewControllers.menu.on('return', view => { - if(view === enableToggleView) { - return this.enableToggleUpdate(enableToggleView.focusedItemIndex); + if (view === enableToggleView) { + return this.enableToggleUpdate( + enableToggleView.focusedItemIndex + ); } else if (view === otpTypeView) { return this.otpTypeUpdate(otpTypeView.focusedItemIndex); } }); return callback(null); - } + }, ], err => { return cb(err); @@ -148,12 +168,13 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { displayDetails(details, cb) { const modOpts = { - extraArgs : { - artData : iconv.encode(`${details}\r\n`, 'cp437'), - } + extraArgs: { + artData: iconv.encode(`${details}\r\n`, 'cp437'), + }, }; this.gotoMenu( - this.menuConfig.config.userTwoFactorAuthOTPConfigShowDetails || 'userTwoFactorAuthOTPConfigShowDetails', + this.menuConfig.config.userTwoFactorAuthOTPConfigShowDetails || + 'userTwoFactorAuthOTPConfigShowDetails', modOpts, cb ); @@ -163,12 +184,12 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { const otp = otpFromType(this.client.user.getProperty(UserProps.AuthFactor2OTP)); let qrCode; - if(!otp) { + if (!otp) { qrCode = this.getStatusText('otpNotEnabled'); } else { const qrOptions = { - username : this.client.user.username, - qrType : 'ascii', + username: this.client.user.username, + qrType: 'ascii', }; qrCode = createQRCode( @@ -177,7 +198,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { this.client.user.getProperty(UserProps.AuthFactor2OTPSecret) ); - if(qrCode) { + if (qrCode) { qrCode = qrCode.replace(/\n/g, '\r\n'); } else { qrCode = this.getStatusText('qrNotAvail'); @@ -197,13 +218,16 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { showBackupCodes(cb) { let info; const noBackupCodes = this.getStatusText('noBackupCodes'); - if(!this.isOTPEnabledForUser()) { + if (!this.isOTPEnabledForUser()) { info = this.getStatusText('otpNotEnabled'); } else { try { - info = JSON.parse(this.client.user.getProperty(UserProps.AuthFactor2OTPBackupCodes) || '[]').join(', '); + info = JSON.parse( + this.client.user.getProperty(UserProps.AuthFactor2OTPBackupCodes) || + '[]' + ).join(', '); info = info || noBackupCodes; - } catch(e) { + } catch (e) { info = noBackupCodes; } } @@ -211,7 +235,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } generateNewBackupCodes(cb) { - if(!this.isOTPEnabledForUser()) { + if (!this.isOTPEnabledForUser()) { const info = this.getStatusText('otpNotEnabled'); return this.displayDetails(info, cb); } @@ -221,7 +245,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { UserProps.AuthFactor2OTPBackupCodes, JSON.stringify(backupCodes), err => { - if(err) { + if (err) { return cb(err); } const info = backupCodes.join(', '); @@ -232,21 +256,25 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { saveChanges(formData, cb) { const enabled = 1 === _.get(formData, 'value.enableToggle', 0); - return enabled ? this.saveChangesEnable(formData, cb) : this.saveChangesDisable(cb); + return enabled + ? this.saveChangesEnable(formData, cb) + : this.saveChangesDisable(cb); } saveChangesEnable(formData, cb) { // User must have an email address set to save const emailRegExp = /[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(.[a-z0-9-]+)*/; const emailAddr = this.client.user.getProperty(UserProps.EmailAddress); - if(!emailAddr || !emailRegExp.test(emailAddr)) { + if (!emailAddr || !emailRegExp.test(emailAddr)) { const info = this.getStatusText('emailRequired'); return this.displayDetails(info, cb); } - const otpTypeProp = this.otpTypeFromOTPTypeIndex(_.get(formData, 'value.otpType')); + const otpTypeProp = this.otpTypeFromOTPTypeIndex( + _.get(formData, 'value.otpType') + ); - const saveFailedError = (err) => { + const saveFailedError = err => { const info = this.getStatusText('saveError'); this.displayDetails(info, () => { return cb(err); @@ -254,16 +282,18 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { }; // sanity check - if(!otpFromType(otpTypeProp)) { - return saveFailedError(Errors.Invalid('Cannot convert selected index to valid OTP type')); + if (!otpFromType(otpTypeProp)) { + return saveFailedError( + Errors.Invalid('Cannot convert selected index to valid OTP type') + ); } this.removeUserOTPProperties(err => { - if(err) { + if (err) { return saveFailedError(err); } WebRegister.sendRegisterEmail(this.client.user, otpTypeProp, err => { - if(err) { + if (err) { return saveFailedError(err); } @@ -284,7 +314,7 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { saveChangesDisable(cb) { this.removeUserOTPProperties(err => { - if(err) { + if (err) { return cb(err); } @@ -298,41 +328,46 @@ exports.getModule = class User2FA_OTPConfigModule extends MenuModule { } getInfoText(key) { - return _.get(this.config, [ 'infoText', key ], DefaultMsg.infoText[key]); + return _.get(this.config, ['infoText', key], DefaultMsg.infoText[key]); } getStatusText(key) { - return _.get(this.config, [ 'statusText', key ], DefaultMsg.statusText[key]); + return _.get(this.config, ['statusText', key], DefaultMsg.statusText[key]); } enableToggleUpdate(idx) { const key = { - 0 : 'disabled', - 1 : 'enabled', + 0: 'disabled', + 1: 'enabled', }[idx]; - this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); + this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { + infoText: this.getInfoText(key), + }); } otpTypeIndexFromUserOTPType(defaultIndex = 0) { const type = this.client.user.getProperty(UserProps.AuthFactor2OTP); - return { - [ OTPTypes.RFC6238_TOTP ] : 0, - [ OTPTypes.RFC4266_HOTP ] : 1, - [ OTPTypes.GoogleAuthenticator ] : 2, - }[type] || defaultIndex; + return ( + { + [OTPTypes.RFC6238_TOTP]: 0, + [OTPTypes.RFC4266_HOTP]: 1, + [OTPTypes.GoogleAuthenticator]: 2, + }[type] || defaultIndex + ); } otpTypeFromOTPTypeIndex(idx) { return { - 0 : OTPTypes.RFC6238_TOTP, - 1 : OTPTypes.RFC4266_HOTP, - 2 : OTPTypes.GoogleAuthenticator, + 0: OTPTypes.RFC6238_TOTP, + 1: OTPTypes.RFC4266_HOTP, + 2: OTPTypes.GoogleAuthenticator, }[idx]; } otpTypeUpdate(idx) { const key = this.otpTypeFromOTPTypeIndex(idx); - this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { infoText : this.getInfoText(key) } ); + this.updateCustomViewTextsWithFilter('menu', MciViewIds.customRangeStart, { + infoText: this.getInfoText(key), + }); } }; - diff --git a/core/user_2fa_otp_web_register.js b/core/user_2fa_otp_web_register.js index 2b7d294c..c8c8dcb3 100644 --- a/core/user_2fa_otp_web_register.js +++ b/core/user_2fa_otp_web_register.js @@ -2,40 +2,33 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const getServer = require('./listening_server.js').getServer; -const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const Config = require('./config.js').get; +const getServer = require('./listening_server.js').getServer; +const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; const { createToken, deleteToken, getTokenInfo, WellKnownTokenTypes, -} = require('./user_temp_token.js'); -const { - prepareOTP, - createBackupCodes, - otpFromType, -} = require('./user_2fa_otp.js'); -const { sendMail } = require('./email.js'); -const UserProps = require('./user_property.js'); -const Log = require('./logger.js').log; -const { - getConnectionByUserId -} = require('./client_connections.js'); +} = require('./user_temp_token.js'); +const { prepareOTP, createBackupCodes, otpFromType } = require('./user_2fa_otp.js'); +const { sendMail } = require('./email.js'); +const UserProps = require('./user_property.js'); +const Log = require('./logger.js').log; +const { getConnectionByUserId } = require('./client_connections.js'); // deps -const async = require('async'); -const fs = require('fs-extra'); -const _ = require('lodash'); -const url = require('url'); -const querystring = require('querystring'); +const async = require('async'); +const fs = require('fs-extra'); +const _ = require('lodash'); +const url = require('url'); +const querystring = require('querystring'); function getWebServer() { return getServer(webServerPackageName); } -const DefaultEmailTextTemplate = - `%USERNAME%: +const DefaultEmailTextTemplate = `%USERNAME%: You have requested to enable 2-Factor Authentication via One-Time-Password for your account on %BOARDNAME%. @@ -45,8 +38,7 @@ for your account on %BOARDNAME%. %REGISTER_URL% `; -module.exports = class User2FA_OTPWebRegister -{ +module.exports = class User2FA_OTPWebRegister { static startup(cb) { return User2FA_OTPWebRegister.registerRoutes(cb); } @@ -54,23 +46,29 @@ module.exports = class User2FA_OTPWebRegister static sendRegisterEmail(user, otpType, cb) { async.waterfall( [ - (callback) => { + callback => { return createToken( user.userId, WellKnownTokenTypes.AuthFactor2OTPRegister, - { bits : 128 }, + { bits: 128 }, callback ); }, (token, callback) => { const config = Config(); - const txtTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailText'); - const htmlTemplateFile = _.get(config, 'users.twoFactorAuth.otp.registerEmailHtml'); + const txtTemplateFile = _.get( + config, + 'users.twoFactorAuth.otp.registerEmailText' + ); + const htmlTemplateFile = _.get( + config, + 'users.twoFactorAuth.otp.registerEmailHtml' + ); fs.readFile(txtTemplateFile, 'utf8', (err, textTemplate) => { textTemplate = textTemplate || DefaultEmailTextTemplate; fs.readFile(htmlTemplateFile, 'utf8', (err, htmlTemplate) => { - htmlTemplate = htmlTemplate || null; // be explicit for waterfall + htmlTemplate = htmlTemplate || null; // be explicit for waterfall return callback(null, token, textTemplate, htmlTemplate); }); }); @@ -81,37 +79,44 @@ module.exports = class User2FA_OTPWebRegister `/enable_2fa_otp?token=${token}&otpType=${otpType}` ); - const replaceTokens = (s) => { + const replaceTokens = s => { return s - .replace(/%BOARDNAME%/g, Config().general.boardName) - .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, token) - .replace(/%REGISTER_URL%/g, registerUrl) - ; + .replace(/%BOARDNAME%/g, Config().general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace(/%TOKEN%/g, token) + .replace(/%REGISTER_URL%/g, registerUrl); }; textTemplate = replaceTokens(textTemplate); - if(htmlTemplate) { + if (htmlTemplate) { htmlTemplate = replaceTokens(htmlTemplate); } const message = { - to : `${user.getProperty(UserProps.RealName) || user.username} <${user.getProperty(UserProps.EmailAddress)}>`, + to: `${ + user.getProperty(UserProps.RealName) || user.username + } <${user.getProperty(UserProps.EmailAddress)}>`, // from will be filled in - subject : '2-Factor Authentication Registration', - text : textTemplate, - html : htmlTemplate, + subject: '2-Factor Authentication Registration', + text: textTemplate, + html: htmlTemplate, }; sendMail(message, (err, info) => { - if(err) { - Log.warn({ error : err.message }, 'Failed sending 2FA/OTP register email'); + if (err) { + Log.warn( + { error: err.message }, + 'Failed sending 2FA/OTP register email' + ); } else { - Log.info({ info }, 'Successfully sent 2FA/OTP register email'); + Log.info( + { info }, + 'Successfully sent 2FA/OTP register email' + ); } return callback(err); }); - } + }, ], err => { return cb(err); @@ -128,39 +133,40 @@ module.exports = class User2FA_OTPWebRegister } static routeRegisterGet(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + const webServer = getWebServer(); // must be valid, we just got a req! - const urlParts = url.parse(req.url, true); - const token = urlParts.query && urlParts.query.token; - const otpType = urlParts.query && urlParts.query.otpType; + const urlParts = url.parse(req.url, true); + const token = urlParts.query && urlParts.query.token; + const otpType = urlParts.query && urlParts.query.otpType; - if(!token || !otpType) { + if (!token || !otpType) { return User2FA_OTPWebRegister.accessDenied(webServer, resp); } getTokenInfo(token, (err, tokenInfo) => { - if(err) { + if (err) { // assume expired return webServer.instance.respondWithError( resp, 410, - 'Invalid or expired registration link.', 'Expired Link' + 'Invalid or expired registration link.', + 'Expired Link' ); } - if(tokenInfo.tokenType !== WellKnownTokenTypes.AuthFactor2OTPRegister) { + if (tokenInfo.tokenType !== WellKnownTokenTypes.AuthFactor2OTPRegister) { return User2FA_OTPWebRegister.accessDenied(webServer, resp); } const prepareOptions = { - qrType : 'data', - cellSize : 8, - username : tokenInfo.user.username, + qrType: 'data', + cellSize: 8, + username: tokenInfo.user.username, }; prepareOTP(otpType, prepareOptions, (err, otpInfo) => { - if(err) { - Log.error({ error : err.message }, 'Failed to prepare OTP'); + if (err) { + Log.error({ error: err.message }, 'Failed to prepare OTP'); return User2FA_OTPWebRegister.accessDenied(webServer, resp); } @@ -170,14 +176,13 @@ module.exports = class User2FA_OTPWebRegister _.get(config, 'users.twoFactorAuth.otp.registerPageTemplate'), (templateData, next) => { const finalPage = templateData - .replace(/%BOARDNAME%/g, config.general.boardName) - .replace(/%USERNAME%/g, tokenInfo.user.username) - .replace(/%TOKEN%/g, token) - .replace(/%OTP_TYPE%/g, otpType) - .replace(/%POST_URL%/g, postUrl) - .replace(/%QR_IMG_DATA%/g, otpInfo.qr || '') - .replace(/%SECRET%/g, otpInfo.secret) - ; + .replace(/%BOARDNAME%/g, config.general.boardName) + .replace(/%USERNAME%/g, tokenInfo.user.username) + .replace(/%TOKEN%/g, token) + .replace(/%OTP_TYPE%/g, otpType) + .replace(/%POST_URL%/g, postUrl) + .replace(/%QR_IMG_DATA%/g, otpInfo.qr || '') + .replace(/%SECRET%/g, otpInfo.secret); return next(null, finalPage); }, resp @@ -187,10 +192,15 @@ module.exports = class User2FA_OTPWebRegister } static routeRegisterPost(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + const webServer = getWebServer(); // must be valid, we just got a req! const badRequest = () => { - return webServer.instance.respondWithError(resp, 400, 'Bad Request.', 'Bad Request'); + return webServer.instance.respondWithError( + resp, + 400, + 'Bad Request.', + 'Bad Request' + ); }; let bodyData = ''; @@ -201,45 +211,53 @@ module.exports = class User2FA_OTPWebRegister req.on('end', () => { const formData = querystring.parse(bodyData); - if(!formData.token || !formData.otpType || !formData.otp || - !formData.secret) - { + if ( + !formData.token || + !formData.otpType || + !formData.otp || + !formData.secret + ) { return badRequest(); } const otp = otpFromType(formData.otpType); - if(!otp) { + if (!otp) { return badRequest(); } - const valid = otp.verify( { token : formData.otp, secret : formData.secret } ); - if(!valid) { + const valid = otp.verify({ token: formData.otp, secret: formData.secret }); + if (!valid) { return User2FA_OTPWebRegister.accessDenied(webServer, resp); } getTokenInfo(formData.token, (err, tokenInfo) => { - if(err) { + if (err) { return User2FA_OTPWebRegister.accessDenied(webServer, resp); } const backupCodes = createBackupCodes(); const props = { - [ UserProps.AuthFactor2OTP ] : formData.otpType, - [ UserProps.AuthFactor2OTPSecret ] : formData.secret, - [ UserProps.AuthFactor2OTPBackupCodes ] : JSON.stringify(backupCodes), + [UserProps.AuthFactor2OTP]: formData.otpType, + [UserProps.AuthFactor2OTPSecret]: formData.secret, + [UserProps.AuthFactor2OTPBackupCodes]: JSON.stringify(backupCodes), }; tokenInfo.user.persistProperties(props, err => { - if(err) { - return webServer.instance.respondWithError(resp, 500, 'Internal Server Error', 'Internal Server Error'); + if (err) { + return webServer.instance.respondWithError( + resp, + 500, + 'Internal Server Error', + 'Internal Server Error' + ); } // // User may be online still - find account & update it if so // const clientConn = getConnectionByUserId(tokenInfo.user.userId); - if(clientConn && clientConn.user) { + if (clientConn && clientConn.user) { // just update live props, we've already persisted them. _.each(props, (v, n) => { clientConn.user.setProperty(n, v); @@ -248,8 +266,11 @@ module.exports = class User2FA_OTPWebRegister // we can now remove the token - no need to wait deleteToken(formData.token, err => { - if(err) { - Log.error({error : err.message, token : formData.token}, 'Failed to delete temporary token'); + if (err) { + Log.error( + { error: err.message, token: formData.token }, + 'Failed to delete temporary token' + ); } }); @@ -268,21 +289,21 @@ ${backupCodes} static registerRoutes(cb) { const webServer = getWebServer(); - if(!webServer || !webServer.instance.isEnabled()) { - return cb(null); // no webserver enabled + if (!webServer || !webServer.instance.isEnabled()) { + return cb(null); // no webserver enabled } [ { - method : 'GET', - path : '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9_]+$', - handler : User2FA_OTPWebRegister.routeRegisterGet, + method: 'GET', + path: '^\\/enable_2fa_otp\\?token\\=[a-f0-9]+&otpType\\=[a-zA-Z0-9_]+$', + handler: User2FA_OTPWebRegister.routeRegisterGet, }, { - method : 'POST', - path : '^\\/enable_2fa_otp$', - handler : User2FA_OTPWebRegister.routeRegisterPost, - } + method: 'POST', + path: '^\\/enable_2fa_otp$', + handler: User2FA_OTPWebRegister.routeRegisterPost, + }, ].forEach(r => { webServer.instance.addRoute(r); }); diff --git a/core/user_achievements_earned.js b/core/user_achievements_earned.js index 1004a9d0..4b0f657e 100644 --- a/core/user_achievements_earned.js +++ b/core/user_achievements_earned.js @@ -2,25 +2,23 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const { - getAchievementsEarnedByUser -} = require('./achievement.js'); -const UserProps = require('./user_property.js'); +const { MenuModule } = require('./menu_module.js'); +const { getAchievementsEarnedByUser } = require('./achievement.js'); +const UserProps = require('./user_property.js'); // deps -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); exports.moduleInfo = { - name : 'User Achievements Earned', - desc : 'Lists achievements earned by a user', - author : 'NuSkooler', + name: 'User Achievements Earned', + desc: 'Lists achievements earned by a user', + author: 'NuSkooler', }; const MciViewIds = { - achievementList : 1, - customRangeStart : 10, // updated @ index update + achievementList: 1, + customRangeStart: 10, // updated @ index update }; exports.getModule = class UserAchievementsEarned extends MenuModule { @@ -30,47 +28,60 @@ exports.getModule = class UserAchievementsEarned extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.waterfall( [ - (callback) => { + callback => { this.prepViewController('achievements', 0, mciData.menu, err => { return callback(err); }); }, - (callback) => { - return this.validateMCIByViewIds('achievements', MciViewIds.achievementList, callback); + callback => { + return this.validateMCIByViewIds( + 'achievements', + MciViewIds.achievementList, + callback + ); }, - (callback) => { - return getAchievementsEarnedByUser(this.client.user.userId, callback); + callback => { + return getAchievementsEarnedByUser( + this.client.user.userId, + callback + ); }, (achievementsEarned, callback) => { this.achievementsEarned = achievementsEarned; - const achievementListView = this.viewControllers.achievements.getView(MciViewIds.achievementList); + const achievementListView = + this.viewControllers.achievements.getView( + MciViewIds.achievementList + ); achievementListView.on('index update', idx => { this.selectionIndexUpdate(idx); }); const dateTimeFormat = _.get( - this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateFormat('short')); + this, + 'menuConfig.config.dateTimeFormat', + this.client.currentTheme.helpers.getDateFormat('short') + ); - achievementListView.setItems(achievementsEarned.map(achiev => Object.assign( - achiev, - this.getUserInfo(), - { - ts : achiev.timestamp.format(dateTimeFormat), - } - ))); + achievementListView.setItems( + achievementsEarned.map(achiev => + Object.assign(achiev, this.getUserInfo(), { + ts: achiev.timestamp.format(dateTimeFormat), + }) + ) + ); achievementListView.redraw(); this.selectionIndexUpdate(0); return callback(null); - } + }, ], err => { return cb(err); @@ -82,21 +93,29 @@ exports.getModule = class UserAchievementsEarned extends MenuModule { getUserInfo() { // :TODO: allow args to pass in a different user - ie from user list -> press A for achievs, so on... return { - userId : this.client.user.userId, - userName : this.client.user.username, - realName : this.client.user.getProperty(UserProps.RealName), - location : this.client.user.getProperty(UserProps.Location), - affils : this.client.user.getProperty(UserProps.Affiliations), - totalCount : this.client.user.getPropertyAsNumber(UserProps.AchievementTotalCount), - totalPoints : this.client.user.getPropertyAsNumber(UserProps.AchievementTotalPoints), + userId: this.client.user.userId, + userName: this.client.user.username, + realName: this.client.user.getProperty(UserProps.RealName), + location: this.client.user.getProperty(UserProps.Location), + affils: this.client.user.getProperty(UserProps.Affiliations), + totalCount: this.client.user.getPropertyAsNumber( + UserProps.AchievementTotalCount + ), + totalPoints: this.client.user.getPropertyAsNumber( + UserProps.AchievementTotalPoints + ), }; } selectionIndexUpdate(index) { const achiev = this.achievementsEarned[index]; - if(!achiev) { + if (!achiev) { return; } - this.updateCustomViewTextsWithFilter('achievements', MciViewIds.customRangeStart, achiev); + this.updateCustomViewTextsWithFilter( + 'achievements', + MciViewIds.customRangeStart, + achiev + ); } }; diff --git a/core/user_config.js b/core/user_config.js index 4ccd7017..87c7cebe 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -2,43 +2,41 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const theme = require('./theme.js'); -const sysValidate = require('./system_view_validate.js'); -const UserProps = require('./user_property.js'); -const { - getISOTimestampString -} = require('./database.js'); +const MenuModule = require('./menu_module.js').MenuModule; +const ViewController = require('./view_controller.js').ViewController; +const theme = require('./theme.js'); +const sysValidate = require('./system_view_validate.js'); +const UserProps = require('./user_property.js'); +const { getISOTimestampString } = require('./database.js'); // deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { - name : 'User Configuration', - desc : 'Module for user configuration', - author : 'NuSkooler', + name: 'User Configuration', + desc: 'Module for user configuration', + author: 'NuSkooler', }; const MciCodeIds = { - RealName : 1, - BirthDate : 2, - Sex : 3, - Loc : 4, - Affils : 5, - Email : 6, - Web : 7, - TermHeight : 8, - Theme : 9, - Password : 10, - PassConfirm : 11, - ThemeInfo : 20, - ErrorMsg : 21, + RealName: 1, + BirthDate: 2, + Sex: 3, + Loc: 4, + Affils: 5, + Email: 6, + Web: 7, + TermHeight: 8, + Theme: 9, + Password: 10, + PassConfirm: 11, + ThemeInfo: 20, + ErrorMsg: 21, - SaveCancel : 25, + SaveCancel: 25, }; exports.getModule = class UserConfigModule extends MenuModule { @@ -51,11 +49,14 @@ exports.getModule = class UserConfigModule extends MenuModule { // // Validation support // - validateEmailAvail : function(data, cb) { + validateEmailAvail: function (data, cb) { // // If nothing changed, we know it's OK // - if(self.client.user.properties[UserProps.EmailAddress].toLowerCase() === data.toLowerCase()) { + if ( + self.client.user.properties[UserProps.EmailAddress].toLowerCase() === + data.toLowerCase() + ) { return cb(null); } @@ -63,11 +64,11 @@ exports.getModule = class UserConfigModule extends MenuModule { return sysValidate.validateEmailAvail(data, cb); }, - validatePassword : function(data, cb) { + validatePassword: function (data, cb) { // // Blank is OK - this means we won't be changing it // - if(!data || 0 === data.length) { + if (!data || 0 === data.length) { return cb(null); } @@ -75,19 +76,23 @@ exports.getModule = class UserConfigModule extends MenuModule { return sysValidate.validatePasswordSpec(data, cb); }, - validatePassConfirmMatch : function(data, cb) { + validatePassConfirmMatch: function (data, cb) { var passwordView = self.getMenuView(MciCodeIds.Password); - cb(passwordView.getData() === data ? null : new Error('Passwords do not match')); + cb( + passwordView.getData() === data + ? null + : new Error('Passwords do not match') + ); }, - viewValidationListener : function(err, cb) { + viewValidationListener: function (err, cb) { var errMsgView = self.getMenuView(MciCodeIds.ErrorMsg); var newFocusId; - if(errMsgView) { - if(err) { + if (errMsgView) { + if (err) { errMsgView.setText(err.message); - if(err.view.getId() === MciCodeIds.PassConfirm) { + if (err.view.getId() === MciCodeIds.PassConfirm) { newFocusId = MciCodeIds.Password; var passwordView = self.getMenuView(MciCodeIds.Password); passwordView.clearText(); @@ -103,19 +108,22 @@ exports.getModule = class UserConfigModule extends MenuModule { // // Handlers // - saveChanges : function(formData, extraArgs, cb) { + saveChanges: function (formData, extraArgs, cb) { assert(formData.value.password === formData.value.passwordConfirm); const newProperties = { - [ UserProps.RealName ] : formData.value.realName, - [ UserProps.Birthdate ] : getISOTimestampString(formData.value.birthdate), - [ UserProps.Sex ] : formData.value.sex, - [ UserProps.Location ] : formData.value.location, - [ UserProps.Affiliations ] : formData.value.affils, - [ UserProps.EmailAddress ] : formData.value.email, - [ UserProps.WebAddress ] : formData.value.web, - [ UserProps.TermHeight ] : formData.value.termHeight.toString(), - [ UserProps.ThemeId ] : self.availThemeInfo[formData.value.theme].themeId, + [UserProps.RealName]: formData.value.realName, + [UserProps.Birthdate]: getISOTimestampString( + formData.value.birthdate + ), + [UserProps.Sex]: formData.value.sex, + [UserProps.Location]: formData.value.location, + [UserProps.Affiliations]: formData.value.affils, + [UserProps.EmailAddress]: formData.value.email, + [UserProps.WebAddress]: formData.value.web, + [UserProps.TermHeight]: formData.value.termHeight.toString(), + [UserProps.ThemeId]: + self.availThemeInfo[formData.value.theme].themeId, }; // runtime set theme @@ -123,8 +131,11 @@ exports.getModule = class UserConfigModule extends MenuModule { // persist all changes self.client.user.persistProperties(newProperties, err => { - if(err) { - self.client.log.warn( { error : err.toString() }, 'Failed persisting updated properties'); + if (err) { + self.client.log.warn( + { error: err.toString() }, + 'Failed persisting updated properties' + ); // :TODO: warn end user! return self.prevMenu(cb); } @@ -133,15 +144,23 @@ exports.getModule = class UserConfigModule extends MenuModule { // 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'); + 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); } - return self.prevMenu(cb); - }); + ); } else { return self.prevMenu(cb); } @@ -156,67 +175,121 @@ exports.getModule = class UserConfigModule extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } - const self = this; - const vc = self.viewControllers.menu = new ViewController( { client : self.client} ); + const self = this; + const vc = (self.viewControllers.menu = new ViewController({ + client: self.client, + })); let currentThemeIdIndex = 0; async.series( [ function loadFromConfig(callback) { - vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); + vc.loadFromMenuConfig( + { callingMenu: self, mciMap: mciData.menu }, + callback + ); }, function prepareAvailableThemes(callback) { - self.availThemeInfo = _.sortBy([...theme.getAvailableThemes()].map(entry => { - const theme = entry[1].get(); - 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'); + self.availThemeInfo = _.sortBy( + [...theme.getAvailableThemes()].map(entry => { + const theme = entry[1].get(); + 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[UserProps.ThemeId]; - })); + currentThemeIdIndex = Math.max( + 0, + _.findIndex(self.availThemeInfo, function cmp(ti) { + return ( + ti.themeId === + self.client.user.properties[UserProps.ThemeId] + ); + }) + ); callback(null); }, function populateViews(callback) { const user = self.client.user; - self.setViewText('menu', MciCodeIds.RealName, user.properties[UserProps.RealName]); - self.setViewText('menu', MciCodeIds.BirthDate, moment(user.properties[UserProps.Birthdate]).format('YYYYMMDD')); - self.setViewText('menu', MciCodeIds.Sex, user.properties[UserProps.Sex]); - self.setViewText('menu', MciCodeIds.Loc, user.properties[UserProps.Location]); - self.setViewText('menu', MciCodeIds.Affils, user.properties[UserProps.Affiliations]); - self.setViewText('menu', MciCodeIds.Email, user.properties[UserProps.EmailAddress]); - self.setViewText('menu', MciCodeIds.Web, user.properties[UserProps.WebAddress]); - self.setViewText('menu', MciCodeIds.TermHeight, user.properties[UserProps.TermHeight].toString()); - + self.setViewText( + 'menu', + MciCodeIds.RealName, + user.properties[UserProps.RealName] + ); + self.setViewText( + 'menu', + MciCodeIds.BirthDate, + moment(user.properties[UserProps.Birthdate]).format( + 'YYYYMMDD' + ) + ); + self.setViewText( + 'menu', + MciCodeIds.Sex, + user.properties[UserProps.Sex] + ); + self.setViewText( + 'menu', + MciCodeIds.Loc, + user.properties[UserProps.Location] + ); + self.setViewText( + 'menu', + MciCodeIds.Affils, + user.properties[UserProps.Affiliations] + ); + self.setViewText( + 'menu', + MciCodeIds.Email, + user.properties[UserProps.EmailAddress] + ); + self.setViewText( + 'menu', + MciCodeIds.Web, + user.properties[UserProps.WebAddress] + ); + self.setViewText( + 'menu', + MciCodeIds.TermHeight, + user.properties[UserProps.TermHeight].toString() + ); var themeView = self.getMenuView(MciCodeIds.Theme); - if(themeView) { + if (themeView) { themeView.setItems(_.map(self.availThemeInfo, 'name')); themeView.setFocusItemIndex(currentThemeIdIndex); } var realNameView = self.getMenuView(MciCodeIds.RealName); - if(realNameView) { - realNameView.setFocus(true); // :TODO: HACK! menu.hjson sets focus, but manual population above breaks this. Needs a real fix! + 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'); + 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 4b1548b8..7382d819 100644 --- a/core/user_group.js +++ b/core/user_group.js @@ -1,38 +1,41 @@ /* jslint node: true */ 'use strict'; -const userDb = require('./database.js').dbs.user; +const userDb = require('./database.js').dbs.user; -const async = require('async'); -const _ = require('lodash'); +const async = require('async'); +const _ = require('lodash'); -exports.getGroupsForUser = getGroupsForUser; -exports.addUserToGroup = addUserToGroup; -exports.addUserToGroups = addUserToGroups; -exports.removeUserFromGroup = removeUserFromGroup; +exports.getGroupsForUser = getGroupsForUser; +exports.addUserToGroup = addUserToGroup; +exports.addUserToGroups = addUserToGroups; +exports.removeUserFromGroup = removeUserFromGroup; function getGroupsForUser(userId, cb) { - const sql = - `SELECT group_name + const sql = `SELECT group_name FROM user_group_member WHERE user_id=?;`; 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)) { + if (!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; transOrDb = userDb; } @@ -40,7 +43,7 @@ function addUserToGroup(userId, groupName, transOrDb, cb) { transOrDb.run( `REPLACE INTO user_group_member (group_name, user_id) VALUES(?, ?);`, - [ groupName, userId ], + [groupName, userId], err => { return cb(err); } @@ -48,19 +51,22 @@ function addUserToGroup(userId, groupName, transOrDb, cb) { } 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 WHERE group_name=? AND user_id=?;`, - [ groupName, userId ], + [groupName, userId], err => { return cb(err); } diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index 67b880c8..4bf9a5a0 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -2,37 +2,36 @@ 'use strict'; // ENiGMA½ -const Art = require('./art.js'); -const { - getActiveConnections -} = require('./client_connections.js'); -const ANSI = require('./ansi_term.js'); -const { pipeToAnsi } = require('./color_codes.js'); +const Art = require('./art.js'); +const { getActiveConnections } = require('./client_connections.js'); +const ANSI = require('./ansi_term.js'); +const { pipeToAnsi } = require('./color_codes.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); -module.exports = class UserInterruptQueue -{ +module.exports = class UserInterruptQueue { constructor(client) { - this.client = client; - this.queue = []; + this.client = client; + this.queue = []; } static queue(interruptItem, opts) { opts = opts || {}; - if(!opts.clients) { + if (!opts.clients) { let omitNodes = []; - if(Array.isArray(opts.omit)) { + if (Array.isArray(opts.omit)) { omitNodes = opts.omit; - } else if(opts.omit) { - omitNodes = [ opts.omit ]; + } else if (opts.omit) { + omitNodes = [opts.omit]; } - omitNodes = omitNodes.map(n => _.isNumber(n) ? n : n.node); - opts.clients = getActiveConnections(true).filter(ac => !omitNodes.includes(ac.node)); + omitNodes = omitNodes.map(n => (_.isNumber(n) ? n : n.node)); + opts.clients = getActiveConnections(true).filter( + ac => !omitNodes.includes(ac.node) + ); } - if(!Array.isArray(opts.clients)) { - opts.clients = [ opts.clients ]; + if (!Array.isArray(opts.clients)) { + opts.clients = [opts.clients]; } opts.clients.forEach(c => { c.interruptQueue.queueItem(interruptItem); @@ -40,7 +39,7 @@ module.exports = class UserInterruptQueue } queueItem(interruptItem) { - if(!_.isString(interruptItem.contents) && !_.isString(interruptItem.text)) { + if (!_.isString(interruptItem.contents) && !_.isString(interruptItem.text)) { return; } @@ -48,14 +47,17 @@ module.exports = class UserInterruptQueue interruptItem.pause = _.get(interruptItem, 'pause', true); try { - this.client.currentMenuModule.attemptInterruptNow(interruptItem, (err, ateIt) => { - if(err) { - // :TODO: Log me - } else if(true !== ateIt) { - this.queue.push(interruptItem); + this.client.currentMenuModule.attemptInterruptNow( + interruptItem, + (err, ateIt) => { + if (err) { + // :TODO: Log me + } else if (true !== ateIt) { + this.queue.push(interruptItem); + } } - }); - } catch(e) { + ); + } catch (e) { this.queue.push(interruptItem); } } @@ -65,12 +67,12 @@ module.exports = class UserInterruptQueue } displayNext(options, cb) { - if(!cb && _.isFunction(options)) { + if (!cb && _.isFunction(options)) { cb = options; options = {}; } const interruptItem = this.queue.pop(); - if(!interruptItem) { + if (!interruptItem) { return cb(null); } @@ -79,15 +81,15 @@ module.exports = class UserInterruptQueue } displayWithItem(interruptItem, cb) { - if(interruptItem.cls) { + if (interruptItem.cls) { this.client.term.rawWrite(ANSI.resetScreen()); } else { this.client.term.rawWrite('\r\n\r\n'); } const maybePauseAndFinish = () => { - if(interruptItem.pause) { - this.client.currentMenuModule.pausePrompt( () => { + if (interruptItem.pause) { + this.client.currentMenuModule.pausePrompt(() => { return cb(null); }); } else { @@ -95,18 +97,22 @@ module.exports = class UserInterruptQueue } }; - if(interruptItem.contents) { + if (interruptItem.contents) { Art.display(this.client, interruptItem.contents, err => { - if(err) { + if (err) { return cb(err); } //this.client.term.rawWrite('\r\n\r\n'); // :TODO: Prob optional based on contents vs text maybePauseAndFinish(); }); } else { - this.client.term.write(pipeToAnsi(`${interruptItem.text}\r\n\r\n`, this.client), true, () => { - maybePauseAndFinish(); - }); + this.client.term.write( + pipeToAnsi(`${interruptItem.text}\r\n\r\n`, this.client), + true, + () => { + maybePauseAndFinish(); + } + ); } } -}; \ No newline at end of file +}; diff --git a/core/user_list.js b/core/user_list.js index 3b342fab..fb9f46f1 100644 --- a/core/user_list.js +++ b/core/user_list.js @@ -2,24 +2,24 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); -const { getUserList } = require('./user.js'); -const { Errors } = require('./enig_error.js'); -const UserProps = require('./user_property.js'); +const { MenuModule } = require('./menu_module.js'); +const { getUserList } = require('./user.js'); +const { Errors } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); // deps -const moment = require('moment'); -const async = require('async'); -const _ = require('lodash'); +const moment = require('moment'); +const async = require('async'); +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 { @@ -29,53 +29,71 @@ exports.getModule = class UserListModule extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (next) => { + next => { return this.prepViewController('userList', 0, mciData.menu, next); }, - (next) => { - const userListView = this.viewControllers.userList.getView(MciViewIds.userList); - if(!userListView) { - return cb(Errors.MissingMci(`Missing user list MCI ${MciViewIds.userList}`)); + next => { + const userListView = this.viewControllers.userList.getView( + MciViewIds.userList + ); + if (!userListView) { + return cb( + Errors.MissingMci( + `Missing user list MCI ${MciViewIds.userList}` + ) + ); } const fetchOpts = { - properties : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations, UserProps.LastLoginTs ], - propsCamelCase : true, // e.g. real_name -> realName + properties: [ + UserProps.RealName, + UserProps.Location, + UserProps.Affiliations, + UserProps.LastLoginTs, + ], + propsCamelCase: true, // e.g. real_name -> realName }; getUserList(fetchOpts, (err, userList) => { - if(err) { + if (err) { return next(err); } const dateTimeFormat = _.get( - this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateTimeFormat('short')); + this, + 'menuConfig.config.dateTimeFormat', + this.client.currentTheme.helpers.getDateTimeFormat( + 'short' + ) + ); userList = userList.map(entry => { - return Object.assign( - entry, - { - text : entry.userName, - affils : entry.affiliation, - lastLoginTs : moment(entry.lastLoginTimestamp).format(dateTimeFormat), - } - ); + return Object.assign(entry, { + text: entry.userName, + affils: entry.affiliation, + lastLoginTs: moment(entry.lastLoginTimestamp).format( + dateTimeFormat + ), + }); }); userListView.setItems(userList); userListView.redraw(); return next(null); }); - } + }, ], err => { - if(err) { - this.client.log.error( { error : err.message }, 'Error loading user list'); + if (err) { + this.client.log.error( + { error: err.message }, + 'Error loading user list' + ); } return cb(err); } diff --git a/core/user_log_name.js b/core/user_log_name.js index 77fa996c..6475aa83 100644 --- a/core/user_log_name.js +++ b/core/user_log_name.js @@ -5,18 +5,18 @@ // Common (but not all!) user log names // module.exports = { - NewUser : 'new_user', - Login : 'login', - Logoff : 'logoff', - UlFiles : 'ul_files', // value=count - UlFileBytes : 'ul_file_bytes', // value=total bytes - DlFiles : 'dl_files', // value=count - DlFileBytes : 'dl_file_bytes', // value=total bytes - PostMessage : 'post_msg', // value=areaTag - SendMail : 'send_mail', - RunDoor : 'run_door', // value=doorTag|unknown - RunDoorMinutes : 'run_door_minutes', // value=minutes ran - SendNodeMsg : 'send_node_msg', // value=global|direct - AchievementEarned : 'achievement_earned', // value=achievementTag - AchievementPointsEarned : 'achievement_pts_earned', // value=points earned + NewUser: 'new_user', + Login: 'login', + Logoff: 'logoff', + UlFiles: 'ul_files', // value=count + UlFileBytes: 'ul_file_bytes', // value=total bytes + DlFiles: 'dl_files', // value=count + DlFileBytes: 'dl_file_bytes', // value=total bytes + PostMessage: 'post_msg', // value=areaTag + SendMail: 'send_mail', + RunDoor: 'run_door', // value=doorTag|unknown + RunDoorMinutes: 'run_door_minutes', // value=minutes ran + SendNodeMsg: 'send_node_msg', // value=global|direct + AchievementEarned: 'achievement_earned', // value=achievementTag + AchievementPointsEarned: 'achievement_pts_earned', // value=points earned }; diff --git a/core/user_login.js b/core/user_login.js index 3db6b5cc..d4a2bf0b 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -2,52 +2,49 @@ 'use strict'; // ENiGMA½ -const setClientTheme = require('./theme.js').setClientTheme; +const setClientTheme = require('./theme.js').setClientTheme; const clientConnections = require('./client_connections.js').clientConnections; -const StatLog = require('./stat_log.js'); -const logger = require('./logger.js'); -const Events = require('./events.js'); -const Config = require('./config.js').get; -const { - Errors, - ErrorReasons -} = require('./enig_error.js'); -const UserProps = require('./user_property.js'); -const SysProps = require('./system_property.js'); -const SystemLogKeys = require('./system_log.js'); -const User = require('./user.js'); +const StatLog = require('./stat_log.js'); +const logger = require('./logger.js'); +const Events = require('./events.js'); +const Config = require('./config.js').get; +const { Errors, ErrorReasons } = require('./enig_error.js'); +const UserProps = require('./user_property.js'); +const SysProps = require('./system_property.js'); +const SystemLogKeys = require('./system_log.js'); +const User = require('./user.js'); const { getMessageConferenceByTag, getMessageAreaByTag, getSuitableMessageConfAndAreaTags, -} = require('./message_area.js'); -const { - getFileAreaByTag, - getDefaultFileAreaTag, -} = require('./file_base_area.js'); +} = require('./message_area.js'); +const { getFileAreaByTag, getDefaultFileAreaTag } = require('./file_base_area.js'); // deps -const async = require('async'); -const _ = require('lodash'); -const assert = require('assert'); +const async = require('async'); +const _ = require('lodash'); +const assert = require('assert'); -exports.userLogin = userLogin; -exports.recordLogin = recordLogin; -exports.transformLoginError = transformLoginError; +exports.userLogin = userLogin; +exports.recordLogin = recordLogin; +exports.transformLoginError = transformLoginError; function userLogin(client, username, password, options, cb) { - if(!cb && _.isFunction(options)) { + if (!cb && _.isFunction(options)) { cb = options; options = {}; } const config = Config(); - if(config.users.badUserNames.includes(username.toLowerCase())) { - client.log.info( { username, ip : client.remoteAddress }, 'Attempt to login with banned username'); + if (config.users.badUserNames.includes(username.toLowerCase())) { + client.log.info( + { username, ip: client.remoteAddress }, + 'Attempt to login with banned username' + ); // slow down a bit to thwart brute force attacks - return setTimeout( () => { + return setTimeout(() => { return cb(Errors.BadLogin('Disallowed username', ErrorReasons.NotAllowed)); }, 2000); } @@ -57,11 +54,11 @@ function userLogin(client, username, password, options, cb) { password, }; - authInfo.type = options.authType || User.AuthFactor1Types.Password; + authInfo.type = options.authType || User.AuthFactor1Types.Password; authInfo.pubKey = options.ctx; client.user.authenticateFactor1(authInfo, err => { - if(err) { + if (err) { return cb(transformLoginError(err, client, username)); } @@ -74,50 +71,52 @@ function userLogin(client, username, password, options, cb) { // Ensure this user is not already logged in. // const existingClientConnection = clientConnections.find(cc => { - return user !== cc.user && // not current connection - user.userId === cc.user.userId; // ...but same user + return ( + user !== cc.user && // not current connection + user.userId === cc.user.userId + ); // ...but same user }); - if(existingClientConnection) { + if (existingClientConnection) { client.log.info( { - existingNodeId : existingClientConnection.node, - username : user.username, - userId : user.userId + existingNodeId: existingClientConnection.node, + username: user.username, + userId: user.userId, }, 'Already logged in' ); - return cb(Errors.BadLogin( - `User ${user.username} already logged in.`, - ErrorReasons.AlreadyLoggedIn - )); + return cb( + Errors.BadLogin( + `User ${user.username} already logged in.`, + ErrorReasons.AlreadyLoggedIn + ) + ); } // update client logger with addition of username - client.log = logger.log.child( - { - nodeId : client.log.fields.nodeId, - sessionId : client.log.fields.sessionId, - username : user.username, - } - ); + client.log = logger.log.child({ + nodeId: client.log.fields.nodeId, + 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; // convenience + user.sessionId = client.session.uniqueId; // convenience - Events.emit(Events.getSystemEvents().UserLogin, { user } ); + Events.emit(Events.getSystemEvents().UserLogin, { user }); setClientTheme(client, user.properties[UserProps.ThemeId]); postLoginPrep(client, err => { - if(err) { + if (err) { return cb(err); } - if(user.authenticated) { + if (user.authenticated) { return recordLogin(client, cb); } @@ -130,33 +129,45 @@ function userLogin(client, username, password, options, cb) { function postLoginPrep(client, cb) { async.series( [ - (callback) => { + callback => { // // User may (no longer) have read (view) rights to their current // message, conferences and/or areas. Move them out if so. // - const confTag = client.user.getProperty(UserProps.MessageConfTag); - const conf = getMessageConferenceByTag(confTag) || {}; - const area = getMessageAreaByTag(client.user.getProperty(UserProps.MessageAreaTag), confTag) || {}; + const confTag = client.user.getProperty(UserProps.MessageConfTag); + const conf = getMessageConferenceByTag(confTag) || {}; + const area = + getMessageAreaByTag( + client.user.getProperty(UserProps.MessageAreaTag), + confTag + ) || {}; - if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { + if ( + !client.acs.hasMessageConfRead(conf) || + !client.acs.hasMessageAreaRead(area) + ) { // move them out of both area and possibly conf to something suitable, hopefully. - const [newConfTag, newAreaTag] = getSuitableMessageConfAndAreaTags(client); - client.user.persistProperties({ - [ UserProps.MessageConfTag ] : newConfTag, - [ UserProps.MessageAreaTag ] : newAreaTag, - }, - err => { - return callback(err); - }); + const [newConfTag, newAreaTag] = + getSuitableMessageConfAndAreaTags(client); + client.user.persistProperties( + { + [UserProps.MessageConfTag]: newConfTag, + [UserProps.MessageAreaTag]: newAreaTag, + }, + err => { + return callback(err); + } + ); } else { return callback(null); } }, - (callback) => { + callback => { // Likewise for file areas - const area = getFileAreaByTag(client.user.getProperty(UserProps.FileAreaTag)) || {}; - if(!client.acs.hasFileAreaRead(area)) { + const area = + getFileAreaByTag(client.user.getProperty(UserProps.FileAreaTag)) || + {}; + if (!client.acs.hasFileAreaRead(area)) { const areaTag = getDefaultFileAreaTag(client) || ''; client.user.persistProperty(UserProps.FileAreaTag, areaTag, err => { return callback(err); @@ -164,7 +175,7 @@ function postLoginPrep(client, cb) { } else { return callback(null); } - } + }, ], err => { return cb(err); @@ -173,26 +184,31 @@ function postLoginPrep(client, cb) { } function recordLogin(client, cb) { - assert(client.user.authenticated); // don't get in situations where this isn't true + assert(client.user.authenticated); // don't get in situations where this isn't true const user = client.user; async.parallel( [ - (callback) => { + callback => { StatLog.incrementNonPersistentSystemStat(SysProps.LoginsToday, 1); return StatLog.incrementSystemStat(SysProps.LoginCount, 1, callback); }, - (callback) => { - return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback); + callback => { + return StatLog.setUserStat( + user, + UserProps.LastLoginTs, + StatLog.now, + callback + ); }, - (callback) => { + callback => { return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback); }, - (callback) => { + callback => { const loginHistoryMax = Config().statLog.systemEvents.loginHistoryMax; const historyItem = JSON.stringify({ - userId : user.userId, - sessionId : user.sessionId, + userId: user.userId, + sessionId: user.sessionId, }); return StatLog.appendSystemLogEntry( @@ -202,7 +218,7 @@ function recordLogin(client, cb) { StatLog.KeepType.Max, callback ); - } + }, ], err => { return cb(err); @@ -211,12 +227,16 @@ function recordLogin(client, cb) { } function transformLoginError(err, client, username) { - client.sessionFailedLoginAttempts = _.get(client, 'sessionFailedLoginAttempts', 0) + 1; + client.sessionFailedLoginAttempts = + _.get(client, 'sessionFailedLoginAttempts', 0) + 1; const disconnect = Config().users.failedLogin.disconnect; - if(disconnect > 0 && client.sessionFailedLoginAttempts >= disconnect) { + if (disconnect > 0 && client.sessionFailedLoginAttempts >= disconnect) { err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany); } - client.log.info( { username, ip : client.remoteAddress, reason : err.message }, 'Failed login attempt'); + client.log.info( + { username, ip: client.remoteAddress, reason: err.message }, + 'Failed login attempt' + ); return err; -} \ No newline at end of file +} diff --git a/core/user_property.js b/core/user_property.js index d55a0ebe..5b39762c 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -8,62 +8,60 @@ // can utilize their own properties as well! // module.exports = { - PassPbkdf2Salt : 'pw_pbkdf2_salt', - PassPbkdf2Dk : 'pw_pbkdf2_dk', + PassPbkdf2Salt: 'pw_pbkdf2_salt', + PassPbkdf2Dk: 'pw_pbkdf2_dk', - AccountStatus : 'account_status', // See User.AccountStatus enum + AccountStatus: 'account_status', // See User.AccountStatus enum - RealName : 'real_name', - Sex : 'sex', - Birthdate : 'birthdate', - Location : 'location', - Affiliations : 'affiliation', - EmailAddress : 'email_address', - WebAddress : 'web_address', - TermHeight : 'term_height', - TermWidth : 'term_width', - ThemeId : 'theme_id', - AccountCreated : 'account_created', - LastLoginTs : 'last_login_timestamp', - LoginCount : 'login_count', - UserComment : 'user_comment', // NYI - AutoSignature : 'auto_signature', + RealName: 'real_name', + Sex: 'sex', + Birthdate: 'birthdate', + Location: 'location', + Affiliations: 'affiliation', + EmailAddress: 'email_address', + WebAddress: 'web_address', + TermHeight: 'term_height', + TermWidth: 'term_width', + ThemeId: 'theme_id', + AccountCreated: 'account_created', + LastLoginTs: 'last_login_timestamp', + LoginCount: 'login_count', + UserComment: 'user_comment', // NYI + AutoSignature: 'auto_signature', - DownloadQueue : 'dl_queue', // see download_queue.js + DownloadQueue: 'dl_queue', // see download_queue.js - FailedLoginAttempts : 'failed_login_attempts', - AccountLockedTs : 'account_locked_timestamp', - AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status before lock out + FailedLoginAttempts: 'failed_login_attempts', + AccountLockedTs: 'account_locked_timestamp', + AccountLockedPrevStatus: 'account_locked_prev_status', // previous account status before lock out - EmailPwResetToken : 'email_password_reset_token', - EmailPwResetTokenTs : 'email_password_reset_token_ts', + EmailPwResetToken: 'email_password_reset_token', + EmailPwResetTokenTs: 'email_password_reset_token_ts', - FileAreaTag : 'file_area_tag', - FileBaseFilters : 'file_base_filters', - FileBaseFilterActiveUuid : 'file_base_filter_active_uuid', - FileBaseLastViewedId : 'user_file_base_last_viewed', - FileDlTotalCount : 'dl_total_count', - FileUlTotalCount : 'ul_total_count', - FileDlTotalBytes : 'dl_total_bytes', - FileUlTotalBytes : 'ul_total_bytes', + FileAreaTag: 'file_area_tag', + FileBaseFilters: 'file_base_filters', + FileBaseFilterActiveUuid: 'file_base_filter_active_uuid', + FileBaseLastViewedId: 'user_file_base_last_viewed', + FileDlTotalCount: 'dl_total_count', + FileUlTotalCount: 'ul_total_count', + FileDlTotalBytes: 'dl_total_bytes', + FileUlTotalBytes: 'ul_total_bytes', - MessageConfTag : 'message_conf_tag', - MessageAreaTag : 'message_area_tag', - MessagePostCount : 'post_count', + MessageConfTag: 'message_conf_tag', + MessageAreaTag: 'message_area_tag', + MessagePostCount: 'post_count', - DoorRunTotalCount : 'door_run_total_count', - DoorRunTotalMinutes : 'door_run_total_minutes', + DoorRunTotalCount: 'door_run_total_count', + DoorRunTotalMinutes: 'door_run_total_minutes', - AchievementTotalCount : 'achievement_total_count', - AchievementTotalPoints : 'achievement_total_points', + AchievementTotalCount: 'achievement_total_count', + AchievementTotalPoints: 'achievement_total_points', - MinutesOnlineTotalCount : 'minutes_online_total_count', - - SSHPubKey : 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.) - AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s) - AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes - AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA - AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes + MinutesOnlineTotalCount: 'minutes_online_total_count', + SSHPubKey: 'ssh_public_key', // OpenSSH format (ssh-keygen, etc.) + AuthFactor1Types: 'auth_factor1_types', // List of User.AuthFactor1Types value(s) + AuthFactor2OTP: 'auth_factor2_otp', // If present, OTP type for 2FA. See OTPTypes + AuthFactor2OTPSecret: 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA + AuthFactor2OTPBackupCodes: 'auth_factor2_otp_backup', // JSON array of backup codes }; - diff --git a/core/user_temp_token.js b/core/user_temp_token.js index 89c060d6..29a19edf 100644 --- a/core/user_temp_token.js +++ b/core/user_temp_token.js @@ -2,33 +2,31 @@ 'use strict'; // ENiGMA½ -const UserDb = require('./database.js').dbs.user; -const { - getISOTimestampString -} = require('./database.js'); -const { Errors } = require('./enig_error.js'); -const User = require('./user.js'); -const Log = require('./logger.js').log; +const UserDb = require('./database.js').dbs.user; +const { getISOTimestampString } = require('./database.js'); +const { Errors } = require('./enig_error.js'); +const User = require('./user.js'); +const Log = require('./logger.js').log; // deps -const crypto = require('crypto'); -const async = require('async'); -const moment = require('moment'); +const crypto = require('crypto'); +const async = require('async'); +const moment = require('moment'); -exports.createToken = createToken; -exports.deleteToken = deleteToken; -exports.deleteTokenByUserAndType = deleteTokenByUserAndType; -exports.getTokenInfo = getTokenInfo; -exports.temporaryTokenMaintenanceTask = temporaryTokenMaintenanceTask; +exports.createToken = createToken; +exports.deleteToken = deleteToken; +exports.deleteTokenByUserAndType = deleteTokenByUserAndType; +exports.getTokenInfo = getTokenInfo; +exports.temporaryTokenMaintenanceTask = temporaryTokenMaintenanceTask; exports.WellKnownTokenTypes = { - AuthFactor2OTPRegister : 'auth_factor2_otp_register', + AuthFactor2OTPRegister: 'auth_factor2_otp_register', }; -function createToken(userId, tokenType, options = { bits : 128 }, cb) { +function createToken(userId, tokenType, options = { bits: 128 }, cb) { async.waterfall( [ - (callback) => { + callback => { return crypto.randomBytes(options.bits, callback); }, (token, callback) => { @@ -37,12 +35,12 @@ function createToken(userId, tokenType, options = { bits : 128 }, cb) { UserDb.run( `INSERT OR REPLACE INTO user_temporary_token (user_id, token, token_type, timestamp) VALUES (?, ?, ?, ?);`, - [ userId, token, tokenType, getISOTimestampString() ], + [userId, token, tokenType, getISOTimestampString()], err => { return callback(err, token); } ); - } + }, ], (err, token) => { return cb(err, token); @@ -54,7 +52,7 @@ function deleteToken(token, cb) { UserDb.run( `DELETE FROM user_temporary_token WHERE token = ?;`, - [ token ], + [token], err => { return cb(err); } @@ -65,7 +63,7 @@ function deleteTokenByUserAndType(userId, tokenType, cb) { UserDb.run( `DELETE FROM user_temporary_token WHERE user_id = ? AND token_type = ?;`, - [ userId, tokenType ], + [userId, tokenType], err => { return cb(err); } @@ -75,25 +73,27 @@ function deleteTokenByUserAndType(userId, tokenType, cb) { function getTokenInfo(token, cb) { async.waterfall( [ - (callback) => { + callback => { UserDb.get( `SELECT user_id, token_type, timestamp FROM user_temporary_token WHERE token = ?;`, - [ token ], + [token], (err, row) => { - if(err) { + if (err) { return callback(err); } - if(!row) { - return callback(Errors.DoesNotExist('No entry found for token')); + if (!row) { + return callback( + Errors.DoesNotExist('No entry found for token') + ); } const info = { - userId : row.user_id, - tokenType : row.token_type, - timestamp : moment(row.timestamp), + userId: row.user_id, + tokenType: row.token_type, + timestamp: moment(row.timestamp), }; return callback(null, info); } @@ -104,7 +104,7 @@ function getTokenInfo(token, cb) { info.user = user; return callback(err, info); }); - } + }, ], (err, info) => { return cb(err, info); @@ -115,8 +115,10 @@ function getTokenInfo(token, cb) { function temporaryTokenMaintenanceTask(args, cb) { const tokenType = args[0]; - if(!tokenType) { - return Log.error('Cannot run temporary token maintenance task with out specifying "tokenType" as argument 0'); + if (!tokenType) { + return Log.error( + 'Cannot run temporary token maintenance task with out specifying "tokenType" as argument 0' + ); } const expTime = args[1] || '24 hours'; @@ -129,10 +131,13 @@ function temporaryTokenMaintenanceTask(args, cb) { WHERE token_type = ? AND DATETIME("now") >= DATETIME(timestamp, "+${expTime}") );`, - [ tokenType ], + [tokenType], err => { - if(err) { - Log.warn( { error : err.message, tokenType }, 'Failed deleting user temporary token'); + if (err) { + Log.warn( + { error: err.message, tokenType }, + 'Failed deleting user temporary token' + ); } return cb(err); } diff --git a/core/uuid_util.js b/core/uuid_util.js index f731ecc0..918888a7 100644 --- a/core/uuid_util.js +++ b/core/uuid_util.js @@ -1,7 +1,7 @@ /* jslint node: true */ 'use strict'; -const createHash = require('crypto').createHash; +const createHash = require('crypto').createHash; exports.createNamedUUID = createNamedUUID; @@ -10,29 +10,30 @@ 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)) { + if (!Buffer.isBuffer(namespaceUuid)) { namespaceUuid = Buffer.from(namespaceUuid); } - if(!Buffer.isBuffer(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); // bbbb - bb - bb - bb - bbbbbb - digest.copy(u, 0, 0, 4); // time_low - digest.copy(u, 4, 4, 6); // time_mid - digest.copy(u, 6, 6, 8); // time_hi_and_version + digest.copy(u, 0, 0, 4); // time_low + digest.copy(u, 4, 4, 6); // time_mid + digest.copy(u, 6, 6, 8); // time_hi_and_version - u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) - u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 + u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) + u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 u[9] = digest[9]; digest.copy(u, 10, 10, 16); return u; -} \ No newline at end of file +} diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 1837b718..948b1e94 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -2,21 +2,21 @@ 'use strict'; // ENiGMA½ -const MenuView = require('./menu_view.js').MenuView; -const ansi = require('./ansi_term.js'); -const strUtil = require('./string_util.js'); -const formatString = require('./string_format'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps -const util = require('util'); -const _ = require('lodash'); +const util = require('util'); +const _ = require('lodash'); -exports.VerticalMenuView = VerticalMenuView; +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); @@ -25,57 +25,86 @@ function VerticalMenuView(options) { const self = this; // we want page up/page down by default - if(!_.isObject(options.specialKeyMap)) { + if (!_.isObject(options.specialKeyMap)) { Object.assign(this.specialKeyMap, { - 'page up' : [ 'page up' ], - 'page down' : [ 'page down' ], + 'page up': ['page up'], + 'page down': ['page down'], }); } - this.autoAdjustHeightIfEnabled = function() { - if(this.autoAdjustHeight) { - this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); - this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); + this.autoAdjustHeightIfEnabled = function () { + if (this.autoAdjustHeight) { + this.dimens.height = + this.items.length * (this.itemSpacing + 1) - this.itemSpacing; + this.dimens.height = Math.min( + this.dimens.height, + this.client.term.termHeight - this.position.row + ); } }; this.autoAdjustHeightIfEnabled(); - this.updateViewVisibleItems = function() { + 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, + top: self.focusedItemIndex, + bottom: + Math.min( + self.focusedItemIndex + self.maxVisibleItems, + self.items.length + ) - 1, }; }; - this.drawItem = function(index) { + this.drawItem = function (index) { const item = self.items[index]; - if(!item) { + 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}`); + if (cached) { + return self.client.term.write( + `${ansi.goto(item.row, self.position.col)}${cached}` + ); } let text; let sgr; - if(item.focused && self.hasFocusItems()) { + 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 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 = 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)}`; + 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); }; @@ -83,11 +112,11 @@ function VerticalMenuView(options) { util.inherits(VerticalMenuView, MenuView); -VerticalMenuView.prototype.redraw = function() { +VerticalMenuView.prototype.redraw = function () { VerticalMenuView.super_.prototype.redraw.call(this); // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such - if(this.positionCacheExpired) { + if (this.positionCacheExpired) { this.autoAdjustHeightIfEnabled(); this.updateViewVisibleItems(); @@ -96,13 +125,15 @@ VerticalMenuView.prototype.redraw = function() { // 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; + 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) { + while (row <= endRow) { seq += ansi.goto(row, this.position.col) + blank; row += 1; } @@ -110,9 +141,9 @@ VerticalMenuView.prototype.redraw = function() { delete this.oldDimens; } - if(this.items.length) { + if (this.items.length) { let row = this.position.row; - for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { + 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; @@ -121,55 +152,59 @@ VerticalMenuView.prototype.redraw = function() { } }; -VerticalMenuView.prototype.setHeight = function(height) { +VerticalMenuView.prototype.setHeight = function (height) { VerticalMenuView.super_.prototype.setHeight.call(this, height); this.positionCacheExpired = true; this.autoAdjustHeight = false; }; -VerticalMenuView.prototype.setPosition = function(pos) { +VerticalMenuView.prototype.setPosition = function (pos) { VerticalMenuView.super_.prototype.setPosition.call(this, pos); this.positionCacheExpired = true; }; -VerticalMenuView.prototype.setFocus = function(focused) { +VerticalMenuView.prototype.setFocus = function (focused) { VerticalMenuView.super_.prototype.setFocus.call(this, focused); this.redraw(); }; -VerticalMenuView.prototype.setFocusItemIndex = function(index) { - VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex +VerticalMenuView.prototype.setFocusItemIndex = function (index) { + VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex const remainAfterFocus = this.items.length - index; - if(remainAfterFocus >= this.maxVisibleItems) { + if (remainAfterFocus >= this.maxVisibleItems) { this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + top: this.focusedItemIndex, + bottom: + Math.min( + this.focusedItemIndex + this.maxVisibleItems, + this.items.length + ) - 1, }; - this.positionCacheExpired = false; // skip standard behavior + this.positionCacheExpired = false; // skip standard behavior this.autoAdjustHeightIfEnabled(); } this.redraw(); }; -VerticalMenuView.prototype.onKeyPress = function(ch, key) { - if(key) { - if(this.isKeyMapped('up', key.name)) { +VerticalMenuView.prototype.onKeyPress = function (ch, key) { + if (key) { + if (this.isKeyMapped('up', key.name)) { this.focusPrevious(); - } else if(this.isKeyMapped('down', key.name)) { + } else if (this.isKeyMapped('down', key.name)) { this.focusNext(); - } else if(this.isKeyMapped('page up', key.name)) { + } else if (this.isKeyMapped('page up', key.name)) { this.focusPreviousPageItem(); - } else if(this.isKeyMapped('page down', key.name)) { + } else if (this.isKeyMapped('page down', key.name)) { this.focusNextPageItem(); - } else if(this.isKeyMapped('home', key.name)) { + } else if (this.isKeyMapped('home', key.name)) { this.focusFirst(); - } else if(this.isKeyMapped('end', key.name)) { + } else if (this.isKeyMapped('end', key.name)) { this.focusLast(); } } @@ -177,14 +212,14 @@ VerticalMenuView.prototype.onKeyPress = function(ch, key) { VerticalMenuView.super_.prototype.onKeyPress.call(this, ch, key); }; -VerticalMenuView.prototype.getData = function() { +VerticalMenuView.prototype.getData = function () { const item = this.getItem(this.focusedItemIndex); return _.isString(item.data) ? item.data : this.focusedItemIndex; }; -VerticalMenuView.prototype.setItems = function(items) { +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) { + if (this.items && this.items.length) { this.oldDimens = Object.assign({}, this.dimens); } @@ -193,8 +228,8 @@ VerticalMenuView.prototype.setItems = function(items) { this.positionCacheExpired = true; }; -VerticalMenuView.prototype.removeItem = function(index) { - if(this.items && this.items.length) { +VerticalMenuView.prototype.removeItem = function (index) { + if (this.items && this.items.length) { this.oldDimens = Object.assign({}, this.dimens); } @@ -203,18 +238,18 @@ VerticalMenuView.prototype.removeItem = function(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) { +VerticalMenuView.prototype.focusNext = function () { + if (this.items.length - 1 === this.focusedItemIndex) { this.focusedItemIndex = 0; this.viewWindow = { - top : 0, - bottom : Math.min(this.maxVisibleItems, this.items.length) - 1 + top: 0, + bottom: Math.min(this.maxVisibleItems, this.items.length) - 1, }; } else { this.focusedItemIndex++; - if(this.focusedItemIndex > this.viewWindow.bottom) { + if (this.focusedItemIndex > this.viewWindow.bottom) { this.viewWindow.top++; this.viewWindow.bottom++; } @@ -225,26 +260,28 @@ VerticalMenuView.prototype.focusNext = function() { VerticalMenuView.super_.prototype.focusNext.call(this); }; -VerticalMenuView.prototype.focusPrevious = function() { - if(0 === this.focusedItemIndex) { +VerticalMenuView.prototype.focusPrevious = function () { + if (0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; this.viewWindow = { //top : this.items.length - this.maxVisibleItems, - top : Math.max(this.items.length - this.maxVisibleItems, 0), - bottom : this.items.length - 1 + top: Math.max(this.items.length - this.maxVisibleItems, 0), + bottom: this.items.length - 1, }; - } else { this.focusedItemIndex--; - if(this.focusedItemIndex < this.viewWindow.top) { + 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) { + 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; } } @@ -255,18 +292,18 @@ VerticalMenuView.prototype.focusPrevious = function() { VerticalMenuView.super_.prototype.focusPrevious.call(this); }; -VerticalMenuView.prototype.focusPreviousPageItem = function() { +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 + if (0 === this.focusedItemIndex) { + return this.focusPrevious(); // will jump to bottom } const index = Math.max(this.focusedItemIndex - this.dimens.height, 0); - if(index < this.viewWindow.top) { + if (index < this.viewWindow.top) { this.oldDimens = Object.assign({}, this.dimens); } @@ -275,25 +312,32 @@ VerticalMenuView.prototype.focusPreviousPageItem = function() { return VerticalMenuView.super_.prototype.focusPreviousPageItem.call(this); }; -VerticalMenuView.prototype.focusNextPageItem = function() { +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 + 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) { + if (index > this.viewWindow.bottom) { this.oldDimens = Object.assign({}, this.dimens); this.focusedItemIndex = index; this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + top: this.focusedItemIndex, + bottom: + Math.min( + this.focusedItemIndex + this.maxVisibleItems, + this.items.length + ) - 1, }; this.redraw(); @@ -304,25 +348,29 @@ VerticalMenuView.prototype.focusNextPageItem = function() { return VerticalMenuView.super_.prototype.focusNextPageItem.call(this); }; -VerticalMenuView.prototype.focusFirst = function() { - if(0 < this.viewWindow.top) { +VerticalMenuView.prototype.focusFirst = function () { + if (0 < this.viewWindow.top) { this.oldDimens = Object.assign({}, this.dimens); } this.setFocusItemIndex(0); return VerticalMenuView.super_.prototype.focusFirst.call(this); }; -VerticalMenuView.prototype.focusLast = function() { +VerticalMenuView.prototype.focusLast = function () { const index = this.items.length - 1; - if(index > this.viewWindow.bottom) { + if (index > this.viewWindow.bottom) { this.oldDimens = Object.assign({}, this.dimens); this.focusedItemIndex = index; this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + top: this.focusedItemIndex, + bottom: + Math.min( + this.focusedItemIndex + this.maxVisibleItems, + this.items.length + ) - 1, }; this.redraw(); @@ -333,14 +381,14 @@ VerticalMenuView.prototype.focusLast = function() { return VerticalMenuView.super_.prototype.focusLast.call(this); }; -VerticalMenuView.prototype.setFocusItems = function(items) { +VerticalMenuView.prototype.setFocusItems = function (items) { VerticalMenuView.super_.prototype.setFocusItems.call(this, items); this.positionCacheExpired = true; }; -VerticalMenuView.prototype.setItemSpacing = function(itemSpacing) { +VerticalMenuView.prototype.setItemSpacing = function (itemSpacing) { VerticalMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); this.positionCacheExpired = true; -}; \ No newline at end of file +}; diff --git a/core/view.js b/core/view.js index 1a44d830..d9fbfbf8 100644 --- a/core/view.js +++ b/core/view.js @@ -2,34 +2,34 @@ 'use strict'; // ENiGMA½ -const events = require('events'); -const util = require('util'); -const ansi = require('./ansi_term.js'); -const colorCodes = require('./color_codes.js'); -const enigAssert = require('./enigma_assert.js'); -const { renderSubstr } = require('./string_util.js'); +const events = require('events'); +const util = require('util'); +const ansi = require('./ansi_term.js'); +const colorCodes = require('./color_codes.js'); +const enigAssert = require('./enigma_assert.js'); +const { renderSubstr } = require('./string_util.js'); // deps -const _ = require('lodash'); +const _ = require('lodash'); -exports.View = View; +exports.View = View; const VIEW_SPECIAL_KEY_MAP_DEFAULT = { - accept : [ 'return' ], - exit : [ 'esc' ], - backspace : [ 'backspace', 'del', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ - 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', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ + del: ['del'], + next: ['tab'], + up: ['up arrow'], + down: ['down arrow'], + end: ['end'], + home: ['home'], + left: ['left arrow'], + right: ['right arrow'], + clearLine: ['ctrl + y'], }; -exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT; +exports.VIEW_SPECIAL_KEY_MAP_DEFAULT = VIEW_SPECIAL_KEY_MAP_DEFAULT; function View(options) { events.EventEmitter.call(this); @@ -37,258 +37,284 @@ function View(options) { enigAssert(_.isObject(options)); enigAssert(_.isObject(options.client)); - this.client = options.client; - this.cursor = options.cursor || 'show'; - this.cursorStyle = options.cursorStyle || 'default'; + this.client = options.client; + this.cursor = options.cursor || 'show'; + this.cursorStyle = options.cursorStyle || 'default'; - this.acceptsFocus = options.acceptsFocus || false; - this.acceptsInput = options.acceptsInput || false; - this.autoAdjustHeight = _.get(options, 'dimens.height') ? false : _.get(options, 'autoAdjustHeight', true); - this.position = { x : 0, y : 0 }; - this.textStyle = options.textStyle || 'normal'; - this.focusTextStyle = options.focusTextStyle || this.textStyle; + this.acceptsFocus = options.acceptsFocus || false; + this.acceptsInput = options.acceptsInput || false; + this.autoAdjustHeight = _.get(options, 'dimens.height') + ? false + : _.get(options, 'autoAdjustHeight', true); + this.position = { x: 0, y: 0 }; + this.textStyle = options.textStyle || 'normal'; + this.focusTextStyle = options.focusTextStyle || this.textStyle; - if(options.id) { + if (options.id) { this.setId(options.id); } - if(options.position) { + if (options.position) { this.setPosition(options.position); } - if(options.dimens) { + if (options.dimens) { this.setDimension(options.dimens); } else { this.dimens = { - width : options.width || 0, - height : 0 + width: options.width || 0, + height: 0, }; } // :TODO: Just use styleSGRx for these, e.g. styleSGR0, styleSGR1 = norm/focus - this.ansiSGR = options.ansiSGR || ansi.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); - this.ansiFocusSGR = options.ansiFocusSGR || this.ansiSGR; + 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) { + if (this.acceptsInput) { this.specialKeyMap = options.specialKeyMap || VIEW_SPECIAL_KEY_MAP_DEFAULT; - if(_.isObject(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) { + 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() { + this.hideCusor = function () { this.client.term.rawWrite(ansi.hideCursor()); }; - this.restoreCursor = function() { + this.restoreCursor = function () { //this.client.term.write(ansi.setCursorStyle(this.cursorStyle)); - this.client.term.rawWrite('show' === this.cursor ? ansi.showCursor() : ansi.hideCursor()); + this.client.term.rawWrite( + 'show' === this.cursor ? ansi.showCursor() : ansi.hideCursor() + ); }; - this.initDefaultWidth = function(width = 15) { - this.dimens.width = this.dimens.width || Math.min(width, this.client.term.termWidth - this.position.col); + this.initDefaultWidth = function (width = 15) { + this.dimens.width = + this.dimens.width || + Math.min(width, this.client.term.termWidth - this.position.col); }; } util.inherits(View, events.EventEmitter); -View.prototype.setId = function(id) { +View.prototype.setId = function (id) { this.id = id; }; -View.prototype.getId = function() { +View.prototype.getId = function () { return this.id; }; -View.prototype.setPosition = function(pos) { +View.prototype.setPosition = function (pos) { // // Allow the following forms: [row, col], { row : r, col : c }, or (row, col) // - if(util.isArray(pos)) { + if (util.isArray(pos)) { this.position.row = pos[0]; this.position.col = pos[1]; - } else if(_.isNumber(pos.row) && _.isNumber(pos.col)) { + } else if (_.isNumber(pos.row) && _.isNumber(pos.col)) { this.position.row = pos.row; this.position.col = pos.col; - } else if(2 === arguments.length) { + } 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); + 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)); +View.prototype.setDimension = function (dimens) { + enigAssert( + _.isObject(dimens) && _.isNumber(dimens.height) && _.isNumber(dimens.width) + ); this.dimens = dimens; this.autoAdjustHeight = false; }; -View.prototype.setHeight = function(height) { - height = parseInt(height) || 1; - height = Math.min(height, this.client.term.termHeight); +View.prototype.setHeight = function (height) { + height = parseInt(height) || 1; + height = Math.min(height, this.client.term.termHeight); this.dimens.height = height; this.autoAdjustHeight = false; }; -View.prototype.setWidth = function(width) { - width = parseInt(width) || 1; - width = Math.min(width, this.client.term.termWidth - this.position.col); +View.prototype.setWidth = function (width) { + width = parseInt(width) || 1; + width = Math.min(width, this.client.term.termWidth - this.position.col); this.dimens.width = width; }; -View.prototype.getSGR = function() { +View.prototype.getSGR = function () { return this.ansiSGR; }; -View.prototype.getStyleSGR = function(n) { +View.prototype.getStyleSGR = function (n) { n = parseInt(n) || 0; return this['styleSGR' + n]; }; -View.prototype.getFocusSGR = function() { +View.prototype.getFocusSGR = function () { return this.ansiFocusSGR; }; -View.prototype.setSpecialKeyMapOverride = function(specialKeyMapOverride) { +View.prototype.setSpecialKeyMapOverride = function (specialKeyMapOverride) { this.specialKeyMap = Object.assign(this.specialKeyMap, specialKeyMapOverride); }; -View.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'acceptsFocus' : +View.prototype.setPropertyValue = function (propName, value) { + switch (propName) { + case 'acceptsFocus': if (_.isBoolean(value)) { this.acceptsFocus = value; } break; - case 'height' : this.setHeight(value); break; - case 'width' : this.setWidth(value); break; - case 'focus' : this.setFocusProperty(value); break; + case 'height': + this.setHeight(value); + break; + case 'width': + this.setWidth(value); + break; + case 'focus': + this.setFocusProperty(value); + break; - case 'text' : - if('setText' in this) { + 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)) { + case 'fillChar': + if ('fillChar' in this) { + if (_.isNumber(value)) { this.fillChar = String.fromCharCode(value); - } else if(_.isString(value)) { + } else if (_.isString(value)) { this.fillChar = renderSubstr(value, 0, 1); } } break; - case 'submit' : - if(_.isBoolean(value)) { + case 'submit': + if (_.isBoolean(value)) { this.submit = value; - }/* else { + } /* else { this.submit = _.isArray(value) && value.length > 0; } */ break; - case 'resizable' : - if(_.isBoolean(value)) { + case 'resizable': + if (_.isBoolean(value)) { this.resizable = value; } break; - case 'argName' : this.submitArgName = value; break; + case 'argName': + this.submitArgName = value; + break; - case 'omit' : - if(_.isBoolean(value)) { - this.omitFromSubmission = value; break; + case 'omit': + if (_.isBoolean(value)) { + this.omitFromSubmission = value; + break; } break; - case 'validate' : - if(_.isFunction(value)) { + case 'validate': + if (_.isFunction(value)) { this.validate = value; } break; } - if(/styleSGR[0-9]{1,2}/.test(propName)) { - if(_.isObject(value)) { + if (/styleSGR[0-9]{1,2}/.test(propName)) { + if (_.isObject(value)) { this[propName] = ansi.getSGRFromGraphicRendition(value, true); - } else if(_.isString(value)) { + } else if (_.isString(value)) { this[propName] = colorCodes.pipeToAnsi(value); } } }; -View.prototype.redraw = function() { +View.prototype.redraw = function () { this.client.term.write(ansi.goto(this.position.row, this.position.col)); }; -View.prototype.setFocusProperty = function(focused) { +View.prototype.setFocusProperty = function (focused) { // Either this should accept focus, or the focus should be false enigAssert(this.acceptsFocus || !focused, 'View does not accept focus'); this.hasFocus = focused; }; -View.prototype.setFocus = function(focused) { - // Call separate method to differentiate between a value set as a +View.prototype.setFocus = function (focused) { + // Call separate method to differentiate between a value set as a // property vs focus programmatically called. this.setFocusProperty(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'); +View.prototype.onKeyPress = function (ch, key) { + enigAssert(this.hasFocus, 'View does not have focus'); + enigAssert(this.acceptsInput, 'View does not accept input'); - if(!this.hasFocus || !this.acceptsInput) { + if (!this.hasFocus || !this.acceptsInput) { return; } - if(key) { + if (key) { enigAssert(this.specialKeyMap, 'No special key map defined'); - if(this.isKeyMapped('accept', key.name)) { + if (this.isKeyMapped('accept', key.name)) { this.emit('action', 'accept', key); - } else if(this.isKeyMapped('next', key.name)) { + } else if (this.isKeyMapped('next', key.name)) { this.emit('action', 'next', key); } } - if(ch) { + if (ch) { enigAssert(1 === ch.length); } this.emit('key press', ch, key); }; -View.prototype.getData = function() { -}; +View.prototype.getData = function () {}; diff --git a/core/view_controller.js b/core/view_controller.js index f51c307f..8f00d312 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -2,22 +2,22 @@ 'use strict'; // ENiGMA½ -var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; -var menuUtil = require('./menu_util.js'); -var asset = require('./asset.js'); -var ansi = require('./ansi_term.js'); +var MCIViewFactory = require('./mci_view_factory.js').MCIViewFactory; +var menuUtil = require('./menu_util.js'); +var asset = require('./asset.js'); +var ansi = require('./ansi_term.js'); // deps -var events = require('events'); -var util = require('util'); -var assert = require('assert'); -var async = require('async'); -var _ = require('lodash'); -var paths = require('path'); +var events = require('events'); +var util = require('util'); +var assert = require('assert'); +var async = require('async'); +var _ = require('lodash'); +var paths = require('path'); -exports.ViewController = ViewController; +exports.ViewController = ViewController; -var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/; +var MCI_REGEXP = /([A-Z]{2})([0-9]{1,2})/; function ViewController(options) { assert(_.isObject(options)); @@ -25,92 +25,97 @@ function ViewController(options) { events.EventEmitter.call(this); - var self = this; + var self = this; - this.client = options.client; - this.views = {}; // map of ID -> view - this.formId = options.formId || 0; - this.mciViewFactory = new MCIViewFactory(this.client); // :TODO: can this not be a singleton? - this.noInput = _.isBoolean(options.noInput) ? options.noInput : false; - this.actionKeyMap = {}; + 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 = {}; // // Small wrapper/proxy around handleAction() to ensure we do not allow // input/additional actions queued while performing an action // - this.handleActionWrapper = function(formData, actionBlock, cb) { - if(self.waitActionCompletion) { - if(cb) { + this.handleActionWrapper = function (formData, actionBlock, cb) { + if (self.waitActionCompletion) { + if (cb) { return cb(null); } return; // ignore until this is finished! } - self.client.log.trace( { actionBlock }, 'Action match' ); + self.client.log.trace({ actionBlock }, 'Action match'); self.waitActionCompletion = true; - menuUtil.handleAction(self.client, formData, actionBlock, (err) => { - if(err) { + 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 ); + if ('ALREADYTHERE' === err.reasonCode) { + self.client.log.trace(err.reason); } else { - self.client.log.warn( { err : err }, 'Error during handleAction()'); + self.client.log.warn({ err: err }, 'Error during handleAction()'); } } self.waitActionCompletion = false; - if(cb) { + if (cb) { return cb(null); } }); }; - this.clientKeyPressHandler = function(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)) { + 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() : { }; + } else if (_.isString(actionForKey.action)) { + const formData = self.getFocusedView() ? self.getFormData() : {}; self.handleActionWrapper( - Object.assign( { ch : ch, key : key }, formData ), // formData + key info - actionForKey); // actionBlock + Object.assign({ ch: ch, key: key }, formData), // formData + key info + actionForKey + ); // actionBlock } } else { - if(self.focusedView && self.focusedView.acceptsInput) { + 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 }); + 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) { + 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.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(); @@ -119,23 +124,23 @@ function ViewController(options) { } }; - this.submitForm = function(key) { + this.submitForm = function (key) { self.emit('submit', this.getFormData(key)); }; - this.getLogFriendlyFormData = function(formData) { + this.getLogFriendlyFormData = function (formData) { var safeFormData = _.cloneDeep(formData); - if(safeFormData.value.password) { + if (safeFormData.value.password) { safeFormData.value.password = '*****'; } - if(safeFormData.value.passwordConfirm) { + if (safeFormData.value.passwordConfirm) { safeFormData.value.passwordConfirm = '*****'; } return safeFormData; }; - this.switchFocusEvent = function(event, view) { - if(self.emitSwitchFocus) { + this.switchFocusEvent = function (event, view) { + if (self.emitSwitchFocus) { return; } @@ -144,81 +149,101 @@ function ViewController(options) { 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) { - + this.setViewPropertiesFromMCIConf = function (view, conf) { var propAsset; var propValue; - for(var propName in conf) { + for (var propName in conf) { propAsset = asset.getViewPropertyAsset(conf[propName]); - if(propAsset) { - switch(propAsset.type) { - case 'config' : + if (propAsset) { + switch (propAsset.type) { + case 'config': propValue = asset.resolveConfigAsset(conf[propName]); break; - case 'sysStat' : + 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) { + case 'method': + case 'systemMethod': + if ('validate' === propName) { // :TODO: handle propAsset.location for @method script specification - if('systemMethod' === propAsset.type) { + if ('systemMethod' === propAsset.type) { // :TODO: implementation validation @systemMethod handling! - var methodModule = require(paths.join(__dirname, 'system_view_validate.js')); - if(_.isFunction(methodModule[propAsset.asset])) { + 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]; + if ( + _.isFunction( + self.client.currentMenuModule.menuMethods[ + propAsset.asset + ] + ) + ) { + propValue = + self.client.currentMenuModule.menuMethods[ + propAsset.asset + ]; } } } else { - if(_.isString(propAsset.location)) { + if (_.isString(propAsset.location)) { // :TODO: clean this code up! } else { - if('systemMethod' === propAsset.type) { + if ('systemMethod' === propAsset.type) { // :TODO: } else { // local to current module var currentModule = self.client.currentMenuModule; - if(_.isFunction(currentModule.menuMethods[propAsset.asset])) { + if ( + _.isFunction( + currentModule.menuMethods[propAsset.asset] + ) + ) { // :TODO: Fix formData & extraArgs... this all needs general processing - propValue = currentModule.menuMethods[propAsset.asset]({}, {});//formData, conf.extraArgs); + propValue = currentModule.menuMethods[ + propAsset.asset + ]({}, {}); //formData, conf.extraArgs); } } } } break; - default : + default: propValue = conf[propName]; break; } @@ -226,70 +251,76 @@ function ViewController(options) { propValue = conf[propName]; } - if(!_.isUndefined(propValue)) { + if (!_.isUndefined(propValue)) { view.setPropertyValue(propName, propValue); } } }; - this.applyViewConfig = function(config, cb) { + 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; - } - - 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; - } - - const view = self.getView(viewId); - - if(!view) { - self.client.log.warn( { viewId : viewId }, 'Cannot find view'); - nextItem(null); - return; - } - - const mciConf = config.mci[mci]; - - self.setViewPropertiesFromMCIConf(view, mciConf); - - if(mciConf.focus) { - initialFocusId = 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'); + 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; } - } - return cb(err, { initialFocusId : initialFocusId } ); - }); + 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; + } + + const view = self.getView(viewId); + + if (!view) { + self.client.log.warn({ viewId: viewId }, 'Cannot find view'); + nextItem(null); + return; + } + + const mciConf = config.mci[mci]; + + self.setViewPropertiesFromMCIConf(view, mciConf); + + if (mciConf.focus) { + initialFocusId = 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' + ); + } + } + + return cb(err, { initialFocusId: initialFocusId }); + } + ); }; // method for comparing submitted form data to configuration entries - this.actionBlockValueComparator = function(formValue, actionValue) { + this.actionBlockValueComparator = function (formValue, actionValue) { // // For a match to occur, one of the following must be true: // @@ -302,12 +333,12 @@ function ViewController(options) { // * actionValue is a string: This represents a view with // "argName" set that must be present in formValue. // - if(_.isUndefined(actionValue)) { + if (_.isUndefined(actionValue)) { return false; } - if(_.isNumber(actionValue) || _.isString(actionValue)) { - if(_.isUndefined(formValue[actionValue])) { + if (_.isNumber(actionValue) || _.isString(actionValue)) { + if (_.isUndefined(formValue[actionValue])) { return false; } } else { @@ -319,13 +350,16 @@ function ViewController(options) { } */ var actionValueKeys = Object.keys(actionValue); - for(var i = 0; i < actionValueKeys.length; ++i) { + for (var i = 0; i < actionValueKeys.length; ++i) { var viewId = actionValueKeys[i]; - if(!_.has(formValue, viewId)) { + if (!_.has(formValue, viewId)) { return false; } - if(null !== actionValue[viewId] && actionValue[viewId] !== formValue[viewId]) { + if ( + null !== actionValue[viewId] && + actionValue[viewId] !== formValue[viewId] + ) { return false; } } @@ -333,16 +367,16 @@ function ViewController(options) { return true; }; - if(!options.detached) { + if (!options.detached) { this.attachClientEvents(); } - this.setViewFocusWithEvents = function(view, focused) { - if(!view || !view.acceptsFocus) { + this.setViewFocusWithEvents = function (view, focused) { + if (!view || !view.acceptsFocus) { return; } - if(focused) { + if (focused) { self.switchFocusEvent('return', view); self.focusedView = view; } else { @@ -352,18 +386,22 @@ function ViewController(options) { view.setFocus(focused); }; - this.validateView = function(view, cb) { - if(view && _.isFunction(view.validate)) { + 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 + 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); - }); + viewValidationListener( + err, + function validationComplete(newViewFocusId) { + cb(err, newViewFocusId); + } + ); } else { cb(err); } @@ -376,8 +414,8 @@ function ViewController(options) { util.inherits(ViewController, events.EventEmitter); -ViewController.prototype.attachClientEvents = function() { - if(this.attached) { +ViewController.prototype.attachClientEvents = function () { + if (this.attached) { return; } @@ -394,58 +432,58 @@ ViewController.prototype.attachClientEvents = function() { this.attached = true; }; -ViewController.prototype.detachClientEvents = function() { - if(!this.attached) { +ViewController.prototype.detachClientEvents = function () { + if (!this.attached) { return; } this.client.removeListener('key press', this.clientKeyPressHandler); - for(var id in this.views) { + for (var id in this.views) { this.views[id].removeAllListeners(); } this.attached = false; }; -ViewController.prototype.viewExists = function(id) { +ViewController.prototype.viewExists = function (id) { return id in this.views; }; -ViewController.prototype.addView = function(view) { +ViewController.prototype.addView = function (view) { assert(!this.viewExists(view.id), 'View with ID ' + view.id + ' already exists'); this.views[view.id] = view; }; -ViewController.prototype.getView = function(id) { +ViewController.prototype.getView = function (id) { return this.views[id]; }; -ViewController.prototype.hasView = function(id) { +ViewController.prototype.hasView = function (id) { return this.getView(id) ? true : false; }; -ViewController.prototype.getViewsByMciCode = function(mciCode) { - if(!Array.isArray(mciCode)) { - mciCode = [ mciCode ]; +ViewController.prototype.getViewsByMciCode = function (mciCode) { + if (!Array.isArray(mciCode)) { + mciCode = [mciCode]; } const views = []; _.each(this.views, v => { - if(mciCode.includes(v.mciCode)) { + if (mciCode.includes(v.mciCode)) { views.push(v); } }); return views; }; -ViewController.prototype.getFocusedView = function() { +ViewController.prototype.getFocusedView = function () { return this.focusedView; }; -ViewController.prototype.setFocus = function(focused) { - if(focused) { +ViewController.prototype.setFocus = function (focused) { + if (focused) { this.attachClientEvents(); } else { this.detachClientEvents(); @@ -454,21 +492,21 @@ ViewController.prototype.setFocus = function(focused) { this.setViewFocusWithEvents(this.focusedView, focused); }; -ViewController.prototype.resetInitialFocus = function() { - if(this.formInitialFocusId) { +ViewController.prototype.resetInitialFocus = function () { + if (this.formInitialFocusId) { return this.switchFocus(this.formInitialFocusId); } }; -ViewController.prototype.switchFocus = function(id) { +ViewController.prototype.switchFocus = function (id) { // // Perform focus switching validation now // - var self = this; + var self = this; var focusedView = self.focusedView; self.validateView(focusedView, function validated(err, newFocusedViewId) { - if(err) { + if (err) { var newFocusedView = self.getView(newFocusedViewId) || focusedView; self.setViewFocusWithEvents(newFocusedView, true); } else { @@ -483,28 +521,28 @@ ViewController.prototype.switchFocus = function(id) { }); }; -ViewController.prototype.nextFocus = function() { +ViewController.prototype.nextFocus = function () { let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId]; // find the next view that accepts focus - while(nextFocusView && nextFocusView.nextId) { + while (nextFocusView && nextFocusView.nextId) { nextFocusView = this.getView(nextFocusView.nextId); - if(!nextFocusView || nextFocusView.acceptsFocus) { + if (!nextFocusView || nextFocusView.acceptsFocus) { break; } } - if(nextFocusView && this.focusedView !== nextFocusView) { + if (nextFocusView && this.focusedView !== nextFocusView) { this.switchFocus(nextFocusView.id); } }; -ViewController.prototype.setViewOrder = function(order) { +ViewController.prototype.setViewOrder = function (order) { var viewIdOrder = order || []; - if(0 === viewIdOrder.length) { - for(var id in this.views) { - if(this.views[id].acceptsFocus) { + if (0 === viewIdOrder.length) { + for (var id in this.views) { + if (this.views[id].acceptsFocus) { viewIdOrder.push(id); } } @@ -514,24 +552,25 @@ ViewController.prototype.setViewOrder = function(order) { }); } - if(viewIdOrder.length > 0) { + if (viewIdOrder.length > 0) { var count = viewIdOrder.length - 1; - for(var i = 0; i < count; ++i) { + 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; + var lastId = + viewIdOrder.length > 1 ? viewIdOrder[viewIdOrder.length - 1] : this.firstId; this.views[lastId].nextId = this.firstId; } }; -ViewController.prototype.redrawAll = function(initialFocusId) { +ViewController.prototype.redrawAll = function (initialFocusId) { this.client.term.rawWrite(ansi.hideCursor()); - for(var id in this.views) { - if(initialFocusId === id) { - continue; // will draw @ focus + for (var id in this.views) { + if (initialFocusId === id) { + continue; // will draw @ focus } this.views[id].redraw(); } @@ -539,13 +578,15 @@ ViewController.prototype.redrawAll = function(initialFocusId) { this.client.term.rawWrite(ansi.showCursor()); }; -ViewController.prototype.loadFromPromptConfig = function(options, cb) { +ViewController.prototype.loadFromPromptConfig = function (options, cb) { assert(_.isObject(options)); assert(_.isObject(options.mciMap)); - var self = this; - var promptConfig = _.isObject(options.config) ? options.config : self.client.currentMenuModule.menuConfig.promptConfig; - var initialFocusId = 1; // default to first + var self = this; + var promptConfig = _.isObject(options.config) + ? options.config + : self.client.currentMenuModule.menuConfig.promptConfig; + var initialFocusId = 1; // default to first async.waterfall( [ @@ -555,7 +596,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { }); }, function applyViewConfiguration(callback) { - if(_.isObject(promptConfig.mci)) { + if (_.isObject(promptConfig.mci)) { self.applyViewConfig(promptConfig, function configApplied(err, info) { initialFocusId = info.initialFocusId; callback(err); @@ -565,13 +606,12 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { } }, function prepareFormSubmission(callback) { - if(false === self.noInput) { - + if (false === self.noInput) { self.on('submit', function promptSubmit(formData) { - self.client.log.trace( { formData }, 'Prompt submit'); + self.client.log.trace({ formData }, 'Prompt submit'); const doSubmitNotify = () => { - if(options.submitNotify) { + if (options.submitNotify) { options.submitNotify(); } }; @@ -582,7 +622,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { }); }; - if(_.isString(self.client.currentMenuModule.menuConfig.action)) { + if (_.isString(self.client.currentMenuModule.menuConfig.action)) { handleIt(formData, self.client.currentMenuModule.menuConfig); } else { // @@ -595,23 +635,42 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { // const menuConfig = self.client.currentMenuModule.menuConfig; let submitConf; - if(Array.isArray(menuConfig.submit)) { // standalone prompts)) { + if (Array.isArray(menuConfig.submit)) { + // standalone prompts)) { submitConf = menuConfig.submit; } else { // look for embedded prompt configurations - using their own form ID within the menu submitConf = - _.get(menuConfig, [ 'form', formData.id, 'submit', formData.submitId ]) || - _.get(menuConfig, [ 'form', formData.id, 'submit', '*' ]); + _.get(menuConfig, [ + 'form', + formData.id, + 'submit', + formData.submitId, + ]) || + _.get(menuConfig, [ + 'form', + formData.id, + 'submit', + '*', + ]); } - if(!Array.isArray(submitConf)) { + if (!Array.isArray(submitConf)) { doSubmitNotify(); - return self.client.log.debug('No configuration to handle submit'); + return self.client.log.debug( + 'No configuration to handle submit' + ); } // locate any matching action block - const actionBlock = submitConf.find(actionBlock => _.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)); - if(actionBlock) { + const actionBlock = submitConf.find(actionBlock => + _.isEqualWith( + formData.value, + actionBlock.value, + self.actionBlockValueComparator + ) + ); + if (actionBlock) { handleIt(formData, actionBlock); } else { doSubmitNotify(); @@ -623,7 +682,7 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { callback(null); }, function loadActionKeys(callback) { - if(!_.isObject(promptConfig) || !_.isArray(promptConfig.actionKeys)) { + if (!_.isObject(promptConfig) || !_.isArray(promptConfig.actionKeys)) { return callback(null); } @@ -637,14 +696,13 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { // // Ultimately, create a map of key -> { action block } // - if(!_.isArray(ak.keys)) { + if (!_.isArray(ak.keys)) { return; } ak.keys.forEach(kn => { self.actionKeyMap[kn] = ak; }); - }); return callback(null); @@ -654,11 +712,11 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { callback(null); }, function setInitialViewFocus(callback) { - if(initialFocusId) { + if (initialFocusId) { self.switchFocus(initialFocusId); } callback(null); - } + }, ], function complete(err) { cb(err); @@ -666,17 +724,17 @@ ViewController.prototype.loadFromPromptConfig = function(options, cb) { ); }; -ViewController.prototype.loadFromMenuConfig = function(options, cb) { +ViewController.prototype.loadFromMenuConfig = function (options, cb) { assert(_.isObject(options)); - if(!_.isObject(options.mciMap)) { + 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 self = this; + var formIdKey = options.formId ? options.formId.toString() : '0'; + this.formInitialFocusId = 1; // default to first var formConfig; // :TODO: honor options.withoutForm @@ -684,18 +742,28 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { async.waterfall( [ function findMatchingFormConfig(callback) { - menuUtil.getFormConfigByIDAndMap(self.client.currentMenuModule.menuConfig, formIdKey, options.mciMap, function matchingConfig(err, fc) { - formConfig = fc; + 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); } - - callback(null); - }); + ); }, function createViews(callback) { self.createViewsFromMCI(options.mciMap, function viewsCreated(err) { @@ -727,7 +795,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { }, */ function applyViewConfiguration(callback) { - if(_.isObject(formConfig)) { + if (_.isObject(formConfig)) { self.applyViewConfig(formConfig, function configApplied(err, info) { self.formInitialFocusId = info.initialFocusId; callback(err); @@ -737,28 +805,37 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { } }, function prepareFormSubmission(callback) { - if(!_.isObject(formConfig) || !_.isObject(formConfig.submit)) { + if (!_.isObject(formConfig) || !_.isObject(formConfig.submit)) { callback(null); return; } self.on('submit', function formSubmit(formData) { - self.client.log.trace( { formData }, 'Form submit'); + self.client.log.trace({ formData }, 'Form submit'); // // Locate configuration for this form ID // const confForFormId = - _.get(formConfig, [ 'submit', formData.submitId ]) || - _.get(formConfig, [ 'submit', '*' ]); + _.get(formConfig, ['submit', formData.submitId]) || + _.get(formConfig, ['submit', '*']); - if(!Array.isArray(confForFormId)) { - return self.client.log.debug( { formId : formData.submitId }, 'No configuration for form ID'); + if (!Array.isArray(confForFormId)) { + return self.client.log.debug( + { formId: formData.submitId }, + 'No configuration for form ID' + ); } // locate a matching action block, if any - const actionBlock = confForFormId.find(actionBlock => _.isEqualWith(formData.value, actionBlock.value, self.actionBlockValueComparator)); - if(actionBlock) { + const actionBlock = confForFormId.find(actionBlock => + _.isEqualWith( + formData.value, + actionBlock.value, + self.actionBlockValueComparator + ) + ); + if (actionBlock) { self.handleActionWrapper(formData, actionBlock); } }); @@ -766,7 +843,7 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { callback(null); }, function loadActionKeys(callback) { - if(!_.isObject(formConfig) || !_.isArray(formConfig.actionKeys)) { + if (!_.isObject(formConfig) || !_.isArray(formConfig.actionKeys)) { callback(null); return; } @@ -781,14 +858,13 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { // // Ultimately, create a map of key -> { action block } // - if(!_.isArray(ak.keys)) { + if (!_.isArray(ak.keys)) { return; } ak.keys.forEach(function actionKeyName(kn) { self.actionKeyMap[kn] = ak; }); - }); callback(null); @@ -798,28 +874,28 @@ ViewController.prototype.loadFromMenuConfig = function(options, cb) { callback(null); }, function setInitialViewFocus(callback) { - if(self.formInitialFocusId) { + if (self.formInitialFocusId) { self.switchFocus(self.formInitialFocusId); } callback(null); - } + }, ], function complete(err) { - if(_.isFunction(cb)) { + if (_.isFunction(cb)) { cb(err); } } ); }; -ViewController.prototype.formatMCIString = function(format) { +ViewController.prototype.formatMCIString = function (format) { var self = this; var view; return format.replace(/{(\d+)}/g, function replacer(match, number) { view = self.getView(number); - if(!view) { + if (!view) { return match; } @@ -827,7 +903,7 @@ ViewController.prototype.formatMCIString = function(format) { }); }; -ViewController.prototype.getFormData = function(key) { +ViewController.prototype.getFormData = function (key) { /* Example form data: { @@ -843,12 +919,12 @@ ViewController.prototype.getFormData = function(key) { } */ const formData = { - id : this.formId, - submitId : this.focusedView.id, - value : {}, + id: this.formId, + submitId: this.focusedView.id, + value: {}, }; - if(key) { + if (key) { formData.key = key; } @@ -856,23 +932,26 @@ ViewController.prototype.getFormData = function(key) { _.each(this.views, view => { try { // don't fill forms with static, non user-editable data data - if(!view.acceptsInput) { + if (!view.acceptsInput) { return; } // some form values may be omitted from submission all together - if(view.omitFromSubmission) { + if (view.omitFromSubmission) { return; } viewData = view.getData(); - if(_.isUndefined(viewData)) { + 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' + ); } }); diff --git a/core/web_password_reset.js b/core/web_password_reset.js index 89c3fd33..085eab64 100644 --- a/core/web_password_reset.js +++ b/core/web_password_reset.js @@ -2,26 +2,25 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').get; -const Errors = require('./enig_error.js').Errors; -const getServer = require('./listening_server.js').getServer; -const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; -const User = require('./user.js'); -const userDb = require('./database.js').dbs.user; +const Config = require('./config.js').get; +const Errors = require('./enig_error.js').Errors; +const getServer = require('./listening_server.js').getServer; +const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; +const User = require('./user.js'); +const userDb = require('./database.js').dbs.user; const getISOTimestampString = require('./database.js').getISOTimestampString; -const Log = require('./logger.js').log; -const UserProps = require('./user_property.js'); +const Log = require('./logger.js').log; +const UserProps = require('./user_property.js'); // deps -const async = require('async'); -const crypto = require('crypto'); -const fs = require('graceful-fs'); -const url = require('url'); -const querystring = require('querystring'); -const _ = require('lodash'); +const async = require('async'); +const crypto = require('crypto'); +const fs = require('graceful-fs'); +const url = require('url'); +const querystring = require('querystring'); +const _ = require('lodash'); -const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = - `%USERNAME%: +const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT = `%USERNAME%: A password reset has been requested for your account on %BOARDNAME%. * If this was not you, please ignore this email. @@ -33,34 +32,37 @@ function getWebServer() { } class WebPasswordReset { - static startup(cb) { - WebPasswordReset.registerRoutes( err => { + WebPasswordReset.registerRoutes(err => { return cb(err); }); } static sendForgotPasswordEmail(username, cb) { const webServer = getServer(webServerPackageName); - if(!webServer || !webServer.instance.isEnabled()) { + if (!webServer || !webServer.instance.isEnabled()) { return cb(Errors.General('Web server is not enabled')); } async.waterfall( [ function getEmailAddress(callback) { - if(!username) { + if (!username) { return callback(Errors.MissingParam('Missing "username"')); } User.getUserIdAndName(username, (err, userId) => { - if(err) { + if (err) { return callback(err); } User.getUser(userId, (err, user) => { - if(err || !user.properties[UserProps.EmailAddress]) { - return callback(Errors.DoesNotExist('No email address associated with this user')); + if (err || !user.properties[UserProps.EmailAddress]) { + return callback( + Errors.DoesNotExist( + 'No email address associated with this user' + ) + ); } return callback(null, user); @@ -72,15 +74,15 @@ class WebPasswordReset { // Reset "token" is simply HEX encoded cryptographically generated bytes // crypto.randomBytes(256, (err, token) => { - if(err) { + if (err) { return callback(err); } token = token.toString('hex'); const newProperties = { - [ UserProps.EmailPwResetToken ] : token, - [ UserProps.EmailPwResetTokenTs ] : getISOTimestampString(), + [UserProps.EmailPwResetToken]: token, + [UserProps.EmailPwResetTokenTs]: getISOTimestampString(), }; // we simply place the reset token in the user's properties @@ -88,57 +90,84 @@ class WebPasswordReset { 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; - } + 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); - }); - }); + 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[UserProps.EmailPwResetToken]}`); + const resetUrl = webServer.instance.buildUrl( + `/reset_password?token=${ + user.properties[UserProps.EmailPwResetToken] + }` + ); function replaceTokens(s) { return s - .replace(/%BOARDNAME%/g, Config().general.boardName) - .replace(/%USERNAME%/g, user.username) - .replace(/%TOKEN%/g, user.properties[UserProps.EmailPwResetToken]) - .replace(/%RESET_URL%/g, resetUrl) - ; + .replace(/%BOARDNAME%/g, Config().general.boardName) + .replace(/%USERNAME%/g, user.username) + .replace( + /%TOKEN%/g, + user.properties[UserProps.EmailPwResetToken] + ) + .replace(/%RESET_URL%/g, resetUrl); } textTemplate = replaceTokens(textTemplate); - if(htmlTemplate) { + if (htmlTemplate) { htmlTemplate = replaceTokens(htmlTemplate); } const message = { - to : `${user.properties[UserProps.RealName]||user.username} <${user.properties[UserProps.EmailAddress]}>`, + to: `${user.properties[UserProps.RealName] || user.username} <${ + user.properties[UserProps.EmailAddress] + }>`, // from will be filled in - subject : 'Forgot Password', - text : textTemplate, - html : htmlTemplate, + subject: 'Forgot Password', + text: textTemplate, + html: htmlTemplate, }; sendMail(message, (err, info) => { - if(err) { - Log.warn( { error : err.message }, 'Failed sending password reset email' ); + if (err) { + Log.warn( + { error: err.message }, + 'Failed sending password reset email' + ); } else { - Log.info( { info : info }, 'Successfully sent password reset email'); + Log.info( + { info: info }, + 'Successfully sent password reset email' + ); } return callback(err); }); - } + }, ], err => { return cb(err); @@ -153,27 +182,27 @@ class WebPasswordReset { static registerRoutes(cb) { const webServer = getWebServer(); - if(!webServer) { - return cb(null); // no webserver enabled + 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, + 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, - } + method: 'POST', + path: '^\\/reset_password$', + handler: WebPasswordReset.routeResetPasswordPost, + }, ].forEach(r => { webServer.instance.addRoute(r); }); @@ -181,7 +210,6 @@ class WebPasswordReset { return cb(null); } - static fileNotFound(webServer, resp) { return webServer.instance.fileNotFound(resp); } @@ -194,13 +222,19 @@ class WebPasswordReset { async.waterfall( [ function validateToken(callback) { - User.getUserIdsWithProperty('email_password_reset_token', token, (err, userIds) => { - if(userIds && userIds.length === 1) { - return callback(null, userIds[0]); - } + 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')); - }); + return callback( + Errors.Invalid('Invalid password reset token') + ); + } + ); }, function getUser(userId, callback) { User.getUser(userId, (err, user) => { @@ -215,19 +249,24 @@ class WebPasswordReset { } static routeResetPasswordGet(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + const webServer = getWebServer(); // must be valid, we just got a req! - const urlParts = url.parse(req.url, true); - const token = urlParts.query && urlParts.query.token; + const urlParts = url.parse(req.url, true); + const token = urlParts.query && urlParts.query.token; - if(!token) { + if (!token) { return WebPasswordReset.accessDenied(webServer, resp); } WebPasswordReset.getUserByToken(token, (err, user) => { - if(err) { + if (err) { // assume it's expired - return webServer.instance.respondWithError(resp, 410, 'Invalid or expired reset link.', 'Expired Link'); + return webServer.instance.respondWithError( + resp, + 410, + 'Invalid or expired reset link.', + 'Expired Link' + ); } const postResetUrl = webServer.instance.buildUrl('/reset_password'); @@ -236,14 +275,11 @@ class WebPasswordReset { 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) - ; - + .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 @@ -252,7 +288,7 @@ class WebPasswordReset { } static routeResetPasswordPost(req, resp) { - const webServer = getWebServer(); // must be valid, we just got a req! + const webServer = getWebServer(); // must be valid, we just got a req! let bodyData = ''; req.on('data', data => { @@ -260,39 +296,53 @@ class WebPasswordReset { }); function badRequest() { - return webServer.instance.respondWithError(resp, 400, 'Bad Request.', 'Bad Request'); + return webServer.instance.respondWithError( + resp, + 400, + 'Bad Request.', + 'Bad Request' + ); } req.on('end', () => { const formData = querystring.parse(bodyData); const config = Config(); - if(!formData.token || !formData.password || !formData.confirm_password || + if ( + !formData.token || + !formData.password || + !formData.confirm_password || formData.password !== formData.confirm_password || - formData.password.length < config.users.passwordMin || formData.password.length > config.users.passwordMax) - { + formData.password.length < config.users.passwordMin || + formData.password.length > config.users.passwordMax + ) { return badRequest(); } WebPasswordReset.getUserByToken(formData.token, (err, user) => { - if(err) { + if (err) { return badRequest(); } user.setNewAuthCredentials(formData.password, err => { - if(err) { + if (err) { return badRequest(); } // delete assoc properties - no need to wait for completion - user.removeProperties([ UserProps.EmailPwResetToken, UserProps.EmailPwResetTokenTs ]); + user.removeProperties([ + UserProps.EmailPwResetToken, + UserProps.EmailPwResetTokenTs, + ]); - if(true === _.get(config, 'users.unlockAtEmailPwReset')) { + if (true === _.get(config, 'users.unlockAtEmailPwReset')) { Log.info( - { username : user.username, userId : user.userId }, + { username: user.username, userId: user.userId }, 'Remove any lock on account due to password reset policy' ); - user.unlockAccount( () => { /* dummy */ } ); + user.unlockAccount(() => { + /* dummy */ + }); } resp.writeHead(200); @@ -304,7 +354,6 @@ class WebPasswordReset { } function performMaintenanceTask(args, cb) { - const forgotPassExpireTime = args[0] || '24 hours'; // remove all reset token associated properties older than |forgotPassExpireTime| @@ -317,13 +366,16 @@ function performMaintenanceTask(args, cb) { 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'); + if (err) { + Log.warn( + { error: err.message }, + 'Failed deleting old email reset tokens' + ); } return cb(err); } ); } -exports.WebPasswordReset = WebPasswordReset; -exports.performMaintenanceTask = performMaintenanceTask; \ No newline at end of file +exports.WebPasswordReset = WebPasswordReset; +exports.performMaintenanceTask = performMaintenanceTask; diff --git a/core/whos_online.js b/core/whos_online.js index 5910bd29..0ce3a63f 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -2,23 +2,23 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); +const { MenuModule } = require('./menu_module.js'); const { getActiveConnectionList } = require('./client_connections.js'); -const { Errors } = require('./enig_error.js'); +const { Errors } = require('./enig_error.js'); // deps -const async = require('async'); -const _ = require('lodash'); +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 { @@ -28,33 +28,47 @@ exports.getModule = class WhosOnlineModule extends MenuModule { mciReady(mciData, cb) { super.mciReady(mciData, err => { - if(err) { + if (err) { return cb(err); } async.series( [ - (next) => { + next => { return this.prepViewController('online', 0, mciData.menu, next); }, - (next) => { - const onlineListView = this.viewControllers.online.getView(MciViewIds.onlineList); - if(!onlineListView) { - return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.onlineList}`)); + next => { + const onlineListView = this.viewControllers.online.getView( + MciViewIds.onlineList + ); + if (!onlineListView) { + return cb( + Errors.MissingMci( + `Missing online list MCI ${MciViewIds.onlineList}` + ) + ); } - const onlineList = getActiveConnectionList(true).slice(0, onlineListView.height).map( - oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) }) - ); + const onlineList = getActiveConnectionList(true) + .slice(0, onlineListView.height) + .map(oe => + Object.assign(oe, { + text: oe.userName, + timeOn: _.upperFirst(oe.timeOn.humanize()), + }) + ); onlineListView.setItems(onlineList); onlineListView.redraw(); return next(null); - } + }, ], err => { - if(err) { - this.client.log.error( { error : err.message }, 'Error loading who\'s online'); + if (err) { + this.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 afeca1f8..f88ef2f4 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -1,21 +1,39 @@ /* jslint node: true */ 'use strict'; -const { - ansiRenderStringLength, -} = require('./string_util'); +const { ansiRenderStringLength } = require('./string_util'); // deps -const assert = require('assert'); -const _ = require('lodash'); +const assert = require('assert'); +const _ = require('lodash'); -exports.wordWrapText = wordWrapText; +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', +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', ]; const REGEXP_WORD_WRAP = new RegExp(`\t|[${SPACE_CHARS.join('')}]`, 'g'); @@ -25,8 +43,8 @@ function wordWrapText(text, options) { assert(_.isNumber(options.width)); options.tabHandling = options.tabHandling || 'expand'; - options.tabWidth = options.tabWidth || 4; - options.tabChar = options.tabChar || ' '; + options.tabWidth = options.tabWidth || 4; + options.tabChar = options.tabChar || ' '; //const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g'); // @@ -34,15 +52,18 @@ function wordWrapText(text, options) { // 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}}\\x1b\\[[\\?=;0-9]*[ABCDEFGHJKLMSTfhlmnprsu]|.{0,${options.width}}`, + 'g' + ); let m; let word; let c; let renderLen; - let i = 0; - let wordStart = 0; - let result = { wrapped : [ '' ], renderLen : [ 0 ] }; + let i = 0; + let wordStart = 0; + let result = { wrapped: [''], renderLen: [0] }; function expandTab(column) { const remainWidth = options.tabWidth - (column % options.tabWidth); @@ -50,18 +71,21 @@ function wordWrapText(text, options) { } function appendWord() { - word.match(REGEXP_GOBBLE).forEach( w => { + word.match(REGEXP_GOBBLE).forEach(w => { renderLen = ansiRenderStringLength(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.wrapped[i] += w; result.renderLen[i] = (result.renderLen[i] || 0) + renderLen; } }); @@ -79,16 +103,17 @@ function wordWrapText(text, options) { // // * If a word is ultimately too long to fit, break it up until it does. // - while(null !== (m = REGEXP_WORD_WRAP.exec(text))) { + 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) { + if (SPACE_CHARS.indexOf(c) > -1) { word += m[0]; - } else if('\t' === c) { - if('expand' === options.tabHandling) { + } 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; + word += + expandTab(result.wrapped[i].length + word.length) + options.tabChar; } else { word += m[0]; } diff --git a/main.js b/main.js index 53a320f6..663ba875 100755 --- a/main.js +++ b/main.js @@ -9,4 +9,4 @@ If this file does not run directly, ensure it's executable: > chmod u+x main.js */ -require('./core/bbs.js').main(); \ No newline at end of file +require('./core/bbs.js').main(); diff --git a/package.json b/package.json index fca97f9e..b0610a63 100644 --- a/package.json +++ b/package.json @@ -1,70 +1,70 @@ { - "name": "enigma-bbs", - "version": "0.0.13-beta", - "description": "ENiGMA½ Bulletin Board System", - "author": "Bryan Ashby ", - "license": "BSD-2-Clause", - "scripts": { - "start": "node main.js" - }, - "repository": { - "type": "git", - "url": "https://github.com/NuSkooler/enigma-bbs.git" - }, - "homepage": "https://github.com/NuSkooler/enigma-bbs", - "bugs": { - "url": "https://github.com/NuSkooler/enigma-bbs/issues" - }, - "keywords": [ - "bbs", - "telnet", - "ssh", - "retro" - ], - "dependencies": { - "@breejs/later": "4.1.0", - "async": "^3.2.3", - "binary-parser": "2.0.2", - "buffers": "github:NuSkooler/node-buffers", - "bunyan": "1.8.15", - "deepdash": "^5.3.9", - "exiftool": "^0.0.3", - "fs-extra": "^10.0.1", - "glob": "^7.2.0", - "graceful-fs": "^4.2.10", - "hashids": "^2.2.10", - "hjson": "3.2.2", - "iconv-lite": "0.6.3", - "ini-config-parser": "^1.0.4", - "inquirer": "^8.2.2", - "lodash": "4.17.21", - "lru-cache": "^7.8.0", - "mime-types": "^2.1.35", - "minimist": "^1.2.6", - "moment": "^2.29.2", - "nntp-server": "^1.0.3", - "node-pty": "0.10.1", - "nodemailer": "^6.7.3", - "otplib": "11.0.1", - "qrcode-generator": "^1.4.4", - "rlogin": "^1.0.0", - "sane": "5.0.1", - "sanitize-filename": "^1.6.3", - "sqlite3": "^4.2.0", - "sqlite3-trans": "^1.2.2", - "ssh2": "^1.9.0", - "telnet-socket": "^0.2.3", - "temptmp": "^1.1.0", - "uuid": "8.3.2", - "uuid-parse": "1.1.0", - "ws": "7.4.3", - "yazl": "^2.5.1" - }, - "devDependencies": { - "eslint": "^8.13.0", - "prettier": "2.6.2" - }, - "engines": { - "node": ">=14" - } + "name": "enigma-bbs", + "version": "0.0.13-beta", + "description": "ENiGMA½ Bulletin Board System", + "author": "Bryan Ashby ", + "license": "BSD-2-Clause", + "scripts": { + "start": "node main.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/NuSkooler/enigma-bbs.git" + }, + "homepage": "https://github.com/NuSkooler/enigma-bbs", + "bugs": { + "url": "https://github.com/NuSkooler/enigma-bbs/issues" + }, + "keywords": [ + "bbs", + "telnet", + "ssh", + "retro" + ], + "dependencies": { + "@breejs/later": "4.1.0", + "async": "^3.2.3", + "binary-parser": "2.0.2", + "buffers": "github:NuSkooler/node-buffers", + "bunyan": "1.8.15", + "deepdash": "^5.3.9", + "exiftool": "^0.0.3", + "fs-extra": "^10.0.1", + "glob": "^7.2.0", + "graceful-fs": "^4.2.10", + "hashids": "^2.2.10", + "hjson": "3.2.2", + "iconv-lite": "0.6.3", + "ini-config-parser": "^1.0.4", + "inquirer": "^8.2.2", + "lodash": "4.17.21", + "lru-cache": "^7.8.0", + "mime-types": "^2.1.35", + "minimist": "^1.2.6", + "moment": "^2.29.2", + "nntp-server": "^1.0.3", + "node-pty": "0.10.1", + "nodemailer": "^6.7.3", + "otplib": "11.0.1", + "qrcode-generator": "^1.4.4", + "rlogin": "^1.0.0", + "sane": "5.0.1", + "sanitize-filename": "^1.6.3", + "sqlite3": "^4.2.0", + "sqlite3-trans": "^1.2.2", + "ssh2": "^1.9.0", + "telnet-socket": "^0.2.3", + "temptmp": "^1.1.0", + "uuid": "8.3.2", + "uuid-parse": "1.1.0", + "ws": "7.4.3", + "yazl": "^2.5.1" + }, + "devDependencies": { + "eslint": "^8.13.0", + "prettier": "2.6.2" + }, + "engines": { + "node": ">=14" + } } diff --git a/util/dump_ftn_packet.js b/util/dump_ftn_packet.js index 88eeece8..e988613c 100755 --- a/util/dump_ftn_packet.js +++ b/util/dump_ftn_packet.js @@ -9,48 +9,50 @@ const { Packet } = require('../core/ftn_mail_packet.js'); const argv = require('minimist')(process.argv.slice(2)); function main() { - if(0 === argv._.length) { - console.error('usage: dump_ftn_packet.js PATH'); - process.exitCode = -1; - return; - } + if (0 === argv._.length) { + console.error('usage: dump_ftn_packet.js PATH'); + process.exitCode = -1; + return; + } - const packet = new Packet(); - const packetPath = argv._[0]; + const packet = new Packet(); + const packetPath = argv._[0]; - packet.read( - packetPath, - (dataType, data, next) => { - if('header' === dataType) { - console.info('--- header ---'); - console.info(`Created : ${data.created.format('dddd, MMMM Do YYYY, h:mm:ss a')}`); - console.info(`Dst. Addr : ${data.destAddress.toString()}`); - console.info(`Src. Addr : ${data.origAddress.toString()}`); - console.info('--- raw header ---'); - console.info(data); - console.info('--------------'); - console.info(''); - } else if('message' === dataType) { - console.info('--- message ---'); - console.info(`To : ${data.toUserName}`); - console.info(`From : ${data.fromUserName}`); - console.info(`Subject : ${data.subject}`); - console.info('--- raw message ---'); - console.info(data); - console.info('---------------'); - } + packet.read( + packetPath, + (dataType, data, next) => { + if ('header' === dataType) { + console.info('--- header ---'); + console.info( + `Created : ${data.created.format('dddd, MMMM Do YYYY, h:mm:ss a')}` + ); + console.info(`Dst. Addr : ${data.destAddress.toString()}`); + console.info(`Src. Addr : ${data.origAddress.toString()}`); + console.info('--- raw header ---'); + console.info(data); + console.info('--------------'); + console.info(''); + } else if ('message' === dataType) { + console.info('--- message ---'); + console.info(`To : ${data.toUserName}`); + console.info(`From : ${data.fromUserName}`); + console.info(`Subject : ${data.subject}`); + console.info('--- raw message ---'); + console.info(data); + console.info('---------------'); + } - return next(null); - }, - (err) => { - if(err) { - return console.error(`Error processing packet: ${err.message}`); - } - console.info(''); - console.info('--- EOF --- '); - console.info(''); - } - ); + return next(null); + }, + err => { + if (err) { + return console.error(`Error processing packet: ${err.message}`); + } + console.info(''); + console.info('--- EOF --- '); + console.info(''); + } + ); } main(); diff --git a/util/exiftool2desc.js b/util/exiftool2desc.js index 5299bcb5..582f99a5 100755 --- a/util/exiftool2desc.js +++ b/util/exiftool2desc.js @@ -6,27 +6,48 @@ // :TODO: Make this it's own sep tool/repo -const exiftool = require('exiftool'); -const fs = require('graceful-fs'); -const moment = require('moment'); +const exiftool = require('exiftool'); +const fs = require('graceful-fs'); +const moment = require('moment'); -const TOOL_VERSION = '1.0.0.0'; +const TOOL_VERSION = '1.0.0.0'; // map fileTypes -> handlers const FILETYPE_HANDLERS = {}; -[ 'AIFF', 'APE', 'FLAC', 'OGG', 'MP3' ].forEach(ext => FILETYPE_HANDLERS[ext] = audioFile); -[ 'PDF', 'DOC', 'DOCX', 'DOCM', 'ODB', 'ODC', 'ODF', 'ODG', 'ODI', 'ODP', 'ODS', 'ODT' ].forEach(ext => FILETYPE_HANDLERS[ext] = documentFile); -[ 'PNG', 'JPEG', 'GIF', 'WEBP', 'XCF' ].forEach(ext => FILETYPE_HANDLERS[ext] = imageFile); -[ 'MP4', 'MOV', 'AVI', 'MKV', 'MPG', 'MPEG', 'M4V', 'WMV' ].forEach(ext => FILETYPE_HANDLERS[ext] = videoFile); +['AIFF', 'APE', 'FLAC', 'OGG', 'MP3'].forEach( + ext => (FILETYPE_HANDLERS[ext] = audioFile) +); +[ + 'PDF', + 'DOC', + 'DOCX', + 'DOCM', + 'ODB', + 'ODC', + 'ODF', + 'ODG', + 'ODI', + 'ODP', + 'ODS', + 'ODT', +].forEach(ext => (FILETYPE_HANDLERS[ext] = documentFile)); +['PNG', 'JPEG', 'GIF', 'WEBP', 'XCF'].forEach( + ext => (FILETYPE_HANDLERS[ext] = imageFile) +); +['MP4', 'MOV', 'AVI', 'MKV', 'MPG', 'MPEG', 'M4V', 'WMV'].forEach( + ext => (FILETYPE_HANDLERS[ext] = videoFile) +); function audioFile(metadata) { // nothing if we don't know at least the author or title - if(!metadata.author && !metadata.title) { + if (!metadata.author && !metadata.title) { return; } - let desc = `${metadata.artist||'Unknown Artist'} - ${metadata.title||'Unknown'} (`; - if(metadata.year) { + let desc = `${metadata.artist || 'Unknown Artist'} - ${ + metadata.title || 'Unknown' + } (`; + if (metadata.year) { desc += `${metadata.year}, `; } desc += `${metadata.audioBitrate})`; @@ -39,12 +60,12 @@ function videoFile(metadata) { function documentFile(metadata) { // nothing if we don't know at least the author or title - if(!metadata.author && !metadata.title) { + if (!metadata.author && !metadata.title) { return; } let result = metadata.author || ''; - if(result) { + if (result) { result += ' - '; } result += metadata.title || 'Unknown Title'; @@ -53,12 +74,12 @@ function documentFile(metadata) { function imageFile(metadata) { let desc = `${metadata.fileType} image (`; - if(metadata.animationIterations) { + if (metadata.animationIterations) { desc += 'Animated, '; } desc += `${metadata.imageSize}px`; const created = moment(metadata.createdate); - if(created.isValid()) { + if (created.isValid()) { desc += `, ${created.format('YYYY')})`; } else { desc += ')'; @@ -67,19 +88,19 @@ function imageFile(metadata) { } function main() { - const argv = exports.argv = require('minimist')(process.argv.slice(2), { - alias : { - h : 'help', - v : 'version', - } - }); + const argv = (exports.argv = require('minimist')(process.argv.slice(2), { + alias: { + h: 'help', + v: 'version', + }, + })); - if(argv.version) { + if (argv.version) { console.info(TOOL_VERSION); return 0; } - if(0 === argv._.length || argv.help) { + if (0 === argv._.length || argv.help) { console.info('usage: exiftool2desc.js [--version] [--help] PATH'); return 0; } @@ -87,22 +108,22 @@ function main() { const path = argv._[0]; fs.readFile(path, (err, data) => { - if(err) { + if (err) { return -1; } exiftool.metadata(data, (err, metadata) => { - if(err) { + if (err) { return -1; } const handler = FILETYPE_HANDLERS[metadata.fileType]; - if(!handler) { + if (!handler) { return -1; } const info = handler(metadata); - if(!info) { + if (!info) { return -1; } @@ -112,4 +133,4 @@ function main() { }); } -return main(); \ No newline at end of file +return main(); diff --git a/util/to_ansi.js b/util/to_ansi.js index 72838493..d4a8b367 100755 --- a/util/to_ansi.js +++ b/util/to_ansi.js @@ -6,25 +6,25 @@ const { controlCodesToAnsi } = require('../core/color_codes.js'); -const fs = require('graceful-fs'); +const fs = require('graceful-fs'); const iconv = require('iconv-lite'); const ToolVersion = '1.0.0'; function main() { - const argv = exports.argv = require('minimist')(process.argv.slice(2), { - alias : { - h : 'help', - v : 'version', - } - }); + const argv = (exports.argv = require('minimist')(process.argv.slice(2), { + alias: { + h: 'help', + v: 'version', + }, + })); - if(argv.version) { + if (argv.version) { console.info(ToolVersion); return 0; } - if(0 === argv._.length || argv.help) { + if (0 === argv._.length || argv.help) { console.info('usage: to_ansi.js [--version] [--help] PATH'); return 0; } @@ -32,7 +32,7 @@ function main() { const path = argv._[0]; fs.readFile(path, (err, data) => { - if(err) { + if (err) { console.error(err.message); return -1; } From 5a6827f46b6e4acf2c8bc15813beb10c16be99fc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 5 Jun 2022 16:41:16 -0600 Subject: [PATCH 3/5] ESLint + Prettier update --- .eslintrc.json | 4 ++-- package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 6eebcc94..e19846d8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,7 @@ "es6": true, "node": true }, - "extends": ["eslint:recommended"], + "extends": ["eslint:recommended", "prettier"], "rules": { "indent": [ "error", @@ -16,7 +16,7 @@ "quotes": ["error", "single"], "semi": ["error", "always"], "comma-dangle": 0, - "no-trailing-spaces": "warn" + "no-trailing-spaces": "error" }, "parserOptions": { "ecmaVersion": 2020 diff --git a/package.json b/package.json index b0610a63..abb9071c 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ }, "devDependencies": { "eslint": "^8.13.0", + "eslint-config-prettier": "^8.5.0", "prettier": "2.6.2" }, "engines": { diff --git a/yarn.lock b/yarn.lock index eab9528a..cd250d1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -494,6 +494,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-prettier@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== + eslint-scope@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" From dc0a06d1954e3481b2411d0c9aa02ac630a819ee Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 10 Jun 2022 16:18:22 -0600 Subject: [PATCH 4/5] Note on Prettier --- CONTRIBUTING.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9eb2ef48..4ed2cd93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ # Contributing ## Style -Though you'll see a lot of older style callback code, please utilize modern JavaScript. ES6 classes, arrow functions, and builtins. -There is almost never a reason to use `var`. Prefer `const` where you can and and `let` otherwise. -Save with UNIX line feeds, UTF-8 without BOM, and tabs set to 4 spaces. +* In general, [Prettier](https://prettier.io) is used. See the Prettier website for instructions. +* Though you'll see a lot of older style callback code, please utilize modern JavaScript. ES6 classes, arrow functions, and builtins. +* There is almost never a reason to use `var`. Prefer `const` where you can and and `let` otherwise. +* Save with UNIX line feeds, UTF-8 without BOM, and tabs set to 4 spaces. From a7fe9dbeb08ad590afc512e0619db71ef93ddeca Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 10 Jun 2022 17:21:53 -0600 Subject: [PATCH 5/5] More doc updates on Prettier --- CONTRIBUTING.md | 4 ++-- WHATSNEW.md | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ed2cd93..ec10cff1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing -## Style -* In general, [Prettier](https://prettier.io) is used. See the Prettier website for instructions. +## Style & Formatting +* In general, [Prettier](https://prettier.io) is used. See the [Prettier installation and basic instructions](https://prettier.io/docs/en/install.html) for more information. * Though you'll see a lot of older style callback code, please utilize modern JavaScript. ES6 classes, arrow functions, and builtins. * There is almost never a reason to use `var`. Prefer `const` where you can and and `let` otherwise. * Save with UNIX line feeds, UTF-8 without BOM, and tabs set to 4 spaces. diff --git a/WHATSNEW.md b/WHATSNEW.md index 5602f258..1595d32f 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -2,9 +2,10 @@ This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. ## 0.0.13-beta +* **Note for contributors**: ENiGMA has switched to [Prettier](https://prettier.io) for formatting/style. Please see [CONTRIBUTING](CONTRIBUTING.md) and the Prettier website for more information. * Removed terminal `cursor position reports` from most locations in the code. This should greatly increase the number of terminal programs that work with Enigma 1/2. For more information, see [Issue #222](https://github.com/NuSkooler/enigma-bbs/issues/222). This may also resolve other issues, such as [Issue #365](https://github.com/NuSkooler/enigma-bbs/issues/365), and [Issue #320](https://github.com/NuSkooler/enigma-bbs/issues/320). Anyone that previously had terminal incompatibilities please re-check and let us know! -* Bumped up the minimum [Node.js](https://nodejs.org/en/) version to V14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience. -* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in `UPGRADE.md` +* Bumped up the minimum [Node.js](https://nodejs.org/en/) version to v14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience. +* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in [UPGRADE](UPGRADE.md). * Many additional backward-compatible bug fixes since the first release of 0.0.12-beta. See the [project repository](https://github.com/NuSkooler/enigma-bbs) for more information. ## 0.0.12-beta