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