From 52585c78f04b21d7d75cc56325a94b60cbb9d10f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 21 Jul 2018 14:32:06 -0600 Subject: [PATCH] Major changes around events, event log, etc. * User event log is now functional & attached to various events * Add additional missing system events * Completely re-write last_callers to have new functionality, etc. * Events.addListenerMultipleEvents() * New 'moduleInitialize' export for module init vs Event specific registerEvents * Add docs on last_callers mod --- UPGRADE.md | 2 + WHATSNEW.md | 2 + core/abracadabra.js | 5 +- core/bbs.js | 6 + core/config.js | 2 +- core/database.js | 13 +- core/events.js | 46 ++---- core/fse.js | 46 +++--- core/last_callers.js | 286 ++++++++++++++++++++++------------- core/mask_edit_text_view.js | 3 +- core/module_util.js | 54 +++++++ core/nua.js | 6 +- core/stat_log.js | 41 ++++- core/user.js | 11 +- core/user_login.js | 6 +- docs/modding/last-callers.md | 34 +++++ 16 files changed, 392 insertions(+), 171 deletions(-) create mode 100644 docs/modding/last-callers.md diff --git a/UPGRADE.md b/UPGRADE.md index 25fe477f..956789c3 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -57,6 +57,8 @@ webSocket: { proxied: true // X-Forwarded-Proto: https support } ``` +* The module export `registerEvents` has been deprecated. If you have a module that depends on this, use the new more generic `moduleInitialize` export instead. +* The `system.db` `user_event_log` table has been updated to include a unique session ID. Previously this table was not used, but you will need to perform a slight maintenance task before it can be properly used. After updating to `0.0.9-alpha`, please run the following: `sqlite3 db/system.db DROP TABLE user_event_log;`. The new table format will be created and used at startup. # 0.0.7-alpha to 0.0.8-alpha diff --git a/WHATSNEW.md b/WHATSNEW.md index bedc54e4..5e92784e 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -16,6 +16,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Ability to delete from personal mailbox (finally!) * Add ability to skip file and/or message areas during newscan. Set config.omitFileAreaTags and config.omitMessageAreaTags in new_scan configuration of your menu.hjson * `{userName}` (sanatized) and `{userNameRaw}` as well as `{cwd}` have been added to param options when launching a door. +* Any module may now register for a system startup intiialization via the `initializeModules(initInfo, cb)` export. +* User event log is now functional. Various events a user performs will be persisted to the `system.db` `user_event_log` table for up to 90 days. An example usage can be found in the updated `last_callers` module where events are turned into Ami/X style actions. Please see `UPGRADE.md`! ## 0.0.8-alpha diff --git a/core/abracadabra.js b/core/abracadabra.js index c0bb2c83..9ada9e5a 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -1,11 +1,12 @@ /* jslint node: true */ 'use strict'; -const MenuModule = require('./menu_module.js').MenuModule; +const { MenuModule } = require('./menu_module.js'); const DropFile = require('./dropfile.js'); const Door = require('./door.js'); const theme = require('./theme.js'); const ansi = require('./ansi_term.js'); +const Events = require('./events.js'); const async = require('async'); const assert = require('assert'); @@ -145,6 +146,8 @@ exports.getModule = class AbracadabraModule extends MenuModule { } runDoor() { + Events.emit(Events.getSystemEvents().UserRunDoor, { user : this.client.user } ); + this.client.term.write(ansi.resetScreen()); const exeInfo = { diff --git a/core/bbs.js b/core/bbs.js index 4436eb02..f3d59200 100644 --- a/core/bbs.js +++ b/core/bbs.js @@ -267,6 +267,9 @@ function initialize(cb) { function readyEvents(callback) { return require('./events.js').startup(callback); }, + function genericModulesInit(callback) { + return require('./module_util.js').initializeModules(callback); + }, function listenConnections(callback) { return require('./listening_server.js').startup(callback); }, @@ -286,6 +289,9 @@ function initialize(cb) { initServices.eventScheduler = modInst; return callback(err); }); + }, + function listenUserEventsForStatLog(callback) { + return require('./stat_log.js').initUserEvents(callback); } ], function onComplete(err) { diff --git a/core/config.js b/core/config.js index 9c33f38f..3d7c6d70 100644 --- a/core/config.js +++ b/core/config.js @@ -147,7 +147,7 @@ function getDefaultConfig() { users : { usernameMin : 2, usernameMax : 16, // Note that FidoNet wants 36 max - usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ ]+$', + usernamePattern : '^[A-Za-z0-9~!@#$%^&*()\\-\\_+ .]+$', passwordMin : 6, passwordMax : 128, diff --git a/core/database.js b/core/database.js index ba48b404..8bae6a87 100644 --- a/core/database.js +++ b/core/database.js @@ -18,6 +18,7 @@ const dbs = {}; exports.getTransactionDatabase = getTransactionDatabase; exports.getModDatabasePath = getModDatabasePath; +exports.loadDatabaseForMod = loadDatabaseForMod; exports.getISOTimestampString = getISOTimestampString; exports.sanatizeString = sanatizeString; exports.initializeDatabases = initializeDatabases; @@ -55,6 +56,15 @@ function getModDatabasePath(moduleInfo, suffix) { return paths.join(conf.config.paths.modsDb, `${full}.sqlite3`); } +function loadDatabaseForMod(modInfo, cb) { + const db = getTransactionDatabase(new sqlite3.Database( + getModDatabasePath(modInfo), + err => { + return cb(err, db); + } + )); +} + function getISOTimestampString(ts) { ts = ts || moment(); return ts.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); @@ -131,10 +141,11 @@ const DB_INIT_TABLE = { id INTEGER PRIMARY KEY, timestamp DATETIME NOT NULL, user_id INTEGER NOT NULL, + session_id VARCHAR NOT NULL, log_name VARCHAR NOT NULL, log_value VARCHAR NOT NULL, - UNIQUE(timestamp, user_id, log_name) + UNIQUE(timestamp, user_id, session_id, log_name) );` ); diff --git a/core/events.js b/core/events.js index 6284597e..73253fe3 100644 --- a/core/events.js +++ b/core/events.js @@ -1,20 +1,14 @@ /* jslint node: true */ 'use strict'; -const paths = require('path'); const events = require('events'); const Log = require('./logger.js').log; const SystemEvents = require('./system_events.js'); -// deps -const _ = require('lodash'); -const async = require('async'); -const glob = require('glob'); - module.exports = new class Events extends events.EventEmitter { constructor() { super(); - this.setMaxListeners(32); // :TODO: play with this... + this.setMaxListeners(64); // :TODO: play with this... } getSystemEvents() { @@ -41,39 +35,21 @@ module.exports = new class Events extends events.EventEmitter { return super.once(event, listener); } + addListenerMultipleEvents(events, listener) { + Log.trace( { events }, 'Registring event listeners'); + events.forEach(eventName => { + this.on(eventName, event => { + listener(eventName, event); + }); + }); + } + removeListener(event, listener) { Log.trace( { event : event }, 'Removing listener'); return super.removeListener(event, listener); } startup(cb) { - async.each(require('./module_util.js').getModulePaths(), (modulePath, nextPath) => { - glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { - if(err) { - return nextPath(err); - } - - async.each(files, (moduleName, nextModule) => { - const fullModulePath = paths.join(modulePath, moduleName); - - try { - const mod = require(fullModulePath); - - if(_.isFunction(mod.registerEvents)) { - // :TODO: ... or just systemInit() / systemShutdown() & mods could call Events.on() / Events.removeListener() ? - mod.registerEvents(this); - } - } catch(e) { - Log.warn( { error : e }, 'Exception during module "registerEvents"'); - } - - return nextModule(null); - }, err => { - return nextPath(err); - }); - }); - }, err => { - return cb(err); - }); + return cb(null); } }; diff --git a/core/fse.js b/core/fse.js index 7d3565ad..fcf27ad4 100644 --- a/core/fse.js +++ b/core/fse.js @@ -2,26 +2,34 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const ansi = require('./ansi_term.js'); -const theme = require('./theme.js'); -const Message = require('./message.js'); -const updateMessageAreaLastReadId = require('./message_area.js').updateMessageAreaLastReadId; -const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; -const User = require('./user.js'); -const StatLog = require('./stat_log.js'); -const stringFormat = require('./string_format.js'); -const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; -const { isAnsi, cleanControlCodes, insert } = require('./string_util.js'); -const Config = require('./config.js').get; -const { getAddressedToInfo } = require('./mail_util.js'); +const { MenuModule } = require('./menu_module.js'); +const { ViewController } = require('./view_controller.js'); +const ansi = require('./ansi_term.js'); +const theme = require('./theme.js'); +const Message = require('./message.js'); +const { + updateMessageAreaLastReadId +} = require('./message_area.js'); +const { getMessageAreaByTag } = require('./message_area.js'); +const User = require('./user.js'); +const StatLog = require('./stat_log.js'); +const stringFormat = require('./string_format.js'); +const { + MessageAreaConfTempSwitcher +} = require('./mod_mixins.js'); +const { + isAnsi, cleanControlCodes, + insert +} = require('./string_util.js'); +const Config = require('./config.js').get; +const { getAddressedToInfo } = require('./mail_util.js'); +const Events = require('./events.js'); // deps -const async = require('async'); -const assert = require('assert'); -const _ = require('lodash'); -const moment = require('moment'); +const async = require('async'); +const assert = require('assert'); +const _ = require('lodash'); +const moment = require('moment'); exports.moduleInfo = { name : 'Full Screen Editor (FSE)', @@ -463,12 +471,14 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul updateUserStats(cb) { if(Message.isPrivateAreaTag(this.message.areaTag)) { + Events.emit(Events.getSystemEvents().UserSendMail, { user : this.client.user }); if(cb) { cb(null); } return; // don't inc stats for private messages } + Events.emit(Events.getSystemEvents().UserPostMessage, { user : this.client.user, areaTag : this.message.areaTag }); return StatLog.incrementUserStat(this.client.user, 'post_count', 1, cb); } diff --git a/core/last_callers.js b/core/last_callers.js index 52ec08f9..84ca01c3 100644 --- a/core/last_callers.js +++ b/core/last_callers.js @@ -2,27 +2,16 @@ 'use strict'; // ENiGMA½ -const MenuModule = require('./menu_module.js').MenuModule; -const ViewController = require('./view_controller.js').ViewController; -const StatLog = require('./stat_log.js'); -const User = require('./user.js'); -const stringFormat = require('./string_format.js'); +const { MenuModule } = require('./menu_module.js'); +const StatLog = require('./stat_log.js'); +const User = require('./user.js'); +const sysDb = require('./database.js').dbs.system; // deps const moment = require('moment'); const async = require('async'); const _ = require('lodash'); -/* - Available listFormat object members: - userId - userName - location - affiliation - ts - -*/ - exports.moduleInfo = { name : 'Last Callers', desc : 'Last callers to the system', @@ -37,6 +26,9 @@ const MciCodeIds = { exports.getModule = class LastCallersModule extends MenuModule { constructor(options) { super(options); + + this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {}); + this.actionIndicatorDefault = _.get(options, 'menuConfig.config.actionIndicatorDefault', '-'); } mciReady(mciData, cb) { @@ -45,107 +37,191 @@ exports.getModule = class LastCallersModule extends MenuModule { return cb(err); } - const self = this; - const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); - - let loginHistory; - let callersView; - - async.series( + async.waterfall( [ - function loadFromConfig(callback) { - const loadOpts = { - callingMenu : self, - mciMap : mciData.menu, - noInput : true, - }; - - vc.loadFromMenuConfig(loadOpts, callback); - }, - function fetchHistory(callback) { - callersView = vc.getView(MciCodeIds.CallerList); - - // fetch up - StatLog.getSystemLogEntries('user_login_history', StatLog.Order.TimestampDesc, 200, (err, lh) => { - loginHistory = lh; - - if(self.menuConfig.config.hideSysOpLogin) { - const noOpLoginHistory = loginHistory.filter(lh => { - return false === User.isRootUserId(parseInt(lh.log_value)); // log_value=userId - }); - - // - // If we have enough items to display, or hideSysOpLogin is set to 'always', - // then set loginHistory to our filtered list. Else, we'll leave it be. - // - if(noOpLoginHistory.length >= callersView.dimens.height || 'always' === self.menuConfig.config.hideSysOpLogin) { - loginHistory = noOpLoginHistory; - } - } - - // - // Finally, we need to trim up the list to the needed size - // - loginHistory = loginHistory.slice(0, callersView.dimens.height); - - return callback(err); + (next) => { + this.prepViewController('callers', 0, mciData.menu, err => { + return next(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); - - User.getUserName(item.userId, (err, userName) => { - if(err) { - item.deleted = true; - return next(null); - } else { - item.userName = userName || 'N/A'; - - User.loadProperties(item.userId, getPropOpts, (err, props) => { - if(!err && props) { - item.location = props.location || 'N/A'; - item.affiliation = item.affils = (props.affiliation || 'N/A'); - } else { - item.location = 'N/A'; - item.affiliation = item.affils = 'N/A'; - } - return next(null); - }); - } - }); - }, - err => { - loginHistory = loginHistory.filter(lh => true !== lh.deleted); - return callback(err); - } - ); + (next) => { + this.fetchHistory( (err, loginHistory) => { + return next(err, loginHistory); + }); }, - function populateList(callback) { - const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affiliation} - {ts}'; - - callersView.setItems(_.map(loginHistory, ce => stringFormat(listFormat, ce) ) ); - + (loginHistory, next) => { + this.loadUserForHistoryItems(loginHistory, (err, updatedHistory) => { + return next(err, updatedHistory); + }); + }, + (loginHistory, next) => { + const callersView = this.viewControllers.callers.getView(MciCodeIds.CallerList); + callersView.setItems(loginHistory); callersView.redraw(); - return callback(null); + return next(null); } ], - (err) => { + err => { if(err) { - self.client.log.error( { error : err.toString() }, 'Error loading last callers'); + this.client.log.warn( { error : err.message }, 'Error loading last callers'); } - cb(err); + return cb(err); } ); }); } + + getCollapse(conf) { + let collapse = _.get(this, conf); + collapse = collapse && collapse.match(/^([0-9]+)\s*(minutes|seconds|hours|days|months)$/); + if(collapse) { + return moment.duration(parseInt(collapse[1]), collapse[2]); + } + } + + fetchHistory(cb) { + const callersView = this.viewControllers.callers.getView(MciCodeIds.CallerList); + if(!callersView || 0 === callersView.dimens.height) { + return cb(null); + } + + StatLog.getSystemLogEntries( + 'user_login_history', + StatLog.Order.TimestampDesc, + 200, // max items to fetch - we need more than max displayed for filtering/etc. + (err, loginHistory) => { + if(err) { + return cb(err); + } + + const dateTimeFormat = _.get( + this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateFormat('short')); + + loginHistory = loginHistory.map(item => { + try { + const historyItem = JSON.parse(item.log_value); + if(_.isObject(historyItem)) { + item.userId = historyItem.userId; + item.sessionId = historyItem.sessionId; + } else { + item.userId = historyItem; // older format + item.sessionId = '-none-'; + } + } catch(e) { + return null; // we'll filter this out + } + + item.timestamp = moment(item.timestamp); + + return Object.assign( + item, + { + ts : moment(item.timestamp).format(dateTimeFormat) + } + ); + }); + + const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide'); + const sysOpCollapse = this.getCollapse('menuConfig.config.sysop.collapse'); + + if(hideSysOp) { + loginHistory = loginHistory.filter(item => false === User.isRootUserId(item.userId)); + } else if(sysOpCollapse) { + // :TODO: DRY op & user collapse code + const maxAge = sysOpCollapse.asSeconds(); + let lastUserId; + let lastTimestamp; + + loginHistory = loginHistory.filter(item => { + const op = User.isRootUserId(item.userId); + const repeat = lastUserId === item.userId; + const recent = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).seconds() < maxAge : false; + + lastUserId = item.userId; + lastTimestamp = item.timestamp; + + return !op || !repeat || !recent; + }); + } + + const userCollapse = this.getCollapse('menuConfig.config.user.collapse'); + if(userCollapse) { + const maxAge = userCollapse.asSeconds(); + let lastUserId; + let lastTimestamp; + + loginHistory = loginHistory.filter(item => { + const repeat = lastUserId === item.userId; + const recent = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).seconds() < maxAge : false; + + lastUserId = item.userId; + lastTimestamp = item.timestamp; + + return !repeat || !recent; + }); + } + + return cb( + null, + loginHistory.slice(0, callersView.dimens.height) // trim the fat + ); + } + ); + } + + loadUserForHistoryItems(loginHistory, cb) { + const getPropOpts = { + names : [ 'real_name', 'location', 'affiliation' ] + }; + + const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k); + let indicatorSumsSql; + if(actionIndicatorNames.length > 0) { + indicatorSumsSql = actionIndicatorNames.map(i => { + return `SUM(CASE WHEN log_value='${_.snakeCase(i)}' THEN 1 ELSE 0 END) AS ${i}`; + }); + } + + async.map(loginHistory, (item, next) => { + User.getUserName(item.userId, (err, userName) => { + if(err) { + return cb(null, null); + } + + item.userName = item.text = (userName || 'N/A'); + + User.loadProperties(item.userId, getPropOpts, (err, props) => { + item.location = (props && props.location) || 'N/A'; + item.affiliation = item.affils = (props && props.affiliation) || 'N/A'; + item.realName = (props && props.real_name) || 'N/A'; + + if(!indicatorSumsSql) { + return next(null, item); + } + + sysDb.get( + `SELECT ${indicatorSumsSql.join(', ')} + FROM user_event_log + WHERE user_id=? AND session_id=? + LIMIT 1;`, + [ item.userId, item.sessionId ], + (err, results) => { + if(_.isObject(results)) { + item.actions = ''; + Object.keys(results).forEach(n => { + const indicator = results[n] > 0 ? this.actionIndicators[n] || this.actionIndicatorDefault : this.actionIndicatorDefault; + item[n] = indicator; + item.actions += indicator; + }); + } + return next(null, item); + } + ); + }); + }); + }, + (err, mapped) => { + return cb(err, mapped.filter(item => item)); // remove deleted + }); + } }; diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index dbc782a0..abd04cb1 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -25,7 +25,8 @@ exports.MaskEditTextView = MaskEditTextView; // :TODO: // * Hint, e.g. YYYY/MM/DD // * Return values with literals in place -// +// * Tab in/out results in oddities such as cursor placement & ability to type in non-pattern chars +// * There exists some sort of condition that allows pattern position to get out of sync function MaskEditTextView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); diff --git a/core/module_util.js b/core/module_util.js index 5a575f3e..ae342d4b 100644 --- a/core/module_util.js +++ b/core/module_util.js @@ -3,6 +3,7 @@ // ENiGMA½ const Config = require('./config.js').get; +const Log = require('./logger.js').log; // deps const fs = require('graceful-fs'); @@ -10,12 +11,14 @@ const paths = require('path'); const _ = require('lodash'); const assert = require('assert'); const async = require('async'); +const glob = require('glob'); // exports exports.loadModuleEx = loadModuleEx; exports.loadModule = loadModule; exports.loadModulesForCategory = loadModulesForCategory; exports.getModulePaths = getModulePaths; +exports.initializeModules = initializeModules; function loadModuleEx(options, cb) { assert(_.isObject(options)); @@ -108,3 +111,54 @@ function getModulePaths() { config.paths.scannerTossers, ]; } + +function initializeModules(cb) { + const Events = require('./events.js'); + + const modulePaths = getModulePaths().concat(__dirname); + + async.each(modulePaths, (modulePath, nextPath) => { + glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { + if(err) { + return nextPath(err); + } + + const ourPath = paths.join(__dirname, __filename); + + async.each(files, (moduleName, nextModule) => { + const fullModulePath = paths.join(modulePath, moduleName); + if(ourPath === fullModulePath) { + return nextModule(null); + } + + try { + const mod = require(fullModulePath); + + if(_.isFunction(mod.moduleInitialize)) { + const initInfo = { + events : Events, + }; + + mod.moduleInitialize(initInfo, err => { + if(err) { + Log.warn( { error : err.message, modulePath : fullModulePath }, 'Error during "moduleInitialize"'); + } + return nextModule(null); + }); + } else { + return nextModule(null); + } + } catch(e) { + Log.warn( { error : e }, 'Exception during "moduleInitialize"'); + return nextModule(null); + } + }, + err => { + return nextPath(err); + }); + }); + }, + err => { + return cb(err); + }); +} diff --git a/core/nua.js b/core/nua.js index 7eafe16d..18b9a719 100644 --- a/core/nua.js +++ b/core/nua.js @@ -103,7 +103,11 @@ exports.getModule = class NewUserAppModule extends MenuModule { } // :TODO: User.create() should validate email uniqueness! - newUser.create(formData.value.password, err => { + const createUserInfo = { + password : formData.value.password, + sessionId : self.client.session.uniqueId, // used for events/etc. + }; + newUser.create(createUserInfo, err => { if(err) { self.client.log.info( { error : err, username : formData.value.username }, 'New user creation failed'); diff --git a/core/stat_log.js b/core/stat_log.js index 4b076e6d..9e655999 100644 --- a/core/stat_log.js +++ b/core/stat_log.js @@ -252,10 +252,16 @@ class StatLog { appendUserLogEntry(user, logName, logValue, keepDays, cb) { sysDb.run( - `INSERT INTO user_event_log (timestamp, user_id, log_name, log_value) - VALUES (?, ?, ?, ?);`, - [ this.now, user.userId, logName, logValue ], - () => { + `INSERT INTO user_event_log (timestamp, user_id, session_id, log_name, log_value) + VALUES (?, ?, ?, ?, ?);`, + [ this.now, user.userId, user.sessionId, logName, logValue ], + err => { + if(err) { + if(cb) { + cb(err); + } + return; + } // // Handle keepDays // @@ -280,6 +286,33 @@ class StatLog { } ); } + + initUserEvents(cb) { + // + // We map some user events directly to user stat log entries such that they + // are persisted for a time. + // + const Events = require('./events.js'); + const systemEvents = Events.getSystemEvents(); + + const interestedEvents = [ + systemEvents.NewUser, + systemEvents.UserUpload, systemEvents.UserDownload, + systemEvents.UserPostMessage, systemEvents.UserSendMail, + systemEvents.UserRunDoor, + ]; + + Events.addListenerMultipleEvents(interestedEvents, (eventName, event) => { + this.appendUserLogEntry( + event.user, + 'system_event', + eventName.replace(/^codes\.l33t\.enigma\.system\./, ''), // strip package name prefix + 90 + ); + }); + + return cb(null); + } } module.exports = new StatLog(); diff --git a/core/user.js b/core/user.js index e3314cc7..18acc02c 100644 --- a/core/user.js +++ b/core/user.js @@ -179,7 +179,7 @@ module.exports = class User { ); } - create(password, cb) { + create(createUserInfo , cb) { assert(0 === this.userId); const config = Config(); @@ -219,7 +219,7 @@ module.exports = class User { ); }, function genAuthCredentials(trans, callback) { - User.generatePasswordDerivedKeyAndSalt(password, (err, info) => { + User.generatePasswordDerivedKeyAndSalt(createUserInfo.password, (err, info) => { if(err) { return callback(err); } @@ -244,7 +244,12 @@ module.exports = class User { }); }, function sendEvent(trans, callback) { - Events.emit(Events.getSystemEvents().NewUser, { user : self }); + Events.emit( + Events.getSystemEvents().NewUser, + { + user : Object.assign({}, self, { sessionId : createUserInfo.sessionId } ) + } + ); return callback(null, trans); } ], diff --git a/core/user_login.js b/core/user_login.js index 07a0a2d2..2030152d 100644 --- a/core/user_login.js +++ b/core/user_login.js @@ -87,7 +87,11 @@ function userLogin(client, username, password, cb) { }, function recordLoginHistory(callback) { const LOGIN_HISTORY_MAX = 200; // history of up to last 200 callers - return StatLog.appendSystemLogEntry('user_login_history', user.userId, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); + const historyItem = JSON.stringify({ + userId : user.userId, + sessionId : user.sessionId, + }); + return StatLog.appendSystemLogEntry('user_login_history', historyItem, LOGIN_HISTORY_MAX, StatLog.KeepType.Max, callback); } ], err => { diff --git a/docs/modding/last-callers.md b/docs/modding/last-callers.md new file mode 100644 index 00000000..1f7aee8e --- /dev/null +++ b/docs/modding/last-callers.md @@ -0,0 +1,34 @@ +--- +layout: page +title: Last Callers +--- +## The Last Callers Module +The built in `last_callers` module provides flexible retro last callers mod. + +## Configuration +### Config Block +Available `config` block entries: +* `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` format. +* `user`: User options: + * `collapse`: Collapse or roll up entries that fall within the period specified. May be a string in the form of `30 minutes`, `3 weeks`, `1 hour`, etc. +* `sysop`: Sysop options: + * `collapse`: Collapse or roll up entries that fall within the period specified. May be a string in the form of `30 minutes`, `3 weeks`, `1 hour`, etc. + * `hide`: Hide all +op logins +* `actionIndicators`: Maps user actions to indicators. For example: `userDownload` to "D". Available indicators: + * `userDownload` + * `userUpload` + * `userPostMsg` + * `userSendMail` + * `userRunDoor` +* `actionIndicatorDefault`: Default indicator when an action is not set. Defaults to "-". + +### Theming +When in a list view, the following `itemFormat` object is provided: +* `userId`: User ID. +* `realName`: User's real name or "N/A". +* `ts`: Timestamp in `dateTimeFormat` format. +* `location`: User's location or "N/A". +* `affiliation` or `affils`: Users affiliations or "N/A". +* `actions`: A string built by concatenating action indicators for a users logged in session. For example, given a indincator of `userDownload` mapped to "D", the string may be "-D----". The format was made popular on Amiga style boards. + +