enigma-bbs/core/top_x.js

268 lines
9.4 KiB
JavaScript

/* jslint node: true */
'use strict';
// ENiGMA½
const { MenuModule } = require('./menu_module.js');
const UserProps = require('./user_property.js');
const UserLogNames = require('./user_log_name.js');
const { Errors } = require('./enig_error.js');
const UserDb = require('./database.js').dbs.user;
const SysDb = require('./database.js').dbs.system;
const User = require('./user.js');
// deps
const _ = require('lodash');
const async = require('async');
exports.moduleInfo = {
name: 'TopX',
desc: 'Displays users top X stats',
author: 'NuSkooler',
packageName: 'codes.l33t.enigma.topx',
};
const FormIds = {
menu: 0,
};
exports.getModule = class TopXModule extends MenuModule {
constructor(options) {
super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
extraArgs: options.extraArgs,
});
}
mciReady(mciData, cb) {
super.mciReady(mciData, err => {
if (err) {
return cb(err);
}
async.series(
[
callback => {
const userPropValues = _.values(UserProps);
const userLogValues = _.values(UserLogNames);
const hasMci = (c, t) => {
if (!Array.isArray(t)) {
t = [t];
}
return t.some(t => _.isObject(mciData, ['menu', `${t}${c}`]));
};
return this.validateConfigFields(
{
mciMap: (key, config) => {
const mciCodes = Object.keys(config.mciMap)
.map(mci => {
return parseInt(mci);
})
.filter(mci => !isNaN(mci));
if (0 === mciCodes.length) {
return false;
}
return mciCodes.every(mci => {
const o = config.mciMap[mci];
if (!_.isObject(o)) {
return false;
}
const type = o.type;
switch (type) {
case 'userProp':
if (!userPropValues.includes(o.value)) {
return false;
}
// VM# must exist for this mci
if (
!_.isObject(mciData, [
'menu',
`VM${mci}`,
])
) {
return false;
}
break;
case 'userEventLog':
if (!userLogValues.includes(o.value)) {
return false;
}
// VM# must exist for this mci
if (!hasMci(mci, ['VM'])) {
return false;
}
break;
default:
return false;
}
return true;
});
},
},
callback
);
},
callback => {
return this.prepViewController(
'menu',
FormIds.menu,
mciData.menu,
callback
);
},
callback => {
async.forEachSeries(
Object.keys(this.config.mciMap),
(mciCode, nextMciCode) => {
return this.populateTopXList(mciCode, nextMciCode);
},
err => {
return callback(err);
}
);
},
],
err => {
return cb(err);
}
);
});
}
populateTopXList(mciCode, cb) {
const listView = this.viewControllers.menu.getView(mciCode);
if (!listView) {
return cb(Errors.UnexpectedState(`Failed to get view for MCI ${mciCode}`));
}
const type = this.config.mciMap[mciCode].type;
switch (type) {
case 'userProp':
return this.populateTopXUserProp(listView, mciCode, cb);
case 'userEventLog':
return this.populateTopXUserEventLog(listView, mciCode, cb);
// we should not hit here; validation happens up front
default:
return cb(Errors.UnexpectedState(`Unexpected type: ${type}`));
}
}
rowsToItems(rows, cb) {
let position = 1;
async.mapSeries(
rows,
(row, nextRow) => {
this.loadUserInfo(row.user_id, (err, userInfo) => {
if (err) {
return nextRow(err);
}
return nextRow(
null,
Object.assign(userInfo, {
position: position++,
value: row.value,
})
);
});
},
(err, items) => {
return cb(err, items);
}
);
}
populateTopXUserEventLog(listView, mciCode, cb) {
const mciMap = this.config.mciMap[mciCode];
const count = listView.dimens.height || 1;
const daysBack = mciMap.daysBack;
const shouldSum = _.get(mciMap, 'sum', true);
const valueSql = shouldSum ? 'SUM(CAST(log_value AS INTEGER))' : 'COUNT()';
const dateSql = daysBack
? `AND DATETIME(timestamp) >= DATETIME('now', '-${daysBack} days')`
: '';
SysDb.all(
`SELECT user_id, ${valueSql} AS value
FROM user_event_log
WHERE log_name = ? ${dateSql}
GROUP BY user_id
ORDER BY value DESC
LIMIT ${count};`,
[mciMap.value],
(err, rows) => {
if (err) {
return cb(err);
}
this.rowsToItems(rows, (err, items) => {
if (err) {
return cb(err);
}
listView.setItems(items);
listView.redraw();
return cb(null);
});
}
);
}
populateTopXUserProp(listView, mciCode, cb) {
const count = listView.dimens.height || 1;
UserDb.all(
`SELECT user_id, CAST(prop_value AS INTEGER) AS value
FROM user_property
WHERE prop_name = ?
ORDER BY value DESC
LIMIT ${count};`,
[this.config.mciMap[mciCode].value],
(err, rows) => {
if (err) {
return cb(err);
}
this.rowsToItems(rows, (err, items) => {
if (err) {
return cb(err);
}
listView.setItems(items);
listView.redraw();
return cb(null);
});
}
);
}
loadUserInfo(userId, cb) {
const getPropOpts = {
names: [UserProps.RealName, UserProps.Location, UserProps.Affiliations],
};
const userInfo = { userId };
User.getUserName(userId, (err, userName) => {
if (err) {
return cb(err);
}
userInfo.userName = userName;
User.loadProperties(userId, getPropOpts, (err, props) => {
if (err) {
return cb(err);
}
userInfo.location = props[UserProps.Location] || '';
userInfo.affils = userInfo.affiliation =
props[UserProps.Affiliations] || '';
userInfo.realName = props[UserProps.RealName] || '';
return cb(null, userInfo);
});
});
}
};