diff --git a/core/bbs.js b/core/bbs.js index 3480da93..48260db3 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -149,49 +149,58 @@ function initialize(cb) { function initDatabases(callback) { database.initializeDatabases(callback); }, - function initSystemProperties(callback) { - require('./system_property.js').loadSystemProperties(callback); + function initStatLog(callback) { + require('./stat_log.js').init(callback); }, function initThemes(callback) { // Have to pull in here so it's after Config init - var theme = require('./theme.js'); - theme.initAvailableThemes(function onThemesInit(err, themeCount) { + require('./theme.js').initAvailableThemes(function onThemesInit(err, themeCount) { logger.log.info({ themeCount : themeCount }, 'Themes initialized'); callback(err); }); }, - function loadSysOpInformation(callback) { + function loadSysOpInformation2(callback) { // - // If user 1 has been created, we have a SysOp. Cache some information - // into Config. - // - var user = require('./user.js'); // must late load + // Copy over some +op information from the user DB -> system propertys. + // * Makes this accessible for MCI codes, easy non-blocking access, etc. + // * We do this every time as the op is free to change this information just + // like any other user + // + const user = require('./user.js'); - user.getUserName(1, function unLoaded(err, sysOpUsername) { - if(err) { - callback(null); // non-fatal here - } else { - // - // Load some select properties to cache - // - var propLoadOpts = { - userId : 1, - names : [ 'real_name', 'sex', 'email_address' ], - }; + async.waterfall( + [ + function getOpUserName(next) { + return user.getUserName(1, next); + }, + function getOpProps(opUserName, next) { + const propLoadOpts = { + userId : 1, + names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ], + }; + user.loadProperties(propLoadOpts, (err, opProps) => { + return next(err, opUserName, opProps); + }); + } + ], + (err, opUserName, opProps) => { + const StatLog = require('./stat_log.js'); - user.loadProperties(propLoadOpts, function propsLoaded(err, props) { - if(!err) { - conf.config.general.sysOp = { - username : sysOpUsername, - properties : props, - }; + if(err) { + [ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => { + StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A'); + }); + } else { + opProps.username = opUserName; - logger.log.info( { sysOp : conf.config.general.sysOp }, 'System Operator information cached'); - } - callback(null); // any error is again, non-fatal here - }); + _.each(opProps, (v, k) => { + StatLog.setNonPeristentSystemStat(`sysop_${k}`, v); + }); + } + + return callback(null); } - }); + ); }, function readyMessageNetworkSupport(callback) { require('./msg_network.js').startup(callback); diff --git a/core/database.js b/core/database.js index f8b084e7..b6437cf5 100644 --- a/core/database.js +++ b/core/database.js @@ -100,26 +100,36 @@ function createSystemTables() { dbs.system.run('PRAGMA foreign_keys = ON;'); + // Various stat/event logging - see stat_log.js dbs.system.run( - 'CREATE TABLE IF NOT EXISTS system_property (' + - ' prop_name VARCHAR PRIMARY KEY NOT NULL,' + - ' prop_value VARCHAR NOT NULL' + - ');' - ); + `CREATE TABLE IF NOT EXISTS system_stat ( + stat_name VARCHAR PRIMARY KEY NOT NULL, + stat_value VARCHAR NOT NULL + );` + ); - // - // system_log can round log_timestamp for daily, monthly, etc. - // statistics as well as unique entries. - // -/* dbs.system.run( - 'CREATE TABLE IF NOT EXISTS system_log (' + - ' log_timestamp DATETIME PRIMARY KEY NOT NULL ( ' + - ' log_name VARCHARNOT NULL,' + - ' log_value VARCHAR NOT NULL,' + - ' UNIQUE(log_timestamp, log_name)' + - ');' - );*/ + `CREATE TABLE IF NOT EXISTS system_event_log ( + id INTEGER PRIMARY KEY, + timestamp DATETIME NOT NULL, + log_name VARCHAR NOT NULL, + log_value VARCHAR NOT NULL, + + UNIQUE(timestamp, log_name) + );` + ); + + dbs.system.run( + `CREATE TABLE IF NOT EXISTS user_event_log ( + id INTEGER PRIMARY KEY, + timestamp DATETIME NOT NULL, + user_id INTEGER NOT NULL, + log_name VARCHAR NOT NULL, + log_value VARCHAR NOT NULL, + + UNIQUE(timestamp, user_id, log_name) + );` + ); } function createUserTables() { diff --git a/core/dropfile.js b/core/dropfile.js index b44d7efe..517bc36e 100644 --- a/core/dropfile.js +++ b/core/dropfile.js @@ -2,11 +2,11 @@ 'use strict'; var Config = require('./config.js').config; +const StatLog = require('./stat_log.js'); var fs = require('fs'); var paths = require('path'); var _ = require('lodash'); -var async = require('async'); var moment = require('moment'); var iconv = require('iconv-lite'); @@ -121,7 +121,7 @@ function DropFile(client, fileType) { moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" 'X:\\GEN\\', // "Path to the GEN directory" - Config.general.sysOp.username, // "Sysop's Name (name BBS refers to Sysop as)" + StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)" self.client.user.username, // "Alias name" '00:05', // "Event time (hh:mm)" (note: wat?) 'Y', // "If its an error correcting connection (Y/N)" @@ -143,7 +143,7 @@ function DropFile(client, fileType) { '0', // "Total Doors Opened" '0', // "Total Messages Left" - ].join('\r\n') + '\r\n', 'cp437'); + ].join('\r\n') + '\r\n', 'cp437'); }; this.getDoor32Buffer = function() { @@ -178,7 +178,7 @@ function DropFile(client, fileType) { // // Note that usernames are just used for first/last names here // - var opUn = /[^\s]*/.exec(Config.general.sysOp.username)[0]; + var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0]; var un = /[^\s]*/.exec(self.client.user.username)[0]; var secLevel = self.client.user.getLegacySecurityLevel().toString(); diff --git a/core/fse.js b/core/fse.js index ea8e69b5..c6e93aa0 100644 --- a/core/fse.js +++ b/core/fse.js @@ -1,6 +1,7 @@ /* jslint node: true */ 'use strict'; +// ENiGMA½ const MenuModule = require('../core/menu_module.js').MenuModule; const ViewController = require('../core/view_controller.js').ViewController; const ansi = require('../core/ansi_term.js'); @@ -10,7 +11,10 @@ const getMessageAreaByTag = require('../core/message_area.js').getMessageAreaB const updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId; const getUserIdAndName = require('../core/user.js').getUserIdAndName; const cleanControlCodes = require('../core/string_util.js').cleanControlCodes; +const StatLog = require('./stat_log.js'); + +// deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); @@ -295,6 +299,21 @@ function FullScreenEditorModule(options) { ); }; + this.updateUserStats = function(cb) { + if(Message.isPrivateAreaTag(this.message.areaTag)) { + if(cb) { + return cb(null); + } + } + + StatLog.incrementUserStat( + self.client.user, + 'post_count', + 1, + cb + ); + }; + this.redrawFooter = function(options, cb) { async.waterfall( [ diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 0c120e67..96cb7d98 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -2,12 +2,12 @@ 'use strict'; // ENiGMA½ -const Config = require('./config.js').config; -const Log = require('./logger.js').log; -const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; -const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; -const clientConnections = require('./client_connections.js'); -const sysProp = require('./system_property.js'); +const Config = require('./config.js').config; +const Log = require('./logger.js').log; +const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; +const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; +const clientConnections = require('./client_connections.js'); +const StatLog = require('./stat_log.js'); // deps const packageJson = require('../package.json'); @@ -34,9 +34,14 @@ function getPredefinedMCIValue(client, code) { VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, VN : function version() { return packageJson.version; }, - // :TODO: SysOp username - // :TODO: SysOp real name - + // +op info + SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); }, + SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); }, + SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); }, + SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); }, + SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); }, + SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); }, + // :TODO: op age, web, ????? // // Current user / session @@ -60,6 +65,16 @@ function getPredefinedMCIValue(client, code) { MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, CS : function currentStatus() { return client.currentStatus; }, + PS : function userPostCount() { + const postCount = client.user.properties.post_count || 0; + return postCount.toString(); + }, + PC : function userPostCallRatio() { + const postCount = client.user.properties.post_count || 0; + const callCount = client.user.properties.login_count; + const ratio = ~~((postCount / callCount) * 100); + return `${ratio}%`; + }, MD : function currentMenuDescription() { return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; @@ -73,11 +88,14 @@ function getPredefinedMCIValue(client, code) { const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); return conf ? conf.name : ''; }, - ML : function messageAreaDescription() { const area = getMessageAreaByTag(client.user.properties.message_area_tag); return area ? area.desc : ''; }, + CM : function messageConfDescription() { + const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); + return conf ? conf.desc : ''; + }, SH : function termHeight() { return client.term.termHeight.toString(); }, SW : function termWidth() { return client.term.termWidth.toString(); }, @@ -103,7 +121,7 @@ function getPredefinedMCIValue(client, code) { }, OA : function systemArchitecture() { return os.arch(); }, - SC : function systemCpuModel() { + SC : function systemCpuModel() { // // Clean up CPU strings a bit for better display // @@ -114,11 +132,11 @@ function getPredefinedMCIValue(client, code) { // :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 - // :TODO: Node version/info + NV : function nodeVersion() { return process.version; }, AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, - TC : function totalCalls() { return sysProp.getSystemProperty('login_count').toString(); }, + TC : function totalCalls() { return StatLog.getSystemStat('login_count').toString(); }, // // Special handling for XY diff --git a/core/user.js b/core/user.js index 0f92d81e..7703f8fe 100644 --- a/core/user.js +++ b/core/user.js @@ -358,28 +358,6 @@ User.prototype.persistAllProperties = function(cb) { assert(this.userId > 0); this.persistProperties(this.properties, cb); - - /* - var self = this; - - var stmt = userDb.prepare( - 'REPLACE INTO user_property (user_id, prop_name, prop_value) ' + - 'VALUES (?, ?, ?);'); - - async.each(Object.keys(this.properties), function property(propName, callback) { - stmt.run(self.userId, propName, self.properties[propName], function onRun(err) { - callback(err); - }); - }, function complete(err) { - if(err) { - cb(err); - } else { - stmt.finalize(function finalized() { - cb(null); - }); - } - }); -*/ }; User.prototype.setNewAuthCredentials = function(password, cb) { diff --git a/core/user_login.js b/core/user_login.js index 0c22490c..4c3cfcc6 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -1,16 +1,15 @@ /* jslint node: true */ 'use strict'; -var setClientTheme = require('./theme.js').setClientTheme; -var clientConnections = require('./client_connections.js').clientConnections; -var userDb = require('./database.js').dbs.user; -var sysProp = require('./system_property.js'); -var logger = require('./logger.js'); -var Config = require('./config.js').config; +// ENiGMA½ +const setClientTheme = require('./theme.js').setClientTheme; +const clientConnections = require('./client_connections.js').clientConnections; +const userDb = require('./database.js').dbs.user; +const StatLog = require('./stat_log.js'); +const logger = require('./logger.js'); -var async = require('async'); -var _ = require('lodash'); -var assert = require('assert'); +// deps +const async = require('async'); exports.userLogin = userLogin; @@ -24,8 +23,8 @@ function userLogin(client, username, password, cb) { cb(err); } else { - var now = new Date(); - var user = client.user; + const now = new Date(); + const user = client.user; // // Ensure this user is not already logged in. @@ -60,46 +59,21 @@ function userLogin(client, username, password, cb) { async.parallel( [ - function setTheme(callback) { - setClientTheme(client, user.properties.theme_id); - callback(null); - }, + function setTheme(callback) { + setClientTheme(client, user.properties.theme_id); + callback(null); + }, function updateSystemLoginCount(callback) { - var sysLoginCount = sysProp.getSystemProperty('login_count') || 0; - sysLoginCount = parseInt(sysLoginCount, 10) + 1; - sysProp.persistSystemProperty('login_count', sysLoginCount, callback); + StatLog.incrementSystemStat('login_count', 1, callback); }, function recordLastLogin(callback) { - user.persistProperty('last_login_timestamp', now.toISOString(), function persisted(err) { - callback(err); - }); + StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback); }, function updateUserLoginCount(callback) { - if(!user.properties.login_count) { - user.properties.login_count = 1; - } else { - user.properties.login_count++; - } - - user.persistProperty('login_count', user.properties.login_count, function persisted(err) { - callback(err); - }); + StatLog.incrementUserStat(user, 'login_count', 1, callback); }, function recordLoginHistory(callback) { - userDb.serialize(function serialized() { - userDb.run( - 'INSERT INTO user_login_history (user_id, user_name, timestamp) ' + - 'VALUES(?, ?, ?);', [ user.userId, user.username, now.toISOString() ] - ); - - // keep 30 days of records - userDb.run( - 'DELETE FROM user_login_history ' + - 'WHERE timestamp <= DATETIME("now", "-30 day");' - ); - }); - - callback(null); + StatLog.appendSystemLogEntry('user_login_history', user.userId, 30, callback); } ], function complete(err) { diff --git a/mods/last_callers.js b/mods/last_callers.js index 656794f9..4eb10077 100644 --- a/mods/last_callers.js +++ b/mods/last_callers.js @@ -1,15 +1,17 @@ /* jslint node: true */ 'use strict'; -var MenuModule = require('../core/menu_module.js').MenuModule; -var userDb = require('../core/database.js').dbs.user; -var ViewController = require('../core/view_controller.js').ViewController; -var getSystemLoginHistory = require('../core/stats.js').getSystemLoginHistory; +// ENiGMA½ +const MenuModule = require('../core/menu_module.js').MenuModule; +const ViewController = require('../core/view_controller.js').ViewController; +const StatLog = require('../core/stat_log.js'); +const getUserName = require('../core/user.js').getUserName; +const loadProperties = require('../core/user.js').loadProperties; -var moment = require('moment'); -var async = require('async'); -var assert = require('assert'); -var _ = require('lodash'); +// deps +const moment = require('moment'); +const async = require('async'); +const _ = require('lodash'); /* Available listFormat object members: @@ -41,11 +43,11 @@ function LastCallersModule(options) { require('util').inherits(LastCallersModule, MenuModule); LastCallersModule.prototype.mciReady = function(mciData, cb) { - var self = this; - var vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); + const self = this; + const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - var loginHistory; - var callersView; + let loginHistory; + let callersView; async.series( [ @@ -53,7 +55,7 @@ LastCallersModule.prototype.mciReady = function(mciData, cb) { LastCallersModule.super_.prototype.mciReady.call(self, mciData, callback); }, function loadFromConfig(callback) { - var loadOpts = { + const loadOpts = { callingMenu : self, mciMap : mciData.menu, noInput : true, @@ -64,51 +66,53 @@ LastCallersModule.prototype.mciReady = function(mciData, cb) { function fetchHistory(callback) { callersView = vc.getView(MciCodeIds.CallerList); - getSystemLoginHistory(callersView.dimens.height, function historyRetrieved(err, lh) { + StatLog.getSystemLogEntries('user_login_history', 'timestamp_desc', callersView.dimens.height, (err, lh) => { loginHistory = lh; - callback(err); + return callback(err); }); }, - function fetchUserProperties(callback) { - async.each(loginHistory, function entry(histEntry, next) { - userDb.each( - 'SELECT prop_name, prop_value ' + - 'FROM user_property ' + - 'WHERE user_id=? AND (prop_name="location" OR prop_name="affiliation");', - [ histEntry.userId ], - function propRow(err, propEntry) { - histEntry[propEntry.prop_name] = propEntry.prop_value; - }, - function complete(err) { - next(); - } - ); - }, function complete(err) { - callback(err); - }); + function getUserNamesAndProperties(callback) { + const getPropOpts = { + names : [ 'location', 'affiliation' ] + }; + + const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; + + async.each( + loginHistory, + (item, next) => { + item.userId = parseInt(item.log_value); + item.ts = moment(item.timestamp).format(dateTimeFormat); + + getUserName(item.userId, (err, userName) => { + item.userName = userName; + getPropOpts.userId = item.userId; + + loadProperties(getPropOpts, (err, props) => { + if(!err) { + item.location = props.location; + item.affiliation = item.affils = props.affiliation; + } + return next(); + }); + }); + }, + callback + ); }, function populateList(callback) { - var listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affils} - {ts}'; - var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD'; + const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affils} - {ts}'; - callersView.setItems(_.map(loginHistory, function formatCallEntry(ce) { - return listFormat.format({ - userId : ce.userId, - userName : ce.userName, - ts : moment(ce.timestamp).format(dateTimeFormat), - location : ce.location, - affils : ce.affiliation, - }); - })); + callersView.setItems(_.map(loginHistory, ce => listFormat.format(ce) ) ); // :TODO: This is a hack until pipe codes are better implemented callersView.focusItems = callersView.items; callersView.redraw(); - callback(null); + return callback(null); } ], - function complete(err) { + (err) => { if(err) { self.client.log.error( { error : err.toString() }, 'Error loading last callers'); } diff --git a/mods/msg_area_post_fse.js b/mods/msg_area_post_fse.js index f8ee7742..8a515895 100644 --- a/mods/msg_area_post_fse.js +++ b/mods/msg_area_post_fse.js @@ -38,6 +38,9 @@ function AreaPostFSEModule(options) { }, function saveMessage(callback) { return persistMessage(msg, callback); + }, + function updateStats(callback) { + self.updateUserStats(callback); } ], function complete(err) {