From 2788c37492777c69b904d0380b493041261717f6 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 10 Jan 2019 20:34:52 -0700 Subject: [PATCH] + ACS: AC for achievement count check + ACS: AP for achievement point check + User minutes used on the system are now tracked + MCI: TO for total time spent online system (friendly format) * Fix up a couple ACS bugs with |value| * Fix formatting of achievement text + Add more achievements * Fix achievement duration formatting --- WHATSNEW.md | 1 + art/themes/luciano_blocktronics/theme.hjson | 6 +-- config/achievements.hjson | 47 +++++++++++++++++---- core/achievement.js | 24 ++++++----- core/acs_parser.js | 16 ++++++- core/ansi_term.js | 2 + core/client.js | 38 +++++++++++++++-- core/predefined_mci.js | 4 ++ core/user.js | 16 +++++++ core/user_property.js | 2 + docs/configuration/acs.md | 4 +- misc/acs_parser.pegjs | 16 ++++++- 12 files changed, 149 insertions(+), 27 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 01fc8edc..39dfca49 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -26,6 +26,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * `oputil.js user rm` and `oputil.js user info` are in! See [oputil CLI](/docs/admin/oputil.md). * Performing a file scan/import using `oputil.js fb scan` now recognizes various `FILES.BBS` formats. * Usernames found in the `config.users.badUserNames` are now not only disallowed from applying, but disconnected at any login attempt. +* Total minutes online is now tracked for users. Of course, it only starts after you get the update :) ## 0.0.8-alpha diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 1dfeac55..99164a2b 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -989,9 +989,9 @@ pointsSGR: "|12" textSGR: "|00|03" globalTextSGR: "|03" - boardName: "|10" - userName: "|11" - achievedValue: "|15" + boardNameSGR: "|10" + userNameSGR: "|11" + achievedValueSGR: "|15" } overrides: { diff --git a/config/achievements.hjson b/config/achievements.hjson index ba050673..63ea5ee1 100644 --- a/config/achievements.hjson +++ b/config/achievements.hjson @@ -307,29 +307,60 @@ match: { 1: { title: "Nevermind!" - globalText: "{userName} ran a door for {achievedValue!durationSeconds}. Guess it's not their thing!" - text: "You ran a door for only {achievedValue!durationSeconds}. Not your thing?" + globalText: "{userName} ran a door for {achievedValue!durationMinutes}. Guess it's not their thing!" + text: "You ran a door for only {achievedValue!durationMinutes}. Not your thing?" points: 5 } 10: { title: "It's OK I Guess" - globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" - text: "You ran a door for {achievedValue!durationSeconds}!" + globalText: "{userName} ran a door for {achievedValue!durationMinutes}!" + text: "You ran a door for {achievedValue!durationMinutes}!" points: 10 } 30: { title: "Good Game" - globalText: "{userName} ran a door for {achievedValue!durationSeconds}!" - text: "You ran a door for {achievedValue!durationSeconds}!" + globalText: "{userName} ran a door for {achievedValue!durationMinutes}!" + text: "You ran a door for {achievedValue!durationMinutes}!" points: 20 } 60: { title: "Textmode Dragon Slayer" - globalText: "{userName} has spent {achievedValue!durationSeconds} in a door!" - text: "You've spent {achievedValue!durationSeconds} in a door!" + globalText: "{userName} has spent {achievedValue!durationMinutes} in a door!" + text: "You've spent {achievedValue!durationMinutes} in a door!" points: 25 } } } + + user_total_system_online_minutes: { + type: userStatSet + statName: minutes_online_total_count + match: { + 30: { + title: "Just Poking Around" + globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!" + text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 5 + } + 60: { + title: "Mildly Interesting" + globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!" + text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 15 + } + 120: { + title: "Nothing Better to Do" + globalText: "{userName} has spent {achievedValue!durationMinutes} on {boardName}!" + text: "You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 25 + } + 1440: { + title: "Idle Bot" + globalText: "{userName} is probably a bot. They've spent {achievedValue!durationMinutes} on {boardName}!" + text: "You're a bot, aren't you? You've been on {boardName} for a total of {achievedValue!durationMinutes}!" + points: 50 + } + } + } } } \ No newline at end of file diff --git a/core/achievement.js b/core/achievement.js index ccd43a54..ad7644ba 100644 --- a/core/achievement.js +++ b/core/achievement.js @@ -406,19 +406,23 @@ class Achievements { getFormattedTextFor(info, textType, defaultSgr = '|07') { const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); - const defSgr = themeDefaults[`${textType}SGR`] || defaultSgr; + const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr; - const wrap = (fieldName, value) => { - return `${themeDefaults[fieldName] || defSgr}${value}${defSgr}`; + const formatObj = this.getFormatObject(info); + + const wrap = (input) => { + const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g'); + return input.replace(re, (m, formatVar, formatOpts) => { + const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr; + let r = `${varSgr}{${formatVar}`; + if(formatOpts) { + r += formatOpts; + } + return `${r}}${textTypeSgr}`; + }); }; - let formatObj = this.getFormatObject(info); - formatObj = _.reduce(formatObj, (out, v, k) => { - out[k] = wrap(k, v); - return out; - }, {}); - - return stringFormat(`${defSgr}${info.details[textType]}`, formatObj); + return stringFormat(`${textTypeSgr}${wrap(info.details[textType])}`, formatObj); } createAchievementInterruptItems(info, cb) { diff --git a/core/acs_parser.js b/core/acs_parser.js index d6983b17..d4084b95 100644 --- a/core/acs_parser.js +++ b/core/acs_parser.js @@ -1004,7 +1004,7 @@ function peg$parse(input, options) { TW : function termWidth() { return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; }, - ID : function isUserId(value) { + ID : function isUserId() { if(!user) { return false; } @@ -1024,6 +1024,20 @@ function peg$parse(input, options) { const midnight = now.clone().startOf('day') const minutesPastMidnight = now.diff(midnight, 'minutes'); return !isNaN(value) && minutesPastMidnight >= value; + }, + AC : function achievementCount() { + if(!user) { + return false; + } + const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0; + return !isNan(value) && points >= value; + }, + AP : function achievementPoints() { + if(!user) { + return false; + } + const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0; + return !isNan(value) && points >= value; } }[acsCode](value); } catch (e) { diff --git a/core/ansi_term.js b/core/ansi_term.js index f00fd011..353c46c8 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -23,6 +23,8 @@ // General // * http://en.wikipedia.org/wiki/ANSI_escape_code // * http://www.inwap.com/pdp10/ansicode.txt +// * Excellent information with many standards covered (for hterm): +// https://chromium.googlesource.com/apps/libapps/+/master/hterm/doc/ControlSequences.md // // Other Implementations // * https://github.com/chjj/term.js/blob/master/src/term.js diff --git a/core/client.js b/core/client.js index 300285a3..894119bf 100644 --- a/core/client.js +++ b/core/client.js @@ -40,6 +40,7 @@ const MenuStack = require('./menu_stack.js'); const ACS = require('./acs.js'); const Events = require('./events.js'); const UserInterruptQueue = require('./user_interrupt_queue.js'); +const UserProps = require('./user_property.js'); // deps const stream = require('stream'); @@ -442,13 +443,36 @@ Client.prototype.startIdleMonitor = function() { // // Every 1m, check for idle. + // We also update minutes spent online the system here, + // if we have a authenticated user. // this.idleCheck = setInterval( () => { const nowMs = Date.now(); - const idleLogoutSeconds = this.user.isAuthenticated() ? - Config().users.idleLogoutSeconds : - Config().users.preAuthIdleLogoutSeconds; + let idleLogoutSeconds; + if(this.user.isAuthenticated()) { + idleLogoutSeconds = Config().users.idleLogoutSeconds; + + // + // We don't really want to be firing off an event every 1m for + // every user, but want at least some updates for various things + // such as achievements. Send off every 5m. + // + const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1); + if(0 === (minOnline % 5)) { + Events.emit( + Events.getSystemEvents().UserStatIncrement, + { + user : this.user, + statName : UserProps.MinutesOnlineTotalCount, + statIncrementBy : 1, + statValue : minOnline + } + ); + } + } else { + idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds; + } if(nowMs - this.lastKeyPressMs >= (idleLogoutSeconds * 1000)) { this.emit('idle timeout'); @@ -473,6 +497,14 @@ Client.prototype.end = function () { currentModule.leave(); } + // persist time online for authenticated users + if(this.user.isAuthenticated()) { + this.user.persistProperty( + UserProps.MinutesOnlineTotalCount, + this.user.getProperty(UserProps.MinutesOnlineTotalCount) + ); + } + this.stopIdleMonitor(); try { diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 39e339cc..2e7ed5ff 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -160,6 +160,10 @@ const PREDEFINED_MCI_GENERATORS = { const minutes = client.user.properties[UserProps.DoorRunTotalMinutes] || 0; return moment.duration(minutes, 'minutes').humanize(); }, + TO : function friendlyTotalTimeOnSystem(client) { + const minutes = client.user.properties[UserProps.MinutesOnlineTotalCount] || 0; + return moment.duration(minutes, 'minutes').humanize(); + }, // // Date/Time diff --git a/core/user.js b/core/user.js index ba89b387..3b261dc6 100644 --- a/core/user.js +++ b/core/user.js @@ -443,6 +443,22 @@ module.exports = class User { ); } + setProperty(propName, propValue) { + this.properties[propName] = propValue; + } + + incrementProperty(propName, incrementBy) { + incrementBy = incrementBy || 1; + let newValue = parseInt(this.getProperty(propName)); + if(newValue) { + newValue += incrementBy; + } else { + newValue = incrementBy; + } + this.setProperty(propName, newValue); + return newValue; + } + getProperty(propName) { return this.properties[propName]; } diff --git a/core/user_property.js b/core/user_property.js index a1489e82..56e47e66 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -55,5 +55,7 @@ module.exports = { AchievementTotalCount : 'achievement_total_count', AchievementTotalPoints : 'achievement_total_points', + + MinutesOnlineTotalCount : 'minutes_online_total_count', }; diff --git a/docs/configuration/acs.md b/docs/configuration/acs.md index 1ed83bb5..d0a45d06 100644 --- a/docs/configuration/acs.md +++ b/docs/configuration/acs.md @@ -34,7 +34,9 @@ The following are ACS codes available as of this writing: | NRratio | User has upload/download count ratio >= _ratio_ | | KRratio | User has a upload/download byte ratio >= _ratio_ | | PCratio | User has a post/call ratio >= _ratio_ | -| MMminutes | It is currently >= _minutes_ past midnight (system time) +| MMminutes | It is currently >= _minutes_ past midnight (system time) | +| ACachievementCount | User has >= _achievementCount_ achievements | +| APachievementPoints | User has >= _achievementPoints_ achievement points | \* Many more ACS codes are planned for the near future. diff --git a/misc/acs_parser.pegjs b/misc/acs_parser.pegjs index bd6a8d96..8a39deea 100644 --- a/misc/acs_parser.pegjs +++ b/misc/acs_parser.pegjs @@ -160,7 +160,7 @@ TW : function termWidth() { return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; }, - ID : function isUserId(value) { + ID : function isUserId() { if(!user) { return false; } @@ -180,6 +180,20 @@ const midnight = now.clone().startOf('day') const minutesPastMidnight = now.diff(midnight, 'minutes'); return !isNaN(value) && minutesPastMidnight >= value; + }, + AC : function achievementCount() { + if(!user) { + return false; + } + const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0; + return !isNan(value) && points >= value; + }, + AP : function achievementPoints() { + if(!user) { + return false; + } + const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0; + return !isNan(value) && points >= value; } }[acsCode](value); } catch (e) {