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.
This commit is contained in:
Bryan Ashby 2022-05-11 20:30:25 -06:00
parent 6502f3b55e
commit 868e14aa8e
No known key found for this signature in database
GPG Key ID: C2C1B501E4EFD994
16 changed files with 135 additions and 18 deletions

View File

@ -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}"

View File

@ -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,

View File

@ -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;

View File

@ -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 );
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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)) {

View File

@ -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

View File

@ -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() {

View File

@ -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)) {

View File

@ -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)) {

View File

@ -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
};

View File

@ -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

View File

@ -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()) })
);

View File

@ -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:

View File

@ -79,3 +79,5 @@ 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.
* `newMessagesAddrTo`: Number of new messages **addressed to the current user**.