671 lines
22 KiB
JavaScript
671 lines
22 KiB
JavaScript
// ENiGMA½
|
|
const { MenuModule } = require('./menu_module');
|
|
const stringFormat = require('./string_format');
|
|
const Events = require('./events');
|
|
|
|
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');
|
|
const { pipeToAnsi } = require('./color_codes');
|
|
const MultiLineEditTextView =
|
|
require('./multi_line_edit_text_view').MultiLineEditTextView;
|
|
|
|
// deps
|
|
const async = require('async');
|
|
const _ = require('lodash');
|
|
const moment = require('moment');
|
|
const bunyan = require('bunyan');
|
|
|
|
exports.moduleInfo = {
|
|
name: 'WFC',
|
|
desc: 'Semi-Traditional Waiting For Caller',
|
|
author: 'NuSkooler',
|
|
};
|
|
|
|
const FormIds = {
|
|
main: 0,
|
|
help: 1,
|
|
fullLog: 2,
|
|
confirmKickPrompt: 3,
|
|
};
|
|
|
|
const MciViewIds = {
|
|
main: {
|
|
nodeStatus: 1,
|
|
quickLogView: 2,
|
|
selectedNodeStatusInfo: 3,
|
|
confirmXy: 4,
|
|
|
|
customRangeStart: 10,
|
|
},
|
|
};
|
|
|
|
// 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) {
|
|
super(options);
|
|
|
|
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
|
|
extraArgs: options.extraArgs,
|
|
});
|
|
|
|
//
|
|
// Enforce that we have at least a secure connection in our ACS check
|
|
//
|
|
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.refreshing = false;
|
|
|
|
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);
|
|
this.visibilityToggled = true; // we won't restore it in this case
|
|
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 nodeId = parseInt(formData.ch); // 1-based
|
|
if (isNaN(nodeId)) {
|
|
return cb(null);
|
|
}
|
|
|
|
const index = this._getNodeStatusIndexByNodeId(nodeStatusView, nodeId);
|
|
if (index > -1) {
|
|
this.selectedNodeStatusIndex = index;
|
|
this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex);
|
|
|
|
const nodeStatusSelectionView = this.getView(
|
|
'main',
|
|
MciViewIds.main.selectedNodeStatusInfo
|
|
);
|
|
|
|
if (nodeStatusSelectionView) {
|
|
const item = nodeStatusView.getItem(index);
|
|
this._updateNodeStatusSelection(nodeStatusSelectionView, item);
|
|
}
|
|
}
|
|
|
|
return cb(null);
|
|
},
|
|
kickSelectedNode: (formData, extraArgs, cb) => {
|
|
return this._confirmKickSelectedNode(cb);
|
|
},
|
|
kickNodeYes: (formData, extraArgs, cb) => {
|
|
return this._kickSelectedNode(cb);
|
|
},
|
|
kickNodeNo: (formData, extraArgs, cb) => {
|
|
//this._startRefreshing();
|
|
return cb(null);
|
|
},
|
|
};
|
|
}
|
|
|
|
initSequence() {
|
|
async.series(
|
|
[
|
|
callback => {
|
|
return this.beforeArt(callback);
|
|
},
|
|
callback => {
|
|
return this._displayMainPage(false, callback);
|
|
},
|
|
],
|
|
() => {
|
|
this.finishedLoading();
|
|
}
|
|
);
|
|
}
|
|
|
|
_displayMainPage(clearScreen, cb) {
|
|
async.series(
|
|
[
|
|
callback => {
|
|
return this.displayArtAndPrepViewController(
|
|
'main',
|
|
FormIds.main,
|
|
{ clearScreen },
|
|
callback
|
|
);
|
|
},
|
|
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
|
|
|
|
this.logRingBuffer = new bunyan.RingBuffer({
|
|
limit: quickLogView.dimens.height || 24,
|
|
});
|
|
Log.log.addStream({
|
|
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.selectedNodeStatusInfo
|
|
);
|
|
|
|
if (nodeStatusView && nodeStatusSelectionView) {
|
|
nodeStatusView.on('index update', index => {
|
|
const item = nodeStatusView.getItem(index);
|
|
this._updateNodeStatusSelection(
|
|
nodeStatusSelectionView,
|
|
item
|
|
);
|
|
});
|
|
}
|
|
|
|
return callback(null);
|
|
},
|
|
callback => {
|
|
return this._refreshAll(callback);
|
|
},
|
|
],
|
|
err => {
|
|
if (!err) {
|
|
this._startRefreshing();
|
|
}
|
|
return cb(err);
|
|
}
|
|
);
|
|
}
|
|
|
|
enter() {
|
|
this.client.stopIdleMonitor();
|
|
this._applyOpVisibility();
|
|
|
|
Events.on(
|
|
Events.getSystemEvents().ClientDisconnected,
|
|
this._clientDisconnected.bind(this)
|
|
);
|
|
super.enter();
|
|
}
|
|
|
|
leave() {
|
|
_.remove(Log.log.streams, stream => {
|
|
return stream.name === 'wfc-ringbuffer';
|
|
});
|
|
|
|
Events.removeListener(
|
|
Events.getSystemEvents().ClientDisconnected,
|
|
this._clientDisconnected.bind(this)
|
|
);
|
|
|
|
this._restoreOpVisibility();
|
|
|
|
this._stopRefreshing();
|
|
this.client.startIdleMonitor();
|
|
|
|
super.leave();
|
|
}
|
|
|
|
_updateNodeStatusSelection(nodeStatusSelectionView, item) {
|
|
if (item) {
|
|
const nodeStatusSelectionFormat =
|
|
this.config.nodeStatusSelectionFormat || '{text}';
|
|
|
|
const s = stringFormat(nodeStatusSelectionFormat, item);
|
|
|
|
if (nodeStatusSelectionView instanceof MultiLineEditTextView) {
|
|
nodeStatusSelectionView.setAnsi(pipeToAnsi(s, this.client));
|
|
} else {
|
|
nodeStatusSelectionView.setText(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
_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) {
|
|
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();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
_restoreOpVisibility() {
|
|
if (!this.visibilityToggled) {
|
|
this.client.user.setVisibility(this.restoreUserIsVisible);
|
|
}
|
|
}
|
|
|
|
_startRefreshing() {
|
|
if (this.mainRefreshTimer) {
|
|
this._stopRefreshing();
|
|
}
|
|
|
|
this.mainRefreshTimer = setInterval(() => {
|
|
this._refreshAll();
|
|
}, MainStatRefreshTimeMs);
|
|
}
|
|
|
|
_stopRefreshing() {
|
|
if (this.mainRefreshTimer) {
|
|
clearInterval(this.mainRefreshTimer);
|
|
delete this.mainRefreshTimer;
|
|
}
|
|
}
|
|
|
|
_clientDisconnected() {
|
|
const nodeStatusSelectionView = this.getView(
|
|
'main',
|
|
MciViewIds.main.selectedNodeStatusInfo
|
|
);
|
|
if (nodeStatusSelectionView) {
|
|
nodeStatusSelectionView.setText('');
|
|
}
|
|
|
|
this.selectedNodeStatusIndex = 0; // will select during refresh
|
|
this._refreshAll();
|
|
|
|
// have to update the selection view here
|
|
if (nodeStatusSelectionView) {
|
|
const nodeStatusView = this.getView('main', MciViewIds.main.nodeStatus);
|
|
if (nodeStatusView) {
|
|
const item = nodeStatusView.getItem(this.selectedNodeStatusIndex);
|
|
this._updateNodeStatusSelection(nodeStatusSelectionView, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
_refreshAll(cb) {
|
|
if (this.refreshing) {
|
|
if (cb) {
|
|
return cb(null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.refreshing = true;
|
|
|
|
async.series(
|
|
[
|
|
callback => {
|
|
return this._refreshStats(callback);
|
|
},
|
|
callback => {
|
|
return this._refreshNodeStatus(callback);
|
|
},
|
|
callback => {
|
|
return this._refreshQuickLog(callback);
|
|
},
|
|
callback => {
|
|
this.updateCustomViewTextsWithFilter(
|
|
'main',
|
|
MciViewIds.main.customRangeStart,
|
|
this.stats
|
|
);
|
|
return callback(null);
|
|
},
|
|
],
|
|
err => {
|
|
this.refreshing = false;
|
|
if (cb) {
|
|
return cb(err);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
_getStatusStrings(isAvailable, isVisible) {
|
|
const availIndicators = Array.isArray(this.config.statusAvailableIndicators)
|
|
? this.config.statusAvailableIndicators
|
|
: this.client.currentTheme.helpers.getStatusAvailIndicators();
|
|
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) || {};
|
|
const sysLoadStats = StatLog.getSystemStat(SysProps.SystemLoadStats) || {};
|
|
const lastLoginStats = StatLog.getSystemStat(SysProps.LastLogin);
|
|
const processTrafficStats =
|
|
StatLog.getSystemStat(SysProps.ProcessTrafficStats) || {};
|
|
|
|
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()),
|
|
nowTime: now.format(this.getTimeFormat()),
|
|
now: now.format(this._dateTimeFormat('now')),
|
|
|
|
// Current process (our Node.js service)
|
|
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.totalBytes || 0,
|
|
|
|
// 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),
|
|
|
|
// Current
|
|
currentUserName: this.client.user.username,
|
|
currentUserRealName: this.client.user.realName(false) || 'N/A',
|
|
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
|
|
),
|
|
processBytesIngress: processTrafficStats.ingress || 0,
|
|
processBytesEgress: processTrafficStats.egress || 0,
|
|
};
|
|
|
|
return cb(null);
|
|
}
|
|
|
|
_getNodeStatusIndexByNodeId(nodeStatusView, nodeId) {
|
|
return nodeStatusView.getItems().findIndex(entry => entry.node == nodeId);
|
|
}
|
|
|
|
_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) {
|
|
return cb(null);
|
|
}
|
|
|
|
const nodeStatusItems = getActiveConnectionList(AllConnections)
|
|
.slice(0, nodeStatusView.dimens.height)
|
|
.map(ac => {
|
|
// Handle pre-authenticated
|
|
if (!ac.authenticated) {
|
|
ac.text = ac.userName = '*Pre Auth*';
|
|
ac.action = 'Logging In';
|
|
}
|
|
|
|
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',
|
|
});
|
|
});
|
|
|
|
// 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.getItem(0);
|
|
this._updateNodeStatusSelection(nodeStatusSelectionView, item);
|
|
}
|
|
}
|
|
|
|
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[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(level),
|
|
nodeId: rec.nodeId || '*',
|
|
sessionId: rec.sessionId || '',
|
|
message: prefixMssage(rec.msg, level),
|
|
};
|
|
});
|
|
|
|
quickLogView.setItems(logItems);
|
|
quickLogView.redraw();
|
|
|
|
return cb(null);
|
|
}
|
|
|
|
_dateTimeFormat(element) {
|
|
const format = this.config[`${element}DateTimeFormat`];
|
|
return format || this.getDateFormat();
|
|
}
|
|
};
|