Merge pull request #431 from NuSkooler/216-waiting-for-caller
#216: Initial Waiting for Caller (WFC) Support
This commit is contained in:
commit
a6f7fe40c6
|
@ -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:
|
||||
|
||||
|
|
|
@ -5,7 +5,11 @@ 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).
|
||||
* 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.
|
||||
* Added a system method to support setting the client encoding from menus, `@systemMethod:setClientEncoding`.
|
||||
* 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.
|
||||
|
||||
|
@ -17,6 +21,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))
|
||||
* Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system!
|
||||
* The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information.
|
||||
* Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the `fileBaseListEntries` in the default `file_base.in.hjson` template.
|
||||
|
|
Binary file not shown.
|
@ -246,6 +246,96 @@
|
|||
}
|
||||
}
|
||||
|
||||
mainMenuWaitingForCaller: {
|
||||
config: {
|
||||
// formats
|
||||
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|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|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
|
||||
mainInfoFormat18: "|00|15{lastLoginUserName:<26} |07{lastLogin}"
|
||||
|
||||
// system stats
|
||||
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
|
||||
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: {
|
||||
trace : |00|02T
|
||||
debug: |00|03D
|
||||
info: |00|15I
|
||||
warn: |00|14W
|
||||
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
|
||||
}
|
||||
statusAvailableIndicators: [ "N", "Y" ]
|
||||
statusVisibleIndicators: [ "N", "Y" ]
|
||||
|
||||
nodeStatusSelectionFormat: "|00|07{realName:<12}\n|08- |07{serverName:<10}\n|08- |07{remoteAddress:<10}"
|
||||
}
|
||||
0: {
|
||||
mci: {
|
||||
TL16: {
|
||||
fillChar: .
|
||||
}
|
||||
TL20: { width: 30 }
|
||||
TL22: { width: 30 }
|
||||
TL24: { width: 30 }
|
||||
|
||||
// node status
|
||||
VM1: {
|
||||
height: 5
|
||||
width: 37
|
||||
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
|
||||
VM2: {
|
||||
height: 5
|
||||
width: 73
|
||||
itemFormat: "|00|07{nodeId} {levelIndicator} |02{timestamp} {message:<51.50}"
|
||||
}
|
||||
|
||||
MT3: {
|
||||
mode: preview
|
||||
autoScroll: false
|
||||
height: 5
|
||||
width: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageBaseMessageList: {
|
||||
config: {
|
||||
dateTimeFormat: ddd MMM Do
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
[0m[1;30m>> [0;36mkick node[1;30m?[0m [36m%TM1[1m%TM1[0m
|
|
@ -108,7 +108,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)) {
|
||||
|
@ -179,6 +179,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,
|
||||
|
|
98
core/bbs.js
98
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');
|
||||
|
@ -13,6 +10,7 @@ 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');
|
||||
|
@ -151,7 +149,9 @@ 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;
|
||||
|
@ -257,6 +257,8 @@ function initialize(cb) {
|
|||
//
|
||||
const User = require('./user.js');
|
||||
|
||||
// :TODO: use User.getUserInfo() for this!
|
||||
|
||||
const propLoadOpts = {
|
||||
names: [
|
||||
UserProps.RealName,
|
||||
|
@ -270,7 +272,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(
|
||||
|
@ -301,8 +303,9 @@ function initialize(cb) {
|
|||
}
|
||||
);
|
||||
},
|
||||
function initCallsToday(callback) {
|
||||
function initSystemLogStats(callback) {
|
||||
const StatLog = require('./stat_log.js');
|
||||
|
||||
const filter = {
|
||||
logName: SysLogKeys.UserLoginHistory,
|
||||
resultType: 'count',
|
||||
|
@ -319,6 +322,89 @@ function initialize(cb) {
|
|||
return callback(null);
|
||||
});
|
||||
},
|
||||
function initUserLogStats(callback) {
|
||||
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']],
|
||||
];
|
||||
|
||||
async.each(
|
||||
entries,
|
||||
(entry, nextEntry) => {
|
||||
const [logName, [sysPropName, resultType]] = entry;
|
||||
|
||||
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.setNonPersistentSystemStat(sysPropName, stat);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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 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);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: {
|
||||
|
@ -592,6 +597,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:/, '').replace(/^::1$/, 'localhost');
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Default error handlers
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -21,31 +21,72 @@ exports.getConnectionByNodeId = getConnectionByNodeId;
|
|||
const clientConnections = [];
|
||||
exports.clientConnections = clientConnections;
|
||||
|
||||
function getActiveConnections(authUsersOnly = false) {
|
||||
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 => {
|
||||
return (authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly;
|
||||
if (options.authUsersOnly && !conn.user.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
if (options.visibleOnly && !conn.user.isVisible()) {
|
||||
return false;
|
||||
}
|
||||
if (options.availOnly && !conn.user.isAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function getActiveConnectionList(authUsersOnly) {
|
||||
if (!_.isBoolean(authUsersOnly)) {
|
||||
authUsersOnly = true;
|
||||
}
|
||||
|
||||
function getActiveConnectionList(
|
||||
options = { authUsersOnly: true, visibleOnly: true, availOnly: false }
|
||||
) {
|
||||
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
|
||||
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,
|
||||
isVisible: ac.user.isVisible(),
|
||||
isAvailable: ac.user.isAvailable(),
|
||||
remoteAddress: ac.friendlyRemoteAddress(),
|
||||
};
|
||||
|
||||
//
|
||||
// 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];
|
||||
|
@ -57,6 +98,7 @@ function getActiveConnectionList(authUsersOnly) {
|
|||
);
|
||||
entry.timeOn = moment.duration(diff, 'minutes');
|
||||
}
|
||||
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
@ -81,6 +123,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);
|
||||
|
||||
|
@ -90,6 +141,7 @@ function addNewClient(client, clientSock) {
|
|||
|
||||
const connInfo = {
|
||||
remoteAddress: remoteAddress,
|
||||
friendlyRemoteAddress: client.friendlyRemoteAddress(),
|
||||
serverName: client.session.serverName,
|
||||
isSecure: client.session.isSecure,
|
||||
};
|
||||
|
@ -99,7 +151,10 @@ function addNewClient(client, clientSock) {
|
|||
connInfo.family = clientSock.localFamily;
|
||||
}
|
||||
|
||||
client.log.info(connInfo, 'Client connected');
|
||||
client.log.info(
|
||||
connInfo,
|
||||
`Client connected (${connInfo.serverName}/${connInfo.port})`
|
||||
);
|
||||
|
||||
Events.emit(Events.getSystemEvents().ClientConnected, {
|
||||
client: client,
|
||||
|
@ -143,9 +198,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);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -157,9 +157,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;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -32,7 +32,7 @@ module.exports = class Door {
|
|||
});
|
||||
|
||||
conn.once('error', err => {
|
||||
this.client.log.info(
|
||||
this.client.log.warn(
|
||||
{ error: err.message },
|
||||
'Door socket server connection'
|
||||
);
|
||||
|
@ -77,7 +77,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 {
|
||||
|
|
|
@ -114,7 +114,7 @@ class ScheduledEvent {
|
|||
executeAction(reason, cb) {
|
||||
Log.info(
|
||||
{ eventName: this.name, action: this.action, reason: reason },
|
||||
'Executing scheduled event action...'
|
||||
`Executing scheduled event "${this.name}"...`
|
||||
);
|
||||
|
||||
if ('method' === this.action.type) {
|
||||
|
|
|
@ -344,74 +344,17 @@ exports.getModule = class FileAreaList 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)
|
||||
);
|
||||
|
||||
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) {
|
||||
|
@ -436,7 +379,11 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController(
|
||||
'browse',
|
||||
{ clearScreen: clearScreen },
|
||||
FormIds.browse,
|
||||
{
|
||||
clearScreen: clearScreen,
|
||||
artDataPrep: self.displayArtDataPrepCallback.bind(self),
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
@ -528,7 +475,11 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController(
|
||||
'details',
|
||||
{ clearScreen: true },
|
||||
FormIds.details,
|
||||
{
|
||||
clearScreen: true,
|
||||
artDataPrep: self.displayArtDataPrepCallback.bind(self),
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
@ -778,7 +729,12 @@ exports.getModule = class FileAreaList extends MenuModule {
|
|||
|
||||
return self.displayArtAndPrepViewController(
|
||||
name,
|
||||
{ clearScreen: false, noInput: true },
|
||||
FormIds[name],
|
||||
{
|
||||
clearScreen: false,
|
||||
noInput: true,
|
||||
artDataPrep: self.displayArtDataPrepCallback.bind(self),
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
|
|
@ -534,6 +534,12 @@ class FileAreaWebAccess {
|
|||
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
|
||||
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
|
||||
|
||||
StatLog.incrementNonPersistentSystemStat(SysProps.FileDlTodayCount, 1);
|
||||
StatLog.incrementNonPersistentSystemStat(
|
||||
SysProps.FileDlTodayBytes,
|
||||
dlBytes
|
||||
);
|
||||
|
||||
return callback(null, user);
|
||||
},
|
||||
function sendEvent(user, callback) {
|
||||
|
|
|
@ -194,6 +194,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
|||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController(
|
||||
'queueManager',
|
||||
FormIds.queueManager,
|
||||
{ clearScreen: clearScreen },
|
||||
callback
|
||||
);
|
||||
|
@ -209,59 +210,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);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -187,6 +187,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
|
|||
function prepArtAndViewController(callback) {
|
||||
return self.displayArtAndPrepViewController(
|
||||
'queueManager',
|
||||
FormIds.queueManager,
|
||||
{ clearScreen: clearScreen },
|
||||
callback
|
||||
);
|
||||
|
@ -266,59 +267,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);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -150,7 +150,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
|
|||
|
||||
this.client.log.info(
|
||||
{ sentFiles: sentFiles },
|
||||
`Successfully sent ${sentFiles.length} file(s)`
|
||||
`User "${self.client.user.username}" uploaded ${sentFiles.length} file(s)`
|
||||
);
|
||||
}
|
||||
return cb(err);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,6 +12,7 @@ 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');
|
||||
|
@ -574,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);
|
||||
}
|
||||
|
@ -596,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) {
|
||||
|
@ -606,6 +615,69 @@ 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) {
|
||||
|
@ -613,7 +685,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
}
|
||||
|
||||
if (appendMultiLine && view instanceof MultiLineEditTextView) {
|
||||
view.addText(text);
|
||||
view.setAnsi(text);
|
||||
} else {
|
||||
view.setText(text);
|
||||
}
|
||||
|
@ -650,7 +722,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
textView instanceof MultiLineEditTextView
|
||||
) {
|
||||
textView.addText(text);
|
||||
} else {
|
||||
} else if (textView.getData() != text) {
|
||||
textView.setText(text);
|
||||
}
|
||||
}
|
||||
|
@ -752,4 +824,26 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
@ -233,6 +237,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) {
|
||||
|
|
|
@ -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;
|
||||
|
@ -531,6 +532,36 @@ 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;
|
||||
|
|
|
@ -10,7 +10,14 @@ 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);
|
||||
});
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
|||
subject: msg.subject,
|
||||
uuid: msg.messageUuid,
|
||||
},
|
||||
'Message persisted'
|
||||
`User "${self.client.user.username}" posted message to "${msg.toUserName}" (${msg.areaTag})`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
@ -236,7 +237,7 @@ exports.getModule = class NodeMessageModule extends MenuModule {
|
|||
},
|
||||
]
|
||||
.concat(
|
||||
getActiveConnectionList(true).map(node =>
|
||||
getActiveConnectionList(UserMessageableConnections).map(node =>
|
||||
Object.assign(node, {
|
||||
text: -1 == node.node ? '-ALL-' : node.node.toString(),
|
||||
})
|
||||
|
|
|
@ -130,7 +130,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
|
|||
};
|
||||
newUser.create(createUserInfo, err => {
|
||||
if (err) {
|
||||
self.client.log.info(
|
||||
self.client.log.warn(
|
||||
{ error: err, username: formData.value.username },
|
||||
'New user creation failed'
|
||||
);
|
||||
|
@ -144,7 +144,7 @@ exports.getModule = class NewUserAppModule extends MenuModule {
|
|||
} else {
|
||||
self.client.log.info(
|
||||
{ username: formData.value.username, userId: newUser.userId },
|
||||
'New user created'
|
||||
`New user "${formData.value.username}" created`
|
||||
);
|
||||
|
||||
// Cache SysOp information now
|
||||
|
|
|
@ -19,14 +19,30 @@ 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);
|
||||
},
|
||||
callback => {
|
||||
// by fetching a memory or load we'll force a refresh now
|
||||
StatLog.getSystemStat(SysProps.SystemMemoryStats);
|
||||
return callback(null);
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// :TODO: move this to stat_log.js like system memory is handled
|
||||
function setNextRandomRumor(cb) {
|
||||
StatLog.getSystemLogEntries(
|
||||
SysLogKeys.UserAddedRumorz,
|
||||
|
@ -65,10 +81,6 @@ function userStatAsCountString(client, statName, defaultValue) {
|
|||
return toNumberWithCommas(value);
|
||||
}
|
||||
|
||||
function sysStatAsString(statName, defaultValue) {
|
||||
return (StatLog.getSystemStat(statName) || defaultValue).toLocaleString();
|
||||
}
|
||||
|
||||
const PREDEFINED_MCI_GENERATORS = {
|
||||
//
|
||||
// Board
|
||||
|
@ -104,7 +116,6 @@ const PREDEFINED_MCI_GENERATORS = {
|
|||
SE: function opEmail() {
|
||||
return StatLog.getSystemStat(SysProps.SysOpEmailAddress);
|
||||
},
|
||||
// :TODO: op age, web, ?????
|
||||
|
||||
//
|
||||
// Current user / session
|
||||
|
@ -162,8 +173,8 @@ const PREDEFINED_MCI_GENERATORS = {
|
|||
return client.node.toString();
|
||||
},
|
||||
IP: function clientIpAddress(client) {
|
||||
return client.remoteAddress.replace(/^::ffff:/, '');
|
||||
}, // convert any :ffff: IPv4's to 32bit version
|
||||
return client.friendlyRemoteAddress();
|
||||
},
|
||||
ST: function serverName(client) {
|
||||
return client.session.serverName;
|
||||
},
|
||||
|
@ -272,6 +283,23 @@ 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);
|
||||
},
|
||||
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
|
||||
|
@ -318,15 +346,36 @@ const PREDEFINED_MCI_GENERATORS = {
|
|||
.trim();
|
||||
},
|
||||
|
||||
// :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
|
||||
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();
|
||||
},
|
||||
NV: function nodeVersion() {
|
||||
return process.version;
|
||||
},
|
||||
|
||||
AN: function activeNodes() {
|
||||
return clientConnections.getActiveConnections().length.toString();
|
||||
return clientConnections
|
||||
.getActiveConnections(clientConnections.UserVisibleConnections)
|
||||
.length.toString();
|
||||
},
|
||||
|
||||
TC: function totalCalls() {
|
||||
|
@ -336,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();
|
||||
|
@ -346,17 +408,15 @@ const PREDEFINED_MCI_GENERATORS = {
|
|||
//
|
||||
// System File Base, Up/Download Info
|
||||
//
|
||||
// :TODO: DD - Today's # of downloads (iNiQUiTY)
|
||||
//
|
||||
SD: function systemNumDownloads() {
|
||||
return sysStatAsString(SysProps.FileDlTotalCount, 0);
|
||||
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);
|
||||
return StatLog.getFriendlySystemStat(SysProps.FileUlTotalCount, 0);
|
||||
},
|
||||
SP: function systemByteUpload() {
|
||||
const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTotalBytes);
|
||||
|
@ -373,18 +433,59 @@ const PREDEFINED_MCI_GENERATORS = {
|
|||
},
|
||||
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);
|
||||
},
|
||||
FT: function totalUploadsToday() {
|
||||
// Obv/2
|
||||
return StatLog.getFriendlySystemStat(SysProps.FileUlTodayCount, 0);
|
||||
},
|
||||
FB: function totalUploadBytesToday() {
|
||||
const byteSize = StatLog.getSystemStatNum(SysProps.FileUlTodayBytes);
|
||||
return formatByteSize(byteSize, true); // true=withAbbr
|
||||
},
|
||||
DD: function totalDownloadsToday() {
|
||||
// iNiQUiTY
|
||||
return StatLog.getFriendlySystemStat(SysProps.FileDlTodayCount, 0);
|
||||
},
|
||||
DB: function totalDownloadBytesToday() {
|
||||
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: FT - Files uploaded/added *today* (Obv/2)
|
||||
// :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
|
||||
|
||||
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';
|
||||
},
|
||||
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
|
||||
|
@ -424,7 +525,7 @@ function getPredefinedMCIValue(client, code, extra) {
|
|||
} catch (e) {
|
||||
Log.error(
|
||||
{ code: code, exception: e.message },
|
||||
'Exception caught generating predefined MCI value'
|
||||
`Failed generating predefined MCI value (${code})`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
316
core/stat_log.js
316
core/stat_log.js
|
@ -4,10 +4,15 @@
|
|||
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');
|
||||
const { getActiveConnections, AllConnections } = require('./client_connections');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const SysInfo = require('systeminformation');
|
||||
|
||||
/*
|
||||
System Event Log & Stats
|
||||
|
@ -24,6 +29,7 @@ const moment = require('moment');
|
|||
class StatLog {
|
||||
constructor() {
|
||||
this.systemStats = {};
|
||||
this.lastSysInfoStatsRefresh = 0;
|
||||
}
|
||||
|
||||
init(cb) {
|
||||
|
@ -106,7 +112,17 @@ class StatLog {
|
|||
}
|
||||
|
||||
getSystemStat(statName) {
|
||||
return this.systemStats[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();
|
||||
}
|
||||
|
||||
getSystemStatNum(statName) {
|
||||
|
@ -141,13 +157,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;
|
||||
|
||||
|
@ -239,75 +267,18 @@ class StatLog {
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
Find System Log entries by |filter|:
|
||||
|
||||
filter.logName (required)
|
||||
filter.resultType = (obj) | count
|
||||
where obj contains timestamp and log_value
|
||||
filter.limit
|
||||
filter.date - exact date to filter against
|
||||
filter.order = (timestamp) | timestamp_asc | timestamp_desc | random
|
||||
*/
|
||||
//
|
||||
// Find System Log entry(s) by |filter|:
|
||||
//
|
||||
// - logName: Name of log (required)
|
||||
// - resultType: 'obj' | 'count' (default='obj')
|
||||
// - limit: Limit returned results
|
||||
// - date: exact date to filter against
|
||||
// - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random'
|
||||
// (default='timestamp')
|
||||
//
|
||||
findSystemLogEntries(filter, cb) {
|
||||
filter = filter || {};
|
||||
if (!_.isString(filter.logName)) {
|
||||
return cb(Errors.MissingParam('filter.logName is required'));
|
||||
}
|
||||
|
||||
filter.resultType = filter.resultType || 'obj';
|
||||
filter.order = filter.order || 'timestamp';
|
||||
|
||||
let sql;
|
||||
if ('count' === filter.resultType) {
|
||||
sql = `SELECT COUNT() AS count
|
||||
FROM system_event_log`;
|
||||
} else {
|
||||
sql = `SELECT timestamp, log_value
|
||||
FROM system_event_log`;
|
||||
}
|
||||
|
||||
sql += ' WHERE log_name = ?';
|
||||
|
||||
if (filter.date) {
|
||||
filter.date = moment(filter.date);
|
||||
sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format(
|
||||
'YYYY-MM-DD'
|
||||
)}")`;
|
||||
}
|
||||
|
||||
if ('count' !== filter.resultType) {
|
||||
switch (filter.order) {
|
||||
case 'timestamp':
|
||||
case 'timestamp_asc':
|
||||
sql += ' ORDER BY timestamp ASC';
|
||||
break;
|
||||
|
||||
case 'timestamp_desc':
|
||||
sql += ' ORDER BY timestamp DESC';
|
||||
break;
|
||||
|
||||
case 'random':
|
||||
sql += ' ORDER BY RANDOM()';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isNumber(filter.limit) && 0 !== filter.limit) {
|
||||
sql += ` LIMIT ${filter.limit}`;
|
||||
}
|
||||
|
||||
sql += ';';
|
||||
|
||||
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) => {
|
||||
return cb(err, rows);
|
||||
});
|
||||
}
|
||||
return this._findLogEntries('system_event_log', filter, cb);
|
||||
}
|
||||
|
||||
getSystemLogEntries(logName, order, limit, cb) {
|
||||
|
@ -368,6 +339,211 @@ class StatLog {
|
|||
systemEventUserLogInit(this);
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
//
|
||||
// Find User Log entry(s) by |filter|:
|
||||
//
|
||||
// - logName: Name of log (required)
|
||||
// - userId: User ID in which to restrict entries to (missing=all)
|
||||
// - sessionId: Session ID in which to restrict entries to (missing=any)
|
||||
// - resultType: 'obj' | 'count' (default='obj')
|
||||
// - limit: Limit returned results
|
||||
// - date: exact date to filter against
|
||||
// - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random'
|
||||
// (default='timestamp')
|
||||
//
|
||||
findUserLogEntries(filter, cb) {
|
||||
return this._findLogEntries('user_event_log', filter, cb);
|
||||
}
|
||||
|
||||
_refreshSystemStat(statName) {
|
||||
switch (statName) {
|
||||
case SysProps.SystemLoadStats:
|
||||
case SysProps.SystemMemoryStats:
|
||||
return this._refreshSysInfoStats();
|
||||
|
||||
case SysProps.ProcessTrafficStats:
|
||||
return this._refreshProcessTrafficStats();
|
||||
}
|
||||
}
|
||||
|
||||
_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: parseFloat(
|
||||
_.get(sysInfo, 'currentLoad.avgLoad', 0).toFixed(2)
|
||||
),
|
||||
current: parseFloat(
|
||||
_.get(sysInfo, 'currentLoad.currentLoad', 0).toFixed(2)
|
||||
),
|
||||
};
|
||||
|
||||
this.setNonPersistentSystemStat(SysProps.SystemLoadStats, loadStats);
|
||||
})
|
||||
.catch(err => {
|
||||
// :TODO: log me
|
||||
});
|
||||
}
|
||||
|
||||
_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:
|
||||
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)) {
|
||||
return cb(Errors.MissingParam('filter.logName is required'));
|
||||
}
|
||||
|
||||
filter.resultType = filter.resultType || 'obj';
|
||||
filter.order = filter.order || 'timestamp';
|
||||
|
||||
let sql;
|
||||
if ('count' === filter.resultType) {
|
||||
sql = `SELECT COUNT() AS count
|
||||
FROM ${logTable}`;
|
||||
} else {
|
||||
sql = `SELECT timestamp, log_value
|
||||
FROM ${logTable}`;
|
||||
}
|
||||
|
||||
sql += ' WHERE log_name = ?';
|
||||
|
||||
if (_.isNumber(filter.userId)) {
|
||||
sql += ` AND user_id = ${filter.userId}`;
|
||||
}
|
||||
|
||||
if (filter.sessionId) {
|
||||
sql += ` AND session_id = ${filter.sessionId}`;
|
||||
}
|
||||
|
||||
if (filter.date) {
|
||||
filter.date = moment(filter.date);
|
||||
sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format(
|
||||
'YYYY-MM-DD'
|
||||
)}")`;
|
||||
}
|
||||
|
||||
if ('count' !== filter.resultType) {
|
||||
switch (filter.order) {
|
||||
case 'timestamp':
|
||||
case 'timestamp_asc':
|
||||
sql += ' ORDER BY timestamp ASC';
|
||||
break;
|
||||
|
||||
case 'timestamp_desc':
|
||||
sql += ' ORDER BY timestamp DESC';
|
||||
break;
|
||||
|
||||
case 'random':
|
||||
sql += ' ORDER BY RANDOM()';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isNumber(filter.limit) && 0 !== filter.limit) {
|
||||
sql += ` LIMIT ${filter.limit}`;
|
||||
}
|
||||
|
||||
sql += ';';
|
||||
|
||||
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) => {
|
||||
return cb(err, rows);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new StatLog();
|
||||
|
|
|
@ -380,7 +380,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]}`;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -30,6 +31,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);
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
//
|
||||
module.exports = {
|
||||
UserAddedRumorz: 'system_rumorz',
|
||||
UserLoginHistory: 'user_login_history',
|
||||
UserLoginHistory: 'user_login_history', // JSON object
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
@ -17,17 +18,27 @@ 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
|
||||
|
||||
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',
|
||||
|
||||
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
|
||||
};
|
||||
|
|
|
@ -223,7 +223,7 @@ exports.getModule = class TelnetBridgeModule extends MenuModule {
|
|||
);
|
||||
|
||||
if (err) {
|
||||
self.client.log.info(
|
||||
self.client.log.warn(
|
||||
`Telnet bridge connection error: ${err.message}`
|
||||
);
|
||||
}
|
||||
|
|
|
@ -373,6 +373,22 @@ exports.ThemeManager = class ThemeManager {
|
|||
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
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -380,7 +396,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
|
||||
});
|
||||
|
@ -635,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,
|
||||
|
|
91
core/user.js
91
core/user.js
|
@ -20,10 +20,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;
|
||||
|
@ -31,6 +27,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 +70,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;
|
||||
}
|
||||
|
@ -125,6 +130,30 @@ 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;
|
||||
}
|
||||
|
||||
setAvailability(available) {
|
||||
if (available) {
|
||||
this.statusFlags &= ~User.StatusFlags.NotAvailable;
|
||||
} else {
|
||||
this.statusFlags |= User.StatusFlags.NotAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
setVisibility(visible) {
|
||||
if (visible) {
|
||||
this.statusFlags &= ~User.StatusFlags.NotVisible;
|
||||
} else {
|
||||
this.statusFlags |= User.StatusFlags.NotVisible;
|
||||
}
|
||||
}
|
||||
|
||||
getLegacySecurityLevel() {
|
||||
if (this.isRoot() || this.isGroupMember('sysops')) {
|
||||
return 100;
|
||||
|
@ -703,6 +732,47 @@ 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;
|
||||
}
|
||||
|
@ -835,6 +905,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 = [];
|
||||
|
||||
|
|
|
@ -139,11 +139,14 @@ 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,
|
||||
|
@ -155,7 +158,7 @@ exports.getModule = class UserConfigModule extends MenuModule {
|
|||
);
|
||||
} else {
|
||||
self.client.log.info(
|
||||
'User changed authentication credentials'
|
||||
`User "${self.client.user.username}" updated authentication credentials`
|
||||
);
|
||||
}
|
||||
return self.prevMenu(cb);
|
||||
|
|
|
@ -26,7 +26,12 @@ module.exports = class UserInterruptQueue {
|
|||
omitNodes = [opts.omit];
|
||||
}
|
||||
omitNodes = omitNodes.map(n => (_.isNumber(n) ? n : n.node));
|
||||
opts.clients = getActiveConnections(true).filter(
|
||||
const connOpts = {
|
||||
authUsersOnly: true,
|
||||
visibleOnly: true,
|
||||
availOnly: true,
|
||||
};
|
||||
opts.clients = getActiveConnections(connOpts).filter(
|
||||
ac => !omitNodes.includes(ac.node)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ const { getFileAreaByTag, getDefaultFileAreaTag } = require('./file_base_area.js
|
|||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const assert = require('assert');
|
||||
const moment = require('moment');
|
||||
|
||||
exports.userLogin = userLogin;
|
||||
exports.recordLogin = recordLogin;
|
||||
|
@ -40,7 +41,7 @@ function userLogin(client, username, password, options, cb) {
|
|||
if (config.users.badUserNames.includes(username.toLowerCase())) {
|
||||
client.log.info(
|
||||
{ username, ip: client.remoteAddress },
|
||||
'Attempt to login with banned username'
|
||||
`Attempt to login with banned username "${username}"`
|
||||
);
|
||||
|
||||
// slow down a bit to thwart brute force attacks
|
||||
|
@ -78,13 +79,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,
|
||||
},
|
||||
'Already logged in'
|
||||
`User "${user.username}" already logged in on node ${existingClientConnection.node}`
|
||||
);
|
||||
|
||||
return cb(
|
||||
|
@ -102,7 +103,7 @@ function userLogin(client, username, password, options, cb) {
|
|||
username: user.username,
|
||||
});
|
||||
|
||||
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
|
||||
|
@ -187,6 +188,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 => {
|
||||
|
@ -197,7 +200,7 @@ function recordLogin(client, cb) {
|
|||
return StatLog.setUserStat(
|
||||
user,
|
||||
UserProps.LastLoginTs,
|
||||
StatLog.now,
|
||||
loginTimestamp,
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
@ -219,6 +222,24 @@ function recordLogin(client, cb) {
|
|||
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 => {
|
||||
return cb(err);
|
||||
|
@ -234,9 +255,9 @@ function transformLoginError(err, client, username) {
|
|||
err = Errors.BadLogin('To many failed login attempts', ErrorReasons.TooMany);
|
||||
}
|
||||
|
||||
client.log.info(
|
||||
client.log.warn(
|
||||
{ username, ip: client.remoteAddress, reason: err.message },
|
||||
'Failed login attempt'
|
||||
`Failed login attempt for user "${username}", ${client.friendlyRemoteAddress()}`
|
||||
);
|
||||
return err;
|
||||
}
|
||||
|
|
|
@ -59,6 +59,8 @@ module.exports = {
|
|||
|
||||
MinutesOnlineTotalCount: 'minutes_online_total_count',
|
||||
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -48,13 +50,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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -100,7 +100,7 @@ function VerticalMenuView(options) {
|
|||
}
|
||||
|
||||
text = `${sgr}${strUtil.pad(
|
||||
text,
|
||||
`${text}${this.styleSGR1}`,
|
||||
this.dimens.width,
|
||||
this.fillChar,
|
||||
this.justify
|
||||
|
@ -108,6 +108,18 @@ 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);
|
||||
|
@ -150,6 +162,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) {
|
||||
|
@ -174,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.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
|
||||
|
@ -214,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;
|
||||
};
|
||||
|
||||
|
@ -392,3 +412,11 @@ VerticalMenuView.prototype.setItemSpacing = function (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);
|
||||
};
|
||||
|
|
|
@ -130,7 +130,7 @@ View.prototype.setPosition = function (pos) {
|
|||
//
|
||||
// Allow the following forms: [row, col], { row : r, col : c }, or (row, col)
|
||||
//
|
||||
if (util.isArray(pos)) {
|
||||
if (Array.isArray(pos)) {
|
||||
this.position.row = pos[0];
|
||||
this.position.col = pos[1];
|
||||
} else if (_.isNumber(pos.row) && _.isNumber(pos.col)) {
|
||||
|
|
|
@ -281,9 +281,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];
|
||||
|
@ -303,14 +301,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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,626 @@
|
|||
// ENiGMA½
|
||||
const { MenuModule } = require('./menu_module');
|
||||
const stringFormat = require('./string_format');
|
||||
|
||||
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
|
||||
//
|
||||
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 = {
|
||||
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._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.getItems()[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.getItems()[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();
|
||||
super.enter();
|
||||
}
|
||||
|
||||
leave() {
|
||||
_.remove(Log.log.streams, stream => {
|
||||
return stream.name === 'wfc-ringbuffer';
|
||||
});
|
||||
|
||||
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() {
|
||||
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 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.totalFileBytes || 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.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
|
||||
),
|
||||
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.getItems()[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();
|
||||
}
|
||||
};
|
|
@ -3,7 +3,10 @@
|
|||
|
||||
// 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
|
||||
|
@ -49,12 +52,14 @@ exports.getModule = class WhosOnlineModule extends MenuModule {
|
|||
);
|
||||
}
|
||||
|
||||
const onlineList = getActiveConnectionList(true)
|
||||
const onlineList = getActiveConnectionList(UserVisibleConnections)
|
||||
.slice(0, onlineListView.height)
|
||||
.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...
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
@ -88,9 +88,29 @@ There are many predefined MCI codes that can be used anywhere on the system (pla
|
|||
| `SU` | Total uploads, system wide |
|
||||
| `SP` | Total uploaded amount, system wide (formatted to appropriate bytes/megs/etc.) |
|
||||
| `TF` | Total number of files on the system |
|
||||
| `TB` | Total amount of files on the system (formatted to appropriate bytes/megs/gigs/etc.) |
|
||||
| `TB` | Total file base size (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* |
|
||||
| `FT` | Total number of uploads to the system *today* |
|
||||
| `FB` | Total upload amount *today* (formatted to appropriate bytes/megs/etc. ) |
|
||||
| `DD` | Total number of downloads from the system *today* |
|
||||
| `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)<br>(May not be available on some platforms) |
|
||||
| `CL` | System current load percentage<br>(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 |
|
||||
| `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 |
|
||||
| `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:
|
||||
|
||||
|
@ -103,7 +123,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.
|
||||
|
||||
|
@ -129,7 +149,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.
|
||||
|
@ -235,4 +255,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".
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ A horizontal menu view supports displaying a list of times on a screen horizonta
|
|||
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:<path>` where `<path>` 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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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**.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -40,9 +40,9 @@ 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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 |
|
||||
|-------------|----------|-------------|
|
||||
|
|
|
@ -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.
|
|
@ -10,7 +10,7 @@ All message network configuration occurs under the `messageNetworks.<name>` bloc
|
|||
1. `messageNetworks.<name>.networks`: Global/general configuration for a particular network where `<name>` is for example `ftn` or `qwk`.
|
||||
2. `messageNetworks.<name>.areas`: Provides mapping of ENiGMA½ **area tags** to their external counterparts.
|
||||
|
||||
:information_source: A related section under `scannerTossers.<name>` 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.<name>` 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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
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. 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)<br/>
|
||||
|
||||
## 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.
|
||||
|
||||
## 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+](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. 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
|
||||
mainMenuWaitingForCaller: {
|
||||
config: {
|
||||
// initial +op over secure connection only
|
||||
acs: SCID1GM[sysops]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> :lock: ENiGMA½ will enforce ACS of at least `SC` (secure connection)
|
||||
|
||||
## Theming
|
||||
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).
|
||||
* `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.
|
||||
* `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.
|
||||
* `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.
|
||||
* `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).
|
||||
* `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.
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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`:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -73,8 +73,8 @@
|
|||
|
||||
menus: {
|
||||
mainMenu: {
|
||||
art: MMENU
|
||||
desc: Main Menu
|
||||
art: MMENU
|
||||
prompt: menuCommand
|
||||
config: {
|
||||
font: cp437
|
||||
|
@ -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
|
||||
|
@ -414,6 +488,7 @@
|
|||
}
|
||||
|
||||
mainMenuUserConfig: {
|
||||
desc: User Config
|
||||
module: user_config
|
||||
art: CONFSCR
|
||||
form: {
|
||||
|
@ -504,7 +579,7 @@
|
|||
}
|
||||
|
||||
mainMenuGlobalNewScan: {
|
||||
desc: Performing New Scan
|
||||
desc: New Scan
|
||||
module: new_scan
|
||||
art: NEWSCAN
|
||||
config: {
|
||||
|
@ -513,7 +588,7 @@
|
|||
}
|
||||
|
||||
mainMenuFeedbackToSysOp: {
|
||||
desc: Feedback to SysOp
|
||||
desc: SysOp Feedback
|
||||
module: msg_area_post_fse
|
||||
config: {
|
||||
art: {
|
||||
|
@ -802,7 +877,7 @@
|
|||
}
|
||||
|
||||
bbsList: {
|
||||
desc: Viewing BBS List
|
||||
desc: BBS List
|
||||
module: bbs_list
|
||||
config: {
|
||||
cls: true
|
||||
|
@ -920,8 +995,8 @@
|
|||
}
|
||||
|
||||
fullLogoffSequencePreAd: {
|
||||
art: PRELOGAD
|
||||
desc: Logging Off
|
||||
art: PRELOGAD
|
||||
next: fullLogoffSequenceRandomBoardAd
|
||||
config: {
|
||||
cls: true
|
||||
|
@ -930,8 +1005,8 @@
|
|||
}
|
||||
|
||||
fullLogoffSequenceRandomBoardAd: {
|
||||
art: OTHRBBS
|
||||
desc: Logging Off
|
||||
art: OTHRBBS
|
||||
next: logoff
|
||||
config: {
|
||||
baudRate: 57600
|
||||
|
@ -941,8 +1016,8 @@
|
|||
}
|
||||
|
||||
logoff: {
|
||||
art: LOGOFF
|
||||
desc: Logging Off
|
||||
art: LOGOFF
|
||||
next: @systemMethod:logoff
|
||||
}
|
||||
|
||||
|
@ -1003,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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -58,7 +58,8 @@
|
|||
"uuid": "8.3.2",
|
||||
"uuid-parse": "1.1.0",
|
||||
"ws": "7.4.3",
|
||||
"yazl": "^2.5.1"
|
||||
"yazl": "^2.5.1",
|
||||
"systeminformation": "^5.11.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.13.0",
|
||||
|
|
|
@ -1831,6 +1831,11 @@ supports-color@^7.1.0:
|
|||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
|
||||
|
|
Loading…
Reference in New Issue