From 28cea6d0c50595634457bc16d5db41532dab1fbe Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 13 Jul 2020 21:08:25 -0600 Subject: [PATCH 01/52] Stub --- core/wfc.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 core/wfc.js diff --git a/core/wfc.js b/core/wfc.js new file mode 100644 index 00000000..941ed305 --- /dev/null +++ b/core/wfc.js @@ -0,0 +1,17 @@ +// ENiGMA½ +const { MenuModule } = require('./menu_module'); + +exports.moduleInfo = { + name : 'WFC', + desc : 'Semi-Traditional Waiting For Caller', + author : 'NuSkooler', +}; + +exports.getModule = class WaitingForCallerModule extends MenuModule { + constructor(options) { + super(options); + + this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + } +}; + From fa2b70dbdb550a7d3eb66e83ca4da3888335a629 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 25 Sep 2020 15:41:21 -0600 Subject: [PATCH 02/52] WIP --- core/wfc.js | 50 +++++++++++++++++++++++ misc/menu_templates/message_base.in.hjson | 4 +- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/core/wfc.js b/core/wfc.js index 941ed305..c08b9ffa 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -1,17 +1,67 @@ // ENiGMA½ const { MenuModule } = require('./menu_module'); +// deps +const async = require('async'); +const _ = require('lodash'); + exports.moduleInfo = { name : 'WFC', desc : 'Semi-Traditional Waiting For Caller', author : 'NuSkooler', }; +const FormIds = { + main : 0, +}; + +const MciViewIds = { + main : { + nodeStatus : 0, + quickLogView : 1, + + customRangeStart : 10, + } +}; + +// Secure + 2FA + root user + 'wfc' group. +const DefaultACS = 'SCAF2ID1GM[wfc]'; + exports.getModule = class WaitingForCallerModule extends MenuModule { constructor(options) { super(options); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); + + this.config.acs = this.config.acs || DefaultACS; + if (!this.config.acs.includes('SC')) { + this.config.acs = 'SC' + this.config.acs; // secure connection at the very, very least + } + } + + mciReady(mciData, cb) { + super.mciReady(mciData, err => { + if (err) { + return cb(err); + } + + async.series( + [ + (callback) => { + return this.prepViewController('main', FormIds.main, mciData.menu, callback); + }, + (callback) => { + // const requiredCodes = [ + // ]; + // return this.validateMCIByViewIds('main', requiredCodes, callback); + return callback(null); + }, + ], + err => { + return cb(err); + } + ); + }); } }; diff --git a/misc/menu_templates/message_base.in.hjson b/misc/menu_templates/message_base.in.hjson index fd4adf97..51bf4a19 100644 --- a/misc/menu_templates/message_base.in.hjson +++ b/misc/menu_templates/message_base.in.hjson @@ -251,7 +251,7 @@ submit: { *: [ { - value: { message: null } + value: { messageIndex: null } action: @method:selectMessage } ] @@ -799,7 +799,7 @@ } }, - // default prompt entry used by the 'msg_lsit' module + // default prompt entry used by the 'msg_list' module deleteMessageFromListPrompt: { art: MSGDELPMPT mci: { From e7483569e721d754dd369d7ea7c0eb4ae155c1f9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 9 Nov 2020 19:40:55 -0700 Subject: [PATCH 03/52] Merge --- core/client_connections.js | 3 ++ core/stat_log.js | 6 +++- core/wfc.js | 67 ++++++++++++++++++++++++++++++++++++-- docs/servers/web-server.md | 19 ++++++----- 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index d1a6be6e..744f1f2a 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -41,12 +41,15 @@ function getActiveConnectionList(authUsersOnly) { authenticated : ac.user.isAuthenticated(), userId : ac.user.userId, action : _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'), + serverName : ac.session.serverName, + isSecure : ac.session.isSecure, }; // // There may be a connection, but not a logged in user as of yet // if(ac.user.isAuthenticated()) { + entry.text = ac.user.username; entry.userName = ac.user.username; entry.realName = ac.user.properties[UserProps.RealName]; entry.location = ac.user.properties[UserProps.Location]; diff --git a/core/stat_log.js b/core/stat_log.js index af88ff57..5355f0c1 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -109,6 +109,10 @@ class StatLog { getSystemStat(statName) { return this.systemStats[statName]; } + getFriendlySystemStat(statName, defaultValue) { + return (this.getSystemStat(statName) || defaultValue).toLocaleString(); + } + getSystemStatNum(statName) { return parseInt(this.getSystemStat(statName)) || 0; } @@ -220,7 +224,7 @@ class StatLog { sysDb.run( `DELETE FROM system_event_log WHERE id IN( - SELECT id + SELECT id FROM system_event_log WHERE log_name = ? ORDER BY id DESC diff --git a/core/wfc.js b/core/wfc.js index c08b9ffa..6bc066cd 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -1,6 +1,13 @@ // ENiGMA½ const { MenuModule } = require('./menu_module'); +const { getActiveConnectionList } = require('./client_connections'); +const StatLog = require('./stat_log'); +const SysProps = require('./system_property'); +const { + formatByteSize, formatByteSizeAbbr, +} = require('./string_util'); + // deps const async = require('async'); const _ = require('lodash'); @@ -17,8 +24,8 @@ const FormIds = { const MciViewIds = { main : { - nodeStatus : 0, - quickLogView : 1, + nodeStatus : 1, + quickLogView : 2, customRangeStart : 10, } @@ -35,7 +42,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { this.config.acs = this.config.acs || DefaultACS; if (!this.config.acs.includes('SC')) { - this.config.acs = 'SC' + this.config.acs; // secure connection at the very, very least + this.config.acs = 'SC' + this.config.acs; // secure connection at the very least } } @@ -56,6 +63,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // return this.validateMCIByViewIds('main', requiredCodes, callback); return callback(null); }, + (callback) => { + return this._refreshNodeStatus(callback); + } ], err => { return cb(err); @@ -63,5 +73,56 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { ); }); } + + _refreshStats(cb) { + const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); + const totalFiles = fileAreaStats.totalFiles || 0; + const totalFileBytes = fileAreaStats.totalBytes || 0; + + this.stats = { + // Totals + totalCalls : StatLog.getFriendlySystemStat(SysProps.LoginCount, 0), + totalPosts : StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0), + //totalUsers : + totalFiles : totalFiles.toLocaleString(), + totalFileBytes : formatByteSize(totalFileBytes, false), + totalFileBytesAbbr : formatByteSizeAbbr(totalFileBytes), + // :TODO: date, time - formatted as per config.dateTimeFormat and such + // :TODO: Most/All current user status should be predefined MCI + // :TODO: lastCaller + // :TODO: totalMemoryBytes, freeMemoryBytes + // :TODO: CPU info/averages/load + // :TODO: processUptime + // :TODO: 24 HOUR stats - + // calls24Hour, posts24Hour, uploadBytes24Hour, downloadBytes24Hour, ... + // :TODO: totals - most avail from MCI + }; + + return cb(null); + } + + _refreshNodeStatus(cb) { + const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); + if (!nodeStatusView) { + return cb(null); + } + + const nodeStatusItems = getActiveConnectionList(false).slice(0, nodeStatusView.height).map(ac => { + // Handle pre-authenticated + if (!ac.authenticated) { + ac.text = ac.username = 'Pre Auth'; + ac.action = 'Logging In'; + } + + return Object.assign(ac, { + timeOn : _.upperFirst(ac.timeOn.humanize()), // make friendly + }); + }); + + nodeStatusView.setItems(nodeStatusItems); + nodeStatusView.redraw(); + + return cb(null); + } }; diff --git a/docs/servers/web-server.md b/docs/servers/web-server.md index 216efb6c..75c7ea18 100644 --- a/docs/servers/web-server.md +++ b/docs/servers/web-server.md @@ -10,18 +10,19 @@ By default the web server is not enabled. To enable it, you will need to at a mi ```hjson contentServers: { - web: { - domain: bbs.yourdomain.com + web: { + domain: bbs.yourdomain.com - http: { - enabled: true - port: 8080 - } - } + http: { + enabled: true + port: 8080 + } + } } ``` The following is a table of all configuration keys available under `contentServers.web`: + | Key | Required | Description | |------|----------|-------------| | `domain` | :+1: | Sets the domain, e.g. `bbs.yourdomain.com`. | @@ -52,9 +53,9 @@ Entries available under `contentServers.web.https`: #### Certificates -If you don't have a TLS certificate for your domain, a good source for a certificate can be [LetsEncrypt](https://letsencrypt.org/) who supplies free and trusted TLS certificates. +If you don't have a TLS certificate for your domain, a good source for a certificate can be [Let's Encrypt](https://letsencrypt.org/) who supplies free and trusted TLS certificates. A common strategy is to place another web server such as [Caddy](https://caddyserver.com/) in front of ENiGMA½ acting as a transparent proxy and TLS termination point. -Keep in mind that the SSL certificate provided by Let's Encrypt's Certbot is by default stored in a privileged location; if your ENIGMA instance is not running as root (which it should not be!), you'll need to copy the SSL certificate somewhere else in order for ENIGMA to use it. +:information_source: Keep in mind that the SSL certificate provided by Let's Encrypt's Certbot is by default stored in a privileged location; if your ENIGMA instance is not running as root (which it should not be!), you'll need to copy the SSL certificate somewhere else in order for ENIGMA to use it. ## Static Routes From d6cc53c263155fce133095576c7632ac4f2c0ed0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 9 Nov 2020 21:32:34 -0700 Subject: [PATCH 04/52] Some more stats --- core/menu_module.js | 16 ++++++++++++++++ core/predefined_mci.js | 1 + core/string_util.js | 2 +- core/wfc.js | 40 ++++++++++++++++++++++++++++++++++++---- package.json | 3 ++- yarn.lock | 5 +++++ 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/core/menu_module.js b/core/menu_module.js index 804065a7..8d325378 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -689,4 +689,20 @@ exports.MenuModule = class MenuModule extends PluginModule { return cb(good ? null : Errors.Invalid(`Invalid or missing config option "${firstBadKey}" (${badReason})`)); } + + // Various common helpers + getDateFormat(defaultStyle='short') { + return this.config.dateFormat || + this.client.currentTheme.helpers.getDateFormat(defaultStyle); + } + + getTimeFormat(defaultStyle='short') { + return this.config.timeFormat || + this.client.currentTheme.helpers.getTimeFormat(defaultStyle); + } + + getDateTimeFormat(defaultStyle='short') { + return this.config.dateTimeFormat || + this.client.currentTheme.helpers.getDateTimeFormat(defaultStyle); + } }; diff --git a/core/predefined_mci.js b/core/predefined_mci.js index a1182a79..a63ad45c 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -267,6 +267,7 @@ const PREDEFINED_MCI_GENERATORS = { // :TODO: DD - Files downloaded *today* (iNiQUiTY) // :TODO: LC - name of last caller to system (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) + // :TODO: ?? - Total users on system // diff --git a/core/string_util.js b/core/string_util.js index 6887275e..6b88ec40 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -337,7 +337,7 @@ function formatByteSizeAbbr(byteSize) { 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)); + let result = parseFloat((byteSize / Math.pow(1024, i)).toFixed(decimals)).toString(); if(withAbbr) { result += ` ${BYTE_SIZE_ABBRS[i]}`; } diff --git a/core/wfc.js b/core/wfc.js index 6bc066cd..e9067335 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -11,6 +11,8 @@ const { // deps const async = require('async'); const _ = require('lodash'); +const moment = require('moment'); +const SysInfo = require('systeminformation'); exports.moduleInfo = { name : 'WFC', @@ -63,6 +65,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // return this.validateMCIByViewIds('main', requiredCodes, callback); return callback(null); }, + (callback) => { + return this._refreshStats(callback); + }, (callback) => { return this._refreshNodeStatus(callback); } @@ -79,7 +84,16 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { const totalFiles = fileAreaStats.totalFiles || 0; const totalFileBytes = fileAreaStats.totalBytes || 0; + // Some stats we can just fill right away this.stats = { + // Date/Time + date : moment().format(this.getDateFormat()), + time : moment().format(this.getTimeFormat()), + dateTime : moment().format(this.getDateTimeFormat()), + + // Current process (our Node.js service) + processUptime : moment.duration(process.uptime(), 'seconds').humanize(), + // Totals totalCalls : StatLog.getFriendlySystemStat(SysProps.LoginCount, 0), totalPosts : StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0), @@ -87,18 +101,36 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { totalFiles : totalFiles.toLocaleString(), totalFileBytes : formatByteSize(totalFileBytes, false), totalFileBytesAbbr : formatByteSizeAbbr(totalFileBytes), - // :TODO: date, time - formatted as per config.dateTimeFormat and such // :TODO: Most/All current user status should be predefined MCI // :TODO: lastCaller // :TODO: totalMemoryBytes, freeMemoryBytes // :TODO: CPU info/averages/load // :TODO: processUptime // :TODO: 24 HOUR stats - - // calls24Hour, posts24Hour, uploadBytes24Hour, downloadBytes24Hour, ... - // :TODO: totals - most avail from MCI + // callsToday, postsToday, uploadsToday, uploadBytesToday, ... + }; - return cb(null); + // Some async work required... + const basicSysInfo = { + mem : 'total, free', + currentLoad : 'avgload, currentLoad', + }; + + SysInfo.get(basicSysInfo) + .then(sysInfo => { + this.stats.totalMemoryBytes = formatByteSize(sysInfo.mem.total, false); + this.stats.totalMemoryBytesAbbr = formatByteSizeAbbr(sysInfo.mem.total); + this.stats.freeMemoryBytes = formatByteSize(sysInfo.mem.free, false); + this.stats.freeMemoryBytesAbbr = formatByteSizeAbbr(sysInfo.mem.free); + + // Not avail on BSD, yet. + this.stats.systemAvgLoad = _.get(sysInfo, 'currentLoad.avgload', 0).toString(); + this.stats.systemCurrentLoad = _.get(sysInfo, 'currentLoad.currentLoad', 0).toString(); + }) + .catch(err => { + return cb(err); + }); } _refreshNodeStatus(cb) { diff --git a/package.json b/package.json index 038cce91..cc869b1f 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "uuid-parse": "1.1.0", "ws": "^7.3.0", "xxhash": "^0.3.0", - "yazl": "^2.5.1" + "yazl": "^2.5.1", + "systeminformation" : "^4.27.5" }, "devDependencies": { "eslint": "^7.2.0" diff --git a/yarn.lock b/yarn.lock index 85cd031a..85f4982c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2306,6 +2306,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +systeminformation@^4.27.5: + version "4.29.3" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.29.3.tgz#6dac4ff96479f1dd5df50582a965afb4c76178f2" + integrity sha512-C5+o6hit4BQpFdZKxXwRqgDpAkXCjxAUi5pB3UznjYPl3XQugwo1V46XltUt+53EaphlHA6j41mh++ksdOuBMA== + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" From 58c577c4bb15929ebb7baf93bd68c646c08d8b8b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Nov 2020 12:25:19 -0700 Subject: [PATCH 05/52] Checkpoint --- core/predefined_mci.js | 29 +++++++++++++-------- core/stat_log_system.js | 28 ++++++++++++++++++++ core/system_property.js | 8 ++++++ core/wfc.js | 57 +++++++++++++++++++++++------------------ 4 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 core/stat_log_system.js diff --git a/core/predefined_mci.js b/core/predefined_mci.js index a63ad45c..222b8cc3 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -24,12 +24,22 @@ const packageJson = require('../package.json'); const os = require('os'); const _ = require('lodash'); const moment = require('moment'); +const async = require('async'); exports.getPredefinedMCIValue = getPredefinedMCIValue; exports.init = init; function init(cb) { - setNextRandomRumor(cb); + async.series( + [ + (callback) => { + return setNextRandomRumor(callback); + }, + ], + err => { + return cb(err); + } + ); } function setNextRandomRumor(cb) { @@ -65,10 +75,6 @@ function userStatAsCountString(client, statName, defaultValue) { return toNumberWithCommas(value); } -function sysStatAsString(statName, defaultValue) { - return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString(); -} - const PREDEFINED_MCI_GENERATORS = { // // Board @@ -212,11 +218,14 @@ const PREDEFINED_MCI_GENERATORS = { .trim(); }, + // :TODO: use new live stat + MB : function totalMemoryBytes() { return getTotalMemoryBytes(); }, + MF : function totalMemoryFreeBytes() { return getTotalMemoryFreeBytes(); }, + // :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; }, - AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, TC : function totalCalls() { return StatLog.getSystemStat(SysProps.LoginCount).toLocaleString(); }, @@ -236,12 +245,12 @@ const PREDEFINED_MCI_GENERATORS = { // // :TODO: DD - Today's # of downloads (iNiQUiTY) // - SD : function systemNumDownloads() { return sysStatAsString(SysProps.FileDlTotalCount, 0); }, + SD : function systemNumDownloads() { return StatLog.getFriendlySystemStat(SysProps.FileDlTotalCount, 0); }, SO : function systemByteDownload() { const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, - SU : function systemNumUploads() { return sysStatAsString(SysProps.FileUlTotalCount, 0); }, + SU : function systemNumUploads() { return StatLog.getFriendlySystemStat(SysProps.FileUlTotalCount, 0); }, SP : function systemByteUpload() { const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr @@ -256,10 +265,10 @@ const PREDEFINED_MCI_GENERATORS = { return formatByteSize(totalBytes, true); // true=withAbbr }, PT : function messagesPostedToday() { // Obv/2 - return sysStatAsString(SysProps.MessagesToday, 0); + return StatLog.getFriendlySystemStat(SysProps.MessagesToday, 0); }, TP : function totalMessagesOnSystem() { // Obv/2 - return sysStatAsString(SysProps.MessageTotalCount, 0); + return StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0); }, // :TODO: NT - New users today (Obv/2) diff --git a/core/stat_log_system.js b/core/stat_log_system.js new file mode 100644 index 00000000..f027c455 --- /dev/null +++ b/core/stat_log_system.js @@ -0,0 +1,28 @@ + +// deps +const SysInfo = require('systeminformation'); +const _ = require('lodash'); + +exports.getSystemInfoStats = getSystemInfoStats; + +function getSystemInfoStats(cb) { + const basicSysInfo = { + mem : 'total, free', + currentLoad : 'avgload, currentLoad', + }; + + SysInfo.get(basicSysInfo) + .then(sysInfo => { + return cb(null, { + totalMemoryBytes : sysInfo.mem.total, + freeMemoryBytes : sysInfo.mem.free, + + // Not avail on BSD, yet. + systemAvgLoad : _.get(sysInfo, 'currentLoad.avgload', 0), + systemCurrentLoad : _.get(sysInfo, 'currentLoad.currentLoad', 0), + }); + }) + .catch(err => { + return cb(err); + }); +} diff --git a/core/system_property.js b/core/system_property.js index ca3cf7cd..a74a57d8 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -30,4 +30,12 @@ module.exports = { // end +op non-persistent NextRandomRumor : 'random_rumor', + + // begin system stat non-persistent... + TotalMemoryBytes : 'sys_total_memory_bytes', + FreeMemoryBytes : 'sys_free_memory_bytes', + AverageLoad : 'sys_average_load', + CurrentLoad : 'sys_current_load', + + // end system stat non persistent }; diff --git a/core/wfc.js b/core/wfc.js index e9067335..53fea670 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -4,9 +4,6 @@ const { MenuModule } = require('./menu_module'); const { getActiveConnectionList } = require('./client_connections'); const StatLog = require('./stat_log'); const SysProps = require('./system_property'); -const { - formatByteSize, formatByteSizeAbbr, -} = require('./string_util'); // deps const async = require('async'); @@ -80,35 +77,47 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } _refreshStats(cb) { - const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); - const totalFiles = fileAreaStats.totalFiles || 0; - const totalFileBytes = fileAreaStats.totalBytes || 0; + const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); // Some stats we can just fill right away this.stats = { // Date/Time - date : moment().format(this.getDateFormat()), - time : moment().format(this.getTimeFormat()), - dateTime : moment().format(this.getDateTimeFormat()), + date : moment().format(this.getDateFormat()), + time : moment().format(this.getTimeFormat()), + dateTime : moment().format(this.getDateTimeFormat()), // Current process (our Node.js service) - processUptime : moment.duration(process.uptime(), 'seconds').humanize(), + processUptimeSeconds : process.uptime(), +// processUptime : moment.duration(process.uptime(), 'seconds').humanize(), // Totals - totalCalls : StatLog.getFriendlySystemStat(SysProps.LoginCount, 0), - totalPosts : StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0), + totalCalls : StatLog.getSystemStatNum(SysProps.LoginCount), + totalPosts : StatLog.getSystemStatNum(SysProps.MessageTotalCount), //totalUsers : - totalFiles : totalFiles.toLocaleString(), - totalFileBytes : formatByteSize(totalFileBytes, false), - totalFileBytesAbbr : formatByteSizeAbbr(totalFileBytes), - // :TODO: Most/All current user status should be predefined MCI + totalFiles : fileAreaStats.totalFiles || 0, + totalFileBytes : fileAreaStats.totalFileBytes || 0, + + // totalUploads : + // totalUploadBytes : + // totalDownloads : + // totalDownloadBytes : + // :TODO: lastCaller // :TODO: totalMemoryBytes, freeMemoryBytes // :TODO: CPU info/averages/load - // :TODO: processUptime - // :TODO: 24 HOUR stats - - // callsToday, postsToday, uploadsToday, uploadBytesToday, ... + // Today's Stats + callsToday : StatLog.getSystemStatNum(SysProps.LoginsToday), + postsToday : StatLog.getSystemStatNum(SysProps.MessagesToday), + // uploadsToday : + // uploadBytesToday : + // downloadsToday : + // downloadBytesToday : + + // Current + // lastCaller : + // lastCallerDate + // lastCallerTime }; // Some async work required... @@ -119,14 +128,12 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { SysInfo.get(basicSysInfo) .then(sysInfo => { - this.stats.totalMemoryBytes = formatByteSize(sysInfo.mem.total, false); - this.stats.totalMemoryBytesAbbr = formatByteSizeAbbr(sysInfo.mem.total); - this.stats.freeMemoryBytes = formatByteSize(sysInfo.mem.free, false); - this.stats.freeMemoryBytesAbbr = formatByteSizeAbbr(sysInfo.mem.free); + this.stats.totalMemoryBytes = sysInfo.mem.total; + this.stats.freeMemoryBytes = sysInfo.mem.free; // Not avail on BSD, yet. - this.stats.systemAvgLoad = _.get(sysInfo, 'currentLoad.avgload', 0).toString(); - this.stats.systemCurrentLoad = _.get(sysInfo, 'currentLoad.currentLoad', 0).toString(); + this.stats.systemAvgLoad = _.get(sysInfo, 'currentLoad.avgload', 0); + this.stats.systemCurrentLoad = _.get(sysInfo, 'currentLoad.currentLoad', 0); }) .catch(err => { return cb(err); From 5fb9716dc64a775f5d366082e3bc52ff8c4dd66c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 22 Nov 2020 18:54:49 -0700 Subject: [PATCH 06/52] Add some new MCI codes --- WHATSNEW.md | 1 + core/predefined_mci.js | 28 ++++++++++++++++++--- core/stat_log.js | 56 ++++++++++++++++++++++++++++++++++++++++- core/stat_log_system.js | 28 --------------------- core/system_property.js | 7 ++---- core/wfc.js | 1 + docs/art/mci.md | 6 +++++ 7 files changed, 90 insertions(+), 37 deletions(-) delete mode 100644 core/stat_log_system.js diff --git a/WHATSNEW.md b/WHATSNEW.md index acef4338..a0d48acf 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -8,6 +8,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired. * New `PV` ACS check for arbitrary user properties. See [ACS](/docs/configuration/acs.md) for details. * The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. +* A number of new MCI codes (see [MCI](./docs/art/mci.md)) ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 222b8cc3..50de9304 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -35,6 +35,11 @@ function init(cb) { (callback) => { return setNextRandomRumor(callback); }, + (callback) => { + // by fetching a memory or load we'll force a refresh now + StatLog.getSystemStat(SysProps.SystemMemoryStats); + return callback(null); + } ], err => { return cb(err); @@ -42,6 +47,7 @@ function init(cb) { ); } +// :TODO: move this to stat_log.js like system memory is handled function setNextRandomRumor(cb) { StatLog.getSystemLogEntries(SysLogKeys.UserAddedRumorz, StatLog.Order.Random, 1, (err, entry) => { if(entry) { @@ -218,9 +224,25 @@ const PREDEFINED_MCI_GENERATORS = { .trim(); }, - // :TODO: use new live stat - MB : function totalMemoryBytes() { return getTotalMemoryBytes(); }, - MF : function totalMemoryFreeBytes() { return getTotalMemoryFreeBytes(); }, + MB : function totalMemoryBytes() { + const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || { totalBytes : 0 }; + return formatByteSize(stats.totalBytes, true); // true=withAbbr + }, + MF : function totalMemoryFreeBytes() { + const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || { freeBytes : 0 }; + return formatByteSize(stats.freeBytes, true); // true=withAbbr + }, + LA : function systemLoadAverage() { + const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { average : 0.0 }; + return stats.average.toLocaleString(); + }, + CL : function systemCurrentLoad() { + const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { current : 0 }; + return `${stats.current}%`; + }, + UU : function systemUptime() { + return moment.duration(process.uptime(), 'seconds').humanize(); + }, // :TODO: MCI for core count, e.g. os.cpus().length diff --git a/core/stat_log.js b/core/stat_log.js index 5355f0c1..55c4620a 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -6,10 +6,12 @@ const { getISOTimestampString } = require('./database.js'); const Errors = require('./enig_error.js'); +const SysProps = require('./system_property.js'); // deps const _ = require('lodash'); const moment = require('moment'); +const SysInfo = require('systeminformation'); /* System Event Log & Stats @@ -26,6 +28,7 @@ const moment = require('moment'); class StatLog { constructor() { this.systemStats = {}; + this.lastSysInfoStatsRefresh = 0; } init(cb) { @@ -107,7 +110,15 @@ class StatLog { ); } - getSystemStat(statName) { return this.systemStats[statName]; } + getSystemStat(statName) { + const stat = this.systemStats[statName]; + + // Some stats are refreshed periodically when they are + // being accessed (e.g. "looked at"). This is handled async. + this._refreshSystemStat(statName); + + return stat; + } getFriendlySystemStat(statName, defaultValue) { return (this.getSystemStat(statName) || defaultValue).toLocaleString(); @@ -377,6 +388,49 @@ class StatLog { systemEventUserLogInit(this); return cb(null); } + + _refreshSystemStat(statName) { + switch (statName) { + case SysProps.SystemLoadStats : + case SysProps.SystemMemoryStats : + return this._refreshSysInfoStats(); + } + } + + _refreshSysInfoStats() { + const now = Math.floor(Date.now() / 1000); + if (now < this.lastSysInfoStatsRefresh + 5) { + return; + } + + this.lastSysInfoStatsRefresh = now; + + const basicSysInfo = { + mem : 'total, free', + currentLoad : 'avgload, currentLoad', + }; + + SysInfo.get(basicSysInfo) + .then(sysInfo => { + const memStats = { + totalBytes : sysInfo.mem.total, + freeBytes : sysInfo.mem.free, + }; + + this.setNonPersistentSystemStat(SysProps.SystemMemoryStats, memStats); + + const loadStats = { + // Not avail on BSD, yet. + average : _.get(sysInfo, 'currentLoad.avgload', 0), + current : _.get(sysInfo, 'currentLoad.currentLoad', 0), + }; + + this.setNonPersistentSystemStat(SysProps.SystemLoadStats, loadStats); + }) + .catch(err => { + // :TODO: log me + }); + } } module.exports = new StatLog(); diff --git a/core/stat_log_system.js b/core/stat_log_system.js deleted file mode 100644 index f027c455..00000000 --- a/core/stat_log_system.js +++ /dev/null @@ -1,28 +0,0 @@ - -// deps -const SysInfo = require('systeminformation'); -const _ = require('lodash'); - -exports.getSystemInfoStats = getSystemInfoStats; - -function getSystemInfoStats(cb) { - const basicSysInfo = { - mem : 'total, free', - currentLoad : 'avgload, currentLoad', - }; - - SysInfo.get(basicSysInfo) - .then(sysInfo => { - return cb(null, { - totalMemoryBytes : sysInfo.mem.total, - freeMemoryBytes : sysInfo.mem.free, - - // Not avail on BSD, yet. - systemAvgLoad : _.get(sysInfo, 'currentLoad.avgload', 0), - systemCurrentLoad : _.get(sysInfo, 'currentLoad.currentLoad', 0), - }); - }) - .catch(err => { - return cb(err); - }); -} diff --git a/core/system_property.js b/core/system_property.js index a74a57d8..a7ebae9d 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -32,10 +32,7 @@ module.exports = { NextRandomRumor : 'random_rumor', // begin system stat non-persistent... - TotalMemoryBytes : 'sys_total_memory_bytes', - FreeMemoryBytes : 'sys_free_memory_bytes', - AverageLoad : 'sys_average_load', - CurrentLoad : 'sys_current_load', - + SystemMemoryStats : 'system_memory_stats', // object { totalBytes, freeBytes } + SystemLoadStats : 'system_load_stats', // object { average, current } // end system stat non persistent }; diff --git a/core/wfc.js b/core/wfc.js index 53fea670..b8bcc184 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -121,6 +121,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { }; // Some async work required... + // :TODO: replace with stat log stats const basicSysInfo = { mem : 'total, free', currentLoad : 'avgload, currentLoad', diff --git a/docs/art/mci.md b/docs/art/mci.md index 32c60e4a..099c5f04 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -80,6 +80,12 @@ for a full listing. Many codes attempt to pay homage to Oblivion/2, iNiQUiTY, et | `TB` | Total amount of files on the system (formatted to appropriate bytes/megs/gigs/etc.) | | `TP` | Total messages posted/imported to the system *currently* | | `PT` | Total messages posted/imported to the system *today* | +| `MB` | System memory | +| `MF` | System _free_ memory | +| `LA` | System load average (e.g. 0.25)
(Not available for all platforms) | +| `CL` | System current load percentage
(Not available for all platforms) | +| `UU` | System uptime in friendly format | + Some additional special case codes also exist: From 9c7fb16196d6367d7bf47af1d18c52bb270383b1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 26 Nov 2020 19:51:00 -0700 Subject: [PATCH 07/52] Last caller information / MCI --- core/bbs.js | 36 +++++++++++++++++++++++++++++++++--- core/predefined_mci.js | 26 ++++++++++++++++++-------- core/system_property.js | 21 +++++++++------------ core/user.js | 35 +++++++++++++++++++++++++++++++++++ core/user_login.js | 23 ++++++++++++++++++++++- docs/art/mci.md | 4 +++- 6 files changed, 120 insertions(+), 25 deletions(-) diff --git a/core/bbs.js b/core/bbs.js index 90c31beb..ebcd0c9d 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -236,6 +236,8 @@ function initialize(cb) { // const User = require('./user.js'); + // :TODO: use User.getUserInfo() for this! + const propLoadOpts = { names : [ UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, @@ -246,7 +248,7 @@ function initialize(cb) { async.waterfall( [ function getOpUserName(next) { - return User.getUserName(1, next); + return User.getUserName(User.RootUserID, next); }, function getOpProps(opUserName, next) { User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { @@ -273,8 +275,9 @@ function initialize(cb) { } ); }, - function initCallsToday(callback) { + function initSystemLogStats(callback) { const StatLog = require('./stat_log.js'); + const filter = { logName : SysLogKeys.UserLoginHistory, resultType : 'count', @@ -288,7 +291,7 @@ function initialize(cb) { return callback(null); }); }, - function initUserTransferStats(callback) { + function initUserLogStats(callback) { const StatLog = require('./stat_log'); const entries = [ @@ -324,6 +327,33 @@ function initialize(cb) { return callback(null); }); }, + function initLastLogin(callback) { + const StatLog = require('./stat_log'); + StatLog.getSystemLogEntries(SysLogKeys.UserLoginHistory, 'timestamp_desc', 1, (err, lastLogin) => { + if (err) { + return callback(null); + } + + let loginObj; + try + { + loginObj = JSON.parse(lastLogin[0].log_value); + loginObj.timestamp = moment(lastLogin[0].timestamp); + } + catch (e) + { + return callback(null); + } + + // For live stats we want to resolve user ID -> name, etc. + const User = require('./user'); + User.getUserInfo(loginObj.userId, (err, props) => { + const stat = Object.assign({}, props, loginObj); + StatLog.setNonPersistentSystemStat(SysProps.LastLogin, stat); + return callback(null); + }); + }); + }, function initMessageStats(callback) { return require('./message_area.js').startup(callback); }, diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 0842a739..b24918b6 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -98,7 +98,6 @@ const PREDEFINED_MCI_GENERATORS = { 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 @@ -243,10 +242,6 @@ const PREDEFINED_MCI_GENERATORS = { UU : function systemUptime() { return moment.duration(process.uptime(), 'seconds').humanize(); }, - - // :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; }, AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, @@ -265,8 +260,6 @@ const PREDEFINED_MCI_GENERATORS = { // // System File Base, Up/Download Info // - // :TODO: DD - Today's # of downloads (iNiQUiTY) - // SD : function systemNumDownloads() { return StatLog.getFriendlySystemStat(SysProps.FileDlTotalCount, 0); }, SO : function systemByteDownload() { const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTotalBytes); @@ -308,10 +301,27 @@ const PREDEFINED_MCI_GENERATORS = { }, // :TODO: NT - New users today (Obv/2) - // :TODO: LC - name of last caller to system (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) // :TODO: ?? - Total users on system + LC : function lastCallerUserName() { // Obv/2 + const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {}; + return lastLogin.userName || 'N/A'; + }, + LD : function lastCallerDate(client) { + const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {}; + if (!lastLogin.timestamp) { + return 'N/A'; + } + return lastLogin.timestamp.format(client.currentTheme.helpers.getDateFormat()); + }, + LT : function lastCallerTime(client) { + const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {}; + if (!lastLogin.timestamp) { + return 'N/A'; + } + return lastLogin.timestamp.format(client.currentTheme.helpers.getTimeFormat()); + }, // // Special handling for XY diff --git a/core/system_property.js b/core/system_property.js index 40c501aa..67049552 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -10,6 +10,7 @@ module.exports = { LoginCount : 'login_count', LoginsToday : 'logins_today', // non-persistent + LastLogin : 'last_login', // object { userId, sessionId, userName, userRealName, timestamp }; non-persistent FileBaseAreaStats : 'file_base_area_stats', // object - see file_base_area.js::getAreaStats FileUlTotalCount : 'ul_total_count', @@ -25,19 +26,15 @@ module.exports = { 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', - // end +op non-persistent + SysOpUsername : 'sysop_username', // non-persistent + SysOpRealName : 'sysop_real_name', // non-persistent + SysOpLocation : 'sysop_location', // non-persistent + SysOpAffiliations : 'sysop_affiliation', // non-persistent + SysOpSex : 'sysop_sex', // non-persistent + SysOpEmailAddress : 'sysop_email_address', // non-persistent NextRandomRumor : 'random_rumor', - // begin system stat non-persistent... - SystemMemoryStats : 'system_memory_stats', // object { totalBytes, freeBytes } - SystemLoadStats : 'system_load_stats', // object { average, current } - // end system stat non persistent + SystemMemoryStats : 'system_memory_stats', // object { totalBytes, freeBytes }; non-persistent + SystemLoadStats : 'system_load_stats', // object { average, current }; non-persistent }; diff --git a/core/user.js b/core/user.js index 7c017e2c..4dad3399 100644 --- a/core/user.js +++ b/core/user.js @@ -630,6 +630,41 @@ module.exports = class User { ); } + static getUserInfo(userId, propsList, cb) { + if (!cb && _.isFunction(propsList)) { + cb = propsList; + propsList = [ + UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, + UserProps.Location, UserProps.Affiliations, + ]; + } + + async.waterfall( + [ + (callback) => { + return User.getUserName(userId, callback); + }, + (userName, callback) => { + User.loadProperties(userId, { names : propsList }, (err, props) => { + return callback(err, Object.assign({}, props, { user_name : userName })); + }); + } + ], + (err, userProps) => { + if (err) { + return cb(err); + } + + const userInfo = {}; + Object.keys(userProps).forEach(key => { + userInfo[_.camelCase(key)] = userProps[key] || 'N/A'; + }); + + return cb(null, userInfo); + } + ); + } + static isRootUserId(userId) { return (User.RootUserID === userId); } diff --git a/core/user_login.js b/core/user_login.js index 3db6b5cc..8c9e41b8 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -30,6 +30,7 @@ const { const async = require('async'); const _ = require('lodash'); const assert = require('assert'); +const moment = require('moment'); exports.userLogin = userLogin; exports.recordLogin = recordLogin; @@ -176,6 +177,8 @@ function recordLogin(client, cb) { assert(client.user.authenticated); // don't get in situations where this isn't true const user = client.user; + const loginTimestamp = StatLog.now; + async.parallel( [ (callback) => { @@ -183,7 +186,7 @@ function recordLogin(client, cb) { return StatLog.incrementSystemStat(SysProps.LoginCount, 1, callback); }, (callback) => { - return StatLog.setUserStat(user, UserProps.LastLoginTs, StatLog.now, callback); + return StatLog.setUserStat(user, UserProps.LastLoginTs, loginTimestamp, callback); }, (callback) => { return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback); @@ -202,6 +205,24 @@ function recordLogin(client, cb) { StatLog.KeepType.Max, callback ); + }, + (callback) => { + // Update live last login information which includes additional + // (pre-resolved) information such as user name/etc. + const lastLogin = { + userId : user.userId, + sessionId : user.sessionId, + userName : user.username, + realName : user.getProperty(UserProps.RealName), + affiliation : user.getProperty(UserProps.Affiliations), + emailAddress : user.getProperty(UserProps.EmailAddress), + sex : user.getProperty(UserProps.Sex), + location : user.getProperty(UserProps.Location), + timestamp : moment(loginTimestamp), + }; + + StatLog.setNonPersistentSystemStat(SysProps.LastLogin, lastLogin); + return callback(null); } ], err => { diff --git a/docs/art/mci.md b/docs/art/mci.md index 235a92a6..c976334a 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -100,7 +100,9 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `LA` | System load average (e.g. 0.25)
(Not available for all platforms) | | `CL` | System current load percentage
(Not available for all platforms) | | `UU` | System uptime in friendly format | - +| `LC` | Last caller to the system (username) | +| `LT` | Time of last caller | +| `LD` | Date of last caller | Some additional special case codes also exist: From b075cbae6594fe3973b90147caf392cba1d50e57 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 22:35:03 -0700 Subject: [PATCH 08/52] Checkpoing on WFC --- art/themes/luciano_blocktronics/theme.hjson | 28 ++++ core/view_controller.js | 8 +- core/wfc.js | 151 +++++++++++++++++--- 3 files changed, 159 insertions(+), 28 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 407e8a1b..b533dffc 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -246,6 +246,34 @@ } } + mainMenuWaitingForCaller: { + config: { + quickLogLevel: warn + quickLogLevelIndicators: { + trace : |00|02T + debug: |00|03D + info: |00|15I + warn: |00|14W + error: |00|12E + fatal: |00|28F + } + } + 0: { + mci: { + VM1: { + height: 5 + widht: 37 + itemFormat: "|00|11{node:<3.2} |10{userName:>13} |08> |04{action:<14.13} |14{serverName}" + } + VM2: { + height: 5 + width: 73 + itemFormat: "{levelIndicator} |15{timestamp} |07{message}" + } + } + } + } + messageBaseMessageList: { config: { dateTimeFormat: ddd MMM Do diff --git a/core/view_controller.js b/core/view_controller.js index f51c307f..923e0aa1 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -254,9 +254,7 @@ function ViewController(options) { const view = self.getView(viewId); if(!view) { - self.client.log.warn( { viewId : viewId }, 'Cannot find view'); - nextItem(null); - return; + return nextItem(null); } const mciConf = config.mci[mci]; @@ -276,11 +274,9 @@ function ViewController(options) { err => { // default to highest ID if no 'submit' entry present if(!submitId) { - var highestIdView = self.getView(highestId); + const highestIdView = self.getView(highestId); if(highestIdView) { highestIdView.submit = true; - } else { - self.client.log.warn( { highestId : highestId }, 'View does not exist'); } } diff --git a/core/wfc.js b/core/wfc.js index b8bcc184..65db0df2 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -4,12 +4,15 @@ const { MenuModule } = require('./menu_module'); const { getActiveConnectionList } = require('./client_connections'); const StatLog = require('./stat_log'); const SysProps = require('./system_property'); +const Log = require('./logger'); +const Config = require('./config.js').get; // deps const async = require('async'); const _ = require('lodash'); const moment = require('moment'); const SysInfo = require('systeminformation'); +const bunyan = require('bunyan'); exports.moduleInfo = { name : 'WFC', @@ -62,6 +65,26 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // return this.validateMCIByViewIds('main', requiredCodes, callback); return callback(null); }, + (callback) => { + const quickLogView = this.viewControllers.main.getView(MciViewIds.main.quickLogView); + if (!quickLogView) { + return callback(null); + } + + const logLevel = this.config.quickLogLevel || + _.get(Config(), 'logging.rotatingFile.level') || + 'info'; + + this.logRingBuffer = new bunyan.RingBuffer({ limit : quickLogView.dimens.height || 24 }); + Log.log.addStream({ + name : 'wfc-ringbuffer', + type : 'raw', + level : logLevel, + stream : this.logRingBuffer + }); + + return callback(null); + }, (callback) => { return this._refreshStats(callback); }, @@ -70,14 +93,59 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } ], err => { + if (!err) { + this._startRefreshing(); + } return cb(err); } ); }); } + leave() { + this._stopRefreshing(); + + super.leave(); + } + + _startRefreshing() { + this.mainRefreshTimer = setInterval( () => { + this._refreshAll(); + }, 5000); + } + + _stopRefreshing() { + if (this.mainRefreshTimer) { + clearInterval(this.mainRefreshTimer); + delete this.mainRefreshTimer; + } + } + + _refreshAll(cb) { + async.series( + [ + (callback) => { + return this._refreshStats(callback); + }, + (callback) => { + return this._refreshNodeStatus(callback); + }, + (callback) => { + return this._refreshQuickLog(callback); + } + ], + err => { + if (cb) { + return cb(err); + } + } + ); + } + _refreshStats(cb) { const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); + const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats); + const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats); // Some stats we can just fill right away this.stats = { @@ -118,27 +186,14 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // lastCaller : // lastCallerDate // lastCallerTime + + totalMemoryBytes : sysMemStats.totalBytes, + freeMemoryBytes : sysMemStats.freeBytes, + systemAvgLoad : sysLoadStats.average, + systemCurrentLoad : sysLoadStats.current, }; - // Some async work required... - // :TODO: replace with stat log stats - const basicSysInfo = { - mem : 'total, free', - currentLoad : 'avgload, currentLoad', - }; - - SysInfo.get(basicSysInfo) - .then(sysInfo => { - this.stats.totalMemoryBytes = sysInfo.mem.total; - this.stats.freeMemoryBytes = sysInfo.mem.free; - - // Not avail on BSD, yet. - this.stats.systemAvgLoad = _.get(sysInfo, 'currentLoad.avgload', 0); - this.stats.systemCurrentLoad = _.get(sysInfo, 'currentLoad.currentLoad', 0); - }) - .catch(err => { - return cb(err); - }); + return cb(null); } _refreshNodeStatus(cb) { @@ -147,15 +202,15 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } - const nodeStatusItems = getActiveConnectionList(false).slice(0, nodeStatusView.height).map(ac => { + const nodeStatusItems = getActiveConnectionList(false).slice(0, nodeStatusView.dimens.height).map(ac => { // Handle pre-authenticated if (!ac.authenticated) { - ac.text = ac.username = 'Pre Auth'; + ac.text = ac.userName = 'Pre Auth'; ac.action = 'Logging In'; } return Object.assign(ac, { - timeOn : _.upperFirst(ac.timeOn.humanize()), // make friendly + timeOn : _.upperFirst((ac.timeOn || moment.duration(0)).humanize()), // make friendly }); }); @@ -164,5 +219,57 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } + + _refreshQuickLog(cb) { + const quickLogView = this.viewControllers.main.getView(MciViewIds.main.quickLogView); + if (!quickLogView) { + return cb(null); + } + + const records = this.logRingBuffer.records; + if (records.length === 0) { + return cb(null); + } + + const hasChanged = this.lastLogTime !== records[records.length - 1].time; + this.lastLogTime = records[records.length - 1].time; + + if (!hasChanged) { + return cb(null); + } + + const quickLogTimestampFormat = this.config.quickLogTimestampFormat || + this.getDateTimeFormat('short'); + + const levelIndicators = this.config.quickLogLevelIndicators || + { + trace : 'T', + debug : 'D', + info : 'I', + warn : 'W', + error : 'E', + fatal : 'F', + }; + + const makeLevelIndicator = (level) => { + return levelIndicators[bunyan.nameFromLevel[level]] || '?'; + }; + + const logItems = records.map(rec => { + return { + timestamp : moment(rec.time).format(quickLogTimestampFormat), + level : rec.level, + levelIndicator : makeLevelIndicator(rec.level), + nodeId : rec.nodeId, + sessionId : rec.sessionId || '', + message : rec.msg, + }; + }); + + quickLogView.setItems(logItems); + quickLogView.redraw(); + + return cb(null); + } }; From a64bbb163564310573a9e72494a4dbfdba9b9db7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 23:01:05 -0700 Subject: [PATCH 09/52] * Don't crash attempting to get desc from menu stack * Remove ring buffer monitor at WFC exit --- core/client_connections.js | 10 +++++++++- core/wfc.js | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/client_connections.js b/core/client_connections.js index 744f1f2a..393446b3 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -36,11 +36,19 @@ function getActiveConnectionList(authUsersOnly) { const now = moment(); return _.map(getActiveConnections(authUsersOnly), ac => { + let action; + try { + // attempting to fetch a bad menu stack item can blow up/assert + action = _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'); + } catch(e) { + action = 'Unknown'; + } + const entry = { node : ac.node, authenticated : ac.user.isAuthenticated(), userId : ac.user.userId, - action : _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'), + action : action, serverName : ac.session.serverName, isSecure : ac.session.isSecure, }; diff --git a/core/wfc.js b/core/wfc.js index 65db0df2..4f8dedb8 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -103,6 +103,10 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } leave() { + _.remove(Log.log.streams, stream => { + return stream.name === 'wfc-ringbuffer'; + }); + this._stopRefreshing(); super.leave(); From 7aafb0b0c45ba7e6688e7662c48303cbba784c02 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 7 Dec 2020 19:52:54 -0700 Subject: [PATCH 10/52] Checkpoint --- art/themes/luciano_blocktronics/theme.hjson | 11 +++- core/wfc.js | 63 +++++++++++++-------- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index b533dffc..3054f4e5 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -248,7 +248,16 @@ mainMenuWaitingForCaller: { config: { - quickLogLevel: warn + nowDateTimeFormat: "|00|11dddd|08, |11MMMM Do YYYY |08/ |11h|08:|11mm|08:|11ss|03a" + lastLoginDateTimeFormat: "|00|11ddd h|08:|11mm|03a" + + mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03mail|08: " + mainInfoFormat11: "|00|10{callsToday:>5}" + mainInfoFormat12: "|00|10{postsToday:>5}" + + mainInfoFormat19: "|00|10{lastLoginUserName:<19} |02{lastLogin}" + + quickLogLevel: info quickLogLevelIndicators: { trace : |00|02T debug: |00|03D diff --git a/core/wfc.js b/core/wfc.js index 4f8dedb8..4d9c951b 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -4,6 +4,7 @@ const { MenuModule } = require('./menu_module'); const { getActiveConnectionList } = require('./client_connections'); const StatLog = require('./stat_log'); const SysProps = require('./system_property'); +const UserProps = require('./user_property'); const Log = require('./logger'); const Config = require('./config.js').get; @@ -71,9 +72,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return callback(null); } - const logLevel = this.config.quickLogLevel || - _.get(Config(), 'logging.rotatingFile.level') || - 'info'; + const logLevel = this.config.quickLogLevel || // WFC specific + _.get(Config(), 'logging.rotatingFile.level') || // ...or system setting + 'info'; // ...or default to info this.logRingBuffer = new bunyan.RingBuffer({ limit : quickLogView.dimens.height || 24 }); Log.log.addStream({ @@ -86,10 +87,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return callback(null); }, (callback) => { - return this._refreshStats(callback); - }, - (callback) => { - return this._refreshNodeStatus(callback); + return this._refreshAll(callback); } ], err => { @@ -136,6 +134,14 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { }, (callback) => { return this._refreshQuickLog(callback); + }, + (callback) => { + this.updateCustomViewTextsWithFilter( + 'main', + MciViewIds.main.customRangeStart, + this.stats + ); + return callback(null); } ], err => { @@ -147,20 +153,22 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } _refreshStats(cb) { - const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); - const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats); - const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats); + const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); + const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats); + const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats); + const lastLoginStats = StatLog.getSystemStat(SysProps.LastLogin); + + const now = moment(); // Some stats we can just fill right away this.stats = { // Date/Time - date : moment().format(this.getDateFormat()), - time : moment().format(this.getTimeFormat()), - dateTime : moment().format(this.getDateTimeFormat()), + nowDate : now.format(this.getDateFormat()), + nowTime : now.format(this.getTimeFormat()), + now : now.format(this._dateTimeFormat('now')), // Current process (our Node.js service) processUptimeSeconds : process.uptime(), -// processUptime : moment.duration(process.uptime(), 'seconds').humanize(), // Totals totalCalls : StatLog.getSystemStatNum(SysProps.LoginCount), @@ -174,22 +182,22 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // totalDownloads : // totalDownloadBytes : - // :TODO: lastCaller - // :TODO: totalMemoryBytes, freeMemoryBytes - // :TODO: CPU info/averages/load - // Today's Stats callsToday : StatLog.getSystemStatNum(SysProps.LoginsToday), postsToday : StatLog.getSystemStatNum(SysProps.MessagesToday), - // uploadsToday : - // uploadBytesToday : - // downloadsToday : - // downloadBytesToday : + uploadsToday : StatLog.getSystemStatNum(SysProps.FileUlTodayCount), + uploadBytesToday : StatLog.getSystemStatNum(SysProps.FileUlTodayBytes), + downloadsToday : StatLog.getSystemStatNum(SysProps.FileDlTodayCount), + downloadsBytesToday : StatLog.getSystemStatNum(SysProps.FileDlTodayBytes), // Current - // lastCaller : - // lastCallerDate - // lastCallerTime + currentUserName : this.client.user.username, + currentUserRealName : this.client.user.getProperty(UserProps.RealName) || this.client.user.username, + lastLoginUserName : lastLoginStats.userName, + lastLoginRealName : lastLoginStats.realName, + lastLoginDate : moment(lastLoginStats.timestamp).format(this.getDateFormat()), + lastLoginTime : moment(lastLoginStats.timestamp).format(this.getTimeFormat()), + lastLogin : moment(lastLoginStats.timestamp).format(this._dateTimeFormat('lastLogin')), totalMemoryBytes : sysMemStats.totalBytes, freeMemoryBytes : sysMemStats.freeBytes, @@ -275,5 +283,10 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } + + _dateTimeFormat(element) { + const format = this.config[`${element}DateTimeFormat`]; + return format || this.getDateFormat(); + } }; From 3f7b0295bafaa2d12411d058b289ef29aabcd48d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 9 Dec 2020 18:54:51 -0700 Subject: [PATCH 11/52] Cleanup --- core/wfc.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/wfc.js b/core/wfc.js index 4d9c951b..7559388d 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -12,7 +12,6 @@ const Config = require('./config.js').get; const async = require('async'); const _ = require('lodash'); const moment = require('moment'); -const SysInfo = require('systeminformation'); const bunyan = require('bunyan'); exports.moduleInfo = { @@ -160,7 +159,6 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { const now = moment(); - // Some stats we can just fill right away this.stats = { // Date/Time nowDate : now.format(this.getDateFormat()), From 3d070ddf35ef6cffd69ab99801edc86cea39197b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 8 Apr 2022 17:58:48 -0600 Subject: [PATCH 12/52] Temporary WFC WIP screen --- art/themes/luciano_blocktronics/wfc.ans | Bin 0 -> 2364 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 art/themes/luciano_blocktronics/wfc.ans diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans new file mode 100644 index 0000000000000000000000000000000000000000..b682f92a9caabfe8eef3cc0bfbc8d544d7263396 GIT binary patch literal 2364 zcmbtVOK;jh5GJ>ZV{a{&y>P3@?%LoWPR3A#Vjit+s)!S6ofOquS3pwrcaeK4g`dz1 z{WYC^`B7}=fDhI_X1Ry|KjV2(!O%XAm}>4Ti|` zZ9nk%0cMBN?}@7epy>!yfDj1CpAGCAPk`hxo^l5F!g_NtA)uCEh|?_S9^3%mDRWj) zw3P%fYv~Vksi4DAFx#J+fb*{QupE6JZ+8{w^gQ%HMd(s-an&Aa8m*RyhB%gdjPoou zmYg(CIef67BGDpJW@_q;fDQhqlueB=$cwDaZDR(!Rhl|tU!0~ZohEdeCl$Da7FefR zoTEYMN>k5r?b@D?9NTq=AG35zi}~=##Q-&5i86H4@Fxnk;U-@o2`8(JlMIQ>=M@(R zkJFATBnd81k`mLAs3>6|tT74mXTI%vG7>qikGaNp<`RT?H=aM7r219eF3?Vc1VCwb zs**^mIz^_;AXqjLDo8$ehq6h?;>|Bu!;F$GE{;vy?PxQTKHTz53!}H2Fx29@wfLSH zU`j}!Q6KH%-73 zQmWw=N$9o_TYpDUy!wJx4|iIyio)l9jKk-?{QH z-yAswu#oh9vEs4twfqo_oonRIMrbh}e&UY5*h0z^E=w4^0}ucx=v|B+)-Yvm@1qFv zeYe&1dMJ4`p}?)ooZut?QZ$Z=v(-t*NbZ`44;WkRl6!Ssre1wr9KG`JSKb`!Z#_G3 zh8Xi#?1gW7?|qBoDBNV^I#y<*mK0NrEzKt4 Date: Tue, 12 Apr 2022 22:16:42 -0600 Subject: [PATCH 13/52] Cleanup and handle stats that are not yet ready --- core/wfc.js | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/core/wfc.js b/core/wfc.js index 7559388d..6260f6ec 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -152,9 +152,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } _refreshStats(cb) { - const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats); - const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats); - const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats); + const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || {}; + const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || {}; + const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats) || {}; const lastLoginStats = StatLog.getSystemStat(SysProps.LastLogin); const now = moment(); @@ -197,10 +197,10 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { lastLoginTime : moment(lastLoginStats.timestamp).format(this.getTimeFormat()), lastLogin : moment(lastLoginStats.timestamp).format(this._dateTimeFormat('lastLogin')), - totalMemoryBytes : sysMemStats.totalBytes, - freeMemoryBytes : sysMemStats.freeBytes, - systemAvgLoad : sysLoadStats.average, - systemCurrentLoad : sysLoadStats.current, + totalMemoryBytes : sysMemStats.totalBytes || 0, + freeMemoryBytes : sysMemStats.freeBytes || 0, + systemAvgLoad : sysLoadStats.average || 0, + systemCurrentLoad : sysLoadStats.current || 0, }; return cb(null); @@ -212,16 +212,18 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } - const nodeStatusItems = getActiveConnectionList(false).slice(0, nodeStatusView.dimens.height).map(ac => { - // Handle pre-authenticated - if (!ac.authenticated) { - ac.text = ac.userName = 'Pre Auth'; - ac.action = 'Logging In'; - } + const nodeStatusItems = getActiveConnectionList(false) + .slice(0, nodeStatusView.dimens.height) + .map(ac => { + // Handle pre-authenticated + if (!ac.authenticated) { + ac.text = ac.userName = '*Pre Auth*'; + ac.action = 'Logging In'; + } - return Object.assign(ac, { - timeOn : _.upperFirst((ac.timeOn || moment.duration(0)).humanize()), // make friendly - }); + return Object.assign(ac, { + timeOn : _.upperFirst((ac.timeOn || moment.duration(0)).humanize()), // make friendly + }); }); nodeStatusView.setItems(nodeStatusItems); @@ -248,7 +250,8 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } - const quickLogTimestampFormat = this.config.quickLogTimestampFormat || + const quickLogTimestampFormat = + this.config.quickLogTimestampFormat || this.getDateTimeFormat('short'); const levelIndicators = this.config.quickLogLevelIndicators || From dd7d24f22e16c3ab991c8891a8847e1f8393e27b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 1 May 2022 12:41:20 -0600 Subject: [PATCH 14/52] Many WFC related improvements (WIP) * Update systeminformation to 5.x * More work on WFC display of basic stats -- nearly complete * Disable idle timeout when on WFC --- WHATSNEW.md | 7 ++++++- art/themes/luciano_blocktronics/theme.hjson | 18 ++++++++++++++---- art/themes/luciano_blocktronics/wfc.ans | Bin 2364 -> 2987 bytes core/stat_log.js | 6 +++--- core/view.js | 4 ++-- core/wfc.js | 6 ++++++ docs/_docs/art/mci.md | 6 +++--- package.json | 2 +- yarn.lock | 8 ++++---- 9 files changed, 39 insertions(+), 18 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 4ae304d6..da29647a 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -2,11 +2,16 @@ This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. ## 0.0.13-beta -* 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! +* Removed terminal `cursor position reports` from most locations in the code. This should greatly increase the number of terminal programs that work with ENiGMA½. 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` +* New Waiting For Caller (WFC) support via the `wfc.js` module. +* Many new system statistics available via the StatLog such as current and average load, memory, etc. +* Many new MCI codes: `MB`, `MF`, `LA`, `CL`, `UU`, `FT`, `DD`, `FB`, `DB`, `LC`, `LT`, `LD`, and more. See [MCI](./docs/art/mci.md). +* SyncTERM style font support detection. * 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 * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](./docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). * Development now occurs against [Node.js 14 LTS](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V14.md). diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 4353a1fe..1e2f9b4a 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -249,13 +249,21 @@ mainMenuWaitingForCaller: { config: { nowDateTimeFormat: "|00|11dddd|08, |11MMMM Do YYYY |08/ |11h|08:|11mm|08:|11ss|03a" - lastLoginDateTimeFormat: "|00|11ddd h|08:|11mm|03a" + lastLoginDateTimeFormat: "|00|10ddd hh|08:|10mm|02a" mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03mail|08: " mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" - mainInfoFormat19: "|00|10{lastLoginUserName:<19} |02{lastLogin}" + mainInfoFormat18: "|00|10{lastLoginUserName:<27} |02{lastLogin}" + + mainInfoFormat19: "|00|10{freeMemoryBytes!sizeWithoutAbbr} |02{freeMemoryBytes!sizeAbbr} free |08/ |10{totalMemoryBytes!sizeWithoutAbbr} |02{totalMemoryBytes!sizeAbbr}" + mainInfoFormat20: "|00|10{systemCurrentLoad} |02% |08/ |10{systemAvgLoad} |02load avg|08." + mainInfoFormat21: "|00|10{processUptimeSeconds!durationSeconds}" + + mainInfoFormat23: "|00|10{totalCalls:>5}" + mainInfoFormat24: "|00|10{totalPosts:>5}" + mainInfoFormat22: "|00|10{totalFiles} |08/ |10{totalFileBytes!sizeWithoutAbbr} |02{totalFileBytes!sizeAbbr}" quickLogLevel: info quickLogLevelIndicators: { @@ -269,15 +277,17 @@ } 0: { mci: { + TL19: { width: 23 } + TL20: { width: 23 } VM1: { height: 5 widht: 37 - itemFormat: "|00|11{node:<3.2} |10{userName:>13} |08> |04{action:<14.13} |14{serverName}" + itemFormat: "|00|11{node:<3.2} |10{userName:>13} |08> |02{action:<14.13} |14{serverName}" } VM2: { height: 5 width: 73 - itemFormat: "{levelIndicator} |15{timestamp} |07{message}" + itemFormat: "{levelIndicator} |15{timestamp} |07{message:<51.50}" } } } diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index b682f92a9caabfe8eef3cc0bfbc8d544d7263396..a8f5087a5e4f5592e7b71163f647b2706d3594a4 100644 GIT binary patch literal 2987 zcmcgu&2H2%5Z-buPXK%IrPsxFw!5I7(rloHr8b8sud|VDu@ee*6CgzKYPr}HJ z;;&QdCFfUCp+MS1i8B~rHaJ|W zw(_=>c(mYDZsgyCqGG?CmX${(N(mSF)}a-f>32$z3v9m>i{h+ zw&u5ctlB`ULQ)!z-LOZH9?)3zQzoKCcx&J)<5@;D5 zU^HvE5)rrh05Izka}?|J101I6JOIYF#GF94hHUE}Hg1e!u19-i@AY6AzYN{j^QozS ww0X|2!*5xZCYy$1G;SF3lT*&=DP`x+d%dHhW$#4~pJ&ta8ChMprB%KE15$Ac?*IS* literal 2364 zcmbtVOK;jh5GJ>ZV{a{&y>P3@?%LoWPR3A#Vjit+s)!S6ofOquS3pwrcaeK4g`dz1 z{WYC^`B7}=fDhI_X1Ry|KjV2(!O%XAm}>4Ti|` zZ9nk%0cMBN?}@7epy>!yfDj1CpAGCAPk`hxo^l5F!g_NtA)uCEh|?_S9^3%mDRWj) zw3P%fYv~Vksi4DAFx#J+fb*{QupE6JZ+8{w^gQ%HMd(s-an&Aa8m*RyhB%gdjPoou zmYg(CIef67BGDpJW@_q;fDQhqlueB=$cwDaZDR(!Rhl|tU!0~ZohEdeCl$Da7FefR zoTEYMN>k5r?b@D?9NTq=AG35zi}~=##Q-&5i86H4@Fxnk;U-@o2`8(JlMIQ>=M@(R zkJFATBnd81k`mLAs3>6|tT74mXTI%vG7>qikGaNp<`RT?H=aM7r219eF3?Vc1VCwb zs**^mIz^_;AXqjLDo8$ehq6h?;>|Bu!;F$GE{;vy?PxQTKHTz53!}H2Fx29@wfLSH zU`j}!Q6KH%-73 zQmWw=N$9o_TYpDUy!wJx4|iIyio)l9jKk-?{QH z-yAswu#oh9vEs4twfqo_oonRIMrbh}e&UY5*h0z^E=w4^0}ucx=v|B+)-Yvm@1qFv zeYe&1dMJ4`p}?)ooZut?QZ$Z=v(-t*NbZ`44;WkRl6!Ssre1wr9KG`JSKb`!Z#_G3 zh8Xi#?1gW7?|qBoDBNV^I#y<*mK0NrEzKt4 { return stream.name === 'wfc-ringbuffer'; }); this._stopRefreshing(); + this.client.startIdleMonitor(); super.leave(); } diff --git a/docs/_docs/art/mci.md b/docs/_docs/art/mci.md index 43c08162..3d7a0c19 100644 --- a/docs/_docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -97,8 +97,8 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `DB` | Total download amount *today* (formatted to appropriate bytes/megs/etc. ) | | `MB` | System memory | | `MF` | System _free_ memory | -| `LA` | System load average (e.g. 0.25)
(Not available for all platforms) | -| `CL` | System current load percentage
(Not available for all platforms) | +| `LA` | System load average (e.g. 0.25)
(May not be available on some platforms) | +| `CL` | System current load percentage
(May not be available on some platforms) | | `UU` | System uptime in friendly format | | `LC` | Last caller to the system (username) | | `LT` | Time of last caller | @@ -135,7 +135,7 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `BT` | Button | A button | ...it's a button. See [Button](views/button_view.md) | | `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists. See [Vertical Menu](views/vertical_menu_view.md) | | `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar. See [Horizontal Menu](views/horizontal_menu_view.md) | -| `FM` | Full Menu | A menu that can go both vertical and horizontal. | See [Full Menu](views/full_menu_view.md) | +| `FM` | Full Menu | A menu that can go both vertical and horizontal. | See [Full Menu](views/full_menu_view.md) | | `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options. See [Spinner Menu](views/spinner_menu_view.md) | | `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input. See [Toggle Menu](views/toggle_menu_view.md)| | `PL` | Predefined Label | Show environment information | See [Predefined Label](views/predefined_label_view.md)| diff --git a/package.json b/package.json index efb1ea5d..8dbb5a6a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "uuid-parse": "1.1.0", "ws": "7.4.3", "yazl": "^2.5.1", - "systeminformation" : "^4.27.5" + "systeminformation" : "^5.11.14" }, "devDependencies": { "eslint": "^8.13.0" diff --git a/yarn.lock b/yarn.lock index e3d6607f..3a038a02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,10 +1821,10 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -systeminformation@^4.27.5: - version "4.34.23" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.34.23.tgz#54c54ced5adc49c27cda953b73c0819ed56edd45" - integrity sha512-33+lQwlLxXoxy0o9WLOgw8OjbXeS3Jv+pSl+nxKc2AOClBI28HsdRPpH0u9Xa9OVjHLT9vonnOMw1ug7YXI0dA== +systeminformation@^5.11.14: + version "5.11.14" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.14.tgz#21fcb6f05d33e17d69c236b9c1b3d9c53d1d2b3a" + integrity sha512-m8CJx3fIhKohanB0ExTk5q53uI1J0g5B09p77kU+KxnxRVpADVqTAwCg1PFelqKsj4LHd+qmVnumb511Hg4xow== tar@^4: version "4.4.19" From 44505f664a7e1d88a544bc8017c48d4fb48a6d65 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 1 May 2022 17:40:31 -0600 Subject: [PATCH 15/52] Updates and add start of WFC doc --- art/themes/luciano_blocktronics/theme.hjson | 22 +++++++++------ art/themes/luciano_blocktronics/wfc.ans | Bin 2987 -> 2985 bytes core/stat_log.js | 4 +-- docs/_docs/modding/wfc.md | 29 ++++++++++++++++++++ 4 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 docs/_docs/modding/wfc.md diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 1e2f9b4a..a7fd3863 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -254,15 +254,19 @@ mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03mail|08: " mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" + mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr} |02{uploadBytesToday!sizeAbbr}" + mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadsBytesToday!sizeWithoutAbbr} |02{downloadsBytesToday!sizeAbbr}" - mainInfoFormat18: "|00|10{lastLoginUserName:<27} |02{lastLogin}" + mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" - mainInfoFormat19: "|00|10{freeMemoryBytes!sizeWithoutAbbr} |02{freeMemoryBytes!sizeAbbr} free |08/ |10{totalMemoryBytes!sizeWithoutAbbr} |02{totalMemoryBytes!sizeAbbr}" - mainInfoFormat20: "|00|10{systemCurrentLoad} |02% |08/ |10{systemAvgLoad} |02load avg|08." - mainInfoFormat21: "|00|10{processUptimeSeconds!durationSeconds}" + mainInfoFormat16: "" - mainInfoFormat23: "|00|10{totalCalls:>5}" - mainInfoFormat24: "|00|10{totalPosts:>5}" + mainInfoFormat17: "|00|10{freeMemoryBytes!sizeWithoutAbbr} |02{freeMemoryBytes!sizeAbbr} free |08/ |10{totalMemoryBytes!sizeWithoutAbbr} |02{totalMemoryBytes!sizeAbbr}" + mainInfoFormat18: "|00|10{systemCurrentLoad} |02% |08/ |10{systemAvgLoad} |02load avg|08." + mainInfoFormat19: "|00|10{processUptimeSeconds!durationSeconds}" + + mainInfoFormat20: "|00|10{totalCalls:>5}" + mainInfoFormat21: "|00|10{totalPosts:>5}" mainInfoFormat22: "|00|10{totalFiles} |08/ |10{totalFileBytes!sizeWithoutAbbr} |02{totalFileBytes!sizeAbbr}" quickLogLevel: info @@ -277,8 +281,10 @@ } 0: { mci: { - TL19: { width: 23 } - TL20: { width: 23 } + TL17: { width: 23 } + TL18: { width: 23 } + TL19: { width: 10 } + VM1: { height: 5 widht: 37 diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index a8f5087a5e4f5592e7b71163f647b2706d3594a4..bf07f2fa0f41c5c12fa6551984df8a1e4a8d9e8a 100644 GIT binary patch delta 205 zcmZ22zEXUH0@GwOHUU;+>1c!8$@{q^C+D%VO)g;em|VcfF-oa7T{7tR*}!hIe7__yd+eOsRB^BL9RYrlNmN+FEF!CzQCeB`8IP4qd8Oo+vG|X zZbpmAg(zQCf*2zMXb1%un+BuF Date: Sun, 1 May 2022 19:58:00 -0600 Subject: [PATCH 16/52] More docs, total user count MCI and stat --- art/themes/luciano_blocktronics/theme.hjson | 7 ++++--- art/themes/luciano_blocktronics/wfc.ans | Bin 2985 -> 2973 bytes core/bbs.js | 12 +++++++++++ core/predefined_mci.js | 4 ++++ core/system_property.js | 2 ++ core/user.js | 13 ++++++++++++ core/wfc.js | 2 +- docs/_docs/art/mci.md | 1 + docs/_docs/modding/wfc.md | 21 +++++++++++++++++++- 9 files changed, 57 insertions(+), 5 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index a7fd3863..9a7d208c 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -266,8 +266,9 @@ mainInfoFormat19: "|00|10{processUptimeSeconds!durationSeconds}" mainInfoFormat20: "|00|10{totalCalls:>5}" - mainInfoFormat21: "|00|10{totalPosts:>5}" - mainInfoFormat22: "|00|10{totalFiles} |08/ |10{totalFileBytes!sizeWithoutAbbr} |02{totalFileBytes!sizeAbbr}" + mainInfoFormat21: "|00|10{totalPosts:>7}" + mainInfoFormat22: "|00|10{totalUsers:>5}" + mainInfoFormat23: "|00|10{totalFiles} |08/ |10{totalFileBytes!sizeWithoutAbbr} |02{totalFileBytes!sizeAbbr}" quickLogLevel: info quickLogLevelIndicators: { @@ -283,7 +284,7 @@ mci: { TL17: { width: 23 } TL18: { width: 23 } - TL19: { width: 10 } + TL19: { width: 13 } VM1: { height: 5 diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index bf07f2fa0f41c5c12fa6551984df8a1e4a8d9e8a..198ba676ad229d4ba1360823eec03d4d0cf38a9d 100644 GIT binary patch delta 43 zcmV+`0M!4f7o8WdKL?YL0R{pxGL!KK6O(WX29v-B1(R_HcmXrB>Iazv0W_0@3nehr B4SfIr delta 40 wcmbO$zEXUHJ^SR_Y+URnMh4Q+2Dy{{xMU_Lu-7sgZNA4olbO+Bas{_40R4&!`Tzg` diff --git a/core/bbs.js b/core/bbs.js index ebcd0c9d..8d5fc2f1 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -354,6 +354,18 @@ function initialize(cb) { }); }); }, + function initUserCount(callback) { + const User = require('./user.js'); + User.getUserCount((err, count) => { + if(err) { + return callback(err); + } + + const StatLog = require('./stat_log'); + StatLog.setNonPersistentSystemStat(SysProps.TotalUserCount, count); + return callback(null); + }); + }, function initMessageStats(callback) { return require('./message_area.js').startup(callback); }, diff --git a/core/predefined_mci.js b/core/predefined_mci.js index b24918b6..364a1e1c 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -304,6 +304,10 @@ const PREDEFINED_MCI_GENERATORS = { // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) // :TODO: ?? - Total users on system + TU : function totalSystemUsers() { + return StatLog.getSystemStatNum(SysProps.TotalUserCount) || 1; + }, + LC : function lastCallerUserName() { // Obv/2 const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {}; return lastLogin.userName || 'N/A'; diff --git a/core/system_property.js b/core/system_property.js index 67049552..f39b3a27 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -37,4 +37,6 @@ module.exports = { SystemMemoryStats : 'system_memory_stats', // object { totalBytes, freeBytes }; non-persistent SystemLoadStats : 'system_load_stats', // object { average, current }; non-persistent + + TotalUserCount : 'user_total_count', // non-persistent }; diff --git a/core/user.js b/core/user.js index 4dad3399..99f587c9 100644 --- a/core/user.js +++ b/core/user.js @@ -793,6 +793,19 @@ module.exports = class User { ); } + static getUserCount(cb) { + userDb.get( + `SELECT count() AS user_count + FROM user;`, + (err, row) => { + if(err) { + return cb(err); + } + return cb(null, row.user_count); + } + ); + } + static getUserList(options, cb) { const userList = []; diff --git a/core/wfc.js b/core/wfc.js index a94bfb42..59e398b2 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -177,7 +177,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // Totals totalCalls : StatLog.getSystemStatNum(SysProps.LoginCount), totalPosts : StatLog.getSystemStatNum(SysProps.MessageTotalCount), - //totalUsers : + totalUsers : StatLog.getSystemStatNum(SysProps.TotalUserCount), totalFiles : fileAreaStats.totalFiles || 0, totalFileBytes : fileAreaStats.totalFileBytes || 0, diff --git a/docs/_docs/art/mci.md b/docs/_docs/art/mci.md index 3d7a0c19..05fa9dd5 100644 --- a/docs/_docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -103,6 +103,7 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `LC` | Last caller to the system (username) | | `LT` | Time of last caller | | `LD` | Date of last caller | +| `TU` | Total number of users on the system | Some additional special case codes also exist: diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index 3f07054a..d4dd73b0 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -6,6 +6,24 @@ title: Waiting For Caller (WFC) The `wfc.js` module provides a Waiting For Caller (WFC) type dashboard from a bygone era. ENiGMA½'s WFC can be accessed over secure connections for accounts with the proper ACS. See **Security** information. ## Security +The system allows any user with the proper security to access the WFC / system operator functionality. The security policy is enforced by ACS with the default of `SCAF2ID1GM[wfc]`, meaning the following are true: + +1. Securely Connected (such as SSH or Secure WebSocket, but not Telnet) +2. Auth Factor 2+. That is, the user has 2FA enabled. +3. User ID of 1 (root/admin) +4. The user belongs to the `wfc` group. + +To change the ACS required, specify a alternative `acs` in the `config` block. For example: +```hjson +mainMenuWaitingForCaller: { + // ... + config: { + acs: SCID1GM[sysops] + } +} +``` + +:information_source: ENiGMA½ will enforce ACS of at least `SC` (secure connection) ## Theming The following MCI codes are available: @@ -26,4 +44,5 @@ The following MCI codes are available: * `{now}`: Current date and/or time in `nowDateTimeFormat` format. * `{processUptimeSeconds}`: Process (the BBS) uptime in seconds. * `{totalCalls}`: Total calls to the system. - * `{totalPosts}`: Total posts to the system. \ No newline at end of file + * `{totalPosts}`: Total posts to the system. + * `{totalUsers}`: Total users on the system. \ No newline at end of file From 09927a6ec1c9bc2362f964cf541487f13bb3df34 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 May 2022 10:25:59 -0600 Subject: [PATCH 17/52] Tidy --- core/wfc.js | 3 ++- docs/_docs/modding/wfc.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/wfc.js b/core/wfc.js index 59e398b2..36ed52e9 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -35,6 +35,7 @@ const MciViewIds = { // Secure + 2FA + root user + 'wfc' group. const DefaultACS = 'SCAF2ID1GM[wfc]'; +const MainStatRefreshTimeMs = 5000; // 5s exports.getModule = class WaitingForCallerModule extends MenuModule { constructor(options) { @@ -118,7 +119,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { _startRefreshing() { this.mainRefreshTimer = setInterval( () => { this._refreshAll(); - }, 5000); + }, MainStatRefreshTimeMs); } _stopRefreshing() { diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index d4dd73b0..d8a2fbe6 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -18,6 +18,7 @@ To change the ACS required, specify a alternative `acs` in the `config` block. F mainMenuWaitingForCaller: { // ... config: { + // initial +op over secure connection only acs: SCID1GM[sysops] } } From bb86f386e9328fd569e1de2a44987d77298a1e02 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 4 May 2022 10:48:31 -0600 Subject: [PATCH 18/52] Currently no required MCI codes --- core/wfc.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/wfc.js b/core/wfc.js index 36ed52e9..66bf291f 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -60,12 +60,6 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { (callback) => { return this.prepViewController('main', FormIds.main, mciData.menu, callback); }, - (callback) => { - // const requiredCodes = [ - // ]; - // return this.validateMCIByViewIds('main', requiredCodes, callback); - return callback(null); - }, (callback) => { const quickLogView = this.viewControllers.main.getView(MciViewIds.main.quickLogView); if (!quickLogView) { From 9e5b3369a50ddf9545e0c2020529f84df81c9507 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 May 2022 10:48:40 -0600 Subject: [PATCH 19/52] New live stat: Total new users today * Add NT (Obv/2 throwback) MCI for new users today * Keep live stat up to date in stat log * Exposed via WFC --- art/themes/luciano_blocktronics/theme.hjson | 16 ++++++++++++---- core/abracadabra.js | 2 +- core/bbs.js | 11 +++++------ core/client_connections.js | 2 +- core/connect.js | 2 +- core/event_scheduler.js | 2 +- core/misc_scheduled_events.js | 7 ++++++- core/predefined_mci.js | 4 +++- core/sys_event_user_log.js | 2 ++ core/system_property.js | 1 + core/user_login.js | 4 ++-- core/wfc.js | 17 +++++++++++++---- docs/_docs/art/mci.md | 1 + 13 files changed, 49 insertions(+), 22 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 9a7d208c..d89b4951 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -259,7 +259,7 @@ mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" - mainInfoFormat16: "" + mainInfoFormat16: "|00|10{newUsersToday}" mainInfoFormat17: "|00|10{freeMemoryBytes!sizeWithoutAbbr} |02{freeMemoryBytes!sizeAbbr} free |08/ |10{totalMemoryBytes!sizeWithoutAbbr} |02{totalMemoryBytes!sizeAbbr}" mainInfoFormat18: "|00|10{systemCurrentLoad} |02% |08/ |10{systemAvgLoad} |02load avg|08." @@ -279,22 +279,30 @@ error: |00|12E fatal: |00|28F } + quickLogLevelMessagePrefixes: { + trace : |00|02 + debug: |00|03 + info: |00|07 + warn: |00|14 + error: |00|12 + fatal: |00|28 + } } 0: { mci: { TL17: { width: 23 } TL18: { width: 23 } - TL19: { width: 13 } + TL19: { width: 14 } VM1: { height: 5 - widht: 37 + width: 36 itemFormat: "|00|11{node:<3.2} |10{userName:>13} |08> |02{action:<14.13} |14{serverName}" } VM2: { height: 5 width: 73 - itemFormat: "{levelIndicator} |15{timestamp} |07{message:<51.50}" + itemFormat: "|00|07{nodeId} {levelIndicator} |02{timestamp} {message:<51.50}" } } } diff --git a/core/abracadabra.js b/core/abracadabra.js index a8a72b1d..9dc4b79d 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -109,7 +109,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { name : self.config.name, activeCount : activeDoorNodeInstances[self.config.name] }, - 'Too many active instances'); + `Too many active instances of door "${self.config.name}"`); if(_.isString(self.config.tooManyArt)) { theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { diff --git a/core/bbs.js b/core/bbs.js index 8d5fc2f1..e1273130 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -295,10 +295,11 @@ function initialize(cb) { const StatLog = require('./stat_log'); const entries = [ - [ UserLogNames.UlFiles, [ SysProps.FileUlTodayCount, 'count' ] ], - [ UserLogNames.UlFileBytes, [ SysProps.FileUlTodayBytes, 'obj' ] ], - [ UserLogNames.DlFiles, [ SysProps.FileDlTodayCount, 'count' ] ], - [ UserLogNames.DlFileBytes, [ SysProps.FileDlTodayBytes, 'obj' ] ], + [ UserLogNames.UlFiles, [ SysProps.FileUlTodayCount, 'count' ] ], + [ UserLogNames.UlFileBytes, [ SysProps.FileUlTodayBytes, 'obj' ] ], + [ UserLogNames.DlFiles, [ SysProps.FileDlTodayCount, 'count' ] ], + [ UserLogNames.DlFileBytes, [ SysProps.FileDlTodayBytes, 'obj' ] ], + [ UserLogNames.NewUser, [ SysProps.NewUsersTodayCount, 'count' ] ], ]; async.each(entries, (entry, nextEntry) => { @@ -310,8 +311,6 @@ function initialize(cb) { date : moment(), }; - filter.logName = logName; - StatLog.findUserLogEntries(filter, (err, stat) => { if (!err) { if (resultType === 'obj') { diff --git a/core/client_connections.js b/core/client_connections.js index 393446b3..ca5061e6 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -105,7 +105,7 @@ function addNewClient(client, clientSock) { connInfo.family = clientSock.localFamily; } - client.log.info(connInfo, 'Client connected'); + client.log.info(connInfo, `Client connected (${connInfo.port}/${connInfo.serverName})`); Events.emit( Events.getSystemEvents().ClientConnected, diff --git a/core/connect.js b/core/connect.js index 7301e098..5aba7795 100644 --- a/core/connect.js +++ b/core/connect.js @@ -150,7 +150,7 @@ 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('Enabling SyncTERM font support'); client.term.syncTermFontsEnabled = true; } }, diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 4a464cd8..8ea9bcde 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -108,7 +108,7 @@ class ScheduledEvent { } 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 "${this.name}"...`); if('method' === this.action.type) { const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') diff --git a/core/misc_scheduled_events.js b/core/misc_scheduled_events.js index 1650b697..fbcd84ce 100644 --- a/core/misc_scheduled_events.js +++ b/core/misc_scheduled_events.js @@ -10,7 +10,12 @@ function dailyMaintenanceScheduledEvent(args, cb) { // // Various stats need reset daily // - [ SysProps.LoginsToday, SysProps.MessagesToday ].forEach(prop => { + // :TODO: files/etc. here + const resetProps = [ + SysProps.LoginsToday, SysProps.MessagesToday, SysProps.NewUsersTodayCount, + ]; + + resetProps.forEach(prop => { StatLog.setNonPersistentSystemStat(prop, 0); }); diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 364a1e1c..c43cef43 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -299,8 +299,10 @@ const PREDEFINED_MCI_GENERATORS = { const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTodayBytes); return formatByteSize(byteSize, true); // true=withAbbr }, + NT : function totalNewUsersToday() { // Obv/2 + return StatLog.getSystemStatNum(SysProps.NewUsersTodayCount); + }, - // :TODO: NT - New users today (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) // :TODO: ?? - Total users on system diff --git a/core/sys_event_user_log.js b/core/sys_event_user_log.js index 63ae0e55..57f17659 100644 --- a/core/sys_event_user_log.js +++ b/core/sys_event_user_log.js @@ -3,6 +3,7 @@ const Events = require('./events.js'); const LogNames = require('./user_log_name.js'); +const SysProps = require('./system_property.js'); const DefaultKeepForDays = 365; @@ -26,6 +27,7 @@ module.exports = function systemEventUserLogInit(statLog) { const detailHandler = { [ systemEvents.NewUser ] : (e) => { append(e, LogNames.NewUser, 1); + statLog.incrementNonPersistentSystemStat(SysProps.NewUsersTodayCount, 1); }, [ systemEvents.UserLogin ] : (e) => { append(e, LogNames.Login, 1); diff --git a/core/system_property.js b/core/system_property.js index f39b3a27..bbd8e058 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -39,4 +39,5 @@ module.exports = { SystemLoadStats : 'system_load_stats', // object { average, current }; non-persistent TotalUserCount : 'user_total_count', // non-persistent + NewUsersTodayCount : 'user_new_today_count', // non-persistent }; diff --git a/core/user_login.js b/core/user_login.js index 8c9e41b8..2f8c502d 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -86,7 +86,7 @@ function userLogin(client, username, password, options, cb) { username : user.username, userId : user.userId }, - 'Already logged in' + `User ${user.username} already logged in` ); return cb(Errors.BadLogin( @@ -104,7 +104,7 @@ function userLogin(client, username, password, options, cb) { } ); - client.log.info('Successful login'); + client.log.info(`User ${user.username} successfully logged in`); // User's unique session identifier is the same as the connection itself user.sessionId = client.session.uniqueId; // convenience diff --git a/core/wfc.js b/core/wfc.js index 66bf291f..6e0c6c94 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -188,6 +188,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { uploadBytesToday : StatLog.getSystemStatNum(SysProps.FileUlTodayBytes), downloadsToday : StatLog.getSystemStatNum(SysProps.FileDlTodayCount), downloadsBytesToday : StatLog.getSystemStatNum(SysProps.FileDlTodayBytes), + newUsersToday : StatLog.getSystemStatNum(SysProps.NewUsersTodayCount), // Current currentUserName : this.client.user.username, @@ -265,18 +266,26 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { fatal : 'F', }; + const makeLevelIndicator = (level) => { - return levelIndicators[bunyan.nameFromLevel[level]] || '?'; + return levelIndicators[level] || '?'; + }; + + const quickLogLevelMessagePrefixes = this.config.quickLogLevelMessagePrefixes || {}; + const prefixMssage = (message, level) => { + const prefix = quickLogLevelMessagePrefixes[level] || ''; + return `${prefix}${message}`; }; const logItems = records.map(rec => { + const level = bunyan.nameFromLevel[rec.level]; return { timestamp : moment(rec.time).format(quickLogTimestampFormat), level : rec.level, - levelIndicator : makeLevelIndicator(rec.level), - nodeId : rec.nodeId, + levelIndicator : makeLevelIndicator(level), + nodeId : rec.nodeId || '*', sessionId : rec.sessionId || '', - message : rec.msg, + message : prefixMssage(rec.msg, level), }; }); diff --git a/docs/_docs/art/mci.md b/docs/_docs/art/mci.md index 05fa9dd5..b1eb3395 100644 --- a/docs/_docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -104,6 +104,7 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `LT` | Time of last caller | | `LD` | Date of last caller | | `TU` | Total number of users on the system | +| `NT` | Total *new* users *today* | Some additional special case codes also exist: From 18420fd7a73e29d5ab16c6f1a2085dfe3100390d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 May 2022 11:19:18 -0600 Subject: [PATCH 20/52] Additional information in WFC docs --- core/door.js | 2 +- docs/_docs/modding/wfc.md | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/door.js b/core/door.js index 59d18dff..99232183 100644 --- a/core/door.js +++ b/core/door.js @@ -32,7 +32,7 @@ module.exports = class Door { }); conn.once('error', err => { - this.client.log.info( { error : err.message }, 'Door socket server connection'); + this.client.log.warn( { error : err.message }, 'Door socket server connection'); return this.restoreIo(conn); }); diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index d8a2fbe6..edc747f9 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -3,16 +3,21 @@ layout: page title: Waiting For Caller (WFC) --- ## The Waiting For Caller (WFC) Module -The `wfc.js` module provides a Waiting For Caller (WFC) type dashboard from a bygone era. ENiGMA½'s WFC can be accessed over secure connections for accounts with the proper ACS. See **Security** information. +The `wfc.js` module provides a Waiting For Caller (WFC) type dashboard from a bygone era. Many traditional features are available including newer concepts for modern times. Node spy is left out as it feels like something that should be left in the past. + +## Accessing the WFC +By default, the WFC may be accessed via the `!WFC` main menu command when connected over a secure connection via a user with the proper ACS. This can be configured as per any other menu in the system. Note that ENiGMA½ does not expose the WFC as a standalone application as this would be much less flexible. To connect locally, simply use your favorite terminal or for example: `ssh -l yourname localhost 8889`. See **Security** below for more information. ## Security The system allows any user with the proper security to access the WFC / system operator functionality. The security policy is enforced by ACS with the default of `SCAF2ID1GM[wfc]`, meaning the following are true: 1. Securely Connected (such as SSH or Secure WebSocket, but not Telnet) -2. Auth Factor 2+. That is, the user has 2FA enabled. +2. [Auth Factor 2+](modding/user-2fa-otp-config.md). That is, the user has 2FA enabled. 3. User ID of 1 (root/admin) 4. The user belongs to the `wfc` group. +:information_source: Due to the above, the WFC screen is **disabled** by default as at a minimum, you'll need to add your user to the `wfc` group. + To change the ACS required, specify a alternative `acs` in the `config` block. For example: ```hjson mainMenuWaitingForCaller: { From bd28de9a69f46ae5f7490769faf7db30b31b97b6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 May 2022 11:47:04 -0600 Subject: [PATCH 21/52] Some logging cleanup --- core/file_transfer.js | 2 +- core/msg_area_post_fse.js | 2 +- core/nua.js | 4 ++-- core/telnet_bridge.js | 2 +- core/user_config.js | 7 ++++--- core/user_login.js | 10 +++++----- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/core/file_transfer.js b/core/file_transfer.js index bb341051..f2596fe2 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -144,7 +144,7 @@ exports.getModule = class TransferFileModule extends MenuModule { }); - this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); + this.client.log.info( { sentFiles : sentFiles }, `User "${self.client.user.username}" uploaded ${sentFiles.length} file(s)` ); } return cb(err); }); diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 5f25fa19..edb6f1f5 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -50,7 +50,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { // 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 }, - 'Message persisted' + `User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})` ); } diff --git a/core/nua.js b/core/nua.js index 2cb4c26b..9d65833e 100644 --- a/core/nua.js +++ b/core/nua.js @@ -117,7 +117,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { }; newUser.create(createUserInfo, err => { if(err) { - self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); + self.client.log.warn( { error : err, username : formData.value.username }, 'New user creation failed'); self.gotoMenu(extraArgs.error, err => { if(err) { @@ -126,7 +126,7 @@ exports.getModule = class NewUserAppModule extends MenuModule { 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 "${formData.value.username}" created`); // Cache SysOp information now // :TODO: Similar to bbs.js. DRY diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index b7ff09e1..ccb53d43 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -215,7 +215,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { self.client.removeListener('key press', connectionKeyPressHandler); if(err) { - self.client.log.info(`Telnet bridge connection error: ${err.message}`); + self.client.log.warn(`Telnet bridge connection error: ${err.message}`); } callback(clientTerminated ? new Error('Client connection terminated') : null); diff --git a/core/user_config.js b/core/user_config.js index 4ccd7017..3c7b3060 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -128,17 +128,18 @@ exports.getModule = class UserConfigModule extends MenuModule { // :TODO: warn end user! return self.prevMenu(cb); } + + self.client.log.info(`User "${self.client.user.username}" updated configuration`); + // // New password if it's not empty // - self.client.log.info('User updated properties'); - if(formData.value.password.length > 0) { self.client.user.setNewAuthCredentials(formData.value.password, err => { if(err) { self.client.log.error( { err : err }, 'Failed storing new authentication credentials'); } else { - self.client.log.info('User changed authentication credentials'); + self.client.log.info(`User "${self.client.user.username}" updated authentication credentials`); } return self.prevMenu(cb); }); diff --git a/core/user_login.js b/core/user_login.js index 2f8c502d..a5e2ece3 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -45,7 +45,7 @@ function userLogin(client, username, password, options, cb) { const config = Config(); if(config.users.badUserNames.includes(username.toLowerCase())) { - client.log.info( { username, ip : client.remoteAddress }, 'Attempt to login with banned username'); + client.log.info( { username, ip : client.remoteAddress }, `Attempt to login with banned username "${username}"`); // slow down a bit to thwart brute force attacks return setTimeout( () => { @@ -80,13 +80,13 @@ function userLogin(client, username, password, options, cb) { }); if(existingClientConnection) { - client.log.info( + client.log.warn( { existingNodeId : existingClientConnection.node, username : user.username, userId : user.userId }, - `User ${user.username} already logged in` + `User "${user.username}" already logged in on node ${existingClientConnection.node}` ); return cb(Errors.BadLogin( @@ -104,7 +104,7 @@ function userLogin(client, username, password, options, cb) { } ); - client.log.info(`User ${user.username} successfully logged in`); + client.log.info(`User "${user.username}" successfully logged in`); // User's unique session identifier is the same as the connection itself user.sessionId = client.session.uniqueId; // convenience @@ -238,6 +238,6 @@ function transformLoginError(err, client, username) { 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.warn( { username, ip : client.remoteAddress, reason : err.message }, `Failed login attempt for user "${username}", ${client.remoteAddress}`); return err; } \ No newline at end of file From f7788fc01c98bca50655dcca7a90ea3c29894a32 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 7 May 2022 17:13:10 -0600 Subject: [PATCH 22/52] WFC documentation updates --- art/themes/luciano_blocktronics/theme.hjson | 2 +- core/wfc.js | 2 +- docs/_docs/modding/wfc.md | 67 +++++++++++++++------ docs/_docs/modding/whos-online.md | 5 ++ 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index d89b4951..91ab2c69 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -255,7 +255,7 @@ mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr} |02{uploadBytesToday!sizeAbbr}" - mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadsBytesToday!sizeWithoutAbbr} |02{downloadsBytesToday!sizeAbbr}" + mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadBytesToday!sizeWithoutAbbr} |02{downloadsBytesToday!sizeAbbr}" mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" diff --git a/core/wfc.js b/core/wfc.js index 6e0c6c94..9e63f680 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -187,7 +187,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { uploadsToday : StatLog.getSystemStatNum(SysProps.FileUlTodayCount), uploadBytesToday : StatLog.getSystemStatNum(SysProps.FileUlTodayBytes), downloadsToday : StatLog.getSystemStatNum(SysProps.FileDlTodayCount), - downloadsBytesToday : StatLog.getSystemStatNum(SysProps.FileDlTodayBytes), + downloadBytesToday : StatLog.getSystemStatNum(SysProps.FileDlTodayBytes), newUsersToday : StatLog.getSystemStatNum(SysProps.NewUsersTodayCount), // Current diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index edc747f9..0b2e0b5e 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -6,7 +6,7 @@ title: Waiting For Caller (WFC) The `wfc.js` module provides a Waiting For Caller (WFC) type dashboard from a bygone era. Many traditional features are available including newer concepts for modern times. Node spy is left out as it feels like something that should be left in the past. ## Accessing the WFC -By default, the WFC may be accessed via the `!WFC` main menu command when connected over a secure connection via a user with the proper ACS. This can be configured as per any other menu in the system. Note that ENiGMA½ does not expose the WFC as a standalone application as this would be much less flexible. To connect locally, simply use your favorite terminal or for example: `ssh -l yourname localhost 8889`. See **Security** below for more information. +By default, the WFC may be accessed via the `!WFC` main menu command when connected over a secure connection via a user with the proper [ACS](../configuration/acs.md). This can be configured as per any other menu in the system. Note that ENiGMA½ does not expose the WFC as a standalone application as this would be much less flexible. To connect locally, simply use your favorite terminal or for example: `ssh -l yourname localhost 8889`. See **Security** below for more information. ## Security The system allows any user with the proper security to access the WFC / system operator functionality. The security policy is enforced by ACS with the default of `SCAF2ID1GM[wfc]`, meaning the following are true: @@ -21,7 +21,6 @@ The system allows any user with the proper security to access the WFC / system o To change the ACS required, specify a alternative `acs` in the `config` block. For example: ```hjson mainMenuWaitingForCaller: { - // ... config: { // initial +op over secure connection only acs: SCID1GM[sysops] @@ -33,22 +32,50 @@ mainMenuWaitingForCaller: { ## Theming The following MCI codes are available: -* MCI 1 (`VM1`): Node status list with the following format items available: - * `{text}`: Username or `*Pre Auth*`. - * `{action}`: Current action/menu. - * `{timeOn}`: How long the node has been connected. -* MCI 2 (`VM2`): Quick log with the following format keys available: - * `{timestamp}`: Log entry timestamp in `quickLogTimestampFormat` format. - * `{level}`: Log entry level from Bunyan. - * `{levelIndicator}`: `T` for TRACE, `D` for DEBUG, `I` for INFO, `W` for WARN, `E` for ERROR, or `F` for FATAL. - * `{nodeId}`: Node ID. - * `{sessionId}`: Session ID. - * `{message}`: Log message. +* `VM1`: Node status list with the following format items available: + * `text`: Username or `*Pre Auth*`. + * `action`: Current action/menu. + * `timeOn`: How long the node has been connected. +* `VM2`: Quick log with the following format keys available: + * `timestamp`: Log entry timestamp in `quickLogTimestampFormat` format. + * `level`: Log entry level from Bunyan. + * `levelIndicator`: Level indicators can be overridden with the `quickLogLevelIndicators` key (see defaults below) + * `quickLogLevelIndicators`: A **map** defaulting to the following`: + * `trace` : `T` + * `debug`: `D` + * `info`: `I` + * `warn`: `W` + * `error`: `E` + * `fatal`: `F` + * `nodeId`: Node ID. + * `sessionId`: Session ID. + * `quickLogLevelMessagePrefixes`: A **map** of log level names (see above) to message prefixes. Commonly used for changing message color with pipe codes, such as `|04` for red errors. + * `message`: Log message. * MCI 10...99: Custom entries with the following format keys available: - * `{nowDate}`: Current date in the `dateFormat` style, defaulting to `short`. - * `{nowTime}`: Current time in the `timeFormat` style, defaulting to `short`. - * `{now}`: Current date and/or time in `nowDateTimeFormat` format. - * `{processUptimeSeconds}`: Process (the BBS) uptime in seconds. - * `{totalCalls}`: Total calls to the system. - * `{totalPosts}`: Total posts to the system. - * `{totalUsers}`: Total users on the system. \ No newline at end of file + * `nowDate`: Current date in the `dateFormat` style, defaulting to `short`. + * `nowTime`: Current time in the `timeFormat` style, defaulting to `short`. + * `now`: Current date and/or time in `nowDateTimeFormat` format. + * `processUptimeSeconds`: Process (the BBS) uptime in seconds. + * `totalCalls`: Total calls to the system. + * `totalPosts`: Total posts to the system. + * `totalUsers`: Total users on the system. + * `totalFiles`: Total number of files on the system. + * `totalFileBytes`: Total size in bytes of the file base. + * `callsToday`: Number of calls today. + * `postsToday`: Number of posts today. + * `uploadsToday`: Number of uploads today. + * `uploadBytesToday`: Total size in bytes of uploads today. + * `downloadsToday`: Number of downloads today. + * `downloadsBytesToday`: Total size in bytes of uploads today. + * `newUsersToday`: Number of new users today. + * `currentUserName`: Current user name. + * `currentUserRealName`: Current user's real name. + * `lastLoginUserName`: Last login username. + * `lastLoginRealName`: Last login user's real name. + * `lastLoginDate`: Last login date in `dateFormat` format. + * `lastLoginTime`: Last login time in `timeFormat` format. + * `lastLogin`: Last login date/time. + * `totalMemoryBytes`: Total system memory in bytes. + * `freeMemoryBytes`: Free system memory in bytes. + * `systemAvgLoad`: System average load. + * `systemCurrentLoad`: System current load. \ No newline at end of file diff --git a/docs/_docs/modding/whos-online.md b/docs/_docs/modding/whos-online.md index 22fde0ff..963abeef 100644 --- a/docs/_docs/modding/whos-online.md +++ b/docs/_docs/modding/whos-online.md @@ -8,6 +8,7 @@ The built in `whos_online` module provides a basic who's online mod. ### Theming The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `userId`: User ID. +* `authenticated`: boolean if the client has a logged in user or not. * `userName`: Login username. * `node`: Node ID the user is connected to. * `timeOn`: A human friendly amount of time the user has been online. @@ -15,4 +16,8 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `location`: User's location. * `affiliation` or `affils`: Users affiliations. * `action`: Current action/view in the system taken from the `desc` field of the current MenuModule they are interacting with. For example, "Playing L.O.R.D". +* `isSecure`: Is the client securely connected? +* `serverName`: Name of connected server such as "Telnet" or "SSH". + +:information_source: These properties are available via the `client_connections.js` `getActiveConnectionList()` API. From 14be645de3a195acb806273fae4128b7f39c2631 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 8 May 2022 11:38:03 -0600 Subject: [PATCH 23/52] Minor updates --- art/themes/luciano_blocktronics/theme.hjson | 2 +- core/wfc.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 91ab2c69..7e59fa7a 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -255,7 +255,7 @@ mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr} |02{uploadBytesToday!sizeAbbr}" - mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadBytesToday!sizeWithoutAbbr} |02{downloadsBytesToday!sizeAbbr}" + mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadBytesToday!sizeWithoutAbbr} |02{downloadBytesToday!sizeAbbr}" mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" diff --git a/core/wfc.js b/core/wfc.js index 9e63f680..2c61e7b0 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -21,7 +21,7 @@ exports.moduleInfo = { }; const FormIds = { - main : 0, + main : 0, }; const MciViewIds = { From 24491000ad87ee6530b37a5e51fe4f2a99f85e5d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 8 May 2022 19:24:38 -0600 Subject: [PATCH 24/52] Update and add missing 'desc' to menu templates --- misc/menu_templates/file_base.in.hjson | 10 ++++++---- misc/menu_templates/login.in.hjson | 22 ++++++++++++++++------ misc/menu_templates/main.in.hjson | 15 ++++++++------- misc/menu_templates/message_base.in.hjson | 17 ++++++++++++----- misc/menu_templates/new_user.in.hjson | 9 ++++++--- misc/menu_templates/private_mail.in.hjson | 3 ++- 6 files changed, 50 insertions(+), 26 deletions(-) diff --git a/misc/menu_templates/file_base.in.hjson b/misc/menu_templates/file_base.in.hjson index afd497d0..8b102c68 100644 --- a/misc/menu_templates/file_base.in.hjson +++ b/misc/menu_templates/file_base.in.hjson @@ -56,8 +56,8 @@ } fileBaseListEntries: { - module: file_area_list desc: Browsing Files + module: file_area_list config: { art: { browse: FBRWSE @@ -575,8 +575,8 @@ } fileBaseSearch: { - module: file_base_search desc: Searching Files + module: file_base_search art: FSEARCH form: { 0: { @@ -648,8 +648,8 @@ } fileBaseSetNewScanDate: { - module: set_newscan_date desc: File Base + module: set_newscan_date art: SETFNSDATE config: { target: file @@ -679,6 +679,7 @@ } fileBaseExportListFilter: { + desc: File List Export module: file_base_search art: FBLISTEXPSEARCH config: { @@ -754,6 +755,7 @@ } fileBaseExportList: { + desc: File List Export module: file_base_user_list_export art: FBLISTEXP config: { @@ -812,7 +814,7 @@ // default menu entry used by the 'file_base_download_manager' module // for protocol selection fileTransferProtocolSelection: { - desc: Protocol selection + desc: Protocol Selection module: file_transfer_protocol_select art: FPROSEL form: { diff --git a/misc/menu_templates/login.in.hjson b/misc/menu_templates/login.in.hjson index c3fc5340..e708bea5 100644 --- a/misc/menu_templates/login.in.hjson +++ b/misc/menu_templates/login.in.hjson @@ -4,6 +4,7 @@ // Send telnet connections to matrix where users can login, apply, etc. // telnetConnected: { + desc: Telnet Connect art: CONNECT next: matrix config: { nextTimeout: 1500 } @@ -15,6 +16,7 @@ // depending on user ACS. // sshConnected: { + desc: SSH Connect art: CONNECT next: [ { @@ -34,6 +36,7 @@ // application process. // sshConnectedNewUser: { + desc: SSH Connect art: CONNECT next: newUserApplicationPreSsh config: { nextTimeout: 1500 } @@ -41,6 +44,7 @@ // Ye ol' standard matrix matrix: { + desc: Login Matrix art: matrix form: { 0: { @@ -106,6 +110,7 @@ } login: { + desc: Login art: USERLOG next: [ { @@ -156,6 +161,7 @@ } loginAttemptTooNode: { + desc: Already Logged In art: TOONODE config: { cls: true @@ -165,6 +171,7 @@ } loginAttemptAccountLocked: { + desc: Account Locked art: ACCOUNTLOCKED config: { cls: true @@ -174,6 +181,7 @@ } loginAttemptAccountDisabled: { + desc: Account Disabled art: ACCOUNTDISABLED config: { cls: true @@ -183,6 +191,7 @@ } loginAttemptAccountInactive: { + desc: Inactive Account art: ACCOUNTINACTIVE config: { cls: true @@ -192,7 +201,7 @@ } forgotPassword: { - desc: Forgot password + desc: Forgot Password prompt: forgotPasswordPrompt submit: [ { @@ -204,7 +213,7 @@ } forgotPasswordSubmitted: { - desc: Forgot password + desc: Forgot Password art: FORGOTPWSENT config: { cls: true @@ -240,7 +249,7 @@ } fullLoginSequenceOnelinerz: { - desc: Viewing Onelinerz + desc: Onelinerz module: onelinerz next: [ { @@ -350,7 +359,7 @@ } fullLoginSequenceNewScan: { - desc: Performing New Scan + desc: New Scan module: new_scan art: NEWSCAN next: fullLoginSequenceSysStats @@ -360,7 +369,7 @@ } newScanMessageList: { - desc: New Messages + desc: New Message List module: msg_list art: NEWMSGS config: { @@ -403,8 +412,8 @@ } newScanFileBaseList: { - module: file_area_list desc: New Files + module: file_area_list config: { art: { browse: FNEWBRWSE @@ -557,6 +566,7 @@ } loginTwoFactorAuthOTP: { + desc: 2FA art: 2FAOTP next: fullLoginSequenceLoginArt form: { diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson index f2caef56..24651adc 100644 --- a/misc/menu_templates/main.in.hjson +++ b/misc/menu_templates/main.in.hjson @@ -73,8 +73,8 @@ menus: { mainMenu: { - art: MMENU desc: Main Menu + art: MMENU prompt: menuCommand config: { font: cp437 @@ -414,6 +414,7 @@ } mainMenuUserConfig: { + desc: User Config module: user_config art: CONFSCR form: { @@ -504,7 +505,7 @@ } mainMenuGlobalNewScan: { - desc: Performing New Scan + desc: New Scan module: new_scan art: NEWSCAN config: { @@ -513,7 +514,7 @@ } mainMenuFeedbackToSysOp: { - desc: Feedback to SysOp + desc: SysOp Feedback module: msg_area_post_fse config: { art: { @@ -802,7 +803,7 @@ } bbsList: { - desc: Viewing BBS List + desc: BBS List module: bbs_list config: { cls: true @@ -920,8 +921,8 @@ } fullLogoffSequencePreAd: { - art: PRELOGAD desc: Logging Off + art: PRELOGAD next: fullLogoffSequenceRandomBoardAd config: { cls: true @@ -930,8 +931,8 @@ } fullLogoffSequenceRandomBoardAd: { - art: OTHRBBS desc: Logging Off + art: OTHRBBS next: logoff config: { baudRate: 57600 @@ -941,8 +942,8 @@ } logoff: { - art: LOGOFF desc: Logging Off + art: LOGOFF next: @systemMethod:logoff } diff --git a/misc/menu_templates/message_base.in.hjson b/misc/menu_templates/message_base.in.hjson index 367e1903..d20d3077 100644 --- a/misc/menu_templates/message_base.in.hjson +++ b/misc/menu_templates/message_base.in.hjson @@ -1,8 +1,8 @@ { menus: { messageBaseMainMenu: { + desc: Message Menu art: MSGMNU - desc: Message Area prompt: messageBaseMenuPrompt config: { interrupt: realtime @@ -68,7 +68,7 @@ } messageBaseNewPost: { - desc: Posting message, + desc: Posting Message module: msg_area_post_fse config: { art: { @@ -184,6 +184,7 @@ } messageBaseChangeCurrentConference: { + desc: Changing Confs art: CCHANGE module: msg_conf_list form: { @@ -209,6 +210,7 @@ } messageBaseChangeCurrentArea: { + desc: Message Area List art: CHANGE module: msg_area_list form: { @@ -234,6 +236,7 @@ } messageBaseMessageList: { + desc: Message List module: msg_list art: MSGLIST config: { @@ -262,8 +265,8 @@ } messageBaseSetNewScanDate: { + desc: New Scan Update module: set_newscan_date - desc: Message Base art: SETMNSDATE config: { target: message @@ -297,7 +300,7 @@ } messageBaseSearch: { - desc: Message Search + desc: Searching Messages module: message_base_search art: MSEARCH config: { @@ -360,7 +363,7 @@ } messageBaseSearchResultsMessageList: { - desc: Message Search + desc: Searching Messages module: msg_list art: MSRCHLST config: { @@ -474,6 +477,7 @@ } messageAreaViewPost: { + desc: Viewing Message module: msg_area_view_fse config: { art: { @@ -599,6 +603,7 @@ } messageAreaReplyPost: { + desc: Replying to Message module: msg_area_post_fse config: { art: { @@ -764,6 +769,7 @@ // conferences using the conference tag as an art spec. // changeMessageConfPreArt: { + desc: Viewing Art module: show_art config: { method: messageConf @@ -781,6 +787,7 @@ // areas using the area tag as an art spec. // changeMessageAreaPreArt: { + desc: Viewing Art module: show_art config: { method: messageArea diff --git a/misc/menu_templates/new_user.in.hjson b/misc/menu_templates/new_user.in.hjson index 084e3788..a574a0d0 100644 --- a/misc/menu_templates/new_user.in.hjson +++ b/misc/menu_templates/new_user.in.hjson @@ -2,9 +2,9 @@ menus: { // A quick preamble - defaults to warning about broken terminals newUserApplicationPre: { + desc: Applying art: NEWUSER1 next: newUserApplication - desc: Applying config: { pause: true cls: true @@ -13,6 +13,7 @@ } newUserApplication: { + desc: Applying module: nua art: NUA next: [ @@ -112,9 +113,9 @@ // A quick preamble - defaults to warning about broken terminals (SSH version) newUserApplicationPreSsh: { + desc: Applying art: NEWUSER1 next: newUserApplicationSsh - desc: Applying config: { pause: true cls: true @@ -127,6 +128,7 @@ // Canceling this form logs off vs falling back to matrix // newUserApplicationSsh: { + desc: Applying module: nua art: NUA fallback: logoff @@ -221,13 +223,14 @@ } newUserFeedbackToSysOpPreamble: { + desc: Applying art: LETTER config: { pause: true } next: newUserFeedbackToSysOp } newUserFeedbackToSysOp: { - desc: Feedback to SysOp + desc: SysOp Feedback module: msg_area_post_fse next: [ { diff --git a/misc/menu_templates/private_mail.in.hjson b/misc/menu_templates/private_mail.in.hjson index ebf4d104..e27314dc 100644 --- a/misc/menu_templates/private_mail.in.hjson +++ b/misc/menu_templates/private_mail.in.hjson @@ -1,8 +1,8 @@ { menus: { privateMailMenu: { - art: MAILMNU desc: Private Mail + art: MAILMNU prompt: menuCommand config: { interrupt: realtime @@ -142,6 +142,7 @@ } privateMailMenuInbox: { + desc: Viewing Inbox module: msg_list art: PRVMSGLIST config: { From 6502f3b55ec7b896b09d18bac25561c1b9a19729 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 8 May 2022 22:15:57 -0600 Subject: [PATCH 25/52] Initial support for user status flags (NotAvail, NotVisible, ...) --- core/client_connections.js | 24 +++++++++++++++--------- core/node_msg.js | 2 +- core/user.js | 27 +++++++++++++++++++++++++-- core/wfc.js | 21 ++++++++++++++++++++- core/whos_online.js | 2 +- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/core/client_connections.js b/core/client_connections.js index ca5061e6..4d73a3ad 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -21,21 +21,24 @@ exports.getConnectionByNodeId = getConnectionByNodeId; const clientConnections = []; exports.clientConnections = clientConnections; -function getActiveConnections(authUsersOnly = false) { +function getActiveConnections(options = { authUsersOnly: true, visibleOnly: true }) { return clientConnections.filter(conn => { - return ((authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly); + if (options.authUsersOnly && !conn.user.isAuthenticated()) { + return false; + } + if (options.visibleOnly && !conn.user.isVisible()) { + return false; + } + + return true; + //return ((options.authUsersOnly && conn.user.isAuthenticated()) || !options.authUsersOnly); }); } -function getActiveConnectionList(authUsersOnly) { - - if(!_.isBoolean(authUsersOnly)) { - authUsersOnly = true; - } - +function getActiveConnectionList(options = { authUsersOnly: true, visibleOnly: true }) { const now = moment(); - return _.map(getActiveConnections(authUsersOnly), ac => { + return _.map(getActiveConnections(options), ac => { let action; try { // attempting to fetch a bad menu stack item can blow up/assert @@ -51,6 +54,8 @@ function getActiveConnectionList(authUsersOnly) { action : action, serverName : ac.session.serverName, isSecure : ac.session.isSecure, + isVisible : ac.user.isVisible(), + isAvailable : ac.user.isAvailable(), }; // @@ -66,6 +71,7 @@ function getActiveConnectionList(authUsersOnly) { const diff = now.diff(moment(ac.user.properties[UserProps.LastLoginTs]), 'minutes'); entry.timeOn = moment.duration(diff, 'minutes'); } + return entry; }); } diff --git a/core/node_msg.js b/core/node_msg.js index bb64757c..08f67333 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -204,7 +204,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { location : 'N/A', affils : 'N/A', timeOn : 'N/A', - }].concat(getActiveConnectionList(true) + }].concat(getActiveConnectionList() .map(node => Object.assign(node, { text : -1 == node.node ? '-ALL-' : node.node.toString() } )) ).filter(node => node.node !== this.client.node); // remove our client's node this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node diff --git a/core/user.js b/core/user.js index 99f587c9..0206e868 100644 --- a/core/user.js +++ b/core/user.js @@ -23,8 +23,6 @@ const moment = require('moment'); const sanatizeFilename = require('sanitize-filename'); const ssh2 = require('ssh2'); -exports.isRootUserId = function(id) { return 1 === id; }; - module.exports = class User { constructor() { this.userId = 0; @@ -32,6 +30,7 @@ module.exports = class User { this.properties = {}; // name:value this.groups = []; // group membership(s) this.authFactor = User.AuthFactors.None; + this.statusFlags = User.StatusFlags.None; } // static property accessors @@ -73,6 +72,14 @@ module.exports = class User { }; } + static get StatusFlags() { + return { + None : 0x00000000, + NotAvailable : 0x00000001, // Not currently available for chat, message, page, etc. + NotVisible : 0x00000002, // Invisible -- does not show online, last callers, etc. + } + } + isAuthenticated() { return true === this.authenticated; } @@ -121,6 +128,22 @@ module.exports = class User { return sanatizeFilename(name) || `user${this.userId.toString()}`; } + isAvailable() { + return (this.statusFlags & User.StatusFlags.NotAvailable) == 0; + } + + isVisible() { + return (this.statusFlags & User.StatusFlags.NotVisible) == 0; + } + + setVisibility(visible) { + if (visible) { + this.statusFlags &= ~User.StatusFlags.NotVisible; + } else { + this.statusFlags |= User.StatusFlags.NotVisible; + } + } + getLegacySecurityLevel() { if(this.isRoot() || this.isGroupMember('sysops')) { return 100; diff --git a/core/wfc.js b/core/wfc.js index 2c61e7b0..ba66685f 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -96,6 +96,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { enter() { this.client.stopIdleMonitor(); + this._applyOpVisibility(); super.enter(); } @@ -104,12 +105,30 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return stream.name === 'wfc-ringbuffer'; }); + this._restoreOpVisibility(); + this._stopRefreshing(); this.client.startIdleMonitor(); super.leave(); } + _applyOpVisibility() { + const vis = this.config.opVisibility || 'current'; + this.restoreUserIsVisible = this.client.user.isVisible(); + + switch (vis) { + case 'hidden' : this.client.user.setVisibility(false); break; + case 'visible' : this.client.user.setVisibility(true); break; + default : break; + } + + } + + _restoreOpVisibility() { + this.client.user.setVisibility(this.restoreUserIsVisible); + } + _startRefreshing() { this.mainRefreshTimer = setInterval( () => { this._refreshAll(); @@ -214,7 +233,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } - const nodeStatusItems = getActiveConnectionList(false) + const nodeStatusItems = getActiveConnectionList({authUsersOnly: false, visibleOnly: false}) .slice(0, nodeStatusView.dimens.height) .map(ac => { // Handle pre-authenticated diff --git a/core/whos_online.js b/core/whos_online.js index 5910bd29..c634d2bc 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -43,7 +43,7 @@ exports.getModule = class WhosOnlineModule extends MenuModule { return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.onlineList}`)); } - const onlineList = getActiveConnectionList(true).slice(0, onlineListView.height).map( + const onlineList = getActiveConnectionList().slice(0, onlineListView.height).map( oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) }) ); From 868e14aa8ee463894e384cb9ce46b1c57e6e0967 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 11 May 2022 20:30:25 -0600 Subject: [PATCH 26/52] New MCI codes & user status flags support additions * New MCI and WFC properties for user new private and "addressed to" mail * Additional support for user status flags in connection lists, etc. --- art/themes/luciano_blocktronics/theme.hjson | 3 +- core/abracadabra.js | 1 + core/bbs.js | 2 +- core/client_connections.js | 21 ++++++-- core/door.js | 2 +- core/menu_module.js | 2 +- core/message_area.js | 22 ++++++++ core/node_msg.js | 3 +- core/predefined_mci.js | 8 ++- core/stat_log.js | 59 ++++++++++++++++++++- core/user_interrupt_queue.js | 5 ++ core/user_property.js | 2 + core/wfc.js | 11 ++-- core/whos_online.js | 6 ++- docs/_docs/art/mci.md | 2 + docs/_docs/modding/wfc.md | 4 +- 16 files changed, 135 insertions(+), 18 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 7e59fa7a..5cdf0223 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -248,10 +248,11 @@ mainMenuWaitingForCaller: { config: { + quickLogTimestampFormat: "|01|02MM|08/|02DD hh:mm:ssa" nowDateTimeFormat: "|00|11dddd|08, |11MMMM Do YYYY |08/ |11h|08:|11mm|08:|11ss|03a" lastLoginDateTimeFormat: "|00|10ddd hh|08:|10mm|02a" - mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03mail|08: " + mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03mail|08: |11{newPrivateMail}|03 prv|08, |11{newMessagesAddrTo}|03 addr to" mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr} |02{uploadBytesToday!sizeAbbr}" diff --git a/core/abracadabra.js b/core/abracadabra.js index 9dc4b79d..b7315334 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -163,6 +163,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.client.term.write(ansi.resetScreen()); const exeInfo = { + name : this.config.name, cmd : this.config.cmd, cwd : this.config.cwd || paths.dirname(this.config.cmd), args : this.config.args, diff --git a/core/bbs.js b/core/bbs.js index e1273130..0703a8cc 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -141,7 +141,7 @@ function shutdownSystem() { [ function closeConnections(callback) { const ClientConns = require('./client_connections.js'); - const activeConnections = ClientConns.getActiveConnections(); + const activeConnections = ClientConns.getActiveConnections(ClientConns.AllConnections); let i = activeConnections.length; while(i--) { const activeTerm = activeConnections[i].term; diff --git a/core/client_connections.js b/core/client_connections.js index 4d73a3ad..1f24a584 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -21,7 +21,16 @@ exports.getConnectionByNodeId = getConnectionByNodeId; const clientConnections = []; exports.clientConnections = clientConnections; -function getActiveConnections(options = { authUsersOnly: true, visibleOnly: true }) { +const AllConnections = { authUsersOnly: false, visibleOnly: false, availOnly: false }; +exports.AllConnections = AllConnections; + +const UserVisibleConnections = { authUsersOnly: false, visibleOnly: true, availOnly: false }; +exports.UserVisibleConnections = UserVisibleConnections; + +const UserMessageableConnections = { authUsersOnly: true, visibleOnly: true, availOnly: true }; +exports.UserMessageableConnections = UserMessageableConnections; + +function getActiveConnections(options = { authUsersOnly: true, visibleOnly: true, availOnly: false }) { return clientConnections.filter(conn => { if (options.authUsersOnly && !conn.user.isAuthenticated()) { return false; @@ -29,13 +38,15 @@ function getActiveConnections(options = { authUsersOnly: true, visibleOnly: true if (options.visibleOnly && !conn.user.isVisible()) { return false; } + if (options.availOnly && !conn.user.isAvailable()) { + return false; + } return true; - //return ((options.authUsersOnly && conn.user.isAuthenticated()) || !options.authUsersOnly); }); } -function getActiveConnectionList(options = { authUsersOnly: true, visibleOnly: true }) { +function getActiveConnectionList(options = { authUsersOnly: true, visibleOnly: true, availOnly: false }) { const now = moment(); return _.map(getActiveConnections(options), ac => { @@ -149,9 +160,9 @@ function removeClient(client) { } function getConnectionByUserId(userId) { - return getActiveConnections().find( ac => userId === ac.user.userId ); + return getActiveConnections(AllConnections).find( ac => userId === ac.user.userId ); } function getConnectionByNodeId(nodeId) { - return getActiveConnections().find( ac => nodeId == ac.node ); + return getActiveConnections(AllConnections).find( ac => nodeId == ac.node ); } diff --git a/core/door.js b/core/door.js index 99232183..324813e1 100644 --- a/core/door.js +++ b/core/door.js @@ -74,7 +74,7 @@ module.exports = class Door { this.client.log.info( { cmd : exeInfo.cmd, args, io : this.io }, - 'Executing external door process' + `Executing external door (${exeInfo.name})` ); try { diff --git a/core/menu_module.js b/core/menu_module.js index 64fd56b5..0e4cfc2d 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -598,7 +598,7 @@ exports.MenuModule = class MenuModule extends PluginModule { if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { textView.addText(text); - } else { + } else if (textView.getData() != text) { textView.setText(text); } } diff --git a/core/message_area.js b/core/message_area.js index 30ad14ed..65c659f5 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -40,6 +40,7 @@ exports.filterMessageListByReadACS = filterMessageListByReadACS; exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; exports.getMessageListForArea = getMessageListForArea; exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; +exports.getNewMessageCountAddressedToUser = getNewMessageCountAddressedToUser; exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; exports.getMessageAreaLastReadId = getMessageAreaLastReadId; @@ -489,6 +490,26 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) { }); } +// New message count -- for all areas available to the user +// that are addressed to that user (ie: matching username) +// Does NOT Include private messages. +function getNewMessageCountAddressedToUser(client, cb) { + const areaTags = getAllAvailableMessageAreaTags(client).filter(areaTag => areaTag !== Message.WellKnownAreaTags.Private); + + let newMessageCount = 0; + async.forEach(areaTags, (areaTag, nextAreaTag) => { + getMessageAreaLastReadId(client.user.userId, areaTag, (_, lastMessageId) => { + lastMessageId = lastMessageId || 0; + getNewMessageCountInAreaForUser(client.user.userId, areaTag, (err, count) => { + newMessageCount += count; + return nextAreaTag(err); + }); + }); + }, () => { + return cb(null, newMessageCount); + }); +} + function getNewMessagesInAreaForUser(userId, areaTag, cb) { getMessageAreaLastReadId(userId, areaTag, (err, lastMessageId) => { lastMessageId = lastMessageId || 0; @@ -509,6 +530,7 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) { }); } + function getMessageListForArea(client, areaTag, filter, cb) { if(!cb && _.isFunction(filter)) { diff --git a/core/node_msg.js b/core/node_msg.js index 08f67333..390812b5 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -6,6 +6,7 @@ const { MenuModule } = require('./menu_module.js'); const { getActiveConnectionList, getConnectionByNodeId, + UserMessageableConnections, } = require('./client_connections.js'); const UserInterruptQueue = require('./user_interrupt_queue.js'); const { getThemeArt } = require('./theme.js'); @@ -204,7 +205,7 @@ exports.getModule = class NodeMessageModule extends MenuModule { location : 'N/A', affils : 'N/A', timeOn : 'N/A', - }].concat(getActiveConnectionList() + }].concat(getActiveConnectionList(UserMessageableConnections) .map(node => Object.assign(node, { text : -1 == node.node ? '-ALL-' : node.node.toString() } )) ).filter(node => node.node !== this.client.node); // remove our client's node this.nodeList.sort( (a, b) => a.node - b.node ); // sort by node diff --git a/core/predefined_mci.js b/core/predefined_mci.js index c43cef43..130276de 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -186,6 +186,12 @@ const PREDEFINED_MCI_GENERATORS = { const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0; return moment.duration(minutes, 'minutes').humanize(); }, + NM : function userNewMessagesAddressedToCount(client) { + return StatLog.getUserStatNumByClient(client, UserProps.NewAddressedToMessageCount); + }, + NP : function userNewPrivateMailCount(client) { + return StatLog.getUserStatNumByClient(client, UserProps.NewPrivateMailCount); + }, // // Date/Time @@ -243,7 +249,7 @@ const PREDEFINED_MCI_GENERATORS = { return moment.duration(process.uptime(), 'seconds').humanize(); }, NV : function nodeVersion() { return process.version; }, - AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, + AN : function activeNodes() { return clientConnections.getActiveConnections(clientConnections.UserVisibleConnections).length.toString(); }, TC : function totalCalls() { return StatLog.getSystemStat(SysProps.LoginCount).toLocaleString(); }, TT : function totalCallsToday() { diff --git a/core/stat_log.js b/core/stat_log.js index 90246b4e..d36517cb 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -7,6 +7,8 @@ const { } = require('./database.js'); const Errors = require('./enig_error.js'); const SysProps = require('./system_property.js'); +const UserProps = require('./user_property'); +const Message = require('./message'); // deps const _ = require('lodash'); @@ -152,13 +154,25 @@ class StatLog { } getUserStat(user, statName) { - return user.properties[statName]; + return user.getProperty(statName); + } + + getUserStatByClient(client, statName) { + const stat = this.getUserStat(client.user, statName); + this._refreshUserStat(client, statName); + return stat; } getUserStatNum(user, statName) { return parseInt(this.getUserStat(user, statName)) || 0; } + getUserStatNumByClient(client, statName, ttlSeconds=10) { + const stat = this.getUserStatNum(client.user, statName); + this._refreshUserStat(client, statName, ttlSeconds); + return stat; + } + incrementUserStat(user, statName, incrementBy, cb) { incrementBy = incrementBy || 1; @@ -391,6 +405,49 @@ class StatLog { }); } + _refreshUserStat(client, statName, ttlSeconds) { + switch(statName) { + case UserProps.NewPrivateMailCount: + this._wrapUserRefreshWithCachedTTL(client, statName, this._refreshUserPrivateMailCount, ttlSeconds); + break; + + case UserProps.NewAddressedToMessageCount: + this._wrapUserRefreshWithCachedTTL(client, statName, this._refreshUserNewAddressedToMessageCount, ttlSeconds); + break; + } + } + + _wrapUserRefreshWithCachedTTL(client, statName, updateMethod, ttlSeconds) { + client.statLogRefreshCache = client.statLogRefreshCache || new Map(); + + const now = Math.floor(Date.now() / 1000); + const old = client.statLogRefreshCache.get(statName) || 0; + if (now < old + ttlSeconds) { + return; + } + + updateMethod(client); + client.statLogRefreshCache.set(statName, now); + } + + _refreshUserPrivateMailCount(client) { + const MsgArea = require('./message_area'); + MsgArea.getNewMessageCountInAreaForUser(client.user.userId, Message.WellKnownAreaTags.Private, (err, count) => { + if (!err) { + client.user.setProperty(UserProps.NewPrivateMailCount, count); + } + }); + } + + _refreshUserNewAddressedToMessageCount(client) { + const MsgArea = require('./message_area'); + MsgArea.getNewMessageCountAddressedToUser(client, (err, count) => { + if(!err) { + client.user.setProperty(UserProps.NewAddressedToMessageCount, count); + } + }); + } + _findLogEntries(logTable, filter, cb) { filter = filter || {}; if(!_.isString(filter.logName)) { diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index 67b880c8..28f52e54 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -29,6 +29,11 @@ module.exports = class UserInterruptQueue omitNodes = [ opts.omit ]; } omitNodes = omitNodes.map(n => _.isNumber(n) ? n : n.node); + const connOpts = { + authUsersOnly: true, + visibleOnly: true, + availOnly: true, + }; opts.clients = getActiveConnections(true).filter(ac => !omitNodes.includes(ac.node)); } if(!Array.isArray(opts.clients)) { diff --git a/core/user_property.js b/core/user_property.js index d55a0ebe..e37c729e 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -65,5 +65,7 @@ module.exports = { AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes + NewPrivateMailCount : 'new_private_mail_count', // non-persistent + NewAddressedToMessageCount : 'new_addr_to_msg_count', // non-persistent }; diff --git a/core/wfc.js b/core/wfc.js index ba66685f..be5b5e65 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -1,7 +1,10 @@ // ENiGMA½ const { MenuModule } = require('./menu_module'); -const { getActiveConnectionList } = require('./client_connections'); +const { + getActiveConnectionList, + AllConnections +} = require('./client_connections'); const StatLog = require('./stat_log'); const SysProps = require('./system_property'); const UserProps = require('./user_property'); @@ -36,6 +39,7 @@ const MciViewIds = { // Secure + 2FA + root user + 'wfc' group. const DefaultACS = 'SCAF2ID1GM[wfc]'; const MainStatRefreshTimeMs = 5000; // 5s +const MailCountTTLSeconds = 10; exports.getModule = class WaitingForCallerModule extends MenuModule { constructor(options) { @@ -217,11 +221,12 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { lastLoginDate : moment(lastLoginStats.timestamp).format(this.getDateFormat()), lastLoginTime : moment(lastLoginStats.timestamp).format(this.getTimeFormat()), lastLogin : moment(lastLoginStats.timestamp).format(this._dateTimeFormat('lastLogin')), - totalMemoryBytes : sysMemStats.totalBytes || 0, freeMemoryBytes : sysMemStats.freeBytes || 0, systemAvgLoad : sysLoadStats.average || 0, systemCurrentLoad : sysLoadStats.current || 0, + newPrivateMail : StatLog.getUserStatNumByClient(this.client, UserProps.NewPrivateMailCount, MailCountTTLSeconds), + newMessagesAddrTo : StatLog.getUserStatNumByClient(this.client, UserProps.NewAddressedToMessageCount, MailCountTTLSeconds), }; return cb(null); @@ -233,7 +238,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } - const nodeStatusItems = getActiveConnectionList({authUsersOnly: false, visibleOnly: false}) + const nodeStatusItems = getActiveConnectionList(AllConnections) .slice(0, nodeStatusView.dimens.height) .map(ac => { // Handle pre-authenticated diff --git a/core/whos_online.js b/core/whos_online.js index c634d2bc..2c1080ce 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -3,7 +3,9 @@ // ENiGMA½ const { MenuModule } = require('./menu_module.js'); -const { getActiveConnectionList } = require('./client_connections.js'); +const { + getActiveConnectionList, + UserVisibleConnections } = require('./client_connections.js'); const { Errors } = require('./enig_error.js'); // deps @@ -43,7 +45,7 @@ exports.getModule = class WhosOnlineModule extends MenuModule { return cb(Errors.MissingMci(`Missing online list MCI ${MciViewIds.onlineList}`)); } - const onlineList = getActiveConnectionList().slice(0, onlineListView.height).map( + const onlineList = getActiveConnectionList(UserVisibleConnections).slice(0, onlineListView.height).map( oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) }) ); diff --git a/docs/_docs/art/mci.md b/docs/_docs/art/mci.md index b1eb3395..a1a807d8 100644 --- a/docs/_docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -105,6 +105,8 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `LD` | Date of last caller | | `TU` | Total number of users on the system | | `NT` | Total *new* users *today* | +| `NM` | Count of new messages **address to the current user** across all message areas in which they have access | +| `NP` | Count of new private mail to the current user | Some additional special case codes also exist: diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index 0b2e0b5e..eb632b31 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -78,4 +78,6 @@ The following MCI codes are available: * `totalMemoryBytes`: Total system memory in bytes. * `freeMemoryBytes`: Free system memory in bytes. * `systemAvgLoad`: System average load. - * `systemCurrentLoad`: System current load. \ No newline at end of file + * `systemCurrentLoad`: System current load. + * `newPrivateMail`: Number of new **privae** mail for current user. + * `newMessagesAddrTo`: Number of new messages **addressed to the current user**. \ No newline at end of file From 2b3d5be3d9de85f42a4466a98b1b359d8b055de5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 24 May 2022 19:46:39 -0600 Subject: [PATCH 27/52] Add MCI codes, helpers, and format keys for user availability and visibility * New MCI codes: IA and IV * availInicator and visIndicator to WFC format keys * New helpers for availalbe and visible indicators (see themes) --- art/themes/luciano_blocktronics/STATUS.ANS | Bin 4639 -> 5083 bytes art/themes/luciano_blocktronics/theme.hjson | 6 ++-- core/predefined_mci.js | 8 ++++++ core/theme.js | 10 ++++++- core/wfc.js | 29 +++++++++++++++++++- docs/_docs/art/mci.md | 2 ++ docs/_docs/art/themes.md | 2 ++ docs/_docs/modding/wfc.md | 5 +++- 8 files changed, 57 insertions(+), 5 deletions(-) diff --git a/art/themes/luciano_blocktronics/STATUS.ANS b/art/themes/luciano_blocktronics/STATUS.ANS index dc2b0ca88cc1f74e440a91bba90cf1758ad776d4..e16186216f19073d439b6c46cf759b65ebac5d3c 100644 GIT binary patch literal 5083 zcmb_gv2Ghj5S5Y6ZJHozu3;@6NgtE2Ql!KX3=2dQx>2P_bizPODWRkyUuFAuc=7#8 z-g`5*-9ix4o6w>vxg-n^OFl@Blb;qY?U55?hS8R~WWY3imv#B9-iU^0OzW?D1+ zYG_&%{I+M&F7zdu(jra0Y`0vmJDd1+uo#3I*2A$uuz~iScDu$}xI2D19NQEq#HWZD z;I0bGyPLU!(K|n_A~xHofk*uFM^V%@f7S&8kl~V-$D?H$@@;;Avk6F<^VxYBokyI{ zgNs&r3QxYP25LaTbb~Mc?0_0TI1GVug@_7F6|UaD-+|Lm!mc;_d6aJq+kWUoZ%3M7 z{m10iRBGyVgQ1}`Ehi)VKt9Nbj}hGBd#q`^04Tm}XUj|PSK;@+@6+&(7?Wpj z_WISAg?Wob%r3;TAr1iMCIk*NP;yAbh~`*hU{f0dVy!|W$ea?(YC>)zh3P?9+}b1bopB(ZCz;!dIx&ox6*IuF9N#7wY|hu2rPw<`ugog(q^G+|wx zj46|^(ZhM~)eqf=5wmZEuVn#3lBzLHY-SvqLg$Ryf~#e}Ra;RgMzC3XIB{gqS;Pef zRk*)fJ#2HqMA4gGe4kn8EUX=*EXCIv@yE!I3~MM|B4Ms4LF*U{P{ zEMWmMqyz*4Y;-z_VsSTR)G7I8r37l~{irCdVG-r~CeCup!^uXfaC3CK(%X& zouELJbr6A%84nO8DDp^5xohj{&t_qLONIaXCI`dK)?qC}Q zYG(a(_k+G6DGte4dG>nyIB4I|VJs@CU5%L_Cs-T1M_8eDrfYDxEXK%{U zMso7buu+i_5ehPD4wo~{P)brOzg2 zf8%TnW(EW>SjzE2BV1nFNC)x3tAQnwXMMm-=|q=WlJvtXx$)GZ7)nsMN@ET{t5Xfz zJX#N7u;b|2+F(9hUH`F|Kh48@$%~0a8(w*n;cIGliF}Skyk$s0KBL5n1QaxDgHB3z zl$GixGckt)jwG+N@*zA7oB2`zkeYb}%;Z^#xB=ZT90)_|1$w={dt5%4M;w7>(mOt} zpu};{W9{pn9804QUALmd?Jx-3I*xZxGBx8mD@lbx%K=hBF<7v5*153cq= zxF|}T6j<%3KN_?Gt(fL&CJM#gTW2PPjr zbit+|#0f+pb7v8jQ4Aj3IQ>l5I6~d?n6MeDn$kWbVlg#`zH*X{v6XnZy4N_&#K}}R zHii{EODIlziKZVP&3IXq^mobD%>;jbbUe6=EG#Voc78N&{-*}dqkj1I;&^uQQ@`(+ n{*T9?KN$314Mu-`eQ@yN#q{8t1AV?a`|)k@?)9;~rSJa$el$6T literal 4639 zcmb_g%Wf1$6g6A#qHLnD>CLQ}o*8-ouN;#ggs`kQlMO3mn}mqAtr)V%R}mKZneNcP zlJmH?y2qZd$_UMLb=AFf&bf~&bN|)+U^PEj&fDt#tGeyG@iX*8fB)6I#rygQUtEND z;{rSlTvS#3_HPv?57R<3vg!BXt?blw;rkI*E*EVN!{r*A(URjBKHeTKxm-}XZoeu{ z(rBmvM*sLzRejH&z5>qhw=7&n2Nk$4oA&bR{6Z?sj-NI}M-Kv1taBfD#N2?d9*^m} zG1h@xfU%*b5*Cji9$X56eTu;o>9Mf#v(0fGiY%rSw_*TY{t6y}WazsAZ&qV4c(yxbjgzx;l!H`{oz zjYXyTZ1e2lPF>^W%7_Of2>=_JKqgpV*97r0%Z;q9j2MU#uykJ~W{6Fx2!&Y!gvb<1 z(zL%_y}i6RH*ju=x_aW7*~`=AF0il;Uw6tRB`NtZApvQVMM`c4!GY3)t0N#L%9aH| zYmPyplMGr62o|(Z@vH(-K&~5N)1F;jmQ+uA0-}1T+u7+)kLYu1D&r{rQr(4g$9+n& zjy1?vcOnKDlaTG?>qt33pdWNd#MuU9958+K1QDC|ty9enYEr++>iJ2nrUApP+DH!& zEV_|;62u$9gNFh&I3nJBjl@y61tEojvGTL(B*TR0bVC=2(KzXBlv41NhVTj+XRqJA zyK(M$8zi5i5rk)(qqQB>r4J4LKGVG%1q{C617WY$3TP3Zu|n_^`K1g?xKS=7!aSoH z8xuvr7J7xoI;jpbh!%8xb@To%dWCi3- zsb)pJ-5ABDJ%8}-ChO=ni>JqR9(JWxm}G=kVx{DWDrpoe3hU7D2}u}Y;um5nm{B8G zk1%PvQ9wY+?}AeXnWj@e2nnXJ8p)}ki@sAMc0)kb{L5uUT&tTur5u|%NqB(s=jn@j zftwF+ScP6*6s)z#nF}(^+h=kDgkq-D19$xb4o~sqC;Fy`M=Ibp!Ly?D4EtwhjQ5qb zPtgxwh}7n!UIu!e)TIR&0xBs)iFO8D>|rC&fEAh2wErkn^Y(LBb$Hgo&k{UChWcV< z!4Z7IJ<@Nz@K^ zU?4-T?+$=dC@x!!7$W7MDz`GN2KI?y1rREugojIAs;$$G?9O*J2DK2lX=lej*1G4A z5xi#86hn_iHOC+}VG2yJV($$hxsh=c&jRS=!p5Hi(HipWz8+E6`=37{E^ zSlG(09FtT)>lihrN;0Hgp1_;pm8-mnh6ZF`qK+(@=n+f4B&@QAZbAbUGDaSrU46KG zFOTflem6Tra)w3bsMZI7K_5COaY?9r39vdIckpoV#_$7+GREdPGHp-E9VaYAME~~Q z1~$cGIa3#2oH@&;gS~`|y`ieE-`{w~=T&qJG?K%UZ4I0bSZfG~qq>ETFr{7=A>$*o zp~c6KVS|m!nj~l7;8&bYmUt&AV2mhLES)rJ-?f-Mxd-Ma9*Ve=b5pL|FX&7;tmsOf za)TnVtOM^E4T^v5=gl#|jqFs(s79{T6rmsd>nkru<(HZOV?T?vFSQ``?}( tZjQd6&tdt0eHQcj;r#Jp{r6XUd-v|0?0vJR&)3htds+Sb?9ij~{V#Oy2`m5r diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 5cdf0223..a9e2bdc3 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -249,10 +249,10 @@ mainMenuWaitingForCaller: { config: { quickLogTimestampFormat: "|01|02MM|08/|02DD hh:mm:ssa" - nowDateTimeFormat: "|00|11dddd|08, |11MMMM Do YYYY |08/ |11h|08:|11mm|08:|11ss|03a" + nowDateTimeFormat: "|00|11ddd|08, |11MMMM Do YYYY|08, |11h|08:|11mm|08:|11ss|03a" lastLoginDateTimeFormat: "|00|10ddd hh|08:|10mm|02a" - mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03mail|08: |11{newPrivateMail}|03 prv|08, |11{newMessagesAddrTo}|03 addr to" + mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03Prv|08:|11{newPrivateMail} |03Addr|08:|11{newMessagesAddrTo} |08- |03Avail|08:|11{availIndicator} |03Vis|08:|11{visIndicator}" mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr} |02{uploadBytesToday!sizeAbbr}" @@ -288,6 +288,8 @@ error: |00|12 fatal: |00|28 } + statusAvailableIndicators: [ "N", "Y" ] + statusVisibleIndicators: [ "N", "Y" ] } 0: { mci: { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 130276de..78938443 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -192,6 +192,14 @@ const PREDEFINED_MCI_GENERATORS = { NP : function userNewPrivateMailCount(client) { return StatLog.getUserStatNumByClient(client, UserProps.NewPrivateMailCount); }, + IA : function userStatusAvailableIndicator(client) { + const indicators = client.currentTheme.helpers.getStatusAvailIndicators(); + return client.user.isAvailable() ? (indicators[0] || 'Y') : (indicators[1] || 'N'); + }, + IV : function userStatusVisibleIndicator(client) { + const indicators = client.currentTheme.helpers.getStatusVisibleIndicators(); + return client.user.isVisible() ? (indicators[0] || 'Y') : (indicators[1] || 'N'); + }, // // Date/Time diff --git a/core/theme.js b/core/theme.js index a511ba33..f3bdcd27 100644 --- a/core/theme.js +++ b/core/theme.js @@ -348,7 +348,15 @@ exports.ThemeManager = class ThemeManager { getDateTimeFormat : function(style = 'short') { const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); - } + }, + getStatusAvailIndicators : function() { + const format = Config().theme.statusAvailableIndicators || [ 'Y', 'N' ]; + return _.get(theme, 'customization.defaults.statusAvailableIndicators', format); + }, + getStatusVisibleIndicators : function() { + const format = Config().theme.statusVisibleIndicators || [ 'Y', 'N' ]; + return _.get(theme, 'customization.defaults.statusVisibleIndicators', format); + }, }; } diff --git a/core/wfc.js b/core/wfc.js index be5b5e65..aaf6bce8 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -53,6 +53,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } } + // initSequence -> MenuModule.displayArtAndPrepViewController() (make common) + // main, help, log, ... + mciReady(mciData, cb) { super.mciReady(mciData, err => { if (err) { @@ -118,9 +121,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } _applyOpVisibility() { - const vis = this.config.opVisibility || 'current'; this.restoreUserIsVisible = this.client.user.isVisible(); + const vis = this.config.opVisibility || 'current'; switch (vis) { case 'hidden' : this.client.user.setVisibility(false); break; case 'visible' : this.client.user.setVisibility(true); break; @@ -175,6 +178,20 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { ); } + _getStatusStrings(isAvailable, isVisible) { + const availIndicators = Array.isArray(this.config.statusAvailableIndicators) ? + this.config.statusAvailableIndicators : + this.client.currentTheme.helpers.getStatusAvailableIndicators(); + const visIndicators = Array.isArray(this.config.statusVisibleIndicators) ? + this.config.statusVisibleIndicators : + this.client.currentTheme.helpers.getStatusVisibleIndicators(); + + return [ + isAvailable ? (availIndicators[1] || 'Y') : (availIndicators[0] || 'N'), + isVisible ? (visIndicators[1] || 'Y') : (visIndicators[0] || 'N'), + ]; + } + _refreshStats(cb) { const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || {}; const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || {}; @@ -183,6 +200,10 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { const now = moment(); + const [availIndicator, visIndicator] = this._getStatusStrings( + this.client.user.isAvailable(), this.client.user.isVisible() + ); + this.stats = { // Date/Time nowDate : now.format(this.getDateFormat()), @@ -216,6 +237,8 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // Current currentUserName : this.client.user.username, currentUserRealName : this.client.user.getProperty(UserProps.RealName) || this.client.user.username, + availIndicator : availIndicator, + visIndicator : visIndicator, lastLoginUserName : lastLoginStats.userName, lastLoginRealName : lastLoginStats.realName, lastLoginDate : moment(lastLoginStats.timestamp).format(this.getDateFormat()), @@ -247,7 +270,11 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { ac.action = 'Logging In'; } + const [availIndicator, visIndicator] = this._getStatusStrings(ac.isAvailable, ac.isVisible); + return Object.assign(ac, { + availIndicator, + visIndicator, timeOn : _.upperFirst((ac.timeOn || moment.duration(0)).humanize()), // make friendly }); }); diff --git a/docs/_docs/art/mci.md b/docs/_docs/art/mci.md index a1a807d8..8c2bb7ef 100644 --- a/docs/_docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -107,6 +107,8 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `NT` | Total *new* users *today* | | `NM` | Count of new messages **address to the current user** across all message areas in which they have access | | `NP` | Count of new private mail to the current user | +| `IA` | Indicator as to rather the current user is **available** or not. See also `getStatusAvailIndicators()` in [Themes](themes.md) | +| `IV` | Indicator as to rather the curent user is **visible** or not. See also `getStatusVisibleIndicators()` in [Themes](themes.md) | Some additional special case codes also exist: diff --git a/docs/_docs/art/themes.md b/docs/_docs/art/themes.md index f7b234de..ad311566 100644 --- a/docs/_docs/art/themes.md +++ b/docs/_docs/art/themes.md @@ -51,6 +51,8 @@ Override system defaults. | `dateFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for dates. | | `timeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for times. | | `dateTimeFormat` | Sets the [moment.js](https://momentjs.com/docs/#/displaying/) style `short` and/or `long` format for date/time combinations. | +| `getStatusAvailIndicators` | An array[2] of **availability** status indicators. Defaults to `[ 'Y', 'N' ]`. | +| `getStatusVisibleIndicators` | An array[2] of **visibility** status indicators. Defaults to `[ 'Y', 'N' ]`. | Example: ```hjson diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index eb632b31..ea56c2f6 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -80,4 +80,7 @@ The following MCI codes are available: * `systemAvgLoad`: System average load. * `systemCurrentLoad`: System current load. * `newPrivateMail`: Number of new **privae** mail for current user. - * `newMessagesAddrTo`: Number of new messages **addressed to the current user**. \ No newline at end of file + * `newMessagesAddrTo`: Number of new messages **addressed to the current user**. + + +:information_source: While [Standard MCI](../art/mci.md) codes work on any menu, they will **not** refresh. For values that may change over time, please use the custom format values above. \ No newline at end of file From 2ab50fb670754cc458d24c47594cb28e2f3c9540 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 24 May 2022 20:14:41 -0600 Subject: [PATCH 28/52] Add missing docs on new indicators --- docs/_docs/modding/wfc.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index ea56c2f6..dbc866f6 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -81,6 +81,8 @@ The following MCI codes are available: * `systemCurrentLoad`: System current load. * `newPrivateMail`: Number of new **privae** mail for current user. * `newMessagesAddrTo`: Number of new messages **addressed to the current user**. + * `availIndicator`: Is the current user availalbe? Displayed via `statusAvailableIndicators` or system theme. See also [Themes](../art/themes.md). + * `visIndicator`: Is the current user visible? Displayed via `statusVisibleIndicators` or system theme. See also [Themes](../art/themes.md). :information_source: While [Standard MCI](../art/mci.md) codes work on any menu, they will **not** refresh. For values that may change over time, please use the custom format values above. \ No newline at end of file From f02624c14d0096376b2c7e0f91543ef71fa4059f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 31 May 2022 12:28:26 -0600 Subject: [PATCH 29/52] Add ability to toggle avail/invis from WFC --- art/themes/luciano_blocktronics/theme.hjson | 11 +++++++---- art/themes/luciano_blocktronics/wfc.ans | Bin 2973 -> 3050 bytes core/user.js | 8 ++++++++ core/wfc.js | 13 +++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index a9e2bdc3..79aff03b 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -255,12 +255,12 @@ mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03Prv|08:|11{newPrivateMail} |03Addr|08:|11{newMessagesAddrTo} |08- |03Avail|08:|11{availIndicator} |03Vis|08:|11{visIndicator}" mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" - mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr} |02{uploadBytesToday!sizeAbbr}" - mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadBytesToday!sizeWithoutAbbr} |02{downloadBytesToday!sizeAbbr}" + mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr:>3} |02{uploadBytesToday!sizeAbbr}" + mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadBytesToday!sizeWithoutAbbr:>3} |02{downloadBytesToday!sizeAbbr}" mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" - mainInfoFormat16: "|00|10{newUsersToday}" + mainInfoFormat16: "|00|10{newUsersToday:>5}" mainInfoFormat17: "|00|10{freeMemoryBytes!sizeWithoutAbbr} |02{freeMemoryBytes!sizeAbbr} free |08/ |10{totalMemoryBytes!sizeWithoutAbbr} |02{totalMemoryBytes!sizeAbbr}" mainInfoFormat18: "|00|10{systemCurrentLoad} |02% |08/ |10{systemAvgLoad} |02load avg|08." @@ -269,7 +269,7 @@ mainInfoFormat20: "|00|10{totalCalls:>5}" mainInfoFormat21: "|00|10{totalPosts:>7}" mainInfoFormat22: "|00|10{totalUsers:>5}" - mainInfoFormat23: "|00|10{totalFiles} |08/ |10{totalFileBytes!sizeWithoutAbbr} |02{totalFileBytes!sizeAbbr}" + mainInfoFormat23: "|00|10{totalFiles:>4} |08/ |10{totalFileBytes!sizeWithoutAbbr:>4} |02{totalFileBytes!sizeAbbr}" quickLogLevel: info quickLogLevelIndicators: { @@ -293,6 +293,9 @@ } 0: { mci: { + TL16: { + fillChar: . + } TL17: { width: 23 } TL18: { width: 23 } TL19: { width: 14 } diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index 198ba676ad229d4ba1360823eec03d4d0cf38a9d..2333e61062de66df13064f51a8cf6279c28a4c92 100644 GIT binary patch delta 273 zcmbO${z`m8xu|rsu|clF5fre0vLlo5#QDaGKuN>gko@%YoKywrXv17b>1YFMpio(2 zW)4u!Ja^)~S}AlzVNgYx#giv82~VzI%#j2uaVyQq0h{6jRgj-vJb5F#$Yw?+O-6OF z62JTuh((4_C3<=wYfYg%1qFr4vpMA^f8ey5?9atB`2q{yWCvFE$rqSefo%27x0#!m xAa=QA78m3sR)Vdwhw7dz$S$_IhE0WO@&hh0Mz9|@-{qLW#B5~QIJtsb6#&ZdSWo}} delta 173 zcmaDQK39A~Id8B+aEN0_u!3~7u|e)+eoncGiwq{)GV)HIz{)=P0u$$C7RDSD{#;c4 zW-}&DMwkM|&66D&MJL;GSxsKgq_l};Ez{&qCWuiAEUcTav1u`BfNV0%^^uM?um;i! qCHV?vnW^OpX{9+i3OV`d#nRD+)BQ%fxJ8XfpXIw<-Wz>@sBl diff --git a/core/user.js b/core/user.js index 0206e868..e5063608 100644 --- a/core/user.js +++ b/core/user.js @@ -136,6 +136,14 @@ module.exports = class User { return (this.statusFlags & User.StatusFlags.NotVisible) == 0; } + setAvailability(available) { + if (available) { + this.statusFlags &= ~User.StatusFlags.NotAvailable; + } else { + this.statusFlags |= User.StatusFlags.NotAvailable; + } + } + setVisibility(visible) { if (visible) { this.statusFlags &= ~User.StatusFlags.NotVisible; diff --git a/core/wfc.js b/core/wfc.js index aaf6bce8..bc87f6f4 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -51,6 +51,19 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { if (!this.config.acs.includes('SC')) { this.config.acs = 'SC' + this.config.acs; // secure connection at the very least } + + this.menuMethods = { + toggleAvailable : (formData, extraArgs, cb) => { + const avail = this.client.user.isAvailable(); + this.client.user.setAvailability(!avail); + return this._refreshAll(cb); + }, + toggleVisible : (formData, extraArgs, cb) => { + const visible = this.client.user.isVisible(); + this.client.user.setVisibility(!visible); + return this._refreshAll(cb); + }, + } } // initSequence -> MenuModule.displayArtAndPrepViewController() (make common) From 2e4df79d529eb55c48bfc28d0edd8377d0070ea5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 2 Jun 2022 11:12:23 -0600 Subject: [PATCH 30/52] displayArtAndPrepViewController() is now available in MenuModule and derived classes * This functionality was common enough to move to MenuModule and can shorthand a good amount of boilerplate code. See code for usage. --- core/file_area_list.js | 98 +++++++++----------------- core/file_base_download_manager.js | 59 ++-------------- core/file_base_web_download_manager.js | 60 ++-------------- core/menu_module.js | 61 ++++++++++++++++ 4 files changed, 105 insertions(+), 173 deletions(-) diff --git a/core/file_area_list.js b/core/file_area_list.js index 637c3c3e..6c31a763 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -306,68 +306,17 @@ exports.getModule = class FileAreaList extends MenuModule { return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo); } - displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; - - async.waterfall( - [ - function readyAndDisplayArt(callback) { - if(options.clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } - - theme.displayThemedAsset( - config.art[name], - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { - const vcOpts = { - client : self.client, - formId : FormIds[name], - }; - - if(!_.isUndefined(options.noInput)) { - vcOpts.noInput = options.noInput; - } - - const vc = self.addViewController(name, new ViewController(vcOpts)); - - if('details' === name) { - try { - self.detailsInfoArea = { - top : artData.mciMap.XY2.position, - bottom : artData.mciMap.XY3.position, - }; - } catch(e) { - return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!')); - } - } - - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], - }; - - return vc.loadFromMenuConfig(loadOpts, callback); - } - - self.viewControllers[name].setFocus(true); - return callback(null); - - }, - ], - err => { - return cb(err); + displayArtDataPrepCallback(name, artData, viewController) { + if ('details' === name) { + try { + this.detailsInfoArea = { + top: artData.mciMap.XY2.position, + bottom: artData.mciMap.XY3.position, + }; + } catch(e) { + throw Errors.DoesNotExist('Missing XY2 and XY3 position indicators!'); } - ); + } } displayBrowsePage(clearScreen, cb) { @@ -388,7 +337,12 @@ exports.getModule = class FileAreaList extends MenuModule { return callback(null); }, function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); + return self.displayArtAndPrepViewController( + 'browse', + FormIds.browse, + { clearScreen : clearScreen, artDataPrep: self.displayArtDataPrepCallback.bind(self) }, + callback + ); }, function loadCurrentFileInfo(callback) { self.currentFileEntry = new FileEntry(); @@ -457,12 +411,17 @@ 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', + FormIds.details, + { clearScreen : true, artDataPrep: self.displayArtDataPrepCallback.bind(self) }, + callback + ); }, function populateViews(callback) { self.populateCustomLabels('details', MciViewIds.details.customRangeStart); @@ -678,7 +637,16 @@ exports.getModule = class FileAreaList extends MenuModule { gotoTopPos(); } - return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback); + return self.displayArtAndPrepViewController( + name, + FormIds[name], + { + clearScreen : false, + noInput : true, + artDataPrep: self.displayArtDataPrepCallback.bind(self) + }, + callback + ); }, function populateViews(callback) { self.lastDetailsViewController = self.viewControllers[name]; diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index 8487697f..2d34ec55 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -168,7 +168,11 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { async.series( [ function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); + return self.displayArtAndPrepViewController( + 'queueManager', FormIds.queueManager, + { clearScreen : clearScreen }, + callback + ); }, function populateViews(callback) { return self.updateDownloadQueueView(callback); @@ -181,57 +185,4 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { } ); } - - displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; - - async.waterfall( - [ - function readyAndDisplayArt(callback) { - if(options.clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } - - theme.displayThemedAsset( - config.art[name], - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { - const vcOpts = { - client : self.client, - formId : FormIds[name], - }; - - if(!_.isUndefined(options.noInput)) { - vcOpts.noInput = options.noInput; - } - - const vc = self.addViewController(name, new ViewController(vcOpts)); - - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], - }; - - return vc.loadFromMenuConfig(loadOpts, callback); - } - - self.viewControllers[name].setFocus(true); - return callback(null); - - }, - ], - err => { - return cb(err); - } - ); - } }; diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index cf509cd9..6d4fe5db 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -173,7 +173,12 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { async.series( [ function prepArtAndViewController(callback) { - return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); + return self.displayArtAndPrepViewController( + 'queueManager', + FormIds.queueManager, + { clearScreen : clearScreen }, + callback + ); }, function prepareQueueDownloadLinks(callback) { const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; @@ -226,57 +231,4 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { } ); } - - displayArtAndPrepViewController(name, options, cb) { - const self = this; - const config = this.menuConfig.config; - - async.waterfall( - [ - function readyAndDisplayArt(callback) { - if(options.clearScreen) { - self.client.term.rawWrite(ansi.resetScreen()); - } - - theme.displayThemedAsset( - config.art[name], - self.client, - { font : self.menuConfig.font, trailingLF : false }, - (err, artData) => { - return callback(err, artData); - } - ); - }, - function prepeareViewController(artData, callback) { - if(_.isUndefined(self.viewControllers[name])) { - const vcOpts = { - client : self.client, - formId : FormIds[name], - }; - - if(!_.isUndefined(options.noInput)) { - vcOpts.noInput = options.noInput; - } - - const vc = self.addViewController(name, new ViewController(vcOpts)); - - const loadOpts = { - callingMenu : self, - mciMap : artData.mciMap, - formId : FormIds[name], - }; - - return vc.loadFromMenuConfig(loadOpts, callback); - } - - self.viewControllers[name].setFocus(true); - return callback(null); - - }, - ], - err => { - return cb(err); - } - ); - } }; diff --git a/core/menu_module.js b/core/menu_module.js index 0e4cfc2d..9ea78c94 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -11,6 +11,7 @@ const stringFormat = require('../core/string_format.js'); const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; const Errors = require('../core/enig_error.js').Errors; const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); +const EnigAssert = require('./enigma_assert'); // deps const async = require('async'); @@ -563,6 +564,66 @@ exports.MenuModule = class MenuModule extends PluginModule { }); } + displayArtAndPrepViewController(name, formId, options, cb) { + const config = this.menuConfig.config; + EnigAssert(_.isObject(config)); + + async.waterfall( + [ + (callback) => { + if(options.clearScreen) { + this.client.term.rawWrite(ansi.resetScreen()); + } + + theme.displayThemedAsset( + config.art[name], + this.client, + { font : this.menuConfig.font, trailingLF : false }, + (err, artData) => { + return callback(err, artData); + } + ); + }, + (artData, callback) => { + if(_.isUndefined(this.viewControllers[name])) { + const vcOpts = { + client : this.client, + formId : formId, + }; + + if(!_.isUndefined(options.noInput)) { + vcOpts.noInput = options.noInput; + } + + const vc = this.addViewController(name, new ViewController(vcOpts)); + + if (_.isFunction(options.artDataPrep)) { + try { + options.artDataPrep(name, artData, vc); + } catch(e) { + return callback(e); + } + } + + const loadOpts = { + callingMenu : this, + mciMap : artData.mciMap, + formId : formId, + }; + + return vc.loadFromMenuConfig(loadOpts, callback); + } + + this.viewControllers[name].setFocus(true); + return callback(null); + }, + ], + err => { + return cb(err); + } + ); + } + setViewText(formName, mciId, text, appendMultiLine) { const view = this.getView(formName, mciId); if(!view) { From 3d191a9c6c9255fac439f66cb9064f4d554e960f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 4 Jun 2022 15:37:31 -0600 Subject: [PATCH 31/52] Add pages to WFC --- core/theme.js | 2 +- core/wfc.js | 128 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 96 insertions(+), 34 deletions(-) diff --git a/core/theme.js b/core/theme.js index f3bdcd27..9e897acc 100644 --- a/core/theme.js +++ b/core/theme.js @@ -364,7 +364,7 @@ exports.ThemeManager = class ThemeManager { async.each([ ...this.availableThemes.keys() ], (themeId, nextThemeId) => { this._loadTheme(themeId, err => { if (!err) { - Log.info({ themeId }, 'Theme reloaded'); + Log.info({ themeId }, `Theme "${themeId}" reloaded`); } return nextThemeId(null); // always proceed }); diff --git a/core/wfc.js b/core/wfc.js index bc87f6f4..55d64f7e 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -24,7 +24,9 @@ exports.moduleInfo = { }; const FormIds = { - main : 0, + main: 0, + help: 1, + fullLog: 2, }; const MciViewIds = { @@ -52,6 +54,8 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { this.config.acs = 'SC' + this.config.acs; // secure connection at the very least } + this.selectedNodeStatusIndex = -1; // no selection + this.menuMethods = { toggleAvailable : (formData, extraArgs, cb) => { const avail = this.client.user.isAvailable(); @@ -63,29 +67,61 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { this.client.user.setVisibility(!visible); return this._refreshAll(cb); }, + displayHelp : (formData, extraArgs, cb) => { + return this._displayHelpPage(cb); + }, + setNodeStatusSelection : (formData, extraArgs, cb) => { + const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); + if (!nodeStatusView) { + return cb(null); + } + + const index = parseInt(formData.ch); // 1-based + if (isNaN(index) || nodeStatusView.getCount() < index) { + return cb(null); + } + + this.selectedNodeStatusIndex = index - 1; + this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); + return cb(null); + } } } - // initSequence -> MenuModule.displayArtAndPrepViewController() (make common) - // main, help, log, ... - - mciReady(mciData, cb) { - super.mciReady(mciData, err => { - if (err) { - return cb(err); + initSequence() { + async.series( + [ + (callback) => { + return this.beforeArt(callback); + }, + (callback) => { + return this._displayMainPage(false, callback); + } + ], + () => { + this.finishedLoading(); } + ); + } - async.series( - [ - (callback) => { - return this.prepViewController('main', FormIds.main, mciData.menu, callback); - }, - (callback) => { - const quickLogView = this.viewControllers.main.getView(MciViewIds.main.quickLogView); - if (!quickLogView) { - return callback(null); - } + _displayMainPage(clearScreen, cb) { + async.series( + [ + (callback) => { + return this.displayArtAndPrepViewController( + 'main', + FormIds.main, + { clearScreen }, + callback + ); + }, + (callback) => { + const quickLogView = this.viewControllers.main.getView(MciViewIds.main.quickLogView); + if (!quickLogView) { + return callback(null); + } + if (!this.logRingBuffer) { const logLevel = this.config.quickLogLevel || // WFC specific _.get(Config(), 'logging.rotatingFile.level') || // ...or system setting 'info'; // ...or default to info @@ -97,21 +133,35 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { level : logLevel, stream : this.logRingBuffer }); + } - return callback(null); - }, - (callback) => { - return this._refreshAll(callback); - } - ], - err => { - if (!err) { - this._startRefreshing(); - } - return cb(err); + return callback(null); + }, + (callback) => { + return this._refreshAll(callback); } - ); - }); + ], + err => { + if (!err) { + this._startRefreshing(); + } + return cb(err); + } + ); + } + + _displayHelpPage(cb) { + this._stopRefreshing(); + + this.displayAsset( + this.menuConfig.config.art.help, + { clearScreen : true }, + () => { + this.client.waitForKeyPress( () => { + return this._displayMainPage(true, cb); + }); + } + ); } enter() { @@ -150,6 +200,10 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } _startRefreshing() { + if (this.mainRefreshTimer) { + this._stopRefreshing(); + } + this.mainRefreshTimer = setInterval( () => { this._refreshAll(); }, MainStatRefreshTimeMs); @@ -268,6 +322,14 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } + _selectNodeByIndex(nodeStatusView, index) { + if (index >= 0 && nodeStatusView.getFocusItemIndex() !== index) { + nodeStatusView.setFocusItemIndex(index); + } else { + nodeStatusView.redraw(); + } + } + _refreshNodeStatus(cb) { const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); if (!nodeStatusView) { @@ -292,9 +354,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { }); }); + // :TODO: Currently this always redraws due to setItems(). We really need painters alg.; The alternative now is to compare items... yuk. nodeStatusView.setItems(nodeStatusItems); - nodeStatusView.redraw(); - + this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); // redraws return cb(null); } From 0b11e629a6732835d50ab39eccf4e092a752790a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 4 Jun 2022 16:32:50 -0600 Subject: [PATCH 32/52] VerticalMenuView 'focusItemAtTop' property, and selection by node ID on WFC * Add new property to change how focus items are handed in VM * Select node by node iD (key press) on WFC --- art/themes/luciano_blocktronics/theme.hjson | 18 ++++++++++---- core/menu_view.js | 4 ++++ core/vertical_menu_view.js | 26 +++++++++++++++++---- core/wfc.js | 15 ++++++++---- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 79aff03b..13830fad 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -248,24 +248,30 @@ mainMenuWaitingForCaller: { config: { + // formats quickLogTimestampFormat: "|01|02MM|08/|02DD hh:mm:ssa" nowDateTimeFormat: "|00|11ddd|08, |11MMMM Do YYYY|08, |11h|08:|11mm|08:|11ss|03a" lastLoginDateTimeFormat: "|00|10ddd hh|08:|10mm|02a" + // header mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03Prv|08:|11{newPrivateMail} |03Addr|08:|11{newMessagesAddrTo} |08- |03Avail|08:|11{availIndicator} |03Vis|08:|11{visIndicator}" + + // today mainInfoFormat11: "|00|10{callsToday:>5}" mainInfoFormat12: "|00|10{postsToday:>5}" mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr:>3} |02{uploadBytesToday!sizeAbbr}" mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadBytesToday!sizeWithoutAbbr:>3} |02{downloadBytesToday!sizeAbbr}" - - mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" - mainInfoFormat16: "|00|10{newUsersToday:>5}" + // last login + mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" + + // system stats mainInfoFormat17: "|00|10{freeMemoryBytes!sizeWithoutAbbr} |02{freeMemoryBytes!sizeAbbr} free |08/ |10{totalMemoryBytes!sizeWithoutAbbr} |02{totalMemoryBytes!sizeAbbr}" mainInfoFormat18: "|00|10{systemCurrentLoad} |02% |08/ |10{systemAvgLoad} |02load avg|08." mainInfoFormat19: "|00|10{processUptimeSeconds!durationSeconds}" + // totals mainInfoFormat20: "|00|10{totalCalls:>5}" mainInfoFormat21: "|00|10{totalPosts:>7}" mainInfoFormat22: "|00|10{totalUsers:>5}" @@ -300,11 +306,15 @@ TL18: { width: 23 } TL19: { width: 14 } + // node status VM1: { height: 5 width: 36 - itemFormat: "|00|11{node:<3.2} |10{userName:>13} |08> |02{action:<14.13} |14{serverName}" + itemFormat: "|00 |11{node:<3.2} |10{userName:<13} |02{action:<14.13} |14{serverName}" + focusItemFormat: "|00|15> |11{node:<3.2} |10{userName:<13} |02{action:<14.13} |14{serverName}" + focusItemAtTop: false } + // quick log VM2: { height: 5 width: 73 diff --git a/core/menu_view.js b/core/menu_view.js index 9c750aba..7e24b637 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -226,6 +226,10 @@ MenuView.prototype.setFocusItemIndex = function(index) { this.focusedItemIndex = index; }; +MenuView.prototype.getFocusItemIndex = function() { + return this.focusedItemIndex; +}; + MenuView.prototype.onKeyPress = function(ch, key) { const itemIndex = this.getHotKeyItemIndex(ch); if(itemIndex >= 0) { diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 1837b718..6130ebde 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -11,12 +11,14 @@ const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps const util = require('util'); const _ = require('lodash'); +const { throws } = require('assert'); exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { options.cursor = options.cursor || 'hide'; options.justify = options.justify || 'left'; + this.focusItemAtTop = true; MenuView.call(this, options); @@ -44,9 +46,11 @@ function VerticalMenuView(options) { this.updateViewVisibleItems = function() { self.maxVisibleItems = Math.ceil(self.dimens.height / (self.itemSpacing + 1)); + const topIndex = (this.focusItemAtTop ? throws.focusedItemIndex : 0) || 0; + self.viewWindow = { - top : self.focusedItemIndex, - bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1, + top : topIndex, + bottom : Math.min(topIndex + self.maxVisibleItems, self.items.length) - 1, }; }; @@ -143,11 +147,15 @@ VerticalMenuView.prototype.setFocus = function(focused) { VerticalMenuView.prototype.setFocusItemIndex = function(index) { VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - const remainAfterFocus = this.items.length - index; + const remainAfterFocus = this.focusItemAtTop ? + this.items.length - index : + this.items.length; if(remainAfterFocus >= this.maxVisibleItems) { + const topIndex = (this.focusItemAtTop ? throws.focusedItemIndex : 0) || 0; + this.viewWindow = { - top : this.focusedItemIndex, - bottom : Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length) - 1 + top : topIndex, + bottom : Math.min(topIndex + this.maxVisibleItems, this.items.length) - 1 }; this.positionCacheExpired = false; // skip standard behavior @@ -343,4 +351,12 @@ VerticalMenuView.prototype.setItemSpacing = function(itemSpacing) { VerticalMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); this.positionCacheExpired = true; +}; + +VerticalMenuView.prototype.setPropertyValue = function(propName, value) { + if (propName === 'focusItemAtTop' && _.isBoolean(value)) { + this.focusItemAtTop = value; + } + + VerticalMenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; \ No newline at end of file diff --git a/core/wfc.js b/core/wfc.js index 55d64f7e..4ae93b4c 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -76,13 +76,16 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } - const index = parseInt(formData.ch); // 1-based - if (isNaN(index) || nodeStatusView.getCount() < index) { + const nodeId = parseInt(formData.ch); // 1-based + if (isNaN(nodeId)) { return cb(null); } - this.selectedNodeStatusIndex = index - 1; - this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); + const index = this._getNodeByNodeId(nodeStatusView, nodeId); + if (index > -1) { + this.selectedNodeStatusIndex = index; + this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); + } return cb(null); } } @@ -322,6 +325,10 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } + _getNodeByNodeId(nodeStatusView, nodeId) { + return nodeStatusView.getItems().findIndex(entry => entry.node == nodeId); + } + _selectNodeByIndex(nodeStatusView, index) { if (index >= 0 && nodeStatusView.getFocusItemIndex() !== index) { nodeStatusView.setFocusItemIndex(index); From 1ba866f2cae87123939098467037c457a4ed0e70 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 4 Jun 2022 17:39:48 -0600 Subject: [PATCH 33/52] Add client.friendlyRemoteAddress() for 'clean' remote IP --- core/client.js | 5 +++++ core/client_connections.js | 1 + core/predefined_mci.js | 2 +- core/user_login.js | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/client.js b/core/client.js index 9e0e2f82..fcc9bcdf 100644 --- a/core/client.js +++ b/core/client.js @@ -575,6 +575,11 @@ Client.prototype.isLocal = function() { return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); }; +Client.prototype.friendlyRemoteAddress = function() { + // convert any :ffff: IPv4's to 32bit version + return this.remoteAddress.replace(/^::ffff:/, '') +} + /////////////////////////////////////////////////////////////////////////////// // Default error handlers /////////////////////////////////////////////////////////////////////////////// diff --git a/core/client_connections.js b/core/client_connections.js index 1f24a584..ae1de622 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -113,6 +113,7 @@ function addNewClient(client, clientSock) { const connInfo = { remoteAddress : remoteAddress, + freiendlyRemoteAddress: client.friendlyRemoteAddress(), serverName : client.session.serverName, isSecure : client.session.isSecure, }; diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 78938443..690b4029 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -121,7 +121,7 @@ const PREDEFINED_MCI_GENERATORS = { 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 + IP : function clientIpAddress(client) { return client.remoteAddress.friendlyRemoteAddress() }, ST : function serverName(client) { return client.session.serverName; }, FN : function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); diff --git a/core/user_login.js b/core/user_login.js index a5e2ece3..2c327442 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -238,6 +238,6 @@ function transformLoginError(err, client, username) { err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany); } - client.log.warn( { username, ip : client.remoteAddress, reason : err.message }, `Failed login attempt for user "${username}", ${client.remoteAddress}`); + client.log.warn( { username, ip : client.remoteAddress, reason : err.message }, `Failed login attempt for user "${username}", ${client.friendlyRemoteAddress()}`); return err; } \ No newline at end of file From ba5775adc92168b21a64e0678cdd02ea5532f767 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 10 Jun 2022 16:08:22 -0600 Subject: [PATCH 34/52] Updates to drawing, additional props, friendly IP addrs, etc. --- art/themes/luciano_blocktronics/theme.hjson | 6 ++--- art/themes/luciano_blocktronics/wfc.ans | Bin 3050 -> 3050 bytes core/client.js | 4 +++- core/client_connections.js | 2 +- core/predefined_mci.js | 4 ++-- core/vertical_menu_view.js | 14 +++++++++++ core/wfc.js | 25 ++++++++++++++++++-- docs/_docs/modding/wfc.md | 13 ++++++++++ 8 files changed, 59 insertions(+), 9 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 13830fad..a3fc4f85 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -309,9 +309,9 @@ // node status VM1: { height: 5 - width: 36 - itemFormat: "|00 |11{node:<3.2} |10{userName:<13} |02{action:<14.13} |14{serverName}" - focusItemFormat: "|00|15> |11{node:<3.2} |10{userName:<13} |02{action:<14.13} |14{serverName}" + width: 37 + itemFormat: "|00 |11{node:<3.2} |10{userName:<12} |02{action:<14.13} |14{serverName}" + focusItemFormat: "|00|15> |11{node:<3.2} |10{userName:<12} |02{action:<14.13} |14{serverName}" focusItemAtTop: false } // quick log diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index 2333e61062de66df13064f51a8cf6279c28a4c92..79fe75b9da91d866d9921267b10f5b547d93c566 100644 GIT binary patch delta 15 WcmaDQ{z`nqL2hO<1Cz-|xYYnO1O=o3 delta 15 WcmaDQ{z`nqL2hPKBg@H0xYYnOA_b}d diff --git a/core/client.js b/core/client.js index fcc9bcdf..8c292003 100644 --- a/core/client.js +++ b/core/client.js @@ -577,7 +577,9 @@ Client.prototype.isLocal = function() { Client.prototype.friendlyRemoteAddress = function() { // convert any :ffff: IPv4's to 32bit version - return this.remoteAddress.replace(/^::ffff:/, '') + return this.remoteAddress + .replace(/^::ffff:/, '') + .replace(/^::1$/, 'localhost'); } /////////////////////////////////////////////////////////////////////////////// diff --git a/core/client_connections.js b/core/client_connections.js index ae1de622..102f79a3 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -123,7 +123,7 @@ function addNewClient(client, clientSock) { connInfo.family = clientSock.localFamily; } - client.log.info(connInfo, `Client connected (${connInfo.port}/${connInfo.serverName})`); + client.log.info(connInfo, `Client connected (${connInfo.serverName}/${connInfo.port})`); Events.emit( Events.getSystemEvents().ClientConnected, diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 690b4029..b57dca98 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -121,7 +121,7 @@ const PREDEFINED_MCI_GENERATORS = { 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.friendlyRemoteAddress() }, + IP : function clientIpAddress(client) { return client.friendlyRemoteAddress() }, ST : function serverName(client) { return client.session.serverName; }, FN : function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); @@ -370,7 +370,7 @@ function getPredefinedMCIValue(client, code, extra) { try { value = generator(client, extra); } catch(e) { - Log.error( { code : code, exception : e.message }, 'Exception caught generating predefined MCI value' ); + Log.error( { code : code, exception : e.message }, `Failed generating predefined MCI value (${code})` ); } return value; diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 6130ebde..a1daf025 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -83,6 +83,15 @@ function VerticalMenuView(options) { self.client.term.write(`${ansi.goto(item.row, self.position.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); }; + + this.drawRemovedItem = function(index) { + if (index <= this.items.length - 1) { + return; + } + const row = this.position.row + index; + this.client.term.rawWrite(`${ansi.goto(row, this.position.col)}${ansi.normal()}${this.fillChar.repeat(this.dimens.width)}`) + }; + } util.inherits(VerticalMenuView, MenuView); @@ -123,6 +132,11 @@ VerticalMenuView.prototype.redraw = function() { this.drawItem(i); } } + + const remain = Math.max(0, this.dimens.height - this.items.length); + for (let i = this.items.length; i < remain; ++i) { + this.drawRemovedItem(i); + } }; VerticalMenuView.prototype.setHeight = function(height) { diff --git a/core/wfc.js b/core/wfc.js index 4ae93b4c..40eedfe1 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -1,5 +1,6 @@ // ENiGMA½ const { MenuModule } = require('./menu_module'); +const stringFormat = require('./string_format'); const { getActiveConnectionList, @@ -33,6 +34,7 @@ const MciViewIds = { main : { nodeStatus : 1, quickLogView : 2, + nodeStatusSelection : 3, customRangeStart : 10, } @@ -119,7 +121,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { ); }, (callback) => { - const quickLogView = this.viewControllers.main.getView(MciViewIds.main.quickLogView); + const quickLogView = this.getView('main', MciViewIds.main.quickLogView); if (!quickLogView) { return callback(null); } @@ -138,6 +140,20 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { }); } + const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); + const nodeStatusSelectionView = this.getView('main', MciViewIds.main.nodeStatusSelection); + const nodeStatusSelectionFormat = this.config.nodeStatusSelectionFormat || '{text}'; + if (nodeStatusView && nodeStatusSelectionView) { + nodeStatusView.on('index update', index => { + const item = nodeStatusView.getItems()[index]; + if (item) { + nodeStatusSelectionView.setText(stringFormat(nodeStatusSelectionFormat, item)); + // :TODO: Update view + // :TODO: this is not triggered by key-presses (1, 2, ...) -- we need to handle that as well + } + }); + } + return callback(null); }, (callback) => { @@ -354,10 +370,15 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { const [availIndicator, visIndicator] = this._getStatusStrings(ac.isAvailable, ac.isVisible); + const timeOn = ac.timeOn || moment.duration(0); + return Object.assign(ac, { availIndicator, visIndicator, - timeOn : _.upperFirst((ac.timeOn || moment.duration(0)).humanize()), // make friendly + timeOnMinutes : timeOn.asMinutes(), + timeOn : _.upperFirst(timeOn.humanize()), // make friendly + affils : ac.affils || 'N/A', + realName : ac.realName || 'N/A', }); }); diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index dbc866f6..1e6831df 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -35,7 +35,20 @@ The following MCI codes are available: * `VM1`: Node status list with the following format items available: * `text`: Username or `*Pre Auth*`. * `action`: Current action/menu. + * `affils`: Any affiliations related to the if `authenticated`, else "N/A". + * `authenticated`: Boolean rather the node is authenticated (logged in) or not. + * `availIndicator`: Indicator of availability (e.g. for messaging)? Displayed via `statusAvailableIndicators` or system theme. See also [Themes](../art/themes.md). + * `isAvailalbe`: Boolean rather the node is availalbe (e.g. for messaging) or not. + * `isSecure`: Is the node securely connected (ie: SSL)? + * `isVisible`: Boolean rather the node is visible to others or not. + * `node`: The node ID. + * `realName`: Real name of authenticated user, or "N/A". + * `serverName`: Name of connected server such as "Telnet" or "SSH". * `timeOn`: How long the node has been connected. + * `timeOnMinutes`: How long in **minutes** the node has been connected. + * `userId`: User ID of authenticated node, or 0 if not yet authenticated. + * `userName`: User name of authenticated user or "*Pre Auth*" + * `visIndicator`: Indicator of visibility. Displayed via `statusVisibleIndicators` or system theme. See also [Themes](../art/themes.md). * `VM2`: Quick log with the following format keys available: * `timestamp`: Log entry timestamp in `quickLogTimestampFormat` format. * `level`: Log entry level from Bunyan. From c93b8cda81ef0272e0e40360fd1729f4dcf84b54 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Jun 2022 14:11:36 -0600 Subject: [PATCH 35/52] Fix up some merge mistakes --- core/theme.js | 11 --------- core/user_config.js | 3 ++- core/view_controller.js | 50 ++++++++++++++++++++--------------------- 3 files changed, 26 insertions(+), 38 deletions(-) diff --git a/core/theme.js b/core/theme.js index 77672c45..3fef5886 100644 --- a/core/theme.js +++ b/core/theme.js @@ -364,7 +364,6 @@ exports.ThemeManager = class ThemeManager { const format = Config().theme.timeFormat[style] || 'h:mm a'; return _.get(theme, `customization.defaults.timeFormat.${style}`, format); }, -<<<<<<< HEAD getDateTimeFormat : function(style = 'short') { const format = Config().theme.dateTimeFormat[style] || 'MM/DD/YYYY h:mm a'; return _.get(theme, `customization.defaults.dateTimeFormat.${style}`, format); @@ -376,16 +375,6 @@ exports.ThemeManager = class ThemeManager { getStatusVisibleIndicators : function() { const format = Config().theme.statusVisibleIndicators || [ 'Y', 'N' ]; return _.get(theme, 'customization.defaults.statusVisibleIndicators', 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 - ); ->>>>>>> 7c01946d6e8ac0023e9692744803466aa96cbadf }, }; } diff --git a/core/user_config.js b/core/user_config.js index 975bc87e..3f7d1bcc 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -152,7 +152,8 @@ exports.getModule = class UserConfigModule extends MenuModule { } else { self.client.log.info(`User "${self.client.user.username}" updated authentication credentials`); } - ); + return self.prevMenu(cb); + }); } else { return self.prevMenu(cb); } diff --git a/core/view_controller.js b/core/view_controller.js index e8862a1e..696e18b7 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -257,44 +257,42 @@ function ViewController(options) { } }; - 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; - } + async.each(Object.keys(config.mci || {}), function entry(mci, nextItem) { + const mciMatch = mci.match(MCI_REGEXP); // :TODO: How to handle auto-generated IDs???? + if(null === mciMatch) { + self.client.log.warn( { mci : mci }, 'Unable to parse MCI code'); + return; + } - const viewId = parseInt(mciMatch[2]); - assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used + const viewId = parseInt(mciMatch[2]); + assert(!isNaN(viewId), 'Cannot parse view ID: ' + mciMatch[2]); // shouldn't be possible with RegExp used - if (viewId > highestId) { - highestId = viewId; - } + if(viewId > highestId) { + highestId = viewId; + } - const view = self.getView(viewId); + const view = self.getView(viewId); if(!view) { return nextItem(null); } - const mciConf = config.mci[mci]; + const mciConf = config.mci[mci]; - self.setViewPropertiesFromMCIConf(view, mciConf); + self.setViewPropertiesFromMCIConf(view, mciConf); - if (mciConf.focus) { - initialFocusId = viewId; - } + if(mciConf.focus) { + initialFocusId = viewId; + } - if (true === view.submit) { - submitId = viewId; - } + if(true === view.submit) { + submitId = viewId; + } nextItem(null); }, @@ -305,10 +303,10 @@ function ViewController(options) { if(highestIdView) { highestIdView.submit = true; } - - return cb(err, { initialFocusId: initialFocusId }); } - ); + + return cb(err, { initialFocusId : initialFocusId } ); + }); }; // method for comparing submitted form data to configuration entries From 9172fdda9dff422325d3fb18dbbb246623446e9d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Jun 2022 14:12:03 -0600 Subject: [PATCH 36/52] Re-apply some Prettier formatting after merge --- core/abracadabra.js | 38 ++-- core/bbs.js | 125 ++++++----- core/client.js | 8 +- core/client_connections.js | 72 +++--- core/door.js | 7 +- core/event_scheduler.js | 5 +- core/file_area_list.js | 18 +- core/file_area_web.js | 31 ++- core/file_base_download_manager.js | 5 +- core/file_base_web_download_manager.js | 2 +- core/file_transfer.js | 5 +- core/menu_module.js | 51 +++-- core/menu_view.js | 4 +- core/message_area.js | 84 +++---- core/misc_scheduled_events.js | 4 +- core/msg_area_post_fse.js | 6 +- core/node_msg.js | 54 +++-- core/nua.js | 12 +- core/predefined_mci.js | 219 ++++++++++++------ core/stat_log.js | 108 +++++---- core/string_util.js | 6 +- core/sys_event_user_log.js | 6 +- core/system_log.js | 4 +- core/system_property.js | 40 ++-- core/telnet_bridge.js | 11 +- core/theme.js | 31 ++- core/user.js | 54 +++-- core/user_config.js | 28 ++- core/user_interrupt_queue.js | 6 +- core/user_login.js | 53 +++-- core/user_property.js | 4 +- core/vertical_menu_view.js | 37 +-- core/view.js | 4 +- core/view_controller.js | 91 ++++---- core/wfc.js | 299 ++++++++++++++----------- core/whos_online.js | 18 +- package.json | 140 ++++++------ 37 files changed, 978 insertions(+), 712 deletions(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 200c28aa..2a9dd035 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -108,14 +108,22 @@ exports.getModule = class AbracadabraModule extends MenuModule { name: self.config.name, activeCount: activeDoorNodeInstances[self.config.name], }, - `Too many active instances of door "${self.config.name}"`); + `Too many active instances of door "${self.config.name}"` + ); - 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' @@ -171,14 +179,14 @@ exports.getModule = class AbracadabraModule extends MenuModule { this.client.term.write(ansi.resetScreen()); const exeInfo = { - name : this.config.name, - 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, + name: this.config.name, + 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) { diff --git a/core/bbs.js b/core/bbs.js index 2d8e895b..ddc6bfb5 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -6,14 +6,14 @@ //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 UserLogNames = require('./user_log_name'); +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 UserLogNames = require('./user_log_name'); // deps const async = require('async'); @@ -152,7 +152,9 @@ function shutdownSystem() { [ function closeConnections(callback) { const ClientConns = require('./client_connections.js'); - const activeConnections = ClientConns.getActiveConnections(ClientConns.AllConnections); + const activeConnections = ClientConns.getActiveConnections( + ClientConns.AllConnections + ); let i = activeConnections.length; while (i--) { const activeTerm = activeConnections[i].term; @@ -327,68 +329,77 @@ function initialize(cb) { const StatLog = require('./stat_log'); const entries = [ - [ UserLogNames.UlFiles, [ SysProps.FileUlTodayCount, 'count' ] ], - [ UserLogNames.UlFileBytes, [ SysProps.FileUlTodayBytes, 'obj' ] ], - [ UserLogNames.DlFiles, [ SysProps.FileDlTodayCount, 'count' ] ], - [ UserLogNames.DlFileBytes, [ SysProps.FileDlTodayBytes, 'obj' ] ], - [ UserLogNames.NewUser, [ SysProps.NewUsersTodayCount, 'count' ] ], + [UserLogNames.UlFiles, [SysProps.FileUlTodayCount, 'count']], + [UserLogNames.UlFileBytes, [SysProps.FileUlTodayBytes, 'obj']], + [UserLogNames.DlFiles, [SysProps.FileDlTodayCount, 'count']], + [UserLogNames.DlFileBytes, [SysProps.FileDlTodayBytes, 'obj']], + [UserLogNames.NewUser, [SysProps.NewUsersTodayCount, 'count']], ]; - async.each(entries, (entry, nextEntry) => { - const [ logName, [sysPropName, resultType] ] = entry; + async.each( + entries, + (entry, nextEntry) => { + const [logName, [sysPropName, resultType]] = entry; - const filter = { - logName, - resultType, - date : moment(), - }; + const filter = { + logName, + resultType, + date: moment(), + }; - StatLog.findUserLogEntries(filter, (err, stat) => { - if (!err) { - if (resultType === 'obj') { - stat = stat.reduce( (bytes, entry) => bytes + parseInt(entry.log_value) || 0, 0); + StatLog.findUserLogEntries(filter, (err, stat) => { + if (!err) { + if (resultType === 'obj') { + stat = stat.reduce( + (bytes, entry) => + bytes + parseInt(entry.log_value) || 0, + 0 + ); + } + + StatLog.setNonPersistentSystemStat(sysPropName, stat); } - - StatLog.setNonPersistentSystemStat(sysPropName, stat); - } - return nextEntry(null); - }); - }, - () => { - return callback(null); - }); + return nextEntry(null); + }); + }, + () => { + return callback(null); + } + ); }, function initLastLogin(callback) { const StatLog = require('./stat_log'); - StatLog.getSystemLogEntries(SysLogKeys.UserLoginHistory, 'timestamp_desc', 1, (err, lastLogin) => { - if (err) { - return callback(null); - } + StatLog.getSystemLogEntries( + SysLogKeys.UserLoginHistory, + 'timestamp_desc', + 1, + (err, lastLogin) => { + if (err) { + return callback(null); + } - let loginObj; - try - { - loginObj = JSON.parse(lastLogin[0].log_value); - loginObj.timestamp = moment(lastLogin[0].timestamp); - } - catch (e) - { - return callback(null); - } + let loginObj; + try { + loginObj = JSON.parse(lastLogin[0].log_value); + loginObj.timestamp = moment(lastLogin[0].timestamp); + } catch (e) { + return callback(null); + } - // For live stats we want to resolve user ID -> name, etc. - const User = require('./user'); - User.getUserInfo(loginObj.userId, (err, props) => { - const stat = Object.assign({}, props, loginObj); - StatLog.setNonPersistentSystemStat(SysProps.LastLogin, stat); - return callback(null); - }); - }); + // For live stats we want to resolve user ID -> name, etc. + const User = require('./user'); + User.getUserInfo(loginObj.userId, (err, props) => { + const stat = Object.assign({}, props, loginObj); + StatLog.setNonPersistentSystemStat(SysProps.LastLogin, stat); + return callback(null); + }); + } + ); }, function initUserCount(callback) { const User = require('./user.js'); User.getUserCount((err, count) => { - if(err) { + if (err) { return callback(err); } diff --git a/core/client.js b/core/client.js index b187e17e..30a0f7cd 100644 --- a/core/client.js +++ b/core/client.js @@ -592,12 +592,10 @@ Client.prototype.isLocal = function () { return ['127.0.0.1', '::ffff:127.0.0.1'].includes(this.remoteAddress); }; -Client.prototype.friendlyRemoteAddress = function() { +Client.prototype.friendlyRemoteAddress = function () { // convert any :ffff: IPv4's to 32bit version - return this.remoteAddress - .replace(/^::ffff:/, '') - .replace(/^::1$/, 'localhost'); -} + return this.remoteAddress.replace(/^::ffff:/, '').replace(/^::1$/, 'localhost'); +}; /////////////////////////////////////////////////////////////////////////////// // Default error handlers diff --git a/core/client_connections.js b/core/client_connections.js index f34fe8a8..fea32a3e 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -24,13 +24,23 @@ exports.clientConnections = clientConnections; const AllConnections = { authUsersOnly: false, visibleOnly: false, availOnly: false }; exports.AllConnections = AllConnections; -const UserVisibleConnections = { authUsersOnly: false, visibleOnly: true, availOnly: false }; +const UserVisibleConnections = { + authUsersOnly: false, + visibleOnly: true, + availOnly: false, +}; exports.UserVisibleConnections = UserVisibleConnections; -const UserMessageableConnections = { authUsersOnly: true, visibleOnly: true, availOnly: true }; +const UserMessageableConnections = { + authUsersOnly: true, + visibleOnly: true, + availOnly: true, +}; exports.UserMessageableConnections = UserMessageableConnections; -function getActiveConnections(options = { authUsersOnly: true, visibleOnly: true, availOnly: false }) { +function getActiveConnections( + options = { authUsersOnly: true, visibleOnly: true, availOnly: false } +) { return clientConnections.filter(conn => { if (options.authUsersOnly && !conn.user.isAuthenticated()) { return false; @@ -46,7 +56,9 @@ function getActiveConnections(options = { authUsersOnly: true, visibleOnly: true }); } -function getActiveConnectionList(options = { authUsersOnly: true, visibleOnly: true, availOnly: false }) { +function getActiveConnectionList( + options = { authUsersOnly: true, visibleOnly: true, availOnly: false } +) { const now = moment(); return _.map(getActiveConnections(options), ac => { @@ -54,33 +66,36 @@ function getActiveConnectionList(options = { authUsersOnly: true, visibleOnly: t try { // attempting to fetch a bad menu stack item can blow up/assert action = _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'); - } catch(e) { + } catch (e) { action = 'Unknown'; } const entry = { - node : ac.node, - authenticated : ac.user.isAuthenticated(), - userId : ac.user.userId, - action : action, - serverName : ac.session.serverName, - isSecure : ac.session.isSecure, - isVisible : ac.user.isVisible(), - isAvailable : ac.user.isAvailable(), + node: ac.node, + authenticated: ac.user.isAuthenticated(), + userId: ac.user.userId, + action: action, + serverName: ac.session.serverName, + isSecure: ac.session.isSecure, + isVisible: ac.user.isVisible(), + isAvailable: ac.user.isAvailable(), }; // // There may be a connection, but not a logged in user as of yet // - if(ac.user.isAuthenticated()) { - entry.text = ac.user.username; - 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.text = ac.user.username; + 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; @@ -115,10 +130,10 @@ function addNewClient(client, clientSock) { client.log = logger.log.child({ nodeId, sessionId: client.session.uniqueId }); const connInfo = { - remoteAddress : remoteAddress, + remoteAddress: remoteAddress, freiendlyRemoteAddress: client.friendlyRemoteAddress(), - serverName : client.session.serverName, - isSecure : client.session.isSecure, + serverName: client.session.serverName, + isSecure: client.session.isSecure, }; if (client.log.debug()) { @@ -126,7 +141,10 @@ function addNewClient(client, clientSock) { connInfo.family = clientSock.localFamily; } - client.log.info(connInfo, `Client connected (${connInfo.serverName}/${connInfo.port})`); + client.log.info( + connInfo, + `Client connected (${connInfo.serverName}/${connInfo.port})` + ); Events.emit(Events.getSystemEvents().ClientConnected, { client: client, @@ -170,9 +188,9 @@ function removeClient(client) { } function getConnectionByUserId(userId) { - return getActiveConnections(AllConnections).find( ac => userId === ac.user.userId ); + return getActiveConnections(AllConnections).find(ac => userId === ac.user.userId); } function getConnectionByNodeId(nodeId) { - return getActiveConnections(AllConnections).find( ac => nodeId == ac.node ); + return getActiveConnections(AllConnections).find(ac => nodeId == ac.node); } diff --git a/core/door.js b/core/door.js index 42fc17f8..305ae162 100644 --- a/core/door.js +++ b/core/door.js @@ -32,7 +32,10 @@ module.exports = class Door { }); conn.once('error', err => { - this.client.log.warn( { error : err.message }, 'Door socket server connection'); + this.client.log.warn( + { error: err.message }, + 'Door socket server connection' + ); return this.restoreIo(conn); }); @@ -73,7 +76,7 @@ module.exports = class Door { 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 (${exeInfo.name})` ); diff --git a/core/event_scheduler.js b/core/event_scheduler.js index c31a1deb..076e116f 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -112,7 +112,10 @@ class ScheduledEvent { } executeAction(reason, cb) { - Log.info( { eventName : this.name, action : this.action, reason : reason }, `Executing scheduled event "${this.name}"...`); + Log.info( + { eventName: this.name, action: this.action, reason: reason }, + `Executing scheduled event "${this.name}"...` + ); if ('method' === this.action.type) { const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') diff --git a/core/file_area_list.js b/core/file_area_list.js index f7501ae4..77b079ff 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -351,7 +351,7 @@ exports.getModule = class FileAreaList extends MenuModule { top: artData.mciMap.XY2.position, bottom: artData.mciMap.XY3.position, }; - } catch(e) { + } catch (e) { throw Errors.DoesNotExist('Missing XY2 and XY3 position indicators!'); } } @@ -380,7 +380,10 @@ exports.getModule = class FileAreaList extends MenuModule { return self.displayArtAndPrepViewController( 'browse', FormIds.browse, - { clearScreen : clearScreen, artDataPrep: self.displayArtDataPrepCallback.bind(self) }, + { + clearScreen: clearScreen, + artDataPrep: self.displayArtDataPrepCallback.bind(self), + }, callback ); }, @@ -473,7 +476,10 @@ exports.getModule = class FileAreaList extends MenuModule { return self.displayArtAndPrepViewController( 'details', FormIds.details, - { clearScreen : true, artDataPrep: self.displayArtDataPrepCallback.bind(self) }, + { + clearScreen: true, + artDataPrep: self.displayArtDataPrepCallback.bind(self), + }, callback ); }, @@ -725,9 +731,9 @@ exports.getModule = class FileAreaList extends MenuModule { name, FormIds[name], { - clearScreen : false, - noInput : true, - artDataPrep: self.displayArtDataPrepCallback.bind(self) + clearScreen: false, + noInput: true, + artDataPrep: self.displayArtDataPrepCallback.bind(self), }, callback ); diff --git a/core/file_area_web.js b/core/file_area_web.js index 525d094b..4ffe2491 100644 --- a/core/file_area_web.js +++ b/core/file_area_web.js @@ -534,23 +534,22 @@ class FileAreaWebAccess { StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1); StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes); - StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1); - StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayBytes, dlBytes); + StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1); + StatLog.incrementNonPersistentSystemStat( + SysProps.FileDlTodayBytes, + dlBytes + ); - return callback(null, user); - }, - function sendEvent(user, callback) { - Events.emit( - Events.getSystemEvents().UserDownload, - { - user : user, - files : fileEntries, - } - ); - return callback(null); - } - ] - ); + return callback(null, user); + }, + function sendEvent(user, callback) { + Events.emit(Events.getSystemEvents().UserDownload, { + user: user, + files: fileEntries, + }); + return callback(null); + }, + ]); } } diff --git a/core/file_base_download_manager.js b/core/file_base_download_manager.js index 92490783..b8564b73 100644 --- a/core/file_base_download_manager.js +++ b/core/file_base_download_manager.js @@ -193,8 +193,9 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { [ function prepArtAndViewController(callback) { return self.displayArtAndPrepViewController( - 'queueManager', FormIds.queueManager, - { clearScreen : clearScreen }, + 'queueManager', + FormIds.queueManager, + { clearScreen: clearScreen }, callback ); }, diff --git a/core/file_base_web_download_manager.js b/core/file_base_web_download_manager.js index 6d30c996..233a247e 100644 --- a/core/file_base_web_download_manager.js +++ b/core/file_base_web_download_manager.js @@ -188,7 +188,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { return self.displayArtAndPrepViewController( 'queueManager', FormIds.queueManager, - { clearScreen : clearScreen }, + { clearScreen: clearScreen }, callback ); }, diff --git a/core/file_transfer.js b/core/file_transfer.js index c99f25a0..af166d24 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -148,7 +148,10 @@ exports.getModule = class TransferFileModule extends MenuModule { sentFiles.push(f.path); }); - this.client.log.info( { sentFiles : sentFiles }, `User "${self.client.user.username}" uploaded ${sentFiles.length} file(s)` ); + this.client.log.info( + { sentFiles: sentFiles }, + `User "${self.client.user.username}" uploaded ${sentFiles.length} file(s)` + ); } return cb(err); }); diff --git a/core/menu_module.js b/core/menu_module.js index 613c7949..f233b1f7 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -613,45 +613,48 @@ exports.MenuModule = class MenuModule extends PluginModule { async.waterfall( [ - (callback) => { - if(options.clearScreen) { + callback => { + if (options.clearScreen) { this.client.term.rawWrite(ansi.resetScreen()); } theme.displayThemedAsset( config.art[name], this.client, - { font : this.menuConfig.font, trailingLF : false }, + { font: this.menuConfig.font, trailingLF: false }, (err, artData) => { return callback(err, artData); } ); }, (artData, callback) => { - if(_.isUndefined(this.viewControllers[name])) { + if (_.isUndefined(this.viewControllers[name])) { const vcOpts = { - client : this.client, - formId : formId, + client: this.client, + formId: formId, }; - if(!_.isUndefined(options.noInput)) { + if (!_.isUndefined(options.noInput)) { vcOpts.noInput = options.noInput; } - const vc = this.addViewController(name, new ViewController(vcOpts)); + const vc = this.addViewController( + name, + new ViewController(vcOpts) + ); if (_.isFunction(options.artDataPrep)) { try { options.artDataPrep(name, artData, vc); - } catch(e) { + } catch (e) { return callback(e); } } const loadOpts = { - callingMenu : this, - mciMap : artData.mciMap, - formId : formId, + callingMenu: this, + mciMap: artData.mciMap, + formId: formId, }; return vc.loadFromMenuConfig(loadOpts, callback); @@ -815,18 +818,24 @@ exports.MenuModule = class MenuModule extends PluginModule { } // Various common helpers - getDateFormat(defaultStyle='short') { - return this.config.dateFormat || - this.client.currentTheme.helpers.getDateFormat(defaultStyle); + getDateFormat(defaultStyle = 'short') { + return ( + this.config.dateFormat || + this.client.currentTheme.helpers.getDateFormat(defaultStyle) + ); } - getTimeFormat(defaultStyle='short') { - return this.config.timeFormat || - this.client.currentTheme.helpers.getTimeFormat(defaultStyle); + getTimeFormat(defaultStyle = 'short') { + return ( + this.config.timeFormat || + this.client.currentTheme.helpers.getTimeFormat(defaultStyle) + ); } - getDateTimeFormat(defaultStyle='short') { - return this.config.dateTimeFormat || - this.client.currentTheme.helpers.getDateTimeFormat(defaultStyle); + getDateTimeFormat(defaultStyle = 'short') { + return ( + this.config.dateTimeFormat || + this.client.currentTheme.helpers.getDateTimeFormat(defaultStyle) + ); } }; diff --git a/core/menu_view.js b/core/menu_view.js index 952546fc..3b145bfa 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -233,11 +233,11 @@ MenuView.prototype.setFocusItemIndex = function (index) { this.focusedItemIndex = index; }; -MenuView.prototype.getFocusItemIndex = function() { +MenuView.prototype.getFocusItemIndex = function () { return this.focusedItemIndex; }; -MenuView.prototype.onKeyPress = function(ch, key) { +MenuView.prototype.onKeyPress = function (ch, key) { const itemIndex = this.getHotKeyItemIndex(ch); if (itemIndex >= 0) { this.setFocusItemIndex(itemIndex); diff --git a/core/message_area.js b/core/message_area.js index ef903154..32825c71 100644 --- a/core/message_area.js +++ b/core/message_area.js @@ -24,29 +24,29 @@ 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.getNewMessageCountAddressedToUser = getNewMessageCountAddressedToUser; -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.getNewMessageCountAddressedToUser = getNewMessageCountAddressedToUser; +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 @@ -536,20 +536,30 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) { // that are addressed to that user (ie: matching username) // Does NOT Include private messages. function getNewMessageCountAddressedToUser(client, cb) { - const areaTags = getAllAvailableMessageAreaTags(client).filter(areaTag => areaTag !== Message.WellKnownAreaTags.Private); + const areaTags = getAllAvailableMessageAreaTags(client).filter( + areaTag => areaTag !== Message.WellKnownAreaTags.Private + ); let newMessageCount = 0; - async.forEach(areaTags, (areaTag, nextAreaTag) => { - getMessageAreaLastReadId(client.user.userId, areaTag, (_, lastMessageId) => { - lastMessageId = lastMessageId || 0; - getNewMessageCountInAreaForUser(client.user.userId, areaTag, (err, count) => { - newMessageCount += count; - return nextAreaTag(err); + async.forEach( + areaTags, + (areaTag, nextAreaTag) => { + getMessageAreaLastReadId(client.user.userId, areaTag, (_, lastMessageId) => { + lastMessageId = lastMessageId || 0; + getNewMessageCountInAreaForUser( + client.user.userId, + areaTag, + (err, count) => { + newMessageCount += count; + return nextAreaTag(err); + } + ); }); - }); - }, () => { - return cb(null, newMessageCount); - }); + }, + () => { + return cb(null, newMessageCount); + } + ); } function getNewMessagesInAreaForUser(userId, areaTag, cb) { @@ -572,10 +582,8 @@ 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, diff --git a/core/misc_scheduled_events.js b/core/misc_scheduled_events.js index 444cd3c7..ba794eb8 100644 --- a/core/misc_scheduled_events.js +++ b/core/misc_scheduled_events.js @@ -12,7 +12,9 @@ function dailyMaintenanceScheduledEvent(args, cb) { // // :TODO: files/etc. here const resetProps = [ - SysProps.LoginsToday, SysProps.MessagesToday, SysProps.NewUsersTodayCount, + SysProps.LoginsToday, + SysProps.MessagesToday, + SysProps.NewUsersTodayCount, ]; resetProps.forEach(prop => { diff --git a/core/msg_area_post_fse.js b/core/msg_area_post_fse.js index 645a8793..9eacd1f5 100644 --- a/core/msg_area_post_fse.js +++ b/core/msg_area_post_fse.js @@ -46,7 +46,11 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { } 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, + }, `User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})` ); } diff --git a/core/node_msg.js b/core/node_msg.js index b0b11fa2..71530f9c 100644 --- a/core/node_msg.js +++ b/core/node_msg.js @@ -7,13 +7,13 @@ const { getActiveConnectionList, getConnectionByNodeId, UserMessageableConnections, -} = 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'); @@ -221,22 +221,30 @@ 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(UserMessageableConnections) - .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(UserMessageableConnections).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) { diff --git a/core/nua.js b/core/nua.js index 1aaf67c1..4f6f355d 100644 --- a/core/nua.js +++ b/core/nua.js @@ -129,8 +129,11 @@ exports.getModule = class NewUserAppModule extends MenuModule { sessionId: self.client.session.uniqueId, // used for events/etc. }; newUser.create(createUserInfo, err => { - if(err) { - self.client.log.warn( { error : err, username : formData.value.username }, 'New user creation failed'); + if (err) { + self.client.log.warn( + { error: err, username: formData.value.username }, + 'New user creation failed' + ); self.gotoMenu(extraArgs.error, err => { if (err) { @@ -139,7 +142,10 @@ exports.getModule = class NewUserAppModule extends MenuModule { return cb(null); }); } else { - self.client.log.info( { username : formData.value.username, userId : newUser.userId }, `New user "${formData.value.username}" created`); + self.client.log.info( + { username: formData.value.username, userId: newUser.userId }, + `New user "${formData.value.username}" created` + ); // Cache SysOp information now // :TODO: Similar to bbs.js. DRY diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 105f8a74..8461d283 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -15,11 +15,11 @@ 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 async = require('async'); +const packageJson = require('../package.json'); +const os = require('os'); +const _ = require('lodash'); +const moment = require('moment'); +const async = require('async'); exports.getPredefinedMCIValue = getPredefinedMCIValue; exports.init = init; @@ -27,14 +27,14 @@ exports.init = init; function init(cb) { async.series( [ - (callback) => { + callback => { return setNextRandomRumor(callback); }, - (callback) => { + callback => { // by fetching a memory or load we'll force a refresh now StatLog.getSystemStat(SysProps.SystemMemoryStats); return callback(null); - } + }, ], err => { return cb(err); @@ -98,38 +98,87 @@ const PREDEFINED_MCI_GENERATORS = { }, // +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); + }, // // 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.friendlyRemoteAddress() }, - 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.friendlyRemoteAddress(); + }, + ST: function serverName(client) { + return client.session.serverName; + }, + FN: function activeFileBaseFilterName(client) { const activeFilter = FileBaseFilters.getActiveFilter(client); return activeFilter ? activeFilter.name : '(Unknown)'; }, @@ -234,19 +283,22 @@ const PREDEFINED_MCI_GENERATORS = { const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0; return moment.duration(minutes, 'minutes').humanize(); }, - NM : function userNewMessagesAddressedToCount(client) { - return StatLog.getUserStatNumByClient(client, UserProps.NewAddressedToMessageCount); + NM: function userNewMessagesAddressedToCount(client) { + return StatLog.getUserStatNumByClient( + client, + UserProps.NewAddressedToMessageCount + ); }, - NP : function userNewPrivateMailCount(client) { + NP: function userNewPrivateMailCount(client) { return StatLog.getUserStatNumByClient(client, UserProps.NewPrivateMailCount); }, - IA : function userStatusAvailableIndicator(client) { + IA: function userStatusAvailableIndicator(client) { const indicators = client.currentTheme.helpers.getStatusAvailIndicators(); - return client.user.isAvailable() ? (indicators[0] || 'Y') : (indicators[1] || 'N'); + return client.user.isAvailable() ? indicators[0] || 'Y' : indicators[1] || 'N'; }, - IV : function userStatusVisibleIndicator(client) { + IV: function userStatusVisibleIndicator(client) { const indicators = client.currentTheme.helpers.getStatusVisibleIndicators(); - return client.user.isVisible() ? (indicators[0] || 'Y') : (indicators[1] || 'N'); + return client.user.isVisible() ? indicators[0] || 'Y' : indicators[1] || 'N'; }, // @@ -294,27 +346,37 @@ const PREDEFINED_MCI_GENERATORS = { .trim(); }, - MB : function totalMemoryBytes() { - const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || { totalBytes : 0 }; + MB: function totalMemoryBytes() { + const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || { + totalBytes: 0, + }; return formatByteSize(stats.totalBytes, true); // true=withAbbr }, - MF : function totalMemoryFreeBytes() { - const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || { freeBytes : 0 }; - return formatByteSize(stats.freeBytes, true); // true=withAbbr + MF: function totalMemoryFreeBytes() { + const stats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || { + freeBytes: 0, + }; + return formatByteSize(stats.freeBytes, true); // true=withAbbr }, - LA : function systemLoadAverage() { - const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { average : 0.0 }; + LA: function systemLoadAverage() { + const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { average: 0.0 }; return stats.average.toLocaleString(); }, - CL : function systemCurrentLoad() { - const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { current : 0 }; + CL: function systemCurrentLoad() { + const stats = StatLog.getSystemStat(SysProps.SystemLoadStats) || { current: 0 }; return `${stats.current}%`; }, - UU : function systemUptime() { + UU: function systemUptime() { return moment.duration(process.uptime(), 'seconds').humanize(); }, - NV : function nodeVersion() { return process.version; }, - AN : function activeNodes() { return clientConnections.getActiveConnections(clientConnections.UserVisibleConnections).length.toString(); }, + NV: function nodeVersion() { + return process.version; + }, + AN: function activeNodes() { + return clientConnections + .getActiveConnections(clientConnections.UserVisibleConnections) + .length.toString(); + }, TC: function totalCalls() { return StatLog.getSystemStat(SysProps.LoginCount).toLocaleString(); @@ -333,13 +395,17 @@ const PREDEFINED_MCI_GENERATORS = { // // System File Base, Up/Download Info // - SD : function systemNumDownloads() { return StatLog.getFriendlySystemStat(SysProps.FileDlTotalCount, 0); }, - SO : function systemByteDownload() { + SD: function systemNumDownloads() { + return StatLog.getFriendlySystemStat(SysProps.FileDlTotalCount, 0); + }, + SO: function systemByteDownload() { const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, - SU : function systemNumUploads() { return StatLog.getFriendlySystemStat(SysProps.FileUlTotalCount, 0); }, - SP : function systemByteUpload() { + SU: function systemNumUploads() { + return StatLog.getFriendlySystemStat(SysProps.FileUlTotalCount, 0); + }, + SP: function systemByteUpload() { const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTotalBytes); return formatByteSize(byteSize, true); // true=withAbbr }, @@ -352,49 +418,55 @@ const PREDEFINED_MCI_GENERATORS = { const totalBytes = parseInt(_.get(areaStats, 'totalBytes', 0)); return formatByteSize(totalBytes, true); // true=withAbbr }, - PT : function messagesPostedToday() { // Obv/2 + PT: function messagesPostedToday() { + // Obv/2 return StatLog.getFriendlySystemStat(SysProps.MessagesToday, 0); }, - TP : function totalMessagesOnSystem() { // Obv/2 + TP: function totalMessagesOnSystem() { + // Obv/2 return StatLog.getFriendlySystemStat(SysProps.MessageTotalCount, 0); }, - FT : function totalUploadsToday() { // Obv/2 + FT: function totalUploadsToday() { + // Obv/2 return StatLog.getFriendlySystemStat(SysProps.FileUlTodayCount, 0); }, - FB : function totalUploadBytesToday() { + FB: function totalUploadBytesToday() { const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTodayBytes); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - DD : function totalDownloadsToday() { // iNiQUiTY + DD: function totalDownloadsToday() { + // iNiQUiTY return StatLog.getFriendlySystemStat(SysProps.FileDlTodayCount, 0); }, - DB : function totalDownloadBytesToday() { + DB: function totalDownloadBytesToday() { const byteSize = StatLog.getSystemStatNum(SysProps.FileDlTodayBytes); - return formatByteSize(byteSize, true); // true=withAbbr + return formatByteSize(byteSize, true); // true=withAbbr }, - NT : function totalNewUsersToday() { // Obv/2 + NT: function totalNewUsersToday() { + // Obv/2 return StatLog.getSystemStatNum(SysProps.NewUsersTodayCount); }, // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) // :TODO: ?? - Total users on system - TU : function totalSystemUsers() { + TU: function totalSystemUsers() { return StatLog.getSystemStatNum(SysProps.TotalUserCount) || 1; }, - LC : function lastCallerUserName() { // Obv/2 + LC: function lastCallerUserName() { + // Obv/2 const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {}; return lastLogin.userName || 'N/A'; }, - LD : function lastCallerDate(client) { + LD: function lastCallerDate(client) { const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {}; if (!lastLogin.timestamp) { return 'N/A'; } return lastLogin.timestamp.format(client.currentTheme.helpers.getDateFormat()); }, - LT : function lastCallerTime(client) { + LT: function lastCallerTime(client) { const lastLogin = StatLog.getSystemStat(SysProps.LastLogin) || {}; if (!lastLogin.timestamp) { return 'N/A'; @@ -437,8 +509,11 @@ function getPredefinedMCIValue(client, code, extra) { let value; try { value = generator(client, extra); - } catch(e) { - Log.error( { code : code, exception : e.message }, `Failed generating predefined MCI value (${code})` ); + } catch (e) { + Log.error( + { code: code, exception: e.message }, + `Failed generating predefined MCI value (${code})` + ); } return value; diff --git a/core/stat_log.js b/core/stat_log.js index fe3c2614..8c80fc6c 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -1,19 +1,17 @@ /* jslint node: true */ 'use strict'; -const sysDb = require('./database.js').dbs.system; -const { - getISOTimestampString -} = require('./database.js'); -const Errors = require('./enig_error.js'); -const SysProps = require('./system_property.js'); +const sysDb = require('./database.js').dbs.system; +const { getISOTimestampString } = require('./database.js'); +const Errors = require('./enig_error.js'); +const SysProps = require('./system_property.js'); const UserProps = require('./user_property'); const Message = require('./message'); // deps -const _ = require('lodash'); -const moment = require('moment'); -const SysInfo = require('systeminformation'); +const _ = require('lodash'); +const moment = require('moment'); +const SysInfo = require('systeminformation'); /* System Event Log & Stats @@ -171,7 +169,7 @@ class StatLog { return parseInt(this.getUserStat(user, statName)) || 0; } - getUserStatNumByClient(client, statName, ttlSeconds=10) { + getUserStatNumByClient(client, statName, ttlSeconds = 10) { const stat = this.getUserStatNum(client.user, statName); this._refreshUserStat(client, statName, ttlSeconds); return stat; @@ -359,8 +357,8 @@ class StatLog { _refreshSystemStat(statName) { switch (statName) { - case SysProps.SystemLoadStats : - case SysProps.SystemMemoryStats : + case SysProps.SystemLoadStats: + case SysProps.SystemMemoryStats: return this._refreshSysInfoStats(); } } @@ -374,23 +372,27 @@ class StatLog { this.lastSysInfoStatsRefresh = now; const basicSysInfo = { - mem : 'total, free', - currentLoad : 'avgLoad, currentLoad', + mem: 'total, free', + currentLoad: 'avgLoad, currentLoad', }; SysInfo.get(basicSysInfo) .then(sysInfo => { const memStats = { - totalBytes : sysInfo.mem.total, - freeBytes : sysInfo.mem.free, + totalBytes: sysInfo.mem.total, + freeBytes: sysInfo.mem.free, }; this.setNonPersistentSystemStat(SysProps.SystemMemoryStats, memStats); const loadStats = { // Not avail on BSD, yet. - average : parseFloat(_.get(sysInfo, 'currentLoad.avgLoad', 0).toFixed(2)), - current : parseFloat(_.get(sysInfo, 'currentLoad.currentLoad', 0).toFixed(2)), + average: parseFloat( + _.get(sysInfo, 'currentLoad.avgLoad', 0).toFixed(2) + ), + current: parseFloat( + _.get(sysInfo, 'currentLoad.currentLoad', 0).toFixed(2) + ), }; this.setNonPersistentSystemStat(SysProps.SystemLoadStats, loadStats); @@ -401,13 +403,23 @@ class StatLog { } _refreshUserStat(client, statName, ttlSeconds) { - switch(statName) { + switch (statName) { case UserProps.NewPrivateMailCount: - this._wrapUserRefreshWithCachedTTL(client, statName, this._refreshUserPrivateMailCount, ttlSeconds); + this._wrapUserRefreshWithCachedTTL( + client, + statName, + this._refreshUserPrivateMailCount, + ttlSeconds + ); break; case UserProps.NewAddressedToMessageCount: - this._wrapUserRefreshWithCachedTTL(client, statName, this._refreshUserNewAddressedToMessageCount, ttlSeconds); + this._wrapUserRefreshWithCachedTTL( + client, + statName, + this._refreshUserNewAddressedToMessageCount, + ttlSeconds + ); break; } } @@ -427,17 +439,21 @@ class StatLog { _refreshUserPrivateMailCount(client) { const MsgArea = require('./message_area'); - MsgArea.getNewMessageCountInAreaForUser(client.user.userId, Message.WellKnownAreaTags.Private, (err, count) => { - if (!err) { - client.user.setProperty(UserProps.NewPrivateMailCount, count); + MsgArea.getNewMessageCountInAreaForUser( + client.user.userId, + Message.WellKnownAreaTags.Private, + (err, count) => { + if (!err) { + client.user.setProperty(UserProps.NewPrivateMailCount, count); + } } - }); + ); } _refreshUserNewAddressedToMessageCount(client) { const MsgArea = require('./message_area'); MsgArea.getNewMessageCountAddressedToUser(client, (err, count) => { - if(!err) { + if (!err) { client.user.setProperty(UserProps.NewAddressedToMessageCount, count); } }); @@ -445,21 +461,19 @@ class StatLog { _findLogEntries(logTable, 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 ${logTable}`; } else { - sql = - `SELECT timestamp, log_value + sql = `SELECT timestamp, log_value FROM ${logTable}`; } @@ -473,40 +487,42 @@ class StatLog { sql += ` AND session_id = ${filter.sessionId}`; } - 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); }); } diff --git a/core/string_util.js b/core/string_util.js index 57815194..3f78b37d 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -379,9 +379,9 @@ function formatByteSizeAbbr(byteSize) { } 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)).toString(); - 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)).toString(); + if (withAbbr) { result += ` ${BYTE_SIZE_ABBRS[i]}`; } return result; diff --git a/core/sys_event_user_log.js b/core/sys_event_user_log.js index 8061b11f..1f42ff29 100644 --- a/core/sys_event_user_log.js +++ b/core/sys_event_user_log.js @@ -1,9 +1,9 @@ /* jslint node: true */ 'use strict'; -const Events = require('./events.js'); -const LogNames = require('./user_log_name.js'); -const SysProps = require('./system_property.js'); +const Events = require('./events.js'); +const LogNames = require('./user_log_name.js'); +const SysProps = require('./system_property.js'); const DefaultKeepForDays = 365; diff --git a/core/system_log.js b/core/system_log.js index 47e10fa4..1a833f01 100644 --- a/core/system_log.js +++ b/core/system_log.js @@ -5,6 +5,6 @@ // Common SYSTEM/global log keys // module.exports = { - UserAddedRumorz : 'system_rumorz', - UserLoginHistory : 'user_login_history', // JSON object + UserAddedRumorz: 'system_rumorz', + UserLoginHistory: 'user_login_history', // JSON object }; diff --git a/core/system_property.js b/core/system_property.js index c9e17b8e..b8d8b5c8 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -8,9 +8,9 @@ // their own! // module.exports = { - LoginCount : 'login_count', - LoginsToday : 'logins_today', // non-persistent - LastLogin : 'last_login', // object { userId, sessionId, userName, userRealName, timestamp }; non-persistent + LoginCount: 'login_count', + LoginsToday: 'logins_today', // non-persistent + LastLogin: 'last_login', // object { userId, sessionId, userName, userRealName, timestamp }; non-persistent FileBaseAreaStats: 'file_base_area_stats', // object - see file_base_area.js::getAreaStats FileUlTotalCount: 'ul_total_count', @@ -18,26 +18,26 @@ module.exports = { FileDlTotalCount: 'dl_total_count', FileDlTotalBytes: 'dl_total_bytes', - FileUlTodayCount : 'ul_today_count', // non-persistent - FileUlTodayBytes : 'ul_today_bytes', // non-persistent - FileDlTodayCount : 'dl_today_count', // non-persistent - FileDlTodayBytes : 'dl_today_bytes', // non-persistent + FileUlTodayCount: 'ul_today_count', // non-persistent + FileUlTodayBytes: 'ul_today_bytes', // non-persistent + FileDlTodayCount: 'dl_today_count', // non-persistent + FileDlTodayBytes: 'dl_today_bytes', // 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 + 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 - SysOpUsername : 'sysop_username', // non-persistent - SysOpRealName : 'sysop_real_name', // non-persistent - SysOpLocation : 'sysop_location', // non-persistent - SysOpAffiliations : 'sysop_affiliation', // non-persistent - SysOpSex : 'sysop_sex', // non-persistent - SysOpEmailAddress : 'sysop_email_address', // non-persistent + SysOpUsername: 'sysop_username', // non-persistent + SysOpRealName: 'sysop_real_name', // non-persistent + SysOpLocation: 'sysop_location', // non-persistent + SysOpAffiliations: 'sysop_affiliation', // non-persistent + SysOpSex: 'sysop_sex', // non-persistent + SysOpEmailAddress: 'sysop_email_address', // non-persistent - NextRandomRumor : 'random_rumor', + NextRandomRumor: 'random_rumor', - SystemMemoryStats : 'system_memory_stats', // object { totalBytes, freeBytes }; non-persistent - SystemLoadStats : 'system_load_stats', // object { average, current }; non-persistent + SystemMemoryStats: 'system_memory_stats', // object { totalBytes, freeBytes }; non-persistent + SystemLoadStats: 'system_load_stats', // object { average, current }; non-persistent - TotalUserCount : 'user_total_count', // non-persistent - NewUsersTodayCount : 'user_new_today_count', // non-persistent + TotalUserCount: 'user_total_count', // non-persistent + NewUsersTodayCount: 'user_new_today_count', // non-persistent }; diff --git a/core/telnet_bridge.js b/core/telnet_bridge.js index 993fc60e..b4a836cf 100644 --- a/core/telnet_bridge.js +++ b/core/telnet_bridge.js @@ -217,10 +217,15 @@ exports.getModule = class TelnetBridgeModule extends MenuModule { }); telnetConnection.on('end', err => { - self.client.removeListener('key press', connectionKeyPressHandler); + self.client.removeListener( + 'key press', + connectionKeyPressHandler + ); - if(err) { - self.client.log.warn(`Telnet bridge connection error: ${err.message}`); + if (err) { + self.client.log.warn( + `Telnet bridge connection error: ${err.message}` + ); } callback( diff --git a/core/theme.js b/core/theme.js index 3fef5886..e97a11da 100644 --- a/core/theme.js +++ b/core/theme.js @@ -364,17 +364,30 @@ exports.ThemeManager = class ThemeManager { 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 + ); }, - getStatusAvailIndicators : function() { - const format = Config().theme.statusAvailableIndicators || [ 'Y', 'N' ]; - return _.get(theme, 'customization.defaults.statusAvailableIndicators', format); + getStatusAvailIndicators: function () { + const format = Config().theme.statusAvailableIndicators || ['Y', 'N']; + return _.get( + theme, + 'customization.defaults.statusAvailableIndicators', + format + ); }, - getStatusVisibleIndicators : function() { - const format = Config().theme.statusVisibleIndicators || [ 'Y', 'N' ]; - return _.get(theme, 'customization.defaults.statusVisibleIndicators', format); + getStatusVisibleIndicators: function () { + const format = Config().theme.statusVisibleIndicators || ['Y', 'N']; + return _.get( + theme, + 'customization.defaults.statusVisibleIndicators', + format + ); }, }; } diff --git a/core/user.js b/core/user.js index e4ce1e23..58cf69d9 100644 --- a/core/user.js +++ b/core/user.js @@ -12,22 +12,22 @@ 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'); 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.statusFlags = User.StatusFlags.None; + this.userId = 0; + this.username = ''; + this.properties = {}; // name:value + this.groups = []; // group membership(s) + this.authFactor = User.AuthFactors.None; + this.statusFlags = User.StatusFlags.None; } // static property accessors @@ -72,10 +72,10 @@ module.exports = class User { static get StatusFlags() { return { - None : 0x00000000, - NotAvailable : 0x00000001, // Not currently available for chat, message, page, etc. - NotVisible : 0x00000002, // Invisible -- does not show online, last callers, etc. - } + None: 0x00000000, + NotAvailable: 0x00000001, // Not currently available for chat, message, page, etc. + NotVisible: 0x00000002, // Invisible -- does not show online, last callers, etc. + }; } isAuthenticated() { @@ -736,21 +736,27 @@ module.exports = class User { if (!cb && _.isFunction(propsList)) { cb = propsList; propsList = [ - UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, - UserProps.Location, UserProps.Affiliations, + UserProps.RealName, + UserProps.Sex, + UserProps.EmailAddress, + UserProps.Location, + UserProps.Affiliations, ]; } async.waterfall( [ - (callback) => { + callback => { return User.getUserName(userId, callback); }, (userName, callback) => { - User.loadProperties(userId, { names : propsList }, (err, props) => { - return callback(err, Object.assign({}, props, { user_name : userName })); + User.loadProperties(userId, { names: propsList }, (err, props) => { + return callback( + err, + Object.assign({}, props, { user_name: userName }) + ); }); - } + }, ], (err, userProps) => { if (err) { @@ -904,7 +910,7 @@ module.exports = class User { `SELECT count() AS user_count FROM user;`, (err, row) => { - if(err) { + if (err) { return cb(err); } return cb(null, row.user_count); diff --git a/core/user_config.js b/core/user_config.js index 3f7d1bcc..859c63e8 100644 --- a/core/user_config.js +++ b/core/user_config.js @@ -140,20 +140,30 @@ exports.getModule = class UserConfigModule extends MenuModule { return self.prevMenu(cb); } - self.client.log.info(`User "${self.client.user.username}" updated configuration`); + self.client.log.info( + `User "${self.client.user.username}" updated configuration` + ); // // New password if it's not empty // - 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 "${self.client.user.username}" updated 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 "${self.client.user.username}" updated authentication credentials` + ); + } + return self.prevMenu(cb); } - return self.prevMenu(cb); - }); + ); } else { return self.prevMenu(cb); } diff --git a/core/user_interrupt_queue.js b/core/user_interrupt_queue.js index 5d785703..c495d5a4 100644 --- a/core/user_interrupt_queue.js +++ b/core/user_interrupt_queue.js @@ -25,13 +25,15 @@ module.exports = class UserInterruptQueue { } else if (opts.omit) { omitNodes = [opts.omit]; } - omitNodes = omitNodes.map(n => _.isNumber(n) ? n : n.node); + omitNodes = omitNodes.map(n => (_.isNumber(n) ? n : n.node)); const connOpts = { authUsersOnly: true, visibleOnly: true, availOnly: true, }; - opts.clients = getActiveConnections(connOpts).filter(ac => !omitNodes.includes(ac.node)); + opts.clients = getActiveConnections(connOpts).filter( + ac => !omitNodes.includes(ac.node) + ); } if (!Array.isArray(opts.clients)) { opts.clients = [opts.clients]; diff --git a/core/user_login.js b/core/user_login.js index 9f151343..e7242132 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -21,10 +21,10 @@ const { const { getFileAreaByTag, getDefaultFileAreaTag } = require('./file_base_area.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.userLogin = userLogin; exports.recordLogin = recordLogin; @@ -38,8 +38,11 @@ function userLogin(client, username, password, options, cb) { const config = Config(); - if(config.users.badUserNames.includes(username.toLowerCase())) { - client.log.info( { username, ip : client.remoteAddress }, `Attempt to login with banned username "${username}"`); + if (config.users.badUserNames.includes(username.toLowerCase())) { + client.log.info( + { username, ip: client.remoteAddress }, + `Attempt to login with banned username "${username}"` + ); // slow down a bit to thwart brute force attacks return setTimeout(() => { @@ -75,7 +78,7 @@ function userLogin(client, username, password, options, cb) { ); // ...but same user }); - if(existingClientConnection) { + if (existingClientConnection) { client.log.warn( { existingNodeId: existingClientConnection.node, @@ -193,8 +196,13 @@ function recordLogin(client, cb) { StatLog.incrementNonPersistentSystemStat(SysProps.LoginsToday, 1); return StatLog.incrementSystemStat(SysProps.LoginCount, 1, callback); }, - (callback) => { - return StatLog.setUserStat(user, UserProps.LastLoginTs, loginTimestamp, callback); + callback => { + return StatLog.setUserStat( + user, + UserProps.LastLoginTs, + loginTimestamp, + callback + ); }, callback => { return StatLog.incrementUserStat(user, UserProps.LoginCount, 1, callback); @@ -214,24 +222,24 @@ function recordLogin(client, cb) { callback ); }, - (callback) => { + callback => { // Update live last login information which includes additional // (pre-resolved) information such as user name/etc. const lastLogin = { - userId : user.userId, - sessionId : user.sessionId, - userName : user.username, - realName : user.getProperty(UserProps.RealName), - affiliation : user.getProperty(UserProps.Affiliations), - emailAddress : user.getProperty(UserProps.EmailAddress), - sex : user.getProperty(UserProps.Sex), - location : user.getProperty(UserProps.Location), - timestamp : moment(loginTimestamp), + userId: user.userId, + sessionId: user.sessionId, + userName: user.username, + realName: user.getProperty(UserProps.RealName), + affiliation: user.getProperty(UserProps.Affiliations), + emailAddress: user.getProperty(UserProps.EmailAddress), + sex: user.getProperty(UserProps.Sex), + location: user.getProperty(UserProps.Location), + timestamp: moment(loginTimestamp), }; StatLog.setNonPersistentSystemStat(SysProps.LastLogin, lastLogin); return callback(null); - } + }, ], err => { return cb(err); @@ -247,6 +255,9 @@ function transformLoginError(err, client, username) { err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany); } - client.log.warn( { username, ip : client.remoteAddress, reason : err.message }, `Failed login attempt for user "${username}", ${client.friendlyRemoteAddress()}`); + client.log.warn( + { username, ip: client.remoteAddress, reason: err.message }, + `Failed login attempt for user "${username}", ${client.friendlyRemoteAddress()}` + ); return err; } diff --git a/core/user_property.js b/core/user_property.js index f0af68be..c3f979b3 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -59,8 +59,8 @@ module.exports = { MinutesOnlineTotalCount: 'minutes_online_total_count', - NewPrivateMailCount : 'new_private_mail_count', // non-persistent - NewAddressedToMessageCount : 'new_addr_to_msg_count', // non-persistent + NewPrivateMailCount: 'new_private_mail_count', // non-persistent + NewAddressedToMessageCount: 'new_addr_to_msg_count', // non-persistent 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 diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index b6f01af3..59455a91 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -9,16 +9,16 @@ 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'); const { throws } = require('assert'); exports.VerticalMenuView = VerticalMenuView; function VerticalMenuView(options) { - options.cursor = options.cursor || 'hide'; - options.justify = options.justify || 'left'; - this.focusItemAtTop = true; + options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; + this.focusItemAtTop = true; MenuView.call(this, options); @@ -53,8 +53,8 @@ function VerticalMenuView(options) { const topIndex = (this.focusItemAtTop ? throws.focusedItemIndex : 0) || 0; self.viewWindow = { - top : topIndex, - bottom : Math.min(topIndex + self.maxVisibleItems, self.items.length) - 1, + top: topIndex, + bottom: Math.min(topIndex + self.maxVisibleItems, self.items.length) - 1, }; }; @@ -109,14 +109,17 @@ function VerticalMenuView(options) { this.setRenderCacheItem(index, text, item.focused); }; - this.drawRemovedItem = function(index) { + this.drawRemovedItem = function (index) { if (index <= this.items.length - 1) { return; } const row = this.position.row + index; - this.client.term.rawWrite(`${ansi.goto(row, this.position.col)}${ansi.normal()}${this.fillChar.repeat(this.dimens.width)}`) + this.client.term.rawWrite( + `${ansi.goto(row, this.position.col)}${ansi.normal()}${this.fillChar.repeat( + this.dimens.width + )}` + ); }; - } util.inherits(VerticalMenuView, MenuView); @@ -188,15 +191,15 @@ VerticalMenuView.prototype.setFocus = function (focused) { VerticalMenuView.prototype.setFocusItemIndex = function (index) { VerticalMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - const remainAfterFocus = this.focusItemAtTop ? - this.items.length - index : - this.items.length; - if(remainAfterFocus >= this.maxVisibleItems) { + const remainAfterFocus = this.focusItemAtTop + ? this.items.length - index + : this.items.length; + if (remainAfterFocus >= this.maxVisibleItems) { const topIndex = (this.focusItemAtTop ? throws.focusedItemIndex : 0) || 0; this.viewWindow = { - top : topIndex, - bottom : Math.min(topIndex + this.maxVisibleItems, this.items.length) - 1 + top: topIndex, + bottom: Math.min(topIndex + this.maxVisibleItems, this.items.length) - 1, }; this.positionCacheExpired = false; // skip standard behavior @@ -407,7 +410,7 @@ VerticalMenuView.prototype.setItemSpacing = function (itemSpacing) { this.positionCacheExpired = true; }; -VerticalMenuView.prototype.setPropertyValue = function(propName, value) { +VerticalMenuView.prototype.setPropertyValue = function (propName, value) { if (propName === 'focusItemAtTop' && _.isBoolean(value)) { this.focusItemAtTop = value; } diff --git a/core/view.js b/core/view.js index 341517f7..5063322a 100644 --- a/core/view.js +++ b/core/view.js @@ -130,7 +130,7 @@ View.prototype.setPosition = function (pos) { // // Allow the following forms: [row, col], { row : r, col : c }, or (row, col) // - if(Array.isArray(pos)) { + if (Array.isArray(pos)) { this.position.row = pos[0]; this.position.col = pos[1]; } else if (_.isNumber(pos.row) && _.isNumber(pos.col)) { @@ -285,7 +285,7 @@ View.prototype.setFocusProperty = function (focused) { this.hasFocus = focused; }; -View.prototype.setFocus = function(focused) { +View.prototype.setFocus = function (focused) { // Call separate method to differentiate between a value set as a // property vs focus programmatically called. this.setFocusProperty(focused); diff --git a/core/view_controller.js b/core/view_controller.js index 696e18b7..daa37b51 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -257,56 +257,59 @@ function ViewController(options) { } }; - 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) { - return nextItem(null); - } - - 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) { - const highestIdView = self.getView(highestId); - if(highestIdView) { - highestIdView.submit = true; + 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) { + return nextItem(null); + } + + 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) { + const highestIdView = self.getView(highestId); + if (highestIdView) { + highestIdView.submit = true; + } + } + + return cb(err, { initialFocusId: initialFocusId }); + } + ); }; // method for comparing submitted form data to configuration entries diff --git a/core/wfc.js b/core/wfc.js index 40eedfe1..b36070f8 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -2,10 +2,7 @@ const { MenuModule } = require('./menu_module'); const stringFormat = require('./string_format'); -const { - getActiveConnectionList, - AllConnections -} = require('./client_connections'); +const { getActiveConnectionList, AllConnections } = require('./client_connections'); const StatLog = require('./stat_log'); const SysProps = require('./system_property'); const UserProps = require('./user_property'); @@ -19,9 +16,9 @@ const moment = require('moment'); const bunyan = require('bunyan'); exports.moduleInfo = { - name : 'WFC', - desc : 'Semi-Traditional Waiting For Caller', - author : 'NuSkooler', + name: 'WFC', + desc: 'Semi-Traditional Waiting For Caller', + author: 'NuSkooler', }; const FormIds = { @@ -31,13 +28,13 @@ const FormIds = { }; const MciViewIds = { - main : { - nodeStatus : 1, - quickLogView : 2, - nodeStatusSelection : 3, + main: { + nodeStatus: 1, + quickLogView: 2, + nodeStatusSelection: 3, - customRangeStart : 10, - } + customRangeStart: 10, + }, }; // Secure + 2FA + root user + 'wfc' group. @@ -49,30 +46,32 @@ exports.getModule = class WaitingForCallerModule 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.acs = this.config.acs || DefaultACS; if (!this.config.acs.includes('SC')) { - this.config.acs = 'SC' + this.config.acs; // secure connection at the very least + this.config.acs = 'SC' + this.config.acs; // secure connection at the very least } this.selectedNodeStatusIndex = -1; // no selection this.menuMethods = { - toggleAvailable : (formData, extraArgs, cb) => { + toggleAvailable: (formData, extraArgs, cb) => { const avail = this.client.user.isAvailable(); this.client.user.setAvailability(!avail); return this._refreshAll(cb); }, - toggleVisible : (formData, extraArgs, cb) => { + toggleVisible: (formData, extraArgs, cb) => { const visible = this.client.user.isVisible(); this.client.user.setVisibility(!visible); return this._refreshAll(cb); }, - displayHelp : (formData, extraArgs, cb) => { + displayHelp: (formData, extraArgs, cb) => { return this._displayHelpPage(cb); }, - setNodeStatusSelection : (formData, extraArgs, cb) => { + setNodeStatusSelection: (formData, extraArgs, cb) => { const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); if (!nodeStatusView) { return cb(null); @@ -89,19 +88,19 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); } return cb(null); - } - } + }, + }; } initSequence() { async.series( [ - (callback) => { + callback => { return this.beforeArt(callback); }, - (callback) => { + callback => { return this._displayMainPage(false, callback); - } + }, ], () => { this.finishedLoading(); @@ -112,7 +111,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { _displayMainPage(clearScreen, cb) { async.series( [ - (callback) => { + callback => { return this.displayArtAndPrepViewController( 'main', FormIds.main, @@ -120,34 +119,49 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { callback ); }, - (callback) => { - const quickLogView = this.getView('main', MciViewIds.main.quickLogView); + callback => { + const quickLogView = this.getView( + 'main', + MciViewIds.main.quickLogView + ); if (!quickLogView) { return callback(null); } if (!this.logRingBuffer) { - const logLevel = this.config.quickLogLevel || // WFC specific - _.get(Config(), 'logging.rotatingFile.level') || // ...or system setting - 'info'; // ...or default to info + const logLevel = + this.config.quickLogLevel || // WFC specific + _.get(Config(), 'logging.rotatingFile.level') || // ...or system setting + 'info'; // ...or default to info - this.logRingBuffer = new bunyan.RingBuffer({ limit : quickLogView.dimens.height || 24 }); + this.logRingBuffer = new bunyan.RingBuffer({ + limit: quickLogView.dimens.height || 24, + }); Log.log.addStream({ - name : 'wfc-ringbuffer', - type : 'raw', - level : logLevel, - stream : this.logRingBuffer + name: 'wfc-ringbuffer', + type: 'raw', + level: logLevel, + stream: this.logRingBuffer, }); } - const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); - const nodeStatusSelectionView = this.getView('main', MciViewIds.main.nodeStatusSelection); - const nodeStatusSelectionFormat = this.config.nodeStatusSelectionFormat || '{text}'; + const nodeStatusView = this.getView( + 'main', + MciViewIds.main.nodeStatus + ); + const nodeStatusSelectionView = this.getView( + 'main', + MciViewIds.main.nodeStatusSelection + ); + const nodeStatusSelectionFormat = + this.config.nodeStatusSelectionFormat || '{text}'; if (nodeStatusView && nodeStatusSelectionView) { nodeStatusView.on('index update', index => { const item = nodeStatusView.getItems()[index]; if (item) { - nodeStatusSelectionView.setText(stringFormat(nodeStatusSelectionFormat, item)); + nodeStatusSelectionView.setText( + stringFormat(nodeStatusSelectionFormat, item) + ); // :TODO: Update view // :TODO: this is not triggered by key-presses (1, 2, ...) -- we need to handle that as well } @@ -156,9 +170,9 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return callback(null); }, - (callback) => { + callback => { return this._refreshAll(callback); - } + }, ], err => { if (!err) { @@ -172,15 +186,11 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { _displayHelpPage(cb) { this._stopRefreshing(); - this.displayAsset( - this.menuConfig.config.art.help, - { clearScreen : true }, - () => { - this.client.waitForKeyPress( () => { - return this._displayMainPage(true, cb); - }); - } - ); + this.displayAsset(this.menuConfig.config.art.help, { clearScreen: true }, () => { + this.client.waitForKeyPress(() => { + return this._displayMainPage(true, cb); + }); + }); } enter() { @@ -207,11 +217,15 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { const vis = this.config.opVisibility || 'current'; switch (vis) { - case 'hidden' : this.client.user.setVisibility(false); break; - case 'visible' : this.client.user.setVisibility(true); break; - default : break; + case 'hidden': + this.client.user.setVisibility(false); + break; + case 'visible': + this.client.user.setVisibility(true); + break; + default: + break; } - } _restoreOpVisibility() { @@ -223,7 +237,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { this._stopRefreshing(); } - this.mainRefreshTimer = setInterval( () => { + this.mainRefreshTimer = setInterval(() => { this._refreshAll(); }, MainStatRefreshTimeMs); } @@ -238,23 +252,23 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { _refreshAll(cb) { async.series( [ - (callback) => { + callback => { return this._refreshStats(callback); }, - (callback) => { + callback => { return this._refreshNodeStatus(callback); }, - (callback) => { + callback => { return this._refreshQuickLog(callback); }, - (callback) => { + callback => { this.updateCustomViewTextsWithFilter( 'main', MciViewIds.main.customRangeStart, this.stats ); return callback(null); - } + }, ], err => { if (cb) { @@ -265,46 +279,47 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } _getStatusStrings(isAvailable, isVisible) { - const availIndicators = Array.isArray(this.config.statusAvailableIndicators) ? - this.config.statusAvailableIndicators : - this.client.currentTheme.helpers.getStatusAvailableIndicators(); - const visIndicators = Array.isArray(this.config.statusVisibleIndicators) ? - this.config.statusVisibleIndicators : - this.client.currentTheme.helpers.getStatusVisibleIndicators(); + const availIndicators = Array.isArray(this.config.statusAvailableIndicators) + ? this.config.statusAvailableIndicators + : this.client.currentTheme.helpers.getStatusAvailableIndicators(); + const visIndicators = Array.isArray(this.config.statusVisibleIndicators) + ? this.config.statusVisibleIndicators + : this.client.currentTheme.helpers.getStatusVisibleIndicators(); return [ - isAvailable ? (availIndicators[1] || 'Y') : (availIndicators[0] || 'N'), - isVisible ? (visIndicators[1] || 'Y') : (visIndicators[0] || 'N'), + isAvailable ? availIndicators[1] || 'Y' : availIndicators[0] || 'N', + isVisible ? visIndicators[1] || 'Y' : visIndicators[0] || 'N', ]; } _refreshStats(cb) { - const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || {}; - const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || {}; - const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats) || {}; - const lastLoginStats = StatLog.getSystemStat(SysProps.LastLogin); + const fileAreaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || {}; + const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || {}; + const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats) || {}; + const lastLoginStats = StatLog.getSystemStat(SysProps.LastLogin); const now = moment(); const [availIndicator, visIndicator] = this._getStatusStrings( - this.client.user.isAvailable(), this.client.user.isVisible() + this.client.user.isAvailable(), + this.client.user.isVisible() ); this.stats = { // Date/Time - nowDate : now.format(this.getDateFormat()), - nowTime : now.format(this.getTimeFormat()), - now : now.format(this._dateTimeFormat('now')), + nowDate: now.format(this.getDateFormat()), + nowTime: now.format(this.getTimeFormat()), + now: now.format(this._dateTimeFormat('now')), // Current process (our Node.js service) - processUptimeSeconds : process.uptime(), + processUptimeSeconds: process.uptime(), // Totals - totalCalls : StatLog.getSystemStatNum(SysProps.LoginCount), - totalPosts : StatLog.getSystemStatNum(SysProps.MessageTotalCount), - totalUsers : StatLog.getSystemStatNum(SysProps.TotalUserCount), - totalFiles : fileAreaStats.totalFiles || 0, - totalFileBytes : fileAreaStats.totalFileBytes || 0, + totalCalls: StatLog.getSystemStatNum(SysProps.LoginCount), + totalPosts: StatLog.getSystemStatNum(SysProps.MessageTotalCount), + totalUsers: StatLog.getSystemStatNum(SysProps.TotalUserCount), + totalFiles: fileAreaStats.totalFiles || 0, + totalFileBytes: fileAreaStats.totalFileBytes || 0, // totalUploads : // totalUploadBytes : @@ -312,30 +327,42 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { // totalDownloadBytes : // Today's Stats - callsToday : StatLog.getSystemStatNum(SysProps.LoginsToday), - postsToday : StatLog.getSystemStatNum(SysProps.MessagesToday), - uploadsToday : StatLog.getSystemStatNum(SysProps.FileUlTodayCount), - uploadBytesToday : StatLog.getSystemStatNum(SysProps.FileUlTodayBytes), - downloadsToday : StatLog.getSystemStatNum(SysProps.FileDlTodayCount), - downloadBytesToday : StatLog.getSystemStatNum(SysProps.FileDlTodayBytes), - newUsersToday : StatLog.getSystemStatNum(SysProps.NewUsersTodayCount), + callsToday: StatLog.getSystemStatNum(SysProps.LoginsToday), + postsToday: StatLog.getSystemStatNum(SysProps.MessagesToday), + uploadsToday: StatLog.getSystemStatNum(SysProps.FileUlTodayCount), + uploadBytesToday: StatLog.getSystemStatNum(SysProps.FileUlTodayBytes), + downloadsToday: StatLog.getSystemStatNum(SysProps.FileDlTodayCount), + downloadBytesToday: StatLog.getSystemStatNum(SysProps.FileDlTodayBytes), + newUsersToday: StatLog.getSystemStatNum(SysProps.NewUsersTodayCount), // Current - currentUserName : this.client.user.username, - currentUserRealName : this.client.user.getProperty(UserProps.RealName) || this.client.user.username, - availIndicator : availIndicator, - visIndicator : visIndicator, - lastLoginUserName : lastLoginStats.userName, - lastLoginRealName : lastLoginStats.realName, - lastLoginDate : moment(lastLoginStats.timestamp).format(this.getDateFormat()), - lastLoginTime : moment(lastLoginStats.timestamp).format(this.getTimeFormat()), - lastLogin : moment(lastLoginStats.timestamp).format(this._dateTimeFormat('lastLogin')), - totalMemoryBytes : sysMemStats.totalBytes || 0, - freeMemoryBytes : sysMemStats.freeBytes || 0, - systemAvgLoad : sysLoadStats.average || 0, - systemCurrentLoad : sysLoadStats.current || 0, - newPrivateMail : StatLog.getUserStatNumByClient(this.client, UserProps.NewPrivateMailCount, MailCountTTLSeconds), - newMessagesAddrTo : StatLog.getUserStatNumByClient(this.client, UserProps.NewAddressedToMessageCount, MailCountTTLSeconds), + currentUserName: this.client.user.username, + currentUserRealName: + this.client.user.getProperty(UserProps.RealName) || + this.client.user.username, + availIndicator: availIndicator, + visIndicator: visIndicator, + lastLoginUserName: lastLoginStats.userName, + lastLoginRealName: lastLoginStats.realName, + lastLoginDate: moment(lastLoginStats.timestamp).format(this.getDateFormat()), + lastLoginTime: moment(lastLoginStats.timestamp).format(this.getTimeFormat()), + lastLogin: moment(lastLoginStats.timestamp).format( + this._dateTimeFormat('lastLogin') + ), + totalMemoryBytes: sysMemStats.totalBytes || 0, + freeMemoryBytes: sysMemStats.freeBytes || 0, + systemAvgLoad: sysLoadStats.average || 0, + systemCurrentLoad: sysLoadStats.current || 0, + newPrivateMail: StatLog.getUserStatNumByClient( + this.client, + UserProps.NewPrivateMailCount, + MailCountTTLSeconds + ), + newMessagesAddrTo: StatLog.getUserStatNumByClient( + this.client, + UserProps.NewAddressedToMessageCount, + MailCountTTLSeconds + ), }; return cb(null); @@ -364,32 +391,37 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { .map(ac => { // Handle pre-authenticated if (!ac.authenticated) { - ac.text = ac.userName = '*Pre Auth*'; - ac.action = 'Logging In'; + ac.text = ac.userName = '*Pre Auth*'; + ac.action = 'Logging In'; } - const [availIndicator, visIndicator] = this._getStatusStrings(ac.isAvailable, ac.isVisible); + const [availIndicator, visIndicator] = this._getStatusStrings( + ac.isAvailable, + ac.isVisible + ); const timeOn = ac.timeOn || moment.duration(0); return Object.assign(ac, { availIndicator, visIndicator, - timeOnMinutes : timeOn.asMinutes(), - timeOn : _.upperFirst(timeOn.humanize()), // make friendly - affils : ac.affils || 'N/A', - realName : ac.realName || 'N/A', + timeOnMinutes: timeOn.asMinutes(), + timeOn: _.upperFirst(timeOn.humanize()), // make friendly + affils: ac.affils || 'N/A', + realName: ac.realName || 'N/A', }); - }); + }); // :TODO: Currently this always redraws due to setItems(). We really need painters alg.; The alternative now is to compare items... yuk. nodeStatusView.setItems(nodeStatusItems); - this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); // redraws + this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); // redraws return cb(null); } _refreshQuickLog(cb) { - const quickLogView = this.viewControllers.main.getView(MciViewIds.main.quickLogView); + const quickLogView = this.viewControllers.main.getView( + MciViewIds.main.quickLogView + ); if (!quickLogView) { return cb(null); } @@ -407,25 +439,23 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } const quickLogTimestampFormat = - this.config.quickLogTimestampFormat || - this.getDateTimeFormat('short'); + this.config.quickLogTimestampFormat || this.getDateTimeFormat('short'); - const levelIndicators = this.config.quickLogLevelIndicators || - { - trace : 'T', - debug : 'D', - info : 'I', - warn : 'W', - error : 'E', - fatal : 'F', - }; + const levelIndicators = this.config.quickLogLevelIndicators || { + trace: 'T', + debug: 'D', + info: 'I', + warn: 'W', + error: 'E', + fatal: 'F', + }; - - const makeLevelIndicator = (level) => { + const makeLevelIndicator = level => { return levelIndicators[level] || '?'; }; - const quickLogLevelMessagePrefixes = this.config.quickLogLevelMessagePrefixes || {}; + const quickLogLevelMessagePrefixes = + this.config.quickLogLevelMessagePrefixes || {}; const prefixMssage = (message, level) => { const prefix = quickLogLevelMessagePrefixes[level] || ''; return `${prefix}${message}`; @@ -434,12 +464,12 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { const logItems = records.map(rec => { const level = bunyan.nameFromLevel[rec.level]; return { - timestamp : moment(rec.time).format(quickLogTimestampFormat), - level : rec.level, - levelIndicator : makeLevelIndicator(level), - nodeId : rec.nodeId || '*', - sessionId : rec.sessionId || '', - message : prefixMssage(rec.msg, level), + timestamp: moment(rec.time).format(quickLogTimestampFormat), + level: rec.level, + levelIndicator: makeLevelIndicator(level), + nodeId: rec.nodeId || '*', + sessionId: rec.sessionId || '', + message: prefixMssage(rec.msg, level), }; }); @@ -454,4 +484,3 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return format || this.getDateFormat(); } }; - diff --git a/core/whos_online.js b/core/whos_online.js index 5fb1bbb3..fb59429e 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -2,11 +2,12 @@ 'use strict'; // ENiGMA½ -const { MenuModule } = require('./menu_module.js'); +const { MenuModule } = require('./menu_module.js'); const { getActiveConnectionList, - UserVisibleConnections } = require('./client_connections.js'); -const { Errors } = require('./enig_error.js'); + UserVisibleConnections, +} = require('./client_connections.js'); +const { Errors } = require('./enig_error.js'); // deps const async = require('async'); @@ -51,9 +52,14 @@ exports.getModule = class WhosOnlineModule extends MenuModule { ); } - const onlineList = getActiveConnectionList(UserVisibleConnections).slice(0, onlineListView.height).map( - oe => Object.assign(oe, { text : oe.userName, timeOn : _.upperFirst(oe.timeOn.humanize()) }) - ); + const onlineList = getActiveConnectionList(UserVisibleConnections) + .slice(0, onlineListView.height) + .map(oe => + Object.assign(oe, { + text: oe.userName, + timeOn: _.upperFirst(oe.timeOn.humanize()), + }) + ); onlineListView.setItems(onlineList); onlineListView.redraw(); diff --git a/package.json b/package.json index ef914f2c..1e795709 100644 --- a/package.json +++ b/package.json @@ -1,72 +1,72 @@ { - "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", - "systeminformation" : "^5.11.14" - }, - "devDependencies": { - "eslint": "^8.13.0", - "eslint-config-prettier": "^8.5.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", + "systeminformation": "^5.11.14" + }, + "devDependencies": { + "eslint": "^8.13.0", + "eslint-config-prettier": "^8.5.0", + "prettier": "2.6.2" + }, + "engines": { + "node": ">=14" + } } From 6c99a070d315fff26a3b64c1aa3b089a7fe88bc5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Jun 2022 15:22:24 -0600 Subject: [PATCH 37/52] Minor cleanup --- UPGRADE.md | 3 ++- WHATSNEW.md | 2 +- core/bbs.js | 3 --- core/bbs_link.js | 2 +- core/client_term.js | 20 ++++++++++---------- core/config_loader.js | 2 +- docs/_docs/modding/wfc.md | 6 +++--- 7 files changed, 18 insertions(+), 20 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 800e6fd2..f82367bb 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -34,7 +34,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or * All features and changes are backwards compatible. There are a few new configuration options in a new `term` section in the configuration. These are all optional, but include the following options in case you use them: ```hjson -{ +{ term: { // checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals. @@ -54,6 +54,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or ``` In addition to these, there are also new options for `term.cp437TermList` and `term.utf8TermList`. Under most circumstances these should not need to be changed. If you want to customize these lists, more information is available in `config_default.js` +* To enable the new Waiting for Caller (WFC) support, please see [WFC](docs/modding/wfc.md). # 0.0.11-beta to 0.0.12-beta * Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information. diff --git a/WHATSNEW.md b/WHATSNEW.md index d89f1b7b..d58d28f8 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -5,8 +5,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * **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. +* **New Waiting For Caller (WFC)** support via the `wfc.js` module. * 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). -* New Waiting For Caller (WFC) support via the `wfc.js` module. * Many new system statistics available via the StatLog such as current and average load, memory, etc. * Many new MCI codes: `MB`, `MF`, `LA`, `CL`, `UU`, `FT`, `DD`, `FB`, `DB`, `LC`, `LT`, `LD`, and more. See [MCI](./docs/art/mci.md). * SyncTERM style font support detection. diff --git a/core/bbs.js b/core/bbs.js index ddc6bfb5..211311cf 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -2,9 +2,6 @@ /* eslint-disable no-console */ 'use strict'; -//var SegfaultHandler = require('segfault-handler'); -//SegfaultHandler.registerHandler('enigma-bbs-segfault.log'); - // ENiGMA½ const conf = require('./config.js'); const logger = require('./logger.js'); diff --git a/core/bbs_link.js b/core/bbs_link.js index b5258123..17317435 100644 --- a/core/bbs_link.js +++ b/core/bbs_link.js @@ -129,7 +129,7 @@ exports.getModule = class BBSLinkModule extends MenuModule { '/auth.php?key=' + randomKey, headers, function resp(err, body) { - var status = body.trim(); + const status = body.trim(); if ('complete' === status) { return callback(null); diff --git a/core/client_term.js b/core/client_term.js index fa7f8ba2..4e78b96c 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -2,19 +2,19 @@ 'use strict'; // ENiGMA½ -var Log = require('./logger.js').log; -var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; +const Log = require('./logger.js').log; +const renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; const Config = require('./config.js').get; -var iconv = require('iconv-lite'); -var assert = require('assert'); -var _ = require('lodash'); +const iconv = require('iconv-lite'); +const assert = require('assert'); +const _ = require('lodash'); exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { this.output = output; - var outputEncoding = 'cp437'; + let outputEncoding = 'cp437'; assert(iconv.encodingExists(outputEncoding)); // convert line feeds such as \n -> \r\n @@ -26,10 +26,10 @@ 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'; + let termType = 'unknown'; + let termHeight = 0; + let termWidth = 0; + let termClient = 'unknown'; this.currentSyncFont = 'not_set'; diff --git a/core/config_loader.js b/core/config_loader.js index c84f0186..be14776f 100644 --- a/core/config_loader.js +++ b/core/config_loader.js @@ -69,7 +69,7 @@ module.exports = class ConfigLoader { defaultConfig, config, (defaultVal, configVal, key, target, source) => { - var path; + let path; while (true) { // eslint-disable-line no-constant-condition if (!stack.length) { diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index 1e6831df..d78a7b45 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -16,7 +16,7 @@ The system allows any user with the proper security to access the WFC / system o 3. User ID of 1 (root/admin) 4. The user belongs to the `wfc` group. -:information_source: Due to the above, the WFC screen is **disabled** by default as at a minimum, you'll need to add your user to the `wfc` group. +> :information_source: Due to the above, the WFC screen is **disabled** by default as at a minimum, you'll need to add your user to the `wfc` group. To change the ACS required, specify a alternative `acs` in the `config` block. For example: ```hjson @@ -28,7 +28,7 @@ mainMenuWaitingForCaller: { } ``` -:information_source: ENiGMA½ will enforce ACS of at least `SC` (secure connection) +> :notebook: ENiGMA½ will enforce ACS of at least `SC` (secure connection) ## Theming The following MCI codes are available: @@ -98,4 +98,4 @@ The following MCI codes are available: * `visIndicator`: Is the current user visible? Displayed via `statusVisibleIndicators` or system theme. See also [Themes](../art/themes.md). -:information_source: While [Standard MCI](../art/mci.md) codes work on any menu, they will **not** refresh. For values that may change over time, please use the custom format values above. \ No newline at end of file +> :information_source: While [Standard MCI](../art/mci.md) codes work on any menu, they will **not** refresh. For values that may change over time, please use the custom format values above. \ No newline at end of file From dde5079414f184104e7155eca8784176b7bc6aa0 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 12 Jun 2022 16:28:29 -0600 Subject: [PATCH 38/52] Tidy --- UPGRADE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/UPGRADE.md b/UPGRADE.md index f82367bb..93c7b1d4 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,6 +1,8 @@ # Introduction This document covers basic upgrade notes for major ENiGMA½ version updates. +> :information_source: Be sure to read the version-to-version upgrade notes below for each upgrade! + # Before Upgrading * Always back up your system! (See [Administration](./docs/admin/administration.md)) * Seriously, always back up your system! @@ -30,6 +32,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or # 0.0.12-beta to 0.0.13-beta +* To enable the new Waiting for Caller (WFC) support, please see [WFC](docs/modding/wfc.md). * :exclamation: The SSH server's `ssh2` module has gone through a major upgrade. Existing users will need to comment out two SSH KEX algorithms from their `config.hjson` if present else clients such as NetRunner will not be able to connect over SSH. Comment out `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` * All features and changes are backwards compatible. There are a few new configuration options in a new `term` section in the configuration. These are all optional, but include the following options in case you use them: @@ -54,7 +57,6 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or ``` In addition to these, there are also new options for `term.cp437TermList` and `term.utf8TermList`. Under most circumstances these should not need to be changed. If you want to customize these lists, more information is available in `config_default.js` -* To enable the new Waiting for Caller (WFC) support, please see [WFC](docs/modding/wfc.md). # 0.0.11-beta to 0.0.12-beta * Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information. From 1a93ab9be03184c68c9b65070cc0a82ec891dc1f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 13 Jun 2022 21:53:11 -0600 Subject: [PATCH 39/52] Add ability to kick selected node at WFC --- art/themes/luciano_blocktronics/wfc.ans | Bin 3050 -> 3055 bytes core/menu_module.js | 10 ++- core/menu_view.js | 4 + core/theme.js | 10 +++ core/vertical_menu_view.js | 3 + core/wfc.js | 98 +++++++++++++++++++++++- 6 files changed, 121 insertions(+), 4 deletions(-) diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index 79fe75b9da91d866d9921267b10f5b547d93c566..92a7f302f8b3477573c3de820bd808434a9d875d 100644 GIT binary patch delta 26 icmaDQ{$6~;HV#$=)rd%w&D%J5n3)ZY+9n_6Rs{fnfC!lY delta 23 fcmaDa{z`nqHjas60-H~B{9$4?Fln58idz)`c?}6B diff --git a/core/menu_module.js b/core/menu_module.js index f233b1f7..fd234934 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -575,8 +575,13 @@ exports.MenuModule = class MenuModule extends PluginModule { } } - //let artHeight; + const originalSubmitNotify = options.submitNotify; + options.submitNotify = () => { + if (_.isFunction(originalSubmitNotify)) { + originalSubmitNotify(); + } + if (prevVc) { prevVc.setFocus(true); } @@ -597,6 +602,9 @@ exports.MenuModule = class MenuModule extends PluginModule { options.viewController.setFocus(true); this.optionalMoveToPosition(position); + if (!options.position) { + options.position = position; + } theme.displayThemedPrompt(promptName, this.client, options, (err, artInfo) => { /* if(artInfo) { diff --git a/core/menu_view.js b/core/menu_view.js index 3b145bfa..ed2eb9fb 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -198,6 +198,10 @@ MenuView.prototype.getItems = function () { }; MenuView.prototype.getItem = function (index) { + if (index > this.items.length - 1) { + return null; + } + if (this.complexItems) { return this.items[index]; } diff --git a/core/theme.js b/core/theme.js index e97a11da..a08ccd39 100644 --- a/core/theme.js +++ b/core/theme.js @@ -651,6 +651,16 @@ function displayThemedPrompt(name, client, options, cb) { ? new ViewController({ client: client }) : options.viewController; + // adjust MCI positions relative to |position| + if (options.position) { + _.forEach(artInfo.mciMap, mci => { + if (mci.position) { + mci.position[0] = options.position.row; + mci.position[1] += options.position.col; + } + }); + } + const loadOpts = { promptName: name, mciMap: artInfo.mciMap, diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 59455a91..2252ec6d 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -231,6 +231,9 @@ VerticalMenuView.prototype.onKeyPress = function (ch, key) { VerticalMenuView.prototype.getData = function () { const item = this.getItem(this.focusedItemIndex); + if (!item) { + return this.focusedItemIndex; + } return _.isString(item.data) ? item.data : this.focusedItemIndex; }; diff --git a/core/wfc.js b/core/wfc.js index b36070f8..a82e1b9f 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -2,12 +2,18 @@ const { MenuModule } = require('./menu_module'); const stringFormat = require('./string_format'); -const { getActiveConnectionList, AllConnections } = require('./client_connections'); +const { + getActiveConnectionList, + AllConnections, + getConnectionByNodeId, + removeClient, +} = require('./client_connections'); const StatLog = require('./stat_log'); const SysProps = require('./system_property'); const UserProps = require('./user_property'); const Log = require('./logger'); const Config = require('./config.js').get; +const { Errors } = require('./enig_error'); // deps const async = require('async'); @@ -25,13 +31,15 @@ const FormIds = { main: 0, help: 1, fullLog: 2, + confirmKickPrompt: 3, }; const MciViewIds = { main: { nodeStatus: 1, quickLogView: 2, - nodeStatusSelection: 3, + selectedNodeStatusInfo: 3, + confirmXy: 4, customRangeStart: 10, }, @@ -89,6 +97,17 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { } return cb(null); }, + kickSelectedNode: (formData, extraArgs, cb) => { + return this._confirmKickSelectedNode(cb); + }, + kickNodeYes: (formData, extraArgs, cb) => { + //this._startRefreshing(); + return this._kickSelectedNode(cb); + }, + kickNodeNo: (formData, extraArgs, cb) => { + //this._startRefreshing(); + return cb(null); + }, }; } @@ -151,7 +170,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { ); const nodeStatusSelectionView = this.getView( 'main', - MciViewIds.main.nodeStatusSelection + MciViewIds.main.selectedNodeStatusInfo ); const nodeStatusSelectionFormat = this.config.nodeStatusSelectionFormat || '{text}'; @@ -212,6 +231,79 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { super.leave(); } + _getSelectedNodeItem() { + const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); + if (!nodeStatusView) { + return null; + } + + return nodeStatusView.getItem(nodeStatusView.getFocusItemIndex()); + } + + _confirmKickSelectedNode(cb) { + const nodeItem = this._getSelectedNodeItem(); + if (!nodeItem) { + return cb(null); + } + + const confirmView = this.getView('main', MciViewIds.main.confirmXy); + if (!confirmView) { + return cb( + Errors.MissingMci(`Missing prompt XY${MciViewIds.main.confirmXy} MCI`) + ); + } + + // disallow kicking self + if (this.client.node === parseInt(nodeItem.node)) { + return cb(null); + } + + const promptOptions = { + clearAtSubmit: true, + submitNotify: () => { + this._startRefreshing(); + }, + }; + + if (confirmView.dimens.width) { + promptOptions.clearWidth = confirmView.dimens.width; + } + + this._stopRefreshing(); + return this.promptForInput( + { + formName: 'confirmKickPrompt', + formId: FormIds.confirmKickPrompt, + promptName: this.config.confirmKickNodePrompt || 'confirmKickNodePrompt', + prevFormName: 'main', + position: confirmView.position, + }, + promptOptions, + err => { + return cb(err); + } + ); + } + + _kickSelectedNode(cb) { + const nodeItem = this._getSelectedNodeItem(); + if (!nodeItem) { + return cb(Errors.UnexpectedState('Expecting a selected node')); + } + + const client = getConnectionByNodeId(parseInt(nodeItem.node)); + if (!client) { + return cb( + Errors.UnexpectedState(`Expecting a client for node ID ${nodeItem.node}`) + ); + } + + // :TODO: optional kick art + + removeClient(client); + return cb(null); + } + _applyOpVisibility() { this.restoreUserIsVisible = this.client.user.isVisible(); From 3d50c4e80d6658350a64d8eeaf231a1d1228841b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 23 Jun 2022 15:06:00 -0600 Subject: [PATCH 40/52] Fix friendly remote address member --- core/client_connections.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/client_connections.js b/core/client_connections.js index fea32a3e..ce39fe1c 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -131,7 +131,7 @@ function addNewClient(client, clientSock) { const connInfo = { remoteAddress: remoteAddress, - freiendlyRemoteAddress: client.friendlyRemoteAddress(), + friendlyRemoteAddress: client.friendlyRemoteAddress(), serverName: client.session.serverName, isSecure: client.session.isSecure, }; From 2040ccd5517789996ce62a6f021df1bbe0f6d08d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 23 Jun 2022 22:23:11 -0600 Subject: [PATCH 41/52] Basic selection display --- art/themes/luciano_blocktronics/theme.hjson | 2 + art/themes/luciano_blocktronics/wfc.ans | Bin 3055 -> 3055 bytes core/wfc.js | 59 +++++++++++++------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index a3fc4f85..cf3b744f 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -296,6 +296,8 @@ } statusAvailableIndicators: [ "N", "Y" ] statusVisibleIndicators: [ "N", "Y" ] + + nodeStatusSelectionFormat: "|00|10{realName}" } 0: { mci: { diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index 92a7f302f8b3477573c3de820bd808434a9d875d..9c1e6ede43aa7c319f0b9df2d1ef10cdd35f8654 100644 GIT binary patch delta 28 kcmaDa{$6}TAq$IYh>!8)k1S%FyIE}5nT(7lpX62p0F?R(MF0Q* delta 26 icmaDa{$6}TA -1) { this.selectedNodeStatusIndex = index; this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); + + const nodeStatusSelectionView = this.getView( + 'main', + MciViewIds.main.selectedNodeStatusInfo + ); + + if (nodeStatusSelectionView) { + const item = nodeStatusView.getItems()[index]; + this._updateNodeStatusSelection(nodeStatusSelectionView, item); + } } + return cb(null); }, kickSelectedNode: (formData, extraArgs, cb) => { @@ -172,18 +183,14 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { 'main', MciViewIds.main.selectedNodeStatusInfo ); - const nodeStatusSelectionFormat = - this.config.nodeStatusSelectionFormat || '{text}'; + if (nodeStatusView && nodeStatusSelectionView) { nodeStatusView.on('index update', index => { const item = nodeStatusView.getItems()[index]; - if (item) { - nodeStatusSelectionView.setText( - stringFormat(nodeStatusSelectionFormat, item) - ); - // :TODO: Update view - // :TODO: this is not triggered by key-presses (1, 2, ...) -- we need to handle that as well - } + this._updateNodeStatusSelection( + nodeStatusSelectionView, + item + ); }); } @@ -202,16 +209,6 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { ); } - _displayHelpPage(cb) { - this._stopRefreshing(); - - this.displayAsset(this.menuConfig.config.art.help, { clearScreen: true }, () => { - this.client.waitForKeyPress(() => { - return this._displayMainPage(true, cb); - }); - }); - } - enter() { this.client.stopIdleMonitor(); this._applyOpVisibility(); @@ -231,6 +228,26 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { super.leave(); } + _updateNodeStatusSelection(nodeStatusSelectionView, item) { + if (item) { + const nodeStatusSelectionFormat = + this.config.nodeStatusSelectionFormat || '{text}'; + nodeStatusSelectionView.setText( + stringFormat(nodeStatusSelectionFormat, item) + ); + } + } + + _displayHelpPage(cb) { + this._stopRefreshing(); + + this.displayAsset(this.menuConfig.config.art.help, { clearScreen: true }, () => { + this.client.waitForKeyPress(() => { + return this._displayMainPage(true, cb); + }); + }); + } + _getSelectedNodeItem() { const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus); if (!nodeStatusView) { @@ -460,7 +477,7 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return cb(null); } - _getNodeByNodeId(nodeStatusView, nodeId) { + _getNodeStatusIndexByNodeId(nodeStatusView, nodeId) { return nodeStatusView.getItems().findIndex(entry => entry.node == nodeId); } From e6cceeee3a1acedb14c0334c4e31d9af1e0a2f0d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Jul 2022 12:35:24 -0600 Subject: [PATCH 42/52] Fix drawing issue --- core/vertical_menu_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index 2252ec6d..aa196f80 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -100,7 +100,7 @@ function VerticalMenuView(options) { } text = `${sgr}${strUtil.pad( - text, + `${text}${this.styleSGR1}`, this.dimens.width, this.fillChar, this.justify From 547d21683e424d9d464224ef7768203eccba1538 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Jul 2022 12:35:39 -0600 Subject: [PATCH 43/52] Some minor doc updates --- docs/_docs/admin/updating.md | 4 ++-- docs/_docs/art/general.md | 4 ++-- docs/_docs/art/mci.md | 10 +++++----- docs/_docs/art/views/button_view.md | 10 +++++----- docs/_docs/art/views/edit_text_view.md | 4 ++-- docs/_docs/art/views/full_menu_view.md | 10 +++++----- docs/_docs/art/views/horizontal_menu_view.md | 6 +++--- docs/_docs/art/views/mask_edit_text_view.md | 6 +++--- docs/_docs/art/views/multi_line_edit_text_view.md | 8 ++++---- docs/_docs/art/views/predefined_label_view.md | 12 ++++++------ docs/_docs/art/views/spinner_menu_view.md | 4 ++-- docs/_docs/art/views/text_view.md | 10 +++++----- docs/_docs/art/views/toggle_menu_view.md | 4 ++-- docs/_docs/art/views/vertical_menu_view.md | 4 ++-- docs/_docs/configuration/archivers.md | 2 +- docs/_docs/configuration/config-files.md | 10 +++++----- docs/_docs/configuration/config-hjson.md | 2 +- docs/_docs/configuration/event-scheduler.md | 2 +- docs/_docs/configuration/external-binaries.md | 4 ++-- docs/_docs/configuration/hjson.md | 2 +- docs/_docs/configuration/menu-hjson.md | 6 +++--- docs/_docs/configuration/security.md | 6 +++--- docs/_docs/filebase/uploads.md | 4 ++-- docs/_docs/installation/docker.md | 6 +++--- docs/_docs/installation/install-script.md | 2 +- docs/_docs/installation/manual.md | 6 +++--- docs/_docs/messageareas/bso-import-export.md | 2 +- .../_docs/messageareas/configuring-a-message-area.md | 2 +- docs/_docs/messageareas/ftn.md | 6 +++--- docs/_docs/messageareas/message-networks.md | 2 +- docs/_docs/messageareas/qwk.md | 2 +- docs/_docs/modding/local-doors.md | 2 +- docs/_docs/modding/user-2fa-otp-config.md | 2 +- docs/_docs/modding/wfc.md | 4 ++-- docs/_docs/modding/whos-online.md | 2 +- docs/_docs/servers/contentservers/gopher.md | 6 +++--- docs/_docs/servers/contentservers/web-server.md | 2 +- 37 files changed, 90 insertions(+), 90 deletions(-) diff --git a/docs/_docs/admin/updating.md b/docs/_docs/admin/updating.md index ae9412e4..87cf437e 100644 --- a/docs/_docs/admin/updating.md +++ b/docs/_docs/admin/updating.md @@ -23,9 +23,9 @@ npm install # or 'yarn' 5. If there are updates to the `art/themes/luciano_blocktronics/theme.hjson` file and you have a custom theme, you may want to look at them as well. 6. Finally, restart your running ENiGMA½ instance. -:information_source: Visual diff tools such as [DiffMerge](https://www.sourcegear.com/diffmerge/downloads.php) (free, works on all major platforms) can be very helpful for the tasks outlined above! +> :information_source: Visual diff tools such as [DiffMerge](https://www.sourcegear.com/diffmerge/downloads.php) (free, works on all major platforms) can be very helpful for the tasks outlined above! -:bulb: It is recommended to [monitor logs](../troubleshooting/monitoring-logs.md) and poke around a bit after an update! +> :bulb: It is recommended to [monitor logs](../troubleshooting/monitoring-logs.md) and poke around a bit after an update! diff --git a/docs/_docs/art/general.md b/docs/_docs/art/general.md index 551d231e..3ddbf590 100644 --- a/docs/_docs/art/general.md +++ b/docs/_docs/art/general.md @@ -149,12 +149,12 @@ Other "fonts" also available: * `iso8859_1` * `cp1131` -:information_source: See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. +> :information_source: See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. #### SyncTERM Style Baud Rates The `baudRate` member can set a [SyncTERM](http://syncterm.bbsdev.net/) style emulated baud rate. May be `300`, `600`, `1200`, `2400`, `4800`, `9600`, `19200`, `38400`, `57600`, `76800`, or `115200`. A value of `ulimited`, `off`, or `0` resets (disables) the rate. -:information_source: See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. +> :information_source: See [this specification](https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt) for more information. ### Common Example ```hjson diff --git a/docs/_docs/art/mci.md b/docs/_docs/art/mci.md index 8c2bb7ef..de921710 100644 --- a/docs/_docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -8,9 +8,9 @@ ENiGMA½ supports a variety of MCI codes. Some **predefined** codes produce info ## General Information MCI codes are composed of two characters and are prefixed with a percent (%) symbol. -:information_source: To explicitly tie a MCI to a specific View ID, suffix the MCI code with a number. For example: `%BN1`. +> :information_source: To explicitly tie a MCI to a specific View ID, suffix the MCI code with a number. For example: `%BN1`. -:information_source: Standard (non-focus) and focus colors are set by placing duplicate codes back to back in art files: +> :information_source: Standard (non-focus) and focus colors are set by placing duplicate codes back to back in art files: ![Example](../assets/images/mci-example1.png "MCI Colors") @@ -121,7 +121,7 @@ Some additional special case codes also exist: | `XY` | A special code that may be utilized for placement identification when creating menus or to extend an otherwise empty space in an art file down the screen. | -:information_source: More are added all +> :information_source: More are added all the time so also check out [core/predefined_mci.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) for a full listing. @@ -147,7 +147,7 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `PL` | Predefined Label | Show environment information | See [Predefined Label](views/predefined_label_view.md)| | `KE` | Key Entry | A *single* key input control | Think hotkeys | -:information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information. +> :information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information. ### Mask Edits Mask Edits (`%ME`) use the special `maskPattern` property to control a _mask_. This can be useful for gathering dates, phone numbers, so on. @@ -253,4 +253,4 @@ Suppose a format object contains the following elements: `userName` and `affils` ![Example](../assets/images/text-format-example1.png "Text Format") -:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". +> :bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". diff --git a/docs/_docs/art/views/button_view.md b/docs/_docs/art/views/button_view.md index 65a753ca..0770ea45 100644 --- a/docs/_docs/art/views/button_view.md +++ b/docs/_docs/art/views/button_view.md @@ -7,9 +7,9 @@ A button view supports displaying a button on a screen. ## General Information -:information_source: A button view is defined with a percent (%) and the characters BT, followed by the view number. For example: `%BT1` +> :information_source: A button view is defined with a percent (%) and the characters BT, followed by the view number. For example: `%BT1` -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties @@ -30,11 +30,11 @@ A button view supports displaying a button on a screen. The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. -:information_source: If `textOverflow` is not specified at all, a button can become wider than the `width` if needed to display the text value. +> :information_source: If `textOverflow` is not specified at all, a button can become wider than the `width` if needed to display the text value. -:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed +> :information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed -:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` +> :information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` ## Example diff --git a/docs/_docs/art/views/edit_text_view.md b/docs/_docs/art/views/edit_text_view.md index c372246d..45e247de 100644 --- a/docs/_docs/art/views/edit_text_view.md +++ b/docs/_docs/art/views/edit_text_view.md @@ -7,9 +7,9 @@ An edit text view supports editing form values on a screen. This can be for new ## General Information -:information_source: An edit text view is defined with a percent (%) and the characters ET, followed by the view number. For example: `%ET1`. This is generally used on a form in order to allow a user to enter or edit a text value. +> :information_source: An edit text view is defined with a percent (%) and the characters ET, followed by the view number. For example: `%ET1`. This is generally used on a form in order to allow a user to enter or edit a text value. -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties diff --git a/docs/_docs/art/views/full_menu_view.md b/docs/_docs/art/views/full_menu_view.md index 19ff365a..5e18e1e4 100644 --- a/docs/_docs/art/views/full_menu_view.md +++ b/docs/_docs/art/views/full_menu_view.md @@ -9,9 +9,9 @@ A full menu view supports displaying a list of times on a screen in a very confi Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. -:information_source: A full menu view is defined with a percent (%) and the characters FM, followed by the view number. For example: `%FM1` +> :information_source: A full menu view is defined with a percent (%) and the characters FM, followed by the view number. For example: `%FM1` -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties @@ -76,11 +76,11 @@ If the list is for display only (there is no form action associated with it) you The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. Note, because columns are automatically calculated, this can only occur when the text is too long to fit the `width` using a single column. -:information_source: If `textOverflow` is not specified at all, a menu can become wider than the `width` if needed to display a single column. +> :information_source: If `textOverflow` is not specified at all, a menu can become wider than the `width` if needed to display a single column. -:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed +> :information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed -:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` +> :information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` ## Examples diff --git a/docs/_docs/art/views/horizontal_menu_view.md b/docs/_docs/art/views/horizontal_menu_view.md index 90dc4438..d7527632 100644 --- a/docs/_docs/art/views/horizontal_menu_view.md +++ b/docs/_docs/art/views/horizontal_menu_view.md @@ -3,15 +3,15 @@ layout: page title: Horizontal Menu View --- ## Horizontal Menu View -A horizontal menu view supports displaying a list of times on a screen horizontally (side to side, in a single row) similar to a lightbox. +A horizontal menu view supports displaying a list of times on a screen horizontally (side to side, in a single row) similar to a lightbox. ## General Information Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. -:information_source: A horizontal menu view is defined with a percent (%) and the characters HM, followed by the view number (if used.) For example: `%HM1` +> :information_source: A horizontal menu view is defined with a percent (%) and the characters HM, followed by the view number (if used.) For example: `%HM1` -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties diff --git a/docs/_docs/art/views/mask_edit_text_view.md b/docs/_docs/art/views/mask_edit_text_view.md index a03e83c5..8654d6f5 100644 --- a/docs/_docs/art/views/mask_edit_text_view.md +++ b/docs/_docs/art/views/mask_edit_text_view.md @@ -7,9 +7,9 @@ A mask edit text view supports editing form values on a screen. This can be for ## General Information -:information_source: A mask edit text view is defined with a percent (%) and the characters ME, followed by the view number. For example: `%ME1`. This is generally used on a form in order to allow a user to enter or edit a text value. +> :information_source: A mask edit text view is defined with a percent (%) and the characters ME, followed by the view number. For example: `%ME1`. This is generally used on a form in order to allow a user to enter or edit a text value. -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties @@ -45,7 +45,7 @@ Any value other than the entries above is treated like a literal value to be dis | `##-AAA-####` | Matches a date of type day-month-year (i.e. 01-MAR-2010) | | `# foot ## inches`| Matches a height in feet and inches (i.e. 6 foot 2 inches) | - + ## Example ![Example](../../assets/images/mask_edit_text_view_example1.gif "Masked Text Edit View") diff --git a/docs/_docs/art/views/multi_line_edit_text_view.md b/docs/_docs/art/views/multi_line_edit_text_view.md index 870360ba..5882cba7 100644 --- a/docs/_docs/art/views/multi_line_edit_text_view.md +++ b/docs/_docs/art/views/multi_line_edit_text_view.md @@ -7,9 +7,9 @@ A text display / editor designed to edit or display a message. ## General Information -:information_source: A multi line edit text view is defined with a percent (%) and the characters MT, followed by the view number. For example: `%MT1` +> :information_source: A multi line edit text view is defined with a percent (%) and the characters MT, followed by the view number. For example: `%MT1` -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties @@ -31,9 +31,9 @@ The mode of a multi line edit text view controls how the view behaves. The follo | preview | preview the text, including scrolling | | read-only | No scrolling or editing the view | -:information_source: If `mode` is not set, the default mode is "edit" +> :information_source: If `mode` is not set, the default mode is "edit" -:information_source: With mode preview, scrolling the contents is allowed, but is not with read-only. +> :information_source: With mode preview, scrolling the contents is allowed, but is not with read-only. ## Example diff --git a/docs/_docs/art/views/predefined_label_view.md b/docs/_docs/art/views/predefined_label_view.md index cae23f55..b9349a23 100644 --- a/docs/_docs/art/views/predefined_label_view.md +++ b/docs/_docs/art/views/predefined_label_view.md @@ -7,11 +7,11 @@ A predefined label view supports displaying a predefined MCI label on a screen. ## General Information -:information_source: A predefined label view is defined with a percent (%) and the characters PL, followed by the view number and then the predefined MCI value in parenthesis. For example: `%PL1(VL)` to display the Version Label. *NOTE*: this is an alternate way of placing MCI codes, as the MCI can also be placed on the art page directly with the code. For example `%VL`. The difference between these is that the PL version can have additional formatting options applied to it. +> :information_source: A predefined label view is defined with a percent (%) and the characters PL, followed by the view number and then the predefined MCI value in parenthesis. For example: `%PL1(VL)` to display the Version Label. *NOTE*: this is an alternate way of placing MCI codes, as the MCI can also be placed on the art page directly with the code. For example `%VL`. The difference between these is that the PL version can have additional formatting options applied to it. -:information_source: See *Predefined Codes* in [MCI](../mci.md) for the list of available MCI codes. +> :information_source: See *Predefined Codes* in [MCI](../mci.md) for the list of available MCI codes. -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties @@ -27,11 +27,11 @@ A predefined label view supports displaying a predefined MCI label on a screen. The `textOverflow` option is used to specify what happens when a predefined MCI string is too long to fit in the `width` defined. -:information_source: If `textOverflow` is not specified at all, a predefined label view can become wider than the `width` if needed to display the MCI value. +> :information_source: If `textOverflow` is not specified at all, a predefined label view can become wider than the `width` if needed to display the MCI value. -:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed +> :information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed -:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` +> :information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` ## Example diff --git a/docs/_docs/art/views/spinner_menu_view.md b/docs/_docs/art/views/spinner_menu_view.md index 0f7139f8..e2f76624 100644 --- a/docs/_docs/art/views/spinner_menu_view.md +++ b/docs/_docs/art/views/spinner_menu_view.md @@ -9,9 +9,9 @@ A spinner menu view supports displaying a set of times on a screen as a list, wi Items can be selected on a menu via the cursor keys or by selecting them via a `hotKey` - see ***Hot Keys*** below. -:information_source: A spinner menu view is defined with a percent (%) and the characters SM, followed by the view number (if used.) For example: `%SM1` +> :information_source: A spinner menu view is defined with a percent (%) and the characters SM, followed by the view number (if used.) For example: `%SM1` -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties diff --git a/docs/_docs/art/views/text_view.md b/docs/_docs/art/views/text_view.md index 3bec8ed8..8eaf9478 100644 --- a/docs/_docs/art/views/text_view.md +++ b/docs/_docs/art/views/text_view.md @@ -7,9 +7,9 @@ A text label view supports displaying simple text on a screen. ## General Information -:information_source: A text label view is defined with a percent (%) and the characters TL, followed by the view number. For example: `%TL1` +> :information_source: A text label view is defined with a percent (%) and the characters TL, followed by the view number. For example: `%TL1` -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties @@ -26,11 +26,11 @@ A text label view supports displaying simple text on a screen. The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. -:information_source: If `textOverflow` is not specified at all, a text label can become wider than the `width` if needed to display the text value. +> :information_source: If `textOverflow` is not specified at all, a text label can become wider than the `width` if needed to display the text value. -:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed +> :information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed -:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` +> :information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` ## Example diff --git a/docs/_docs/art/views/toggle_menu_view.md b/docs/_docs/art/views/toggle_menu_view.md index 65c1eabd..819f431b 100644 --- a/docs/_docs/art/views/toggle_menu_view.md +++ b/docs/_docs/art/views/toggle_menu_view.md @@ -9,9 +9,9 @@ A toggle menu view supports displaying a list of options on a screen horizontall Items can be selected on a menu via the left and right cursor keys, or by selecting them via a `hotKey` - see ***Hot Keys*** below. -:information_source: A toggle menu view is defined with a percent (%) and the characters TM, followed by the view number (if used.) For example: `%TM1` +> :information_source: A toggle menu view is defined with a percent (%) and the characters TM, followed by the view number (if used.) For example: `%TM1` -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties diff --git a/docs/_docs/art/views/vertical_menu_view.md b/docs/_docs/art/views/vertical_menu_view.md index e46f92ae..ed439dbc 100644 --- a/docs/_docs/art/views/vertical_menu_view.md +++ b/docs/_docs/art/views/vertical_menu_view.md @@ -9,9 +9,9 @@ A vertical menu view supports displaying a list of times on a screen vertically Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. -:information_source: A vertical menu view is defined with a percent (%) and the characters VM, followed by the view number (if used.) For example: `%VM1`. +> :information_source: A vertical menu view is defined with a percent (%) and the characters VM, followed by the view number (if used.) For example: `%VM1`. -:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. +> :information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them. ### Properties diff --git a/docs/_docs/configuration/archivers.md b/docs/_docs/configuration/archivers.md index 0f55a58b..904d9925 100644 --- a/docs/_docs/configuration/archivers.md +++ b/docs/_docs/configuration/archivers.md @@ -8,7 +8,7 @@ ENiGMA½ can detect and process various archive formats such as zip and arj for Archivers are manged via the `archives:archivers` configuration block of `config.hjson`. Each entry in this section defines an **external archiver** that can be referenced in other sections of `config.hjson` as and in code. Entries define how to `compress`, `decompress` (a full archive), `list`, and `extract` (specific files from an archive). -:bulb: Generally you do not need to anything beyond installing supporting binaries. No `config.hjson` editing necessary; Please see [External Binaries](external-binaries.md)! +> :bulb: Generally you do not need to anything beyond installing supporting binaries. No `config.hjson` editing necessary; Please see [External Binaries](external-binaries.md)! ### Archiver Configuration Archiver entries in `config.hjson` are mostly self explanatory with the exception of `list` commands that require some additional information. The `args` member for an entry is an array of arguments to pass to `cmd`. Some variables are available to `args` that will be expanded by the system: diff --git a/docs/_docs/configuration/config-files.md b/docs/_docs/configuration/config-files.md index dbba9816..3f1e49a6 100644 --- a/docs/_docs/configuration/config-files.md +++ b/docs/_docs/configuration/config-files.md @@ -8,7 +8,7 @@ ENiGMA½ configuration files such as the [system config](config-hjson.md), [menu ## Hot-Reload Nearly all of ENiGMA½'s configuration can be hot-reloaded. That is, a live system can have it's configuration modified and it will be loaded in place. -:bulb: [Monitoring live logs](../troubleshooting/monitoring-logs.md) is useful when making live changes. The system will complain if something is wrong! +> :bulb: [Monitoring live logs](../troubleshooting/monitoring-logs.md) is useful when making live changes. The system will complain if something is wrong! ## Common Directives ### Includes @@ -71,7 +71,7 @@ Consider `actionKeys` in a menu. Often times you may show a screen and the user } ``` -:information_source: An unresolved `@reference` will be left intact. +> :information_source: An unresolved `@reference` will be left intact. ### Environment Variables Especially in a container environment such as [Docker](../installation/docker.md), environment variable access in configuration files can become very handy. ENiGMA½ provides a flexible way to access variables using the `@environment` directive. The most basic form of `@environment:VAR_NAME` produces a string value. Additionally a `:type` suffix can be supplied to coerece the value to a particular type. Variables pointing to a comma separated list can be turned to arrays using an additional `:array` suffix. @@ -97,11 +97,11 @@ Below is a table of the various forms: | `@environment:SOME_VAR:timestamp` | "2020-01-05" | A [moment](https://momentjs.com/) object representing 2020-01-05 | | `@environment:SOME_VAR:timestamp:array` | "2020-01-05,2016-05-16T01:15:37'" | An array of [moment](https://momentjs.com/) objects representing 2020-01-05 and 2016-05-16T01:15:37 | -:bulb: `bool` may be used as an alias to `boolean`. +> :bulb: `bool` may be used as an alias to `boolean`. -:bulb: `timestamp` values can be in any form that [moment can parse](https://momentjs.com/docs/#/parsing/). +> :bulb: `timestamp` values can be in any form that [moment can parse](https://momentjs.com/docs/#/parsing/). -:information_source: An unresolved or invalid `@environment` will be left intact. +> :information_source: An unresolved or invalid `@environment` will be left intact. Consider the following fragment: ```hjson diff --git a/docs/_docs/configuration/config-hjson.md b/docs/_docs/configuration/config-hjson.md index 28d379c5..f92feb07 100644 --- a/docs/_docs/configuration/config-hjson.md +++ b/docs/_docs/configuration/config-hjson.md @@ -7,7 +7,7 @@ The main system configuration file, `config.hjson` both overrides defaults and p The default path is `/enigma-bbs/config/config.hjson` though this can be overridden using the `--config` parameter when invoking `main.js`. -:information_source: See also [Configuration Files](config-files.md). Additionally [HJSON General Information](hjson.md) may be helpful for more information on the HJSON format. +> :information_source: See also [Configuration Files](config-files.md). Additionally [HJSON General Information](hjson.md) may be helpful for more information on the HJSON format. ### Creating a Configuration Your initial configuration skeleton should be created using the `oputil.js` command line utility. From your enigma-bbs root directory: diff --git a/docs/_docs/configuration/event-scheduler.md b/docs/_docs/configuration/event-scheduler.md index 544e082a..accfa7aa 100644 --- a/docs/_docs/configuration/event-scheduler.md +++ b/docs/_docs/configuration/event-scheduler.md @@ -26,7 +26,7 @@ As mentioned above, `schedule` may contain a [Later style](https://bunkat.github An `@watch` clause monitors a specified file for changes and takes the following form: `@watch:` where `` is a fully qualified path. -:bulb: If you would like to have a schedule **and** watch a file for changes, place the `@watch` clause second and separated with the word `or`. For example: `every 24 hours or @watch:/path/to/somefile.txt`. +> :bulb: If you would like to have a schedule **and** watch a file for changes, place the `@watch` clause second and separated with the word `or`. For example: `every 24 hours or @watch:/path/to/somefile.txt`. ### Actions Events can kick off actions by calling a method (function) provided by the system or custom module in addition to executing arbritary binaries or scripts. diff --git a/docs/_docs/configuration/external-binaries.md b/docs/_docs/configuration/external-binaries.md index 1932a963..656aac3c 100644 --- a/docs/_docs/configuration/external-binaries.md +++ b/docs/_docs/configuration/external-binaries.md @@ -22,9 +22,9 @@ Below is a table of pre-configured archivers. Remember that you can override set | `TarGz` | .tar.gz, .gzip | [Wikipedia](https://en.wikipedia.org/wiki/Gzip) | `tar` | `tar` | [TAR.EXE](https://ss64.com/nt/tar.html) -:information_source: For more information see `core/config_default.js` +> :information_source: For more information see `core/config_default.js` -:information_source: For information on changing configuration or adding more archivers see [Archivers](archivers.md). +> :information_source: For information on changing configuration or adding more archivers see [Archivers](archivers.md). ## File Transfer Protocols Handlers for legacy file transfer protocols such as Z-Modem and Y-Modem. diff --git a/docs/_docs/configuration/hjson.md b/docs/_docs/configuration/hjson.md index 98aef757..a3c52d4e 100644 --- a/docs/_docs/configuration/hjson.md +++ b/docs/_docs/configuration/hjson.md @@ -40,7 +40,7 @@ See https://hjson.org/users.html for more more editors & plugins. ### Hot-Reload A.K.A. Live Editing ENiGMA½'s configuration, menu, and theme files can edited while your BBS is running. When a file is saved, it is hot-reloaded into the running system. If users are currently connected and you change a menu for example, the next reload of that menu will show the changes. -:information_source: See also [Configuration Files](../configuration/config-files.md) +> :information_source: See also [Configuration Files](../configuration/config-files.md) ### CaSe SeNsiTiVE Configuration keys are **case sensitive**. That means if a configuration key is `boardName` for example, `boardname`, or `BOARDNAME` **will not work**. diff --git a/docs/_docs/configuration/menu-hjson.md b/docs/_docs/configuration/menu-hjson.md index ab935222..e5e75daa 100644 --- a/docs/_docs/configuration/menu-hjson.md +++ b/docs/_docs/configuration/menu-hjson.md @@ -5,9 +5,9 @@ title: Menu HSJON ## Menu HJSON The core of a ENiGMA½ based BBS is it's menus driven by what will be referred to as `menu.hjson`. Throughout ENiGMA½ documentation, when `menu.hjson` is referenced, we're actually talking about `config/menus/yourboardname-*.hjson`. These files determine the menus (or screens) a user can see, the order they come in, how they interact with each other, ACS configuration, and so on. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. -:information_source: See also [HJSON General Information](hjson.md) for more information on the HJSON file format. +> :information_source: See also [HJSON General Information](hjson.md) for more information on the HJSON file format. -:bulb: Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: +> :bulb: Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: * Classical navigation and menus such as Main, Messages, and Files. * Art file display. @@ -24,7 +24,7 @@ showSomeArt: { ``` As you can see a menu can be very simple. -:information_source: Remember that the top level menu may include additional files using the `includes` directive. See [Configuration Files](config-files.md) for more information on this. +> :information_source: Remember that the top level menu may include additional files using the `includes` directive. See [Configuration Files](config-files.md) for more information on this. ## Common Menu Entry Members Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. Menus that use their own module contain a `module` declaration: diff --git a/docs/_docs/configuration/security.md b/docs/_docs/configuration/security.md index 49870f17..f9a90751 100644 --- a/docs/_docs/configuration/security.md +++ b/docs/_docs/configuration/security.md @@ -15,7 +15,7 @@ Enabling Two-Factor Authentication via One-Time-Password (2FA/OTP) on an account * One or more secure servers enabled such as [SSH](../servers/ssh.md) or secure [WebSockets](../servers/websocket.md) (that is, WebSockets over a secure connection such as TLS). * The [web server](../servers/web-server.md) enabled and exposed over TLS (HTTPS). -:information_source: For WebSockets and the web server, ENiGMA½ _may_ listen on insecure channels if behind a secure web proxy. +> :information_source: For WebSockets and the web server, ENiGMA½ _may_ listen on insecure channels if behind a secure web proxy. ### User Registration Flow Due to the nature of 2FA/OTP, even if enabled on your system, users must opt-in and enable this feature on their account. Users must also have a valid email address such that a registration link can be sent to them. To opt-in, users must enable the option, which will cause the system to email them a registration link. Following the link provides the following: @@ -24,9 +24,9 @@ Due to the nature of 2FA/OTP, even if enabled on your system, users must opt-in 2. If applicable, a scannable QR code for easy device entry (e.g. Google Authenticator) 3. A confirmation prompt in which the user must enter a OTP code. If entered correctly, this validates everything is set up properly and 2FA/OTP will be enabled for the account. Backup codes will also be provided at this time. Future logins will now prompt the user for their OTP after they enter their standard password. -:warning: Serving 2FA/OTP registration links over insecure (HTTP) can expose secrets intended for the user and is **highly** discouraged! +> :warning: Serving 2FA/OTP registration links over insecure (HTTP) can expose secrets intended for the user and is **highly** discouraged! -:memo: +ops can also manually enable or disable 2FA/OTP for a user using [oputil](../admin/oputil.md), but this is generally discouraged. +> :memo: +ops can also manually enable or disable 2FA/OTP for a user using [oputil](../admin/oputil.md), but this is generally discouraged. #### Recovery In the situation that a user loses their 2FA/OTP device (such as a lost phone with Google Auth), there are some options: diff --git a/docs/_docs/filebase/uploads.md b/docs/_docs/filebase/uploads.md index 3d994440..786226e6 100644 --- a/docs/_docs/filebase/uploads.md +++ b/docs/_docs/filebase/uploads.md @@ -19,6 +19,6 @@ uploads: { } ```` -:information_source: Remember that uploads in a particular area are stored **using the first storage tag defined in that area.** +> :information_source: Remember that uploads in a particular area are stored **using the first storage tag defined in that area.** -:bulb: Any ACS checks are allowed. See [ACS](../configuration/acs.md) +> :bulb: Any ACS checks are allowed. See [ACS](../configuration/acs.md) diff --git a/docs/_docs/installation/docker.md b/docs/_docs/installation/docker.md index 1fa1de49..3d7f0c6d 100644 --- a/docs/_docs/installation/docker.md +++ b/docs/_docs/installation/docker.md @@ -40,13 +40,13 @@ if you make any changes to your host config folder they will persist, and you ca ```docker restart ENiGMABBS``` -:bulb: Configuration will be stored in `$(pwd)/enigma-bbs/config`. +> :bulb: Configuration will be stored in `$(pwd)/enigma-bbs/config`. -:bulb: Windows users - you'll need to switch out `$(pwd)/enigma-bbs/config` for a Windows-style path. +> :bulb: Windows users - you'll need to switch out `$(pwd)/enigma-bbs/config` for a Windows-style path. ## Volumes -Containers by their nature are ephermeral. Meaning, stuff you want to keep (config, database, mail) needs +Containers by their nature are ephermeral. Meaning, stuff you want to keep (config, database, mail) needs to be stored outside of the running container. As such, the following volumes are mountable: | Volume | Usage | diff --git a/docs/_docs/installation/install-script.md b/docs/_docs/installation/install-script.md index 809d38db..3923e554 100644 --- a/docs/_docs/installation/install-script.md +++ b/docs/_docs/installation/install-script.md @@ -14,7 +14,7 @@ on GitHub before running it! The script will install `nvm`, Node.js and grab the latest ENiGMA BBS from GitHub. It will also guide you through creating a basic configuration file, and recommend some packages to install. -:information_source: After installing: +> :information_source: After installing: * Read [External Binaries](../configuration/external-binaries.md) * Read [Updating](../admin/updating.md) diff --git a/docs/_docs/installation/manual.md b/docs/_docs/installation/manual.md index eb8101aa..72ae2633 100644 --- a/docs/_docs/installation/manual.md +++ b/docs/_docs/installation/manual.md @@ -24,7 +24,7 @@ Node Version Manager (NVM) is an excellent way to install and manage Node.js ver ```bash curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash ``` -:information_source: Do not cut+paste the above command! Visit the [NVM](https://github.com/creationix/nvm) page and run the latest version! +> :information_source: Do not cut+paste the above command! Visit the [NVM](https://github.com/creationix/nvm) page and run the latest version! Next, install Node.js with NVM: ```bash @@ -52,9 +52,9 @@ npm install # yarn also works ## Other Recommended Packages ENiGMA BBS makes use of a few packages for archive and legacy protocol support. They're not pre-requisites for running ENiGMA, but without them you'll miss certain functionality. Once installed, they should be made available on your systems `PATH`. -:information_source: Please see [External Binaries](../configuration/external-binaries.md) for information on setting these up. +> :information_source: Please see [External Binaries](../configuration/external-binaries.md) for information on setting these up. -:information_source: Additional information in [Archivers](../configuration/archivers.md) and [File Transfer Protocols](../configuration/file-transfer-protocols.md) +> :information_source: Additional information in [Archivers](../configuration/archivers.md) and [File Transfer Protocols](../configuration/file-transfer-protocols.md) ## Config Files You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compliant JSON is also OK). See [Configuration](../configuration/hjson.md) for more information. diff --git a/docs/_docs/messageareas/bso-import-export.md b/docs/_docs/messageareas/bso-import-export.md index bf6ecc03..dcb29e88 100644 --- a/docs/_docs/messageareas/bso-import-export.md +++ b/docs/_docs/messageareas/bso-import-export.md @@ -5,7 +5,7 @@ title: BSO Import / Export ## BSO Import / Export The scanner/tosser module `ftn_bso` provides **B**inkley **S**tyle **O**utbound (BSO) import/toss and scan/export of messages EchoMail and NetMail messages. Configuration is supplied in `config.hjson` under `scannerTossers.ftn_bso`. -:information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts to perform packet transport**! An external [mailer](http://www.filegate.net/bbsmailers.htm) such as [Binkd](https://github.com/pgul/binkd) is required for this task! +> :information_source: ENiGMA½'s `ftn_bso` module is not a mailer and **makes no attempts to perform packet transport**! An external [mailer](http://www.filegate.net/bbsmailers.htm) such as [Binkd](https://github.com/pgul/binkd) is required for this task! ### Configuration Let's look at some of the basic configuration: diff --git a/docs/_docs/messageareas/configuring-a-message-area.md b/docs/_docs/messageareas/configuring-a-message-area.md index b8d3f5a5..2214e437 100644 --- a/docs/_docs/messageareas/configuring-a-message-area.md +++ b/docs/_docs/messageareas/configuring-a-message-area.md @@ -10,7 +10,7 @@ Message Conferences are the top level container for *1:n* Message *Areas* via th Each conference is represented by a entry under `messageConferences`. Each entries top level key is it's *conference tag*. -:bulb: It is **highly** recommended to use snake_case style message *conference tags* and *area tags*! +> :bulb: It is **highly** recommended to use snake_case style message *conference tags* and *area tags*! | Config Item | Required | Description | |-------------|----------|-------------| diff --git a/docs/_docs/messageareas/ftn.md b/docs/_docs/messageareas/ftn.md index 794b76c2..22da103f 100644 --- a/docs/_docs/messageareas/ftn.md +++ b/docs/_docs/messageareas/ftn.md @@ -15,7 +15,7 @@ Getting a fully running FTN enabled system requires a few configuration points: 2. `messageNetworks.ftn.areas`: Establishes local area mappings (ENiGMA½ to/from FTN area tags) and per-area specific configurations. 3. `scannerTossers.ftn_bso`: General configuration for the scanner/tosser (import/export) process. This is also where we configure per-node (uplink) settings. -:information_source: ENiGMA½'s `ftn_bso` module is **not a mailer** and makes **no attempts** to perform packet transport! An external utility such as Binkd is required for this task. +> :information_source: ENiGMA½'s `ftn_bso` module is **not a mailer** and makes **no attempts** to perform packet transport! An external utility such as Binkd is required for this task. #### Networks The `networks` block is a per-network configuration where each entry's ID (or "key") may be referenced elsewhere in `config.hjson`. For example, consider two networks: ArakNet (`araknet`) and fsxNet (`fsxnet`): @@ -70,7 +70,7 @@ Example: } ``` -:bulb: You can import `AREAS.BBS` or FTN style `.NA` files using [oputil](../admin/oputil.md)! +> :bulb: You can import `AREAS.BBS` or FTN style `.NA` files using [oputil](../admin/oputil.md)! #### A More Complete Example Below is a more complete *example* illustrating some of the concepts above: @@ -101,7 +101,7 @@ Below is a more complete *example* illustrating some of the concepts above: } ``` -:information_source: Remember for a complete FTN experience, you'll probably also want to configure [FTN/BSO scanner/tosser](bso-import-export.md) settings. +> :information_source: Remember for a complete FTN experience, you'll probably also want to configure [FTN/BSO scanner/tosser](bso-import-export.md) settings. #### FTN/BSO Scanner Tosser Please see the [FTN/BSO Scanner/Tosser](bso-import-export.md) documentation for information on this area. \ No newline at end of file diff --git a/docs/_docs/messageareas/message-networks.md b/docs/_docs/messageareas/message-networks.md index 52a4227e..a36010f4 100644 --- a/docs/_docs/messageareas/message-networks.md +++ b/docs/_docs/messageareas/message-networks.md @@ -10,7 +10,7 @@ All message network configuration occurs under the `messageNetworks.` bloc 1. `messageNetworks..networks`: Global/general configuration for a particular network where `` is for example `ftn` or `qwk`. 2. `messageNetworks..areas`: Provides mapping of ENiGMA½ **area tags** to their external counterparts. -:information_source: A related section under `scannerTossers.` may provide configuration for scanning (importing) and tossing (exporting) messages for a particular network type. As an example, FidoNet-Style networks often work with BinkleyTerm Style Outbound (BSO) and thus the [FTN/BSO scanner/tosser](bso-import-export.md) (`ftn_bso`) module. +> :information_source: A related section under `scannerTossers.` may provide configuration for scanning (importing) and tossing (exporting) messages for a particular network type. As an example, FidoNet-Style networks often work with BinkleyTerm Style Outbound (BSO) and thus the [FTN/BSO scanner/tosser](bso-import-export.md) (`ftn_bso`) module. ### Currently Supported Networks The following networks are supported out of the box. Remember that you can create modules to add others if desired! diff --git a/docs/_docs/messageareas/qwk.md b/docs/_docs/messageareas/qwk.md index 140180f5..c4b1e177 100644 --- a/docs/_docs/messageareas/qwk.md +++ b/docs/_docs/messageareas/qwk.md @@ -16,7 +16,7 @@ QWK must be considered a semi-standard as there are many implementations. What f ### Configuration QWK configuration occurs in the `messageNetworks.qwk` config block of `config.hjson`. As QWK wants to deal with conference numbers and ENiGMA½ uses area tags (conferences and conference tags are only used for logical grouping), a mapping can be made. -:information_source: During a regular, non QWK-Net exports, conference numbers can be auto-generated. Note that for QWK-Net style networks, you will need to create mappings however. +> :information_source: During a regular, non QWK-Net exports, conference numbers can be auto-generated. Note that for QWK-Net style networks, you will need to create mappings however. Example: ```hjson diff --git a/docs/_docs/modding/local-doors.md b/docs/_docs/modding/local-doors.md index ce948875..3c2404bd 100644 --- a/docs/_docs/modding/local-doors.md +++ b/docs/_docs/modding/local-doors.md @@ -5,7 +5,7 @@ title: Local Doors ## Local Doors ENiGMA½ has many ways to add doors to your system. In addition to the [many built in door server modules](door-servers.md), local doors are of course also supported using the ! The `abracadabra` module! -:information_source: See also [Let’s add a DOS door to Enigma½ BBS](https://medium.com/retro-future/lets-add-a-dos-game-to-enigma-1-2-41f257deaa3c) by Robbie Whiting for a great writeup on adding doors! +> :information_source: See also [Let’s add a DOS door to Enigma½ BBS](https://medium.com/retro-future/lets-add-a-dos-game-to-enigma-1-2-41f257deaa3c) by Robbie Whiting for a great writeup on adding doors! ## The abracadabra Module The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and perform I/O through standard I/O (stdio) or a temporary TCP server. diff --git a/docs/_docs/modding/user-2fa-otp-config.md b/docs/_docs/modding/user-2fa-otp-config.md index f2e5f945..b8a3f32a 100644 --- a/docs/_docs/modding/user-2fa-otp-config.md +++ b/docs/_docs/modding/user-2fa-otp-config.md @@ -5,7 +5,7 @@ title: 2FA/OTP Config ## The 2FA/OTP Config Module The `user_2fa_otp_config` module provides opt-in, configuration, and viewing of Two-Factor Authentication via One-Time-Password (2FA/OTP) settings. In order to allow users access to 2FA/OTP, the system must be properly configured. See [Security](../configuration/security.md) for more information. -:information_source: By default, the 2FA/OTP configuration menu may only be accessed by users connected securely (ACS `SC`). It is highly recommended to leave this default as accessing these settings over a plain-text connection could expose private secrets! +> :information_source: By default, the 2FA/OTP configuration menu may only be accessed by users connected securely (ACS `SC`). It is highly recommended to leave this default as accessing these settings over a plain-text connection could expose private secrets! ## Configuration diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index d78a7b45..be691413 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -16,7 +16,7 @@ The system allows any user with the proper security to access the WFC / system o 3. User ID of 1 (root/admin) 4. The user belongs to the `wfc` group. -> :information_source: Due to the above, the WFC screen is **disabled** by default as at a minimum, you'll need to add your user to the `wfc` group. +> :information_source: Due to the above, the WFC screen is **disabled** by default as at a minimum, you'll need to add your user to the `wfc` group. See also [Security](../configuration/security.md) for more information on keeping your system secure! To change the ACS required, specify a alternative `acs` in the `config` block. For example: ```hjson @@ -28,7 +28,7 @@ mainMenuWaitingForCaller: { } ``` -> :notebook: ENiGMA½ will enforce ACS of at least `SC` (secure connection) +> :lock: ENiGMA½ will enforce ACS of at least `SC` (secure connection) ## Theming The following MCI codes are available: diff --git a/docs/_docs/modding/whos-online.md b/docs/_docs/modding/whos-online.md index 963abeef..744c9d08 100644 --- a/docs/_docs/modding/whos-online.md +++ b/docs/_docs/modding/whos-online.md @@ -19,5 +19,5 @@ The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): * `isSecure`: Is the client securely connected? * `serverName`: Name of connected server such as "Telnet" or "SSH". -:information_source: These properties are available via the `client_connections.js` `getActiveConnectionList()` API. +> :information_source: These properties are available via the `client_connections.js` `getActiveConnectionList()` API. diff --git a/docs/_docs/servers/contentservers/gopher.md b/docs/_docs/servers/contentservers/gopher.md index 03c34bed..2bbbcce0 100644 --- a/docs/_docs/servers/contentservers/gopher.md +++ b/docs/_docs/servers/contentservers/gopher.md @@ -29,11 +29,11 @@ ENiGMA will pre-process `gophermap` files replacing in following variables: * `{publicHostname}`: The public hostname from your config. * `{publicPort}`: The public port from your config. -:information_source: See [Wikipedia](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) for more information on the `gophermap` format. +> :information_source: See [Wikipedia](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) for more information on the `gophermap` format. -:information_source: See [RFC 1436](https://tools.ietf.org/html/rfc1436) for the original Gopher spec. +> :information_source: See [RFC 1436](https://tools.ietf.org/html/rfc1436) for the original Gopher spec. -:bulb: Tools such as [gfu](https://rawtext.club/~sloum/gfu.html) may help you with `gophermap`'s +> :bulb: Tools such as [gfu](https://rawtext.club/~sloum/gfu.html) may help you with `gophermap`'s ### Example Gophermap An example `gophermap` living in `enigma-bbs/gopher`: diff --git a/docs/_docs/servers/contentservers/web-server.md b/docs/_docs/servers/contentservers/web-server.md index 75c7ea18..2f9fa1cd 100644 --- a/docs/_docs/servers/contentservers/web-server.md +++ b/docs/_docs/servers/contentservers/web-server.md @@ -55,7 +55,7 @@ Entries available under `contentServers.web.https`: If you don't have a TLS certificate for your domain, a good source for a certificate can be [Let's Encrypt](https://letsencrypt.org/) who supplies free and trusted TLS certificates. A common strategy is to place another web server such as [Caddy](https://caddyserver.com/) in front of ENiGMA½ acting as a transparent proxy and TLS termination point. -:information_source: Keep in mind that the SSL certificate provided by Let's Encrypt's Certbot is by default stored in a privileged location; if your ENIGMA instance is not running as root (which it should not be!), you'll need to copy the SSL certificate somewhere else in order for ENIGMA to use it. +> :information_source: Keep in mind that the SSL certificate provided by Let's Encrypt's Certbot is by default stored in a privileged location; if your ENIGMA instance is not running as root (which it should not be!), you'll need to copy the SSL certificate somewhere else in order for ENIGMA to use it. ## Static Routes From 821380fcb5d39823c81b7bff469694e51f04b9e1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Jul 2022 22:17:25 -0600 Subject: [PATCH 44/52] Template updates --- art/themes/luciano_blocktronics/theme.hjson | 2 +- misc/menu_templates/main.in.hjson | 74 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index cf3b744f..6bc9b7af 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -250,7 +250,7 @@ config: { // formats quickLogTimestampFormat: "|01|02MM|08/|02DD hh:mm:ssa" - nowDateTimeFormat: "|00|11ddd|08, |11MMMM Do YYYY|08, |11h|08:|11mm|08:|11ss|03a" + nowDateTimeFormat: "|00|11ddd|08, |11MMMM Do YYYY|08, |11h|08:|11mm|03a" lastLoginDateTimeFormat: "|00|10ddd hh|08:|10mm|02a" // header diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson index 24651adc..85857fc7 100644 --- a/misc/menu_templates/main.in.hjson +++ b/misc/menu_templates/main.in.hjson @@ -153,6 +153,10 @@ value: { command: "MRC" } action: @menu:mrc } + { + value: { command: "!WFC" } + action: @menu:mainMenuWaitingForCaller + } { value: { command: "2FA" } action: [ @@ -193,6 +197,76 @@ } } + mainMenuWaitingForCaller: { + desc: -WFC- + module: wfc + + config: { + art: { + main: wfc + help: wfchelp + } + } + + form: { + 0: { + mci: { + VM1: { + focus: true + } + VM2: { + focus: false + acceptsFocus: false + acceptsInput: false + } + } + + actionKeys: [ + { + keys: [ "a", "shift + a" ] + action: @method:toggleAvailable + } + { + keys: [ "v", "shift + v" ] + action: @method:toggleVisible + } + { + keys: [ "?", "h", "shift + h" ] + action: @method:displayHelp + } + { + keys: [ "1", "2", "3", "4", "5", "6", "7", "8", "9" ] + action: @method:setNodeStatusSelection + } + { + keys: [ "k", "shift + k" ] + action: @method:kickSelectedNode + } + { + keys: [ "escape", "q", "shift + q" ] + action: @systemMethod:prevMenu + } + ] + } + + // confirmKickNodePrompt + 3: { + submit: { + *: [ + { + value: { promptValue: 0 } + action: @method:kickNodeYes + } + { + value: { promptValue: 1 } + action: @method:kickNodeNo + } + ] + } + } + } + } + mrc: { desc: MRC Chat module: mrc From e8f6a2f70218e5c9b8bb27402062e1529af201db Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 16 Jul 2022 22:23:34 -0600 Subject: [PATCH 45/52] Missing defaults --- .../luciano_blocktronics/wfckicknodeprompt.ans | 1 + misc/menu_templates/main.in.hjson | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 art/themes/luciano_blocktronics/wfckicknodeprompt.ans diff --git a/art/themes/luciano_blocktronics/wfckicknodeprompt.ans b/art/themes/luciano_blocktronics/wfckicknodeprompt.ans new file mode 100644 index 00000000..a11d90eb --- /dev/null +++ b/art/themes/luciano_blocktronics/wfckicknodeprompt.ans @@ -0,0 +1 @@ +>> kick node? %TM1%TM1 diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson index 85857fc7..208892ab 100644 --- a/misc/menu_templates/main.in.hjson +++ b/misc/menu_templates/main.in.hjson @@ -1078,6 +1078,20 @@ ] } + // WFC + confirmKickNodePrompt: { + art: wfckicknodeprompt + mci: { + TM1: { + argName: promptValue + items: [ "yes", "no" ] + focus: true + hotKeys: { Y: 0, N: 1 } + hotKeySubmit: true + } + } + } + //////////////////////////////////////////////////////////////////////// // These entries are required by the system and must exist. // You can still modify/theme them, however. From 17ddd732474ecb32225bc05cf92013aabf5b242d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 17 Jul 2022 23:02:08 -0600 Subject: [PATCH 46/52] Allow for WFC status to be MLTEV --- art/themes/luciano_blocktronics/theme.hjson | 9 ++++++++- art/themes/luciano_blocktronics/wfc.ans | Bin 3055 -> 3055 bytes core/menu_module.js | 2 +- core/wfc.js | 15 +++++++++++---- docs/_docs/modding/wfc.md | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 6bc9b7af..fc3a7682 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -297,7 +297,7 @@ statusAvailableIndicators: [ "N", "Y" ] statusVisibleIndicators: [ "N", "Y" ] - nodeStatusSelectionFormat: "|00|10{realName}" + nodeStatusSelectionFormat: "|00|10{realName}\n{serverName}" } 0: { mci: { @@ -322,6 +322,13 @@ width: 73 itemFormat: "|00|07{nodeId} {levelIndicator} |02{timestamp} {message:<51.50}" } + + MT3: { + mode: preview + autoScroll: false + height: 5 + width: 12 + } } } } diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index 9c1e6ede43aa7c319f0b9df2d1ef10cdd35f8654..3588cabf411e39dc50d934db3fbbb89df4f3a316 100644 GIT binary patch delta 28 kcmaDa{$6}T5eu_#i1Fl)EMl9xS*+Qa%?-^bpX62p0GFu=TL1t6 delta 28 kcmaDa{$6}T5esvOkMZP>EMl9xS*+Qa&5VpEpX62p0GEgfRsaA1 diff --git a/core/menu_module.js b/core/menu_module.js index fd234934..eb98ef29 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -685,7 +685,7 @@ exports.MenuModule = class MenuModule extends PluginModule { } if (appendMultiLine && view instanceof MultiLineEditTextView) { - view.addText(text); + view.setAnsi(text); } else { view.setText(text); } diff --git a/core/wfc.js b/core/wfc.js index 5635d043..d1ce530a 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -14,6 +14,9 @@ const UserProps = require('./user_property'); const Log = require('./logger'); const Config = require('./config.js').get; const { Errors } = require('./enig_error'); +const { pipeToAnsi } = require('./color_codes'); +const MultiLineEditTextView = + require('./multi_line_edit_text_view').MultiLineEditTextView; // deps const async = require('async'); @@ -112,7 +115,6 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { return this._confirmKickSelectedNode(cb); }, kickNodeYes: (formData, extraArgs, cb) => { - //this._startRefreshing(); return this._kickSelectedNode(cb); }, kickNodeNo: (formData, extraArgs, cb) => { @@ -232,9 +234,14 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { if (item) { const nodeStatusSelectionFormat = this.config.nodeStatusSelectionFormat || '{text}'; - nodeStatusSelectionView.setText( - stringFormat(nodeStatusSelectionFormat, item) - ); + + const s = stringFormat(nodeStatusSelectionFormat, item); + + if (nodeStatusSelectionView instanceof MultiLineEditTextView) { + nodeStatusSelectionView.setAnsi(pipeToAnsi(s, this.client)); + } else { + nodeStatusSelectionView.setText(s); + } } } diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index be691413..31c93365 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -92,7 +92,7 @@ The following MCI codes are available: * `freeMemoryBytes`: Free system memory in bytes. * `systemAvgLoad`: System average load. * `systemCurrentLoad`: System current load. - * `newPrivateMail`: Number of new **privae** mail for current user. + * `newPrivateMail`: Number of new **private** mail for current user. * `newMessagesAddrTo`: Number of new messages **addressed to the current user**. * `availIndicator`: Is the current user availalbe? Displayed via `statusAvailableIndicators` or system theme. See also [Themes](../art/themes.md). * `visIndicator`: Is the current user visible? Displayed via `statusVisibleIndicators` or system theme. See also [Themes](../art/themes.md). From 715202680e6fbe1a7b3bfb7946f5d341becc63c3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 1 Aug 2022 23:09:18 -0600 Subject: [PATCH 47/52] Add Process/enig start ingress/egress bytes stats --- art/themes/luciano_blocktronics/theme.hjson | 2 ++ art/themes/luciano_blocktronics/wfc.ans | Bin 3055 -> 3092 bytes core/client_connections.js | 9 +++++++++ core/login_server_module.js | 1 + core/predefined_mci.js | 13 +++++++++++++ core/stat_log.js | 17 +++++++++++++++++ core/system_property.js | 1 + core/wfc.js | 4 ++++ docs/_docs/art/mci.md | 2 ++ docs/_docs/modding/wfc.md | 2 ++ 10 files changed, 51 insertions(+) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index fc3a7682..ff93609a 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -276,6 +276,8 @@ mainInfoFormat21: "|00|10{totalPosts:>7}" mainInfoFormat22: "|00|10{totalUsers:>5}" mainInfoFormat23: "|00|10{totalFiles:>4} |08/ |10{totalFileBytes!sizeWithoutAbbr:>4} |02{totalFileBytes!sizeAbbr}" + mainInfoFormat24: "|00|10{processBytesIngress!sizeWithoutAbbr:>5} |02{processBytesIngress!sizeAbbr}" + mainInfoFormat25: "|00|10{processBytesEgress!sizeWithoutAbbr:>5} |02{processBytesEgress!sizeAbbr}" quickLogLevel: info quickLogLevelIndicators: { diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index 3588cabf411e39dc50d934db3fbbb89df4f3a316..ecb0eedd5ec20dacb212af45020fe5836dfa4eda 100644 GIT binary patch delta 91 zcmaDaK1E`~1m?+d9PDDAdFe%|#l;HJ(T2IIAwEVX($U5Sxs&HIXD9(#M!BhAX&}=) kR{^BR8Yl)*XDS^HRQ8csc(W?YLUtAl1H;vmPjRaP0RChfO#lD@ delta 33 pcmbOt@m_qx1m?+3j3SfMxvVD3v-)lhV42O%Vs2>OHu*ibDgeFc3Z?)6 diff --git a/core/client_connections.js b/core/client_connections.js index ce39fe1c..e4a75520 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -122,6 +122,15 @@ function addNewClient(client, clientSock) { moment().valueOf(), ]); + // kludge to refresh process update stats at first client + if (clientConnections.length < 1) { + setTimeout(() => { + const StatLog = require('./stat_log'); + const SysProps = require('./system_property'); + StatLog.getSystemStat(SysProps.ProcessTrafficStats); + }, 3000); // slight pause to wait for updates + } + clientConnections.push(client); clientConnections.sort((c1, c2) => c1.session.id - c2.session.id); diff --git a/core/login_server_module.js b/core/login_server_module.js index 95eaebae..5f74801f 100644 --- a/core/login_server_module.js +++ b/core/login_server_module.js @@ -52,6 +52,7 @@ module.exports = class LoginServerModule extends ServerModule { client.session = {}; } + client.rawSocket = clientSock; client.session.serverName = modInfo.name; client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 8461d283..7463186f 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -385,6 +385,19 @@ const PREDEFINED_MCI_GENERATORS = { return StatLog.getSystemStat(SysProps.LoginsToday).toLocaleString(); }, + PI: function processBytesIngress() { + const stats = StatLog.getSystemStat(SysProps.ProcessTrafficStats) || { + ingress: 0, + }; + return stats.ingress.toLocaleString(); + }, + PE: function processBytesEgress() { + const stats = StatLog.getSystemStat(SysProps.ProcessTrafficStats) || { + egress: 0, + }; + return stats.ingress.toLocaleString(); + }, + RR: function randomRumor() { // start the process of picking another random one setNextRandomRumor(); diff --git a/core/stat_log.js b/core/stat_log.js index 8c80fc6c..f2d57bd0 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -7,6 +7,7 @@ const Errors = require('./enig_error.js'); const SysProps = require('./system_property.js'); const UserProps = require('./user_property'); const Message = require('./message'); +const { getActiveConnections, AllConnections } = require('./client_connections'); // deps const _ = require('lodash'); @@ -360,6 +361,9 @@ class StatLog { case SysProps.SystemLoadStats: case SysProps.SystemMemoryStats: return this._refreshSysInfoStats(); + + case SysProps.ProcessTrafficStats: + return this._refreshProcessTrafficStats(); } } @@ -402,6 +406,19 @@ class StatLog { }); } + _refreshProcessTrafficStats() { + const trafficStats = getActiveConnections(AllConnections).reduce( + (stats, conn) => { + stats.ingress += conn.rawSocket.bytesRead; + stats.egress += conn.rawSocket.bytesWritten; + return stats; + }, + { ingress: 0, egress: 0 } + ); + + this.setNonPersistentSystemStat(SysProps.ProcessTrafficStats, trafficStats); + } + _refreshUserStat(client, statName, ttlSeconds) { switch (statName) { case UserProps.NewPrivateMailCount: diff --git a/core/system_property.js b/core/system_property.js index b8d8b5c8..6d29e013 100644 --- a/core/system_property.js +++ b/core/system_property.js @@ -37,6 +37,7 @@ module.exports = { SystemMemoryStats: 'system_memory_stats', // object { totalBytes, freeBytes }; non-persistent SystemLoadStats: 'system_load_stats', // object { average, current }; non-persistent + ProcessTrafficStats: 'system_traffic_bytes_ingress', // object { ingress, egress }; non-persistent TotalUserCount: 'user_total_count', // non-persistent NewUsersTodayCount: 'user_new_today_count', // non-persistent diff --git a/core/wfc.js b/core/wfc.js index d1ce530a..da1e7be8 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -413,6 +413,8 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { const sysMemStats = StatLog.getSystemStat(SysProps.SystemMemoryStats) || {}; const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats) || {}; const lastLoginStats = StatLog.getSystemStat(SysProps.LastLogin); + const processTrafficStats = + StatLog.getSystemStat(SysProps.ProcessTrafficStats) || {}; const now = moment(); @@ -479,6 +481,8 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { UserProps.NewAddressedToMessageCount, MailCountTTLSeconds ), + processBytesIngress: processTrafficStats.ingress || 0, + processBytesEgress: processTrafficStats.egress || 0, }; return cb(null); diff --git a/docs/_docs/art/mci.md b/docs/_docs/art/mci.md index de921710..75cbb45c 100644 --- a/docs/_docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -109,6 +109,8 @@ There are many predefined MCI codes that can be used anywhere on the system (pla | `NP` | Count of new private mail to the current user | | `IA` | Indicator as to rather the current user is **available** or not. See also `getStatusAvailIndicators()` in [Themes](themes.md) | | `IV` | Indicator as to rather the curent user is **visible** or not. See also `getStatusVisibleIndicators()` in [Themes](themes.md) | +| `PI` | Ingress bytes for the current process (since ENiGMA started up) | +| `PE` | Egress bytes for the current process (since ENiGMA started up) | Some additional special case codes also exist: diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index 31c93365..7e30f5ff 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -96,6 +96,8 @@ The following MCI codes are available: * `newMessagesAddrTo`: Number of new messages **addressed to the current user**. * `availIndicator`: Is the current user availalbe? Displayed via `statusAvailableIndicators` or system theme. See also [Themes](../art/themes.md). * `visIndicator`: Is the current user visible? Displayed via `statusVisibleIndicators` or system theme. See also [Themes](../art/themes.md). + * `processBytesIngress`: Ingress bytes since ENiGMA started. + * `processBytesEgress`: Egress bytes since ENiGMA started. > :information_source: While [Standard MCI](../art/mci.md) codes work on any menu, they will **not** refresh. For values that may change over time, please use the custom format values above. \ No newline at end of file From 7268ca9bd6a600dc63c96acc4f6ca85a0aab7748 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 4 Aug 2022 10:52:43 -0600 Subject: [PATCH 48/52] Fix a dumb bug with theme switching; Add TODO to clean this up --- core/client.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/client.js b/core/client.js index 30a0f7cd..88367773 100644 --- a/core/client.js +++ b/core/client.js @@ -97,7 +97,12 @@ function Client(/*input, output*/) { Object.defineProperty(this, 'currentTheme', { get: () => { if (this.currentThemeConfig) { - return this.currentThemeConfig.get(); + // :TODO: clean this up: We have a ugly transition state in which we have a pure raw config vs a ConfigLoader in which get() must be called + try { + return this.currentThemeConfig.get(); + } catch(e) { + return this.currentThemeConfig; + } } else { return { info: { From 8a351ecd7dd19ebf5c9dfc9641ca819c198764d6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 4 Aug 2022 10:52:59 -0600 Subject: [PATCH 49/52] WFC Luciano Blocktronics theme --- art/themes/luciano_blocktronics/theme.hjson | 43 ++++++++++---------- art/themes/luciano_blocktronics/wfc.ans | Bin 3092 -> 3138 bytes 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index ff93609a..56a35ef7 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -249,35 +249,36 @@ mainMenuWaitingForCaller: { config: { // formats - quickLogTimestampFormat: "|01|02MM|08/|02DD hh:mm:ssa" - nowDateTimeFormat: "|00|11ddd|08, |11MMMM Do YYYY|08, |11h|08:|11mm|03a" + quickLogTimestampFormat: "|01|03MM|08/|03DD hh:mm:ssa" + nowDateTimeFormat: "|00|10ddd|08, |10MMMM Do YYYY|08, |10h|08:|10mm|02a" lastLoginDateTimeFormat: "|00|10ddd hh|08:|10mm|02a" // header - mainInfoFormat10: "|00|11{now} {currentUserName} |08- |03Prv|08:|11{newPrivateMail} |03Addr|08:|11{newMessagesAddrTo} |08- |03Avail|08:|11{availIndicator} |03Vis|08:|11{visIndicator}" + mainInfoFormat10: "|00|10{now} |10{currentUserName} |08- |02Prv|08:|10{newPrivateMail} |02Addr|08:|10{newMessagesAddrTo} |08- |02Avail|08:|10{availIndicator} |02Vis|07:|10{visIndicator}" // today - mainInfoFormat11: "|00|10{callsToday:>5}" - mainInfoFormat12: "|00|10{postsToday:>5}" - mainInfoFormat13: "|00|10{uploadsToday:>2} |08/ |10{uploadBytesToday!sizeWithoutAbbr:>3} |02{uploadBytesToday!sizeAbbr}" - mainInfoFormat14: "|00|10{downloadsToday:>2} |08/ |10{downloadBytesToday!sizeWithoutAbbr:>3} |02{downloadBytesToday!sizeAbbr}" - mainInfoFormat16: "|00|10{newUsersToday:>5}" + mainInfoFormat11: "|00|15{callsToday:>5}" + mainInfoFormat12: "|00|15{postsToday:>5}" + mainInfoFormat13: "|00|15{newUsersToday:>5}" + mainInfoFormat14: "|00|15{uploadsToday:<4}" + mainInfoFormat15: "|00|15{downloadsToday:<4}" + mainInfoFormat16: "|00|15{uploadBytesToday!sizeWithoutAbbr:<5} |07{uploadBytesToday!sizeAbbr}" + mainInfoFormat17: "|00|15{downloadBytesToday!sizeWithoutAbbr:<5} |07{downloadBytesToday!sizeAbbr}" + // last login - mainInfoFormat15: "|00|10{lastLoginUserName:<26} |02{lastLogin}" + mainInfoFormat18: "|00|15{lastLoginUserName:<26} |07{lastLogin}" // system stats - mainInfoFormat17: "|00|10{freeMemoryBytes!sizeWithoutAbbr} |02{freeMemoryBytes!sizeAbbr} free |08/ |10{totalMemoryBytes!sizeWithoutAbbr} |02{totalMemoryBytes!sizeAbbr}" - mainInfoFormat18: "|00|10{systemCurrentLoad} |02% |08/ |10{systemAvgLoad} |02load avg|08." - mainInfoFormat19: "|00|10{processUptimeSeconds!durationSeconds}" + mainInfoFormat20: "|00|15{freeMemoryBytes!sizeWithoutAbbr} |07{freeMemoryBytes!sizeAbbr} free |08/ |15{totalMemoryBytes!sizeWithoutAbbr} |07{totalMemoryBytes!sizeAbbr}" + mainInfoFormat22: "|00|15{systemCurrentLoad} |07% |08/ |15{systemAvgLoad} |07load avg|08." + mainInfoFormat24: "|00|15{processUptimeSeconds!durationSeconds} |08/ |15{processBytesIngress!sizeWithoutAbbr:>4} |07{processBytesIngress!sizeAbbr}|08/|15{processBytesEgress!sizeWithoutAbbr:>4} |07{processBytesEgress!sizeAbbr}" // totals - mainInfoFormat20: "|00|10{totalCalls:>5}" - mainInfoFormat21: "|00|10{totalPosts:>7}" - mainInfoFormat22: "|00|10{totalUsers:>5}" - mainInfoFormat23: "|00|10{totalFiles:>4} |08/ |10{totalFileBytes!sizeWithoutAbbr:>4} |02{totalFileBytes!sizeAbbr}" - mainInfoFormat24: "|00|10{processBytesIngress!sizeWithoutAbbr:>5} |02{processBytesIngress!sizeAbbr}" - mainInfoFormat25: "|00|10{processBytesEgress!sizeWithoutAbbr:>5} |02{processBytesEgress!sizeAbbr}" + mainInfoFormat19: "|00|15{totalCalls:>5}" + mainInfoFormat21: "|00|15{totalPosts:>7}" + mainInfoFormat23: "|00|15{totalUsers:>5}" + mainInfoFormat25: "|00|15{totalFiles:>4} |08/ |15{totalFileBytes!sizeWithoutAbbr:>4} |07{totalFileBytes!sizeAbbr}" quickLogLevel: info quickLogLevelIndicators: { @@ -299,7 +300,7 @@ statusAvailableIndicators: [ "N", "Y" ] statusVisibleIndicators: [ "N", "Y" ] - nodeStatusSelectionFormat: "|00|10{realName}\n{serverName}" + nodeStatusSelectionFormat: "|00|11{realName}\n{serverName}" } 0: { mci: { @@ -314,8 +315,8 @@ VM1: { height: 5 width: 37 - itemFormat: "|00 |11{node:<3.2} |10{userName:<12} |02{action:<14.13} |14{serverName}" - focusItemFormat: "|00|15> |11{node:<3.2} |10{userName:<12} |02{action:<14.13} |14{serverName}" + itemFormat: "|00 |11{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}" + focusItemFormat: "|00|15> |11{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}" focusItemAtTop: false } // quick log diff --git a/art/themes/luciano_blocktronics/wfc.ans b/art/themes/luciano_blocktronics/wfc.ans index ecb0eedd5ec20dacb212af45020fe5836dfa4eda..9bf19ca21893a2f8ab6ede54d203ee743f2109c8 100644 GIT binary patch literal 3138 zcmds3&5ja55N18%6DXIy5O3HsgEOlf%*f(~$dB#}DB(odQ8&^+K(Y_E-Zj~MiFJv* ziq+LW(+v#S4Z(wRAYIj6RbPEwRZXE!87cIgis!Rp+;C_C6#2YRQT7`au+wUv)JPPE zaVG-V;T9fcH<{m0?uA$%#sG)f-e+<9BxbQFg9f9v&D2Y8Jy1{^!em+jBC&b`koG19TJ6u z#aPySK>nzCVQMPSIRXpPquTQtz>2lc8etTZYS?Jh&Xi|vmD#{IKSTg<;j(1R!(M=< z0@4>YAkK4Mk-!8L1&j_Eo(qW1H_@RcFg4m>?FN^`f}eB=y$Y_E|HG z>+M#SxwuDpXM`;8ID&G(mdirFOxY6QVN^YKw$48V81L+*FyK0kOq;Vrw?V07nz{Ib z*nF7M8f!x2SXU4Z5U-XiKo6kdSG*yX1sw938kuP;ijisI;1e8Xsvvt?tD>vnbRqr2 zBVgG>g|EM1_sRn(=Y1mR7j+p_ARnHe(82=K%}Q_k{N*X~Wbl1*Jy@x(7eQ35!v-)k zn+%oOuu2DsnTlRpOoMiHxBRohU9|%~@%8+CF-qnT$#}F<*tusfk)|uPubVWP#U8ebnB@e4jWu9!>*Ll%Ty z7zHSB#BJsJjsVP91P@pQ#lq>pTM!5{LFi=W`I>Or^Ra7SSvSgoJVIW=bhS!ij6!$- z)PoqVmg@v`M=D%LT!R`kT=bVovPIh-MD|24u;r8zm&g+M4m&HEl5j7(xnj58!Z~fJ ziOtgO#U5h(-ie)DCJC|-zM_?`kXMOVq?kTc83c1m9+SxfsmijT;rLe}?ufA~fr zQ=ga@zqtEGX6BI1CXsb2K ztY{!}18E~t22S;+2w++%4dhtgGgpMu(DMVC{9;0HDWlnCyFm+A1dAXXj`Q>TCOV{;S#1EPgmgcN5!6VIBIG(=?))~g(8Oohx>6G;Z1-v4&NW)76J!kUh-Q)e#4Q-if zx#iIjIma1M{Tz_VY?HpnkrJI>tOd=Z;&VqP3~$eK%8jOCu+T#KuQd=fmH%X&#QBB9 zWXX!_iwuI@!v8QDwR-c@pj7m3Mjxv>uFJj@kUwle_Hi EZ!i);U;qFB From 95183fd3b3c7c2eec3c7f0deb94ab82addb4f410 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 4 Aug 2022 11:32:09 -0600 Subject: [PATCH 50/52] Cleanup, docs, screen shot of WFC --- art/themes/luciano_blocktronics/theme.hjson | 12 ++++++------ core/client.js | 2 +- core/client_connections.js | 1 + core/wfc.js | 20 +++++++++++++++----- core/whos_online.js | 4 +++- docs/_docs/modding/wfc.md | 2 ++ docs/assets/images/wfc.png | Bin 0 -> 55826 bytes 7 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 docs/assets/images/wfc.png diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 56a35ef7..8f6d01d7 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -300,23 +300,23 @@ statusAvailableIndicators: [ "N", "Y" ] statusVisibleIndicators: [ "N", "Y" ] - nodeStatusSelectionFormat: "|00|11{realName}\n{serverName}" + nodeStatusSelectionFormat: "|00|07{realName:<12}\n|08- |07{serverName:<10}\n|08- |07{remoteAddress:<10}" } 0: { mci: { TL16: { fillChar: . } - TL17: { width: 23 } - TL18: { width: 23 } - TL19: { width: 14 } + TL20: { width: 30 } + TL22: { width: 30 } + TL24: { width: 30 } // node status VM1: { height: 5 width: 37 - itemFormat: "|00 |11{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}" - focusItemFormat: "|00|15> |11{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}" + itemFormat: "|00 |15{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}" + focusItemFormat: "|00|10> |15{node:<3.2} |11{userName:<12} |07{action:<14.13} |15{serverName}" focusItemAtTop: false } // quick log diff --git a/core/client.js b/core/client.js index 88367773..d40c867d 100644 --- a/core/client.js +++ b/core/client.js @@ -100,7 +100,7 @@ function Client(/*input, output*/) { // :TODO: clean this up: We have a ugly transition state in which we have a pure raw config vs a ConfigLoader in which get() must be called try { return this.currentThemeConfig.get(); - } catch(e) { + } catch (e) { return this.currentThemeConfig; } } else { diff --git a/core/client_connections.js b/core/client_connections.js index e4a75520..796c5bf5 100644 --- a/core/client_connections.js +++ b/core/client_connections.js @@ -79,6 +79,7 @@ function getActiveConnectionList( isSecure: ac.session.isSecure, isVisible: ac.user.isVisible(), isAvailable: ac.user.isAvailable(), + remoteAddress: ac.friendlyRemoteAddress(), }; // diff --git a/core/wfc.js b/core/wfc.js index da1e7be8..63343b01 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -439,11 +439,6 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { totalFiles: fileAreaStats.totalFiles || 0, totalFileBytes: fileAreaStats.totalFileBytes || 0, - // totalUploads : - // totalUploadBytes : - // totalDownloads : - // totalDownloadBytes : - // Today's Stats callsToday: StatLog.getSystemStatNum(SysProps.LoginsToday), postsToday: StatLog.getSystemStatNum(SysProps.MessagesToday), @@ -532,9 +527,24 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { }); }); + // If this is our first pass, we'll also update the selection + const firstStatusRefresh = nodeStatusView.getCount() === 0; + // :TODO: Currently this always redraws due to setItems(). We really need painters alg.; The alternative now is to compare items... yuk. nodeStatusView.setItems(nodeStatusItems); this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); // redraws + + if (firstStatusRefresh) { + const nodeStatusSelectionView = this.getView( + 'main', + MciViewIds.main.selectedNodeStatusInfo + ); + if (nodeStatusSelectionView) { + const item = nodeStatusView.getItems()[0]; + this._updateNodeStatusSelection(nodeStatusSelectionView, item); + } + } + return cb(null); } diff --git a/core/whos_online.js b/core/whos_online.js index fb59429e..9bc792ae 100644 --- a/core/whos_online.js +++ b/core/whos_online.js @@ -57,7 +57,9 @@ exports.getModule = class WhosOnlineModule extends MenuModule { .map(oe => Object.assign(oe, { text: oe.userName, - timeOn: _.upperFirst(oe.timeOn.humanize()), + timeOn: oe.timeOn + ? _.upperFirst(oe.timeOn.humanize()) + : 0, // :TODO: fix me. We can always track time... }) ); diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index 7e30f5ff..cdd8a75e 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -5,6 +5,8 @@ title: Waiting For Caller (WFC) ## The Waiting For Caller (WFC) Module The `wfc.js` module provides a Waiting For Caller (WFC) type dashboard from a bygone era. Many traditional features are available including newer concepts for modern times. Node spy is left out as it feels like something that should be left in the past. +![WFC](../../assets/images/wfc.png)
+ ## Accessing the WFC By default, the WFC may be accessed via the `!WFC` main menu command when connected over a secure connection via a user with the proper [ACS](../configuration/acs.md). This can be configured as per any other menu in the system. Note that ENiGMA½ does not expose the WFC as a standalone application as this would be much less flexible. To connect locally, simply use your favorite terminal or for example: `ssh -l yourname localhost 8889`. See **Security** below for more information. diff --git a/docs/assets/images/wfc.png b/docs/assets/images/wfc.png new file mode 100644 index 0000000000000000000000000000000000000000..a900f0498a930be12b3f5722212af1b47e7561d5 GIT binary patch literal 55826 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 Date: Thu, 4 Aug 2022 11:38:05 -0600 Subject: [PATCH 51/52] Add missing remoteAddress key --- docs/_docs/modding/wfc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_docs/modding/wfc.md b/docs/_docs/modding/wfc.md index cdd8a75e..1aaacfe5 100644 --- a/docs/_docs/modding/wfc.md +++ b/docs/_docs/modding/wfc.md @@ -51,6 +51,7 @@ The following MCI codes are available: * `userId`: User ID of authenticated node, or 0 if not yet authenticated. * `userName`: User name of authenticated user or "*Pre Auth*" * `visIndicator`: Indicator of visibility. Displayed via `statusVisibleIndicators` or system theme. See also [Themes](../art/themes.md). + * `remoteAddress`: A friendly formatted remote address such as a IPv4 or IPv6 address. * `VM2`: Quick log with the following format keys available: * `timestamp`: Log entry timestamp in `quickLogTimestampFormat` format. * `level`: Log entry level from Bunyan. From 39b3aeaedf63a8573220ff954e2d717509ed353c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 4 Aug 2022 12:24:02 -0600 Subject: [PATCH 52/52] Fix bug in ensuring at least default ACS or ACS with 'SC' is used for WFC --- core/wfc.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/wfc.js b/core/wfc.js index 63343b01..fd6305b9 100644 --- a/core/wfc.js +++ b/core/wfc.js @@ -61,11 +61,21 @@ exports.getModule = class WaitingForCallerModule extends MenuModule { extraArgs: options.extraArgs, }); - this.config.acs = this.config.acs || DefaultACS; - if (!this.config.acs.includes('SC')) { + // + // Enforce that we have at least a secure connection in our ACS check + // + this.config.acs = this.config.acs; + if (!this.config.acs) { + this.config.acs = DefaultACS; + } else if (!this.config.acs.includes('SC')) { this.config.acs = 'SC' + this.config.acs; // secure connection at the very least } + // ensure the menu instance has this setting + if (!_.has(options, 'menuConfig.config.acs')) { + _.set(options, 'menuConfig.config.acs', this.config.acs); + } + this.selectedNodeStatusIndex = -1; // no selection this.menuMethods = {