// ENiGMA½ const { MenuModule } = require('./menu_module'); const stringFormat = require('./string_format'); const { getActiveConnectionList, AllConnections } = 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; // 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, }; const MciViewIds = { main : { nodeStatus : 1, quickLogView : 2, nodeStatusSelection : 3, 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 }); 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.selectedNodeStatusIndex = -1; // no selection 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); }, 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._getNodeByNodeId(nodeStatusView, nodeId); if (index > -1) { this.selectedNodeStatusIndex = index; this._selectNodeByIndex(nodeStatusView, this.selectedNodeStatusIndex); } 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.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) => { 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() { this.client.stopIdleMonitor(); this._applyOpVisibility(); super.enter(); } leave() { _.remove(Log.log.streams, stream => { return stream.name === 'wfc-ringbuffer'; }); this._restoreOpVisibility(); this._stopRefreshing(); this.client.startIdleMonitor(); super.leave(); } _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() { 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; } } _refreshAll(cb) { 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 => { if (cb) { return cb(err); } } ); } _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) || {}; 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.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.totalFileBytes || 0, // totalUploads : // totalUploadBytes : // totalDownloads : // 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), // 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), }; 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); } 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', }); }); // :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 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(); } };