First pass formatting with Prettier

* Added .prettierrc.json
* Added .prettierignore
* Formatted
This commit is contained in:
Bryan Ashby 2022-06-05 14:04:25 -06:00
parent eecfb33ad5
commit 4881c2123a
172 changed files with 23696 additions and 18029 deletions

View File

@ -3,33 +3,22 @@
"es6": true, "es6": true,
"node": true "node": true
}, },
"extends": [ "extends": ["eslint:recommended"],
"eslint:recommended"
],
"rules": { "rules": {
"indent": [ "indent": [
"error", "error",
4, 4,
{ {
"SwitchCase" : 1 "SwitchCase": 1
} }
], ],
"linebreak-style": [ "linebreak-style": ["error", "unix"],
"error", "quotes": ["error", "single"],
"unix" "semi": ["error", "always"],
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"comma-dangle": 0, "comma-dangle": 0,
"no-trailing-spaces" :"warn" "no-trailing-spaces": "warn"
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2020 "ecmaVersion": 2020
} }
} }

12
.prettierignore Normal file
View File

@ -0,0 +1,12 @@
art
config
db
docs
drop
gopher
logs
misc
www
mkdocs.yml
*.md
.github

View File

@ -1,19 +1,19 @@
{ {
"arrowParens": "avoid", "arrowParens": "avoid",
"bracketSameLine": false, "bracketSameLine": false,
"bracketSpacing": true, "bracketSpacing": true,
"embeddedLanguageFormatting": "auto", "embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css", "htmlWhitespaceSensitivity": "css",
"insertPragma": false, "insertPragma": false,
"jsxSingleQuote": false, "jsxSingleQuote": false,
"printWidth": 90, "printWidth": 90,
"proseWrap": "preserve", "proseWrap": "preserve",
"quoteProps": "as-needed", "quoteProps": "as-needed",
"requirePragma": false, "requirePragma": false,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "es5", "trailingComma": "es5",
"useTabs": false, "useTabs": false,
"vueIndentScriptAndStyle": false "vueIndentScriptAndStyle": false
} }

View File

@ -1,31 +1,28 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const DropFile = require('./dropfile.js'); const DropFile = require('./dropfile.js');
const Door = require('./door.js'); const Door = require('./door.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const { const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
trackDoorRunBegin, const Log = require('./logger').log;
trackDoorRunEnd
} = require('./door_util.js');
const Log = require('./logger').log;
// deps // deps
const async = require('async'); const async = require('async');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
const paths = require('path'); const paths = require('path');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const activeDoorNodeInstances = {}; const activeDoorNodeInstances = {};
exports.moduleInfo = { exports.moduleInfo = {
name : 'Abracadabra', name: 'Abracadabra',
desc : 'External BBS Door Module', desc: 'External BBS Door Module',
author : 'NuSkooler', author: 'NuSkooler',
}; };
/* /*
@ -71,15 +68,15 @@ exports.getModule = class AbracadabraModule extends MenuModule {
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
// .. and/or EnigAssert // .. and/or EnigAssert
assert(_.isString(this.config.name, 'Config \'name\' is required')); assert(_.isString(this.config.name, "Config 'name' is required"));
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); assert(_.isString(this.config.cmd, "Config 'cmd' is required"));
this.config.nodeMax = this.config.nodeMax || 0; this.config.nodeMax = this.config.nodeMax || 0;
this.config.args = this.config.args || []; this.config.args = this.config.args || [];
} }
incrementActiveDoorNodeInstances() { incrementActiveDoorNodeInstances() {
if(activeDoorNodeInstances[this.config.name]) { if (activeDoorNodeInstances[this.config.name]) {
activeDoorNodeInstances[this.config.name] += 1; activeDoorNodeInstances[this.config.name] += 1;
} else { } else {
activeDoorNodeInstances[this.config.name] = 1; activeDoorNodeInstances[this.config.name] = 1;
@ -88,7 +85,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
} }
decrementActiveDoorNodeInstances() { decrementActiveDoorNodeInstances() {
if(true === this.activeDoorInstancesIncremented) { if (true === this.activeDoorInstancesIncremented) {
activeDoorNodeInstances[this.config.name] -= 1; activeDoorNodeInstances[this.config.name] -= 1;
this.activeDoorInstancesIncremented = false; this.activeDoorInstancesIncremented = false;
} }
@ -100,29 +97,43 @@ exports.getModule = class AbracadabraModule extends MenuModule {
async.series( async.series(
[ [
function validateNodeCount(callback) { function validateNodeCount(callback) {
if(self.config.nodeMax > 0 && if (
self.config.nodeMax > 0 &&
_.isNumber(activeDoorNodeInstances[self.config.name]) && _.isNumber(activeDoorNodeInstances[self.config.name]) &&
activeDoorNodeInstances[self.config.name] + 1 > self.config.nodeMax) activeDoorNodeInstances[self.config.name] + 1 >
{ self.config.nodeMax
) {
self.client.log.info( self.client.log.info(
{ {
name : self.config.name, name: self.config.name,
activeCount : activeDoorNodeInstances[self.config.name] activeCount: activeDoorNodeInstances[self.config.name],
}, },
'Too many active instances'); 'Too many active instances'
);
if(_.isString(self.config.tooManyArt)) { if (_.isString(self.config.tooManyArt)) {
theme.displayThemeArt( { client : self.client, name : self.config.tooManyArt }, function displayed() { theme.displayThemeArt(
self.pausePrompt( () => { { client: self.client, name: self.config.tooManyArt },
return callback(Errors.AccessDenied('Too many active instances')); function displayed() {
}); self.pausePrompt(() => {
}); return callback(
Errors.AccessDenied(
'Too many active instances'
)
);
});
}
);
} else { } else {
self.client.term.write('\nToo many active instances. Try again later.\n'); self.client.term.write(
'\nToo many active instances. Try again later.\n'
);
// :TODO: Use MenuModule.pausePrompt() // :TODO: Use MenuModule.pausePrompt()
self.pausePrompt( () => { self.pausePrompt(() => {
return callback(Errors.AccessDenied('Too many active instances')); return callback(
Errors.AccessDenied('Too many active instances')
);
}); });
} }
} else { } else {
@ -135,21 +146,26 @@ exports.getModule = class AbracadabraModule extends MenuModule {
return self.doorInstance.prepare(self.config.io || 'stdio', callback); return self.doorInstance.prepare(self.config.io || 'stdio', callback);
}, },
function generateDropfile(callback) { function generateDropfile(callback) {
if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') { if (
!self.config.dropFileType ||
self.config.dropFileType.toLowerCase() === 'none'
) {
return callback(null); return callback(null);
} }
self.dropFile = new DropFile( self.dropFile = new DropFile(self.client, {
self.client, fileType: self.config.dropFileType,
{ fileType : self.config.dropFileType } });
);
return self.dropFile.createFile(callback); return self.dropFile.createFile(callback);
} },
], ],
function complete(err) { function complete(err) {
if(err) { if (err) {
self.client.log.warn( { error : err.toString() }, 'Could not start door'); self.client.log.warn(
{ error: err.toString() },
'Could not start door'
);
self.lastError = err; self.lastError = err;
self.prevMenu(); self.prevMenu();
} else { } else {
@ -163,18 +179,18 @@ exports.getModule = class AbracadabraModule extends MenuModule {
this.client.term.write(ansi.resetScreen()); this.client.term.write(ansi.resetScreen());
const exeInfo = { const exeInfo = {
cmd : this.config.cmd, cmd: this.config.cmd,
cwd : this.config.cwd || paths.dirname(this.config.cmd), cwd: this.config.cwd || paths.dirname(this.config.cmd),
args : this.config.args, args: this.config.args,
io : this.config.io || 'stdio', io: this.config.io || 'stdio',
encoding : this.config.encoding || 'cp437', encoding: this.config.encoding || 'cp437',
node : this.client.node, node: this.client.node,
env : this.config.env, env: this.config.env,
}; };
if (this.dropFile) { if (this.dropFile) {
exeInfo.dropFile = this.dropFile.fileName; exeInfo.dropFile = this.dropFile.fileName;
exeInfo.dropFilePath = this.dropFile.fullPath; exeInfo.dropFilePath = this.dropFile.fullPath;
} }
const doorTracking = trackDoorRunBegin(this.client, this.config.name); const doorTracking = trackDoorRunBegin(this.client, this.config.name);
@ -187,14 +203,17 @@ exports.getModule = class AbracadabraModule extends MenuModule {
if (exeInfo.dropFilePath) { if (exeInfo.dropFilePath) {
fs.unlink(exeInfo.dropFilePath, err => { fs.unlink(exeInfo.dropFilePath, err => {
if (err) { if (err) {
Log.warn({ error : err, path : exeInfo.dropFilePath }, 'Failed to remove drop file.'); Log.warn(
{ error: err, path: exeInfo.dropFilePath },
'Failed to remove drop file.'
);
} }
}); });
} }
// client may have disconnected while process was active - // client may have disconnected while process was active -
// we're done here if so. // we're done here if so.
if(!this.client.term.output) { if (!this.client.term.output) {
return; return;
} }
@ -204,10 +223,10 @@ exports.getModule = class AbracadabraModule extends MenuModule {
// //
this.client.term.rawWrite( this.client.term.rawWrite(
ansi.normal() + ansi.normal() +
ansi.goto(this.client.term.termHeight, this.client.term.termWidth) + ansi.goto(this.client.term.termHeight, this.client.term.termWidth) +
ansi.setScrollRegion() + ansi.setScrollRegion() +
ansi.goto(this.client.term.termHeight, 0) + ansi.goto(this.client.term.termHeight, 0) +
'\r\n\r\n' '\r\n\r\n'
); );
this.autoNextMenu(); this.autoNextMenu();

View File

@ -2,38 +2,28 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Events = require('./events.js'); const Events = require('./events.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const ConfigLoader = require('./config_loader'); const ConfigLoader = require('./config_loader');
const { getConfigPath } = require('./config_util'); const { getConfigPath } = require('./config_util');
const UserDb = require('./database.js').dbs.user; const UserDb = require('./database.js').dbs.user;
const { const { getISOTimestampString } = require('./database.js');
getISOTimestampString const UserInterruptQueue = require('./user_interrupt_queue.js');
} = require('./database.js'); const { getConnectionByUserId } = require('./client_connections.js');
const UserInterruptQueue = require('./user_interrupt_queue.js'); const UserProps = require('./user_property.js');
const { const { Errors, ErrorReasons } = require('./enig_error.js');
getConnectionByUserId const { getThemeArt } = require('./theme.js');
} = require('./client_connections.js'); const { pipeToAnsi, stripMciColorCodes } = require('./color_codes.js');
const UserProps = require('./user_property.js'); const stringFormat = require('./string_format.js');
const { const StatLog = require('./stat_log.js');
Errors, const Log = require('./logger.js').log;
ErrorReasons
} = require('./enig_error.js');
const { getThemeArt } = require('./theme.js');
const {
pipeToAnsi,
stripMciColorCodes
} = require('./color_codes.js');
const stringFormat = require('./string_format.js');
const StatLog = require('./stat_log.js');
const Log = require('./logger.js').log;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
const moment = require('moment'); const moment = require('moment');
exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser; exports.getAchievementsEarnedByUser = getAchievementsEarnedByUser;
class Achievement { class Achievement {
constructor(data) { constructor(data) {
@ -44,59 +34,65 @@ class Achievement {
} }
static factory(data) { static factory(data) {
if(!data) { if (!data) {
return; return;
} }
let achievement; let achievement;
switch(data.type) { switch (data.type) {
case Achievement.Types.UserStatSet : case Achievement.Types.UserStatSet:
case Achievement.Types.UserStatInc : case Achievement.Types.UserStatInc:
case Achievement.Types.UserStatIncNewVal : case Achievement.Types.UserStatIncNewVal:
achievement = new UserStatAchievement(data); achievement = new UserStatAchievement(data);
break; break;
default : return; default:
return;
} }
if(achievement.isValid()) { if (achievement.isValid()) {
return achievement; return achievement;
} }
} }
static get Types() { static get Types() {
return { return {
UserStatSet : 'userStatSet', UserStatSet: 'userStatSet',
UserStatInc : 'userStatInc', UserStatInc: 'userStatInc',
UserStatIncNewVal : 'userStatIncNewVal', UserStatIncNewVal: 'userStatIncNewVal',
}; };
} }
isValid() { isValid() {
switch(this.data.type) { switch (this.data.type) {
case Achievement.Types.UserStatSet : case Achievement.Types.UserStatSet:
case Achievement.Types.UserStatInc : case Achievement.Types.UserStatInc:
case Achievement.Types.UserStatIncNewVal : case Achievement.Types.UserStatIncNewVal:
if(!_.isString(this.data.statName)) { if (!_.isString(this.data.statName)) {
return false; return false;
} }
if(!_.isObject(this.data.match)) { if (!_.isObject(this.data.match)) {
return false; return false;
} }
break; break;
default : return false; default:
return false;
} }
return true; return true;
} }
getMatchDetails(/*matchAgainst*/) { getMatchDetails(/*matchAgainst*/) {}
}
isValidMatchDetails(details) { isValidMatchDetails(details) {
if(!details || !_.isString(details.title) || !_.isString(details.text) || !_.isNumber(details.points)) { if (
!details ||
!_.isString(details.title) ||
!_.isString(details.text) ||
!_.isNumber(details.points)
) {
return false; return false;
} }
return (_.isString(details.globalText) || !details.globalText); return _.isString(details.globalText) || !details.globalText;
} }
} }
@ -105,11 +101,13 @@ class UserStatAchievement extends Achievement {
super(data); super(data);
// sort match keys for quick match lookup // sort match keys for quick match lookup
this.matchKeys = Object.keys(this.data.match || {}).map(k => parseInt(k)).sort( (a, b) => b - a); this.matchKeys = Object.keys(this.data.match || {})
.map(k => parseInt(k))
.sort((a, b) => b - a);
} }
isValid() { isValid() {
if(!super.isValid()) { if (!super.isValid()) {
return false; return false;
} }
return !Object.keys(this.data.match).some(k => !parseInt(k)); return !Object.keys(this.data.match).some(k => !parseInt(k));
@ -118,11 +116,11 @@ class UserStatAchievement extends Achievement {
getMatchDetails(matchValue) { getMatchDetails(matchValue) {
let ret = []; let ret = [];
let matchField = this.matchKeys.find(v => matchValue >= v); let matchField = this.matchKeys.find(v => matchValue >= v);
if(matchField) { if (matchField) {
const match = this.data.match[matchField]; const match = this.data.match[matchField];
matchField = parseInt(matchField); matchField = parseInt(matchField);
if(this.isValidMatchDetails(match) && !isNaN(matchField)) { if (this.isValidMatchDetails(match) && !isNaN(matchField)) {
ret = [ match, matchField, matchValue ]; ret = [match, matchField, matchValue];
} }
} }
return ret; return ret;
@ -151,7 +149,7 @@ class Achievements {
} }
const configLoaded = () => { const configLoaded = () => {
if(true !== this.config.get().enabled) { if (true !== this.config.get().enabled) {
Log.info('Achievements are not enabled'); Log.info('Achievements are not enabled');
this.enabled = false; this.enabled = false;
this.stopMonitoringUserStatEvents(); this.stopMonitoringUserStatEvents();
@ -163,11 +161,11 @@ class Achievements {
}; };
this.config = new ConfigLoader({ this.config = new ConfigLoader({
onReload : err => { onReload: err => {
if (!err) { if (!err) {
configLoaded(); configLoaded();
} }
} },
}); });
this.config.init(configPath, err => { this.config.init(configPath, err => {
@ -182,10 +180,10 @@ class Achievements {
_getConfigPath() { _getConfigPath() {
const path = _.get(Config(), 'general.achievementFile'); const path = _.get(Config(), 'general.achievementFile');
if(!path) { if (!path) {
return; return;
} }
return getConfigPath(path); // qualify return getConfigPath(path); // qualify
} }
loadAchievementHitCount(user, achievementTag, field, cb) { loadAchievementHitCount(user, achievementTag, field, cb) {
@ -193,7 +191,7 @@ class Achievements {
`SELECT COUNT() AS count `SELECT COUNT() AS count
FROM user_achievement FROM user_achievement
WHERE user_id = ? AND achievement_tag = ? AND match = ?;`, WHERE user_id = ? AND achievement_tag = ? AND match = ?;`,
[ user.userId, achievementTag, field], [user.userId, achievementTag, field],
(err, row) => { (err, row) => {
return cb(err, row ? row.count : 0); return cb(err, row ? row.count : 0);
} }
@ -202,14 +200,23 @@ class Achievements {
record(info, localInterruptItem, cb) { record(info, localInterruptItem, cb) {
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1); StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalCount, 1);
StatLog.incrementUserStat(info.client.user, UserProps.AchievementTotalPoints, info.details.points); StatLog.incrementUserStat(
info.client.user,
UserProps.AchievementTotalPoints,
info.details.points
);
const cleanTitle = stripMciColorCodes(localInterruptItem.title); const cleanTitle = stripMciColorCodes(localInterruptItem.title);
const cleanText = stripMciColorCodes(localInterruptItem.achievText); const cleanText = stripMciColorCodes(localInterruptItem.achievText);
const recordData = [ const recordData = [
info.client.user.userId, info.achievementTag, getISOTimestampString(info.timestamp), info.matchField, info.client.user.userId,
cleanTitle, cleanText, info.details.points, info.achievementTag,
getISOTimestampString(info.timestamp),
info.matchField,
cleanTitle,
cleanText,
info.details.points,
]; ];
UserDb.run( UserDb.run(
@ -217,20 +224,17 @@ class Achievements {
VALUES (?, ?, ?, ?, ?, ?, ?);`, VALUES (?, ?, ?, ?, ?, ?, ?);`,
recordData, recordData,
err => { err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
this.events.emit( this.events.emit(Events.getSystemEvents().UserAchievementEarned, {
Events.getSystemEvents().UserAchievementEarned, user: info.client.user,
{ achievementTag: info.achievementTag,
user : info.client.user, points: info.details.points,
achievementTag : info.achievementTag, title: cleanTitle,
points : info.details.points, text: cleanText,
title : cleanTitle, });
text : cleanText,
}
);
return cb(null); return cb(null);
} }
@ -238,12 +242,12 @@ class Achievements {
} }
display(info, interruptItems, cb) { display(info, interruptItems, cb) {
if(interruptItems.local) { if (interruptItems.local) {
UserInterruptQueue.queue(interruptItems.local, { clients : info.client } ); UserInterruptQueue.queue(interruptItems.local, { clients: info.client });
} }
if(interruptItems.global) { if (interruptItems.global) {
UserInterruptQueue.queue(interruptItems.global, { omit : info.client } ); UserInterruptQueue.queue(interruptItems.global, { omit: info.client });
} }
return cb(null); return cb(null);
@ -252,7 +256,7 @@ class Achievements {
recordAndDisplayAchievement(info, cb) { recordAndDisplayAchievement(info, cb) {
async.waterfall( async.waterfall(
[ [
(callback) => { callback => {
return this.createAchievementInterruptItems(info, callback); return this.createAchievementInterruptItems(info, callback);
}, },
(interruptItems, callback) => { (interruptItems, callback) => {
@ -262,7 +266,7 @@ class Achievements {
}, },
(interruptItems, callback) => { (interruptItems, callback) => {
return this.display(info, interruptItems, callback); return this.display(info, interruptItems, callback);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -271,164 +275,228 @@ class Achievements {
} }
monitorUserStatEvents() { monitorUserStatEvents() {
if(this.userStatEventListeners) { if (this.userStatEventListeners) {
return; // already listening return; // already listening
} }
const listenEvents = [ const listenEvents = [
Events.getSystemEvents().UserStatSet, Events.getSystemEvents().UserStatSet,
Events.getSystemEvents().UserStatIncrement Events.getSystemEvents().UserStatIncrement,
]; ];
this.userStatEventListeners = this.events.addMultipleEventListener(listenEvents, userStatEvent => { this.userStatEventListeners = this.events.addMultipleEventListener(
if([ UserProps.AchievementTotalCount, UserProps.AchievementTotalPoints ].includes(userStatEvent.statName)) { listenEvents,
return; userStatEvent => {
} if (
if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) {
return;
}
// :TODO: Make this code generic - find + return factory created object
const achievementTags = Object.keys(_.pickBy(
_.get(this.config.get(), 'achievements', {}),
achievement => {
if(false === achievement.enabled) {
return false;
}
const acceptedTypes = [
Achievement.Types.UserStatSet,
Achievement.Types.UserStatInc,
Achievement.Types.UserStatIncNewVal,
];
return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName;
}
));
if(0 === achievementTags.length) {
return;
}
async.eachSeries(achievementTags, (achievementTag, nextAchievementTag) => {
const achievement = Achievement.factory(this.getAchievementByTag(achievementTag));
if(!achievement) {
return nextAchievementTag(null);
}
const statValue = parseInt(
[ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ?
userStatEvent.statValue :
userStatEvent.statIncrementBy
);
if(isNaN(statValue)) {
return nextAchievementTag(null);
}
const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue);
if(!details) {
return nextAchievementTag(null);
}
async.waterfall(
[ [
(callback) => { UserProps.AchievementTotalCount,
this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { UserProps.AchievementTotalPoints,
if(err) { ].includes(userStatEvent.statName)
return callback(err); ) {
} return;
return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null); }
});
}, if (
(callback) => { !_.isNumber(userStatEvent.statValue) &&
const client = getConnectionByUserId(userStatEvent.user.userId); !_.isNumber(userStatEvent.statIncrementBy)
if(!client) { ) {
return callback(Errors.UnexpectedState('Failed to get client for user ID')); return;
}
// :TODO: Make this code generic - find + return factory created object
const achievementTags = Object.keys(
_.pickBy(
_.get(this.config.get(), 'achievements', {}),
achievement => {
if (false === achievement.enabled) {
return false;
} }
const acceptedTypes = [
Achievement.Types.UserStatSet,
Achievement.Types.UserStatInc,
Achievement.Types.UserStatIncNewVal,
];
return (
acceptedTypes.includes(achievement.type) &&
achievement.statName === userStatEvent.statName
);
}
)
);
const info = { if (0 === achievementTags.length) {
achievementTag, return;
achievement, }
details,
client,
matchField, // match - may be in odd format
matchValue, // actual value
achievedValue : matchField, // achievement value met
user : userStatEvent.user,
timestamp : moment(),
};
const achievementsInfo = [ info ]; async.eachSeries(
return callback(null, achievementsInfo, info); achievementTags,
}, (achievementTag, nextAchievementTag) => {
(achievementsInfo, basicInfo, callback) => { const achievement = Achievement.factory(
if(true !== achievement.data.retroactive) { this.getAchievementByTag(achievementTag)
return callback(null, achievementsInfo); );
} if (!achievement) {
return nextAchievementTag(null);
}
const index = achievement.matchKeys.findIndex(v => v < matchField); const statValue = parseInt(
if(-1 === index || !Array.isArray(achievement.matchKeys)) { [
return callback(null, achievementsInfo); Achievement.Types.UserStatSet,
} Achievement.Types.UserStatIncNewVal,
].includes(achievement.data.type)
? userStatEvent.statValue
: userStatEvent.statIncrementBy
);
if (isNaN(statValue)) {
return nextAchievementTag(null);
}
// For userStat, any lesser match keys(values) are also met. Example: const [details, matchField, matchValue] =
// matchKeys: [ 500, 200, 100, 20, 10, 2 ] achievement.getMatchDetails(statValue);
// ^---- we met here if (!details) {
// ^------------^ retroactive range return nextAchievementTag(null);
// }
async.eachSeries(achievement.matchKeys.slice(index), (k, nextKey) => {
const [ det, fld, val ] = achievement.getMatchDetails(k);
if(!det) {
return nextKey(null);
}
this.loadAchievementHitCount(userStatEvent.user, achievementTag, fld, (err, count) => { async.waterfall(
if(!err || count && 0 === count) { [
achievementsInfo.push(Object.assign( callback => {
{}, this.loadAchievementHitCount(
basicInfo, userStatEvent.user,
{ achievementTag,
details : det, matchField,
matchField : fld, (err, count) => {
achievedValue : fld, if (err) {
matchValue : val, return callback(err);
} }
)); return callback(
count > 0
? Errors.General(
'Achievement already acquired',
ErrorReasons.TooMany
)
: null
);
}
);
},
callback => {
const client = getConnectionByUserId(
userStatEvent.user.userId
);
if (!client) {
return callback(
Errors.UnexpectedState(
'Failed to get client for user ID'
)
);
} }
return nextKey(null); const info = {
}); achievementTag,
}, achievement,
() => { details,
return callback(null, achievementsInfo); client,
}); matchField, // match - may be in odd format
}, matchValue, // actual value
(achievementsInfo, callback) => { achievedValue: matchField, // achievement value met
// reverse achievementsInfo so we display smallest > largest user: userStatEvent.user,
achievementsInfo.reverse(); timestamp: moment(),
};
async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => { const achievementsInfo = [info];
return this.recordAndDisplayAchievement(achInfo, err => { return callback(null, achievementsInfo, info);
return nextAchInfo(err); },
}); (achievementsInfo, basicInfo, callback) => {
}, if (true !== achievement.data.retroactive) {
return callback(null, achievementsInfo);
}
const index = achievement.matchKeys.findIndex(
v => v < matchField
);
if (
-1 === index ||
!Array.isArray(achievement.matchKeys)
) {
return callback(null, achievementsInfo);
}
// For userStat, any lesser match keys(values) are also met. Example:
// matchKeys: [ 500, 200, 100, 20, 10, 2 ]
// ^---- we met here
// ^------------^ retroactive range
//
async.eachSeries(
achievement.matchKeys.slice(index),
(k, nextKey) => {
const [det, fld, val] =
achievement.getMatchDetails(k);
if (!det) {
return nextKey(null);
}
this.loadAchievementHitCount(
userStatEvent.user,
achievementTag,
fld,
(err, count) => {
if (!err || (count && 0 === count)) {
achievementsInfo.push(
Object.assign({}, basicInfo, {
details: det,
matchField: fld,
achievedValue: fld,
matchValue: val,
})
);
}
return nextKey(null);
}
);
},
() => {
return callback(null, achievementsInfo);
}
);
},
(achievementsInfo, callback) => {
// reverse achievementsInfo so we display smallest > largest
achievementsInfo.reverse();
async.eachSeries(
achievementsInfo,
(achInfo, nextAchInfo) => {
return this.recordAndDisplayAchievement(
achInfo,
err => {
return nextAchInfo(err);
}
);
},
err => {
return callback(err);
}
);
},
],
err => { err => {
return callback(err); if (err && ErrorReasons.TooMany !== err.reasonCode) {
}); Log.warn(
} { error: err.message, userStatEvent },
], 'Error handling achievement for user stat event'
err => { );
if(err && ErrorReasons.TooMany !== err.reasonCode) { }
Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event'); return nextAchievementTag(null); // always try the next, regardless
} }
return nextAchievementTag(null); // always try the next, regardless );
} }
); );
}); }
}); );
} }
stopMonitoringUserStatEvents() { stopMonitoringUserStatEvents() {
if(this.userStatEventListeners) { if (this.userStatEventListeners) {
this.events.removeMultipleEventListener(this.userStatEventListeners); this.events.removeMultipleEventListener(this.userStatEventListeners);
delete this.userStatEventListeners; delete this.userStatEventListeners;
} }
@ -436,34 +504,38 @@ class Achievements {
getFormatObject(info) { getFormatObject(info) {
return { return {
userName : info.user.username, userName: info.user.username,
userRealName : info.user.properties[UserProps.RealName], userRealName: info.user.properties[UserProps.RealName],
userLocation : info.user.properties[UserProps.Location], userLocation: info.user.properties[UserProps.Location],
userAffils : info.user.properties[UserProps.Affiliations], userAffils: info.user.properties[UserProps.Affiliations],
nodeId : info.client.node, nodeId: info.client.node,
title : info.details.title, title: info.details.title,
//text : info.global ? info.details.globalText : info.details.text, //text : info.global ? info.details.globalText : info.details.text,
points : info.details.points, points: info.details.points,
achievedValue : info.achievedValue, achievedValue: info.achievedValue,
matchField : info.matchField, matchField: info.matchField,
matchValue : info.matchValue, matchValue: info.matchValue,
timestamp : moment(info.timestamp).format(info.dateTimeFormat), timestamp: moment(info.timestamp).format(info.dateTimeFormat),
boardName : Config().general.boardName, boardName: Config().general.boardName,
}; };
} }
getFormattedTextFor(info, textType, defaultSgr = '|07') { getFormattedTextFor(info, textType, defaultSgr = '|07') {
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); const themeDefaults = _.get(
const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr; info.client.currentTheme,
'achievements.defaults',
{}
);
const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr;
const formatObj = this.getFormatObject(info); const formatObj = this.getFormatObject(info);
const wrap = (input) => { const wrap = input => {
const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g'); const re = new RegExp(`{(${Object.keys(formatObj).join('|')})([^}]*)}`, 'g');
return input.replace(re, (m, formatVar, formatOpts) => { return input.replace(re, (m, formatVar, formatOpts) => {
const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr; const varSgr = themeDefaults[`${formatVar}SGR`] || textTypeSgr;
let r = `${varSgr}{${formatVar}`; let r = `${varSgr}{${formatVar}`;
if(formatOpts) { if (formatOpts) {
r += formatOpts; r += formatOpts;
} }
return `${r}}${textTypeSgr}`; return `${r}}${textTypeSgr}`;
@ -480,10 +552,10 @@ class Achievements {
info.client.currentTheme.helpers.getDateTimeFormat(); info.client.currentTheme.helpers.getDateTimeFormat();
const title = this.getFormattedTextFor(info, 'title'); const title = this.getFormattedTextFor(info, 'title');
const text = this.getFormattedTextFor(info, 'text'); const text = this.getFormattedTextFor(info, 'text');
let globalText; let globalText;
if(info.details.globalText) { if (info.details.globalText) {
globalText = this.getFormattedTextFor(info, 'globalText'); globalText = this.getFormattedTextFor(info, 'globalText');
} }
@ -492,13 +564,13 @@ class Achievements {
_.get(info.details, `art.${name}`) || _.get(info.details, `art.${name}`) ||
_.get(info.achievement, `art.${name}`) || _.get(info.achievement, `art.${name}`) ||
_.get(this.config.get(), `art.${name}`); _.get(this.config.get(), `art.${name}`);
if(!spec) { if (!spec) {
return callback(null); return callback(null);
} }
const getArtOpts = { const getArtOpts = {
name : spec, name: spec,
client : this.client, client: this.client,
random : false, random: false,
}; };
getThemeArt(getArtOpts, (err, artInfo) => { getThemeArt(getArtOpts, (err, artInfo) => {
// ignore errors // ignore errors
@ -507,67 +579,86 @@ class Achievements {
}; };
const interruptItems = {}; const interruptItems = {};
let itemTypes = [ 'local' ]; let itemTypes = ['local'];
if(globalText) { if (globalText) {
itemTypes.push('global'); itemTypes.push('global');
} }
async.each(itemTypes, (itemType, nextItemType) => { async.each(
async.waterfall( itemTypes,
[ (itemType, nextItemType) => {
(callback) => { async.waterfall(
getArt(`${itemType}Header`, headerArt => { [
return callback(null, headerArt); callback => {
}); getArt(`${itemType}Header`, headerArt => {
}, return callback(null, headerArt);
(headerArt, callback) => {
getArt(`${itemType}Footer`, footerArt => {
return callback(null, headerArt, footerArt);
});
},
(headerArt, footerArt, callback) => {
const itemText = 'global' === itemType ? globalText : text;
interruptItems[itemType] = {
title,
achievText : itemText,
text : `${title}\r\n${itemText}`,
pause : true,
};
if(headerArt || footerArt) {
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {});
const defaultContentsFormat = '{title}\r\n{message}';
const contentsFormat = 'global' === itemType ?
themeDefaults.globalFormat || defaultContentsFormat :
themeDefaults.format || defaultContentsFormat;
const formatObj = Object.assign(this.getFormatObject(info), {
title : this.getFormattedTextFor(info, 'title', ''), // ''=defaultSgr
message : itemText,
}); });
},
(headerArt, callback) => {
getArt(`${itemType}Footer`, footerArt => {
return callback(null, headerArt, footerArt);
});
},
(headerArt, footerArt, callback) => {
const itemText = 'global' === itemType ? globalText : text;
interruptItems[itemType] = {
title,
achievText: itemText,
text: `${title}\r\n${itemText}`,
pause: true,
};
if (headerArt || footerArt) {
const themeDefaults = _.get(
info.client.currentTheme,
'achievements.defaults',
{}
);
const defaultContentsFormat = '{title}\r\n{message}';
const contentsFormat =
'global' === itemType
? themeDefaults.globalFormat ||
defaultContentsFormat
: themeDefaults.format || defaultContentsFormat;
const contents = pipeToAnsi(stringFormat(contentsFormat, formatObj)); const formatObj = Object.assign(
this.getFormatObject(info),
{
title: this.getFormattedTextFor(
info,
'title',
''
), // ''=defaultSgr
message: itemText,
}
);
interruptItems[itemType].contents = const contents = pipeToAnsi(
`${headerArt || ''}\r\n${contents}\r\n${footerArt || ''}`; stringFormat(contentsFormat, formatObj)
} );
return callback(null);
interruptItems[itemType].contents = `${
headerArt || ''
}\r\n${contents}\r\n${footerArt || ''}`;
}
return callback(null);
},
],
err => {
return nextItemType(err);
} }
], );
err => { },
return nextItemType(err); err => {
} return cb(err, interruptItems);
); }
}, );
err => {
return cb(err, interruptItems);
});
} }
} }
let achievementsInstance; let achievementsInstance;
function getAchievementsEarnedByUser(userId, cb) { function getAchievementsEarnedByUser(userId, cb) {
if(!achievementsInstance) { if (!achievementsInstance) {
return cb(Errors.UnexpectedState('Achievements not initialized')); return cb(Errors.UnexpectedState('Achievements not initialized'));
} }
@ -576,39 +667,42 @@ function getAchievementsEarnedByUser(userId, cb) {
FROM user_achievement FROM user_achievement
WHERE user_id = ? WHERE user_id = ?
ORDER BY DATETIME(timestamp);`, ORDER BY DATETIME(timestamp);`,
[ userId ], [userId],
(err, rows) => { (err, rows) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const earned = rows.map(row => { const earned = rows
.map(row => {
const achievement = Achievement.factory(
achievementsInstance.getAchievementByTag(row.achievement_tag)
);
if (!achievement) {
return;
}
const achievement = Achievement.factory(achievementsInstance.getAchievementByTag(row.achievement_tag)); const earnedInfo = {
if(!achievement) { achievementTag: row.achievement_tag,
return; type: achievement.data.type,
} retroactive: achievement.data.retroactive,
title: row.title,
text: row.text,
points: row.points,
timestamp: moment(row.timestamp),
};
const earnedInfo = { switch (earnedInfo.type) {
achievementTag : row.achievement_tag, case [Achievement.Types.UserStatSet]:
type : achievement.data.type, case [Achievement.Types.UserStatInc]:
retroactive : achievement.data.retroactive, case [Achievement.Types.UserStatIncNewVal]:
title : row.title, earnedInfo.statName = achievement.data.statName;
text : row.text, break;
points : row.points, }
timestamp : moment(row.timestamp),
};
switch(earnedInfo.type) { return earnedInfo;
case [ Achievement.Types.UserStatSet ] : })
case [ Achievement.Types.UserStatInc ] : .filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore).
case [ Achievement.Types.UserStatIncNewVal ] :
earnedInfo.statName = achievement.data.statName;
break;
}
return earnedInfo;
}).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore).
return cb(null, earned); return cb(null, earned);
} }
@ -617,8 +711,8 @@ function getAchievementsEarnedByUser(userId, cb) {
exports.moduleInitialize = (initInfo, cb) => { exports.moduleInitialize = (initInfo, cb) => {
achievementsInstance = new Achievements(initInfo.events); achievementsInstance = new Achievements(initInfo.events);
achievementsInstance.init( err => { achievementsInstance.init(err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }

View File

@ -2,12 +2,12 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const checkAcs = require('./acs_parser.js').parse; const checkAcs = require('./acs_parser.js').parse;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
class ACS { class ACS {
constructor(subject) { constructor(subject) {
@ -16,15 +16,15 @@ class ACS {
static get Defaults() { static get Defaults() {
return { return {
MessageConfRead : 'GM[users]', // list/read MessageConfRead: 'GM[users]', // list/read
MessageConfWrite : 'GM[users]', // post/write MessageConfWrite: 'GM[users]', // post/write
MessageAreaRead : 'GM[users]', // list/read; requires parent conf read MessageAreaRead: 'GM[users]', // list/read; requires parent conf read
MessageAreaWrite : 'GM[users]', // post/write; requires parent conf write MessageAreaWrite: 'GM[users]', // post/write; requires parent conf write
FileAreaRead : 'GM[users]', // list FileAreaRead: 'GM[users]', // list
FileAreaWrite : 'GM[sysops]', // upload FileAreaWrite: 'GM[sysops]', // upload
FileAreaDownload : 'GM[users]', // download FileAreaDownload: 'GM[users]', // download
}; };
} }
@ -32,9 +32,9 @@ class ACS {
acs = acs ? acs[scope] : defaultAcs; acs = acs ? acs[scope] : defaultAcs;
acs = acs || defaultAcs; acs = acs || defaultAcs;
try { try {
return checkAcs(acs, { subject : this.subject } ); return checkAcs(acs, { subject: this.subject });
} catch(e) { } catch (e) {
Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); Log.warn({ exception: e, acs: acs }, 'Exception caught checking ACS');
return false; return false;
} }
} }
@ -76,39 +76,42 @@ class ACS {
hasMenuModuleAccess(modInst) { hasMenuModuleAccess(modInst) {
const acs = _.get(modInst, 'menuConfig.config.acs'); const acs = _.get(modInst, 'menuConfig.config.acs');
if(!_.isString(acs)) { if (!_.isString(acs)) {
return true; // no ACS check req. return true; // no ACS check req.
} }
try { try {
return checkAcs(acs, { subject : this.subject } ); return checkAcs(acs, { subject: this.subject });
} catch(e) { } catch (e) {
Log.warn( { exception : e, acs : acs }, 'Exception caught checking ACS'); Log.warn({ exception: e, acs: acs }, 'Exception caught checking ACS');
return false; return false;
} }
} }
getConditionalValue(condArray, memberName) { getConditionalValue(condArray, memberName) {
if(!Array.isArray(condArray)) { if (!Array.isArray(condArray)) {
// no cond array, just use the value // no cond array, just use the value
return condArray; return condArray;
} }
assert(_.isString(memberName)); assert(_.isString(memberName));
const matchCond = condArray.find( cond => { const matchCond = condArray.find(cond => {
if(_.has(cond, 'acs')) { if (_.has(cond, 'acs')) {
try { try {
return checkAcs(cond.acs, { subject : this.subject } ); return checkAcs(cond.acs, { subject: this.subject });
} catch(e) { } catch (e) {
Log.warn( { exception : e, acs : cond }, 'Exception caught checking ACS'); Log.warn(
{ exception: e, acs: cond },
'Exception caught checking ACS'
);
return false; return false;
} }
} else { } else {
return true; // no ACS check req. return true; // no ACS check req.
} }
}); });
if(matchCond) { if (matchCond) {
return matchCond[memberName]; return matchCond[memberName];
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const events = require('events'); const events = require('events');
const util = require('util'); const util = require('util');
const _ = require('lodash'); const _ = require('lodash');
exports.ANSIEscapeParser = ANSIEscapeParser; exports.ANSIEscapeParser = ANSIEscapeParser;
const CR = 0x0d; const CR = 0x0d;
const LF = 0x0a; const LF = 0x0a;
@ -20,49 +20,47 @@ function ANSIEscapeParser(options) {
events.EventEmitter.call(this); events.EventEmitter.call(this);
this.column = 1; this.column = 1;
this.graphicRendition = {}; this.graphicRendition = {};
this.parseState = { this.parseState = {
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
}; };
options = miscUtil.valueWithDefault(options, { options = miscUtil.valueWithDefault(options, {
mciReplaceChar : '', mciReplaceChar: '',
termHeight : 25, termHeight: 25,
termWidth : 80, termWidth: 80,
trailingLF : 'default', // default|omit|no|yes, ... trailingLF: 'default', // default|omit|no|yes, ...
}); });
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
this.row = Math.min(options?.startRow ?? 1, this.termHeight); this.row = Math.min(options?.startRow ?? 1, this.termHeight);
self.moveCursor = function(cols, rows) { self.moveCursor = function (cols, rows) {
self.column += cols; self.column += cols;
self.row += rows; self.row += rows;
self.column = Math.max(self.column, 1); self.column = Math.max(self.column, 1);
self.column = Math.min(self.column, self.termWidth); // can't move past term width self.column = Math.min(self.column, self.termWidth); // can't move past term width
self.row = Math.max(self.row, 1); self.row = Math.max(self.row, 1);
self.positionUpdated(); self.positionUpdated();
}; };
self.saveCursorPosition = function() { self.saveCursorPosition = function () {
self.savedPosition = { self.savedPosition = {
row : self.row, row: self.row,
column : self.column column: self.column,
}; };
}; };
self.restoreCursorPosition = function() { self.restoreCursorPosition = function () {
self.row = self.savedPosition.row; self.row = self.savedPosition.row;
self.column = self.savedPosition.column; self.column = self.savedPosition.column;
delete self.savedPosition; delete self.savedPosition;
@ -70,29 +68,28 @@ function ANSIEscapeParser(options) {
// self.rowUpdated(); // self.rowUpdated();
}; };
self.clearScreen = function() { self.clearScreen = function () {
self.column = 1; self.column = 1;
self.row = 1; self.row = 1;
self.emit('clear screen'); self.emit('clear screen');
}; };
self.positionUpdated = function () {
self.positionUpdated = function() {
self.emit('position update', self.row, self.column); self.emit('position update', self.row, self.column);
}; };
function literal(text) { function literal(text) {
const len = text.length; const len = text.length;
let pos = 0; let pos = 0;
let start = 0; let start = 0;
let charCode; let charCode;
let lastCharCode; let lastCharCode;
while(pos < len) { while (pos < len) {
charCode = text.charCodeAt(pos) & 0xff; // 8bit clean charCode = text.charCodeAt(pos) & 0xff; // 8bit clean
switch(charCode) { switch (charCode) {
case CR : case CR:
self.emit('literal', text.slice(start, pos)); self.emit('literal', text.slice(start, pos));
start = pos; start = pos;
@ -101,7 +98,7 @@ function ANSIEscapeParser(options) {
self.positionUpdated(); self.positionUpdated();
break; break;
case LF : case LF:
// Handle ANSI saved with UNIX-style LF's only // Handle ANSI saved with UNIX-style LF's only
// vs the CRLF pairs // vs the CRLF pairs
if (lastCharCode !== CR) { if (lastCharCode !== CR) {
@ -116,13 +113,13 @@ function ANSIEscapeParser(options) {
self.positionUpdated(); self.positionUpdated();
break; break;
default : default:
if(self.column === self.termWidth) { if (self.column === self.termWidth) {
self.emit('literal', text.slice(start, pos + 1)); self.emit('literal', text.slice(start, pos + 1));
start = pos + 1; start = pos + 1;
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} else { } else {
@ -138,15 +135,15 @@ function ANSIEscapeParser(options) {
// //
// Finalize this chunk // Finalize this chunk
// //
if(self.column > self.termWidth) { if (self.column > self.termWidth) {
self.column = 1; self.column = 1;
self.row += 1; self.row += 1;
self.positionUpdated(); self.positionUpdated();
} }
const rem = text.slice(start); const rem = text.slice(start);
if(rem) { if (rem) {
self.emit('literal', rem); self.emit('literal', rem);
} }
} }
@ -161,18 +158,18 @@ function ANSIEscapeParser(options) {
var id; var id;
do { do {
pos = mciRe.lastIndex; pos = mciRe.lastIndex;
match = mciRe.exec(buffer); match = mciRe.exec(buffer);
if(null !== match) { if (null !== match) {
if(match.index > pos) { if (match.index > pos) {
literal(buffer.slice(pos, match.index)); literal(buffer.slice(pos, match.index));
} }
mciCode = match[1]; mciCode = match[1];
id = match[2] || null; id = match[2] || null;
if(match[3]) { if (match[3]) {
args = match[3].split(','); args = match[3].split(',');
} else { } else {
args = []; args = [];
@ -180,58 +177,62 @@ function ANSIEscapeParser(options) {
// if MCI codes are changing, save off the current color // if MCI codes are changing, save off the current color
var fullMciCode = mciCode + (id || ''); var fullMciCode = mciCode + (id || '');
if(self.lastMciCode !== fullMciCode) { if (self.lastMciCode !== fullMciCode) {
self.lastMciCode = fullMciCode; self.lastMciCode = fullMciCode;
self.graphicRenditionForErase = _.clone(self.graphicRendition); self.graphicRenditionForErase = _.clone(self.graphicRendition);
} }
self.emit('mci', { self.emit('mci', {
position : [self.row, self.column], position: [self.row, self.column],
mci : mciCode, mci: mciCode,
id : id ? parseInt(id, 10) : null, id: id ? parseInt(id, 10) : null,
args : args, args: args,
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) SGR: ansi.getSGRFromGraphicRendition(self.graphicRendition, true),
}); });
if(self.mciReplaceChar.length > 0) { if (self.mciReplaceChar.length > 0) {
const sgrCtrl = ansi.getSGRFromGraphicRendition(self.graphicRenditionForErase); const sgrCtrl = ansi.getSGRFromGraphicRendition(
self.graphicRenditionForErase
);
self.emit('control', sgrCtrl, 'm', sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)); self.emit(
'control',
sgrCtrl,
'm',
sgrCtrl.slice(2).split(/[;m]/).slice(0, 3)
);
literal(new Array(match[0].length + 1).join(self.mciReplaceChar)); literal(new Array(match[0].length + 1).join(self.mciReplaceChar));
} else { } else {
literal(match[0]); literal(match[0]);
} }
} }
} while (0 !== mciRe.lastIndex);
} while(0 !== mciRe.lastIndex); if (pos < buffer.length) {
if(pos < buffer.length) {
literal(buffer.slice(pos)); literal(buffer.slice(pos));
} }
} }
self.reset = function(input) { self.reset = function (input) {
self.column = 1; self.column = 1;
self.row = Math.min(options?.startRow ?? 1, self.termHeight); self.row = Math.min(options?.startRow ?? 1, self.termHeight);
self.parseState = { self.parseState = {
// ignore anything past EOF marker, if any // ignore anything past EOF marker, if any
buffer : input.split(String.fromCharCode(0x1a), 1)[0], buffer: input.split(String.fromCharCode(0x1a), 1)[0],
re : /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsu])/g, // eslint-disable-line no-control-regex
stop : false, stop: false,
}; };
}; };
self.stop = function() { self.stop = function () {
self.parseState.stop = true; self.parseState.stop = true;
}; };
self.parse = function(input) { self.parse = function (input) {
if(input) { if (input) {
self.reset(input); self.reset(input);
} }
@ -240,53 +241,53 @@ function ANSIEscapeParser(options) {
var match; var match;
var opCode; var opCode;
var args; var args;
var re = self.parseState.re; var re = self.parseState.re;
var buffer = self.parseState.buffer; var buffer = self.parseState.buffer;
self.parseState.stop = false; self.parseState.stop = false;
do { do {
if(self.parseState.stop) { if (self.parseState.stop) {
return; return;
} }
pos = re.lastIndex; pos = re.lastIndex;
match = re.exec(buffer); match = re.exec(buffer);
if(null !== match) { if (null !== match) {
if(match.index > pos) { if (match.index > pos) {
parseMCI(buffer.slice(pos, match.index)); parseMCI(buffer.slice(pos, match.index));
} }
opCode = match[2]; opCode = match[2];
args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints
escape(opCode, args); escape(opCode, args);
//self.emit('chunk', match[0]); //self.emit('chunk', match[0]);
self.emit('control', match[0], opCode, args); self.emit('control', match[0], opCode, args);
} }
} while(0 !== re.lastIndex); } while (0 !== re.lastIndex);
if(pos < buffer.length) { if (pos < buffer.length) {
var lastBit = buffer.slice(pos); var lastBit = buffer.slice(pos);
// :TODO: check for various ending LF's, not just DOS \r\n // :TODO: check for various ending LF's, not just DOS \r\n
if('\r\n' === lastBit.slice(-2).toString()) { if ('\r\n' === lastBit.slice(-2).toString()) {
switch(self.trailingLF) { switch (self.trailingLF) {
case 'default' : case 'default':
// //
// Default is to *not* omit the trailing LF // Default is to *not* omit the trailing LF
// if we're going to end on termHeight // if we're going to end on termHeight
// //
if(this.termHeight === self.row) { if (this.termHeight === self.row) {
lastBit = lastBit.slice(0, -2); lastBit = lastBit.slice(0, -2);
} }
break; break;
case 'omit' : case 'omit':
case 'no' : case 'no':
case false : case false:
lastBit = lastBit.slice(0, -2); lastBit = lastBit.slice(0, -2);
break; break;
} }
@ -343,69 +344,69 @@ function ANSIEscapeParser(options) {
function escape(opCode, args) { function escape(opCode, args) {
let arg; let arg;
switch(opCode) { switch (opCode) {
// cursor up // cursor up
case 'A' : case 'A':
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(0, -arg); self.moveCursor(0, -arg);
break; break;
// cursor down // cursor down
case 'B' : case 'B':
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(0, arg); self.moveCursor(0, arg);
break; break;
// cursor forward/right // cursor forward/right
case 'C' : case 'C':
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(arg, 0); self.moveCursor(arg, 0);
break; break;
// cursor back/left // cursor back/left
case 'D' : case 'D':
//arg = args[0] || 1; //arg = args[0] || 1;
arg = isNaN(args[0]) ? 1 : args[0]; arg = isNaN(args[0]) ? 1 : args[0];
self.moveCursor(-arg, 0); self.moveCursor(-arg, 0);
break; break;
case 'f' : // horiz & vertical case 'f': // horiz & vertical
case 'H' : // cursor position case 'H': // cursor position
//self.row = args[0] || 1; //self.row = args[0] || 1;
//self.column = args[1] || 1; //self.column = args[1] || 1;
self.row = isNaN(args[0]) ? 1 : args[0]; self.row = isNaN(args[0]) ? 1 : args[0];
self.column = isNaN(args[1]) ? 1 : args[1]; self.column = isNaN(args[1]) ? 1 : args[1];
//self.rowUpdated(); //self.rowUpdated();
self.positionUpdated(); self.positionUpdated();
break; break;
// save position // save position
case 's' : case 's':
self.saveCursorPosition(); self.saveCursorPosition();
break; break;
// restore position // restore position
case 'u' : case 'u':
self.restoreCursorPosition(); self.restoreCursorPosition();
break; break;
// set graphic rendition // set graphic rendition
case 'm' : case 'm':
self.graphicRendition.reset = false; self.graphicRendition.reset = false;
for(let i = 0, len = args.length; i < len; ++i) { for (let i = 0, len = args.length; i < len; ++i) {
arg = args[i]; arg = args[i];
if(ANSIEscapeParser.foregroundColors[arg]) { if (ANSIEscapeParser.foregroundColors[arg]) {
self.graphicRendition.fg = arg; self.graphicRendition.fg = arg;
} else if(ANSIEscapeParser.backgroundColors[arg]) { } else if (ANSIEscapeParser.backgroundColors[arg]) {
self.graphicRendition.bg = arg; self.graphicRendition.bg = arg;
} else if(ANSIEscapeParser.styles[arg]) { } else if (ANSIEscapeParser.styles[arg]) {
switch(arg) { switch (arg) {
case 0 : case 0:
// clear out everything // clear out everything
delete self.graphicRendition.intensity; delete self.graphicRendition.intensity;
delete self.graphicRendition.underline; delete self.graphicRendition.underline;
@ -421,49 +422,52 @@ function ANSIEscapeParser(options) {
//self.graphicRendition.bg = 49; //self.graphicRendition.bg = 49;
break; break;
case 1 : case 1:
case 2 : case 2:
case 22 : case 22:
self.graphicRendition.intensity = arg; self.graphicRendition.intensity = arg;
break; break;
case 4 : case 4:
case 24 : case 24:
self.graphicRendition.underline = arg; self.graphicRendition.underline = arg;
break; break;
case 5 : case 5:
case 6 : case 6:
case 25 : case 25:
self.graphicRendition.blink = arg; self.graphicRendition.blink = arg;
break; break;
case 7 : case 7:
case 27 : case 27:
self.graphicRendition.negative = arg; self.graphicRendition.negative = arg;
break; break;
case 8 : case 8:
case 28 : case 28:
self.graphicRendition.invisible = arg; self.graphicRendition.invisible = arg;
break; break;
default : default:
Log.trace( { attribute : arg }, 'Unknown attribute while parsing ANSI'); Log.trace(
{ attribute: arg },
'Unknown attribute while parsing ANSI'
);
break; break;
} }
} }
} }
self.emit('sgr update', self.graphicRendition); self.emit('sgr update', self.graphicRendition);
break; // m break; // m
// :TODO: s, u, K // :TODO: s, u, K
// erase display/screen // erase display/screen
case 'J' : case 'J':
// :TODO: Handle other 'J' types! // :TODO: Handle other 'J' types!
if(2 === args[0]) { if (2 === args[0]) {
self.clearScreen(); self.clearScreen();
} }
break; break;
@ -474,30 +478,30 @@ function ANSIEscapeParser(options) {
util.inherits(ANSIEscapeParser, events.EventEmitter); util.inherits(ANSIEscapeParser, events.EventEmitter);
ANSIEscapeParser.foregroundColors = { ANSIEscapeParser.foregroundColors = {
30 : 'black', 30: 'black',
31 : 'red', 31: 'red',
32 : 'green', 32: 'green',
33 : 'yellow', 33: 'yellow',
34 : 'blue', 34: 'blue',
35 : 'magenta', 35: 'magenta',
36 : 'cyan', 36: 'cyan',
37 : 'white', 37: 'white',
39 : 'default', // same as white for most implementations 39: 'default', // same as white for most implementations
90 : 'grey' 90: 'grey',
}; };
Object.freeze(ANSIEscapeParser.foregroundColors); Object.freeze(ANSIEscapeParser.foregroundColors);
ANSIEscapeParser.backgroundColors = { ANSIEscapeParser.backgroundColors = {
40 : 'black', 40: 'black',
41 : 'red', 41: 'red',
42 : 'green', 42: 'green',
43 : 'yellow', 43: 'yellow',
44 : 'blue', 44: 'blue',
45 : 'magenta', 45: 'magenta',
46 : 'cyan', 46: 'cyan',
47 : 'white', 47: 'white',
49 : 'default', // same as black for most implementations 49: 'default', // same as black for most implementations
}; };
Object.freeze(ANSIEscapeParser.backgroundColors); Object.freeze(ANSIEscapeParser.backgroundColors);
@ -512,24 +516,23 @@ Object.freeze(ANSIEscapeParser.backgroundColors);
// can be grouped by concept here in code. // can be grouped by concept here in code.
// //
ANSIEscapeParser.styles = { ANSIEscapeParser.styles = {
0 : 'default', // Everything disabled 0: 'default', // Everything disabled
1 : 'intensityBright', // aka bold 1: 'intensityBright', // aka bold
2 : 'intensityDim', 2: 'intensityDim',
22 : 'intensityNormal', 22: 'intensityNormal',
4 : 'underlineOn', // Not supported by most BBS-like terminals 4: 'underlineOn', // Not supported by most BBS-like terminals
24 : 'underlineOff', // Not supported by most BBS-like terminals 24: 'underlineOff', // Not supported by most BBS-like terminals
5 : 'blinkSlow', // blinkSlow & blinkFast are generally treated the same 5: 'blinkSlow', // blinkSlow & blinkFast are generally treated the same
6 : 'blinkFast', // blinkSlow & blinkFast are generally treated the same 6: 'blinkFast', // blinkSlow & blinkFast are generally treated the same
25 : 'blinkOff', 25: 'blinkOff',
7 : 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG" 7: 'negativeImageOn', // Generally not supported or treated as "reverse FG & BG"
27 : 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG" 27: 'negativeImageOff', // Generally not supported or treated as "reverse FG & BG"
8 : 'invisibleOn', // FG set to BG 8: 'invisibleOn', // FG set to BG
28 : 'invisibleOff', // Not supported by most BBS-like terminals 28: 'invisibleOff', // Not supported by most BBS-like terminals
}; };
Object.freeze(ANSIEscapeParser.styles); Object.freeze(ANSIEscapeParser.styles);

View File

@ -2,58 +2,61 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser; const ANSIEscapeParser = require('./ansi_escape_parser.js').ANSIEscapeParser;
const ANSI = require('./ansi_term.js'); const ANSI = require('./ansi_term.js');
const { const { splitTextAtTerms, renderStringLength } = require('./string_util.js');
splitTextAtTerms,
renderStringLength
} = require('./string_util.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
module.exports = function ansiPrep(input, options, cb) { module.exports = function ansiPrep(input, options, cb) {
if(!input) { if (!input) {
return cb(null, ''); return cb(null, '');
} }
options.termWidth = options.termWidth || 80; options.termWidth = options.termWidth || 80;
options.termHeight = options.termHeight || 25; options.termHeight = options.termHeight || 25;
options.cols = options.cols || options.termWidth || 80; options.cols = options.cols || options.termWidth || 80;
options.rows = options.rows || options.termHeight || 'auto'; options.rows = options.rows || options.termHeight || 'auto';
options.startCol = options.startCol || 1; options.startCol = options.startCol || 1;
options.exportMode = options.exportMode || false; options.exportMode = options.exportMode || false;
options.fillLines = _.get(options, 'fillLines', true); options.fillLines = _.get(options, 'fillLines', true);
options.indent = options.indent || 0; options.indent = options.indent || 0;
// in auto we start out at 25 rows, but can always expand for more // in auto we start out at 25 rows, but can always expand for more
const canvas = Array.from( { length : 'auto' === options.rows ? 25 : options.rows }, () => Array.from( { length : options.cols}, () => new Object() ) ); const canvas = Array.from(
const parser = new ANSIEscapeParser( { termHeight : options.termHeight, termWidth : options.termWidth } ); { length: 'auto' === options.rows ? 25 : options.rows },
() => Array.from({ length: options.cols }, () => new Object())
);
const parser = new ANSIEscapeParser({
termHeight: options.termHeight,
termWidth: options.termWidth,
});
const state = { const state = {
row : 0, row: 0,
col : 0, col: 0,
}; };
let lastRow = 0; let lastRow = 0;
function ensureRow(row) { function ensureRow(row) {
if(canvas[row]) { if (canvas[row]) {
return; return;
} }
canvas[row] = Array.from( { length : options.cols}, () => new Object() ); canvas[row] = Array.from({ length: options.cols }, () => new Object());
} }
parser.on('position update', (row, col) => { parser.on('position update', (row, col) => {
state.row = row - 1; state.row = row - 1;
state.col = col - 1; state.col = col - 1;
if(0 === state.col) { if (0 === state.col) {
state.initialSgr = state.lastSgr; state.initialSgr = state.lastSgr;
} }
lastRow = Math.max(state.row, lastRow); lastRow = Math.max(state.row, lastRow);
}); });
parser.on('literal', literal => { parser.on('literal', literal => {
@ -62,20 +65,23 @@ module.exports = function ansiPrep(input, options, cb) {
// //
literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, ''); literal = literal.replace(/\r?\n|[\r\u2028\u2029]/g, '');
for(let c of literal) { for (let c of literal) {
if(state.col < options.cols && ('auto' === options.rows || state.row < options.rows)) { if (
state.col < options.cols &&
('auto' === options.rows || state.row < options.rows)
) {
ensureRow(state.row); ensureRow(state.row);
if(0 === state.col) { if (0 === state.col) {
canvas[state.row][state.col].initialSgr = state.initialSgr; canvas[state.row][state.col].initialSgr = state.initialSgr;
} }
canvas[state.row][state.col].char = c; canvas[state.row][state.col].char = c;
if(state.sgr) { if (state.sgr) {
canvas[state.row][state.col].sgr = _.clone(state.sgr); canvas[state.row][state.col].sgr = _.clone(state.sgr);
state.lastSgr = canvas[state.row][state.col].sgr; state.lastSgr = canvas[state.row][state.col].sgr;
state.sgr = null; state.sgr = null;
} }
} }
@ -86,9 +92,9 @@ module.exports = function ansiPrep(input, options, cb) {
parser.on('sgr update', sgr => { parser.on('sgr update', sgr => {
ensureRow(state.row); ensureRow(state.row);
if(state.col < options.cols) { if (state.col < options.cols) {
canvas[state.row][state.col].sgr = _.clone(sgr); canvas[state.row][state.col].sgr = _.clone(sgr);
state.lastSgr = canvas[state.row][state.col].sgr; state.lastSgr = canvas[state.row][state.col].sgr;
} else { } else {
state.sgr = sgr; state.sgr = sgr;
} }
@ -96,8 +102,8 @@ module.exports = function ansiPrep(input, options, cb) {
function getLastPopulatedColumn(row) { function getLastPopulatedColumn(row) {
let col = row.length; let col = row.length;
while(--col > 0) { while (--col > 0) {
if(row[col].char || row[col].sgr) { if (row[col].char || row[col].sgr) {
break; break;
} }
} }
@ -113,18 +119,23 @@ module.exports = function ansiPrep(input, options, cb) {
const lastCol = getLastPopulatedColumn(row) + 1; const lastCol = getLastPopulatedColumn(row) + 1;
let i; let i;
line = options.indent ? line = options.indent
output.length > 0 ? ' '.repeat(options.indent) : '' : ? output.length > 0
''; ? ' '.repeat(options.indent)
: ''
: '';
for(i = 0; i < lastCol; ++i) { for (i = 0; i < lastCol; ++i) {
const col = row[i]; const col = row[i];
sgr = !options.asciiMode && 0 === i ? sgr =
col.initialSgr ? ANSI.getSGRFromGraphicRendition(col.initialSgr) : '' : !options.asciiMode && 0 === i
''; ? col.initialSgr
? ANSI.getSGRFromGraphicRendition(col.initialSgr)
: ''
: '';
if(!options.asciiMode && col.sgr) { if (!options.asciiMode && col.sgr) {
sgr += ANSI.getSGRFromGraphicRendition(col.sgr); sgr += ANSI.getSGRFromGraphicRendition(col.sgr);
} }
@ -133,19 +144,22 @@ module.exports = function ansiPrep(input, options, cb) {
output += line; output += line;
if(i < row.length) { if (i < row.length) {
output += `${options.asciiMode ? '' : ANSI.blackBG()}`; output += `${options.asciiMode ? '' : ANSI.blackBG()}`;
if(options.fillLines) { if (options.fillLines) {
output += `${row.slice(i).map( () => ' ').join('')}`;//${lastSgr}`; output += `${row
.slice(i)
.map(() => ' ')
.join('')}`; //${lastSgr}`;
} }
} }
if(options.startCol + i < options.termWidth || options.forceLineTerm) { if (options.startCol + i < options.termWidth || options.forceLineTerm) {
output += '\r\n'; output += '\r\n';
} }
}); });
if(options.exportMode) { if (options.exportMode) {
// //
// If we're in export mode, we do some additional hackery: // If we're in export mode, we do some additional hackery:
// //
@ -156,7 +170,7 @@ module.exports = function ansiPrep(input, options, cb) {
// * Replace contig spaces with ESC[<N>C as well to save... space. // * Replace contig spaces with ESC[<N>C as well to save... space.
// //
// :TODO: this would be better to do as part of the processing above, but this will do for now // :TODO: this would be better to do as part of the processing above, but this will do for now
const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with const MAX_CHARS = 79 - 8; // 79 max, - 8 for max ESC seq's we may prefix a line with
let exportOutput = ''; let exportOutput = '';
let m; let m;
@ -167,30 +181,30 @@ module.exports = function ansiPrep(input, options, cb) {
splitTextAtTerms(output).forEach(fullLine => { splitTextAtTerms(output).forEach(fullLine => {
renderStart = 0; renderStart = 0;
while(fullLine.length > 0) { while (fullLine.length > 0) {
let splitAt; let splitAt;
const ANSI_REGEXP = ANSI.getFullMatchRegExp(); const ANSI_REGEXP = ANSI.getFullMatchRegExp();
wantMore = true; wantMore = true;
while((m = ANSI_REGEXP.exec(fullLine))) { while ((m = ANSI_REGEXP.exec(fullLine))) {
afterSeq = m.index + m[0].length; afterSeq = m.index + m[0].length;
if(afterSeq < MAX_CHARS) { if (afterSeq < MAX_CHARS) {
// after current seq // after current seq
splitAt = afterSeq; splitAt = afterSeq;
} else { } else {
if(m.index < MAX_CHARS) { if (m.index < MAX_CHARS) {
// before last found seq // before last found seq
splitAt = m.index; splitAt = m.index;
wantMore = false; // can't eat up any more wantMore = false; // can't eat up any more
} }
break; // seq's beyond this point are >= MAX_CHARS break; // seq's beyond this point are >= MAX_CHARS
} }
} }
if(splitAt) { if (splitAt) {
if(wantMore) { if (wantMore) {
splitAt = Math.min(fullLine.length, MAX_CHARS - 1); splitAt = Math.min(fullLine.length, MAX_CHARS - 1);
} }
} else { } else {
@ -202,7 +216,8 @@ module.exports = function ansiPrep(input, options, cb) {
renderStart += renderStringLength(part); renderStart += renderStringLength(part);
exportOutput += `${part}\r\n`; exportOutput += `${part}\r\n`;
if(fullLine.length > 0) { // more to go for this line? if (fullLine.length > 0) {
// more to go for this line?
exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`; exportOutput += `${ANSI.up()}${ANSI.right(renderStart)}`;
} else { } else {
exportOutput += ANSI.up(); exportOutput += ANSI.up();

View File

@ -43,48 +43,48 @@
// //
// ENiGMA½ // ENiGMA½
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
// deps // deps
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.getFullMatchRegExp = getFullMatchRegExp; exports.getFullMatchRegExp = getFullMatchRegExp;
exports.getFGColorValue = getFGColorValue; exports.getFGColorValue = getFGColorValue;
exports.getBGColorValue = getBGColorValue; exports.getBGColorValue = getBGColorValue;
exports.sgr = sgr; exports.sgr = sgr;
exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition; exports.getSGRFromGraphicRendition = getSGRFromGraphicRendition;
exports.clearScreen = clearScreen; exports.clearScreen = clearScreen;
exports.resetScreen = resetScreen; exports.resetScreen = resetScreen;
exports.normal = normal; exports.normal = normal;
exports.goHome = goHome; exports.goHome = goHome;
exports.disableVT100LineWrapping = disableVT100LineWrapping; exports.disableVT100LineWrapping = disableVT100LineWrapping;
exports.setSyncTermFont = setSyncTermFont; exports.setSyncTermFont = setSyncTermFont;
exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias; exports.getSyncTermFontFromAlias = getSyncTermFontFromAlias;
exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias; exports.setSyncTermFontWithAlias = setSyncTermFontWithAlias;
exports.setCursorStyle = setCursorStyle; exports.setCursorStyle = setCursorStyle;
exports.setEmulatedBaudRate = setEmulatedBaudRate; exports.setEmulatedBaudRate = setEmulatedBaudRate;
exports.vtxHyperlink = vtxHyperlink; exports.vtxHyperlink = vtxHyperlink;
// //
// See also // See also
// https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js // https://github.com/TooTallNate/ansi.js/blob/master/lib/ansi.js
const ESC_CSI = '\u001b['; const ESC_CSI = '\u001b[';
const CONTROL = { const CONTROL = {
up : 'A', up: 'A',
down : 'B', down: 'B',
forward : 'C', forward: 'C',
right : 'C', right: 'C',
back : 'D', back: 'D',
left : 'D', left: 'D',
nextLine : 'E', nextLine: 'E',
prevLine : 'F', prevLine: 'F',
horizAbsolute : 'G', horizAbsolute: 'G',
// //
// CSI [ p1 ] J // CSI [ p1 ] J
@ -103,10 +103,10 @@ const CONTROL = {
// * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1 // * NetRunner: Always clears a screen *height* (e.g. 25) regardless of p1
// and screen remainder // and screen remainder
// //
eraseData : 'J', eraseData: 'J',
eraseLine : 'K', eraseLine: 'K',
insertLine : 'L', insertLine: 'L',
// //
// CSI [ p1 ] M // CSI [ p1 ] M
@ -128,28 +128,28 @@ const CONTROL = {
// incompatibilities & oddities around this sequence. ANSI-BBS // incompatibilities & oddities around this sequence. ANSI-BBS
// states that it *should* work with any value of p1. // states that it *should* work with any value of p1.
// //
deleteLine : 'M', deleteLine: 'M',
ansiMusic : 'M', ansiMusic: 'M',
scrollUp : 'S', scrollUp: 'S',
scrollDown : 'T', scrollDown: 'T',
setScrollRegion : 'r', setScrollRegion: 'r',
savePos : 's', savePos: 's',
restorePos : 'u', restorePos: 'u',
queryPos : '6n', queryPos: '6n',
queryScreenSize : '255n', // See bansi.txt queryScreenSize: '255n', // See bansi.txt
goto : 'H', // row Pr, column Pc -- same as f goto: 'H', // row Pr, column Pc -- same as f
gotoAlt : 'f', // same as H gotoAlt: 'f', // same as H
blinkToBrightIntensity : '?33h', blinkToBrightIntensity: '?33h',
blinkNormal : '?33l', blinkNormal: '?33l',
emulationSpeed : '*r', // Set output emulation speed. See cterm.txt emulationSpeed: '*r', // Set output emulation speed. See cterm.txt
hideCursor : '?25l', // Nonstandard - cterm.txt hideCursor: '?25l', // Nonstandard - cterm.txt
showCursor : '?25h', // Nonstandard - cterm.txt showCursor: '?25h', // Nonstandard - cterm.txt
queryDeviceAttributes : 'c', // Nonstandard - cterm.txt queryDeviceAttributes: 'c', // Nonstandard - cterm.txt
// :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes // :TODO: see https://code.google.com/p/conemu-maximus5/wiki/AnsiEscapeCodes
// apparently some terms can report screen size and text area via 18t and 19t // apparently some terms can report screen size and text area via 18t and 19t
@ -160,41 +160,44 @@ const CONTROL = {
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
// //
const SGRValues = { const SGRValues = {
reset : 0, reset: 0,
bold : 1, bold: 1,
dim : 2, dim: 2,
blink : 5, blink: 5,
fastBlink : 6, fastBlink: 6,
negative : 7, negative: 7,
hidden : 8, hidden: 8,
normal : 22, // normal: 22, //
steady : 25, steady: 25,
positive : 27, positive: 27,
black : 30, black: 30,
red : 31, red: 31,
green : 32, green: 32,
yellow : 33, yellow: 33,
blue : 34, blue: 34,
magenta : 35, magenta: 35,
cyan : 36, cyan: 36,
white : 37, white: 37,
blackBG : 40, blackBG: 40,
redBG : 41, redBG: 41,
greenBG : 42, greenBG: 42,
yellowBG : 43, yellowBG: 43,
blueBG : 44, blueBG: 44,
magentaBG : 45, magentaBG: 45,
cyanBG : 46, cyanBG: 46,
whiteBG : 47, whiteBG: 47,
}; };
function getFullMatchRegExp(flags = 'g') { function getFullMatchRegExp(flags = 'g') {
// :TODO: expand this a bit - see strip-ansi/etc. // :TODO: expand this a bit - see strip-ansi/etc.
// :TODO: \u009b ? // :TODO: \u009b ?
return new RegExp(/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/, flags); // eslint-disable-line no-control-regex return new RegExp(
/[\u001b][[()#;?]*([0-9]{1,4}(?:;[0-9]{0,4})*)?([0-9A-ORZcf-npqrsuy=><])/,
flags
); // eslint-disable-line no-control-regex
} }
function getFGColorValue(name) { function getFGColorValue(name) {
@ -205,7 +208,6 @@ function getBGColorValue(name) {
return SGRValues[name + 'BG']; return SGRValues[name + 'BG'];
} }
// See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt // See http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/src/conio/cterm.txt
// :TODO: document // :TODO: document
// :TODO: Create mappings for aliases... maybe make this a map to values instead // :TODO: Create mappings for aliases... maybe make this a map to values instead
@ -275,49 +277,48 @@ const SYNCTERM_FONT_AND_ENCODING_TABLE = [
// replaced with '_' for lookup purposes. // replaced with '_' for lookup purposes.
// //
const FONT_ALIAS_TO_SYNCTERM_MAP = { const FONT_ALIAS_TO_SYNCTERM_MAP = {
'cp437' : 'cp437', cp437: 'cp437',
'ibm_vga' : 'cp437', ibm_vga: 'cp437',
'ibmpc' : 'cp437', ibmpc: 'cp437',
'ibm_pc' : 'cp437', ibm_pc: 'cp437',
'pc' : 'cp437', pc: 'cp437',
'cp437_art' : 'cp437', cp437_art: 'cp437',
'ibmpcart' : 'cp437', ibmpcart: 'cp437',
'ibmpc_art' : 'cp437', ibmpc_art: 'cp437',
'ibm_pc_art' : 'cp437', ibm_pc_art: 'cp437',
'msdos_art' : 'cp437', msdos_art: 'cp437',
'msdosart' : 'cp437', msdosart: 'cp437',
'pc_art' : 'cp437', pc_art: 'cp437',
'pcart' : 'cp437', pcart: 'cp437',
'ibm_vga50' : 'cp437', ibm_vga50: 'cp437',
'ibm_vga25g' : 'cp437', ibm_vga25g: 'cp437',
'ibm_ega' : 'cp437', ibm_ega: 'cp437',
'ibm_ega43' : 'cp437', ibm_ega43: 'cp437',
'topaz' : 'topaz', topaz: 'topaz',
'amiga_topaz_1' : 'topaz', amiga_topaz_1: 'topaz',
'amiga_topaz_1+' : 'topaz_plus', 'amiga_topaz_1+': 'topaz_plus',
'topazplus' : 'topaz_plus', topazplus: 'topaz_plus',
'topaz_plus' : 'topaz_plus', topaz_plus: 'topaz_plus',
'amiga_topaz_2' : 'topaz', amiga_topaz_2: 'topaz',
'amiga_topaz_2+' : 'topaz_plus', 'amiga_topaz_2+': 'topaz_plus',
'topaz2plus' : 'topaz_plus', topaz2plus: 'topaz_plus',
'pot_noodle' : 'pot_noodle', pot_noodle: 'pot_noodle',
'p0tnoodle' : 'pot_noodle', p0tnoodle: 'pot_noodle',
'amiga_p0t-noodle' : 'pot_noodle', 'amiga_p0t-noodle': 'pot_noodle',
'mo_soul' : 'mo_soul', mo_soul: 'mo_soul',
'mosoul' : 'mo_soul', mosoul: 'mo_soul',
'mo\'soul' : 'mo_soul', "mo'soul": 'mo_soul',
'amiga_mosoul' : 'mo_soul', amiga_mosoul: 'mo_soul',
'amiga_microknight' : 'microknight', amiga_microknight: 'microknight',
'amiga_microknight+' : 'microknight_plus', 'amiga_microknight+': 'microknight_plus',
'atari' : 'atari',
'atarist' : 'atari',
atari: 'atari',
atarist: 'atari',
}; };
function setSyncTermFont(name, fontPage) { function setSyncTermFont(name, fontPage) {
@ -326,7 +327,7 @@ function setSyncTermFont(name, fontPage) {
assert(p1 >= 0 && p1 <= 3); assert(p1 >= 0 && p1 <= 3);
const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name); const p2 = SYNCTERM_FONT_AND_ENCODING_TABLE.indexOf(name);
if(p2 > -1) { if (p2 > -1) {
return `${ESC_CSI}${p1};${p2} D`; return `${ESC_CSI}${p1};${p2} D`;
} }
@ -343,31 +344,30 @@ function setSyncTermFontWithAlias(nameOrAlias) {
} }
const DEC_CURSOR_STYLE = { const DEC_CURSOR_STYLE = {
'blinking block' : 0, 'blinking block': 0,
'default' : 1, default: 1,
'steady block' : 2, 'steady block': 2,
'blinking underline' : 3, 'blinking underline': 3,
'steady underline' : 4, 'steady underline': 4,
'blinking bar' : 5, 'blinking bar': 5,
'steady bar' : 6, 'steady bar': 6,
}; };
function setCursorStyle(cursorStyle) { function setCursorStyle(cursorStyle) {
const ps = DEC_CURSOR_STYLE[cursorStyle]; const ps = DEC_CURSOR_STYLE[cursorStyle];
if(ps) { if (ps) {
return `${ESC_CSI}${ps} q`; return `${ESC_CSI}${ps} q`;
} }
return ''; return '';
} }
// Create methods such as up(), nextLine(),... // Create methods such as up(), nextLine(),...
Object.keys(CONTROL).forEach(function onControlName(name) { Object.keys(CONTROL).forEach(function onControlName(name) {
const code = CONTROL[name]; const code = CONTROL[name];
exports[name] = function() { exports[name] = function () {
let c = code; let c = code;
if(arguments.length > 0) { if (arguments.length > 0) {
// arguments are array like -- we want an array // arguments are array like -- we want an array
c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code; c = Array.prototype.slice.call(arguments).map(Math.round).join(';') + code;
} }
@ -376,10 +376,10 @@ Object.keys(CONTROL).forEach(function onControlName(name) {
}); });
// Create various color methods such as white(), yellowBG(), reset(), ... // Create various color methods such as white(), yellowBG(), reset(), ...
Object.keys(SGRValues).forEach( name => { Object.keys(SGRValues).forEach(name => {
const code = SGRValues[name]; const code = SGRValues[name];
exports[name] = function() { exports[name] = function () {
return `${ESC_CSI}${code}m`; return `${ESC_CSI}${code}m`;
}; };
}); });
@ -390,18 +390,18 @@ function sgr() {
// - Each element can be either a integer or string found in SGRValues // - Each element can be either a integer or string found in SGRValues
// which in turn maps to a integer // which in turn maps to a integer
// //
if(arguments.length <= 0) { if (arguments.length <= 0) {
return ''; return '';
} }
let result = []; let result = [];
const args = Array.isArray(arguments[0]) ? arguments[0] : arguments; const args = Array.isArray(arguments[0]) ? arguments[0] : arguments;
for(let i = 0; i < args.length; ++i) { for (let i = 0; i < args.length; ++i) {
const arg = args[i]; const arg = args[i];
if(_.isString(arg) && arg in SGRValues) { if (_.isString(arg) && arg in SGRValues) {
result.push(SGRValues[arg]); result.push(SGRValues[arg]);
} else if(_.isNumber(arg)) { } else if (_.isNumber(arg)) {
result.push(arg); result.push(arg);
} }
} }
@ -414,25 +414,25 @@ function sgr() {
// to a ANSI SGR sequence. // to a ANSI SGR sequence.
// //
function getSGRFromGraphicRendition(graphicRendition, initialReset) { function getSGRFromGraphicRendition(graphicRendition, initialReset) {
let sgrSeq = []; let sgrSeq = [];
let styleCount = 0; let styleCount = 0;
[ 'intensity', 'underline', 'blink', 'negative', 'invisible' ].forEach( s => { ['intensity', 'underline', 'blink', 'negative', 'invisible'].forEach(s => {
if(graphicRendition[s]) { if (graphicRendition[s]) {
sgrSeq.push(graphicRendition[s]); sgrSeq.push(graphicRendition[s]);
++styleCount; ++styleCount;
} }
}); });
if(graphicRendition.fg) { if (graphicRendition.fg) {
sgrSeq.push(graphicRendition.fg); sgrSeq.push(graphicRendition.fg);
} }
if(graphicRendition.bg) { if (graphicRendition.bg) {
sgrSeq.push(graphicRendition.bg); sgrSeq.push(graphicRendition.bg);
} }
if(0 === styleCount || initialReset) { if (0 === styleCount || initialReset) {
sgrSeq.unshift(0); sgrSeq.unshift(0);
} }
@ -452,11 +452,11 @@ function resetScreen() {
} }
function normal() { function normal() {
return sgr( [ 'normal', 'reset' ] ); return sgr(['normal', 'reset']);
} }
function goHome() { function goHome() {
return exports.goto(); // no params = home = 1,1 return exports.goto(); // no params = home = 1,1
} }
// //
@ -476,32 +476,36 @@ function disableVT100LineWrapping() {
} }
function setEmulatedBaudRate(rate) { function setEmulatedBaudRate(rate) {
const speed = { const speed =
unlimited : 0, {
off : 0, unlimited: 0,
0 : 0, off: 0,
300 : 1, 0: 0,
600 : 2, 300: 1,
1200 : 3, 600: 2,
2400 : 4, 1200: 3,
4800 : 5, 2400: 4,
9600 : 6, 4800: 5,
19200 : 7, 9600: 6,
38400 : 8, 19200: 7,
57600 : 9, 38400: 8,
76800 : 10, 57600: 9,
115200 : 11, 76800: 10,
}[rate] || 0; 115200: 11,
}[rate] || 0;
return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed); return 0 === speed ? exports.emulationSpeed() : exports.emulationSpeed(1, speed);
} }
function vtxHyperlink(client, url, len) { function vtxHyperlink(client, url, len) {
if(!client.terminalSupports('vtx_hyperlink')) { if (!client.terminalSupports('vtx_hyperlink')) {
return ''; return '';
} }
len = len || url.length; len = len || url.length;
url = url.split('').map(c => c.charCodeAt(0)).join(';'); url = url
.split('')
.map(c => c.charCodeAt(0))
.join(';');
return `${ESC_CSI}1;${len};1;1;${url}\\`; return `${ESC_CSI}1;${len};1;1;${url}\\`;
} }

View File

@ -2,19 +2,19 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const { MenuModule } = require('../core/menu_module.js'); const { MenuModule } = require('../core/menu_module.js');
const { resetScreen } = require('../core/ansi_term.js'); const { resetScreen } = require('../core/ansi_term.js');
const { Errors } = require('../core/enig_error.js'); const { Errors } = require('../core/enig_error.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const SSHClient = require('ssh2').Client; const SSHClient = require('ssh2').Client;
exports.moduleInfo = { exports.moduleInfo = {
name : 'ArchaicNET', name: 'ArchaicNET',
desc : 'ArchaicNET Access Module', desc: 'ArchaicNET Access Module',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class ArchaicNETModule extends MenuModule { exports.getModule = class ArchaicNETModule extends MenuModule {
@ -22,10 +22,10 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
super(options); super(options);
// establish defaults // establish defaults
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.archaicbinary.net'; this.config.host = this.config.host || 'bbs.archaicbinary.net';
this.config.sshPort = this.config.sshPort || 2222; this.config.sshPort = this.config.sshPort || 2222;
this.config.rloginPort = this.config.rloginPort || 8513; this.config.rloginPort = this.config.rloginPort || 8513;
} }
initSequence() { initSequence() {
@ -35,10 +35,12 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
async.series( async.series(
[ [
function validateConfig(callback) { function validateConfig(callback) {
const reqConfs = [ 'username', 'password', 'bbsTag' ]; const reqConfs = ['username', 'password', 'bbsTag'];
for(let req of reqConfs) { for (let req of reqConfs) {
if(!_.isString(_.get(self, [ 'config', req ]))) { if (!_.isString(_.get(self, ['config', req]))) {
return callback(Errors.MissingConfig(`Config requires "${req}"`)); return callback(
Errors.MissingConfig(`Config requires "${req}"`)
);
} }
} }
return callback(null); return callback(null);
@ -51,8 +53,8 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
let needRestore = false; let needRestore = false;
//let pipedStream; //let pipedStream;
const restorePipe = function() { const restorePipe = function () {
if(needRestore && !clientTerminated) { if (needRestore && !clientTerminated) {
self.client.restoreDataHandler(); self.client.restoreDataHandler();
needRestore = false; needRestore = false;
} }
@ -61,75 +63,91 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
sshClient.on('ready', () => { sshClient.on('ready', () => {
// track client termination so we can clean up early // track client termination so we can clean up early
self.client.once('end', () => { self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating ArchaicNET connection'); self.client.log.info(
'Connection ended. Terminating ArchaicNET connection'
);
clientTerminated = true; clientTerminated = true;
return sshClient.end(); return sshClient.end();
}); });
// establish tunnel for rlogin // establish tunnel for rlogin
const fwdPort = self.config.rloginPort + self.client.node; const fwdPort = self.config.rloginPort + self.client.node;
sshClient.forwardOut('127.0.0.1', fwdPort, self.config.host, self.config.rloginPort, (err, stream) => { sshClient.forwardOut(
if(err) { '127.0.0.1',
return sshClient.end(); fwdPort,
self.config.host,
self.config.rloginPort,
(err, stream) => {
if (err) {
return sshClient.end();
}
//
// Send rlogin - [<bbsTag>]<userName> e.g. [Xibalba]NuSkooler
//
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin);
// we need to filter I/O for escape/de-escaping zmodem and the like
self.client.setTemporaryDirectDataHandler(data => {
const tmp = data
.toString('binary')
.replace(/\xff{2}/g, '\xff'); // de-escape
stream.write(Buffer.from(tmp, 'binary'));
});
needRestore = true;
stream.on('data', data => {
const tmp = data
.toString('binary')
.replace(/\xff/g, '\xff\xff'); // escape
self.client.term.rawWrite(Buffer.from(tmp, 'binary'));
});
stream.on('close', () => {
restorePipe();
return sshClient.end();
});
} }
);
//
// Send rlogin - [<bbsTag>]<userName> e.g. [Xibalba]NuSkooler
//
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin);
// we need to filter I/O for escape/de-escaping zmodem and the like
self.client.setTemporaryDirectDataHandler(data => {
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape
stream.write(Buffer.from(tmp, 'binary'));
});
needRestore = true;
stream.on('data', data => {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape
self.client.term.rawWrite(Buffer.from(tmp, 'binary'));
});
stream.on('close', () => {
restorePipe();
return sshClient.end();
});
});
}); });
sshClient.on('error', err => { sshClient.on('error', err => {
return self.client.log.info(`ArchaicNET SSH client error: ${err.message}`); return self.client.log.info(
`ArchaicNET SSH client error: ${err.message}`
);
}); });
sshClient.on('close', hadError => { sshClient.on('close', hadError => {
if(hadError) { if (hadError) {
self.client.warn('Closing ArchaicNET SSH due to error'); self.client.warn('Closing ArchaicNET SSH due to error');
} }
restorePipe(); restorePipe();
return callback(null); return callback(null);
}); });
self.client.log.trace( { host : self.config.host, port : self.config.sshPort }, 'Connecting to ArchaicNET'); self.client.log.trace(
sshClient.connect( { { host: self.config.host, port: self.config.sshPort },
host : self.config.host, 'Connecting to ArchaicNET'
port : self.config.sshPort, );
username : self.config.username, sshClient.connect({
password : self.config.password, host: self.config.host,
port: self.config.sshPort,
username: self.config.username,
password: self.config.password,
}); });
} },
], ],
err => { err => {
if(err) { if (err) {
self.client.log.warn( { error : err.message }, 'ArchaicNET error'); self.client.log.warn({ error: err.message }, 'ArchaicNET error');
} }
// if the client is stil here, go to previous // if the client is stil here, go to previous
if(!clientTerminated) { if (!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }
} }
); );
} }
}; };

View File

@ -2,26 +2,26 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const resolveMimeType = require('./mime_util.js').resolveMimeType; const resolveMimeType = require('./mime_util.js').resolveMimeType;
const Events = require('./events.js'); const Events = require('./events.js');
// base/modules // base/modules
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const _ = require('lodash'); const _ = require('lodash');
const pty = require('node-pty'); const pty = require('node-pty');
const paths = require('path'); const paths = require('path');
let archiveUtil; let archiveUtil;
class Archiver { class Archiver {
constructor(config) { constructor(config) {
this.compress = config.compress; this.compress = config.compress;
this.decompress = config.decompress; this.decompress = config.decompress;
this.list = config.list; this.list = config.list;
this.extract = config.extract; this.extract = config.extract;
} }
ok() { ok() {
@ -29,21 +29,32 @@ class Archiver {
} }
can(what) { can(what) {
if(!_.has(this, [ what, 'cmd' ]) || !_.has(this, [ what, 'args' ])) { if (!_.has(this, [what, 'cmd']) || !_.has(this, [what, 'args'])) {
return false; return false;
} }
return _.isString(this[what].cmd) && Array.isArray(this[what].args) && this[what].args.length > 0; return (
_.isString(this[what].cmd) &&
Array.isArray(this[what].args) &&
this[what].args.length > 0
);
} }
canCompress() { return this.can('compress'); } canCompress() {
canDecompress() { return this.can('decompress'); } return this.can('compress');
canList() { return this.can('list'); } // :TODO: validate entryMatch }
canExtract() { return this.can('extract'); } canDecompress() {
return this.can('decompress');
}
canList() {
return this.can('list');
} // :TODO: validate entryMatch
canExtract() {
return this.can('extract');
}
} }
module.exports = class ArchiveUtil { module.exports = class ArchiveUtil {
constructor() { constructor() {
this.archivers = {}; this.archivers = {};
this.longestSignature = 0; this.longestSignature = 0;
@ -51,7 +62,7 @@ module.exports = class ArchiveUtil {
// singleton access // singleton access
static getInstance(hotReload = true) { static getInstance(hotReload = true) {
if(!archiveUtil) { if (!archiveUtil) {
archiveUtil = new ArchiveUtil(); archiveUtil = new ArchiveUtil();
archiveUtil.init(hotReload); archiveUtil.init(hotReload);
} }
@ -60,7 +71,7 @@ module.exports = class ArchiveUtil {
init(hotReload = true) { init(hotReload = true) {
this.reloadConfig(); this.reloadConfig();
if(hotReload) { if (hotReload) {
Events.on(Events.getSystemEvents().ConfigChanged, () => { Events.on(Events.getSystemEvents().ConfigChanged, () => {
this.reloadConfig(); this.reloadConfig();
}); });
@ -69,13 +80,12 @@ module.exports = class ArchiveUtil {
reloadConfig() { reloadConfig() {
const config = Config(); const config = Config();
if(_.has(config, 'archives.archivers')) { if (_.has(config, 'archives.archivers')) {
Object.keys(config.archives.archivers).forEach(archKey => { Object.keys(config.archives.archivers).forEach(archKey => {
const archConfig = config.archives.archivers[archKey];
const archiver = new Archiver(archConfig);
const archConfig = config.archives.archivers[archKey]; if (!archiver.ok()) {
const archiver = new Archiver(archConfig);
if(!archiver.ok()) {
// :TODO: Log warning - bad archiver/config // :TODO: Log warning - bad archiver/config
} }
@ -83,27 +93,27 @@ module.exports = class ArchiveUtil {
}); });
} }
if(_.isObject(config.fileTypes)) { if (_.isObject(config.fileTypes)) {
const updateSig = (ft) => { const updateSig = ft => {
ft.sig = Buffer.from(ft.sig, 'hex'); ft.sig = Buffer.from(ft.sig, 'hex');
ft.offset = ft.offset || 0; ft.offset = ft.offset || 0;
// :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well // :TODO: this is broken: sig is NOT this long, it's sig.length long; offset needs to allow for -negative values as well
const sigLen = ft.offset + ft.sig.length; const sigLen = ft.offset + ft.sig.length;
if(sigLen > this.longestSignature) { if (sigLen > this.longestSignature) {
this.longestSignature = sigLen; this.longestSignature = sigLen;
} }
}; };
Object.keys(config.fileTypes).forEach(mimeType => { Object.keys(config.fileTypes).forEach(mimeType => {
const fileType = config.fileTypes[mimeType]; const fileType = config.fileTypes[mimeType];
if(Array.isArray(fileType)) { if (Array.isArray(fileType)) {
fileType.forEach(ft => { fileType.forEach(ft => {
if(ft.sig) { if (ft.sig) {
updateSig(ft); updateSig(ft);
} }
}); });
} else if(fileType.sig) { } else if (fileType.sig) {
updateSig(fileType); updateSig(fileType);
} }
}); });
@ -113,15 +123,16 @@ module.exports = class ArchiveUtil {
getArchiver(mimeTypeOrExtension, justExtention) { getArchiver(mimeTypeOrExtension, justExtention) {
const mimeType = resolveMimeType(mimeTypeOrExtension); const mimeType = resolveMimeType(mimeTypeOrExtension);
if(!mimeType) { // lookup returns false on failure if (!mimeType) {
// lookup returns false on failure
return; return;
} }
const config = Config(); const config = Config();
let fileType = _.get(config, [ 'fileTypes', mimeType ] ); let fileType = _.get(config, ['fileTypes', mimeType]);
if(Array.isArray(fileType)) { if (Array.isArray(fileType)) {
if(!justExtention) { if (!justExtention) {
// need extention for lookup; ambiguous as-is :( // need extention for lookup; ambiguous as-is :(
return; return;
} }
@ -129,12 +140,12 @@ module.exports = class ArchiveUtil {
fileType = fileType.find(ft => justExtention === ft.ext); fileType = fileType.find(ft => justExtention === ft.ext);
} }
if(!_.isObject(fileType)) { if (!_.isObject(fileType)) {
return; return;
} }
if(fileType.archiveHandler) { if (fileType.archiveHandler) {
return _.get( config, [ 'archives', 'archivers', fileType.archiveHandler ] ); return _.get(config, ['archives', 'archivers', fileType.archiveHandler]);
} }
} }
@ -149,37 +160,41 @@ module.exports = class ArchiveUtil {
*/ */
detectType(path, cb) { detectType(path, cb) {
const closeFile = (fd) => { const closeFile = fd => {
fs.close(fd, () => { /* sadface */ }); fs.close(fd, () => {
/* sadface */
});
}; };
fs.open(path, 'r', (err, fd) => { fs.open(path, 'r', (err, fd) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const buf = Buffer.alloc(this.longestSignature); const buf = Buffer.alloc(this.longestSignature);
fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => { fs.read(fd, buf, 0, buf.length, 0, (err, bytesRead) => {
if(err) { if (err) {
closeFile(fd); closeFile(fd);
return cb(err); return cb(err);
} }
const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => { const archFormat = _.findKey(Config().fileTypes, fileTypeInfo => {
const fileTypeInfos = Array.isArray(fileTypeInfo) ? fileTypeInfo : [ fileTypeInfo ]; const fileTypeInfos = Array.isArray(fileTypeInfo)
? fileTypeInfo
: [fileTypeInfo];
return fileTypeInfos.find(fti => { return fileTypeInfos.find(fti => {
if(!fti.sig || !fti.archiveHandler) { if (!fti.sig || !fti.archiveHandler) {
return false; return false;
} }
const lenNeeded = fti.offset + fti.sig.length; const lenNeeded = fti.offset + fti.sig.length;
if(bytesRead < lenNeeded) { if (bytesRead < lenNeeded) {
return false; return false;
} }
const comp = buf.slice(fti.offset, fti.offset + fti.sig.length); const comp = buf.slice(fti.offset, fti.offset + fti.sig.length);
return (fti.sig.equals(comp)); return fti.sig.equals(comp);
}); });
}); });
@ -194,20 +209,26 @@ module.exports = class ArchiveUtil {
// so we have this horrible, horrible hack: // so we have this horrible, horrible hack:
let err; let err;
proc.once('data', d => { proc.once('data', d => {
if(_.isString(d) && d.startsWith('execvp(3) failed.')) { if (_.isString(d) && d.startsWith('execvp(3) failed.')) {
err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`);
} }
}); });
proc.once('exit', exitCode => { proc.once('exit', exitCode => {
return cb(exitCode ? Errors.ExternalProcess(`${action} failed with exit code: ${exitCode}`) : err); return cb(
exitCode
? Errors.ExternalProcess(
`${action} failed with exit code: ${exitCode}`
)
: err
);
}); });
} }
compressTo(archType, archivePath, files, workDir, cb) { compressTo(archType, archivePath, files, workDir, cb) {
const archiver = this.getArchiver(archType, paths.extname(archivePath)); const archiver = this.getArchiver(archType, paths.extname(archivePath));
if(!archiver) { if (!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
} }
@ -217,17 +238,17 @@ module.exports = class ArchiveUtil {
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath: archivePath,
fileList : files.join(' '), // :TODO: probably need same hack as extractTo here! fileList: files.join(' '), // :TODO: probably need same hack as extractTo here!
}; };
// :TODO: DRY with extractTo() // :TODO: DRY with extractTo()
const args = archiver.compress.args.map( arg => { const args = archiver.compress.args.map(arg => {
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
}); });
const fileListPos = args.indexOf('{fileList}'); const fileListPos = args.indexOf('{fileList}');
if(fileListPos > -1) { if (fileListPos > -1) {
// replace {fileList} with 0:n sep file list arguments // replace {fileList} with 0:n sep file list arguments
args.splice.apply(args, [fileListPos, 1].concat(files)); args.splice.apply(args, [fileListPos, 1].concat(files));
} }
@ -235,9 +256,13 @@ module.exports = class ArchiveUtil {
let proc; let proc;
try { try {
proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts(workDir)); proc = pty.spawn(archiver.compress.cmd, args, this.getPtyOpts(workDir));
} catch(e) { } catch (e) {
return cb(Errors.ExternalProcess( return cb(
`Error spawning archiver process "${archiver.compress.cmd}" with args "${args.join(' ')}": ${e.message}`) Errors.ExternalProcess(
`Error spawning archiver process "${
archiver.compress.cmd
}" with args "${args.join(' ')}": ${e.message}`
)
); );
} }
@ -247,7 +272,7 @@ module.exports = class ArchiveUtil {
extractTo(archivePath, extractPath, archType, fileList, cb) { extractTo(archivePath, extractPath, archType, fileList, cb) {
let haveFileList; let haveFileList;
if(!cb && _.isFunction(fileList)) { if (!cb && _.isFunction(fileList)) {
cb = fileList; cb = fileList;
fileList = []; fileList = [];
haveFileList = false; haveFileList = false;
@ -257,29 +282,29 @@ module.exports = class ArchiveUtil {
const archiver = this.getArchiver(archType, paths.extname(archivePath)); const archiver = this.getArchiver(archType, paths.extname(archivePath));
if(!archiver) { if (!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath: archivePath,
extractPath : extractPath, extractPath: extractPath,
}; };
let action = haveFileList ? 'extract' : 'decompress'; let action = haveFileList ? 'extract' : 'decompress';
if('extract' === action && !_.isObject(archiver[action])) { if ('extract' === action && !_.isObject(archiver[action])) {
// we're forced to do a full decompress // we're forced to do a full decompress
action = 'decompress'; action = 'decompress';
haveFileList = false; haveFileList = false;
} }
// we need to treat {fileList} special in that it should be broken up to 0:n args // we need to treat {fileList} special in that it should be broken up to 0:n args
const args = archiver[action].args.map( arg => { const args = archiver[action].args.map(arg => {
return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj); return '{fileList}' === arg ? arg : stringFormat(arg, fmtObj);
}); });
const fileListPos = args.indexOf('{fileList}'); const fileListPos = args.indexOf('{fileList}');
if(fileListPos > -1) { if (fileListPos > -1) {
// replace {fileList} with 0:n sep file list arguments // replace {fileList} with 0:n sep file list arguments
args.splice.apply(args, [fileListPos, 1].concat(fileList)); args.splice.apply(args, [fileListPos, 1].concat(fileList));
} }
@ -287,34 +312,42 @@ module.exports = class ArchiveUtil {
let proc; let proc;
try { try {
proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath)); proc = pty.spawn(archiver[action].cmd, args, this.getPtyOpts(extractPath));
} catch(e) { } catch (e) {
return cb(Errors.ExternalProcess( return cb(
`Error spawning archiver process "${archiver[action].cmd}" with args "${args.join(' ')}": ${e.message}`) Errors.ExternalProcess(
`Error spawning archiver process "${
archiver[action].cmd
}" with args "${args.join(' ')}": ${e.message}`
)
); );
} }
return this.spawnHandler(proc, (haveFileList ? 'Extraction' : 'Decompression'), cb); return this.spawnHandler(proc, haveFileList ? 'Extraction' : 'Decompression', cb);
} }
listEntries(archivePath, archType, cb) { listEntries(archivePath, archType, cb) {
const archiver = this.getArchiver(archType, paths.extname(archivePath)); const archiver = this.getArchiver(archType, paths.extname(archivePath));
if(!archiver) { if (!archiver) {
return cb(Errors.Invalid(`Unknown archive type: ${archType}`)); return cb(Errors.Invalid(`Unknown archive type: ${archType}`));
} }
const fmtObj = { const fmtObj = {
archivePath : archivePath, archivePath: archivePath,
}; };
const args = archiver.list.args.map( arg => stringFormat(arg, fmtObj) ); const args = archiver.list.args.map(arg => stringFormat(arg, fmtObj));
let proc; let proc;
try { try {
proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts()); proc = pty.spawn(archiver.list.cmd, args, this.getPtyOpts());
} catch(e) { } catch (e) {
return cb(Errors.ExternalProcess( return cb(
`Error spawning archiver process "${archiver.list.cmd}" with args "${args.join(' ')}": ${e.message}`) Errors.ExternalProcess(
`Error spawning archiver process "${
archiver.list.cmd
}" with args "${args.join(' ')}": ${e.message}`
)
); );
} }
@ -326,19 +359,24 @@ module.exports = class ArchiveUtil {
}); });
proc.once('exit', exitCode => { proc.once('exit', exitCode => {
if(exitCode) { if (exitCode) {
return cb(Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)); return cb(
Errors.ExternalProcess(`List failed with exit code: ${exitCode}`)
);
} }
const entryGroupOrder = archiver.list.entryGroupOrder || { byteSize : 1, fileName : 2 }; const entryGroupOrder = archiver.list.entryGroupOrder || {
byteSize: 1,
fileName: 2,
};
const entries = []; const entries = [];
const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm'); const entryMatchRe = new RegExp(archiver.list.entryMatch, 'gm');
let m; let m;
while((m = entryMatchRe.exec(output))) { while ((m = entryMatchRe.exec(output))) {
entries.push({ entries.push({
byteSize : parseInt(m[entryGroupOrder.byteSize]), byteSize: parseInt(m[entryGroupOrder.byteSize]),
fileName : m[entryGroupOrder.fileName].trim(), fileName: m[entryGroupOrder.fileName].trim(),
}); });
} }
@ -348,12 +386,12 @@ module.exports = class ArchiveUtil {
getPtyOpts(cwd) { getPtyOpts(cwd) {
const opts = { const opts = {
name : 'enigma-archiver', name: 'enigma-archiver',
cols : 80, cols: 80,
rows : 24, rows: 24,
env : process.env, env: process.env,
}; };
if(cwd) { if (cwd) {
opts.cwd = cwd; opts.cwd = cwd;
} }
// :TODO: set cwd to supplied temp path if not sepcific extract // :TODO: set cwd to supplied temp path if not sepcific extract

View File

@ -2,24 +2,24 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const aep = require('./ansi_escape_parser.js'); const aep = require('./ansi_escape_parser.js');
const sauce = require('./sauce.js'); const sauce = require('./sauce.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const assert = require('assert'); const assert = require('assert');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const _ = require('lodash'); const _ = require('lodash');
exports.getArt = getArt; exports.getArt = getArt;
exports.getArtFromPath = getArtFromPath; exports.getArtFromPath = getArtFromPath;
exports.display = display; exports.display = display;
exports.defaultEncodingFromExtension = defaultEncodingFromExtension; exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
// :TODO: Return MCI code information // :TODO: Return MCI code information
// :TODO: process SAUCE comments // :TODO: process SAUCE comments
@ -28,37 +28,37 @@ exports.defaultEncodingFromExtension = defaultEncodingFromExtension;
const SUPPORTED_ART_TYPES = { const SUPPORTED_ART_TYPES = {
// :TODO: the defualt encoding are really useless if they are all the same ... // :TODO: the defualt encoding are really useless if they are all the same ...
// perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf // perhaps .ansamiga and .ascamiga could be supported as well as overrides via conf
'.ans' : { name : 'ANSI', defaultEncoding : 'cp437', eof : 0x1a }, '.ans': { name: 'ANSI', defaultEncoding: 'cp437', eof: 0x1a },
'.asc' : { name : 'ASCII', defaultEncoding : 'cp437', eof : 0x1a }, '.asc': { name: 'ASCII', defaultEncoding: 'cp437', eof: 0x1a },
'.pcb' : { name : 'PCBoard', defaultEncoding : 'cp437', eof : 0x1a }, '.pcb': { name: 'PCBoard', defaultEncoding: 'cp437', eof: 0x1a },
'.bbs' : { name : 'Wildcat', defaultEncoding : 'cp437', eof : 0x1a }, '.bbs': { name: 'Wildcat', defaultEncoding: 'cp437', eof: 0x1a },
'.amiga' : { name : 'Amiga', defaultEncoding : 'amiga', eof : 0x1a }, '.amiga': { name: 'Amiga', defaultEncoding: 'amiga', eof: 0x1a },
'.txt' : { name : 'Amiga Text', defaultEncoding : 'cp437', eof : 0x1a }, '.txt': { name: 'Amiga Text', defaultEncoding: 'cp437', eof: 0x1a },
// :TODO: extentions for wwiv, renegade, celerity, syncronet, ... // :TODO: extentions for wwiv, renegade, celerity, syncronet, ...
// :TODO: extension for atari // :TODO: extension for atari
// :TODO: extension for topaz ansi/ascii. // :TODO: extension for topaz ansi/ascii.
}; };
function getFontNameFromSAUCE(sauce) { function getFontNameFromSAUCE(sauce) {
if(sauce.Character) { if (sauce.Character) {
return sauce.Character.fontName; return sauce.Character.fontName;
} }
} }
function sliceAtEOF(data, eofMarker) { function sliceAtEOF(data, eofMarker) {
let eof = data.length; let eof = data.length;
const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE) const stopPos = Math.max(data.length - 256, 0); // 256 = 2 * sizeof(SAUCE)
for(let i = eof - 1; i > stopPos; i--) { for (let i = eof - 1; i > stopPos; i--) {
if(eofMarker === data[i]) { if (eofMarker === data[i]) {
eof = i; eof = i;
break; break;
} }
} }
if (eof === data.length) { if (eof === data.length) {
return data; // nothing to do return data; // nothing to do
} }
// try to prevent goofs // try to prevent goofs
@ -71,43 +71,46 @@ function sliceAtEOF(data, eofMarker) {
function getArtFromPath(path, options, cb) { function getArtFromPath(path, options, cb) {
fs.readFile(path, (err, data) => { fs.readFile(path, (err, data) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
// //
// Convert from encodedAs -> j // Convert from encodedAs -> j
// //
const ext = paths.extname(path).toLowerCase(); const ext = paths.extname(path).toLowerCase();
const encoding = options.encodedAs || defaultEncodingFromExtension(ext); const encoding = options.encodedAs || defaultEncodingFromExtension(ext);
// :TODO: how are BOM's currently handled if present? Are they removed? Do we need to? // :TODO: how are BOM's currently handled if present? Are they removed? Do we need to?
function sliceOfData() { function sliceOfData() {
if(options.fullFile === true) { if (options.fullFile === true) {
return iconv.decode(data, encoding); return iconv.decode(data, encoding);
} else { } else {
const eofMarker = defaultEofFromExtension(ext); const eofMarker = defaultEofFromExtension(ext);
return iconv.decode(eofMarker ? sliceAtEOF(data, eofMarker) : data, encoding); return iconv.decode(
eofMarker ? sliceAtEOF(data, eofMarker) : data,
encoding
);
} }
} }
function getResult(sauce) { function getResult(sauce) {
const result = { const result = {
data : sliceOfData(), data: sliceOfData(),
fromPath : path, fromPath: path,
}; };
if(sauce) { if (sauce) {
result.sauce = sauce; result.sauce = sauce;
} }
return result; return result;
} }
if(options.readSauce === true) { if (options.readSauce === true) {
sauce.readSAUCE(data, (err, sauce) => { sauce.readSAUCE(data, (err, sauce) => {
if(err) { if (err) {
return cb(null, getResult()); return cb(null, getResult());
} }
@ -115,7 +118,7 @@ function getArtFromPath(path, options, cb) {
// If a encoding was not provided & we have a mapping from // If a encoding was not provided & we have a mapping from
// the information provided by SAUCE, use that. // the information provided by SAUCE, use that.
// //
if(!options.encodedAs) { if (!options.encodedAs) {
/* /*
if(sauce.Character && sauce.Character.fontName) { if(sauce.Character && sauce.Character.fontName) {
var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName]; var enc = SAUCE_FONT_TO_ENCODING_HINT[sauce.Character.fontName];
@ -136,56 +139,58 @@ function getArtFromPath(path, options, cb) {
function getArt(name, options, cb) { function getArt(name, options, cb) {
const ext = paths.extname(name); const ext = paths.extname(name);
options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art); options.basePath = miscUtil.valueWithDefault(options.basePath, Config().paths.art);
options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true); options.asAnsi = miscUtil.valueWithDefault(options.asAnsi, true);
// :TODO: make use of asAnsi option and convert from supported -> ansi // :TODO: make use of asAnsi option and convert from supported -> ansi
if('' !== ext) { if ('' !== ext) {
options.types = [ ext.toLowerCase() ]; options.types = [ext.toLowerCase()];
} else { } else {
if(_.isUndefined(options.types)) { if (_.isUndefined(options.types)) {
options.types = Object.keys(SUPPORTED_ART_TYPES); options.types = Object.keys(SUPPORTED_ART_TYPES);
} else if(_.isString(options.types)) { } else if (_.isString(options.types)) {
options.types = [ options.types.toLowerCase() ]; options.types = [options.types.toLowerCase()];
} }
} }
// If an extension is provided, just read the file now // If an extension is provided, just read the file now
if('' !== ext) { if ('' !== ext) {
const directPath = paths.isAbsolute(name) ? name : paths.join(options.basePath, name); const directPath = paths.isAbsolute(name)
? name
: paths.join(options.basePath, name);
return getArtFromPath(directPath, options, cb); return getArtFromPath(directPath, options, cb);
} }
fs.readdir(options.basePath, (err, files) => { fs.readdir(options.basePath, (err, files) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const filtered = files.filter( file => { const filtered = files.filter(file => {
// //
// Ignore anything not allowed in |options.types| // Ignore anything not allowed in |options.types|
// //
const fext = paths.extname(file); const fext = paths.extname(file);
if(!options.types.includes(fext.toLowerCase())) { if (!options.types.includes(fext.toLowerCase())) {
return false; return false;
} }
const bn = paths.basename(file, fext).toLowerCase(); const bn = paths.basename(file, fext).toLowerCase();
if(options.random) { if (options.random) {
const suppliedBn = paths.basename(name, fext).toLowerCase(); const suppliedBn = paths.basename(name, fext).toLowerCase();
// //
// Random selection enabled. We'll allow for // Random selection enabled. We'll allow for
// basename1.ext, basename2.ext, ... // basename1.ext, basename2.ext, ...
// //
if(!bn.startsWith(suppliedBn)) { if (!bn.startsWith(suppliedBn)) {
return false; return false;
} }
const num = bn.substr(suppliedBn.length); const num = bn.substr(suppliedBn.length);
if(num.length > 0) { if (num.length > 0) {
if(isNaN(parseInt(num, 10))) { if (isNaN(parseInt(num, 10))) {
return false; return false;
} }
} }
@ -194,7 +199,7 @@ function getArt(name, options, cb) {
// We've already validated the extension (above). Must be an exact // We've already validated the extension (above). Must be an exact
// match to basename here // match to basename here
// //
if(bn != paths.basename(name, fext).toLowerCase()) { if (bn != paths.basename(name, fext).toLowerCase()) {
return false; return false;
} }
} }
@ -202,15 +207,18 @@ function getArt(name, options, cb) {
return true; return true;
}); });
if(filtered.length > 0) { if (filtered.length > 0) {
// //
// We should now have: // We should now have:
// - Exactly (1) item in |filtered| if non-random // - Exactly (1) item in |filtered| if non-random
// - 1:n items in |filtered| to choose from if random // - 1:n items in |filtered| to choose from if random
// //
let readPath; let readPath;
if(options.random) { if (options.random) {
readPath = paths.join(options.basePath, filtered[Math.floor(Math.random() * filtered.length)]); readPath = paths.join(
options.basePath,
filtered[Math.floor(Math.random() * filtered.length)]
);
} else { } else {
assert(1 === filtered.length); assert(1 === filtered.length);
readPath = paths.join(options.basePath, filtered[0]); readPath = paths.join(options.basePath, filtered[0]);
@ -230,7 +238,7 @@ function defaultEncodingFromExtension(ext) {
function defaultEofFromExtension(ext) { function defaultEofFromExtension(ext) {
const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()]; const artType = SUPPORTED_ART_TYPES[ext.toLowerCase()];
if(artType) { if (artType) {
return artType.eof; return artType.eof;
} }
} }
@ -240,12 +248,12 @@ function defaultEofFromExtension(ext) {
// * Cancel (disabled | <keys> ) // * Cancel (disabled | <keys> )
// * Resume from pause -> continous (disabled | <keys>) // * Resume from pause -> continous (disabled | <keys>)
function display(client, art, options, cb) { function display(client, art, options, cb) {
if(_.isFunction(options) && !cb) { if (_.isFunction(options) && !cb) {
cb = options; cb = options;
options = {}; options = {};
} }
if(!art || !art.length) { if (!art || !art.length) {
return cb(Errors.Invalid('No art supplied!')); return cb(Errors.Invalid('No art supplied!'));
} }
@ -255,19 +263,19 @@ function display(client, art, options, cb) {
// 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc. // 1) Standard - use internal tracking of locations for MCI -- no CPR's/etc.
// 2) CPR driven // 2) CPR driven
if(!_.isBoolean(options.iceColors)) { if (!_.isBoolean(options.iceColors)) {
// try to detect from SAUCE // try to detect from SAUCE
if(_.has(options, 'sauce.ansiFlags') && (options.sauce.ansiFlags & (1 << 0))) { if (_.has(options, 'sauce.ansiFlags') && options.sauce.ansiFlags & (1 << 0)) {
options.iceColors = true; options.iceColors = true;
} }
} }
const ansiParser = new aep.ANSIEscapeParser({ const ansiParser = new aep.ANSIEscapeParser({
mciReplaceChar : options.mciReplaceChar, mciReplaceChar: options.mciReplaceChar,
termHeight : client.term.termHeight, termHeight: client.term.termHeight,
termWidth : client.term.termWidth, termWidth: client.term.termWidth,
trailingLF : options.trailingLF, trailingLF: options.trailingLF,
startRow : options.startRow, startRow: options.startRow,
}); });
const mciMap = {}; const mciMap = {};
@ -275,37 +283,36 @@ function display(client, art, options, cb) {
ansiParser.on('mci', mciInfo => { ansiParser.on('mci', mciInfo => {
// :TODO: ensure generatedId's do not conflict with any existing |id| // :TODO: ensure generatedId's do not conflict with any existing |id|
const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId; const id = _.isNumber(mciInfo.id) ? mciInfo.id : generatedId;
const mapKey = `${mciInfo.mci}${id}`; const mapKey = `${mciInfo.mci}${id}`;
const mapEntry = mciMap[mapKey]; const mapEntry = mciMap[mapKey];
if(mapEntry) { if (mapEntry) {
mapEntry.focusSGR = mciInfo.SGR; mapEntry.focusSGR = mciInfo.SGR;
mapEntry.focusArgs = mciInfo.args; mapEntry.focusArgs = mciInfo.args;
} else { } else {
mciMap[mapKey] = { mciMap[mapKey] = {
position : mciInfo.position, position: mciInfo.position,
args : mciInfo.args, args: mciInfo.args,
SGR : mciInfo.SGR, SGR: mciInfo.SGR,
code : mciInfo.mci, code: mciInfo.mci,
id : id, id: id,
}; };
if(!mciInfo.id) { if (!mciInfo.id) {
++generatedId; ++generatedId;
} }
} }
}); });
ansiParser.on('literal', literal => client.term.write(literal, false) ); ansiParser.on('literal', literal => client.term.write(literal, false));
ansiParser.on('control', control => client.term.rawWrite(control) ); ansiParser.on('control', control => client.term.rawWrite(control));
ansiParser.on('complete', () => { ansiParser.on('complete', () => {
ansiParser.removeAllListeners(); ansiParser.removeAllListeners();
const extraInfo = { const extraInfo = {
height : ansiParser.row - 1, height: ansiParser.row - 1,
}; };
return cb(null, mciMap, extraInfo); return cb(null, mciMap, extraInfo);
@ -313,11 +320,11 @@ function display(client, art, options, cb) {
let initSeq = ''; let initSeq = '';
if (client.term.syncTermFontsEnabled) { if (client.term.syncTermFontsEnabled) {
if(options.font) { if (options.font) {
initSeq = ansi.setSyncTermFontWithAlias(options.font); initSeq = ansi.setSyncTermFontWithAlias(options.font);
} else if(options.sauce) { } else if (options.sauce) {
let fontName = getFontNameFromSAUCE(options.sauce); let fontName = getFontNameFromSAUCE(options.sauce);
if(fontName) { if (fontName) {
fontName = ansi.getSyncTermFontFromAlias(fontName); fontName = ansi.getSyncTermFontFromAlias(fontName);
} }
@ -327,18 +334,18 @@ function display(client, art, options, cb) {
// at a time. This applies to detection only (e.g. SAUCE). // at a time. This applies to detection only (e.g. SAUCE).
// If explicit, we'll set it no matter what (above) // If explicit, we'll set it no matter what (above)
// //
if(fontName && client.term.currentSyncFont != fontName) { if (fontName && client.term.currentSyncFont != fontName) {
client.term.currentSyncFont = fontName; client.term.currentSyncFont = fontName;
initSeq = ansi.setSyncTermFont(fontName); initSeq = ansi.setSyncTermFont(fontName);
} }
} }
} }
if(options.iceColors) { if (options.iceColors) {
initSeq += ansi.blinkToBrightIntensity(); initSeq += ansi.blinkToBrightIntensity();
} }
if(initSeq) { if (initSeq) {
client.term.rawWrite(initSeq); client.term.rawWrite(initSeq);
} }

View File

@ -2,20 +2,20 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
exports.parseAsset = parseAsset; exports.parseAsset = parseAsset;
exports.getAssetWithShorthand = getAssetWithShorthand; exports.getAssetWithShorthand = getAssetWithShorthand;
exports.getArtAsset = getArtAsset; exports.getArtAsset = getArtAsset;
exports.getModuleAsset = getModuleAsset; exports.getModuleAsset = getModuleAsset;
exports.resolveConfigAsset = resolveConfigAsset; exports.resolveConfigAsset = resolveConfigAsset;
exports.resolveSystemStatAsset = resolveSystemStatAsset; exports.resolveSystemStatAsset = resolveSystemStatAsset;
exports.getViewPropertyAsset = getViewPropertyAsset; exports.getViewPropertyAsset = getViewPropertyAsset;
const ALL_ASSETS = [ const ALL_ASSETS = [
'art', 'art',
@ -30,18 +30,17 @@ const ALL_ASSETS = [
]; ];
const ASSET_RE = new RegExp( const ASSET_RE = new RegExp(
'^@(' + ALL_ASSETS.join('|') + ')' + '^@(' + ALL_ASSETS.join('|') + ')' + /:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source
/:(?:([^:]+):)?([A-Za-z0-9_\-.]+)$/.source
); );
function parseAsset(s) { function parseAsset(s) {
const m = ASSET_RE.exec(s); const m = ASSET_RE.exec(s);
if(m) { if (m) {
const result = { type : m[1] }; const result = { type: m[1] };
if(m[3]) { if (m[3]) {
result.asset = m[3]; result.asset = m[3];
if(m[2]) { if (m[2]) {
result.location = m[2]; result.location = m[2];
} }
} else { } else {
@ -53,11 +52,11 @@ function parseAsset(s) {
} }
function getAssetWithShorthand(spec, defaultType) { function getAssetWithShorthand(spec, defaultType) {
if(!_.isString(spec)) { if (!_.isString(spec)) {
return null; return null;
} }
if('@' === spec[0]) { if ('@' === spec[0]) {
const asset = parseAsset(spec); const asset = parseAsset(spec);
assert(_.isString(asset.type)); assert(_.isString(asset.type));
@ -65,43 +64,43 @@ function getAssetWithShorthand(spec, defaultType) {
} }
return { return {
type : defaultType, type: defaultType,
asset : spec, asset: spec,
}; };
} }
function getArtAsset(spec) { function getArtAsset(spec) {
const asset = getAssetWithShorthand(spec, 'art'); const asset = getAssetWithShorthand(spec, 'art');
if(!asset) { if (!asset) {
return null; return null;
} }
assert( ['art', 'method' ].indexOf(asset.type) > -1); assert(['art', 'method'].indexOf(asset.type) > -1);
return asset; return asset;
} }
function getModuleAsset(spec) { function getModuleAsset(spec) {
const asset = getAssetWithShorthand(spec, 'systemModule'); const asset = getAssetWithShorthand(spec, 'systemModule');
if(!asset) { if (!asset) {
return null; return null;
} }
assert( ['userModule', 'systemModule' ].includes(asset.type) ); assert(['userModule', 'systemModule'].includes(asset.type));
return asset; return asset;
} }
function resolveConfigAsset(spec) { function resolveConfigAsset(spec) {
const asset = parseAsset(spec); const asset = parseAsset(spec);
if(asset) { if (asset) {
assert('config' === asset.type); assert('config' === asset.type);
const path = asset.asset.split('.'); const path = asset.asset.split('.');
let conf = Config(); let conf = Config();
for(let i = 0; i < path.length; ++i) { for (let i = 0; i < path.length; ++i) {
if(_.isUndefined(conf[path[i]])) { if (_.isUndefined(conf[path[i]])) {
return spec; return spec;
} }
conf = conf[path[i]]; conf = conf[path[i]];
@ -114,7 +113,7 @@ function resolveConfigAsset(spec) {
function resolveSystemStatAsset(spec) { function resolveSystemStatAsset(spec) {
const asset = parseAsset(spec); const asset = parseAsset(spec);
if(!asset) { if (!asset) {
return spec; return spec;
} }
@ -124,7 +123,7 @@ function resolveSystemStatAsset(spec) {
} }
function getViewPropertyAsset(src) { function getViewPropertyAsset(src) {
if(!_.isString(src) || '@' !== src.charAt(0)) { if (!_.isString(src) || '@' !== src.charAt(0)) {
return null; return null;
} }

View File

@ -2,60 +2,68 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'User Auto-Sig Editor', name: 'User Auto-Sig Editor',
desc : 'Module for editing auto-sigs', desc: 'Module for editing auto-sigs',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const FormIds = { const FormIds = {
edit : 0, edit: 0,
}; };
const MciViewIds = { const MciViewIds = {
editor : 1, editor: 1,
save : 2, save: 2,
}; };
exports.getModule = class UserAutoSigEditorModule extends MenuModule { exports.getModule = class UserAutoSigEditorModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
extraArgs: options.extraArgs,
});
this.menuMethods = { this.menuMethods = {
saveChanges : (formData, extraArgs, cb) => { saveChanges: (formData, extraArgs, cb) => {
return this.saveChanges(cb); return this.saveChanges(cb);
} },
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
async.series( async.series(
[ [
(callback) => { callback => {
return this.prepViewController('edit', FormIds.edit, mciData.menu, callback); return this.prepViewController(
'edit',
FormIds.edit,
mciData.menu,
callback
);
}, },
(callback) => { callback => {
const requiredCodes = [ MciViewIds.editor, MciViewIds.save ]; const requiredCodes = [MciViewIds.editor, MciViewIds.save];
return this.validateMCIByViewIds('edit', requiredCodes, callback); return this.validateMCIByViewIds('edit', requiredCodes, callback);
}, },
(callback) => { callback => {
const sig = this.client.user.getProperty(UserProps.AutoSignature) || ''; const sig =
this.client.user.getProperty(UserProps.AutoSignature) || '';
this.setViewText('edit', MciViewIds.editor, sig); this.setViewText('edit', MciViewIds.editor, sig);
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -67,8 +75,8 @@ exports.getModule = class UserAutoSigEditorModule extends MenuModule {
saveChanges(cb) { saveChanges(cb) {
const sig = this.getView('edit', MciViewIds.editor).getData().trim(); const sig = this.getView('edit', MciViewIds.editor).getData().trim();
this.client.user.persistProperty(UserProps.AutoSignature, sig, err => { this.client.user.persistProperty(UserProps.AutoSignature, sig, err => {
if(err) { if (err) {
this.client.log.error( { error : err.message }, 'Could not save auto-sig'); this.client.log.error({ error: err.message }, 'Could not save auto-sig');
} }
return this.prevMenu(cb); return this.prevMenu(cb);
}); });

View File

@ -6,35 +6,36 @@
//SegfaultHandler.registerHandler('enigma-bbs-segfault.log'); //SegfaultHandler.registerHandler('enigma-bbs-segfault.log');
// ENiGMA½ // ENiGMA½
const conf = require('./config.js'); const conf = require('./config.js');
const logger = require('./logger.js'); const logger = require('./logger.js');
const database = require('./database.js'); const database = require('./database.js');
const resolvePath = require('./misc_util.js').resolvePath; const resolvePath = require('./misc_util.js').resolvePath;
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const SysProps = require('./system_property.js'); const SysProps = require('./system_property.js');
const SysLogKeys = require('./system_log.js'); const SysLogKeys = require('./system_log.js');
// deps // deps
const async = require('async'); const async = require('async');
const util = require('util'); const util = require('util');
const _ = require('lodash'); const _ = require('lodash');
const mkdirs = require('fs-extra').mkdirs; const mkdirs = require('fs-extra').mkdirs;
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const moment = require('moment'); const moment = require('moment');
// our main entry point // our main entry point
exports.main = main; exports.main = main;
// object with various services we want to de-init/shutdown cleanly if possible // object with various services we want to de-init/shutdown cleanly if possible
const initServices = {}; const initServices = {};
// only include bbs.js once @ startup; this should be fine // only include bbs.js once @ startup; this should be fine
const COPYRIGHT = fs.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8').split(/\r?\n/g)[0]; const COPYRIGHT = fs
.readFileSync(paths.join(__dirname, '../LICENSE.TXT'), 'utf8')
.split(/\r?\n/g)[0];
const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`; const FULL_COPYRIGHT = `ENiGMA½ ${COPYRIGHT}`;
const HELP = const HELP = `${FULL_COPYRIGHT}
`${FULL_COPYRIGHT}
usage: main.js <args> usage: main.js <args>
eg : main.js --config /enigma_install_path/config/ eg : main.js --config /enigma_install_path/config/
@ -61,17 +62,21 @@ function main() {
function processArgs(callback) { function processArgs(callback) {
const argv = require('minimist')(process.argv.slice(2)); const argv = require('minimist')(process.argv.slice(2));
if(argv.help) { if (argv.help) {
return printHelpAndExit(); return printHelpAndExit();
} }
if(argv.version) { if (argv.version) {
return printVersionAndExit(); return printVersionAndExit();
} }
const configOverridePath = argv.config; const configOverridePath = argv.config;
return callback(null, configOverridePath || conf.Config.getDefaultPath(), _.isString(configOverridePath)); return callback(
null,
configOverridePath || conf.Config.getDefaultPath(),
_.isString(configOverridePath)
);
}, },
function initConfig(configPath, configPathSupplied, callback) { function initConfig(configPath, configPathSupplied, callback) {
const configFile = configPath + 'config.hjson'; const configFile = configPath + 'config.hjson';
@ -81,12 +86,14 @@ function main() {
// If the user supplied a path and we can't read/parse it // If the user supplied a path and we can't read/parse it
// then it's a fatal error // then it's a fatal error
// //
if(err) { if (err) {
if('ENOENT' === err.code) { if ('ENOENT' === err.code) {
if(configPathSupplied) { if (configPathSupplied) {
console.error('Configuration file does not exist: ' + configFile); console.error(
'Configuration file does not exist: ' + configFile
);
} else { } else {
configPathSupplied = null; // make non-fatal; we'll go with defaults configPathSupplied = null; // make non-fatal; we'll go with defaults
} }
} else { } else {
errorDisplayed = true; errorDisplayed = true;
@ -104,26 +111,30 @@ function main() {
}, },
function initSystem(callback) { function initSystem(callback) {
initialize(function init(err) { initialize(function init(err) {
if(err) { if (err) {
console.error('Error initializing: ' + util.inspect(err)); console.error('Error initializing: ' + util.inspect(err));
} }
return callback(err); return callback(err);
}); });
} },
], ],
function complete(err) { function complete(err) {
if(!err) { if (!err) {
// note this is escaped: // note this is escaped:
fs.readFile(paths.join(__dirname, '../misc/startup_banner.asc'), 'utf8', (err, banner) => { fs.readFile(
console.info(FULL_COPYRIGHT); paths.join(__dirname, '../misc/startup_banner.asc'),
if(!err) { 'utf8',
console.info(banner); (err, banner) => {
console.info(FULL_COPYRIGHT);
if (!err) {
console.info(banner);
}
console.info('System started!');
} }
console.info('System started!'); );
});
} }
if(err && !errorDisplayed) { if (err && !errorDisplayed) {
console.error('Error initializing: ' + util.inspect(err)); console.error('Error initializing: ' + util.inspect(err));
return process.exit(); return process.exit();
} }
@ -142,37 +153,39 @@ function shutdownSystem() {
const ClientConns = require('./client_connections.js'); const ClientConns = require('./client_connections.js');
const activeConnections = ClientConns.getActiveConnections(); const activeConnections = ClientConns.getActiveConnections();
let i = activeConnections.length; let i = activeConnections.length;
while(i--) { while (i--) {
const activeTerm = activeConnections[i].term; const activeTerm = activeConnections[i].term;
if(activeTerm) { if (activeTerm) {
activeTerm.write('\n\nServer is shutting down NOW! Disconnecting...\n\n'); activeTerm.write(
'\n\nServer is shutting down NOW! Disconnecting...\n\n'
);
} }
ClientConns.removeClient(activeConnections[i]); ClientConns.removeClient(activeConnections[i]);
} }
callback(null); callback(null);
}, },
function stopListeningServers(callback) { function stopListeningServers(callback) {
return require('./listening_server.js').shutdown( () => { return require('./listening_server.js').shutdown(() => {
return callback(null); // ignore err return callback(null); // ignore err
}); });
}, },
function stopEventScheduler(callback) { function stopEventScheduler(callback) {
if(initServices.eventScheduler) { if (initServices.eventScheduler) {
return initServices.eventScheduler.shutdown( () => { return initServices.eventScheduler.shutdown(() => {
return callback(null); // ignore err return callback(null); // ignore err
}); });
} else { } else {
return callback(null); return callback(null);
} }
}, },
function stopFileAreaWeb(callback) { function stopFileAreaWeb(callback) {
require('./file_area_web.js').startup( () => { require('./file_area_web.js').startup(() => {
return callback(null); // ignore err return callback(null); // ignore err
}); });
}, },
function stopMsgNetwork(callback) { function stopMsgNetwork(callback) {
require('./msg_network.js').shutdown(callback); require('./msg_network.js').shutdown(callback);
} },
], ],
() => { () => {
console.info('Goodbye!'); console.info('Goodbye!');
@ -186,30 +199,39 @@ function initialize(cb) {
[ [
function createMissingDirectories(callback) { function createMissingDirectories(callback) {
const Config = conf.get(); const Config = conf.get();
async.each(Object.keys(Config.paths), function entry(pathKey, next) { async.each(
mkdirs(Config.paths[pathKey], function dirCreated(err) { Object.keys(Config.paths),
if(err) { function entry(pathKey, next) {
console.error('Could not create path: ' + Config.paths[pathKey] + ': ' + err.toString()); mkdirs(Config.paths[pathKey], function dirCreated(err) {
} if (err) {
return next(err); console.error(
}); 'Could not create path: ' +
}, function dirCreationComplete(err) { Config.paths[pathKey] +
return callback(err); ': ' +
}); err.toString()
);
}
return next(err);
});
},
function dirCreationComplete(err) {
return callback(err);
}
);
}, },
function basicInit(callback) { function basicInit(callback) {
logger.init(); logger.init();
logger.log.info( logger.log.info(
{ {
version : require('../package.json').version, version: require('../package.json').version,
nodeVersion : process.version, nodeVersion: process.version,
}, },
'**** ENiGMA½ Bulletin Board System Starting Up! ****' '**** ENiGMA½ Bulletin Board System Starting Up! ****'
); );
process.on('SIGINT', shutdownSystem); process.on('SIGINT', shutdownSystem);
require('@breejs/later').date.localTime(); // use local times for later.js/scheduling require('@breejs/later').date.localTime(); // use local times for later.js/scheduling
return callback(null); return callback(null);
}, },
@ -236,9 +258,12 @@ function initialize(cb) {
const User = require('./user.js'); const User = require('./user.js');
const propLoadOpts = { const propLoadOpts = {
names : [ names: [
UserProps.RealName, UserProps.Sex, UserProps.EmailAddress, UserProps.RealName,
UserProps.Location, UserProps.Affiliations, UserProps.Sex,
UserProps.EmailAddress,
UserProps.Location,
UserProps.Affiliations,
], ],
}; };
@ -248,15 +273,19 @@ function initialize(cb) {
return User.getUserName(1, next); return User.getUserName(1, next);
}, },
function getOpProps(opUserName, next) { function getOpProps(opUserName, next) {
User.loadProperties(User.RootUserID, propLoadOpts, (err, opProps) => { User.loadProperties(
return next(err, opUserName, opProps); User.RootUserID,
}); propLoadOpts,
(err, opProps) => {
return next(err, opUserName, opProps);
}
);
}, },
], ],
(err, opUserName, opProps) => { (err, opUserName, opProps) => {
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
if(err) { if (err) {
propLoadOpts.names.concat('username').forEach(v => { propLoadOpts.names.concat('username').forEach(v => {
StatLog.setNonPersistentSystemStat(`sysop_${v}`, 'N/A'); StatLog.setNonPersistentSystemStat(`sysop_${v}`, 'N/A');
}); });
@ -275,14 +304,17 @@ function initialize(cb) {
function initCallsToday(callback) { function initCallsToday(callback) {
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const filter = { const filter = {
logName : SysLogKeys.UserLoginHistory, logName: SysLogKeys.UserLoginHistory,
resultType : 'count', resultType: 'count',
date : moment(), date: moment(),
}; };
StatLog.findSystemLogEntries(filter, (err, callsToday) => { StatLog.findSystemLogEntries(filter, (err, callsToday) => {
if(!err) { if (!err) {
StatLog.setNonPersistentSystemStat(SysProps.LoginsToday, callsToday); StatLog.setNonPersistentSystemStat(
SysProps.LoginsToday,
callsToday
);
} }
return callback(null); return callback(null);
}); });
@ -312,7 +344,8 @@ function initialize(cb) {
return require('./file_area_web.js').startup(callback); return require('./file_area_web.js').startup(callback);
}, },
function readyPasswordReset(callback) { function readyPasswordReset(callback) {
const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset; const WebPasswordReset =
require('./web_password_reset.js').WebPasswordReset;
return WebPasswordReset.startup(callback); return WebPasswordReset.startup(callback);
}, },
function ready2FA_OTPRegister(callback) { function ready2FA_OTPRegister(callback) {
@ -320,15 +353,16 @@ function initialize(cb) {
return User2FA_OTPWebRegister.startup(callback); return User2FA_OTPWebRegister.startup(callback);
}, },
function readyEventScheduler(callback) { function readyEventScheduler(callback) {
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule; const EventSchedulerModule =
EventSchedulerModule.loadAndStart( (err, modInst) => { require('./event_scheduler.js').EventSchedulerModule;
EventSchedulerModule.loadAndStart((err, modInst) => {
initServices.eventScheduler = modInst; initServices.eventScheduler = modInst;
return callback(err); return callback(err);
}); });
}, },
function listenUserEventsForStatLog(callback) { function listenUserEventsForStatLog(callback) {
return require('./stat_log.js').initUserEvents(callback); return require('./stat_log.js').initUserEvents(callback);
} },
], ],
function onComplete(err) { function onComplete(err) {
return cb(err); return cb(err);

View File

@ -1,21 +1,18 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const { resetScreen } = require('./ansi_term.js'); const { resetScreen } = require('./ansi_term.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const { const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
trackDoorRunBegin,
trackDoorRunEnd
} = require('./door_util.js');
// deps // deps
const async = require('async'); const async = require('async');
const http = require('http'); const http = require('http');
const net = require('net'); const net = require('net');
const crypto = require('crypto'); const crypto = require('crypto');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
/* /*
Expected configuration block: Expected configuration block:
@ -42,18 +39,18 @@ const packageJson = require('../package.json');
// :TODO: ENH: Support nodeMax and tooManyArt // :TODO: ENH: Support nodeMax and tooManyArt
exports.moduleInfo = { exports.moduleInfo = {
name : 'BBSLink', name: 'BBSLink',
desc : 'BBSLink Access Module', desc: 'BBSLink Access Module',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class BBSLinkModule extends MenuModule { exports.getModule = class BBSLinkModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'games.bbslink.net'; this.config.host = this.config.host || 'games.bbslink.net';
this.config.port = this.config.port || 23; this.config.port = this.config.port || 23;
} }
initSequence() { initSequence() {
@ -67,12 +64,12 @@ exports.getModule = class BBSLinkModule extends MenuModule {
function validateConfig(callback) { function validateConfig(callback) {
return self.validateConfigFields( return self.validateConfigFields(
{ {
host : 'string', host: 'string',
sysCode : 'string', sysCode: 'string',
authCode : 'string', authCode: 'string',
schemeCode : 'string', schemeCode: 'string',
door : 'string', door: 'string',
port : 'number', port: 'number',
}, },
callback callback
); );
@ -82,19 +79,26 @@ exports.getModule = class BBSLinkModule extends MenuModule {
// Acquire an authentication token // Acquire an authentication token
// //
crypto.randomBytes(16, function rand(ex, buf) { crypto.randomBytes(16, function rand(ex, buf) {
if(ex) { if (ex) {
callback(ex); callback(ex);
} else { } else {
randomKey = buf.toString('base64').substr(0, 6); randomKey = buf.toString('base64').substr(0, 6);
self.simpleHttpRequest('/token.php?key=' + randomKey, null, function resp(err, body) { self.simpleHttpRequest(
if(err) { '/token.php?key=' + randomKey,
callback(err); null,
} else { function resp(err, body) {
token = body.trim(); if (err) {
self.client.log.trace( { token : token }, 'BBSLink token'); callback(err);
callback(null); } else {
token = body.trim();
self.client.log.trace(
{ token: token },
'BBSLink token'
);
callback(null);
}
} }
}); );
} }
}); });
}, },
@ -103,26 +107,40 @@ exports.getModule = class BBSLinkModule extends MenuModule {
// Authenticate the token we acquired previously // Authenticate the token we acquired previously
// //
const headers = { const headers = {
'X-User' : self.client.user.userId.toString(), 'X-User': self.client.user.userId.toString(),
'X-System' : self.config.sysCode, 'X-System': self.config.sysCode,
'X-Auth' : crypto.createHash('md5').update(self.config.authCode + token).digest('hex'), 'X-Auth': crypto
'X-Code' : crypto.createHash('md5').update(self.config.schemeCode + token).digest('hex'), .createHash('md5')
'X-Rows' : self.client.term.termHeight.toString(), .update(self.config.authCode + token)
'X-Key' : randomKey, .digest('hex'),
'X-Door' : self.config.door, 'X-Code': crypto
'X-Token' : token, .createHash('md5')
'X-Type' : 'enigma-bbs', .update(self.config.schemeCode + token)
'X-Version' : packageJson.version, .digest('hex'),
'X-Rows': self.client.term.termHeight.toString(),
'X-Key': randomKey,
'X-Door': self.config.door,
'X-Token': token,
'X-Type': 'enigma-bbs',
'X-Version': packageJson.version,
}; };
self.simpleHttpRequest('/auth.php?key=' + randomKey, headers, function resp(err, body) { self.simpleHttpRequest(
var status = body.trim(); '/auth.php?key=' + randomKey,
headers,
function resp(err, body) {
var status = body.trim();
if('complete' === status) { if ('complete' === status) {
return callback(null); return callback(null);
}
return callback(
Errors.AccessDenied(
`Bad authentication status: ${status}`
)
);
} }
return callback(Errors.AccessDenied(`Bad authentication status: ${status}`)); );
});
}, },
function createTelnetBridge(callback) { function createTelnetBridge(callback) {
// //
@ -130,35 +148,48 @@ exports.getModule = class BBSLinkModule extends MenuModule {
// bridge from us to them // bridge from us to them
// //
const connectOpts = { const connectOpts = {
port : self.config.port, port: self.config.port,
host : self.config.host, host: self.config.host,
}; };
let dataOut; let dataOut;
self.client.term.write(resetScreen()); self.client.term.write(resetScreen());
self.client.term.write(` Connecting to ${self.config.host}, please wait...\n`); self.client.term.write(
` Connecting to ${self.config.host}, please wait...\n`
);
const doorTracking = trackDoorRunBegin(self.client, `bbslink_${self.config.door}`); const doorTracking = trackDoorRunBegin(
self.client,
`bbslink_${self.config.door}`
);
const bridgeConnection = net.createConnection(connectOpts, function connected() { const bridgeConnection = net.createConnection(
self.client.log.info(connectOpts, 'BBSLink bridge connection established'); connectOpts,
function connected() {
self.client.log.info(
connectOpts,
'BBSLink bridge connection established'
);
dataOut = (data) => { dataOut = data => {
return bridgeConnection.write(data); return bridgeConnection.write(data);
}; };
self.client.term.output.on('data', dataOut); self.client.term.output.on('data', dataOut);
self.client.once('end', function clientEnd() { self.client.once('end', function clientEnd() {
self.client.log.info('Connection ended. Terminating BBSLink connection'); self.client.log.info(
clientTerminated = true; 'Connection ended. Terminating BBSLink connection'
bridgeConnection.end(); );
}); clientTerminated = true;
}); bridgeConnection.end();
});
}
);
const restore = () => { const restore = () => {
if(dataOut && self.client.term.output) { if (dataOut && self.client.term.output) {
self.client.term.output.removeListener('data', dataOut); self.client.term.output.removeListener('data', dataOut);
dataOut = null; dataOut = null;
} }
@ -174,22 +205,31 @@ exports.getModule = class BBSLinkModule extends MenuModule {
bridgeConnection.on('end', function connectionEnd() { bridgeConnection.on('end', function connectionEnd() {
restore(); restore();
return callback(clientTerminated ? Errors.General('Client connection terminated') : null); return callback(
clientTerminated
? Errors.General('Client connection terminated')
: null
);
}); });
bridgeConnection.on('error', function error(err) { bridgeConnection.on('error', function error(err) {
self.client.log.info('BBSLink bridge connection error: ' + err.message); self.client.log.info(
'BBSLink bridge connection error: ' + err.message
);
restore(); restore();
return callback(err); return callback(err);
}); });
} },
], ],
function complete(err) { function complete(err) {
if(err) { if (err) {
self.client.log.warn( { error : err.toString() }, 'BBSLink connection error'); self.client.log.warn(
{ error: err.toString() },
'BBSLink connection error'
);
} }
if(!clientTerminated) { if (!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }
} }
@ -198,9 +238,9 @@ exports.getModule = class BBSLinkModule extends MenuModule {
simpleHttpRequest(path, headers, cb) { simpleHttpRequest(path, headers, cb) {
const getOpts = { const getOpts = {
host : this.config.host, host: this.config.host,
path : path, path: path,
headers : headers, headers: headers,
}; };
const req = http.get(getOpts, function response(resp) { const req = http.get(getOpts, function response(resp) {

View File

@ -2,72 +2,69 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { const { getModDatabasePath, getTransactionDatabase } = require('./database.js');
getModDatabasePath,
getTransactionDatabase
} = require('./database.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const User = require('./user.js'); const User = require('./user.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
// deps // deps
const async = require('async'); const async = require('async');
const sqlite3 = require('sqlite3'); const sqlite3 = require('sqlite3');
const _ = require('lodash'); const _ = require('lodash');
// :TODO: add notes field // :TODO: add notes field
const moduleInfo = exports.moduleInfo = { const moduleInfo = (exports.moduleInfo = {
name : 'BBS List', name: 'BBS List',
desc : 'List of other BBSes', desc: 'List of other BBSes',
author : 'Andrew Pamment', author: 'Andrew Pamment',
packageName : 'com.magickabbs.enigma.bbslist' packageName: 'com.magickabbs.enigma.bbslist',
}; });
const MciViewIds = { const MciViewIds = {
view : { view: {
BBSList : 1, BBSList: 1,
SelectedBBSName : 2, SelectedBBSName: 2,
SelectedBBSSysOp : 3, SelectedBBSSysOp: 3,
SelectedBBSTelnet : 4, SelectedBBSTelnet: 4,
SelectedBBSWww : 5, SelectedBBSWww: 5,
SelectedBBSLoc : 6, SelectedBBSLoc: 6,
SelectedBBSSoftware : 7, SelectedBBSSoftware: 7,
SelectedBBSNotes : 8, SelectedBBSNotes: 8,
SelectedBBSSubmitter : 9, SelectedBBSSubmitter: 9,
},
add: {
BBSName: 1,
Sysop: 2,
Telnet: 3,
Www: 4,
Location: 5,
Software: 6,
Notes: 7,
Error: 8,
}, },
add : {
BBSName : 1,
Sysop : 2,
Telnet : 3,
Www : 4,
Location : 5,
Software : 6,
Notes : 7,
Error : 8,
}
}; };
const FormIds = { const FormIds = {
View : 0, View: 0,
Add : 1, Add: 1,
}; };
const SELECTED_MCI_NAME_TO_ENTRY = { const SELECTED_MCI_NAME_TO_ENTRY = {
SelectedBBSName : 'bbsName', SelectedBBSName: 'bbsName',
SelectedBBSSysOp : 'sysOp', SelectedBBSSysOp: 'sysOp',
SelectedBBSTelnet : 'telnet', SelectedBBSTelnet: 'telnet',
SelectedBBSWww : 'www', SelectedBBSWww: 'www',
SelectedBBSLoc : 'location', SelectedBBSLoc: 'location',
SelectedBBSSoftware : 'software', SelectedBBSSoftware: 'software',
SelectedBBSSubmitter : 'submitter', SelectedBBSSubmitter: 'submitter',
SelectedBBSSubmitterId : 'submitterUserId', SelectedBBSSubmitterId: 'submitterUserId',
SelectedBBSNotes : 'notes', SelectedBBSNotes: 'notes',
}; };
exports.getModule = class BBSListModule extends MenuModule { exports.getModule = class BBSListModule extends MenuModule {
@ -79,10 +76,10 @@ exports.getModule = class BBSListModule extends MenuModule {
// //
// Validators // Validators
// //
viewValidationListener : function(err, cb) { viewValidationListener: function (err, cb) {
const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error); const errMsgView = self.viewControllers.add.getView(MciViewIds.add.Error);
if(errMsgView) { if (errMsgView) {
if(err) { if (err) {
errMsgView.setText(err.message); errMsgView.setText(err.message);
} else { } else {
errMsgView.clearText(); errMsgView.clearText();
@ -95,39 +92,48 @@ exports.getModule = class BBSListModule extends MenuModule {
// //
// Key & submit handlers // Key & submit handlers
// //
addBBS : function(formData, extraArgs, cb) { addBBS: function (formData, extraArgs, cb) {
self.displayAddScreen(cb); self.displayAddScreen(cb);
}, },
deleteBBS : function(formData, extraArgs, cb) { deleteBBS: function (formData, extraArgs, cb) {
if(!_.isNumber(self.selectedBBS) || 0 === self.entries.length) { if (!_.isNumber(self.selectedBBS) || 0 === self.entries.length) {
return cb(null); return cb(null);
} }
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); const entriesView = self.viewControllers.view.getView(
MciViewIds.view.BBSList
);
if(self.entries[self.selectedBBS].submitterUserId !== self.client.user.userId && !self.client.user.isSysOp()) { if (
self.entries[self.selectedBBS].submitterUserId !==
self.client.user.userId &&
!self.client.user.isSysOp()
) {
// must be owner or +op // must be owner or +op
return cb(null); return cb(null);
} }
const entry = self.entries[self.selectedBBS]; const entry = self.entries[self.selectedBBS];
if(!entry) { if (!entry) {
return cb(null); return cb(null);
} }
self.database.run( self.database.run(
`DELETE FROM bbs_list `DELETE FROM bbs_list
WHERE id=?;`, WHERE id=?;`,
[ entry.id ], [entry.id],
err => { err => {
if (err) { if (err) {
self.client.log.error( { err : err }, 'Error deleting from BBS list'); self.client.log.error(
{ err: err },
'Error deleting from BBS list'
);
} else { } else {
self.entries.splice(self.selectedBBS, 1); self.entries.splice(self.selectedBBS, 1);
self.setEntries(entriesView); self.setEntries(entriesView);
if(self.entries.length > 0) { if (self.entries.length > 0) {
entriesView.focusPrevious(); entriesView.focusPrevious();
} }
@ -138,15 +144,19 @@ exports.getModule = class BBSListModule extends MenuModule {
} }
); );
}, },
submitBBS : function(formData, extraArgs, cb) { submitBBS: function (formData, extraArgs, cb) {
let ok = true; let ok = true;
[ 'BBSName', 'Sysop', 'Telnet' ].forEach( mciName => { ['BBSName', 'Sysop', 'Telnet'].forEach(mciName => {
if('' === self.viewControllers.add.getView(MciViewIds.add[mciName]).getData()) { if (
'' ===
self.viewControllers.add
.getView(MciViewIds.add[mciName])
.getData()
) {
ok = false; ok = false;
} }
}); });
if(!ok) { if (!ok) {
// validators should prevent this! // validators should prevent this!
return cb(null); return cb(null);
} }
@ -155,12 +165,21 @@ exports.getModule = class BBSListModule extends MenuModule {
`INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes) `INSERT INTO bbs_list (bbs_name, sysop, telnet, www, location, software, submitter_user_id, notes)
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
[ [
formData.value.name, formData.value.sysop, formData.value.telnet, formData.value.www, formData.value.name,
formData.value.location, formData.value.software, self.client.user.userId, formData.value.notes formData.value.sysop,
formData.value.telnet,
formData.value.www,
formData.value.location,
formData.value.software,
self.client.user.userId,
formData.value.notes,
], ],
err => { err => {
if(err) { if (err) {
self.client.log.error( { err : err }, 'Error adding to BBS list'); self.client.log.error(
{ err: err },
'Error adding to BBS list'
);
} }
self.clearAddForm(); self.clearAddForm();
@ -168,10 +187,10 @@ exports.getModule = class BBSListModule extends MenuModule {
} }
); );
}, },
cancelSubmit : function(formData, extraArgs, cb) { cancelSubmit: function (formData, extraArgs, cb) {
self.clearAddForm(); self.clearAddForm();
self.displayBBSList(true, cb); self.displayBBSList(true, cb);
} },
}; };
} }
@ -184,10 +203,10 @@ exports.getModule = class BBSListModule extends MenuModule {
}, },
function display(callback) { function display(callback) {
self.displayBBSList(false, callback); self.displayBBSList(false, callback);
} },
], ],
err => { err => {
if(err) { if (err) {
// :TODO: Handle me -- initSequence() should really take a completion callback // :TODO: Handle me -- initSequence() should really take a completion callback
} }
self.finishedLoading(); self.finishedLoading();
@ -196,21 +215,28 @@ exports.getModule = class BBSListModule extends MenuModule {
} }
drawSelectedEntry(entry) { drawSelectedEntry(entry) {
if(!entry) { if (!entry) {
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
this.setViewText('view', MciViewIds.view[mciName], ''); this.setViewText('view', MciViewIds.view[mciName], '');
}); });
} else { } else {
const youSubmittedFormat = this.menuConfig.youSubmittedFormat || '{submitter} (You!)'; const youSubmittedFormat =
this.menuConfig.youSubmittedFormat || '{submitter} (You!)';
Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => { Object.keys(SELECTED_MCI_NAME_TO_ENTRY).forEach(mciName => {
const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]]; const t = entry[SELECTED_MCI_NAME_TO_ENTRY[mciName]];
if(MciViewIds.view[mciName]) { if (MciViewIds.view[mciName]) {
if (
if('SelectedBBSSubmitter' == mciName && entry.submitterUserId == this.client.user.userId) { 'SelectedBBSSubmitter' == mciName &&
this.setViewText('view',MciViewIds.view.SelectedBBSSubmitter, stringFormat(youSubmittedFormat, entry)); entry.submitterUserId == this.client.user.userId
) {
this.setViewText(
'view',
MciViewIds.view.SelectedBBSSubmitter,
stringFormat(youSubmittedFormat, entry)
);
} else { } else {
this.setViewText('view',MciViewIds.view[mciName], t); this.setViewText('view', MciViewIds.view[mciName], t);
} }
} }
}); });
@ -227,7 +253,7 @@ exports.getModule = class BBSListModule extends MenuModule {
async.waterfall( async.waterfall(
[ [
function clearAndDisplayArt(callback) { function clearAndDisplayArt(callback) {
if(self.viewControllers.add) { if (self.viewControllers.add) {
self.viewControllers.add.setFocus(false); self.viewControllers.add.setFocus(false);
} }
if (clearScreen) { if (clearScreen) {
@ -236,34 +262,41 @@ exports.getModule = class BBSListModule extends MenuModule {
theme.displayThemedAsset( theme.displayThemedAsset(
self.menuConfig.config.art.entries, self.menuConfig.config.art.entries,
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font: self.menuConfig.font, trailingLF: false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if (_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'view', 'view',
new ViewController( { client : self.client, formId : FormIds.View } ) new ViewController({
client: self.client,
formId: FormIds.View,
})
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu: self,
mciMap : artData.mciMap, mciMap: artData.mciMap,
formId : FormIds.View, formId: FormIds.View,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
} else { } else {
self.viewControllers.view.setFocus(true); self.viewControllers.view.setFocus(true);
self.viewControllers.view.getView(MciViewIds.view.BBSList).redraw(); self.viewControllers.view
.getView(MciViewIds.view.BBSList)
.redraw();
return callback(null); return callback(null);
} }
}, },
function fetchEntries(callback) { function fetchEntries(callback) {
const entriesView = self.viewControllers.view.getView(MciViewIds.view.BBSList); const entriesView = self.viewControllers.view.getView(
MciViewIds.view.BBSList
);
self.entries = []; self.entries = [];
self.database.each( self.database.each(
@ -272,16 +305,16 @@ exports.getModule = class BBSListModule extends MenuModule {
(err, row) => { (err, row) => {
if (!err) { if (!err) {
self.entries.push({ self.entries.push({
text : row.bbs_name, // standard field text: row.bbs_name, // standard field
id : row.id, id: row.id,
bbsName : row.bbs_name, bbsName: row.bbs_name,
sysOp : row.sysop, sysOp: row.sysop,
telnet : row.telnet, telnet: row.telnet,
www : row.www, www: row.www,
location : row.location, location: row.location,
software : row.software, software: row.software,
submitterUserId : row.submitter_user_id, submitterUserId: row.submitter_user_id,
notes : row.notes, notes: row.notes,
}); });
} }
}, },
@ -291,18 +324,22 @@ exports.getModule = class BBSListModule extends MenuModule {
); );
}, },
function getUserNames(entriesView, callback) { function getUserNames(entriesView, callback) {
async.each(self.entries, (entry, next) => { async.each(
User.getUserName(entry.submitterUserId, (err, username) => { self.entries,
if(username) { (entry, next) => {
entry.submitter = username; User.getUserName(entry.submitterUserId, (err, username) => {
} else { if (username) {
entry.submitter = 'N/A'; entry.submitter = username;
} } else {
return next(); entry.submitter = 'N/A';
}); }
}, () => { return next();
return callback(null, entriesView); });
}); },
() => {
return callback(null, entriesView);
}
);
}, },
function populateEntries(entriesView, callback) { function populateEntries(entriesView, callback) {
self.setEntries(entriesView); self.setEntries(entriesView);
@ -312,7 +349,7 @@ exports.getModule = class BBSListModule extends MenuModule {
self.drawSelectedEntry(entry); self.drawSelectedEntry(entry);
if(!entry) { if (!entry) {
self.selectedBBS = -1; self.selectedBBS = -1;
} else { } else {
self.selectedBBS = idx; self.selectedBBS = idx;
@ -331,10 +368,10 @@ exports.getModule = class BBSListModule extends MenuModule {
entriesView.redraw(); entriesView.redraw();
return callback(null); return callback(null);
} },
], ],
err => { err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
} }
@ -353,23 +390,26 @@ exports.getModule = class BBSListModule extends MenuModule {
theme.displayThemedAsset( theme.displayThemedAsset(
self.menuConfig.config.art.add, self.menuConfig.config.art.add,
self.client, self.client,
{ font : self.menuConfig.font }, { font: self.menuConfig.font },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function initOrRedrawViewController(artData, callback) { function initOrRedrawViewController(artData, callback) {
if(_.isUndefined(self.viewControllers.add)) { if (_.isUndefined(self.viewControllers.add)) {
const vc = self.addViewController( const vc = self.addViewController(
'add', 'add',
new ViewController( { client : self.client, formId : FormIds.Add } ) new ViewController({
client: self.client,
formId: FormIds.Add,
})
); );
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu: self,
mciMap : artData.mciMap, mciMap: artData.mciMap,
formId : FormIds.Add, formId: FormIds.Add,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -379,10 +419,10 @@ exports.getModule = class BBSListModule extends MenuModule {
self.viewControllers.add.switchFocus(MciViewIds.add.BBSName); self.viewControllers.add.switchFocus(MciViewIds.add.BBSName);
return callback(null); return callback(null);
} }
} },
], ],
err => { err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
} }
@ -390,7 +430,16 @@ exports.getModule = class BBSListModule extends MenuModule {
} }
clearAddForm() { clearAddForm() {
[ 'BBSName', 'Sysop', 'Telnet', 'Www', 'Location', 'Software', 'Error', 'Notes' ].forEach( mciName => { [
'BBSName',
'Sysop',
'Telnet',
'Www',
'Location',
'Software',
'Error',
'Notes',
].forEach(mciName => {
this.setViewText('add', MciViewIds.add[mciName], ''); this.setViewText('add', MciViewIds.add[mciName], '');
}); });
} }
@ -401,13 +450,12 @@ exports.getModule = class BBSListModule extends MenuModule {
async.series( async.series(
[ [
function openDatabase(callback) { function openDatabase(callback) {
self.database = getTransactionDatabase(new sqlite3.Database( self.database = getTransactionDatabase(
getModDatabasePath(moduleInfo), new sqlite3.Database(getModDatabasePath(moduleInfo), callback)
callback );
));
}, },
function createTables(callback) { function createTables(callback) {
self.database.serialize( () => { self.database.serialize(() => {
self.database.run( self.database.run(
`CREATE TABLE IF NOT EXISTS bbs_list ( `CREATE TABLE IF NOT EXISTS bbs_list (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@ -423,7 +471,7 @@ exports.getModule = class BBSListModule extends MenuModule {
); );
}); });
callback(null); callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);

View File

@ -1,17 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const TextView = require('./text_view.js').TextView; const TextView = require('./text_view.js').TextView;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const util = require('util'); const util = require('util');
exports.ButtonView = ButtonView; exports.ButtonView = ButtonView;
function ButtonView(options) { function ButtonView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.justify = miscUtil.valueWithDefault(options.justify, 'center'); options.justify = miscUtil.valueWithDefault(options.justify, 'center');
options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide'); options.cursor = miscUtil.valueWithDefault(options.cursor, 'hide');
TextView.call(this, options); TextView.call(this, options);
@ -20,8 +20,8 @@ function ButtonView(options) {
util.inherits(ButtonView, TextView); util.inherits(ButtonView, TextView);
ButtonView.prototype.onKeyPress = function(ch, key) { ButtonView.prototype.onKeyPress = function (ch, key) {
if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) { if (this.isKeyMapped('accept', key ? key.name : ch) || ' ' === ch) {
this.submitData = 'accept'; this.submitData = 'accept';
this.emit('action', 'accept'); this.emit('action', 'accept');
delete this.submitData; delete this.submitData;
@ -30,6 +30,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
} }
}; };
ButtonView.prototype.getData = function() { ButtonView.prototype.getData = function () {
return this.submitData || null; return this.submitData || null;
}; };

View File

@ -32,22 +32,22 @@
----/snip/---------------------- ----/snip/----------------------
*/ */
// ENiGMA½ // ENiGMA½
const term = require('./client_term.js'); const term = require('./client_term.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const User = require('./user.js'); const User = require('./user.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const MenuStack = require('./menu_stack.js'); const MenuStack = require('./menu_stack.js');
const ACS = require('./acs.js'); const ACS = require('./acs.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserInterruptQueue = require('./user_interrupt_queue.js'); const UserInterruptQueue = require('./user_interrupt_queue.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
// deps // deps
const stream = require('stream'); const stream = require('stream');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.Client = Client; exports.Client = Client;
// :TODO: Move all of the key stuff to it's own module // :TODO: Move all of the key stuff to it's own module
@ -56,86 +56,93 @@ exports.Client = Client;
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html // * http://www.ansi-bbs.org/ansi-bbs-core-server.html
// //
/* eslint-disable no-control-regex */ /* eslint-disable no-control-regex */
const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/; const RE_DSR_RESPONSE_ANYWHERE = /(?:\u001b\[)([0-9;]+)(R)/;
const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/; const RE_DEV_ATTR_RESPONSE_ANYWHERE = /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/;
const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/; const RE_META_KEYCODE_ANYWHERE = /(?:\u001b)([a-zA-Z0-9])/;
const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$'); const RE_META_KEYCODE = new RegExp('^' + RE_META_KEYCODE_ANYWHERE.source + '$');
const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [ const RE_FUNCTION_KEYCODE_ANYWHERE = new RegExp(
'(\\d+)(?:;(\\d+))?([~^$])', '(?:\u001b+)(O|N|\\[|\\[\\[)(?:' +
'(?:M([@ #!a`])(.)(.))', // mouse stuff [
'(?:1;)?(\\d+)?([a-zA-Z@])' '(\\d+)(?:;(\\d+))?([~^$])',
].join('|') + ')'); '(?:M([@ #!a`])(.)(.))', // mouse stuff
'(?:1;)?(\\d+)?([a-zA-Z@])',
].join('|') +
')'
);
/* eslint-enable no-control-regex */ /* eslint-enable no-control-regex */
const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source); const RE_FUNCTION_KEYCODE = new RegExp('^' + RE_FUNCTION_KEYCODE_ANYWHERE.source);
const RE_ESC_CODE_ANYWHERE = new RegExp( [ const RE_ESC_CODE_ANYWHERE = new RegExp(
RE_FUNCTION_KEYCODE_ANYWHERE.source, [
RE_META_KEYCODE_ANYWHERE.source, RE_FUNCTION_KEYCODE_ANYWHERE.source,
RE_DSR_RESPONSE_ANYWHERE.source, RE_META_KEYCODE_ANYWHERE.source,
RE_DEV_ATTR_RESPONSE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source,
/\u001b./.source // eslint-disable-line no-control-regex RE_DEV_ATTR_RESPONSE_ANYWHERE.source,
].join('|')); /\u001b./.source, // eslint-disable-line no-control-regex
].join('|')
);
function Client(/*input, output*/) { function Client(/*input, output*/) {
stream.call(this); stream.call(this);
const self = this; const self = this;
this.user = new User(); this.user = new User();
this.currentThemeConfig = { info : { name : 'N/A', description : 'None' } }; this.currentThemeConfig = { info: { name: 'N/A', description: 'None' } };
this.lastActivityTime = Date.now(); this.lastActivityTime = Date.now();
this.menuStack = new MenuStack(this); this.menuStack = new MenuStack(this);
this.acs = new ACS( { client : this, user : this.user } ); this.acs = new ACS({ client: this, user: this.user });
this.interruptQueue = new UserInterruptQueue(this); this.interruptQueue = new UserInterruptQueue(this);
Object.defineProperty(this, 'currentTheme', { Object.defineProperty(this, 'currentTheme', {
get : () => { get: () => {
if (this.currentThemeConfig) { if (this.currentThemeConfig) {
return this.currentThemeConfig.get(); return this.currentThemeConfig.get();
} else { } else {
return { return {
info : { info: {
name : 'N/A', name: 'N/A',
author : 'N/A', author: 'N/A',
description : 'N/A', description: 'N/A',
group : 'N/A', group: 'N/A',
} },
}; };
} }
}, },
set : (theme) => { set: theme => {
this.currentThemeConfig = theme; this.currentThemeConfig = theme;
} },
}); });
Object.defineProperty(this, 'node', { Object.defineProperty(this, 'node', {
get : function() { get: function () {
return self.session.id; return self.session.id;
} },
}); });
Object.defineProperty(this, 'currentMenuModule', { Object.defineProperty(this, 'currentMenuModule', {
get : function() { get: function () {
return self.menuStack.currentModule; return self.menuStack.currentModule;
} },
}); });
this.setTemporaryDirectDataHandler = function(handler) { this.setTemporaryDirectDataHandler = function (handler) {
this.dataPassthrough = true; // let implementations do with what they will here this.dataPassthrough = true; // let implementations do with what they will here
this.input.removeAllListeners('data'); this.input.removeAllListeners('data');
this.input.on('data', handler); this.input.on('data', handler);
}; };
this.restoreDataHandler = function() { this.restoreDataHandler = function () {
this.dataPassthrough = false; this.dataPassthrough = false;
this.input.removeAllListeners('data'); this.input.removeAllListeners('data');
this.input.on('data', this.dataHandler); this.input.on('data', this.dataHandler);
}; };
this.themeChangedListener = function( { themeId } ) { this.themeChangedListener = function ({ themeId }) {
if(_.get(self.currentTheme, 'info.themeId') === themeId) { if (_.get(self.currentTheme, 'info.themeId') === themeId) {
self.currentThemeConfig = require('./theme.js').getAvailableThemes().get(themeId); self.currentThemeConfig = require('./theme.js')
.getAvailableThemes()
.get(themeId);
} }
}; };
@ -151,14 +158,14 @@ function Client(/*input, output*/) {
// * http://www.ansi-bbs.org/ansi-bbs-core-server.html // * http://www.ansi-bbs.org/ansi-bbs-core-server.html
// * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/ // * Christopher Jeffrey's Blessed library @ https://github.com/chjj/blessed/
// //
this.getTermClient = function(deviceAttr) { this.getTermClient = function (deviceAttr) {
let termClient = { let termClient = {
'63;1;2' : 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android) '63;1;2': 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android)
'50;86;84;88' : 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt '50;86;84;88': 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
}[deviceAttr]; }[deviceAttr];
if(!termClient) { if (!termClient) {
if(_.startsWith(deviceAttr, '67;84;101;114;109')) { if (_.startsWith(deviceAttr, '67;84;101;114;109')) {
// //
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt // See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
// //
@ -173,176 +180,178 @@ function Client(/*input, output*/) {
}; };
/* eslint-disable no-control-regex */ /* eslint-disable no-control-regex */
this.isMouseInput = function(data) { this.isMouseInput = function (data) {
return /\x1b\[M/.test(data) || return (
/\x1b\[M/.test(data) ||
/\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) || /\u001b\[M([\x00\u0020-\uffff]{3})/.test(data) ||
/\u001b\[(\d+;\d+;\d+)M/.test(data) || /\u001b\[(\d+;\d+;\d+)M/.test(data) ||
/\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) || /\u001b\[<(\d+;\d+;\d+)([mM])/.test(data) ||
/\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) || /\u001b\[<(\d+;\d+;\d+;\d+)&w/.test(data) ||
/\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) || /\u001b\[24([0135])~\[(\d+),(\d+)\]\r/.test(data) ||
/\u001b\[(O|I)/.test(data); /\u001b\[(O|I)/.test(data)
);
}; };
/* eslint-enable no-control-regex */ /* eslint-enable no-control-regex */
this.getKeyComponentsFromCode = function(code) { this.getKeyComponentsFromCode = function (code) {
return { return {
// xterm/gnome // xterm/gnome
'OP' : { name : 'f1' }, OP: { name: 'f1' },
'OQ' : { name : 'f2' }, OQ: { name: 'f2' },
'OR' : { name : 'f3' }, OR: { name: 'f3' },
'OS' : { name : 'f4' }, OS: { name: 'f4' },
'OA' : { name : 'up arrow' }, OA: { name: 'up arrow' },
'OB' : { name : 'down arrow' }, OB: { name: 'down arrow' },
'OC' : { name : 'right arrow' }, OC: { name: 'right arrow' },
'OD' : { name : 'left arrow' }, OD: { name: 'left arrow' },
'OE' : { name : 'clear' }, OE: { name: 'clear' },
'OF' : { name : 'end' }, OF: { name: 'end' },
'OH' : { name : 'home' }, OH: { name: 'home' },
// xterm/rxvt // xterm/rxvt
'[11~' : { name : 'f1' }, '[11~': { name: 'f1' },
'[12~' : { name : 'f2' }, '[12~': { name: 'f2' },
'[13~' : { name : 'f3' }, '[13~': { name: 'f3' },
'[14~' : { name : 'f4' }, '[14~': { name: 'f4' },
'[1~' : { name : 'home' }, '[1~': { name: 'home' },
'[2~' : { name : 'insert' }, '[2~': { name: 'insert' },
'[3~' : { name : 'delete' }, '[3~': { name: 'delete' },
'[4~' : { name : 'end' }, '[4~': { name: 'end' },
'[5~' : { name : 'page up' }, '[5~': { name: 'page up' },
'[6~' : { name : 'page down' }, '[6~': { name: 'page down' },
// Cygwin & libuv // Cygwin & libuv
'[[A' : { name : 'f1' }, '[[A': { name: 'f1' },
'[[B' : { name : 'f2' }, '[[B': { name: 'f2' },
'[[C' : { name : 'f3' }, '[[C': { name: 'f3' },
'[[D' : { name : 'f4' }, '[[D': { name: 'f4' },
'[[E' : { name : 'f5' }, '[[E': { name: 'f5' },
// Common impls // Common impls
'[15~' : { name : 'f5' }, '[15~': { name: 'f5' },
'[17~' : { name : 'f6' }, '[17~': { name: 'f6' },
'[18~' : { name : 'f7' }, '[18~': { name: 'f7' },
'[19~' : { name : 'f8' }, '[19~': { name: 'f8' },
'[20~' : { name : 'f9' }, '[20~': { name: 'f9' },
'[21~' : { name : 'f10' }, '[21~': { name: 'f10' },
'[23~' : { name : 'f11' }, '[23~': { name: 'f11' },
'[24~' : { name : 'f12' }, '[24~': { name: 'f12' },
// xterm // xterm
'[A' : { name : 'up arrow' }, '[A': { name: 'up arrow' },
'[B' : { name : 'down arrow' }, '[B': { name: 'down arrow' },
'[C' : { name : 'right arrow' }, '[C': { name: 'right arrow' },
'[D' : { name : 'left arrow' }, '[D': { name: 'left arrow' },
'[E' : { name : 'clear' }, '[E': { name: 'clear' },
'[F' : { name : 'end' }, '[F': { name: 'end' },
'[H' : { name : 'home' }, '[H': { name: 'home' },
// PuTTY // PuTTY
'[[5~' : { name : 'page up' }, '[[5~': { name: 'page up' },
'[[6~' : { name : 'page down' }, '[[6~': { name: 'page down' },
// rvxt // rvxt
'[7~' : { name : 'home' }, '[7~': { name: 'home' },
'[8~' : { name : 'end' }, '[8~': { name: 'end' },
// rxvt with modifiers // rxvt with modifiers
'[a' : { name : 'up arrow', shift : true }, '[a': { name: 'up arrow', shift: true },
'[b' : { name : 'down arrow', shift : true }, '[b': { name: 'down arrow', shift: true },
'[c' : { name : 'right arrow', shift : true }, '[c': { name: 'right arrow', shift: true },
'[d' : { name : 'left arrow', shift : true }, '[d': { name: 'left arrow', shift: true },
'[e' : { name : 'clear', shift : true }, '[e': { name: 'clear', shift: true },
'[2$' : { name : 'insert', shift : true }, '[2$': { name: 'insert', shift: true },
'[3$' : { name : 'delete', shift : true }, '[3$': { name: 'delete', shift: true },
'[5$' : { name : 'page up', shift : true }, '[5$': { name: 'page up', shift: true },
'[6$' : { name : 'page down', shift : true }, '[6$': { name: 'page down', shift: true },
'[7$' : { name : 'home', shift : true }, '[7$': { name: 'home', shift: true },
'[8$' : { name : 'end', shift : true }, '[8$': { name: 'end', shift: true },
'Oa' : { name : 'up arrow', ctrl : true }, Oa: { name: 'up arrow', ctrl: true },
'Ob' : { name : 'down arrow', ctrl : true }, Ob: { name: 'down arrow', ctrl: true },
'Oc' : { name : 'right arrow', ctrl : true }, Oc: { name: 'right arrow', ctrl: true },
'Od' : { name : 'left arrow', ctrl : true }, Od: { name: 'left arrow', ctrl: true },
'Oe' : { name : 'clear', ctrl : true }, Oe: { name: 'clear', ctrl: true },
'[2^' : { name : 'insert', ctrl : true }, '[2^': { name: 'insert', ctrl: true },
'[3^' : { name : 'delete', ctrl : true }, '[3^': { name: 'delete', ctrl: true },
'[5^' : { name : 'page up', ctrl : true }, '[5^': { name: 'page up', ctrl: true },
'[6^' : { name : 'page down', ctrl : true }, '[6^': { name: 'page down', ctrl: true },
'[7^' : { name : 'home', ctrl : true }, '[7^': { name: 'home', ctrl: true },
'[8^' : { name : 'end', ctrl : true }, '[8^': { name: 'end', ctrl: true },
// SyncTERM / EtherTerm // SyncTERM / EtherTerm
'[K' : { name : 'end' }, '[K': { name: 'end' },
'[@' : { name : 'insert' }, '[@': { name: 'insert' },
'[V' : { name : 'page up' }, '[V': { name: 'page up' },
'[U' : { name : 'page down' }, '[U': { name: 'page down' },
// other // other
'[Z' : { name : 'tab', shift : true }, '[Z': { name: 'tab', shift: true },
}[code]; }[code];
}; };
this.on('data', function clientData(data) { this.on('data', function clientData(data) {
// create a uniform format that can be parsed below // create a uniform format that can be parsed below
if(data[0] > 127 && undefined === data[1]) { if (data[0] > 127 && undefined === data[1]) {
data[0] -= 128; data[0] -= 128;
data = '\u001b' + data.toString('utf-8'); data = '\u001b' + data.toString('utf-8');
} else { } else {
data = data.toString('utf-8'); data = data.toString('utf-8');
} }
if(self.isMouseInput(data)) { if (self.isMouseInput(data)) {
return; return;
} }
var buf = []; var buf = [];
var m; var m;
while((m = RE_ESC_CODE_ANYWHERE.exec(data))) { while ((m = RE_ESC_CODE_ANYWHERE.exec(data))) {
buf = buf.concat(data.slice(0, m.index).split('')); buf = buf.concat(data.slice(0, m.index).split(''));
buf.push(m[0]); buf.push(m[0]);
data = data.slice(m.index + m[0].length); data = data.slice(m.index + m[0].length);
} }
buf = buf.concat(data.split('')); // remainder buf = buf.concat(data.split('')); // remainder
buf.forEach(function bufPart(s) { buf.forEach(function bufPart(s) {
var key = { var key = {
seq : s, seq: s,
name : undefined, name: undefined,
ctrl : false, ctrl: false,
meta : false, meta: false,
shift : false, shift: false,
}; };
var parts; var parts;
if((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) { if ((parts = RE_DSR_RESPONSE_ANYWHERE.exec(s))) {
if('R' === parts[2]) { if ('R' === parts[2]) {
const cprArgs = parts[1].split(';').map(v => (parseInt(v, 10) || 0) ); const cprArgs = parts[1].split(';').map(v => parseInt(v, 10) || 0);
if(2 === cprArgs.length) { if (2 === cprArgs.length) {
if(self.cprOffset) { if (self.cprOffset) {
cprArgs[0] = cprArgs[0] + self.cprOffset; cprArgs[0] = cprArgs[0] + self.cprOffset;
cprArgs[1] = cprArgs[1] + self.cprOffset; cprArgs[1] = cprArgs[1] + self.cprOffset;
} }
self.emit('cursor position report', cprArgs); self.emit('cursor position report', cprArgs);
} }
} }
} else if((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) { } else if ((parts = RE_DEV_ATTR_RESPONSE_ANYWHERE.exec(s))) {
assert('c' === parts[2]); assert('c' === parts[2]);
var termClient = self.getTermClient(parts[1]); var termClient = self.getTermClient(parts[1]);
if(termClient) { if (termClient) {
self.term.termClient = termClient; self.term.termClient = termClient;
} }
} else if('\r' === s) { } else if ('\r' === s) {
key.name = 'return'; key.name = 'return';
} else if('\n' === s) { } else if ('\n' === s) {
key.name = 'line feed'; key.name = 'line feed';
} else if('\t' === s) { } else if ('\t' === s) {
key.name = 'tab'; key.name = 'tab';
} else if('\x7f' === s) { } else if ('\x7f' === s) {
// //
// Backspace vs delete is a crazy thing, especially in *nix. // Backspace vs delete is a crazy thing, especially in *nix.
// - ANSI-BBS uses 0x7f for DEL // - ANSI-BBS uses 0x7f for DEL
@ -351,61 +360,63 @@ function Client(/*input, output*/) {
// See http://www.hypexr.org/linux_ruboff.php // See http://www.hypexr.org/linux_ruboff.php
// And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html // And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html
// //
if(self.term.isNixTerm()) { if (self.term.isNixTerm()) {
key.name = 'backspace'; key.name = 'backspace';
} else { } else {
key.name = 'delete'; key.name = 'delete';
} }
} else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) { } else if ('\b' === s || '\x1b\x7f' === s || '\x1b\b' === s) {
// backspace, CTRL-H // backspace, CTRL-H
key.name = 'backspace'; key.name = 'backspace';
key.meta = ('\x1b' === s.charAt(0)); key.meta = '\x1b' === s.charAt(0);
} else if('\x1b' === s || '\x1b\x1b' === s) { } else if ('\x1b' === s || '\x1b\x1b' === s) {
key.name = 'escape'; key.name = 'escape';
key.meta = (2 === s.length); key.meta = 2 === s.length;
} else if (' ' === s || '\x1b ' === s) { } else if (' ' === s || '\x1b ' === s) {
// rather annoying that space can come in other than just " " // rather annoying that space can come in other than just " "
key.name = 'space'; key.name = 'space';
key.meta = (2 === s.length); key.meta = 2 === s.length;
} else if(1 === s.length && s <= '\x1a') { } else if (1 === s.length && s <= '\x1a') {
// CTRL-<letter> // CTRL-<letter>
key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
key.ctrl = true; key.ctrl = true;
} else if(1 === s.length && s >= 'a' && s <= 'z') { } else if (1 === s.length && s >= 'a' && s <= 'z') {
// normal, lowercased letter // normal, lowercased letter
key.name = s; key.name = s;
} else if(1 === s.length && s >= 'A' && s <= 'Z') { } else if (1 === s.length && s >= 'A' && s <= 'Z') {
key.name = s.toLowerCase(); key.name = s.toLowerCase();
key.shift = true; key.shift = true;
} else if ((parts = RE_META_KEYCODE.exec(s))) { } else if ((parts = RE_META_KEYCODE.exec(s))) {
// meta with character key // meta with character key
key.name = parts[1].toLowerCase(); key.name = parts[1].toLowerCase();
key.meta = true; key.meta = true;
key.shift = /^[A-Z]$/.test(parts[1]); key.shift = /^[A-Z]$/.test(parts[1]);
} else if((parts = RE_FUNCTION_KEYCODE.exec(s))) { } else if ((parts = RE_FUNCTION_KEYCODE.exec(s))) {
var code = var code =
(parts[1] || '') + (parts[2] || '') + (parts[1] || '') +
(parts[4] || '') + (parts[9] || ''); (parts[2] || '') +
(parts[4] || '') +
(parts[9] || '');
var modifier = (parts[3] || parts[8] || 1) - 1; var modifier = (parts[3] || parts[8] || 1) - 1;
key.ctrl = !!(modifier & 4); key.ctrl = !!(modifier & 4);
key.meta = !!(modifier & 10); key.meta = !!(modifier & 10);
key.shift = !!(modifier & 1); key.shift = !!(modifier & 1);
key.code = code; key.code = code;
_.assign(key, self.getKeyComponentsFromCode(code)); _.assign(key, self.getKeyComponentsFromCode(code));
} }
var ch; var ch;
if(1 === s.length) { if (1 === s.length) {
ch = s; ch = s;
} else if('space' === key.name) { } else if ('space' === key.name) {
// stupid hack to always get space as a regular char // stupid hack to always get space as a regular char
ch = ' '; ch = ' ';
} }
if(_.isUndefined(key.name)) { if (_.isUndefined(key.name)) {
key = undefined; key = undefined;
} else { } else {
// //
@ -418,14 +429,14 @@ function Client(/*input, output*/) {
key.name; key.name;
} }
if(key || ch) { if (key || ch) {
if(Config().logging.traceUserKeyboardInput) { if (Config().logging.traceUserKeyboardInput) {
self.log.trace( { key : key, ch : escape(ch) }, 'User keyboard input'); // jshint ignore:line self.log.trace({ key: key, ch: escape(ch) }, 'User keyboard input'); // jshint ignore:line
} }
self.lastActivityTime = Date.now(); self.lastActivityTime = Date.now();
if(!self.ignoreInput) { if (!self.ignoreInput) {
self.emit('key press', ch, key); self.emit('key press', ch, key);
} }
} }
@ -435,23 +446,23 @@ function Client(/*input, output*/) {
require('util').inherits(Client, stream); require('util').inherits(Client, stream);
Client.prototype.setInputOutput = function(input, output) { Client.prototype.setInputOutput = function (input, output) {
this.input = input; this.input = input;
this.output = output; this.output = output;
this.term = new term.ClientTerminal(this.output); this.term = new term.ClientTerminal(this.output);
}; };
Client.prototype.setTermType = function(termType) { Client.prototype.setTermType = function (termType) {
this.term.env.TERM = termType; this.term.env.TERM = termType;
this.term.termType = termType; this.term.termType = termType;
this.log.debug( { termType : termType }, 'Set terminal type'); this.log.debug({ termType: termType }, 'Set terminal type');
}; };
Client.prototype.startIdleMonitor = function() { Client.prototype.startIdleMonitor = function () {
// clear existing, if any // clear existing, if any
if(this.idleCheck) { if (this.idleCheck) {
this.stopIdleMonitor(); this.stopIdleMonitor();
} }
@ -462,11 +473,11 @@ Client.prototype.startIdleMonitor = function() {
// We also update minutes spent online the system here, // We also update minutes spent online the system here,
// if we have a authenticated user. // if we have a authenticated user.
// //
this.idleCheck = setInterval( () => { this.idleCheck = setInterval(() => {
const nowMs = Date.now(); const nowMs = Date.now();
let idleLogoutSeconds; let idleLogoutSeconds;
if(this.user.isAuthenticated()) { if (this.user.isAuthenticated()) {
idleLogoutSeconds = Config().users.idleLogoutSeconds; idleLogoutSeconds = Config().users.idleLogoutSeconds;
// //
@ -474,17 +485,17 @@ Client.prototype.startIdleMonitor = function() {
// every user, but want at least some updates for various things // every user, but want at least some updates for various things
// such as achievements. Send off every 5m. // such as achievements. Send off every 5m.
// //
const minOnline = this.user.incrementProperty(UserProps.MinutesOnlineTotalCount, 1); const minOnline = this.user.incrementProperty(
if(0 === (minOnline % 5)) { UserProps.MinutesOnlineTotalCount,
Events.emit( 1
Events.getSystemEvents().UserStatIncrement, );
{ if (0 === minOnline % 5) {
user : this.user, Events.emit(Events.getSystemEvents().UserStatIncrement, {
statName : UserProps.MinutesOnlineTotalCount, user: this.user,
statIncrementBy : 1, statName: UserProps.MinutesOnlineTotalCount,
statValue : minOnline statIncrementBy: 1,
} statValue: minOnline,
); });
} }
} else { } else {
idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds; idleLogoutSeconds = Config().users.preAuthIdleLogoutSeconds;
@ -493,46 +504,52 @@ Client.prototype.startIdleMonitor = function() {
// use override value if set // use override value if set
idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds; idleLogoutSeconds = this.idleLogoutSecondsOverride || idleLogoutSeconds;
if(idleLogoutSeconds > 0 && (nowMs - this.lastActivityTime >= (idleLogoutSeconds * 1000))) { if (
idleLogoutSeconds > 0 &&
nowMs - this.lastActivityTime >= idleLogoutSeconds * 1000
) {
this.emit('idle timeout'); this.emit('idle timeout');
} }
}, 1000 * 60); }, 1000 * 60);
}; };
Client.prototype.stopIdleMonitor = function() { Client.prototype.stopIdleMonitor = function () {
if(this.idleCheck) { if (this.idleCheck) {
clearInterval(this.idleCheck); clearInterval(this.idleCheck);
delete this.idleCheck; delete this.idleCheck;
} }
}; };
Client.prototype.explicitActivityTimeUpdate = function() { Client.prototype.explicitActivityTimeUpdate = function () {
this.lastActivityTime = Date.now(); this.lastActivityTime = Date.now();
}; };
Client.prototype.overrideIdleLogoutSeconds = function(seconds) { Client.prototype.overrideIdleLogoutSeconds = function (seconds) {
this.idleLogoutSecondsOverride = seconds; this.idleLogoutSecondsOverride = seconds;
}; };
Client.prototype.restoreIdleLogoutSeconds = function() { Client.prototype.restoreIdleLogoutSeconds = function () {
delete this.idleLogoutSecondsOverride; delete this.idleLogoutSecondsOverride;
}; };
Client.prototype.end = function () { Client.prototype.end = function () {
if(this.term) { if (this.term) {
this.term.disconnect(); this.term.disconnect();
} }
Events.removeListener(Events.getSystemEvents().ThemeChanged, this.themeChangedListener); Events.removeListener(
Events.getSystemEvents().ThemeChanged,
this.themeChangedListener
);
const currentModule = this.menuStack.getCurrentModule; const currentModule = this.menuStack.getCurrentModule;
if(currentModule) { if (currentModule) {
currentModule.leave(); currentModule.leave();
} }
// persist time online for authenticated users // persist time online for authenticated users
if(this.user.isAuthenticated()) { if (this.user.isAuthenticated()) {
this.user.persistProperty( this.user.persistProperty(
UserProps.MinutesOnlineTotalCount, UserProps.MinutesOnlineTotalCount,
this.user.getProperty(UserProps.MinutesOnlineTotalCount) this.user.getProperty(UserProps.MinutesOnlineTotalCount)
@ -545,13 +562,13 @@ Client.prototype.end = function () {
// //
// We can end up calling 'end' before TTY/etc. is established, e.g. with SSH // We can end up calling 'end' before TTY/etc. is established, e.g. with SSH
// //
if(_.isFunction(this.disconnect)) { if (_.isFunction(this.disconnect)) {
return this.disconnect(); return this.disconnect();
} else { } else {
// legacy fallback // legacy fallback
return this.output.end.apply(this.output, arguments); return this.output.end.apply(this.output, arguments);
} }
} catch(e) { } catch (e) {
// ie TypeError // ie TypeError
} }
}; };
@ -564,15 +581,15 @@ Client.prototype.destroySoon = function () {
return this.output.destroySoon.apply(this.output, arguments); return this.output.destroySoon.apply(this.output, arguments);
}; };
Client.prototype.waitForKeyPress = function(cb) { Client.prototype.waitForKeyPress = function (cb) {
this.once('key press', function kp(ch, key) { this.once('key press', function kp(ch, key) {
cb(ch, key); cb(ch, key);
}); });
}; };
Client.prototype.isLocal = function() { Client.prototype.isLocal = function () {
// :TODO: Handle ipv6 better // :TODO: Handle ipv6 better
return [ '127.0.0.1', '::ffff:127.0.0.1' ].includes(this.remoteAddress); return ['127.0.0.1', '::ffff:127.0.0.1'].includes(this.remoteAddress);
}; };
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -580,7 +597,7 @@ Client.prototype.isLocal = function() {
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something // :TODO: getDefaultHandler(name) -- handlers in default_handlers.js or something
Client.prototype.defaultHandlerMissingMod = function() { Client.prototype.defaultHandlerMissingMod = function () {
var self = this; var self = this;
function handler(err) { function handler(err) {
@ -591,7 +608,6 @@ Client.prototype.defaultHandlerMissingMod = function() {
self.term.write('This has been logged for your SysOp to review.\n'); self.term.write('This has been logged for your SysOp to review.\n');
self.term.write('\nGoodbye!\n'); self.term.write('\nGoodbye!\n');
//self.term.write(err); //self.term.write(err);
//if(miscUtil.isDevelopment() && err.stack) { //if(miscUtil.isDevelopment() && err.stack) {
@ -604,18 +620,18 @@ Client.prototype.defaultHandlerMissingMod = function() {
return handler; return handler;
}; };
Client.prototype.terminalSupports = function(query) { Client.prototype.terminalSupports = function (query) {
const termClient = this.term.termClient; const termClient = this.term.termClient;
switch(query) { switch (query) {
case 'vtx_audio' : case 'vtx_audio':
// https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
return 'vtx' === termClient; return 'vtx' === termClient;
case 'vtx_hyperlink' : case 'vtx_hyperlink':
return 'vtx' === termClient; return 'vtx' === termClient;
default : default:
return false; return false;
} }
}; };

View File

@ -2,34 +2,33 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const logger = require('./logger.js'); const logger = require('./logger.js');
const Events = require('./events.js'); const Events = require('./events.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const hashids = require('hashids/cjs'); const hashids = require('hashids/cjs');
exports.getActiveConnections = getActiveConnections; exports.getActiveConnections = getActiveConnections;
exports.getActiveConnectionList = getActiveConnectionList; exports.getActiveConnectionList = getActiveConnectionList;
exports.addNewClient = addNewClient; exports.addNewClient = addNewClient;
exports.removeClient = removeClient; exports.removeClient = removeClient;
exports.getConnectionByUserId = getConnectionByUserId; exports.getConnectionByUserId = getConnectionByUserId;
exports.getConnectionByNodeId = getConnectionByNodeId; exports.getConnectionByNodeId = getConnectionByNodeId;
const clientConnections = []; const clientConnections = [];
exports.clientConnections = clientConnections; exports.clientConnections = clientConnections;
function getActiveConnections(authUsersOnly = false) { function getActiveConnections(authUsersOnly = false) {
return clientConnections.filter(conn => { return clientConnections.filter(conn => {
return ((authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly); return (authUsersOnly && conn.user.isAuthenticated()) || !authUsersOnly;
}); });
} }
function getActiveConnectionList(authUsersOnly) { function getActiveConnectionList(authUsersOnly) {
if (!_.isBoolean(authUsersOnly)) {
if(!_.isBoolean(authUsersOnly)) {
authUsersOnly = true; authUsersOnly = true;
} }
@ -37,23 +36,26 @@ function getActiveConnectionList(authUsersOnly) {
return _.map(getActiveConnections(authUsersOnly), ac => { return _.map(getActiveConnections(authUsersOnly), ac => {
const entry = { const entry = {
node : ac.node, node: ac.node,
authenticated : ac.user.isAuthenticated(), authenticated: ac.user.isAuthenticated(),
userId : ac.user.userId, userId: ac.user.userId,
action : _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'), action: _.get(ac, 'currentMenuModule.menuConfig.desc', 'Unknown'),
}; };
// //
// There may be a connection, but not a logged in user as of yet // There may be a connection, but not a logged in user as of yet
// //
if(ac.user.isAuthenticated()) { if (ac.user.isAuthenticated()) {
entry.userName = ac.user.username; entry.userName = ac.user.username;
entry.realName = ac.user.properties[UserProps.RealName]; entry.realName = ac.user.properties[UserProps.RealName];
entry.location = ac.user.properties[UserProps.Location]; entry.location = ac.user.properties[UserProps.Location];
entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations]; entry.affils = entry.affiliation = ac.user.properties[UserProps.Affiliations];
const diff = now.diff(moment(ac.user.properties[UserProps.LastLoginTs]), 'minutes'); const diff = now.diff(
entry.timeOn = moment.duration(diff, 'minutes'); moment(ac.user.properties[UserProps.LastLoginTs]),
'minutes'
);
entry.timeOn = moment.duration(diff, 'minutes');
} }
return entry; return entry;
}); });
@ -67,39 +69,42 @@ function addNewClient(client, clientSock) {
for (nodeId = 1; nodeId < Number.MAX_SAFE_INTEGER; ++nodeId) { for (nodeId = 1; nodeId < Number.MAX_SAFE_INTEGER; ++nodeId) {
const existing = clientConnections.find(client => nodeId === client.node); const existing = clientConnections.find(client => nodeId === client.node);
if (!existing) { if (!existing) {
break; // available slot break; // available slot
} }
} }
client.session.id = nodeId; client.session.id = nodeId;
const remoteAddress = client.remoteAddress = clientSock.remoteAddress; const remoteAddress = (client.remoteAddress = clientSock.remoteAddress);
// create a unique identifier one-time ID for this session // create a unique identifier one-time ID for this session
client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([ nodeId, moment().valueOf() ]); client.session.uniqueId = new hashids('ENiGMA½ClientSession').encode([
nodeId,
moment().valueOf(),
]);
clientConnections.push(client); clientConnections.push(client);
clientConnections.sort( (c1, c2) => c1.session.id - c2.session.id); clientConnections.sort((c1, c2) => c1.session.id - c2.session.id);
// Create a client specific logger // Create a client specific logger
// Note that this will be updated @ login with additional information // Note that this will be updated @ login with additional information
client.log = logger.log.child( { nodeId, sessionId : client.session.uniqueId } ); client.log = logger.log.child({ nodeId, sessionId: client.session.uniqueId });
const connInfo = { const connInfo = {
remoteAddress : remoteAddress, remoteAddress: remoteAddress,
serverName : client.session.serverName, serverName: client.session.serverName,
isSecure : client.session.isSecure, isSecure: client.session.isSecure,
}; };
if(client.log.debug()) { if (client.log.debug()) {
connInfo.port = clientSock.localPort; connInfo.port = clientSock.localPort;
connInfo.family = clientSock.localFamily; connInfo.family = clientSock.localFamily;
} }
client.log.info(connInfo, 'Client connected'); client.log.info(connInfo, 'Client connected');
Events.emit( Events.emit(Events.getSystemEvents().ClientConnected, {
Events.getSystemEvents().ClientConnected, client: client,
{ client : client, connectionCount : clientConnections.length } connectionCount: clientConnections.length,
); });
return nodeId; return nodeId;
} }
@ -108,33 +113,39 @@ function removeClient(client) {
client.end(); client.end();
const i = clientConnections.indexOf(client); const i = clientConnections.indexOf(client);
if(i > -1) { if (i > -1) {
clientConnections.splice(i, 1); clientConnections.splice(i, 1);
logger.log.info( logger.log.info(
{ {
connectionCount : clientConnections.length, connectionCount: clientConnections.length,
nodeId : client.node, nodeId: client.node,
}, },
'Client disconnected' 'Client disconnected'
); );
if(client.user && client.user.isValid()) { if (client.user && client.user.isValid()) {
const minutesOnline = moment().diff(moment(client.user.properties[UserProps.LastLoginTs]), 'minutes'); const minutesOnline = moment().diff(
Events.emit(Events.getSystemEvents().UserLogoff, { user : client.user, minutesOnline } ); moment(client.user.properties[UserProps.LastLoginTs]),
'minutes'
);
Events.emit(Events.getSystemEvents().UserLogoff, {
user: client.user,
minutesOnline,
});
} }
Events.emit( Events.emit(Events.getSystemEvents().ClientDisconnected, {
Events.getSystemEvents().ClientDisconnected, client: client,
{ client : client, connectionCount : clientConnections.length } connectionCount: clientConnections.length,
); });
} }
} }
function getConnectionByUserId(userId) { function getConnectionByUserId(userId) {
return getActiveConnections().find( ac => userId === ac.user.userId ); return getActiveConnections().find(ac => userId === ac.user.userId);
} }
function getConnectionByNodeId(nodeId) { function getConnectionByNodeId(nodeId) {
return getActiveConnections().find( ac => nodeId == ac.node ); return getActiveConnections().find(ac => nodeId == ac.node);
} }

View File

@ -2,24 +2,23 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
var Log = require('./logger.js').log; var Log = require('./logger.js').log;
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
const Config = require('./config.js').get; const Config = require('./config.js').get;
var iconv = require('iconv-lite'); var iconv = require('iconv-lite');
var assert = require('assert'); var assert = require('assert');
var _ = require('lodash'); var _ = require('lodash');
exports.ClientTerminal = ClientTerminal;
exports.ClientTerminal = ClientTerminal;
function ClientTerminal(output) { function ClientTerminal(output) {
this.output = output; this.output = output;
var outputEncoding = 'cp437'; var outputEncoding = 'cp437';
assert(iconv.encodingExists(outputEncoding)); assert(iconv.encodingExists(outputEncoding));
// convert line feeds such as \n -> \r\n // convert line feeds such as \n -> \r\n
this.convertLF = true; this.convertLF = true;
this.syncTermFontsEnabled = false; this.syncTermFontsEnabled = false;
@ -27,37 +26,37 @@ function ClientTerminal(output) {
// Some terminal we handle specially // Some terminal we handle specially
// They can also be found in this.env{} // They can also be found in this.env{}
// //
var termType = 'unknown'; var termType = 'unknown';
var termHeight = 0; var termHeight = 0;
var termWidth = 0; var termWidth = 0;
var termClient = 'unknown'; var termClient = 'unknown';
this.currentSyncFont = 'not_set'; this.currentSyncFont = 'not_set';
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc. // Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
this.env = {}; this.env = {};
Object.defineProperty(this, 'outputEncoding', { Object.defineProperty(this, 'outputEncoding', {
get : function() { get: function () {
return outputEncoding; return outputEncoding;
}, },
set : function(enc) { set: function (enc) {
if(iconv.encodingExists(enc)) { if (iconv.encodingExists(enc)) {
outputEncoding = enc; outputEncoding = enc;
} else { } else {
Log.warn({ encoding : enc }, 'Unknown encoding'); Log.warn({ encoding: enc }, 'Unknown encoding');
} }
} },
}); });
Object.defineProperty(this, 'termType', { Object.defineProperty(this, 'termType', {
get : function() { get: function () {
return termType; return termType;
}, },
set : function(ttype) { set: function (ttype) {
termType = ttype.toLowerCase(); termType = ttype.toLowerCase();
if(this.isANSI()) { if (this.isANSI()) {
this.outputEncoding = 'cp437'; this.outputEncoding = 'cp437';
} else { } else {
// :TODO: See how x84 does this -- only set if local/remote are binary // :TODO: See how x84 does this -- only set if local/remote are binary
@ -68,53 +67,56 @@ function ClientTerminal(output) {
// Windows telnet will send "VTNT". If so, set termClient='windows' // Windows telnet will send "VTNT". If so, set termClient='windows'
// there are some others on the page as well // there are some others on the page as well
Log.debug( { encoding : this.outputEncoding }, 'Set output encoding due to terminal type change'); Log.debug(
} { encoding: this.outputEncoding },
'Set output encoding due to terminal type change'
);
},
}); });
Object.defineProperty(this, 'termWidth', { Object.defineProperty(this, 'termWidth', {
get : function() { get: function () {
return termWidth; return termWidth;
}, },
set : function(width) { set: function (width) {
if(width > 0) { if (width > 0) {
termWidth = width; termWidth = width;
} }
} },
}); });
Object.defineProperty(this, 'termHeight', { Object.defineProperty(this, 'termHeight', {
get : function() { get: function () {
return termHeight; return termHeight;
}, },
set : function(height) { set: function (height) {
if(height > 0) { if (height > 0) {
termHeight = height; termHeight = height;
} }
} },
}); });
Object.defineProperty(this, 'termClient', { Object.defineProperty(this, 'termClient', {
get : function() { get: function () {
return termClient; return termClient;
}, },
set : function(tc) { set: function (tc) {
termClient = tc; termClient = tc;
Log.debug( { termClient : this.termClient }, 'Set known terminal client'); Log.debug({ termClient: this.termClient }, 'Set known terminal client');
} },
}); });
} }
ClientTerminal.prototype.disconnect = function() { ClientTerminal.prototype.disconnect = function () {
this.output = null; this.output = null;
}; };
ClientTerminal.prototype.isNixTerm = function() { ClientTerminal.prototype.isNixTerm = function () {
// //
// Standard *nix type terminals // Standard *nix type terminals
// //
if(this.termType.startsWith('xterm')) { if (this.termType.startsWith('xterm')) {
return true; return true;
} }
@ -122,7 +124,7 @@ ClientTerminal.prototype.isNixTerm = function() {
return utf8TermList.includes(this.termType); return utf8TermList.includes(this.termType);
}; };
ClientTerminal.prototype.isANSI = function() { ClientTerminal.prototype.isANSI = function () {
// //
// ANSI terminals should be encoded to CP437 // ANSI terminals should be encoded to CP437
// //
@ -163,35 +165,33 @@ ClientTerminal.prototype.isANSI = function() {
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) { ClientTerminal.prototype.write = function (s, convertLineFeeds, cb) {
this.rawWrite(this.encode(s, convertLineFeeds), cb); this.rawWrite(this.encode(s, convertLineFeeds), cb);
}; };
ClientTerminal.prototype.rawWrite = function(s, cb) { ClientTerminal.prototype.rawWrite = function (s, cb) {
if(this.output && this.output.writable) { if (this.output && this.output.writable) {
this.output.write(s, err => { this.output.write(s, err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
if(err) { if (err) {
Log.warn( { error : err.message }, 'Failed writing to socket'); Log.warn({ error: err.message }, 'Failed writing to socket');
} }
}); });
} }
}; };
ClientTerminal.prototype.pipeWrite = function(s, cb) { ClientTerminal.prototype.pipeWrite = function (s, cb) {
this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds| this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds|
}; };
ClientTerminal.prototype.encode = function(s, convertLineFeeds) { ClientTerminal.prototype.encode = function (s, convertLineFeeds) {
convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF; convertLineFeeds = _.isBoolean(convertLineFeeds) ? convertLineFeeds : this.convertLF;
if(convertLineFeeds && _.isString(s)) { if (convertLineFeeds && _.isString(s)) {
s = s.replace(/\n/g, '\r\n'); s = s.replace(/\n/g, '\r\n');
} }
return iconv.encode(s, this.outputEncoding); return iconv.encode(s, this.outputEncoding);
}; };

View File

@ -1,16 +1,16 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const ANSI = require('./ansi_term.js'); const ANSI = require('./ansi_term.js');
const { getPredefinedMCIValue } = require('./predefined_mci.js'); const { getPredefinedMCIValue } = require('./predefined_mci.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
exports.stripMciColorCodes = stripMciColorCodes; exports.stripMciColorCodes = stripMciColorCodes;
exports.pipeStringLength = pipeStringLength; exports.pipeStringLength = pipeStringLength;
exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi; exports.pipeToAnsi = exports.renegadeToAnsi = renegadeToAnsi;
exports.controlCodesToAnsi = controlCodesToAnsi; exports.controlCodesToAnsi = controlCodesToAnsi;
// :TODO: Not really happy with the module name of "color_codes". Would like something better ... control_code_string? // :TODO: Not really happy with the module name of "color_codes". Would like something better ... control_code_string?
@ -23,97 +23,101 @@ function pipeStringLength(s) {
} }
function ansiSgrFromRenegadeColorCode(cc) { function ansiSgrFromRenegadeColorCode(cc) {
return ANSI.sgr({ return ANSI.sgr(
0 : [ 'reset', 'black' ], {
1 : [ 'reset', 'blue' ], 0: ['reset', 'black'],
2 : [ 'reset', 'green' ], 1: ['reset', 'blue'],
3 : [ 'reset', 'cyan' ], 2: ['reset', 'green'],
4 : [ 'reset', 'red' ], 3: ['reset', 'cyan'],
5 : [ 'reset', 'magenta' ], 4: ['reset', 'red'],
6 : [ 'reset', 'yellow' ], 5: ['reset', 'magenta'],
7 : [ 'reset', 'white' ], 6: ['reset', 'yellow'],
7: ['reset', 'white'],
8 : [ 'bold', 'black' ], 8: ['bold', 'black'],
9 : [ 'bold', 'blue' ], 9: ['bold', 'blue'],
10 : [ 'bold', 'green' ], 10: ['bold', 'green'],
11 : [ 'bold', 'cyan' ], 11: ['bold', 'cyan'],
12 : [ 'bold', 'red' ], 12: ['bold', 'red'],
13 : [ 'bold', 'magenta' ], 13: ['bold', 'magenta'],
14 : [ 'bold', 'yellow' ], 14: ['bold', 'yellow'],
15 : [ 'bold', 'white' ], 15: ['bold', 'white'],
16 : [ 'blackBG' ], 16: ['blackBG'],
17 : [ 'blueBG' ], 17: ['blueBG'],
18 : [ 'greenBG' ], 18: ['greenBG'],
19 : [ 'cyanBG' ], 19: ['cyanBG'],
20 : [ 'redBG' ], 20: ['redBG'],
21 : [ 'magentaBG' ], 21: ['magentaBG'],
22 : [ 'yellowBG' ], 22: ['yellowBG'],
23 : [ 'whiteBG' ], 23: ['whiteBG'],
24 : [ 'blink', 'blackBG' ], 24: ['blink', 'blackBG'],
25 : [ 'blink', 'blueBG' ], 25: ['blink', 'blueBG'],
26 : [ 'blink', 'greenBG' ], 26: ['blink', 'greenBG'],
27 : [ 'blink', 'cyanBG' ], 27: ['blink', 'cyanBG'],
28 : [ 'blink', 'redBG' ], 28: ['blink', 'redBG'],
29 : [ 'blink', 'magentaBG' ], 29: ['blink', 'magentaBG'],
30 : [ 'blink', 'yellowBG' ], 30: ['blink', 'yellowBG'],
31 : [ 'blink', 'whiteBG' ], 31: ['blink', 'whiteBG'],
}[cc] || 'normal'); }[cc] || 'normal'
);
} }
function ansiSgrFromCnetStyleColorCode(cc) { function ansiSgrFromCnetStyleColorCode(cc) {
return ANSI.sgr({ return ANSI.sgr(
c0 : [ 'reset', 'black' ], {
c1 : [ 'reset', 'red' ], c0: ['reset', 'black'],
c2 : [ 'reset', 'green' ], c1: ['reset', 'red'],
c3 : [ 'reset', 'yellow' ], c2: ['reset', 'green'],
c4 : [ 'reset', 'blue' ], c3: ['reset', 'yellow'],
c5 : [ 'reset', 'magenta' ], c4: ['reset', 'blue'],
c6 : [ 'reset', 'cyan' ], c5: ['reset', 'magenta'],
c7 : [ 'reset', 'white' ], c6: ['reset', 'cyan'],
c7: ['reset', 'white'],
c8 : [ 'bold', 'black' ], c8: ['bold', 'black'],
c9 : [ 'bold', 'red' ], c9: ['bold', 'red'],
ca : [ 'bold', 'green' ], ca: ['bold', 'green'],
cb : [ 'bold', 'yellow' ], cb: ['bold', 'yellow'],
cc : [ 'bold', 'blue' ], cc: ['bold', 'blue'],
cd : [ 'bold', 'magenta' ], cd: ['bold', 'magenta'],
ce : [ 'bold', 'cyan' ], ce: ['bold', 'cyan'],
cf : [ 'bold', 'white' ], cf: ['bold', 'white'],
z0 : [ 'blackBG' ], z0: ['blackBG'],
z1 : [ 'redBG' ], z1: ['redBG'],
z2 : [ 'greenBG' ], z2: ['greenBG'],
z3 : [ 'yellowBG' ], z3: ['yellowBG'],
z4 : [ 'blueBG' ], z4: ['blueBG'],
z5 : [ 'magentaBG' ], z5: ['magentaBG'],
z6 : [ 'cyanBG' ], z6: ['cyanBG'],
z7 : [ 'whiteBG' ], z7: ['whiteBG'],
}[cc] || 'normal'); }[cc] || 'normal'
);
} }
function renegadeToAnsi(s, client) { function renegadeToAnsi(s, client) {
if(-1 == s.indexOf('|')) { if (-1 == s.indexOf('|')) {
return s; // no pipe codes present return s; // no pipe codes present
} }
let result = ''; let result = '';
const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g; const re = /\|(?:(C[FBUD])([0-9]{1,2})|([0-9]{2})|([A-Z]{2})|(\|))/g;
let m; let m;
let lastIndex = 0; let lastIndex = 0;
while((m = re.exec(s))) { while ((m = re.exec(s))) {
if(m[3]) { if (m[3]) {
// |## color // |## color
const val = parseInt(m[3], 10); const val = parseInt(m[3], 10);
const attr = ansiSgrFromRenegadeColorCode(val); const attr = ansiSgrFromRenegadeColorCode(val);
result += s.substr(lastIndex, m.index - lastIndex) + attr; result += s.substr(lastIndex, m.index - lastIndex) + attr;
} else if(m[4] || m[1]) { } else if (m[4] || m[1]) {
// |AA MCI code or |Cx## movement where ## is in m[1] // |AA MCI code or |Cx## movement where ## is in m[1]
let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]); let val = getPredefinedMCIValue(client, m[4] || m[1], m[2]);
val = _.isString(val) ? val : m[0]; // value itself or literal val = _.isString(val) ? val : m[0]; // value itself or literal
result += s.substr(lastIndex, m.index - lastIndex) + val; result += s.substr(lastIndex, m.index - lastIndex) + val;
} else if(m[5]) { } else if (m[5]) {
// || -- literal '|', that is. // || -- literal '|', that is.
result += '|'; result += '|';
} }
@ -121,7 +125,7 @@ function renegadeToAnsi(s, client) {
lastIndex = re.lastIndex; lastIndex = re.lastIndex;
} }
return (0 === result.length ? s : result + s.substr(lastIndex)); return 0 === result.length ? s : result + s.substr(lastIndex);
} }
// //
@ -144,26 +148,27 @@ function renegadeToAnsi(s, client) {
// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt // * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt
// //
function controlCodesToAnsi(s, client) { function controlCodesToAnsi(s, client) {
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex const RE =
/(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex
let m; let m;
let result = ''; let result = '';
let lastIndex = 0; let lastIndex = 0;
let v; let v;
let fg; let fg;
let bg; let bg;
while((m = RE.exec(s))) { while ((m = RE.exec(s))) {
switch(m[0].charAt(0)) { switch (m[0].charAt(0)) {
case '|' : case '|':
// Renegade |## // Renegade |##
v = parseInt(m[2], 10); v = parseInt(m[2], 10);
if(isNaN(v)) { if (isNaN(v)) {
v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal v = getPredefinedMCIValue(client, m[2]) || m[0]; // value itself or literal
} }
if(_.isString(v)) { if (_.isString(v)) {
result += s.substr(lastIndex, m.index - lastIndex) + v; result += s.substr(lastIndex, m.index - lastIndex) + v;
} else { } else {
v = ansiSgrFromRenegadeColorCode(v); v = ansiSgrFromRenegadeColorCode(v);
@ -171,9 +176,9 @@ function controlCodesToAnsi(s, client) {
} }
break; break;
case '@' : case '@':
// PCBoard @X## or Wildcat! @##@ // PCBoard @X## or Wildcat! @##@
if('@' === m[0].substr(-1)) { if ('@' === m[0].substr(-1)) {
// Wildcat! // Wildcat!
v = m[6]; v = m[6];
} else { } else {
@ -181,81 +186,83 @@ function controlCodesToAnsi(s, client) {
} }
bg = { bg = {
0 : [ 'blackBG' ], 0: ['blackBG'],
1 : [ 'blueBG' ], 1: ['blueBG'],
2 : [ 'greenBG' ], 2: ['greenBG'],
3 : [ 'cyanBG' ], 3: ['cyanBG'],
4 : [ 'redBG' ], 4: ['redBG'],
5 : [ 'magentaBG' ], 5: ['magentaBG'],
6 : [ 'yellowBG' ], 6: ['yellowBG'],
7 : [ 'whiteBG' ], 7: ['whiteBG'],
8 : [ 'bold', 'blackBG' ], 8: ['bold', 'blackBG'],
9 : [ 'bold', 'blueBG' ], 9: ['bold', 'blueBG'],
A : [ 'bold', 'greenBG' ], A: ['bold', 'greenBG'],
B : [ 'bold', 'cyanBG' ], B: ['bold', 'cyanBG'],
C : [ 'bold', 'redBG' ], C: ['bold', 'redBG'],
D : [ 'bold', 'magentaBG' ], D: ['bold', 'magentaBG'],
E : [ 'bold', 'yellowBG' ], E: ['bold', 'yellowBG'],
F : [ 'bold', 'whiteBG' ], F: ['bold', 'whiteBG'],
}[v.charAt(0)] || [ 'normal' ]; }[v.charAt(0)] || ['normal'];
fg = { fg = {
0 : [ 'reset', 'black' ], 0: ['reset', 'black'],
1 : [ 'reset', 'blue' ], 1: ['reset', 'blue'],
2 : [ 'reset', 'green' ], 2: ['reset', 'green'],
3 : [ 'reset', 'cyan' ], 3: ['reset', 'cyan'],
4 : [ 'reset', 'red' ], 4: ['reset', 'red'],
5 : [ 'reset', 'magenta' ], 5: ['reset', 'magenta'],
6 : [ 'reset', 'yellow' ], 6: ['reset', 'yellow'],
7 : [ 'reset', 'white' ], 7: ['reset', 'white'],
8 : [ 'blink', 'black' ], 8: ['blink', 'black'],
9 : [ 'blink', 'blue' ], 9: ['blink', 'blue'],
A : [ 'blink', 'green' ], A: ['blink', 'green'],
B : [ 'blink', 'cyan' ], B: ['blink', 'cyan'],
C : [ 'blink', 'red' ], C: ['blink', 'red'],
D : [ 'blink', 'magenta' ], D: ['blink', 'magenta'],
E : [ 'blink', 'yellow' ], E: ['blink', 'yellow'],
F : [ 'blink', 'white' ], F: ['blink', 'white'],
}[v.charAt(1)] || ['normal']; }[v.charAt(1)] || ['normal'];
v = ANSI.sgr(fg.concat(bg)); v = ANSI.sgr(fg.concat(bg));
result += s.substr(lastIndex, m.index - lastIndex) + v; result += s.substr(lastIndex, m.index - lastIndex) + v;
break; break;
case '\x03' : case '\x03':
// WWIV // WWIV
v = parseInt(m[8], 10); v = parseInt(m[8], 10);
if(isNaN(v)) { if (isNaN(v)) {
v += m[0]; v += m[0];
} else { } else {
v = ANSI.sgr({ v = ANSI.sgr(
0 : [ 'reset', 'black' ], {
1 : [ 'bold', 'cyan' ], 0: ['reset', 'black'],
2 : [ 'bold', 'yellow' ], 1: ['bold', 'cyan'],
3 : [ 'reset', 'magenta' ], 2: ['bold', 'yellow'],
4 : [ 'bold', 'white', 'blueBG' ], 3: ['reset', 'magenta'],
5 : [ 'reset', 'green' ], 4: ['bold', 'white', 'blueBG'],
6 : [ 'bold', 'blink', 'red' ], 5: ['reset', 'green'],
7 : [ 'bold', 'blue' ], 6: ['bold', 'blink', 'red'],
8 : [ 'reset', 'blue' ], 7: ['bold', 'blue'],
9 : [ 'reset', 'cyan' ], 8: ['reset', 'blue'],
}[v] || 'normal'); 9: ['reset', 'cyan'],
}[v] || 'normal'
);
} }
result += s.substr(lastIndex, m.index - lastIndex) + v; result += s.substr(lastIndex, m.index - lastIndex) + v;
break; break;
case '\x19' : case '\x19':
case '\0x11' : case '\0x11':
// CNET "Y-Style" & "Q-Style" // CNET "Y-Style" & "Q-Style"
v = m[9] || m[11]; v = m[9] || m[11];
if(v) { if (v) {
if('n1' === v) { if ('n1' === v) {
v = '\n'; v = '\n';
} else if('f1' === v) { } else if ('f1' === v) {
v = ANSI.clearScreen(); v = ANSI.clearScreen();
} else { } else {
v = ansiSgrFromCnetStyleColorCode(v); v = ansiSgrFromCnetStyleColorCode(v);
@ -270,5 +277,5 @@ function controlCodesToAnsi(s, client) {
lastIndex = RE.lastIndex; lastIndex = RE.lastIndex;
} }
return (0 === result.length ? s : result + s.substr(lastIndex)); return 0 === result.length ? s : result + s.substr(lastIndex);
} }

View File

@ -2,22 +2,19 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const { MenuModule } = require('../core/menu_module.js'); const { MenuModule } = require('../core/menu_module.js');
const { resetScreen } = require('../core/ansi_term.js'); const { resetScreen } = require('../core/ansi_term.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const { const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
trackDoorRunBegin,
trackDoorRunEnd
} = require('./door_util.js');
// deps // deps
const async = require('async'); const async = require('async');
const RLogin = require('rlogin'); const RLogin = require('rlogin');
exports.moduleInfo = { exports.moduleInfo = {
name : 'CombatNet', name: 'CombatNet',
desc : 'CombatNet Access Module', desc: 'CombatNet Access Module',
author : 'Dave Stephens', author: 'Dave Stephens',
}; };
exports.getModule = class CombatNetModule extends MenuModule { exports.getModule = class CombatNetModule extends MenuModule {
@ -25,9 +22,9 @@ exports.getModule = class CombatNetModule extends MenuModule {
super(options); super(options);
// establish defaults // establish defaults
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'bbs.combatnet.us'; this.config.host = this.config.host || 'bbs.combatnet.us';
this.config.rloginPort = this.config.rloginPort || 4513; this.config.rloginPort = this.config.rloginPort || 4513;
} }
initSequence() { initSequence() {
@ -38,10 +35,10 @@ exports.getModule = class CombatNetModule extends MenuModule {
function validateConfig(callback) { function validateConfig(callback) {
return self.validateConfigFields( return self.validateConfigFields(
{ {
host : 'string', host: 'string',
password : 'string', password: 'string',
bbsTag : 'string', bbsTag: 'string',
rloginPort : 'number', rloginPort: 'number',
}, },
callback callback
); );
@ -52,30 +49,33 @@ exports.getModule = class CombatNetModule extends MenuModule {
let doorTracking; let doorTracking;
const restorePipeToNormal = function() { const restorePipeToNormal = function () {
if(self.client.term.output) { if (self.client.term.output) {
self.client.term.output.removeListener('data', sendToRloginBuffer); self.client.term.output.removeListener(
'data',
sendToRloginBuffer
);
if(doorTracking) { if (doorTracking) {
trackDoorRunEnd(doorTracking); trackDoorRunEnd(doorTracking);
} }
} }
}; };
const rlogin = new RLogin( const rlogin = new RLogin({
{ clientUsername: self.config.password,
clientUsername : self.config.password, serverUsername: `${self.config.bbsTag}${self.client.user.username}`,
serverUsername : `${self.config.bbsTag}${self.client.user.username}`, host: self.config.host,
host : self.config.host, port: self.config.rloginPort,
port : self.config.rloginPort, terminalType: self.client.term.termClient,
terminalType : self.client.term.termClient, terminalSpeed: 57600,
terminalSpeed : 57600 });
}
);
// If there was an error ... // If there was an error ...
rlogin.on('error', err => { rlogin.on('error', err => {
self.client.log.info(`CombatNet rlogin client error: ${err.message}`); self.client.log.info(
`CombatNet rlogin client error: ${err.message}`
);
restorePipeToNormal(); restorePipeToNormal();
return callback(err); return callback(err);
}); });
@ -91,24 +91,29 @@ exports.getModule = class CombatNetModule extends MenuModule {
rlogin.send(buffer); rlogin.send(buffer);
} }
rlogin.on('connect', rlogin.on(
'connect',
/* The 'connect' event handler will be supplied with one argument, /* The 'connect' event handler will be supplied with one argument,
a boolean indicating whether or not the connection was established. */ a boolean indicating whether or not the connection was established. */
function(state) { function (state) {
if(state) { if (state) {
self.client.log.info('Connected to CombatNet'); self.client.log.info('Connected to CombatNet');
self.client.term.output.on('data', sendToRloginBuffer); self.client.term.output.on('data', sendToRloginBuffer);
doorTracking = trackDoorRunBegin(self.client); doorTracking = trackDoorRunBegin(self.client);
} else { } else {
return callback(Errors.General('Failed to establish establish CombatNet connection')); return callback(
Errors.General(
'Failed to establish establish CombatNet connection'
)
);
} }
} }
); );
// If data (a Buffer) has been received from the server ... // If data (a Buffer) has been received from the server ...
rlogin.on('data', (data) => { rlogin.on('data', data => {
self.client.term.rawWrite(data); self.client.term.rawWrite(data);
}); });
@ -116,11 +121,11 @@ exports.getModule = class CombatNetModule extends MenuModule {
rlogin.connect(); rlogin.connect();
// note: no explicit callback() until we're finished! // note: no explicit callback() until we're finished!
} },
], ],
err => { err => {
if(err) { if (err) {
self.client.log.warn( { error : err.message }, 'CombatNet error'); self.client.log.warn({ error: err.message }, 'CombatNet error');
} }
// if the client is still here, go to previous // if the client is still here, go to previous

View File

@ -2,9 +2,9 @@
'use strict'; 'use strict';
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
exports.sortAreasOrConfs = sortAreasOrConfs; exports.sortAreasOrConfs = sortAreasOrConfs;
// //
// Method for sorting message, file, etc. areas and confs // Method for sorting message, file, etc. areas and confs
@ -19,12 +19,12 @@ function sortAreasOrConfs(areasOrConfs, type) {
entryA = type ? a[type] : a; entryA = type ? a[type] : a;
entryB = type ? b[type] : b; entryB = type ? b[type] : b;
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) { if (_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
return entryA.sort - entryB.sort; return entryA.sort - entryB.sort;
} else { } else {
const keyA = entryA.sort ? entryA.sort.toString() : entryA.name; const keyA = entryA.sort ? entryA.sort.toString() : entryA.name;
const keyB = entryB.sort ? entryB.sort.toString() : entryB.name; const keyB = entryB.sort ? entryB.sort.toString() : entryB.name;
return keyA.localeCompare(keyB, { sensitivity : false, numeric : true } ); // "natural" compare return keyA.localeCompare(keyB, { sensitivity: false, numeric: true }); // "natural" compare
} }
}); });
} }

View File

@ -25,13 +25,11 @@ exports.Config = class Config extends ConfigLoader {
'loginServers.ssh.algorithms.compress', 'loginServers.ssh.algorithms.compress',
]; ];
const replaceKeys = [ const replaceKeys = ['args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch'];
'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch',
];
const configOptions = Object.assign({}, options, { const configOptions = Object.assign({}, options, {
defaultConfig : DefaultConfig, defaultConfig: DefaultConfig,
defaultsCustomizer : (defaultVal, configVal, key, path) => { defaultsCustomizer: (defaultVal, configVal, key, path) => {
if (Array.isArray(defaultVal) && Array.isArray(configVal)) { if (Array.isArray(defaultVal) && Array.isArray(configVal)) {
if (replacePaths.includes(path) || replaceKeys.includes(key)) { if (replacePaths.includes(path) || replaceKeys.includes(key)) {
// full replacement using user config value // full replacement using user config value
@ -42,7 +40,7 @@ exports.Config = class Config extends ConfigLoader {
} }
} }
}, },
onReload : err => { onReload: err => {
if (!err) { if (!err) {
const Events = require('./events.js'); const Events = require('./events.js');
Events.emit(Events.getSystemEvents().ConfigChanged); Events.emit(Events.getSystemEvents().ConfigChanged);

View File

@ -2,43 +2,49 @@
'use strict'; 'use strict';
// deps // deps
const paths = require('path'); const paths = require('path');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const hjson = require('hjson'); const hjson = require('hjson');
const sane = require('sane'); const sane = require('sane');
const _ = require('lodash'); const _ = require('lodash');
module.exports = new class ConfigCache module.exports = new (class ConfigCache {
{
constructor() { constructor() {
this.cache = new Map(); // path->parsed config this.cache = new Map(); // path->parsed config
} }
getConfigWithOptions(options, cb) { getConfigWithOptions(options, cb) {
options.hotReload = _.get(options, 'hotReload', true); options.hotReload = _.get(options, 'hotReload', true);
const cached = this.cache.has(options.filePath); const cached = this.cache.has(options.filePath);
if(options.forceReCache || !cached) { if (options.forceReCache || !cached) {
this.recacheConfigFromFile(options.filePath, (err, config) => { this.recacheConfigFromFile(options.filePath, (err, config) => {
if(!err && !cached) { if (!err && !cached) {
if(options.hotReload) { if (options.hotReload) {
const watcher = sane( const watcher = sane(paths.dirname(options.filePath), {
paths.dirname(options.filePath), glob: `**/${paths.basename(options.filePath)}`,
{ });
glob : `**/${paths.basename(options.filePath)}`
}
);
watcher.on('change', (fileName, fileRoot) => { watcher.on('change', (fileName, fileRoot) => {
require('./logger.js').log.info( { fileName, fileRoot }, 'Configuration file changed; re-caching'); require('./logger.js').log.info(
{ fileName, fileRoot },
'Configuration file changed; re-caching'
);
this.recacheConfigFromFile(paths.join(fileRoot, fileName), err => { this.recacheConfigFromFile(
if(!err) { paths.join(fileRoot, fileName),
if(options.callback) { err => {
options.callback( { fileName, fileRoot, configCache : this } ); if (!err) {
if (options.callback) {
options.callback({
fileName,
fileRoot,
configCache: this,
});
}
} }
} }
}); );
}); });
} }
} }
@ -50,12 +56,12 @@ module.exports = new class ConfigCache
} }
getConfig(filePath, cb) { getConfig(filePath, cb) {
return this.getConfigWithOptions( { filePath }, cb); return this.getConfigWithOptions({ filePath }, cb);
} }
recacheConfigFromFile(path, cb) { recacheConfigFromFile(path, cb) {
fs.readFile(path, { encoding : 'utf-8' }, (err, data) => { fs.readFile(path, { encoding: 'utf-8' }, (err, data) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -63,10 +69,13 @@ module.exports = new class ConfigCache
try { try {
parsed = hjson.parse(data); parsed = hjson.parse(data);
this.cache.set(path, parsed); this.cache.set(path, parsed);
} catch(e) { } catch (e) {
try { try {
require('./logger.js').log.error( { filePath : path, error : e.message }, 'Failed to re-cache' ); require('./logger.js').log.error(
} catch(ignored) { { filePath: path, error: e.message },
'Failed to re-cache'
);
} catch (ignored) {
// nothing - we may be failing to parse the config in which we can't log here! // nothing - we may be failing to parse the config in which we can't log here!
} }
return cb(e); return cb(e);
@ -75,4 +84,4 @@ module.exports = new class ConfigCache
return cb(null, parsed); return cb(null, parsed);
}); });
} }
}; })();

File diff suppressed because it is too large Load Diff

View File

@ -14,23 +14,21 @@ module.exports = class ConfigLoader {
defaultsCustomizer = null, defaultsCustomizer = null,
onReload = null, onReload = null,
keepWsc = false, keepWsc = false,
} = } = {
{ hotReload: true,
hotReload : true, defaultConfig: {},
defaultConfig : {}, defaultsCustomizer: null,
defaultsCustomizer : null, onReload: null,
onReload : null, keepWsc: false,
keepWsc : false,
} }
) ) {
{
this.current = {}; this.current = {};
this.hotReload = hotReload; this.hotReload = hotReload;
this.defaultConfig = defaultConfig; this.defaultConfig = defaultConfig;
this.defaultsCustomizer = defaultsCustomizer; this.defaultsCustomizer = defaultsCustomizer;
this.onReload = onReload; this.onReload = onReload;
this.keepWsc = keepWsc; this.keepWsc = keepWsc;
} }
init(baseConfigPath, cb) { init(baseConfigPath, cb) {
@ -61,7 +59,7 @@ module.exports = class ConfigLoader {
// //
async.waterfall( async.waterfall(
[ [
(callback) => { callback => {
return this._loadConfigFile(baseConfigPath, callback); return this._loadConfigFile(baseConfigPath, callback);
}, },
(config, callback) => { (config, callback) => {
@ -72,16 +70,17 @@ module.exports = class ConfigLoader {
config, config,
(defaultVal, configVal, key, target, source) => { (defaultVal, configVal, key, target, source) => {
var path; var path;
while (true) { // eslint-disable-line no-constant-condition while (true) {
// eslint-disable-line no-constant-condition
if (!stack.length) { if (!stack.length) {
stack.push({source, path : []}); stack.push({ source, path: [] });
} }
const prev = stack[stack.length - 1]; const prev = stack[stack.length - 1];
if (source === prev.source) { if (source === prev.source) {
path = prev.path.concat(key); path = prev.path.concat(key);
stack.push({source : configVal, path}); stack.push({ source: configVal, path });
break; break;
} }
@ -89,7 +88,12 @@ module.exports = class ConfigLoader {
} }
path = path.join('.'); path = path.join('.');
return this.defaultsCustomizer(defaultVal, configVal, key, path); return this.defaultsCustomizer(
defaultVal,
configVal,
key,
path
);
} }
); );
@ -118,12 +122,12 @@ module.exports = class ConfigLoader {
_convertTo(value, type) { _convertTo(value, type) {
switch (type) { switch (type) {
case 'bool' : case 'bool':
case 'boolean' : case 'boolean':
value = ('1' === value || 'true' === value.toLowerCase()); value = '1' === value || 'true' === value.toLowerCase();
break; break;
case 'number' : case 'number':
{ {
const num = parseInt(value); const num = parseInt(value);
if (!isNaN(num)) { if (!isNaN(num)) {
@ -132,15 +136,15 @@ module.exports = class ConfigLoader {
} }
break; break;
case 'object' : case 'object':
try { try {
value = JSON.parse(value); value = JSON.parse(value);
} catch(e) { } catch (e) {
// ignored // ignored
} }
break; break;
case 'timestamp' : case 'timestamp':
{ {
const m = moment(value); const m = moment(value);
if (m.isValid()) { if (m.isValid()) {
@ -162,7 +166,9 @@ module.exports = class ConfigLoader {
let value = process.env[varName]; let value = process.env[varName];
if (!value) { if (!value) {
// console is about as good as we can do here // console is about as good as we can do here
return console.info(`WARNING: environment variable "${varName}" from spec "${spec}" not found!`); return console.info(
`WARNING: environment variable "${varName}" from spec "${spec}" not found!`
);
} }
if ('array' === array) { if ('array' === array) {
@ -179,9 +185,9 @@ module.exports = class ConfigLoader {
const options = { const options = {
filePath, filePath,
hotReload : this.hotReload, hotReload: this.hotReload,
keepWsc : this.keepWsc, keepWsc: this.keepWsc,
callback : this._configFileChanged.bind(this), callback: this._configFileChanged.bind(this),
}; };
ConfigCache.getConfigWithOptions(options, (err, config) => { ConfigCache.getConfigWithOptions(options, (err, config) => {
@ -192,7 +198,7 @@ module.exports = class ConfigLoader {
}); });
} }
_configFileChanged({fileName, fileRoot}) { _configFileChanged({ fileName, fileRoot }) {
const reCachedPath = paths.join(fileRoot, fileName); const reCachedPath = paths.join(fileRoot, fileName);
if (this.configPaths.includes(reCachedPath)) { if (this.configPaths.includes(reCachedPath)) {
this._reload(this.baseConfigPath, err => { this._reload(this.baseConfigPath, err => {
@ -205,44 +211,44 @@ module.exports = class ConfigLoader {
_resolveIncludes(configRoot, config, cb) { _resolveIncludes(configRoot, config, cb) {
if (!Array.isArray(config.includes)) { if (!Array.isArray(config.includes)) {
this.configPaths = [ this.baseConfigPath ]; this.configPaths = [this.baseConfigPath];
return cb(null, config); return cb(null, config);
} }
// If a included file is changed, we need to re-cache, so this // If a included file is changed, we need to re-cache, so this
// must be tracked... // must be tracked...
const includePaths = config.includes.map(inc => paths.join(configRoot, inc)); const includePaths = config.includes.map(inc => paths.join(configRoot, inc));
async.eachSeries(includePaths, (includePath, nextIncludePath) => { async.eachSeries(
this._loadConfigFile(includePath, (err, includedConfig) => { includePaths,
if (err) { (includePath, nextIncludePath) => {
return nextIncludePath(err); this._loadConfigFile(includePath, (err, includedConfig) => {
} if (err) {
return nextIncludePath(err);
_.defaultsDeep(config, includedConfig);
return nextIncludePath(null);
});
},
err => {
this.configPaths = [ this.baseConfigPath, ...includePaths ];
return cb(err, config);
});
}
_resolveAtSpecs(config) {
return mapValuesDeep(
config,
value => {
if (_.isString(value) && '@' === value.charAt(0)) {
if (value.startsWith('@reference:')) {
const refPath = value.slice(11);
value = _.get(config, refPath, value);
} else if (value.startsWith('@environment:')) {
value = this._resolveEnvironmentVariable(value) || value;
} }
}
return value; _.defaultsDeep(config, includedConfig);
return nextIncludePath(null);
});
},
err => {
this.configPaths = [this.baseConfigPath, ...includePaths];
return cb(err, config);
} }
); );
} }
_resolveAtSpecs(config) {
return mapValuesDeep(config, value => {
if (_.isString(value) && '@' === value.charAt(0)) {
if (value.startsWith('@reference:')) {
const refPath = value.slice(11);
value = _.get(config, refPath, value);
} else if (value.startsWith('@environment:')) {
value = this._resolveEnvironmentVariable(value) || value;
}
}
return value;
});
}
}; };

View File

@ -2,26 +2,26 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Events = require('./events.js'); const Events = require('./events.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
// deps // deps
const async = require('async'); const async = require('async');
exports.connectEntry = connectEntry; exports.connectEntry = connectEntry;
const withCursorPositionReport = (client, cprHandler, failMessage, cb) => { const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
let giveUpTimer; let giveUpTimer;
const done = function(err) { const done = function (err) {
client.removeListener('cursor position report', cprListener); client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer); clearTimeout(giveUpTimer);
return cb(err); return cb(err);
}; };
const cprListener = (pos) => { const cprListener = pos => {
cprHandler(pos); cprHandler(pos);
return done(null); return done(null);
}; };
@ -29,10 +29,10 @@ const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
client.once('cursor position report', cprListener); client.once('cursor position report', cprListener);
// give up after 2s // give up after 2s
giveUpTimer = setTimeout( () => { giveUpTimer = setTimeout(() => {
return done(Errors.General(failMessage)); return done(Errors.General(failMessage));
}, 2000); }, 2000);
} };
function ansiDiscoverHomePosition(client, cb) { function ansiDiscoverHomePosition(client, cb) {
// //
@ -41,7 +41,7 @@ function ansiDiscoverHomePosition(client, cb) {
// think of home as 0,0. If this is the case, we need to offset // think of home as 0,0. If this is the case, we need to offset
// our positioning to accommodate for such. // our positioning to accommodate for such.
// //
if( !Config().term.checkAnsiHomePosition ) { if (!Config().term.checkAnsiHomePosition) {
// Skip (and assume 1,1) if the home position check is disabled. // Skip (and assume 1,1) if the home position check is disabled.
return cb(null); return cb(null);
} }
@ -54,11 +54,14 @@ function ansiDiscoverHomePosition(client, cb) {
// //
// We expect either 0,0, or 1,1. Anything else will be filed as bad data // We expect either 0,0, or 1,1. Anything else will be filed as bad data
// //
if(h > 1 || w > 1) { if (h > 1 || w > 1) {
return client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values'); return client.log.warn(
{ height: h, width: w },
'Ignoring ANSI home position CPR due to unexpected values'
);
} }
if(0 === h & 0 === w) { if ((0 === h) & (0 === w)) {
// //
// Store a CPR offset in the client. All CPR's from this point on will offset by this amount // Store a CPR offset in the client. All CPR's from this point on will offset by this amount
// //
@ -70,7 +73,7 @@ function ansiDiscoverHomePosition(client, cb) {
cb cb
); );
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
} }
function ansiAttemptDetectUTF8(client, cb) { function ansiAttemptDetectUTF8(client, cb) {
@ -87,7 +90,7 @@ function ansiAttemptDetectUTF8(client, cb) {
// "*nix" terminal -- that is, xterm, etc. // "*nix" terminal -- that is, xterm, etc.
// Also skip this check if checkUtf8Encoding is disabled in the config // Also skip this check if checkUtf8Encoding is disabled in the config
if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) { if (!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
return cb(null); return cb(null);
} }
@ -99,20 +102,24 @@ function ansiAttemptDetectUTF8(client, cb) {
pos => { pos => {
initialPosition = pos; initialPosition = pos;
withCursorPositionReport(client, withCursorPositionReport(
client,
pos => { pos => {
const [_, w] = pos; const [_, w] = pos;
const len = w - initialPosition[1]; const len = w - initialPosition[1];
if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull if (!isNaN(len) && len >= ASCIIPortion.length + 6) {
client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".'); // CP437 displays 3 chars each Unicode skull
client.log.info(
'Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".'
);
client.setTermType('ansi'); client.setTermType('ansi');
} }
}, },
'Detect UTF-8 stage 2 timed out', 'Detect UTF-8 stage 2 timed out',
cb, cb
); );
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
client.term.rawWrite(ansi.queryPos()); client.term.rawWrite(ansi.queryPos());
}, },
'Detect UTF-8 stage 1 timed out', 'Detect UTF-8 stage 1 timed out',
@ -150,7 +157,9 @@ const ansiQuerySyncTermFontSupport = (client, cb) => {
const [_, w] = pos; const [_, w] = pos;
if (w === 1) { if (w === 1) {
// cursor didn't move // cursor didn't move
client.log.info('Client supports SyncTERM fonts or properly ignores unknown ESC sequence'); client.log.info(
'Client supports SyncTERM fonts or properly ignores unknown ESC sequence'
);
client.term.syncTermFontsEnabled = true; client.term.syncTermFontsEnabled = true;
} }
}, },
@ -158,11 +167,13 @@ const ansiQuerySyncTermFontSupport = (client, cb) => {
cb cb
); );
client.term.rawWrite(`${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`); client.term.rawWrite(
} `${ansi.goto(1, 1)}${ansi.setSyncTermFont('cp437')}${ansi.queryPos()}`
);
};
function ansiQueryTermSizeIfNeeded(client, cb) { function ansiQueryTermSizeIfNeeded(client, cb) {
if(client.term.termHeight > 0 || client.term.termWidth > 0) { if (client.term.termHeight > 0 || client.term.termWidth > 0) {
return cb(null); return cb(null);
} }
@ -172,7 +183,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
// //
// If we've already found out, disregard // If we've already found out, disregard
// //
if(client.term.termHeight > 0 || client.term.termWidth > 0) { if (client.term.termHeight > 0 || client.term.termWidth > 0) {
return; return;
} }
@ -182,20 +193,21 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
// 999x999 values we asked to move to. // 999x999 values we asked to move to.
// //
const [h, w] = pos; const [h, w] = pos;
if(h < 10 || h === 999 || w < 10 || w === 999) { if (h < 10 || h === 999 || w < 10 || w === 999) {
return client.log.warn( return client.log.warn(
{ height : h, width : w }, { height: h, width: w },
'Ignoring ANSI CPR screen size query response due to non-sane values'); 'Ignoring ANSI CPR screen size query response due to non-sane values'
);
} }
client.term.termHeight = h; client.term.termHeight = h;
client.term.termWidth = w; client.term.termWidth = w;
client.log.debug( client.log.debug(
{ {
termWidth : client.term.termWidth, termWidth: client.term.termWidth,
termHeight : client.term.termHeight, termHeight: client.term.termHeight,
source : 'ANSI CPR' source: 'ANSI CPR',
}, },
'Window size updated' 'Window size updated'
); );
@ -226,8 +238,7 @@ function displayBanner(term) {
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/ |06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00` |00`);
);
} }
function connectEntry(client, nextMenu) { function connectEntry(client, nextMenu) {
@ -245,20 +256,23 @@ function connectEntry(client, nextMenu) {
}, },
function queryTermSizeByNonStandardAnsi(callback) { function queryTermSizeByNonStandardAnsi(callback) {
ansiQueryTermSizeIfNeeded(client, err => { ansiQueryTermSizeIfNeeded(client, err => {
if(err) { if (err) {
// //
// Check again; We may have got via NAWS/similar before CPR completed. // Check again; We may have got via NAWS/similar before CPR completed.
// //
if(0 === term.termHeight || 0 === term.termWidth) { if (0 === term.termHeight || 0 === term.termWidth) {
// //
// We still don't have something good for term height/width. // We still don't have something good for term height/width.
// Default to DOS size 80x25. // Default to DOS size 80x25.
// //
// :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing??? // :TODO: Netrunner is currently hitting this and it feels wrong. Why is NAWS/ENV/CPR all failing???
client.log.warn( { reason : err.message }, 'Failed to negotiate term size; Defaulting to 80x25!'); client.log.warn(
{ reason: err.message },
'Failed to negotiate term size; Defaulting to 80x25!'
);
term.termHeight = 25; term.termHeight = 25;
term.termWidth = 80; term.termWidth = 80;
} }
} }
@ -268,7 +282,7 @@ function connectEntry(client, nextMenu) {
function checkUtf8IfNeeded(callback) { function checkUtf8IfNeeded(callback) {
return ansiAttemptDetectUTF8(client, callback); return ansiAttemptDetectUTF8(client, callback);
}, },
function querySyncTERMFontSupport(callback) { function querySyncTERMFontSupport(callback) {
return ansiQuerySyncTermFontSupport(client, callback); return ansiQuerySyncTermFontSupport(client, callback);
}, },
], ],
@ -281,9 +295,9 @@ function connectEntry(client, nextMenu) {
displayBanner(term); displayBanner(term);
// fire event // fire event
Events.emit(Events.getSystemEvents().TermDetected, { client : client } ); Events.emit(Events.getSystemEvents().TermDetected, { client: client });
setTimeout( () => { setTimeout(() => {
return client.menuStack.goto(nextMenu); return client.menuStack.goto(nextMenu);
}, 500); }, 500);
} }

View File

@ -1,47 +1,265 @@
const CP437UnicodeTable = [ const CP437UnicodeTable = [
'\u0000', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\u0000',
'\u0007', '\u0008', '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', '\u0001',
'\u000E', '\u000F', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0002',
'\u0015', '\u0016', '\u0017', '\u0018', '\u0019', '\u001A', '\u001B', '\u0003',
'\u001C', '\u001D', '\u001E', '\u001F', '\u0020', '\u0021', '\u0022', '\u0004',
'\u0023', '\u0024', '\u0025', '\u0026', '\u0027', '\u0028', '\u0029', '\u0005',
'\u002A', '\u002B', '\u002C', '\u002D', '\u002E', '\u002F', '\u0030', '\u0006',
'\u0031', '\u0032', '\u0033', '\u0034', '\u0035', '\u0036', '\u0037', '\u0007',
'\u0038', '\u0039', '\u003A', '\u003B', '\u003C', '\u003D', '\u003E', '\u0008',
'\u003F', '\u0040', '\u0041', '\u0042', '\u0043', '\u0044', '\u0045', '\u0009',
'\u0046', '\u0047', '\u0048', '\u0049', '\u004A', '\u004B', '\u004C', '\u000A',
'\u004D', '\u004E', '\u004F', '\u0050', '\u0051', '\u0052', '\u0053', '\u000B',
'\u0054', '\u0055', '\u0056', '\u0057', '\u0058', '\u0059', '\u005A', '\u000C',
'\u005B', '\u005C', '\u005D', '\u005E', '\u005F', '\u0060', '\u0061', '\u000D',
'\u0062', '\u0063', '\u0064', '\u0065', '\u0066', '\u0067', '\u0068', '\u000E',
'\u0069', '\u006A', '\u006B', '\u006C', '\u006D', '\u006E', '\u006F', '\u000F',
'\u0070', '\u0071', '\u0072', '\u0073', '\u0074', '\u0075', '\u0076', '\u0010',
'\u0077', '\u0078', '\u0079', '\u007A', '\u007B', '\u007C', '\u007D', '\u0011',
'\u007E', '\u007F', '\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4', '\u0012',
'\u00E0', '\u00E5', '\u00E7', '\u00EA', '\u00EB', '\u00E8', '\u00EF', '\u0013',
'\u00EE', '\u00EC', '\u00C4', '\u00C5', '\u00C9', '\u00E6', '\u00C6', '\u0014',
'\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9', '\u00FF', '\u00D6', '\u0015',
'\u00DC', '\u00A2', '\u00A3', '\u00A5', '\u20A7', '\u0192', '\u00E1', '\u0016',
'\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1', '\u00AA', '\u00BA', '\u0017',
'\u00BF', '\u2310', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB', '\u0018',
'\u00BB', '\u2591', '\u2592', '\u2593', '\u2502', '\u2524', '\u2561', '\u0019',
'\u2562', '\u2556', '\u2555', '\u2563', '\u2551', '\u2557', '\u255D', '\u001A',
'\u255C', '\u255B', '\u2510', '\u2514', '\u2534', '\u252C', '\u251C', '\u001B',
'\u2500', '\u253C', '\u255E', '\u255F', '\u255A', '\u2554', '\u2569', '\u001C',
'\u2566', '\u2560', '\u2550', '\u256C', '\u2567', '\u2568', '\u2564', '\u001D',
'\u2565', '\u2559', '\u2558', '\u2552', '\u2553', '\u256B', '\u256A', '\u001E',
'\u2518', '\u250C', '\u2588', '\u2584', '\u258C', '\u2590', '\u2580', '\u001F',
'\u03B1', '\u00DF', '\u0393', '\u03C0', '\u03A3', '\u03C3', '\u00B5', '\u0020',
'\u03C4', '\u03A6', '\u0398', '\u03A9', '\u03B4', '\u221E', '\u03C6', '\u0021',
'\u03B5', '\u2229', '\u2261', '\u00B1', '\u2265', '\u2264', '\u2320', '\u0022',
'\u2321', '\u00F7', '\u2248', '\u00B0', '\u2219', '\u00B7', '\u221A', '\u0023',
'\u207F', '\u00B2', '\u25A0', '\u00A0' '\u0024',
'\u0025',
'\u0026',
'\u0027',
'\u0028',
'\u0029',
'\u002A',
'\u002B',
'\u002C',
'\u002D',
'\u002E',
'\u002F',
'\u0030',
'\u0031',
'\u0032',
'\u0033',
'\u0034',
'\u0035',
'\u0036',
'\u0037',
'\u0038',
'\u0039',
'\u003A',
'\u003B',
'\u003C',
'\u003D',
'\u003E',
'\u003F',
'\u0040',
'\u0041',
'\u0042',
'\u0043',
'\u0044',
'\u0045',
'\u0046',
'\u0047',
'\u0048',
'\u0049',
'\u004A',
'\u004B',
'\u004C',
'\u004D',
'\u004E',
'\u004F',
'\u0050',
'\u0051',
'\u0052',
'\u0053',
'\u0054',
'\u0055',
'\u0056',
'\u0057',
'\u0058',
'\u0059',
'\u005A',
'\u005B',
'\u005C',
'\u005D',
'\u005E',
'\u005F',
'\u0060',
'\u0061',
'\u0062',
'\u0063',
'\u0064',
'\u0065',
'\u0066',
'\u0067',
'\u0068',
'\u0069',
'\u006A',
'\u006B',
'\u006C',
'\u006D',
'\u006E',
'\u006F',
'\u0070',
'\u0071',
'\u0072',
'\u0073',
'\u0074',
'\u0075',
'\u0076',
'\u0077',
'\u0078',
'\u0079',
'\u007A',
'\u007B',
'\u007C',
'\u007D',
'\u007E',
'\u007F',
'\u00C7',
'\u00FC',
'\u00E9',
'\u00E2',
'\u00E4',
'\u00E0',
'\u00E5',
'\u00E7',
'\u00EA',
'\u00EB',
'\u00E8',
'\u00EF',
'\u00EE',
'\u00EC',
'\u00C4',
'\u00C5',
'\u00C9',
'\u00E6',
'\u00C6',
'\u00F4',
'\u00F6',
'\u00F2',
'\u00FB',
'\u00F9',
'\u00FF',
'\u00D6',
'\u00DC',
'\u00A2',
'\u00A3',
'\u00A5',
'\u20A7',
'\u0192',
'\u00E1',
'\u00ED',
'\u00F3',
'\u00FA',
'\u00F1',
'\u00D1',
'\u00AA',
'\u00BA',
'\u00BF',
'\u2310',
'\u00AC',
'\u00BD',
'\u00BC',
'\u00A1',
'\u00AB',
'\u00BB',
'\u2591',
'\u2592',
'\u2593',
'\u2502',
'\u2524',
'\u2561',
'\u2562',
'\u2556',
'\u2555',
'\u2563',
'\u2551',
'\u2557',
'\u255D',
'\u255C',
'\u255B',
'\u2510',
'\u2514',
'\u2534',
'\u252C',
'\u251C',
'\u2500',
'\u253C',
'\u255E',
'\u255F',
'\u255A',
'\u2554',
'\u2569',
'\u2566',
'\u2560',
'\u2550',
'\u256C',
'\u2567',
'\u2568',
'\u2564',
'\u2565',
'\u2559',
'\u2558',
'\u2552',
'\u2553',
'\u256B',
'\u256A',
'\u2518',
'\u250C',
'\u2588',
'\u2584',
'\u258C',
'\u2590',
'\u2580',
'\u03B1',
'\u00DF',
'\u0393',
'\u03C0',
'\u03A3',
'\u03C3',
'\u00B5',
'\u03C4',
'\u03A6',
'\u0398',
'\u03A9',
'\u03B4',
'\u221E',
'\u03C6',
'\u03B5',
'\u2229',
'\u2261',
'\u00B1',
'\u2265',
'\u2264',
'\u2320',
'\u2321',
'\u00F7',
'\u2248',
'\u00B0',
'\u2219',
'\u00B7',
'\u221A',
'\u207F',
'\u00B2',
'\u25A0',
'\u00A0',
]; ];
const NonCP437EncodableRegExp = /[^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\u00A0]/; // eslint-disable-line no-control-regex const NonCP437EncodableRegExp =
const isCP437Encodable = (s) => { /[^\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u00C7\u00FC\u00E9\u00E2\u00E4\u00E0\u00E5\u00E7\u00EA\u00EB\u00E8\u00EF\u00EE\u00EC\u00C4\u00C5\u00C9\u00E6\u00C6\u00F4\u00F6\u00F2\u00FB\u00F9\u00FF\u00D6\u00DC\u00A2\u00A3\u00A5\u20A7\u0192\u00E1\u00ED\u00F3\u00FA\u00F1\u00D1\u00AA\u00BA\u00BF\u2310\u00AC\u00BD\u00BC\u00A1\u00AB\u00BB\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255D\u255C\u255B\u2510\u2514\u2534\u252C\u251C\u2500\u253C\u255E\u255F\u255A\u2554\u2569\u2566\u2560\u2550\u256C\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256B\u256A\u2518\u250C\u2588\u2584\u258C\u2590\u2580\u03B1\u00DF\u0393\u03C0\u03A3\u03C3\u00B5\u03C4\u03A6\u0398\u03A9\u03B4\u221E\u03C6\u03B5\u2229\u2261\u00B1\u2265\u2264\u2320\u2321\u00F7\u2248\u00B0\u2219\u00B7\u221A\u207F\u00B2\u25A0\u00A0]/; // eslint-disable-line no-control-regex
const isCP437Encodable = s => {
if (!s.length) { if (!s.length) {
return true; return true;
} }

View File

@ -38,7 +38,7 @@ const CRC32_TABLE = new Int32Array([
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
]); ]);
exports.CRC32 = class CRC32 { exports.CRC32 = class CRC32 {
@ -52,40 +52,40 @@ exports.CRC32 = class CRC32 {
} }
update_4(input) { update_4(input) {
const len = input.length - 3; const len = input.length - 3;
let i = 0; let i = 0;
for(i = 0; i < len;) { for (i = 0; i < len; ) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
} }
while(i < len + 3) { while (i < len + 3) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
} }
} }
update_8(input) { update_8(input) {
const len = input.length - 7; const len = input.length - 7;
let i = 0; let i = 0;
for(i = 0; i < len;) { for (i = 0; i < len; ) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++]) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
} }
while(i < len + 7) { while (i < len + 7) {
this.crc = (this.crc >>> 8) ^ CRC32_TABLE[ (this.crc ^ input[i++] ) & 0xff ]; this.crc = (this.crc >>> 8) ^ CRC32_TABLE[(this.crc ^ input[i++]) & 0xff];
} }
} }
finalize() { finalize() {
return (this.crc ^ (-1)) >>> 0; return (this.crc ^ -1) >>> 0;
} }
}; };

View File

@ -5,25 +5,25 @@
const conf = require('./config'); const conf = require('./config');
// deps // deps
const sqlite3 = require('sqlite3'); const sqlite3 = require('sqlite3');
const sqlite3Trans = require('sqlite3-trans'); const sqlite3Trans = require('sqlite3-trans');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const moment = require('moment'); const moment = require('moment');
// database handles // database handles
const dbs = {}; const dbs = {};
exports.getTransactionDatabase = getTransactionDatabase; exports.getTransactionDatabase = getTransactionDatabase;
exports.getModDatabasePath = getModDatabasePath; exports.getModDatabasePath = getModDatabasePath;
exports.loadDatabaseForMod = loadDatabaseForMod; exports.loadDatabaseForMod = loadDatabaseForMod;
exports.getISOTimestampString = getISOTimestampString; exports.getISOTimestampString = getISOTimestampString;
exports.sanitizeString = sanitizeString; exports.sanitizeString = sanitizeString;
exports.initializeDatabases = initializeDatabases; exports.initializeDatabases = initializeDatabases;
exports.dbs = dbs; exports.dbs = dbs;
function getTransactionDatabase(db) { function getTransactionDatabase(db) {
return sqlite3Trans.wrap(db); return sqlite3Trans.wrap(db);
@ -40,37 +40,38 @@ function getModDatabasePath(moduleInfo, suffix) {
// We expect that moduleInfo defines packageName which will be the base of the modules // We expect that moduleInfo defines packageName which will be the base of the modules
// filename. An optional suffix may be supplied as well. // filename. An optional suffix may be supplied as well.
// //
const HOST_RE = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; const HOST_RE =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
assert(_.isObject(moduleInfo)); assert(_.isObject(moduleInfo));
assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!'); assert(_.isString(moduleInfo.packageName), 'moduleInfo must define "packageName"!');
let full = moduleInfo.packageName; let full = moduleInfo.packageName;
if(suffix) { if (suffix) {
full += `.${suffix}`; full += `.${suffix}`;
} }
assert( assert(
(full.split('.').length > 1 && HOST_RE.test(full)), full.split('.').length > 1 && HOST_RE.test(full),
'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'); 'packageName must follow Reverse Domain Name Notation - https://en.wikipedia.org/wiki/Reverse_domain_name_notation'
);
const Config = conf.get(); const Config = conf.get();
return paths.join(Config.paths.modsDb, `${full}.sqlite3`); return paths.join(Config.paths.modsDb, `${full}.sqlite3`);
} }
function loadDatabaseForMod(modInfo, cb) { function loadDatabaseForMod(modInfo, cb) {
const db = getTransactionDatabase(new sqlite3.Database( const db = getTransactionDatabase(
getModDatabasePath(modInfo), new sqlite3.Database(getModDatabasePath(modInfo), err => {
err => {
return cb(err, db); return cb(err, db);
} })
)); );
} }
function getISOTimestampString(ts) { function getISOTimestampString(ts) {
ts = ts || moment(); ts = ts || moment();
if(!moment.isMoment(ts)) { if (!moment.isMoment(ts)) {
if(_.isString(ts)) { if (_.isString(ts)) {
ts = ts.replace(/\//g, '-'); ts = ts.replace(/\//g, '-');
} }
ts = moment(ts); ts = moment(ts);
@ -79,42 +80,55 @@ function getISOTimestampString(ts) {
} }
function sanitizeString(s) { function sanitizeString(s) {
return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => { // eslint-disable-line no-control-regex return s.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, c => {
// eslint-disable-line no-control-regex
switch (c) { switch (c) {
case '\0' : return '\\0'; case '\0':
case '\x08' : return '\\b'; return '\\0';
case '\x09' : return '\\t'; case '\x08':
case '\x1a' : return '\\z'; return '\\b';
case '\n' : return '\\n'; case '\x09':
case '\r' : return '\\r'; return '\\t';
case '\x1a':
return '\\z';
case '\n':
return '\\n';
case '\r':
return '\\r';
case '"' : case '"':
case '\'' : case "'":
return `${c}${c}`; return `${c}${c}`;
case '\\' : case '\\':
case '%' : case '%':
return `\\${c}`; return `\\${c}`;
} }
}); });
} }
function initializeDatabases(cb) { function initializeDatabases(cb) {
async.eachSeries( [ 'system', 'user', 'message', 'file' ], (dbName, next) => { async.eachSeries(
dbs[dbName] = sqlite3Trans.wrap(new sqlite3.Database(getDatabasePath(dbName), err => { ['system', 'user', 'message', 'file'],
if(err) { (dbName, next) => {
return cb(err); dbs[dbName] = sqlite3Trans.wrap(
} new sqlite3.Database(getDatabasePath(dbName), err => {
if (err) {
return cb(err);
}
dbs[dbName].serialize( () => { dbs[dbName].serialize(() => {
DB_INIT_TABLE[dbName]( () => { DB_INIT_TABLE[dbName](() => {
return next(null); return next(null);
}); });
}); });
})); })
}, err => { );
return cb(err); },
}); err => {
return cb(err);
}
);
} }
function enableForeignKeys(db) { function enableForeignKeys(db) {
@ -122,7 +136,7 @@ function enableForeignKeys(db) {
} }
const DB_INIT_TABLE = { const DB_INIT_TABLE = {
system : (cb) => { system: cb => {
enableForeignKeys(dbs.system); enableForeignKeys(dbs.system);
// Various stat/event logging - see stat_log.js // Various stat/event logging - see stat_log.js
@ -160,7 +174,7 @@ const DB_INIT_TABLE = {
return cb(null); return cb(null);
}, },
user : (cb) => { user: cb => {
enableForeignKeys(dbs.user); enableForeignKeys(dbs.user);
dbs.user.run( dbs.user.run(
@ -229,7 +243,7 @@ const DB_INIT_TABLE = {
return cb(null); return cb(null);
}, },
message : (cb) => { message: cb => {
enableForeignKeys(dbs.message); enableForeignKeys(dbs.message);
dbs.message.run( dbs.message.run(
@ -296,7 +310,6 @@ const DB_INIT_TABLE = {
);` );`
); );
// :TODO: need SQL to ensure cleaned up if delete from message? // :TODO: need SQL to ensure cleaned up if delete from message?
/* /*
dbs.message.run( dbs.message.run(
@ -337,7 +350,7 @@ const DB_INIT_TABLE = {
return cb(null); return cb(null);
}, },
file : (cb) => { file: cb => {
enableForeignKeys(dbs.file); enableForeignKeys(dbs.file);
dbs.file.run( dbs.file.run(
@ -457,5 +470,5 @@ const DB_INIT_TABLE = {
); );
return cb(null); return cb(null);
} },
}; };

View File

@ -1,12 +1,12 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const async = require('async'); const async = require('async');
module.exports = class DescriptIonFile { module.exports = class DescriptIonFile {
constructor() { constructor() {
@ -19,14 +19,14 @@ module.exports = class DescriptIonFile {
getDescription(fileName) { getDescription(fileName) {
const entry = this.get(fileName); const entry = this.get(fileName);
if(entry) { if (entry) {
return entry.desc; return entry.desc;
} }
} }
static createFromFile(path, cb) { static createFromFile(path, cb) {
fs.readFile(path, (err, descData) => { fs.readFile(path, (err, descData) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -35,43 +35,48 @@ module.exports = class DescriptIonFile {
// DESCRIPT.ION entries are terminated with a CR and/or LF // DESCRIPT.ION entries are terminated with a CR and/or LF
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
async.each(lines, (entryData, nextLine) => { async.each(
// lines,
// We allow quoted (long) filenames or non-quoted filenames. (entryData, nextLine) => {
// FILENAME<SPC>DESC<0x04><program data><CR/LF> //
// // We allow quoted (long) filenames or non-quoted filenames.
const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex // FILENAME<SPC>DESC<0x04><program data><CR/LF>
if(!parts) { //
return nextLine(null); const parts = entryData.match(
} /^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/
); // eslint-disable-line no-control-regex
const fileName = parts[1] || parts[2]; if (!parts) {
return nextLine(null);
//
// Un-escape CR/LF's
// - escapped \r and/or \n
// - BBBS style @n - See https://www.bbbs.net/sysop.html
//
const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
descIonFile.entries.set(
fileName,
{
desc : desc,
programId : parts[4],
programData : parts[5],
} }
);
return nextLine(null); const fileName = parts[1] || parts[2];
},
() => { //
return cb( // Un-escape CR/LF's
descIonFile.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized DESCRIPT.ION format'), // - escapped \r and/or \n
descIonFile // - BBBS style @n - See https://www.bbbs.net/sysop.html
); //
}); const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
descIonFile.entries.set(fileName, {
desc: desc,
programId: parts[4],
programData: parts[5],
});
return nextLine(null);
},
() => {
return cb(
descIonFile.entries.size > 0
? null
: Errors.Invalid(
'Invalid or unrecognized DESCRIPT.ION format'
),
descIonFile
);
}
);
}); });
} }
}; };

View File

@ -1,28 +1,28 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const Events = require('./events'); const Events = require('./events');
// deps // deps
const pty = require('node-pty'); const pty = require('node-pty');
const decode = require('iconv-lite').decode; const decode = require('iconv-lite').decode;
const createServer = require('net').createServer; const createServer = require('net').createServer;
const paths = require('path'); const paths = require('path');
const _ = require('lodash'); const _ = require('lodash');
module.exports = class Door { module.exports = class Door {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.restored = false; this.restored = false;
} }
prepare(ioType, cb) { prepare(ioType, cb) {
this.io = ioType; this.io = ioType;
// we currently only have to do any real setup for 'socket' // we currently only have to do any real setup for 'socket'
if('socket' !== ioType) { if ('socket' !== ioType) {
return cb(null); return cb(null);
} }
@ -32,13 +32,16 @@ module.exports = class Door {
}); });
conn.once('error', err => { conn.once('error', err => {
this.client.log.info( { error : err.message }, 'Door socket server connection'); this.client.log.info(
{ error: err.message },
'Door socket server connection'
);
return this.restoreIo(conn); return this.restoreIo(conn);
}); });
this.sockServer.getConnections( (err, count) => { this.sockServer.getConnections((err, count) => {
// We expect only one connection from our DOOR/emulator/etc. // We expect only one connection from our DOOR/emulator/etc.
if(!err && count <= 1) { if (!err && count <= 1) {
this.client.term.output.pipe(conn); this.client.term.output.pipe(conn);
conn.on('data', this.doorDataHandler.bind(this)); conn.on('data', this.doorDataHandler.bind(this));
} }
@ -53,39 +56,39 @@ module.exports = class Door {
run(exeInfo, cb) { run(exeInfo, cb) {
this.encoding = (exeInfo.encoding || 'cp437').toLowerCase(); this.encoding = (exeInfo.encoding || 'cp437').toLowerCase();
if('socket' === this.io && !this.sockServer) { if ('socket' === this.io && !this.sockServer) {
return cb(Errors.UnexpectedState('Socket server is not running')); return cb(Errors.UnexpectedState('Socket server is not running'));
} }
const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd); const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);
const formatObj = { const formatObj = {
dropFile : exeInfo.dropFile, dropFile: exeInfo.dropFile,
dropFilePath : exeInfo.dropFilePath, dropFilePath: exeInfo.dropFilePath,
node : exeInfo.node.toString(), node: exeInfo.node.toString(),
srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1', srvPort: this.sockServer ? this.sockServer.address().port.toString() : '-1',
userId : this.client.user.userId.toString(), userId: this.client.user.userId.toString(),
userName : this.client.user.getSanitizedName(), userName: this.client.user.getSanitizedName(),
userNameRaw : this.client.user.username, userNameRaw: this.client.user.username,
cwd : cwd, cwd: cwd,
}; };
const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) ); const args = exeInfo.args.map(arg => stringFormat(arg, formatObj));
this.client.log.info( this.client.log.info(
{ cmd : exeInfo.cmd, args, io : this.io }, { cmd: exeInfo.cmd, args, io: this.io },
'Executing external door process' 'Executing external door process'
); );
try { try {
this.doorPty = pty.spawn(exeInfo.cmd, args, { this.doorPty = pty.spawn(exeInfo.cmd, args, {
cols : this.client.term.termWidth, cols: this.client.term.termWidth,
rows : this.client.term.termHeight, rows: this.client.term.termHeight,
cwd : cwd, cwd: cwd,
env : exeInfo.env, env: exeInfo.env,
encoding : null, // we want to handle all encoding ourself encoding: null, // we want to handle all encoding ourself
}); });
} catch(e) { } catch (e) {
return cb(e); return cb(e);
} }
@ -93,9 +96,12 @@ module.exports = class Door {
// PID is launched. Make sure it's killed off if the user disconnects. // PID is launched. Make sure it's killed off if the user disconnects.
// //
Events.once(Events.getSystemEvents().ClientDisconnected, evt => { Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
if (this.doorPty && this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId')) { if (
this.doorPty &&
this.client.session.uniqueId === _.get(evt, 'client.session.uniqueId')
) {
this.client.log.info( this.client.log.info(
{ pid : this.doorPty.pid }, { pid: this.doorPty.pid },
'User has disconnected; Killing door process.' 'User has disconnected; Killing door process.'
); );
this.doorPty.kill(); this.doorPty.kill();
@ -103,10 +109,11 @@ module.exports = class Door {
}); });
this.client.log.debug( this.client.log.debug(
{ processId : this.doorPty.pid }, 'External door process spawned' { processId: this.doorPty.pid },
'External door process spawned'
); );
if('stdio' === this.io) { if ('stdio' === this.io) {
this.client.log.debug('Using stdio for door I/O'); this.client.log.debug('Using stdio for door I/O');
this.client.term.output.pipe(this.doorPty); this.client.term.output.pipe(this.doorPty);
@ -116,22 +123,25 @@ module.exports = class Door {
this.doorPty.once('close', () => { this.doorPty.once('close', () => {
return this.restoreIo(this.doorPty); return this.restoreIo(this.doorPty);
}); });
} else if('socket' === this.io) { } else if ('socket' === this.io) {
this.client.log.debug( this.client.log.debug(
{ srvPort : this.sockServer.address().port, srvSocket : this.sockServerSocket }, {
srvPort: this.sockServer.address().port,
srvSocket: this.sockServerSocket,
},
'Using temporary socket server for door I/O' 'Using temporary socket server for door I/O'
); );
} }
this.doorPty.once('exit', exitCode => { this.doorPty.once('exit', exitCode => {
this.client.log.info( { exitCode : exitCode }, 'Door exited'); this.client.log.info({ exitCode: exitCode }, 'Door exited');
if(this.sockServer) { if (this.sockServer) {
this.sockServer.close(); this.sockServer.close();
} }
// we may not get a close // we may not get a close
if('stdio' === this.io) { if ('stdio' === this.io) {
this.restoreIo(this.doorPty); this.restoreIo(this.doorPty);
} }
@ -147,13 +157,13 @@ module.exports = class Door {
} }
restoreIo(piped) { restoreIo(piped) {
if(!this.restored) { if (!this.restored) {
if(this.doorPty) { if (this.doorPty) {
this.doorPty.kill(); this.doorPty.kill();
} }
const output = this.client.term.output; const output = this.client.term.output;
if(output) { if (output) {
output.unpipe(piped); output.unpipe(piped);
output.resume(); output.resume();
} }

View File

@ -2,22 +2,19 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const { resetScreen } = require('./ansi_term.js'); const { resetScreen } = require('./ansi_term.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const { const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
trackDoorRunBegin,
trackDoorRunEnd
} = require('./door_util.js');
// deps // deps
const async = require('async'); const async = require('async');
const SSHClient = require('ssh2').Client; const SSHClient = require('ssh2').Client;
exports.moduleInfo = { exports.moduleInfo = {
name : 'DoorParty', name: 'DoorParty',
desc : 'DoorParty Access Module', desc: 'DoorParty Access Module',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class DoorPartyModule extends MenuModule { exports.getModule = class DoorPartyModule extends MenuModule {
@ -25,10 +22,10 @@ exports.getModule = class DoorPartyModule extends MenuModule {
super(options); super(options);
// establish defaults // establish defaults
this.config = options.menuConfig.config; this.config = options.menuConfig.config;
this.config.host = this.config.host || 'dp.throwbackbbs.com'; this.config.host = this.config.host || 'dp.throwbackbbs.com';
this.config.sshPort = this.config.sshPort || 2022; this.config.sshPort = this.config.sshPort || 2022;
this.config.rloginPort = this.config.rloginPort || 513; this.config.rloginPort = this.config.rloginPort || 513;
} }
initSequence() { initSequence() {
@ -40,12 +37,12 @@ exports.getModule = class DoorPartyModule extends MenuModule {
function validateConfig(callback) { function validateConfig(callback) {
return self.validateConfigFields( return self.validateConfigFields(
{ {
host : 'string', host: 'string',
username : 'string', username: 'string',
password : 'string', password: 'string',
bbsTag : 'string', bbsTag: 'string',
sshPort : 'number', sshPort: 'number',
rloginPort : 'number', rloginPort: 'number',
}, },
callback callback
); );
@ -60,12 +57,12 @@ exports.getModule = class DoorPartyModule extends MenuModule {
let pipedStream; let pipedStream;
let doorTracking; let doorTracking;
const restorePipe = function() { const restorePipe = function () {
if(pipedStream && !pipeRestored && !clientTerminated) { if (pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream); self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume(); self.client.term.output.resume();
if(doorTracking) { if (doorTracking) {
trackDoorRunEnd(doorTracking); trackDoorRunEnd(doorTracking);
doorTracking = null; doorTracking = null;
} }
@ -75,48 +72,60 @@ exports.getModule = class DoorPartyModule extends MenuModule {
sshClient.on('ready', () => { sshClient.on('ready', () => {
// track client termination so we can clean up early // track client termination so we can clean up early
self.client.once('end', () => { self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating DoorParty connection'); self.client.log.info(
'Connection ended. Terminating DoorParty connection'
);
clientTerminated = true; clientTerminated = true;
sshClient.end(); sshClient.end();
}); });
// establish tunnel for rlogin // establish tunnel for rlogin
sshClient.forwardOut('127.0.0.1', self.config.sshPort, self.config.host, self.config.rloginPort, (err, stream) => { sshClient.forwardOut(
if(err) { '127.0.0.1',
return callback(Errors.General('Failed to establish tunnel')); self.config.sshPort,
self.config.host,
self.config.rloginPort,
(err, stream) => {
if (err) {
return callback(
Errors.General('Failed to establish tunnel')
);
}
doorTracking = trackDoorRunBegin(self.client);
//
// Send rlogin
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
// [XA]nuskooler
//
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin);
pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream);
stream.on('data', d => {
// :TODO: we should just pipe this...
self.client.term.rawWrite(d);
});
stream.on('end', () => {
sshClient.end();
});
stream.on('close', () => {
restorePipe();
sshClient.end();
});
} }
);
doorTracking = trackDoorRunBegin(self.client);
//
// Send rlogin
// DoorParty wants the "server username" portion to be in the format of [BBS_TAG]USERNAME, e.g.
// [XA]nuskooler
//
const rlogin = `\x00${self.client.user.username}\x00[${self.config.bbsTag}]${self.client.user.username}\x00${self.client.term.termType}\x00`;
stream.write(rlogin);
pipedStream = stream; // :TODO: this is hacky...
self.client.term.output.pipe(stream);
stream.on('data', d => {
// :TODO: we should just pipe this...
self.client.term.rawWrite(d);
});
stream.on('end', () => {
sshClient.end();
});
stream.on('close', () => {
restorePipe();
sshClient.end();
});
});
}); });
sshClient.on('error', err => { sshClient.on('error', err => {
self.client.log.info(`DoorParty SSH client error: ${err.message}`); self.client.log.info(
`DoorParty SSH client error: ${err.message}`
);
trackDoorRunEnd(doorTracking); trackDoorRunEnd(doorTracking);
}); });
@ -125,23 +134,23 @@ exports.getModule = class DoorPartyModule extends MenuModule {
callback(null); callback(null);
}); });
sshClient.connect( { sshClient.connect({
host : self.config.host, host: self.config.host,
port : self.config.sshPort, port: self.config.sshPort,
username : self.config.username, username: self.config.username,
password : self.config.password, password: self.config.password,
}); });
// note: no explicit callback() until we're finished! // note: no explicit callback() until we're finished!
} },
], ],
err => { err => {
if(err) { if (err) {
self.client.log.warn( { error : err.message }, 'DoorParty error'); self.client.log.warn({ error: err.message }, 'DoorParty error');
} }
// if the client is still here, go to previous // if the client is still here, go to previous
if(!clientTerminated) { if (!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }
} }

View File

@ -1,14 +1,14 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const Events = require('./events.js'); const Events = require('./events.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const moment = require('moment'); const moment = require('moment');
exports.trackDoorRunBegin = trackDoorRunBegin; exports.trackDoorRunBegin = trackDoorRunBegin;
exports.trackDoorRunEnd = trackDoorRunEnd; exports.trackDoorRunEnd = trackDoorRunEnd;
function trackDoorRunBegin(client, doorTag) { function trackDoorRunBegin(client, doorTag) {
const startTime = moment(); const startTime = moment();
@ -23,20 +23,24 @@ function trackDoorRunEnd(trackInfo) {
const { startTime, client, doorTag } = trackInfo; const { startTime, client, doorTag } = trackInfo;
const diff = moment.duration(moment().diff(startTime)); const diff = moment.duration(moment().diff(startTime));
if(diff.asSeconds() >= 45) { if (diff.asSeconds() >= 45) {
StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1); StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalCount, 1);
} }
const runTimeMinutes = Math.floor(diff.asMinutes()); const runTimeMinutes = Math.floor(diff.asMinutes());
if(runTimeMinutes > 0) { if (runTimeMinutes > 0) {
StatLog.incrementUserStat(client.user, UserProps.DoorRunTotalMinutes, runTimeMinutes); StatLog.incrementUserStat(
client.user,
UserProps.DoorRunTotalMinutes,
runTimeMinutes
);
const eventInfo = { const eventInfo = {
runTimeMinutes, runTimeMinutes,
user : client.user, user: client.user,
doorTag : doorTag || 'unknown', doorTag: doorTag || 'unknown',
}; };
Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo); Events.emit(Events.getSystemEvents().UserRunDoor, eventInfo);
} }
} }

View File

@ -1,9 +1,9 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const FileEntry = require('./file_entry'); const FileEntry = require('./file_entry');
const UserProps = require('./user_property'); const UserProps = require('./user_property');
const Events = require('./events'); const Events = require('./events');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
@ -12,9 +12,11 @@ module.exports = class DownloadQueue {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
if(!Array.isArray(this.client.user.downloadQueue)) { if (!Array.isArray(this.client.user.downloadQueue)) {
if(this.client.user.properties[UserProps.DownloadQueue]) { if (this.client.user.properties[UserProps.DownloadQueue]) {
this.loadFromProperty(this.client.user.properties[UserProps.DownloadQueue]); this.loadFromProperty(
this.client.user.properties[UserProps.DownloadQueue]
);
} else { } else {
this.client.user.downloadQueue = []; this.client.user.downloadQueue = [];
} }
@ -33,68 +35,86 @@ module.exports = class DownloadQueue {
this.client.user.downloadQueue = []; this.client.user.downloadQueue = [];
} }
toggle(fileEntry, systemFile=false) { toggle(fileEntry, systemFile = false) {
if(this.isQueued(fileEntry)) { if (this.isQueued(fileEntry)) {
this.client.user.downloadQueue = this.client.user.downloadQueue.filter(e => fileEntry.fileId !== e.fileId); this.client.user.downloadQueue = this.client.user.downloadQueue.filter(
e => fileEntry.fileId !== e.fileId
);
} else { } else {
this.add(fileEntry, systemFile); this.add(fileEntry, systemFile);
} }
} }
add(fileEntry, systemFile=false) { add(fileEntry, systemFile = false) {
this.client.user.downloadQueue.push({ this.client.user.downloadQueue.push({
fileId : fileEntry.fileId, fileId: fileEntry.fileId,
areaTag : fileEntry.areaTag, areaTag: fileEntry.areaTag,
fileName : fileEntry.fileName, fileName: fileEntry.fileName,
path : fileEntry.filePath, path: fileEntry.filePath,
byteSize : fileEntry.meta.byte_size || 0, byteSize: fileEntry.meta.byte_size || 0,
systemFile : systemFile, systemFile: systemFile,
}); });
} }
removeItems(fileIds) { removeItems(fileIds) {
if(!Array.isArray(fileIds)) { if (!Array.isArray(fileIds)) {
fileIds = [ fileIds ]; fileIds = [fileIds];
} }
const [ remain, removed ] = _.partition(this.client.user.downloadQueue, e => ( -1 === fileIds.indexOf(e.fileId) )); const [remain, removed] = _.partition(
this.client.user.downloadQueue,
e => -1 === fileIds.indexOf(e.fileId)
);
this.client.user.downloadQueue = remain; this.client.user.downloadQueue = remain;
return removed; return removed;
} }
isQueued(entryOrId) { isQueued(entryOrId) {
if(entryOrId instanceof FileEntry) { if (entryOrId instanceof FileEntry) {
entryOrId = entryOrId.fileId; entryOrId = entryOrId.fileId;
} }
return this.client.user.downloadQueue.find(e => entryOrId === e.fileId) ? true : false; return this.client.user.downloadQueue.find(e => entryOrId === e.fileId)
? true
: false;
} }
toProperty() { return JSON.stringify(this.client.user.downloadQueue); } toProperty() {
return JSON.stringify(this.client.user.downloadQueue);
}
loadFromProperty(prop) { loadFromProperty(prop) {
try { try {
this.client.user.downloadQueue = JSON.parse(prop); this.client.user.downloadQueue = JSON.parse(prop);
} catch(e) { } catch (e) {
this.client.user.downloadQueue = []; this.client.user.downloadQueue = [];
this.client.log.error( { error : e.message, property : prop }, 'Failed parsing download queue property'); this.client.log.error(
{ error: e.message, property: prop },
'Failed parsing download queue property'
);
} }
} }
addTemporaryDownload(entry) { addTemporaryDownload(entry) {
this.add(entry, true); // true=systemFile this.add(entry, true); // true=systemFile
// clean up after ourselves when the session ends // clean up after ourselves when the session ends
const thisUniqueId = this.client.session.uniqueId; const thisUniqueId = this.client.session.uniqueId;
Events.once(Events.getSystemEvents().ClientDisconnected, evt => { Events.once(Events.getSystemEvents().ClientDisconnected, evt => {
if(thisUniqueId === _.get(evt, 'client.session.uniqueId')) { if (thisUniqueId === _.get(evt, 'client.session.uniqueId')) {
FileEntry.removeEntry(entry, { removePhysFile : true }, err => { FileEntry.removeEntry(entry, { removePhysFile: true }, err => {
const Log = require('./logger').log; const Log = require('./logger').log;
if(err) { if (err) {
Log.warn( { fileId : entry.fileId, path : entry.filePath }, 'Failed removing temporary session download' ); Log.warn(
{ fileId: entry.fileId, path: entry.filePath },
'Failed removing temporary session download'
);
} else { } else {
Log.debug( { fileId : entry.fileId, path : entry.filePath }, 'Removed temporary session download item' ); Log.debug(
{ fileId: entry.fileId, path: entry.filePath },
'Removed temporary session download item'
);
} }
}); });
} }

View File

@ -2,18 +2,18 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const SysProps = require('./system_property.js'); const SysProps = require('./system_property.js');
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const { mkdirs } = require('fs-extra'); const { mkdirs } = require('fs-extra');
// //
// Resources // Resources
@ -25,31 +25,34 @@ const { mkdirs } = require('fs-extra');
// * http://lord.lordlegacy.com/dosemu/ // * http://lord.lordlegacy.com/dosemu/
// //
module.exports = class DropFile { module.exports = class DropFile {
constructor(client, { fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {} ) { constructor(
this.client = client; client,
this.fileType = fileType.toUpperCase(); { fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {}
this.baseDir = baseDir; ) {
this.client = client;
this.fileType = fileType.toUpperCase();
this.baseDir = baseDir;
} }
get fullPath() { get fullPath() {
return paths.join(this.baseDir, ('node' + this.client.node), this.fileName); return paths.join(this.baseDir, 'node' + this.client.node, this.fileName);
} }
get fileName() { get fileName() {
return { return {
DOOR : 'DOOR.SYS', // GAP BBS, many others DOOR: 'DOOR.SYS', // GAP BBS, many others
DOOR32 : 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec) DOOR32: 'door32.sys', // Mystic, EleBBS, Syncronet, Maximus, Telegard, AdeptXBBS (lowercase name as per spec)
CALLINFO : 'CALLINFO.BBS', // Citadel? CALLINFO: 'CALLINFO.BBS', // Citadel?
DORINFO : this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ... DORINFO: this.getDoorInfoFileName(), // RBBS, RemoteAccess, QBBS, ...
CHAIN : 'CHAIN.TXT', // WWIV CHAIN: 'CHAIN.TXT', // WWIV
CURRUSER : 'CURRUSER.BBS', // RyBBS CURRUSER: 'CURRUSER.BBS', // RyBBS
SFDOORS : 'SFDOORS.DAT', // Spitfire SFDOORS: 'SFDOORS.DAT', // Spitfire
PCBOARD : 'PCBOARD.SYS', // PCBoard PCBOARD: 'PCBOARD.SYS', // PCBoard
TRIBBS : 'TRIBBS.SYS', // TriBBS TRIBBS: 'TRIBBS.SYS', // TriBBS
USERINFO : 'USERINFO.DAT', // Wildcat! 3.0+ USERINFO: 'USERINFO.DAT', // Wildcat! 3.0+
JUMPER : 'JUMPER.DAT', // 2AM BBS JUMPER: 'JUMPER.DAT', // 2AM BBS
SXDOOR : 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE SXDOOR: 'SXDOOR.' + _.pad(this.client.node.toString(), 3, '0'), // System/X, dESiRE
INFO : 'INFO.BBS', // Phoenix BBS INFO: 'INFO.BBS', // Phoenix BBS
}[this.fileType]; }[this.fileType];
} }
@ -59,9 +62,9 @@ module.exports = class DropFile {
getHandler() { getHandler() {
return { return {
DOOR : this.getDoorSysBuffer, DOOR: this.getDoorSysBuffer,
DOOR32 : this.getDoor32Buffer, DOOR32: this.getDoor32Buffer,
DORINFO : this.getDoorInfoDefBuffer, DORINFO: this.getDoorInfoDefBuffer,
}[this.fileType]; }[this.fileType];
} }
@ -73,9 +76,9 @@ module.exports = class DropFile {
getDoorInfoFileName() { getDoorInfoFileName() {
let x; let x;
const node = this.client.node; const node = this.client.node;
if(10 === node) { if (10 === node) {
x = 0; x = 0;
} else if(node < 10) { } else if (node < 10) {
x = node; x = node;
} else { } else {
x = String.fromCharCode('a'.charCodeAt(0) + (node - 11)); x = String.fromCharCode('a'.charCodeAt(0) + (node - 11));
@ -84,75 +87,82 @@ module.exports = class DropFile {
} }
getDoorSysBuffer() { getDoorSysBuffer() {
const prop = this.client.user.properties; const prop = this.client.user.properties;
const now = moment(); const now = moment();
const secLevel = this.client.user.getLegacySecurityLevel().toString(); const secLevel = this.client.user.getLegacySecurityLevel().toString();
const fullName = this.client.user.getSanitizedName('real'); const fullName = this.client.user.getSanitizedName('real');
const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY'); const bd = moment(prop[UserProps.Birthdate]).format('MM/DD/YY');
const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024); const upK = Math.floor((parseInt(prop[UserProps.FileUlTotalBytes]) || 0) / 1024);
const downK = Math.floor((parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024); const downK = Math.floor(
(parseInt(prop[UserProps.FileDlTotalBytes]) || 0) / 1024
);
const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format('hh:mm'); const timeOfCall = moment(prop[UserProps.LastLoginTs] || moment()).format(
'hh:mm'
);
// :TODO: fix time remaining // :TODO: fix time remaining
// :TODO: fix default protocol -- user prop: transfer_protocol // :TODO: fix default protocol -- user prop: transfer_protocol
return iconv.encode( [ return iconv.encode(
'COM1:', // "Comm Port - COM0: = LOCAL MODE" [
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) 'COM1:', // "Comm Port - COM0: = LOCAL MODE"
'8', // "Parity - 7 or 8" '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
this.client.node.toString(), // "Node Number - 1 to 99" '8', // "Parity - 7 or 8"
'57600', // "DTE Rate. Actual BPS rate to use. (kg)" this.client.node.toString(), // "Node Number - 1 to 99"
'Y', // "Screen Display - Y=On N=Off (Default to Y)" '57600', // "DTE Rate. Actual BPS rate to use. (kg)"
'Y', // "Printer Toggle - Y=On N=Off (Default to Y)" 'Y', // "Screen Display - Y=On N=Off (Default to Y)"
'Y', // "Page Bell - Y=On N=Off (Default to Y)" 'Y', // "Printer Toggle - Y=On N=Off (Default to Y)"
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" 'Y', // "Page Bell - Y=On N=Off (Default to Y)"
fullName, // "User Full Name" 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
prop[UserProps.Location]|| 'Anywhere', // "Calling From" fullName, // "User Full Name"
'123-456-7890', // "Home Phone" prop[UserProps.Location] || 'Anywhere', // "Calling From"
'123-456-7890', // "Work/Data Phone" '123-456-7890', // "Home Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext) '123-456-7890', // "Work/Data Phone"
secLevel, // "Security Level" 'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
prop[UserProps.LoginCount].toString(), // "Total Times On" secLevel, // "Security Level"
now.format('MM/DD/YY'), // "Last Date Called" prop[UserProps.LoginCount].toString(), // "Total Times On"
'15360', // "Seconds Remaining THIS call (for those that particular)" now.format('MM/DD/YY'), // "Last Date Called"
'256', // "Minutes Remaining THIS call" '15360', // "Seconds Remaining THIS call (for those that particular)"
'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller" '256', // "Minutes Remaining THIS call"
this.client.term.termHeight.toString(), // "Page Length" 'GR', // "Graphics Mode - GR=Graph, NG=Non-Graph, 7E=7,E Caller"
'N', // "User Mode - Y = Expert, N = Novice" this.client.term.termHeight.toString(), // "Page Length"
'1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)" 'N', // "User Mode - Y = Expert, N = Novice"
'1', // "Conference Exited To DOOR From (G)" '1,2,3,4,5,6,7', // "Conferences/Forums Registered In (ABCDEFG)"
'01/01/99', // "User Expiration Date (mm/dd/yy)" '1', // "Conference Exited To DOOR From (G)"
this.client.user.userId.toString(), // "User File's Record Number" '01/01/99', // "User Expiration Date (mm/dd/yy)"
'Z', // "Default Protocol - X, C, Y, G, I, N, Etc." this.client.user.userId.toString(), // "User File's Record Number"
// :TODO: fix up, down, etc. form user properties 'Z', // "Default Protocol - X, C, Y, G, I, N, Etc."
'0', // "Total Uploads" // :TODO: fix up, down, etc. form user properties
'0', // "Total Downloads" '0', // "Total Uploads"
'0', // "Daily Download "K" Total" '0', // "Total Downloads"
'999999', // "Daily Download Max. "K" Limit" '0', // "Daily Download "K" Total"
bd, // "Caller's Birthdate" '999999', // "Daily Download Max. "K" Limit"
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" bd, // "Caller's Birthdate"
'X:\\GEN\\', // "Path to the GEN directory" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)" 'X:\\GEN\\', // "Path to the GEN directory"
this.client.user.getSanitizedName(), // "Alias name" StatLog.getSystemStat(SysProps.SysOpUsername), // "Sysop's Name (name BBS refers to Sysop as)"
'00:05', // "Event time (hh:mm)" (note: wat?) this.client.user.getSanitizedName(), // "Alias name"
'Y', // "If its an error correcting connection (Y/N)" '00:05', // "Event time (hh:mm)" (note: wat?)
'Y', // "ANSI supported & caller using NG mode (Y/N)" 'Y', // "If its an error correcting connection (Y/N)"
'Y', // "Use Record Locking (Y/N)" 'Y', // "ANSI supported & caller using NG mode (Y/N)"
'7', // "BBS Default Color (Standard IBM color code, ie, 1-15)" 'Y', // "Use Record Locking (Y/N)"
// :TODO: fix minutes here also: '7', // "BBS Default Color (Standard IBM color code, ie, 1-15)"
'256', // "Time Credits In Minutes (positive/negative)" // :TODO: fix minutes here also:
'07/07/90', // "Last New Files Scan Date (mm/dd/yy)" '256', // "Time Credits In Minutes (positive/negative)"
timeOfCall, // "Time of This Call" '07/07/90', // "Last New Files Scan Date (mm/dd/yy)"
timeOfCall, // "Time of Last Call (hh:mm)" timeOfCall, // "Time of This Call"
'9999', // "Maximum daily files available" timeOfCall, // "Time of Last Call (hh:mm)"
'0', // "Files d/led so far today" '9999', // "Maximum daily files available"
upK.toString(), // "Total "K" Bytes Uploaded" '0', // "Files d/led so far today"
downK.toString(), // "Total "K" Bytes Downloaded" upK.toString(), // "Total "K" Bytes Uploaded"
prop[UserProps.UserComment] || 'None', // "User Comment" downK.toString(), // "Total "K" Bytes Downloaded"
'0', // "Total Doors Opened" prop[UserProps.UserComment] || 'None', // "User Comment"
'0', // "Total Messages Left" '0', // "Total Doors Opened"
].join('\r\n') + '\r\n', 'cp437'); '0', // "Total Messages Left"
].join('\r\n') + '\r\n',
'cp437'
);
} }
getDoor32Buffer() { getDoor32Buffer() {
@ -163,26 +173,29 @@ module.exports = class DropFile {
// //
// :TODO: local/serial/telnet need to be configurable -- which also changes socket handle! // :TODO: local/serial/telnet need to be configurable -- which also changes socket handle!
const Door32CommTypes = { const Door32CommTypes = {
Local : 0, Local: 0,
Serial : 1, Serial: 1,
Telnet : 2, Telnet: 2,
}; };
const commType = Door32CommTypes.Telnet; const commType = Door32CommTypes.Telnet;
return iconv.encode([ return iconv.encode(
commType.toString(), [
'-1', commType.toString(),
'115200', '-1',
Config().general.boardName, '115200',
this.client.user.userId.toString(), Config().general.boardName,
this.client.user.getSanitizedName('real'), this.client.user.userId.toString(),
this.client.user.getSanitizedName(), this.client.user.getSanitizedName('real'),
this.client.user.getLegacySecurityLevel().toString(), this.client.user.getSanitizedName(),
'546', // :TODO: Minutes left! this.client.user.getLegacySecurityLevel().toString(),
'1', // ANSI '546', // :TODO: Minutes left!
this.client.node.toString(), '1', // ANSI
].join('\r\n') + '\r\n', 'cp437'); this.client.node.toString(),
].join('\r\n') + '\r\n',
'cp437'
);
} }
getDoorInfoDefBuffer() { getDoorInfoDefBuffer() {
@ -194,31 +207,36 @@ module.exports = class DropFile {
// //
// Note that usernames are just used for first/last names here // Note that usernames are just used for first/last names here
// //
const opUserName = /[^\s]*/.exec(StatLog.getSystemStat(SysProps.SysOpUsername))[0]; const opUserName = /[^\s]*/.exec(
const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0]; StatLog.getSystemStat(SysProps.SysOpUsername)
const secLevel = this.client.user.getLegacySecurityLevel().toString(); )[0];
const location = this.client.user.properties[UserProps.Location]; const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0];
const secLevel = this.client.user.getLegacySecurityLevel().toString();
const location = this.client.user.properties[UserProps.Location];
return iconv.encode( [ return iconv.encode(
Config().general.boardName, // "The name of the system." [
opUserName, // "The sysop's name up to the first space." Config().general.boardName, // "The name of the system."
opUserName, // "The sysop's name following the first space." opUserName, // "The sysop's name up to the first space."
'COM1', // "The serial port the modem is connected to, or 0 if logged in on console." opUserName, // "The sysop's name following the first space."
'57600', // "The current port (DTE) rate." 'COM1', // "The serial port the modem is connected to, or 0 if logged in on console."
'0', // "The number "0"" '57600', // "The current port (DTE) rate."
userName, // "The current user's name, up to the first space." '0', // "The number "0""
userName, // "The current user's name, following the first space." userName, // "The current user's name, up to the first space."
location || '', // "Where the user lives, or a blank line if unknown." userName, // "The current user's name, following the first space."
'1', // "The number "0" if TTY, or "1" if ANSI." location || '', // "Where the user lives, or a blank line if unknown."
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." '1', // "The number "0" if TTY, or "1" if ANSI."
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
'-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
].join('\r\n') + '\r\n', 'cp437'); '-1', // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
].join('\r\n') + '\r\n',
'cp437'
);
} }
createFile(cb) { createFile(cb) {
mkdirs(paths.dirname(this.fullPath), err => { mkdirs(paths.dirname(this.fullPath), err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
return fs.writeFile(this.fullPath, this.getContents(), cb); return fs.writeFile(this.fullPath, this.getContents(), cb);

View File

@ -2,57 +2,59 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const TextView = require('./text_view.js').TextView; const TextView = require('./text_view.js').TextView;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT; const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
exports.EditTextView = EditTextView; exports.EditTextView = EditTextView;
const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, { const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, {
delete : [ 'delete', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ delete: ['delete', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
}); });
function EditTextView(options) { function EditTextView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false; options.resizable = false;
if(!_.isObject(options.specialKeyMap)) { if (!_.isObject(options.specialKeyMap)) {
options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP; options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP;
} }
TextView.call(this, options); TextView.call(this, options);
this.initDefaultWidth(); this.initDefaultWidth();
this.cursorPos = { row : 0, col : 0 }; this.cursorPos = { row: 0, col: 0 };
this.clientBackspace = function() { this.clientBackspace = function () {
this.text = this.text.substr(0, this.text.length - 1); this.text = this.text.substr(0, this.text.length - 1);
if(this.text.length >= this.dimens.width) { if (this.text.length >= this.dimens.width) {
this.redraw(); this.redraw();
} else { } else {
this.cursorPos.col -= 1; this.cursorPos.col -= 1;
if(this.cursorPos.col >= 0) { if (this.cursorPos.col >= 0) {
const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); this.client.term.write(
`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`
);
} }
} }
} };
} }
require('util').inherits(EditTextView, TextView); require('util').inherits(EditTextView, TextView);
EditTextView.prototype.onKeyPress = function(ch, key) { EditTextView.prototype.onKeyPress = function (ch, key) {
if(key) { if (key) {
if(this.isKeyMapped('backspace', key.name)) { if (this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) { if (this.text.length > 0) {
this.clientBackspace(); this.clientBackspace();
} }
@ -63,29 +65,29 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
if (this.text.length > 0 && this.cursorPos.col === this.text.length) { if (this.text.length > 0 && this.cursorPos.col === this.text.length) {
this.clientBackspace(); this.clientBackspace();
} }
} else if(this.isKeyMapped('clearLine', key.name)) { } else if (this.isKeyMapped('clearLine', key.name)) {
this.text = ''; this.text = '';
this.cursorPos.col = 0; this.cursorPos.col = 0;
this.setFocus(true); // resetting focus will redraw & adjust cursor this.setFocus(true); // resetting focus will redraw & adjust cursor
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
} }
} }
if(ch && strUtil.isPrintable(ch)) { if (ch && strUtil.isPrintable(ch)) {
if(this.text.length < this.maxLength) { if (this.text.length < this.maxLength) {
ch = strUtil.stylizeString(ch, this.textStyle); ch = strUtil.stylizeString(ch, this.textStyle);
this.text += ch; this.text += ch;
if(this.text.length > this.dimens.width) { if (this.text.length > this.dimens.width) {
// no shortcuts - redraw the view // no shortcuts - redraw the view
this.redraw(); this.redraw();
} else { } else {
this.cursorPos.col += 1; this.cursorPos.col += 1;
if(_.isString(this.textMaskChar)) { if (_.isString(this.textMaskChar)) {
if(this.textMaskChar.length > 0) { if (this.textMaskChar.length > 0) {
this.client.term.write(this.textMaskChar); this.client.term.write(this.textMaskChar);
} }
} else { } else {
@ -98,10 +100,10 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
EditTextView.super_.prototype.onKeyPress.call(this, ch, key); EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
EditTextView.prototype.setText = function(text) { EditTextView.prototype.setText = function (text) {
// draw & set |text| // draw & set |text|
EditTextView.super_.prototype.setText.call(this, text); EditTextView.super_.prototype.setText.call(this, text);
// adjust local cursor tracking // adjust local cursor tracking
this.cursorPos = { row : 0, col : text.length }; this.cursorPos = { row: 0, col: text.length };
}; };

View File

@ -2,26 +2,26 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const nodeMailer = require('nodemailer'); const nodeMailer = require('nodemailer');
exports.sendMail = sendMail; exports.sendMail = sendMail;
function sendMail(message, cb) { function sendMail(message, cb) {
const config = Config(); const config = Config();
if(!_.has(config, 'email.transport')) { if (!_.has(config, 'email.transport')) {
return cb(Errors.MissingConfig('Email "email.transport" configuration missing')); return cb(Errors.MissingConfig('Email "email.transport" configuration missing'));
} }
message.from = message.from || config.email.defaultFrom; message.from = message.from || config.email.defaultFrom;
const transportOptions = Object.assign( {}, config.email.transport, { const transportOptions = Object.assign({}, config.email.transport, {
logger : Log, logger: Log,
}); });
const transport = nodeMailer.createTransport(transportOptions); const transport = nodeMailer.createTransport(transportOptions);

View File

@ -5,53 +5,65 @@ class EnigError extends Error {
constructor(message, code, reason, reasonCode) { constructor(message, code, reason, reasonCode) {
super(message); super(message);
this.name = this.constructor.name; this.name = this.constructor.name;
this.message = message; this.message = message;
this.code = code; this.code = code;
this.reason = reason; this.reason = reason;
this.reasonCode = reasonCode; this.reasonCode = reasonCode;
if(this.reason) { if (this.reason) {
this.message += `: ${this.reason}`; this.message += `: ${this.reason}`;
} }
if(typeof Error.captureStackTrace === 'function') { if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);
} else { } else {
this.stack = (new Error(message)).stack; this.stack = new Error(message).stack;
} }
} }
} }
exports.EnigError = EnigError; exports.EnigError = EnigError;
exports.Errors = { exports.Errors = {
General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode), General: (reason, reasonCode) =>
MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode), new EnigError('An error occurred', -33000, reason, reasonCode),
DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode), MenuStack: (reason, reasonCode) =>
AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode), new EnigError('Menu stack error', -33001, reason, reasonCode),
Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode), DoesNotExist: (reason, reasonCode) =>
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode), new EnigError('Object does not exist', -33002, reason, reasonCode),
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode), AccessDenied: (reason, reasonCode) =>
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode), new EnigError('Access denied', -32003, reason, reasonCode),
MissingParam : (reason, reasonCode) => new EnigError('Missing paramter(s)', -32008, reason, reasonCode), Invalid: (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
MissingMci : (reason, reasonCode) => new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode), ExternalProcess: (reason, reasonCode) =>
BadLogin : (reason, reasonCode) => new EnigError('Bad login attempt', -32010, reason, reasonCode), new EnigError('External process error', -32005, reason, reasonCode),
UserInterrupt : (reason, reasonCode) => new EnigError('User interrupted', -32011, reason, reasonCode), MissingConfig: (reason, reasonCode) =>
NothingToDo : (reason, reasonCode) => new EnigError('Nothing to do', -32012, reason, reasonCode), new EnigError('Missing configuration', -32006, reason, reasonCode),
UnexpectedState: (reason, reasonCode) =>
new EnigError('Unexpected state', -32007, reason, reasonCode),
MissingParam: (reason, reasonCode) =>
new EnigError('Missing paramter(s)', -32008, reason, reasonCode),
MissingMci: (reason, reasonCode) =>
new EnigError('Missing required MCI code(s)', -32009, reason, reasonCode),
BadLogin: (reason, reasonCode) =>
new EnigError('Bad login attempt', -32010, reason, reasonCode),
UserInterrupt: (reason, reasonCode) =>
new EnigError('User interrupted', -32011, reason, reasonCode),
NothingToDo: (reason, reasonCode) =>
new EnigError('Nothing to do', -32012, reason, reasonCode),
}; };
exports.ErrorReasons = { exports.ErrorReasons = {
AlreadyThere : 'ALREADYTHERE', AlreadyThere: 'ALREADYTHERE',
InvalidNextMenu : 'BADNEXT', InvalidNextMenu: 'BADNEXT',
NoPreviousMenu : 'NOPREV', NoPreviousMenu: 'NOPREV',
NoConditionMatch : 'NOCONDMATCH', NoConditionMatch: 'NOCONDMATCH',
NotEnabled : 'NOTENABLED', NotEnabled: 'NOTENABLED',
AlreadyLoggedIn : 'ALREADYLOGGEDIN', AlreadyLoggedIn: 'ALREADYLOGGEDIN',
TooMany : 'TOOMANY', TooMany: 'TOOMANY',
Disabled : 'DISABLED', Disabled: 'DISABLED',
Inactive : 'INACTIVE', Inactive: 'INACTIVE',
Locked : 'LOCKED', Locked: 'LOCKED',
NotAllowed : 'NOTALLOWED', NotAllowed: 'NOTALLOWED',
Invalid2FA : 'INVALID2FA', Invalid2FA: 'INVALID2FA',
}; };

View File

@ -2,17 +2,17 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
// deps // deps
const assert = require('assert'); const assert = require('assert');
module.exports = function(condition, message) { module.exports = function (condition, message) {
if(Config().debug.assertsEnabled) { if (Config().debug.assertsEnabled) {
assert.apply(this, arguments); assert.apply(this, arguments);
} else if(!(condition)) { } else if (!condition) {
const stack = new Error().stack; const stack = new Error().stack;
Log.error( { condition : condition, stack : stack }, message || 'Assertion failed' ); Log.error({ condition: condition, stack: stack }, message || 'Assertion failed');
} }
}; };

View File

@ -2,48 +2,52 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const PluginModule = require('./plugin_module.js').PluginModule; const PluginModule = require('./plugin_module.js').PluginModule;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const _ = require('lodash'); const _ = require('lodash');
const later = require('@breejs/later'); const later = require('@breejs/later');
const path = require('path'); const path = require('path');
const pty = require('node-pty'); const pty = require('node-pty');
const sane = require('sane'); const sane = require('sane');
const moment = require('moment'); const moment = require('moment');
const paths = require('path'); const paths = require('path');
const fse = require('fs-extra'); const fse = require('fs-extra');
exports.getModule = EventSchedulerModule; exports.getModule = EventSchedulerModule;
exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart
exports.moduleInfo = { exports.moduleInfo = {
name : 'Event Scheduler', name: 'Event Scheduler',
desc : 'Support for scheduling arbritary events', desc: 'Support for scheduling arbritary events',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;
class ScheduledEvent { class ScheduledEvent {
constructor(events, name) { constructor(events, name) {
this.name = name; this.name = name;
this.schedule = this.parseScheduleString(events[name].schedule); this.schedule = this.parseScheduleString(events[name].schedule);
this.action = this.parseActionSpec(events[name].action); this.action = this.parseActionSpec(events[name].action);
if(this.action) { if (this.action) {
this.action.args = events[name].args || []; this.action.args = events[name].args || [];
} }
} }
get isValid() { get isValid() {
if((!this.schedule || (!this.schedule.sched && !this.schedule.watchFile)) || !this.action) { if (
!this.schedule ||
(!this.schedule.sched && !this.schedule.watchFile) ||
!this.action
) {
return false; return false;
} }
if('method' === this.action.type && !this.action.location) { if ('method' === this.action.type && !this.action.location) {
return false; return false;
} }
@ -51,118 +55,132 @@ class ScheduledEvent {
} }
parseScheduleString(schedStr) { parseScheduleString(schedStr) {
if(!schedStr) { if (!schedStr) {
return false; return false;
} }
let schedule = {}; let schedule = {};
const m = SCHEDULE_REGEXP.exec(schedStr); const m = SCHEDULE_REGEXP.exec(schedStr);
if(m) { if (m) {
schedStr = schedStr.substr(0, m.index).trim(); schedStr = schedStr.substr(0, m.index).trim();
if('@watch:' === m[1]) { if ('@watch:' === m[1]) {
schedule.watchFile = m[2]; schedule.watchFile = m[2];
} }
} }
if(schedStr.length > 0) { if (schedStr.length > 0) {
const sched = later.parse.text(schedStr); const sched = later.parse.text(schedStr);
if(-1 === sched.error) { if (-1 === sched.error) {
schedule.sched = sched; schedule.sched = sched;
} }
} }
// return undefined if we couldn't parse out anything useful // return undefined if we couldn't parse out anything useful
if(!_.isEmpty(schedule)) { if (!_.isEmpty(schedule)) {
return schedule; return schedule;
} }
} }
parseActionSpec(actionSpec) { parseActionSpec(actionSpec) {
if(actionSpec) { if (actionSpec) {
if('@' === actionSpec[0]) { if ('@' === actionSpec[0]) {
const m = ACTION_REGEXP.exec(actionSpec); const m = ACTION_REGEXP.exec(actionSpec);
if(m) { if (m) {
if(m[2].indexOf(':') > -1) { if (m[2].indexOf(':') > -1) {
const parts = m[2].split(':'); const parts = m[2].split(':');
return { return {
type : m[1], type: m[1],
location : parts[0], location: parts[0],
what : parts[1], what: parts[1],
}; };
} else { } else {
return { return {
type : m[1], type: m[1],
what : m[2], what: m[2],
}; };
} }
} }
} else { } else {
return { return {
type : 'execute', type: 'execute',
what : actionSpec, what: actionSpec,
}; };
} }
} }
} }
executeAction(reason, cb) { executeAction(reason, cb) {
Log.info( { eventName : this.name, action : this.action, reason : reason }, 'Executing scheduled event action...'); Log.info(
{ eventName: this.name, action: this.action, reason: reason },
'Executing scheduled event action...'
);
if('method' === this.action.type) { if ('method' === this.action.type) {
const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js')
try { try {
const methodModule = require(modulePath); const methodModule = require(modulePath);
methodModule[this.action.what](this.action.args, err => { methodModule[this.action.what](this.action.args, err => {
if(err) { if (err) {
Log.debug( Log.debug(
{ error : err.message, eventName : this.name, action : this.action }, {
'Error performing scheduled event action'); error: err.message,
eventName: this.name,
action: this.action,
},
'Error performing scheduled event action'
);
} }
return cb(err); return cb(err);
}); });
} catch(e) { } catch (e) {
Log.warn( Log.warn(
{ error : e.message, eventName : this.name, action : this.action }, { error: e.message, eventName: this.name, action: this.action },
'Failed to perform scheduled event action'); 'Failed to perform scheduled event action'
);
return cb(e); return cb(e);
} }
} else if('execute' === this.action.type) { } else if ('execute' === this.action.type) {
const opts = { const opts = {
// :TODO: cwd // :TODO: cwd
name : this.name, name: this.name,
cols : 80, cols: 80,
rows : 24, rows: 24,
env : process.env, env: process.env,
}; };
let proc; let proc;
try { try {
proc = pty.spawn(this.action.what, this.action.args, opts); proc = pty.spawn(this.action.what, this.action.args, opts);
} catch(e) { } catch (e) {
Log.warn( Log.warn({
{ error: 'Failed to spawn @execute process',
error : 'Failed to spawn @execute process', reason: e.message,
reason : e.message, eventName: this.name,
eventName : this.name, action: this.action,
action : this.action, what: this.action.what,
what : this.action.what, args: this.action.args,
args : this.action.args });
}
);
return cb(e); return cb(e);
} }
proc.once('exit', exitCode => { proc.once('exit', exitCode => {
if(exitCode) { if (exitCode) {
Log.warn( Log.warn(
{ eventName : this.name, action : this.action, exitCode : exitCode }, { eventName: this.name, action: this.action, exitCode: exitCode },
'Bad exit code while performing scheduled event action'); 'Bad exit code while performing scheduled event action'
);
} }
return cb(exitCode ? Errors.ExternalProcess(`Bad exit code while performing scheduled event action: ${exitCode}`) : null); return cb(
exitCode
? Errors.ExternalProcess(
`Bad exit code while performing scheduled event action: ${exitCode}`
)
: null
);
}); });
} }
} }
@ -172,15 +190,15 @@ function EventSchedulerModule(options) {
PluginModule.call(this, options); PluginModule.call(this, options);
const config = Config(); const config = Config();
if(_.has(config, 'eventScheduler')) { if (_.has(config, 'eventScheduler')) {
this.moduleConfig = config.eventScheduler; this.moduleConfig = config.eventScheduler;
} }
const self = this; const self = this;
this.runningActions = new Set(); this.runningActions = new Set();
this.performAction = function(schedEvent, reason) { this.performAction = function (schedEvent, reason) {
if(self.runningActions.has(schedEvent.name)) { if (self.runningActions.has(schedEvent.name)) {
return; // already running return; // already running
} }
@ -193,80 +211,85 @@ function EventSchedulerModule(options) {
} }
// convienence static method for direct load + start // convienence static method for direct load + start
EventSchedulerModule.loadAndStart = function(cb) { EventSchedulerModule.loadAndStart = function (cb) {
const loadModuleEx = require('./module_util.js').loadModuleEx; const loadModuleEx = require('./module_util.js').loadModuleEx;
const loadOpts = { const loadOpts = {
name : path.basename(__filename, '.js'), name: path.basename(__filename, '.js'),
path : __dirname, path: __dirname,
}; };
loadModuleEx(loadOpts, (err, mod) => { loadModuleEx(loadOpts, (err, mod) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const modInst = new mod.getModule(); const modInst = new mod.getModule();
modInst.startup( err => { modInst.startup(err => {
return cb(err, modInst); return cb(err, modInst);
}); });
}); });
}; };
EventSchedulerModule.prototype.startup = function(cb) { EventSchedulerModule.prototype.startup = function (cb) {
this.eventTimers = [];
this.eventTimers = [];
const self = this; const self = this;
if(this.moduleConfig && _.has(this.moduleConfig, 'events')) { if (this.moduleConfig && _.has(this.moduleConfig, 'events')) {
const events = Object.keys(this.moduleConfig.events).map( name => { const events = Object.keys(this.moduleConfig.events).map(name => {
return new ScheduledEvent(this.moduleConfig.events, name); return new ScheduledEvent(this.moduleConfig.events, name);
}); });
events.forEach( schedEvent => { events.forEach(schedEvent => {
if(!schedEvent.isValid) { if (!schedEvent.isValid) {
Log.warn( { eventName : schedEvent.name }, 'Invalid scheduled event entry'); Log.warn({ eventName: schedEvent.name }, 'Invalid scheduled event entry');
return; return;
} }
Log.debug( Log.debug(
{ {
eventName : schedEvent.name, eventName: schedEvent.name,
schedule : this.moduleConfig.events[schedEvent.name].schedule, schedule: this.moduleConfig.events[schedEvent.name].schedule,
action : schedEvent.action, action: schedEvent.action,
next : schedEvent.schedule.sched ? moment(later.schedule(schedEvent.schedule.sched).next(1)).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', next: schedEvent.schedule.sched
? moment(
later.schedule(schedEvent.schedule.sched).next(1)
).format('ddd, MMM Do, YYYY @ h:m:ss a')
: 'N/A',
}, },
'Scheduled event loaded' 'Scheduled event loaded'
); );
if(schedEvent.schedule.sched) { if (schedEvent.schedule.sched) {
this.eventTimers.push(later.setInterval( () => { this.eventTimers.push(
self.performAction(schedEvent, 'Schedule'); later.setInterval(() => {
}, schedEvent.schedule.sched)); self.performAction(schedEvent, 'Schedule');
}, schedEvent.schedule.sched)
);
} }
if(schedEvent.schedule.watchFile) { if (schedEvent.schedule.watchFile) {
const watcher = sane( const watcher = sane(paths.dirname(schedEvent.schedule.watchFile), {
paths.dirname(schedEvent.schedule.watchFile), glob: `**/${paths.basename(schedEvent.schedule.watchFile)}`,
{ });
glob : `**/${paths.basename(schedEvent.schedule.watchFile)}`
}
);
// :TODO: should track watched files & stop watching @ shutdown? // :TODO: should track watched files & stop watching @ shutdown?
[ 'change', 'add', 'delete' ].forEach(event => { ['change', 'add', 'delete'].forEach(event => {
watcher.on(event, (fileName, fileRoot) => { watcher.on(event, (fileName, fileRoot) => {
const eventPath = paths.join(fileRoot, fileName); const eventPath = paths.join(fileRoot, fileName);
if(schedEvent.schedule.watchFile === eventPath) { if (schedEvent.schedule.watchFile === eventPath) {
self.performAction(schedEvent, `Watch file: ${eventPath}`); self.performAction(schedEvent, `Watch file: ${eventPath}`);
} }
}); });
}); });
fse.exists(schedEvent.schedule.watchFile, exists => { fse.exists(schedEvent.schedule.watchFile, exists => {
if(exists) { if (exists) {
self.performAction(schedEvent, `Watch file: ${schedEvent.schedule.watchFile}`); self.performAction(
schedEvent,
`Watch file: ${schedEvent.schedule.watchFile}`
);
} }
}); });
} }
@ -276,9 +299,9 @@ EventSchedulerModule.prototype.startup = function(cb) {
cb(null); cb(null);
}; };
EventSchedulerModule.prototype.shutdown = function(cb) { EventSchedulerModule.prototype.shutdown = function (cb) {
if(this.eventTimers) { if (this.eventTimers) {
this.eventTimers.forEach( et => et.clear() ); this.eventTimers.forEach(et => et.clear());
} }
cb(null); cb(null);

View File

@ -1,17 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const events = require('events'); const events = require('events');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const SystemEvents = require('./system_events.js'); const SystemEvents = require('./system_events.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
module.exports = new class Events extends events.EventEmitter { module.exports = new (class Events extends events.EventEmitter {
constructor() { constructor() {
super(); super();
this.setMaxListeners(64); // :TODO: play with this... this.setMaxListeners(64); // :TODO: play with this...
} }
getSystemEvents() { getSystemEvents() {
@ -19,22 +19,22 @@ module.exports = new class Events extends events.EventEmitter {
} }
addListener(event, listener) { addListener(event, listener) {
Log.trace( { event : event }, 'Registering event listener'); Log.trace({ event: event }, 'Registering event listener');
return super.addListener(event, listener); return super.addListener(event, listener);
} }
emit(event, ...args) { emit(event, ...args) {
Log.trace( { event : event }, 'Emitting event'); Log.trace({ event: event }, 'Emitting event');
return super.emit(event, ...args); return super.emit(event, ...args);
} }
on(event, listener) { on(event, listener) {
Log.trace( { event : event }, 'Registering event listener'); Log.trace({ event: event }, 'Registering event listener');
return super.on(event, listener); return super.on(event, listener);
} }
once(event, listener) { once(event, listener) {
Log.trace( { event : event }, 'Registering single use event listener'); Log.trace({ event: event }, 'Registering single use event listener');
return super.once(event, listener); return super.once(event, listener);
} }
@ -45,32 +45,32 @@ module.exports = new class Events extends events.EventEmitter {
// The returned object must be used with removeMultipleEventListener() // The returned object must be used with removeMultipleEventListener()
// //
addMultipleEventListener(events, listener) { addMultipleEventListener(events, listener) {
Log.trace( { events }, 'Registering event listeners'); Log.trace({ events }, 'Registering event listeners');
const listeners = []; const listeners = [];
events.forEach(eventName => { events.forEach(eventName => {
const listenWrapper = _.partial(listener, _, eventName); const listenWrapper = _.partial(listener, _, eventName);
this.on(eventName, listenWrapper); this.on(eventName, listenWrapper);
listeners.push( { eventName, listenWrapper } ); listeners.push({ eventName, listenWrapper });
}); });
return listeners; return listeners;
} }
removeMultipleEventListener(listeners) { removeMultipleEventListener(listeners) {
Log.trace( { events }, 'Removing listeners'); Log.trace({ events }, 'Removing listeners');
listeners.forEach(listener => { listeners.forEach(listener => {
this.removeListener(listener.eventName, listener.listenWrapper); this.removeListener(listener.eventName, listener.listenWrapper);
}); });
} }
removeListener(event, listener) { removeListener(event, listener) {
Log.trace( { event : event }, 'Removing listener'); Log.trace({ event: event }, 'Removing listener');
return super.removeListener(event, listener); return super.removeListener(event, listener);
} }
startup(cb) { startup(cb) {
return cb(null); return cb(null);
} }
}; })();

View File

@ -2,29 +2,24 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const { resetScreen } = require('./ansi_term.js'); const { resetScreen } = require('./ansi_term.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const { const { getEnigmaUserAgent } = require('./misc_util.js');
getEnigmaUserAgent const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js');
} = require('./misc_util.js');
const {
trackDoorRunBegin,
trackDoorRunEnd
} = require('./door_util.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const joinPath = require('path').join; const joinPath = require('path').join;
const crypto = require('crypto'); const crypto = require('crypto');
const moment = require('moment'); const moment = require('moment');
const https = require('https'); const https = require('https');
const querystring = require('querystring'); const querystring = require('querystring');
const fs = require('fs-extra'); const fs = require('fs-extra');
const SSHClient = require('ssh2').Client; const SSHClient = require('ssh2').Client;
/* /*
Configuration block: Configuration block:
@ -55,41 +50,47 @@ const SSHClient = require('ssh2').Client;
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Exodus', name: 'Exodus',
desc : 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/', desc: 'Exodus Door Server Access Module - https://oddnetwork.org/exodus/',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class ExodusModule extends MenuModule { exports.getModule = class ExodusModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = options.menuConfig.config || {}; this.config = options.menuConfig.config || {};
this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org'; this.config.ticketHost = this.config.ticketHost || 'oddnetwork.org';
this.config.ticketPort = this.config.ticketPort || 1984, (this.config.ticketPort = this.config.ticketPort || 1984),
this.config.ticketPath = this.config.ticketPath || '/exodus'; (this.config.ticketPath = this.config.ticketPath || '/exodus');
this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true); this.config.rejectUnauthorized = _.get(this.config, 'rejectUnauthorized', true);
this.config.sshHost = this.config.sshHost || this.config.ticketHost; this.config.sshHost = this.config.sshHost || this.config.ticketHost;
this.config.sshPort = this.config.sshPort || 22; this.config.sshPort = this.config.sshPort || 22;
this.config.sshUser = this.config.sshUser || 'exodus_server'; this.config.sshUser = this.config.sshUser || 'exodus_server';
this.config.sshKeyPem = this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa'); this.config.sshKeyPem =
this.config.sshKeyPem || joinPath(Config().paths.misc, 'exodus.id_rsa');
} }
initSequence() { initSequence() {
const self = this;
const self = this; let clientTerminated = false;
let clientTerminated = false;
async.waterfall( async.waterfall(
[ [
function validateConfig(callback) { function validateConfig(callback) {
// very basic validation on optionals // very basic validation on optionals
async.each( [ 'board', 'key', 'door' ], (key, next) => { async.each(
return _.isString(self.config[key]) ? next(null) : next(Errors.MissingConfig(`Config requires "${key}"!`)); ['board', 'key', 'door'],
}, callback); (key, next) => {
return _.isString(self.config[key])
? next(null)
: next(Errors.MissingConfig(`Config requires "${key}"!`));
},
callback
);
}, },
function loadCertAuthorities(callback) { function loadCertAuthorities(callback) {
if(!_.isString(self.config.caPem)) { if (!_.isString(self.config.caPem)) {
return callback(null, null); return callback(null, null);
} }
@ -98,31 +99,34 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
}, },
function getTicket(certAuthorities, callback) { function getTicket(certAuthorities, callback) {
const now = moment.utc().unix(); const now = moment.utc().unix();
const sha256 = crypto.createHash('sha256').update(`${self.config.key}${now}`).digest('hex'); const sha256 = crypto
const token = `${sha256}|${now}`; .createHash('sha256')
.update(`${self.config.key}${now}`)
.digest('hex');
const token = `${sha256}|${now}`;
const postData = querystring.stringify({ const postData = querystring.stringify({
token : token, token: token,
board : self.config.board, board: self.config.board,
user : self.client.user.username, user: self.client.user.username,
door : self.config.door, door: self.config.door,
}); });
const reqOptions = { const reqOptions = {
hostname : self.config.ticketHost, hostname: self.config.ticketHost,
port : self.config.ticketPort, port: self.config.ticketPort,
path : self.config.ticketPath, path: self.config.ticketPath,
rejectUnauthorized : self.config.rejectUnauthorized, rejectUnauthorized: self.config.rejectUnauthorized,
method : 'POST', method: 'POST',
headers : { headers: {
'Content-Type' : 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length' : postData.length, 'Content-Length': postData.length,
'User-Agent' : getEnigmaUserAgent(), 'User-Agent': getEnigmaUserAgent(),
} },
}; };
if(certAuthorities) { if (certAuthorities) {
reqOptions.ca = certAuthorities; reqOptions.ca = certAuthorities;
} }
@ -133,8 +137,10 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
res.on('end', () => { res.on('end', () => {
if(ticket.length !== 36) { if (ticket.length !== 36) {
return callback(Errors.Invalid(`Invalid Exodus ticket: ${ticket}`)); return callback(
Errors.Invalid(`Invalid Exodus ticket: ${ticket}`)
);
} }
return callback(null, ticket); return callback(null, ticket);
@ -154,52 +160,58 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
}, },
function establishSecureConnection(ticket, privateKey, callback) { function establishSecureConnection(ticket, privateKey, callback) {
let pipeRestored = false; let pipeRestored = false;
let pipedStream; let pipedStream;
let doorTracking; let doorTracking;
function restorePipe() { function restorePipe() {
if(pipedStream && !pipeRestored && !clientTerminated) { if (pipedStream && !pipeRestored && !clientTerminated) {
self.client.term.output.unpipe(pipedStream); self.client.term.output.unpipe(pipedStream);
self.client.term.output.resume(); self.client.term.output.resume();
if(doorTracking) { if (doorTracking) {
trackDoorRunEnd(doorTracking); trackDoorRunEnd(doorTracking);
} }
} }
} }
self.client.term.write(resetScreen()); self.client.term.write(resetScreen());
self.client.term.write('Connecting to Exodus server, please wait...\n'); self.client.term.write(
'Connecting to Exodus server, please wait...\n'
);
const sshClient = new SSHClient(); const sshClient = new SSHClient();
const window = { const window = {
rows : self.client.term.termHeight, rows: self.client.term.termHeight,
cols : self.client.term.termWidth, cols: self.client.term.termWidth,
width : 0, width: 0,
height : 0, height: 0,
term : 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :( term: 'vt100', // Want to pass |self.client.term.termClient| here, but we end up getting hung up on :(
}; };
const options = { const options = {
env : { env: {
exodus : ticket, exodus: ticket,
}, },
}; };
sshClient.on('ready', () => { sshClient.on('ready', () => {
self.client.once('end', () => { self.client.once('end', () => {
self.client.log.info('Connection ended. Terminating Exodus connection'); self.client.log.info(
'Connection ended. Terminating Exodus connection'
);
clientTerminated = true; clientTerminated = true;
return sshClient.end(); return sshClient.end();
}); });
sshClient.shell(window, options, (err, stream) => { sshClient.shell(window, options, (err, stream) => {
doorTracking = trackDoorRunBegin(self.client, `exodus_${self.config.door}`); doorTracking = trackDoorRunBegin(
self.client,
`exodus_${self.config.door}`
);
pipedStream = stream; // :TODO: ewwwwwwwww hack pipedStream = stream; // :TODO: ewwwwwwwww hack
self.client.term.output.pipe(stream); self.client.term.output.pipe(stream);
stream.on('data', d => { stream.on('data', d => {
@ -212,7 +224,10 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
stream.on('error', err => { stream.on('error', err => {
Log.warn( { error : err.message }, 'Exodus SSH client stream error'); Log.warn(
{ error: err.message },
'Exodus SSH client stream error'
);
}); });
}); });
}); });
@ -223,19 +238,19 @@ exports.getModule = class ExodusModule extends MenuModule {
}); });
sshClient.connect({ sshClient.connect({
host : self.config.sshHost, host: self.config.sshHost,
port : self.config.sshPort, port: self.config.sshPort,
username : self.config.sshUser, username: self.config.sshUser,
privateKey : privateKey, privateKey: privateKey,
}); });
} },
], ],
err => { err => {
if(err) { if (err) {
self.client.log.warn( { error : err.message }, 'Exodus error'); self.client.log.warn({ error: err.message }, 'Exodus error');
} }
if(!clientTerminated) { if (!clientTerminated) {
self.prevMenu(); self.prevMenu();
} }
} }

View File

@ -2,84 +2,88 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; const getSortedAvailableFileAreas =
const FileBaseFilters = require('./file_base_filter.js'); require('./file_base_area.js').getSortedAvailableFileAreas;
const stringFormat = require('./string_format.js'); const FileBaseFilters = require('./file_base_filter.js');
const UserProps = require('./user_property.js'); const stringFormat = require('./string_format.js');
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area Filter Editor', name: 'File Area Filter Editor',
desc : 'Module for adding, deleting, and modifying file base filters', desc: 'Module for adding, deleting, and modifying file base filters',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
editor : { editor: {
searchTerms : 1, searchTerms: 1,
tags : 2, tags: 2,
area : 3, area: 3,
sort : 4, sort: 4,
order : 5, order: 5,
filterName : 6, filterName: 6,
navMenu : 7, navMenu: 7,
// :TODO: use the customs new standard thing - filter obj can have active/selected, etc. // :TODO: use the customs new standard thing - filter obj can have active/selected, etc.
selectedFilterInfo : 10, // { ...filter object ... } selectedFilterInfo: 10, // { ...filter object ... }
activeFilterInfo : 11, // { ...filter object ... } activeFilterInfo: 11, // { ...filter object ... }
error : 12, // validation errors error: 12, // validation errors
} },
}; };
exports.getModule = class FileAreaFilterEdit extends MenuModule { exports.getModule = class FileAreaFilterEdit extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them this.filtersArray = new FileBaseFilters(this.client).toArray(); // ordered, such that we can index into them
this.currentFilterIndex = 0; // into |filtersArray| this.currentFilterIndex = 0; // into |filtersArray|
// //
// Lexical sort + keep currently active filter (if any) as the first item in |filtersArray| // Lexical sort + keep currently active filter (if any) as the first item in |filtersArray|
// //
const activeFilter = FileBaseFilters.getActiveFilter(this.client); const activeFilter = FileBaseFilters.getActiveFilter(this.client);
this.filtersArray.sort( (filterA, filterB) => { this.filtersArray.sort((filterA, filterB) => {
if(activeFilter) { if (activeFilter) {
if(filterA.uuid === activeFilter.uuid) { if (filterA.uuid === activeFilter.uuid) {
return -1; return -1;
} }
if(filterB.uuid === activeFilter.uuid) { if (filterB.uuid === activeFilter.uuid) {
return 1; return 1;
} }
} }
return filterA.name.localeCompare(filterB.name, { sensitivity : false, numeric : true } ); return filterA.name.localeCompare(filterB.name, {
sensitivity: false,
numeric: true,
});
}); });
this.menuMethods = { this.menuMethods = {
saveFilter : (formData, extraArgs, cb) => { saveFilter: (formData, extraArgs, cb) => {
return this.saveCurrentFilter(formData, cb); return this.saveCurrentFilter(formData, cb);
}, },
prevFilter : (formData, extraArgs, cb) => { prevFilter: (formData, extraArgs, cb) => {
this.currentFilterIndex -= 1; this.currentFilterIndex -= 1;
if(this.currentFilterIndex < 0) { if (this.currentFilterIndex < 0) {
this.currentFilterIndex = this.filtersArray.length - 1; this.currentFilterIndex = this.filtersArray.length - 1;
} }
this.loadDataForFilter(this.currentFilterIndex); this.loadDataForFilter(this.currentFilterIndex);
return cb(null); return cb(null);
}, },
nextFilter : (formData, extraArgs, cb) => { nextFilter: (formData, extraArgs, cb) => {
this.currentFilterIndex += 1; this.currentFilterIndex += 1;
if(this.currentFilterIndex >= this.filtersArray.length) { if (this.currentFilterIndex >= this.filtersArray.length) {
this.currentFilterIndex = 0; this.currentFilterIndex = 0;
} }
this.loadDataForFilter(this.currentFilterIndex); this.loadDataForFilter(this.currentFilterIndex);
return cb(null); return cb(null);
}, },
makeFilterActive : (formData, extraArgs, cb) => { makeFilterActive: (formData, extraArgs, cb) => {
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
filters.setActive(this.filtersArray[this.currentFilterIndex].uuid); filters.setActive(this.filtersArray[this.currentFilterIndex].uuid);
@ -87,45 +91,49 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
return cb(null); return cb(null);
}, },
newFilter : (formData, extraArgs, cb) => { newFilter: (formData, extraArgs, cb) => {
this.currentFilterIndex = this.filtersArray.length; // next avail slot this.currentFilterIndex = this.filtersArray.length; // next avail slot
this.clearForm(MciViewIds.editor.searchTerms); this.clearForm(MciViewIds.editor.searchTerms);
return cb(null); return cb(null);
}, },
deleteFilter : (formData, extraArgs, cb) => { deleteFilter: (formData, extraArgs, cb) => {
const selectedFilter = this.filtersArray[this.currentFilterIndex]; const selectedFilter = this.filtersArray[this.currentFilterIndex];
const filterUuid = selectedFilter.uuid; const filterUuid = selectedFilter.uuid;
// cannot delete built-in/system filters // cannot delete built-in/system filters
if(true === selectedFilter.system) { if (true === selectedFilter.system) {
this.showError('Cannot delete built in filters!'); this.showError('Cannot delete built in filters!');
return cb(null); return cb(null);
} }
this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry this.filtersArray.splice(this.currentFilterIndex, 1); // remove selected entry
// remove from stored properties // remove from stored properties
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
filters.remove(filterUuid); filters.remove(filterUuid);
filters.persist( () => { filters.persist(() => {
// //
// If the item was also the active filter, we need to make a new one active // If the item was also the active filter, we need to make a new one active
// //
if(filterUuid === this.client.user.properties[UserProps.FileBaseFilterActiveUuid]) { if (
filterUuid ===
this.client.user.properties[UserProps.FileBaseFilterActiveUuid]
) {
const newActive = this.filtersArray[this.currentFilterIndex]; const newActive = this.filtersArray[this.currentFilterIndex];
if(newActive) { if (newActive) {
filters.setActive(newActive.uuid); filters.setActive(newActive.uuid);
} else { } else {
// nothing to set active to // nothing to set active to
this.client.user.removeProperty('file_base_filter_active_uuid'); this.client.user.removeProperty(
'file_base_filter_active_uuid'
);
} }
} }
// update UI // update UI
this.updateActiveLabel(); this.updateActiveLabel();
if(this.filtersArray.length > 0) { if (this.filtersArray.length > 0) {
this.loadDataForFilter(this.currentFilterIndex); this.loadDataForFilter(this.currentFilterIndex);
} else { } else {
this.clearForm(); this.clearForm();
@ -134,14 +142,16 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
}); });
}, },
viewValidationListener : (err, cb) => { viewValidationListener: (err, cb) => {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); const errorView = this.viewControllers.editor.getView(
MciViewIds.editor.error
);
let newFocusId; let newFocusId;
if(errorView) { if (errorView) {
if(err) { if (err) {
errorView.setText(err.message); errorView.setText(err.message);
err.view.clearText(); // clear out the invalid data err.view.clearText(); // clear out the invalid data
} else { } else {
errorView.clearText(); errorView.clearText();
} }
@ -154,8 +164,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
showError(errMsg) { showError(errMsg) {
const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error); const errorView = this.viewControllers.editor.getView(MciViewIds.editor.error);
if(errorView) { if (errorView) {
if(errMsg) { if (errMsg) {
errorView.setText(errMsg); errorView.setText(errMsg);
} else { } else {
errorView.clearText(); errorView.clearText();
@ -165,31 +175,39 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.addViewController( 'editor', new ViewController( { client : this.client } ) ); const vc = self.addViewController(
'editor',
new ViewController({ client: this.client })
);
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); return vc.loadFromMenuConfig(
{ callingMenu: self, mciMap: mciData.menu },
callback
);
}, },
function populateAreas(callback) { function populateAreas(callback) {
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); self.availAreas = [{ name: '-ALL-' }].concat(
getSortedAvailableFileAreas(self.client) || []
);
const areasView = vc.getView(MciViewIds.editor.area); const areasView = vc.getView(MciViewIds.editor.area);
if(areasView) { if (areasView) {
areasView.setItems( self.availAreas.map( a => a.name ) ); areasView.setItems(self.availAreas.map(a => a.name));
} }
self.updateActiveLabel(); self.updateActiveLabel();
self.loadDataForFilter(self.currentFilterIndex); self.loadDataForFilter(self.currentFilterIndex);
self.viewControllers.editor.resetInitialFocus(); self.viewControllers.editor.resetInitialFocus();
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -204,36 +222,45 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
setText(mciId, text) { setText(mciId, text) {
const view = this.viewControllers.editor.getView(mciId); const view = this.viewControllers.editor.getView(mciId);
if(view) { if (view) {
view.setText(text); view.setText(text);
} }
} }
updateActiveLabel() { updateActiveLabel() {
const activeFilter = FileBaseFilters.getActiveFilter(this.client); const activeFilter = FileBaseFilters.getActiveFilter(this.client);
if(activeFilter) { if (activeFilter) {
const activeFormat = this.menuConfig.config.activeFormat || '{name}'; const activeFormat = this.menuConfig.config.activeFormat || '{name}';
this.setText(MciViewIds.editor.activeFilterInfo, stringFormat(activeFormat, activeFilter)); this.setText(
MciViewIds.editor.activeFilterInfo,
stringFormat(activeFormat, activeFilter)
);
} }
} }
setFocusItemIndex(mciId, index) { setFocusItemIndex(mciId, index) {
const view = this.viewControllers.editor.getView(mciId); const view = this.viewControllers.editor.getView(mciId);
if(view) { if (view) {
view.setFocusItemIndex(index); view.setFocusItemIndex(index);
} }
} }
clearForm(newFocusId) { clearForm(newFocusId) {
[ MciViewIds.editor.searchTerms, MciViewIds.editor.tags, MciViewIds.editor.filterName ].forEach(mciId => { [
MciViewIds.editor.searchTerms,
MciViewIds.editor.tags,
MciViewIds.editor.filterName,
].forEach(mciId => {
this.setText(mciId, ''); this.setText(mciId, '');
}); });
[ MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort ].forEach(mciId => { [MciViewIds.editor.area, MciViewIds.editor.order, MciViewIds.editor.sort].forEach(
this.setFocusItemIndex(mciId, 0); mciId => {
}); this.setFocusItemIndex(mciId, 0);
}
);
if(newFocusId) { if (newFocusId) {
this.viewControllers.editor.switchFocus(newFocusId); this.viewControllers.editor.switchFocus(newFocusId);
} else { } else {
this.viewControllers.editor.resetInitialFocus(); this.viewControllers.editor.resetInitialFocus();
@ -241,11 +268,11 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
} }
getSelectedAreaTag(index) { getSelectedAreaTag(index) {
if(0 === index) { if (0 === index) {
return ''; // -ALL- return ''; // -ALL-
} }
const area = this.availAreas[index]; const area = this.availAreas[index];
if(!area) { if (!area) {
return ''; return '';
} }
return area.areaTag; return area.areaTag;
@ -258,9 +285,12 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
setAreaIndexFromCurrentFilter() { setAreaIndexFromCurrentFilter() {
let index; let index;
const filter = this.getCurrentFilter(); const filter = this.getCurrentFilter();
if(filter) { if (filter) {
// special treatment: areaTag saved as blank ("") if -ALL- // special treatment: areaTag saved as blank ("") if -ALL-
index = (filter.areaTag && this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) || 0; index =
(filter.areaTag &&
this.availAreas.findIndex(area => filter.areaTag === area.areaTag)) ||
0;
} else { } else {
index = 0; index = 0;
} }
@ -270,8 +300,9 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
setOrderByFromCurrentFilter() { setOrderByFromCurrentFilter() {
let index; let index;
const filter = this.getCurrentFilter(); const filter = this.getCurrentFilter();
if(filter) { if (filter) {
index = FileBaseFilters.OrderByValues.findIndex( ob => filter.order === ob ) || 0; index =
FileBaseFilters.OrderByValues.findIndex(ob => filter.order === ob) || 0;
} else { } else {
index = 0; index = 0;
} }
@ -281,8 +312,8 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
setSortByFromCurrentFilter() { setSortByFromCurrentFilter() {
let index; let index;
const filter = this.getCurrentFilter(); const filter = this.getCurrentFilter();
if(filter) { if (filter) {
index = FileBaseFilters.SortByValues.findIndex( sb => filter.sort === sb ) || 0; index = FileBaseFilters.SortByValues.findIndex(sb => filter.sort === sb) || 0;
} else { } else {
index = 0; index = 0;
} }
@ -294,19 +325,19 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
} }
setFilterValuesFromFormData(filter, formData) { setFilterValuesFromFormData(filter, formData) {
filter.name = formData.value.name; filter.name = formData.value.name;
filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex); filter.areaTag = this.getSelectedAreaTag(formData.value.areaIndex);
filter.terms = formData.value.searchTerms; filter.terms = formData.value.searchTerms;
filter.tags = formData.value.tags; filter.tags = formData.value.tags;
filter.order = this.getOrderBy(formData.value.orderByIndex); filter.order = this.getOrderBy(formData.value.orderByIndex);
filter.sort = this.getSortBy(formData.value.sortByIndex); filter.sort = this.getSortBy(formData.value.sortByIndex);
} }
saveCurrentFilter(formData, cb) { saveCurrentFilter(formData, cb) {
const filters = new FileBaseFilters(this.client); const filters = new FileBaseFilters(this.client);
const selectedFilter = this.filtersArray[this.currentFilterIndex]; const selectedFilter = this.filtersArray[this.currentFilterIndex];
if(selectedFilter) { if (selectedFilter) {
// *update* currently selected filter // *update* currently selected filter
this.setFilterValuesFromFormData(selectedFilter, formData); this.setFilterValuesFromFormData(selectedFilter, formData);
filters.replace(selectedFilter.uuid, selectedFilter); filters.replace(selectedFilter.uuid, selectedFilter);
@ -327,10 +358,10 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
loadDataForFilter(filterIndex) { loadDataForFilter(filterIndex) {
const filter = this.filtersArray[filterIndex]; const filter = this.filtersArray[filterIndex];
if(filter) { if (filter) {
this.setText(MciViewIds.editor.searchTerms, filter.terms); this.setText(MciViewIds.editor.searchTerms, filter.terms);
this.setText(MciViewIds.editor.tags, filter.tags); this.setText(MciViewIds.editor.tags, filter.tags);
this.setText(MciViewIds.editor.filterName, filter.name); this.setText(MciViewIds.editor.filterName, filter.name);
this.setAreaIndexFromCurrentFilter(); this.setAreaIndexFromCurrentFilter();
this.setSortByFromCurrentFilter(); this.setSortByFromCurrentFilter();

File diff suppressed because it is too large Load Diff

View File

@ -2,30 +2,30 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const FileDb = require('./database.js').dbs.file; const FileDb = require('./database.js').dbs.file;
const getISOTimestampString = require('./database.js').getISOTimestampString; const getISOTimestampString = require('./database.js').getISOTimestampString;
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const getServer = require('./listening_server.js').getServer; const getServer = require('./listening_server.js').getServer;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const User = require('./user.js'); const User = require('./user.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId; const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName; const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
const Events = require('./events.js'); const Events = require('./events.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const SysProps = require('./system_menu_method.js'); const SysProps = require('./system_menu_method.js');
// deps // deps
const hashids = require('hashids/cjs'); const hashids = require('hashids/cjs');
const moment = require('moment'); const moment = require('moment');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const mimeTypes = require('mime-types'); const mimeTypes = require('mime-types');
const yazl = require('yazl'); const yazl = require('yazl');
function notEnabledError() { function notEnabledError() {
return Errors.General('Web server is not enabled', ErrNotEnabled); return Errors.General('Web server is not enabled', ErrNotEnabled);
@ -33,8 +33,8 @@ function notEnabledError() {
class FileAreaWebAccess { class FileAreaWebAccess {
constructor() { constructor() {
this.hashids = new hashids(Config().general.boardName); this.hashids = new hashids(Config().general.boardName);
this.expireTimers = {}; // hashId->timer this.expireTimers = {}; // hashId->timer
} }
startup(cb) { startup(cb) {
@ -47,21 +47,27 @@ class FileAreaWebAccess {
}, },
function addWebRoute(callback) { function addWebRoute(callback) {
self.webServer = getServer(webServerPackageName); self.webServer = getServer(webServerPackageName);
if(!self.webServer) { if (!self.webServer) {
return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`)); return callback(
Errors.DoesNotExist(
`Server with package name "${webServerPackageName}" does not exist`
)
);
} }
if(self.isEnabled()) { if (self.isEnabled()) {
const routeAdded = self.webServer.instance.addRoute({ const routeAdded = self.webServer.instance.addRoute({
method : 'GET', method: 'GET',
path : Config().fileBase.web.routePath, path: Config().fileBase.web.routePath,
handler : self.routeWebRequest.bind(self), handler: self.routeWebRequest.bind(self),
}); });
return callback(routeAdded ? null : Errors.General('Failed adding route')); return callback(
routeAdded ? null : Errors.General('Failed adding route')
);
} else { } else {
return callback(null); // not enabled, but no error return callback(null); // not enabled, but no error
} }
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -79,8 +85,8 @@ class FileAreaWebAccess {
static getHashIdTypes() { static getHashIdTypes() {
return { return {
SingleFile : 0, SingleFile: 0,
BatchArchive : 1, BatchArchive: 1,
}; };
} }
@ -92,7 +98,7 @@ class FileAreaWebAccess {
`SELECT hash_id, expire_timestamp `SELECT hash_id, expire_timestamp
FROM file_web_serve;`, FROM file_web_serve;`,
(err, row) => { (err, row) => {
if(row) { if (row) {
this.scheduleExpire(row.hash_id, moment(row.expire_timestamp)); this.scheduleExpire(row.hash_id, moment(row.expire_timestamp));
} }
}, },
@ -109,29 +115,28 @@ class FileAreaWebAccess {
FileDb.run( FileDb.run(
`DELETE FROM file_web_serve `DELETE FROM file_web_serve
WHERE hash_id = ?;`, WHERE hash_id = ?;`,
[ hashId ] [hashId]
); );
delete this.expireTimers[hashId]; delete this.expireTimers[hashId];
} }
scheduleExpire(hashId, expireTime) { scheduleExpire(hashId, expireTime) {
// remove any previous entry for this hashId // remove any previous entry for this hashId
const previous = this.expireTimers[hashId]; const previous = this.expireTimers[hashId];
if(previous) { if (previous) {
clearTimeout(previous); clearTimeout(previous);
delete this.expireTimers[hashId]; delete this.expireTimers[hashId];
} }
const timeoutMs = expireTime.diff(moment()); const timeoutMs = expireTime.diff(moment());
if(timeoutMs <= 0) { if (timeoutMs <= 0) {
setImmediate( () => { setImmediate(() => {
this.removeEntry(hashId); this.removeEntry(hashId);
}); });
} else { } else {
this.expireTimers[hashId] = setTimeout( () => { this.expireTimers[hashId] = setTimeout(() => {
this.removeEntry(hashId); this.removeEntry(hashId);
}, timeoutMs); }, timeoutMs);
} }
@ -142,27 +147,32 @@ class FileAreaWebAccess {
`SELECT expire_timestamp FROM `SELECT expire_timestamp FROM
file_web_serve file_web_serve
WHERE hash_id = ?`, WHERE hash_id = ?`,
[ hashId ], [hashId],
(err, result) => { (err, result) => {
if(err || !result) { if (err || !result) {
return cb(err ? err : Errors.DoesNotExist('Invalid or missing hash ID')); return cb(
err ? err : Errors.DoesNotExist('Invalid or missing hash ID')
);
} }
const decoded = this.hashids.decode(hashId); const decoded = this.hashids.decode(hashId);
// decode() should provide an array of [ userId, hashIdType, id, ... ] // decode() should provide an array of [ userId, hashIdType, id, ... ]
if(!Array.isArray(decoded) || decoded.length < 3) { if (!Array.isArray(decoded) || decoded.length < 3) {
return cb(Errors.Invalid('Invalid or unknown hash ID')); return cb(Errors.Invalid('Invalid or unknown hash ID'));
} }
const servedItem = { const servedItem = {
hashId : hashId, hashId: hashId,
userId : decoded[0], userId: decoded[0],
hashIdType : decoded[1], hashIdType: decoded[1],
expireTimestamp : moment(result.expire_timestamp), expireTimestamp: moment(result.expire_timestamp),
}; };
if(FileAreaWebAccess.getHashIdTypes().SingleFile === servedItem.hashIdType) { if (
FileAreaWebAccess.getHashIdTypes().SingleFile ===
servedItem.hashIdType
) {
servedItem.fileIds = decoded.slice(2); servedItem.fileIds = decoded.slice(2);
} }
@ -172,11 +182,17 @@ class FileAreaWebAccess {
} }
getSingleFileHashId(client, fileEntry) { getSingleFileHashId(client, fileEntry) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [ fileEntry.fileId ] ); return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().SingleFile, [
fileEntry.fileId,
]);
} }
getBatchArchiveHashId(client, batchId) { getBatchArchiveHashId(client, batchId) {
return this.getHashId(client, FileAreaWebAccess.getHashIdTypes().BatchArchive, batchId); return this.getHashId(
client,
FileAreaWebAccess.getHashIdTypes().BatchArchive,
batchId
);
} }
getHashId(client, hashIdType, identifier) { getHashId(client, hashIdType, identifier) {
@ -194,13 +210,13 @@ class FileAreaWebAccess {
} }
getExistingTempDownloadServeItem(client, fileEntry, cb) { getExistingTempDownloadServeItem(client, fileEntry, cb) {
if(!this.isEnabled()) { if (!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const hashId = this.getSingleFileHashId(client, fileEntry); const hashId = this.getSingleFileHashId(client, fileEntry);
this.loadServedHashId(hashId, (err, servedItem) => { this.loadServedHashId(hashId, (err, servedItem) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -215,9 +231,9 @@ class FileAreaWebAccess {
dbOrTrans.run( dbOrTrans.run(
`REPLACE INTO file_web_serve (hash_id, expire_timestamp) `REPLACE INTO file_web_serve (hash_id, expire_timestamp)
VALUES (?, ?);`, VALUES (?, ?);`,
[ hashId, getISOTimestampString(expireTime) ], [hashId, getISOTimestampString(expireTime)],
err => { err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -229,13 +245,13 @@ class FileAreaWebAccess {
} }
createAndServeTempDownload(client, fileEntry, options, cb) { createAndServeTempDownload(client, fileEntry, options, cb) {
if(!this.isEnabled()) { if (!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const hashId = this.getSingleFileHashId(client, fileEntry); const hashId = this.getSingleFileHashId(client, fileEntry);
const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId); const url = this.buildSingleFileTempDownloadLink(client, fileEntry, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days'); options.expireTime = options.expireTime || moment().add(2, 'days');
this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => { this._addOrUpdateHashIdRecord(FileDb, hashId, options.expireTime, err => {
return cb(err, url); return cb(err, url);
@ -243,41 +259,45 @@ class FileAreaWebAccess {
} }
createAndServeTempBatchDownload(client, fileEntries, options, cb) { createAndServeTempBatchDownload(client, fileEntries, options, cb) {
if(!this.isEnabled()) { if (!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
const batchId = moment().utc().unix(); const batchId = moment().utc().unix();
const hashId = this.getBatchArchiveHashId(client, batchId); const hashId = this.getBatchArchiveHashId(client, batchId);
const url = this.buildBatchArchiveTempDownloadLink(client, hashId); const url = this.buildBatchArchiveTempDownloadLink(client, hashId);
options.expireTime = options.expireTime || moment().add(2, 'days'); options.expireTime = options.expireTime || moment().add(2, 'days');
FileDb.beginTransaction( (err, trans) => { FileDb.beginTransaction((err, trans) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => { this._addOrUpdateHashIdRecord(trans, hashId, options.expireTime, err => {
if(err) { if (err) {
return trans.rollback( () => { return trans.rollback(() => {
return cb(err); return cb(err);
}); });
} }
async.eachSeries(fileEntries, (entry, nextEntry) => { async.eachSeries(
trans.run( fileEntries,
`INSERT INTO file_web_serve_batch (hash_id, file_id) (entry, nextEntry) => {
trans.run(
`INSERT INTO file_web_serve_batch (hash_id, file_id)
VALUES (?, ?);`, VALUES (?, ?);`,
[ hashId, entry.fileId ], [hashId, entry.fileId],
err => { err => {
return nextEntry(err); return nextEntry(err);
} }
); );
}, err => { },
trans[err ? 'rollback' : 'commit']( () => { err => {
return cb(err, url); trans[err ? 'rollback' : 'commit'](() => {
}); return cb(err, url);
}); });
}
);
}); });
}); });
} }
@ -289,47 +309,46 @@ class FileAreaWebAccess {
routeWebRequest(req, resp) { routeWebRequest(req, resp) {
const hashId = paths.basename(req.url); const hashId = paths.basename(req.url);
Log.debug( { hashId : hashId, url : req.url }, 'File area web request'); Log.debug({ hashId: hashId, url: req.url }, 'File area web request');
this.loadServedHashId(hashId, (err, servedItem) => { this.loadServedHashId(hashId, (err, servedItem) => {
if (err) {
if(err) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
const hashIdTypes = FileAreaWebAccess.getHashIdTypes(); const hashIdTypes = FileAreaWebAccess.getHashIdTypes();
switch(servedItem.hashIdType) { switch (servedItem.hashIdType) {
case hashIdTypes.SingleFile : case hashIdTypes.SingleFile:
return this.routeWebRequestForSingleFile(servedItem, req, resp); return this.routeWebRequestForSingleFile(servedItem, req, resp);
case hashIdTypes.BatchArchive : case hashIdTypes.BatchArchive:
return this.routeWebRequestForBatchArchive(servedItem, req, resp); return this.routeWebRequestForBatchArchive(servedItem, req, resp);
default : default:
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
}); });
} }
routeWebRequestForSingleFile(servedItem, req, resp) { routeWebRequestForSingleFile(servedItem, req, resp) {
Log.debug( { servedItem : servedItem }, 'Single file web request'); Log.debug({ servedItem: servedItem }, 'Single file web request');
const fileEntry = new FileEntry(); const fileEntry = new FileEntry();
servedItem.fileId = servedItem.fileIds[0]; servedItem.fileId = servedItem.fileIds[0];
fileEntry.load(servedItem.fileId, err => { fileEntry.load(servedItem.fileId, err => {
if(err) { if (err) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
const filePath = fileEntry.filePath; const filePath = fileEntry.filePath;
if(!filePath) { if (!filePath) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
fs.stat(filePath, (err, stats) => { fs.stat(filePath, (err, stats) => {
if(err) { if (err) {
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
@ -340,13 +359,18 @@ class FileAreaWebAccess {
resp.on('finish', () => { resp.on('finish', () => {
// transfer completed fully // transfer completed fully
this.updateDownloadStatsForUserIdAndSystem(servedItem.userId, stats.size, [ fileEntry ]); this.updateDownloadStatsForUserIdAndSystem(
servedItem.userId,
stats.size,
[fileEntry]
);
}); });
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'), 'Content-Type':
'Content-Length' : stats.size, mimeTypes.contentType(filePath) || mimeTypes.contentType('.bin'),
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`, 'Content-Length': stats.size,
'Content-Disposition': `attachment; filename="${fileEntry.fileName}"`,
}; };
const readStream = fs.createReadStream(filePath); const readStream = fs.createReadStream(filePath);
@ -357,7 +381,7 @@ class FileAreaWebAccess {
} }
routeWebRequestForBatchArchive(servedItem, req, resp) { routeWebRequestForBatchArchive(servedItem, req, resp) {
Log.debug( { servedItem : servedItem }, 'Batch file web request'); Log.debug({ servedItem: servedItem }, 'Batch file web request');
// //
// We are going to build an on-the-fly zip file stream of 1:n // We are going to build an on-the-fly zip file stream of 1:n
@ -374,53 +398,80 @@ class FileAreaWebAccess {
`SELECT file_id `SELECT file_id
FROM file_web_serve_batch FROM file_web_serve_batch
WHERE hash_id = ?;`, WHERE hash_id = ?;`,
[ servedItem.hashId ], [servedItem.hashId],
(err, fileIdRows) => { (err, fileIdRows) => {
if(err || !Array.isArray(fileIdRows) || 0 === fileIdRows.length) { if (
return callback(Errors.DoesNotExist('Could not get file IDs for batch')); err ||
!Array.isArray(fileIdRows) ||
0 === fileIdRows.length
) {
return callback(
Errors.DoesNotExist(
'Could not get file IDs for batch'
)
);
} }
return callback(null, fileIdRows.map(r => r.file_id)); return callback(
null,
fileIdRows.map(r => r.file_id)
);
} }
); );
}, },
function loadFileEntries(fileIds, callback) { function loadFileEntries(fileIds, callback) {
async.map(fileIds, (fileId, nextFileId) => { async.map(
const fileEntry = new FileEntry(); fileIds,
fileEntry.load(fileId, err => { (fileId, nextFileId) => {
return nextFileId(err, fileEntry); const fileEntry = new FileEntry();
}); fileEntry.load(fileId, err => {
}, (err, fileEntries) => { return nextFileId(err, fileEntry);
if(err) { });
return callback(Errors.DoesNotExist('Could not load file IDs for batch')); },
} (err, fileEntries) => {
if (err) {
return callback(
Errors.DoesNotExist(
'Could not load file IDs for batch'
)
);
}
return callback(null, fileEntries); return callback(null, fileEntries);
}); }
);
}, },
function createAndServeStream(fileEntries, callback) { function createAndServeStream(fileEntries, callback) {
const filePaths = fileEntries.map(fe => fe.filePath); const filePaths = fileEntries.map(fe => fe.filePath);
Log.trace( { filePaths : filePaths }, 'Creating zip archive for batch web request'); Log.trace(
{ filePaths: filePaths },
'Creating zip archive for batch web request'
);
const zipFile = new yazl.ZipFile(); const zipFile = new yazl.ZipFile();
zipFile.on('error', err => { zipFile.on('error', err => {
Log.warn( { error : err.message }, 'Error adding file to batch web request archive'); Log.warn(
{ error: err.message },
'Error adding file to batch web request archive'
);
}); });
filePaths.forEach(fp => { filePaths.forEach(fp => {
zipFile.addFile( zipFile.addFile(
fp, // path to physical file fp, // path to physical file
paths.basename(fp), // filename/path *stored in archive* paths.basename(fp), // filename/path *stored in archive*
{ {
compress : false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us. compress: false, // :TODO: do this smartly - if ext is in set = false, else true via isArchive() or such... mimeDB has this for us.
} }
); );
}); });
zipFile.end( finalZipSize => { zipFile.end(finalZipSize => {
if(-1 === finalZipSize) { if (-1 === finalZipSize) {
return callback(Errors.UnexpectedState('Unable to acquire final zip size')); return callback(
Errors.UnexpectedState('Unable to acquire final zip size')
);
} }
resp.on('close', () => { resp.on('close', () => {
@ -430,24 +481,30 @@ class FileAreaWebAccess {
resp.on('finish', () => { resp.on('finish', () => {
// transfer completed fully // transfer completed fully
self.updateDownloadStatsForUserIdAndSystem(servedItem.userId, finalZipSize, fileEntries); self.updateDownloadStatsForUserIdAndSystem(
servedItem.userId,
finalZipSize,
fileEntries
);
}); });
const batchFileName = `batch_${servedItem.hashId}.zip`; const batchFileName = `batch_${servedItem.hashId}.zip`;
const headers = { const headers = {
'Content-Type' : mimeTypes.contentType(batchFileName) || mimeTypes.contentType('.bin'), 'Content-Type':
'Content-Length' : finalZipSize, mimeTypes.contentType(batchFileName) ||
'Content-Disposition' : `attachment; filename="${batchFileName}"`, mimeTypes.contentType('.bin'),
'Content-Length': finalZipSize,
'Content-Disposition': `attachment; filename="${batchFileName}"`,
}; };
resp.writeHead(200, headers); resp.writeHead(200, headers);
return zipFile.outputStream.pipe(resp); return zipFile.outputStream.pipe(resp);
}); });
} },
], ],
err => { err => {
if(err) { if (err) {
// :TODO: Log me! // :TODO: Log me!
return this.fileNotFound(resp); return this.fileNotFound(resp);
} }
@ -458,41 +515,36 @@ class FileAreaWebAccess {
} }
updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) { updateDownloadStatsForUserIdAndSystem(userId, dlBytes, fileEntries) {
async.waterfall( async.waterfall([
[ function fetchActiveUser(callback) {
function fetchActiveUser(callback) { const clientForUserId = getConnectionByUserId(userId);
const clientForUserId = getConnectionByUserId(userId); if (clientForUserId) {
if(clientForUserId) { return callback(null, clientForUserId.user);
return callback(null, clientForUserId.user);
}
// not online now - look 'em up
User.getUser(userId, (err, assocUser) => {
return callback(err, assocUser);
});
},
function updateStats(user, callback) {
StatLog.incrementUserStat(user, UserProps.FileDlTotalCount, 1);
StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes);
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
return callback(null, user);
},
function sendEvent(user, callback) {
Events.emit(
Events.getSystemEvents().UserDownload,
{
user : user,
files : fileEntries,
}
);
return callback(null);
} }
]
); // not online now - look 'em up
User.getUser(userId, (err, assocUser) => {
return callback(err, assocUser);
});
},
function updateStats(user, callback) {
StatLog.incrementUserStat(user, UserProps.FileDlTotalCount, 1);
StatLog.incrementUserStat(user, UserProps.FileDlTotalBytes, dlBytes);
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, 1);
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, dlBytes);
return callback(null, user);
},
function sendEvent(user, callback) {
Events.emit(Events.getSystemEvents().UserDownload, {
user: user,
files: fileEntries,
});
return callback(null);
},
]);
} }
} }
module.exports = new FileAreaWebAccess(); module.exports = new FileAreaWebAccess();

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,22 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { getSortedAvailableFileAreas } = require('./file_base_area.js'); const { getSortedAvailableFileAreas } = require('./file_base_area.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const SysProps = require('./system_property.js'); const SysProps = require('./system_property.js');
// deps // deps
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area Selector', name: 'File Area Selector',
desc : 'Select from available file areas', desc: 'Select from available file areas',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
areaList : 1, areaList: 1,
}; };
exports.getModule = class FileAreaSelectModule extends MenuModule { exports.getModule = class FileAreaSelectModule extends MenuModule {
@ -25,26 +25,31 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
super(options); super(options);
this.menuMethods = { this.menuMethods = {
selectArea : (formData, extraArgs, cb) => { selectArea: (formData, extraArgs, cb) => {
const filterCriteria = { const filterCriteria = {
areaTag : formData.value.areaTag, areaTag: formData.value.areaTag,
}; };
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs: {
filterCriteria : filterCriteria, filterCriteria: filterCriteria,
}, },
menuFlags : [ 'popParent', 'mergeFlags' ], menuFlags: ['popParent', 'mergeFlags'],
}; };
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); return this.gotoMenu(
} this.menuConfig.config.fileBaseListEntriesMenu ||
'fileBaseListEntries',
menuOpts,
cb
);
},
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -53,7 +58,9 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
async.waterfall( async.waterfall(
[ [
function mergeAreaStats(callback) { function mergeAreaStats(callback) {
const areaStats = StatLog.getSystemStat(SysProps.FileBaseAreaStats) || { areas : {} }; const areaStats = StatLog.getSystemStat(
SysProps.FileBaseAreaStats
) || { areas: {} };
// we could use 'sort' alone, but area/conf sorting has some special properties; user can still override // we could use 'sort' alone, but area/conf sorting has some special properties; user can still override
const availAreas = getSortedAvailableFileAreas(self.client); const availAreas = getSortedAvailableFileAreas(self.client);
@ -66,18 +73,30 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
return callback(null, availAreas); return callback(null, availAreas);
}, },
function prepView(availAreas, callback) { function prepView(availAreas, callback) {
self.prepViewController('allViews', 0, mciData.menu, (err, vc) => { self.prepViewController(
if(err) { 'allViews',
return callback(err); 0,
mciData.menu,
(err, vc) => {
if (err) {
return callback(err);
}
const areaListView = vc.getView(MciViewIds.areaList);
areaListView.setItems(
availAreas.map(area =>
Object.assign(area, {
text: area.name,
data: area.areaTag,
})
)
);
areaListView.redraw();
return callback(null);
} }
);
const areaListView = vc.getView(MciViewIds.areaList); },
areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } )));
areaListView.redraw();
return callback(null);
});
}
], ],
err => { err => {
return cb(err); return cb(err);

View File

@ -2,91 +2,101 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const FileAreaWeb = require('./file_area_web.js'); const FileAreaWeb = require('./file_area_web.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Download Queue Manager', name: 'File Base Download Queue Manager',
desc : 'Module for interacting with download queue/batch', desc: 'Module for interacting with download queue/batch',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const FormIds = { const FormIds = {
queueManager : 0, queueManager: 0,
}; };
const MciViewIds = { const MciViewIds = {
queueManager : { queueManager: {
queue : 1, queue: 1,
navMenu : 2, navMenu: 2,
customRangeStart : 10, customRangeStart: 10,
}, },
}; };
exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
if(_.has(options, 'lastMenuResult.sentFileIds')) { if (_.has(options, 'lastMenuResult.sentFileIds')) {
this.sentFileIds = options.lastMenuResult.sentFileIds; this.sentFileIds = options.lastMenuResult.sentFileIds;
} }
this.fallbackOnly = options.lastMenuResult ? true : false; this.fallbackOnly = options.lastMenuResult ? true : false;
this.menuMethods = { this.menuMethods = {
downloadAll : (formData, extraArgs, cb) => { downloadAll: (formData, extraArgs, cb) => {
const modOpts = { const modOpts = {
extraArgs : { extraArgs: {
sendQueue : this.dlQueue.items, sendQueue: this.dlQueue.items,
direction : 'send', direction: 'send',
} },
}; };
return this.gotoMenu(this.menuConfig.config.fileTransferProtocolSelection || 'fileTransferProtocolSelection', modOpts, cb); return this.gotoMenu(
this.menuConfig.config.fileTransferProtocolSelection ||
'fileTransferProtocolSelection',
modOpts,
cb
);
}, },
removeItem : (formData, extraArgs, cb) => { removeItem: (formData, extraArgs, cb) => {
const selectedItem = this.dlQueue.items[formData.value.queueItem]; const selectedItem = this.dlQueue.items[formData.value.queueItem];
if(!selectedItem) { if (!selectedItem) {
return cb(null); return cb(null);
} }
this.dlQueue.removeItems(selectedItem.fileId); this.dlQueue.removeItems(selectedItem.fileId);
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); return this.removeItemsFromDownloadQueueView(
formData.value.queueItem,
cb
);
}, },
clearQueue : (formData, extraArgs, cb) => { clearQueue: (formData, extraArgs, cb) => {
this.dlQueue.clear(); this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb); return this.removeItemsFromDownloadQueueView('all', cb);
} },
}; };
} }
initSequence() { initSequence() {
if(0 === this.dlQueue.items.length) { if (0 === this.dlQueue.items.length) {
if(this.sendFileIds) { if (this.sendFileIds) {
// we've finished everything up - just fall back // we've finished everything up - just fall back
return this.prevMenu(); return this.prevMenu();
} }
// Simply an empty D/L queue: Present a specialized "empty queue" page // Simply an empty D/L queue: Present a specialized "empty queue" page
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); return this.gotoMenu(
this.menuConfig.config.emptyQueueMenu ||
'fileBaseDownloadManagerEmptyQueue'
);
} }
const self = this; const self = this;
@ -98,7 +108,7 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
}, },
function display(callback) { function display(callback) {
return self.displayQueueManagerPage(false, callback); return self.displayQueueManagerPage(false, callback);
} },
], ],
() => { () => {
return self.finishedLoading(); return self.finishedLoading();
@ -107,12 +117,14 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
} }
removeItemsFromDownloadQueueView(itemIndex, cb) { removeItemsFromDownloadQueueView(itemIndex, cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(
if(!queueView) { MciViewIds.queueManager.queue
);
if (!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
if('all' === itemIndex) { if ('all' === itemIndex) {
queueView.setItems([]); queueView.setItems([]);
queueView.setFocusItems([]); queueView.setFocusItems([]);
} else { } else {
@ -124,28 +136,40 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
} }
displayWebDownloadLinkForFileEntry(fileEntry) { displayWebDownloadLinkForFileEntry(fileEntry) {
FileAreaWeb.getExistingTempDownloadServeItem(this.client, fileEntry, (err, serveItem) => { FileAreaWeb.getExistingTempDownloadServeItem(
if(serveItem && serveItem.url) { this.client,
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; fileEntry,
(err, serveItem) => {
if (serveItem && serveItem.url) {
const webDlExpireTimeFormat =
this.menuConfig.config.webDlExpireTimeFormat ||
'YYYY-MMM-DD @ h:mm';
fileEntry.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; fileEntry.webDlLink =
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
} else { fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(
fileEntry.webDlLink = ''; webDlExpireTimeFormat
fileEntry.webDlExpire = ''; );
} else {
fileEntry.webDlLink = '';
fileEntry.webDlExpire = '';
}
this.updateCustomViewTextsWithFilter(
'queueManager',
MciViewIds.queueManager.customRangeStart,
fileEntry,
{ filter: ['{webDlLink}', '{webDlExpire}'] }
);
} }
);
this.updateCustomViewTextsWithFilter(
'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry,
{ filter : [ '{webDlLink}', '{webDlExpire}' ] }
);
});
} }
updateDownloadQueueView(cb) { updateDownloadQueueView(cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(
if(!queueView) { MciViewIds.queueManager.queue
);
if (!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
@ -168,14 +192,18 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
async.series( async.series(
[ [
function prepArtAndViewController(callback) { function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); return self.displayArtAndPrepViewController(
'queueManager',
{ clearScreen: clearScreen },
callback
);
}, },
function populateViews(callback) { function populateViews(callback) {
return self.updateDownloadQueueView(callback); return self.updateDownloadQueueView(callback);
} },
], ],
err => { err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
} }
@ -183,42 +211,45 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
async.waterfall( async.waterfall(
[ [
function readyAndDisplayArt(callback) { function readyAndDisplayArt(callback) {
if(options.clearScreen) { if (options.clearScreen) {
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
} }
theme.displayThemedAsset( theme.displayThemedAsset(
config.art[name], config.art[name],
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font: self.menuConfig.font, trailingLF: false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function prepeareViewController(artData, callback) { function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) { if (_.isUndefined(self.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : self.client, client: self.client,
formId : FormIds[name], formId: FormIds[name],
}; };
if(!_.isUndefined(options.noInput)) { if (!_.isUndefined(options.noInput)) {
vcOpts.noInput = options.noInput; vcOpts.noInput = options.noInput;
} }
const vc = self.addViewController(name, new ViewController(vcOpts)); const vc = self.addViewController(
name,
new ViewController(vcOpts)
);
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu: self,
mciMap : artData.mciMap, mciMap: artData.mciMap,
formId : FormIds[name], formId: FormIds[name],
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -226,7 +257,6 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
self.viewControllers[name].setFocus(true); self.viewControllers[name].setFocus(true);
return callback(null); return callback(null);
}, },
], ],
err => { err => {

View File

@ -4,8 +4,8 @@
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const { v4 : UUIDv4 } = require('uuid'); const { v4: UUIDv4 } = require('uuid');
module.exports = class FileBaseFilters { module.exports = class FileBaseFilters {
constructor(client) { constructor(client) {
@ -15,7 +15,7 @@ module.exports = class FileBaseFilters {
} }
static get OrderByValues() { static get OrderByValues() {
return [ 'descending', 'ascending' ]; return ['descending', 'ascending'];
} }
static get SortByValues() { static get SortByValues() {
@ -32,7 +32,7 @@ module.exports = class FileBaseFilters {
toArray() { toArray() {
return _.map(this.filters, (filter, uuid) => { return _.map(this.filters, (filter, uuid) => {
return Object.assign( { uuid : uuid }, filter ); return Object.assign({ uuid: uuid }, filter);
}); });
} }
@ -52,7 +52,7 @@ module.exports = class FileBaseFilters {
replace(filterUuid, filterInfo) { replace(filterUuid, filterInfo) {
const filter = this.get(filterUuid); const filter = this.get(filterUuid);
if(!filter) { if (!filter) {
return false; return false;
} }
@ -68,22 +68,25 @@ module.exports = class FileBaseFilters {
load() { load() {
let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters]; let filtersProperty = this.client.user.properties[UserProps.FileBaseFilters];
let defaulted; let defaulted;
if(!filtersProperty) { if (!filtersProperty) {
filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters()); filtersProperty = JSON.stringify(FileBaseFilters.getBuiltInSystemFilters());
defaulted = true; defaulted = true;
} }
try { try {
this.filters = JSON.parse(filtersProperty); this.filters = JSON.parse(filtersProperty);
} catch(e) { } catch (e) {
this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :( this.filters = FileBaseFilters.getBuiltInSystemFilters(); // something bad happened; reset everything back to defaults :(
defaulted = true; defaulted = true;
this.client.log.error( { error : e.message, property : filtersProperty }, 'Failed parsing file base filters property' ); this.client.log.error(
{ error: e.message, property: filtersProperty },
'Failed parsing file base filters property'
);
} }
if(defaulted) { if (defaulted) {
this.persist( err => { this.persist(err => {
if(!err) { if (!err) {
const defaultActiveUuid = this.toArray()[0].uuid; const defaultActiveUuid = this.toArray()[0].uuid;
this.setActive(defaultActiveUuid); this.setActive(defaultActiveUuid);
} }
@ -92,19 +95,29 @@ module.exports = class FileBaseFilters {
} }
persist(cb) { persist(cb) {
return this.client.user.persistProperty(UserProps.FileBaseFilters, JSON.stringify(this.filters), cb); return this.client.user.persistProperty(
UserProps.FileBaseFilters,
JSON.stringify(this.filters),
cb
);
} }
cleanTags(tags) { cleanTags(tags) {
return tags.toLowerCase().replace(/,?\s+|,/g, ' ').trim(); return tags
.toLowerCase()
.replace(/,?\s+|,/g, ' ')
.trim();
} }
setActive(filterUuid) { setActive(filterUuid) {
const activeFilter = this.get(filterUuid); const activeFilter = this.get(filterUuid);
if(activeFilter) { if (activeFilter) {
this.activeFilter = activeFilter; this.activeFilter = activeFilter;
this.client.user.persistProperty(UserProps.FileBaseFilterActiveUuid, filterUuid); this.client.user.persistProperty(
UserProps.FileBaseFilterActiveUuid,
filterUuid
);
return true; return true;
} }
@ -112,41 +125,43 @@ module.exports = class FileBaseFilters {
} }
static getBuiltInSystemFilters() { static getBuiltInSystemFilters() {
const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329'; const U_LATEST = '7458b09d-40ab-4f9b-a0d7-0cf866646329';
const filters = { const filters = {
[ U_LATEST ] : { [U_LATEST]: {
name : 'By Date Added', name: 'By Date Added',
areaTag : '', // all areaTag: '', // all
terms : '', // * terms: '', // *
tags : '', // * tags: '', // *
order : 'descending', order: 'descending',
sort : 'upload_timestamp', sort: 'upload_timestamp',
uuid : U_LATEST, uuid: U_LATEST,
system : true, system: true,
} },
}; };
return filters; return filters;
} }
static getActiveFilter(client) { static getActiveFilter(client) {
return new FileBaseFilters(client).get(client.user.properties[UserProps.FileBaseFilterActiveUuid]); return new FileBaseFilters(client).get(
client.user.properties[UserProps.FileBaseFilterActiveUuid]
);
} }
static getFileBaseLastViewedFileIdByUser(user) { static getFileBaseLastViewedFileIdByUser(user) {
return parseInt((user.properties[UserProps.FileBaseLastViewedId] || 0)); return parseInt(user.properties[UserProps.FileBaseLastViewedId] || 0);
} }
static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) { static setFileBaseLastViewedFileIdForUser(user, fileId, allowOlder, cb) {
if(!cb && _.isFunction(allowOlder)) { if (!cb && _.isFunction(allowOlder)) {
cb = allowOlder; cb = allowOlder;
allowOlder = false; allowOlder = false;
} }
const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user); const current = FileBaseFilters.getFileBaseLastViewedFileIdByUser(user);
if(!allowOlder && fileId < current) { if (!allowOlder && fileId < current) {
if(cb) { if (cb) {
cb(null); cb(null);
} }
return; return;

View File

@ -2,235 +2,283 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const FileArea = require('./file_base_area.js'); const FileArea = require('./file_base_area.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const { const { splitTextAtTerms, isAnsi } = require('./string_util.js');
splitTextAtTerms, const AnsiPrep = require('./ansi_prep.js');
isAnsi, const Log = require('./logger.js').log;
} = require('./string_util.js');
const AnsiPrep = require('./ansi_prep.js');
const Log = require('./logger.js').log;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const moment = require('moment'); const moment = require('moment');
exports.exportFileList = exportFileList; exports.exportFileList = exportFileList;
exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent; exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesScheduledEvent;
function exportFileList(filterCriteria, options, cb) { function exportFileList(filterCriteria, options, cb) {
options.templateEncoding = options.templateEncoding || 'utf8'; options.templateEncoding = options.templateEncoding || 'utf8';
options.entryTemplate = options.entryTemplate || 'descript_ion_export_entry_template.asc'; options.entryTemplate =
options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; options.entryTemplate || 'descript_ion_export_entry_template.asc';
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc?
if(true === options.escapeDesc) { if (true === options.escapeDesc) {
options.escapeDesc = '\\n'; options.escapeDesc = '\\n';
} }
const state = { const state = {
total : 0, total: 0,
current : 0, current: 0,
step : 'preparing', step: 'preparing',
status : 'Preparing', status: 'Preparing',
}; };
const updateProgress = _.isFunction(options.progress) ? const updateProgress = _.isFunction(options.progress)
progCb => { ? progCb => {
return options.progress(state, progCb); return options.progress(state, progCb);
} : }
progCb => { : progCb => {
return progCb(null); return progCb(null);
} };
;
async.waterfall( async.waterfall(
[ [
function readTemplateFiles(callback) { function readTemplateFiles(callback) {
updateProgress(err => { updateProgress(err => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
const templateFiles = [ const templateFiles = [
{ name : options.headerTemplate, req : false }, { name: options.headerTemplate, req: false },
{ name : options.entryTemplate, req : true } { name: options.entryTemplate, req: true },
]; ];
const config = Config(); const config = Config();
async.map(templateFiles, (template, nextTemplate) => { async.map(
if(!template.name && !template.req) { templateFiles,
return nextTemplate(null, Buffer.from([])); (template, nextTemplate) => {
} if (!template.name && !template.req) {
return nextTemplate(null, Buffer.from([]));
}
template.name = paths.isAbsolute(template.name) ? template.name : paths.join(config.paths.misc, template.name); template.name = paths.isAbsolute(template.name)
fs.readFile(template.name, (err, data) => { ? template.name
return nextTemplate(err, data); : paths.join(config.paths.misc, template.name);
}); fs.readFile(template.name, (err, data) => {
}, (err, templates) => { return nextTemplate(err, data);
if(err) {
return callback(Errors.General(err.message));
}
// decode + ensure DOS style CRLF
templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') );
// Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
let descIndent = 0;
if(!options.escapeDesc) {
splitTextAtTerms(templates[1]).some(line => {
const pos = line.indexOf('{fileDesc}');
if(pos > -1) {
descIndent = pos;
return true; // found it!
}
return false; // keep looking
}); });
} },
(err, templates) => {
if (err) {
return callback(Errors.General(err.message));
}
return callback(null, templates[0], templates[1], descIndent); // decode + ensure DOS style CRLF
}); templates = templates.map(tmp =>
iconv
.decode(tmp, options.templateEncoding)
.replace(/\r?\n/g, '\r\n')
);
// Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
let descIndent = 0;
if (!options.escapeDesc) {
splitTextAtTerms(templates[1]).some(line => {
const pos = line.indexOf('{fileDesc}');
if (pos > -1) {
descIndent = pos;
return true; // found it!
}
return false; // keep looking
});
}
return callback(null, templates[0], templates[1], descIndent);
}
);
}); });
}, },
function findFiles(headerTemplate, entryTemplate, descIndent, callback) { function findFiles(headerTemplate, entryTemplate, descIndent, callback) {
state.step = 'gathering'; state.step = 'gathering';
state.status = 'Gathering files for supplied criteria'; state.status = 'Gathering files for supplied criteria';
updateProgress(err => { updateProgress(err => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
FileEntry.findFiles(filterCriteria, (err, fileIds) => { FileEntry.findFiles(filterCriteria, (err, fileIds) => {
if(0 === fileIds.length) { if (0 === fileIds.length) {
return callback(Errors.General('No results for criteria', 'NORESULTS')); return callback(
Errors.General('No results for criteria', 'NORESULTS')
);
} }
return callback(err, headerTemplate, entryTemplate, descIndent, fileIds); return callback(
err,
headerTemplate,
entryTemplate,
descIndent,
fileIds
);
}); });
}); });
}, },
function buildListEntries(headerTemplate, entryTemplate, descIndent, fileIds, callback) { function buildListEntries(
headerTemplate,
entryTemplate,
descIndent,
fileIds,
callback
) {
const formatObj = { const formatObj = {
totalFileCount : fileIds.length, totalFileCount: fileIds.length,
}; };
let current = 0; let current = 0;
let listBody = ''; let listBody = '';
const totals = { fileCount : fileIds.length, bytes : 0 }; const totals = { fileCount: fileIds.length, bytes: 0 };
state.total = fileIds.length; state.total = fileIds.length;
state.step = 'file'; state.step = 'file';
async.eachSeries(fileIds, (fileId, nextFileId) => { async.eachSeries(
const fileInfo = new FileEntry(); fileIds,
current += 1; (fileId, nextFileId) => {
const fileInfo = new FileEntry();
current += 1;
fileInfo.load(fileId, err => { fileInfo.load(fileId, err => {
if(err) { if (err) {
return nextFileId(null); // failed, but try the next return nextFileId(null); // failed, but try the next
}
totals.bytes += fileInfo.meta.byte_size;
const appendFileInfo = () => {
if(options.escapeDesc) {
formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc);
} }
if(options.maxDescLen) { totals.bytes += fileInfo.meta.byte_size;
formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen);
}
listBody += stringFormat(entryTemplate, formatObj); const appendFileInfo = () => {
if (options.escapeDesc) {
state.current = current; formatObj.fileDesc = formatObj.fileDesc.replace(
state.status = `Processing ${fileInfo.fileName}`; /\r?\n/g,
state.fileInfo = formatObj; options.escapeDesc
);
updateProgress(err => {
return nextFileId(err);
});
};
const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
formatObj.fileId = fileId;
formatObj.areaName = _.get(area, 'name') || 'N/A';
formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
formatObj.userRating = fileInfo.userRating || 0;
formatObj.fileName = fileInfo.fileName;
formatObj.fileSize = fileInfo.meta.byte_size;
formatObj.fileDesc = fileInfo.desc || '';
formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth);
formatObj.fileSha256 = fileInfo.fileSha256;
formatObj.fileCrc32 = fileInfo.meta.file_crc32;
formatObj.fileMd5 = fileInfo.meta.file_md5;
formatObj.fileSha1 = fileInfo.meta.file_sha1;
formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A';
formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat);
formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : 'N/A';
formatObj.currentFile = current;
formatObj.progress = Math.floor( (current / fileIds.length) * 100 );
if(isAnsi(fileInfo.desc)) {
AnsiPrep(
fileInfo.desc,
{
cols : Math.min(options.descWidth, 79 - descIndent),
forceLineTerm : true, // ensure each line is term'd
asciiMode : true, // export to ASCII
fillLines : false, // don't fill up to |cols|
indent : descIndent,
},
(err, desc) => {
if(desc) {
formatObj.fileDesc = desc;
}
return appendFileInfo();
} }
if (options.maxDescLen) {
formatObj.fileDesc = formatObj.fileDesc.slice(
0,
options.maxDescLen
);
}
listBody += stringFormat(entryTemplate, formatObj);
state.current = current;
state.status = `Processing ${fileInfo.fileName}`;
state.fileInfo = formatObj;
updateProgress(err => {
return nextFileId(err);
});
};
const area = FileArea.getFileAreaByTag(fileInfo.areaTag);
formatObj.fileId = fileId;
formatObj.areaName = _.get(area, 'name') || 'N/A';
formatObj.areaDesc = _.get(area, 'desc') || 'N/A';
formatObj.userRating = fileInfo.userRating || 0;
formatObj.fileName = fileInfo.fileName;
formatObj.fileSize = fileInfo.meta.byte_size;
formatObj.fileDesc = fileInfo.desc || '';
formatObj.fileDescShort = formatObj.fileDesc.slice(
0,
options.descWidth
); );
} else { formatObj.fileSha256 = fileInfo.fileSha256;
const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; formatObj.fileCrc32 = fileInfo.meta.file_crc32;
formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; formatObj.fileMd5 = fileInfo.meta.file_md5;
return appendFileInfo(); formatObj.fileSha1 = fileInfo.meta.file_sha1;
} formatObj.uploadBy =
}); fileInfo.meta.upload_by_username || 'N/A';
}, err => { formatObj.fileUploadTs = moment(
return callback(err, listBody, headerTemplate, totals); fileInfo.uploadTimestamp
}); ).format(options.tsFormat);
formatObj.fileHashTags =
fileInfo.hashTags.size > 0
? Array.from(fileInfo.hashTags).join(', ')
: 'N/A';
formatObj.currentFile = current;
formatObj.progress = Math.floor(
(current / fileIds.length) * 100
);
if (isAnsi(fileInfo.desc)) {
AnsiPrep(
fileInfo.desc,
{
cols: Math.min(
options.descWidth,
79 - descIndent
),
forceLineTerm: true, // ensure each line is term'd
asciiMode: true, // export to ASCII
fillLines: false, // don't fill up to |cols|
indent: descIndent,
},
(err, desc) => {
if (desc) {
formatObj.fileDesc = desc;
}
return appendFileInfo();
}
);
} else {
const indentSpc =
descIndent > 0 ? ' '.repeat(descIndent) : '';
formatObj.fileDesc =
splitTextAtTerms(formatObj.fileDesc).join(
`\r\n${indentSpc}`
) + '\r\n';
return appendFileInfo();
}
});
},
err => {
return callback(err, listBody, headerTemplate, totals);
}
);
}, },
function buildHeader(listBody, headerTemplate, totals, callback) { function buildHeader(listBody, headerTemplate, totals, callback) {
// header is built last such that we can have totals/etc. // header is built last such that we can have totals/etc.
let filterAreaName; let filterAreaName;
let filterAreaDesc; let filterAreaDesc;
if(filterCriteria.areaTag) { if (filterCriteria.areaTag) {
const area = FileArea.getFileAreaByTag(filterCriteria.areaTag); const area = FileArea.getFileAreaByTag(filterCriteria.areaTag);
filterAreaName = _.get(area, 'name') || 'N/A'; filterAreaName = _.get(area, 'name') || 'N/A';
filterAreaDesc = _.get(area, 'desc') || 'N/A'; filterAreaDesc = _.get(area, 'desc') || 'N/A';
} else { } else {
filterAreaName = '-ALL-'; filterAreaName = '-ALL-';
filterAreaDesc = 'All areas'; filterAreaDesc = 'All areas';
} }
const headerFormatObj = { const headerFormatObj = {
nowTs : moment().format(options.tsFormat), nowTs: moment().format(options.tsFormat),
boardName : Config().general.boardName, boardName: Config().general.boardName,
totalFileCount : totals.fileCount, totalFileCount: totals.fileCount,
totalFileSize : totals.bytes, totalFileSize: totals.bytes,
filterAreaTag : filterCriteria.areaTag || '-ALL-', filterAreaTag: filterCriteria.areaTag || '-ALL-',
filterAreaName : filterAreaName, filterAreaName: filterAreaName,
filterAreaDesc : filterAreaDesc, filterAreaDesc: filterAreaDesc,
filterTerms : filterCriteria.terms || '(none)', filterTerms: filterCriteria.terms || '(none)',
filterHashTags : filterCriteria.tags || '(none)', filterHashTags: filterCriteria.tags || '(none)',
}; };
listBody = stringFormat(headerTemplate, headerFormatObj) + listBody; listBody = stringFormat(headerTemplate, headerFormatObj) + listBody;
@ -238,13 +286,14 @@ function exportFileList(filterCriteria, options, cb) {
}, },
function done(listBody, callback) { function done(listBody, callback) {
delete state.fileInfo; delete state.fileInfo;
state.step = 'finished'; state.step = 'finished';
state.status = 'Finished processing'; state.status = 'Finished processing';
updateProgress( () => { updateProgress(() => {
return callback(null, listBody); return callback(null, listBody);
}); });
} },
], (err, listBody) => { ],
(err, listBody) => {
return cb(err, listBody); return cb(err, listBody);
} }
); );
@ -260,42 +309,59 @@ function updateFileBaseDescFilesScheduledEvent(args, cb) {
// * Multi line descriptions are stored with *escaped* \r\n pairs // * Multi line descriptions are stored with *escaped* \r\n pairs
// * Default template uses 0x2c for <AppData> as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec // * Default template uses 0x2c for <AppData> as per https://stackoverflow.com/questions/1810398/descript-ion-file-spec
// //
const entryTemplate = args[0]; const entryTemplate = args[0];
const headerTemplate = args[1]; const headerTemplate = args[1];
const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck : true }); const areas = FileArea.getAvailableFileAreas(null, { skipAcsCheck: true });
async.each(areas, (area, nextArea) => { async.each(
const storageLocations = FileArea.getAreaStorageLocations(area); areas,
(area, nextArea) => {
const storageLocations = FileArea.getAreaStorageLocations(area);
async.each(storageLocations, (storageLoc, nextStorageLoc) => { async.each(
const filterCriteria = { storageLocations,
areaTag : area.areaTag, (storageLoc, nextStorageLoc) => {
storageTag : storageLoc.storageTag, const filterCriteria = {
}; areaTag: area.areaTag,
storageTag: storageLoc.storageTag,
};
const exportOpts = { const exportOpts = {
headerTemplate : headerTemplate, headerTemplate: headerTemplate,
entryTemplate : entryTemplate, entryTemplate: entryTemplate,
escapeDesc : true, // escape CRLF's escapeDesc: true, // escape CRLF's
maxDescLen : 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes" maxDescLen: 4096, // DESCRIPT.ION: "The line length limit is 4096 bytes"
}; };
exportFileList(filterCriteria, exportOpts, (err, listBody) => { exportFileList(filterCriteria, exportOpts, (err, listBody) => {
const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION');
const descIonPath = paths.join(storageLoc.dir, 'DESCRIPT.ION'); fs.writeFile(
fs.writeFile(descIonPath, iconv.encode(listBody, 'cp437'), err => { descIonPath,
if(err) { iconv.encode(listBody, 'cp437'),
Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION'); err => {
} else { if (err) {
Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION'); Log.warn(
} { error: err.message, path: descIonPath },
return nextStorageLoc(null); 'Failed (re)creating DESCRIPT.ION'
}); );
}); } else {
}, () => { Log.debug(
return nextArea(null); { path: descIonPath },
}); '(Re)generated DESCRIPT.ION'
}, () => { );
return cb(null); }
}); return nextStorageLoc(null);
}
);
});
},
() => {
return nextArea(null);
}
);
},
() => {
return cb(null);
}
);
} }

View File

@ -2,30 +2,31 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const getSortedAvailableFileAreas = require('./file_base_area.js').getSortedAvailableFileAreas; const getSortedAvailableFileAreas =
const FileBaseFilters = require('./file_base_filter.js'); require('./file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('./file_base_filter.js');
// deps // deps
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Search', name: 'File Base Search',
desc : 'Module for quickly searching the file base', desc: 'Module for quickly searching the file base',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
search : { search: {
searchTerms : 1, searchTerms: 1,
search : 2, search: 2,
tags : 3, tags: 3,
area : 4, area: 4,
orderBy : 5, orderBy: 5,
sort : 6, sort: 6,
advSearch : 7, advSearch: 7,
} },
}; };
exports.getModule = class FileBaseSearch extends MenuModule { exports.getModule = class FileBaseSearch extends MenuModule {
@ -33,7 +34,7 @@ exports.getModule = class FileBaseSearch extends MenuModule {
super(options); super(options);
this.menuMethods = { this.menuMethods = {
search : (formData, extraArgs, cb) => { search: (formData, extraArgs, cb) => {
const isAdvanced = formData.submitId === MciViewIds.search.advSearch; const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
return this.searchNow(formData, isAdvanced, cb); return this.searchNow(formData, isAdvanced, cb);
}, },
@ -42,28 +43,36 @@ exports.getModule = class FileBaseSearch extends MenuModule {
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.addViewController( 'search', new ViewController( { client : this.client } ) ); const vc = self.addViewController(
'search',
new ViewController({ client: this.client })
);
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
return vc.loadFromMenuConfig( { callingMenu : self, mciMap : mciData.menu }, callback); return vc.loadFromMenuConfig(
{ callingMenu: self, mciMap: mciData.menu },
callback
);
}, },
function populateAreas(callback) { function populateAreas(callback) {
self.availAreas = [ { name : '-ALL-' } ].concat(getSortedAvailableFileAreas(self.client) || []); self.availAreas = [{ name: '-ALL-' }].concat(
getSortedAvailableFileAreas(self.client) || []
);
const areasView = vc.getView(MciViewIds.search.area); const areasView = vc.getView(MciViewIds.search.area);
areasView.setItems( self.availAreas.map( a => a.name ) ); areasView.setItems(self.availAreas.map(a => a.name));
areasView.redraw(); areasView.redraw();
vc.switchFocus(MciViewIds.search.searchTerms); vc.switchFocus(MciViewIds.search.searchTerms);
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -73,11 +82,11 @@ exports.getModule = class FileBaseSearch extends MenuModule {
} }
getSelectedAreaTag(index) { getSelectedAreaTag(index) {
if(0 === index) { if (0 === index) {
return ''; // -ALL- return ''; // -ALL-
} }
const area = this.availAreas[index]; const area = this.availAreas[index];
if(!area) { if (!area) {
return ''; return '';
} }
return area.areaTag; return area.areaTag;
@ -92,16 +101,16 @@ exports.getModule = class FileBaseSearch extends MenuModule {
} }
getFilterValuesFromFormData(formData, isAdvanced) { getFilterValuesFromFormData(formData, isAdvanced) {
const areaIndex = isAdvanced ? formData.value.areaIndex : 0; const areaIndex = isAdvanced ? formData.value.areaIndex : 0;
const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0; const orderByIndex = isAdvanced ? formData.value.orderByIndex : 0;
const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0; const sortByIndex = isAdvanced ? formData.value.sortByIndex : 0;
return { return {
areaTag : this.getSelectedAreaTag(areaIndex), areaTag: this.getSelectedAreaTag(areaIndex),
terms : formData.value.searchTerms, terms: formData.value.searchTerms,
tags : isAdvanced ? formData.value.tags : '', tags: isAdvanced ? formData.value.tags : '',
order : this.getOrderBy(orderByIndex), order: this.getOrderBy(orderByIndex),
sort : this.getSortBy(sortByIndex), sort: this.getSortBy(sortByIndex),
}; };
} }
@ -109,12 +118,16 @@ exports.getModule = class FileBaseSearch extends MenuModule {
const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced); const filterCriteria = this.getFilterValuesFromFormData(formData, isAdvanced);
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs: {
filterCriteria : filterCriteria, filterCriteria: filterCriteria,
}, },
menuFlags : [ 'popParent' ], menuFlags: ['popParent'],
}; };
return this.gotoMenu(this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries', menuOpts, cb); return this.gotoMenu(
this.menuConfig.config.fileBaseListEntriesMenu || 'fileBaseListEntries',
menuOpts,
cb
);
} }
}; };

View File

@ -2,23 +2,23 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const FileArea = require('./file_base_area.js'); const FileArea = require('./file_base_area.js');
const { renderSubstr } = require('./string_util.js'); const { renderSubstr } = require('./string_util.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const { exportFileList } = require('./file_base_list_export.js'); const { exportFileList } = require('./file_base_list_export.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const fse = require('fs-extra'); const fse = require('fs-extra');
const paths = require('path'); const paths = require('path');
const moment = require('moment'); const moment = require('moment');
const { v4 : UUIDv4 } = require('uuid'); const { v4: UUIDv4 } = require('uuid');
const yazl = require('yazl'); const yazl = require('yazl');
/* /*
Module config block can contain the following: Module config block can contain the following:
@ -44,52 +44,66 @@ const yazl = require('yazl');
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base List Export', name: 'File Base List Export',
desc : 'Exports file base listings for download', desc: 'Exports file base listings for download',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const FormIds = { const FormIds = {
main : 0, main: 0,
}; };
const MciViewIds = { const MciViewIds = {
main : { main: {
status : 1, status: 1,
progressBar : 2, progressBar: 2,
customRangeStart : 10, customRangeStart: 10,
} },
}; };
exports.getModule = class FileBaseListExport extends MenuModule { exports.getModule = class FileBaseListExport extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config = Object.assign(
{},
_.get(options, 'menuConfig.config'),
options.extraArgs
);
this.config.templateEncoding = this.config.templateEncoding || 'utf8'; this.config.templateEncoding = this.config.templateEncoding || 'utf8';
this.config.tsFormat = this.config.tsFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); this.config.tsFormat =
this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ this.config.tsFormat ||
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); this.client.currentTheme.helpers.getDateTimeFormat('short');
this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ
this.config.progBarChar = renderSubstr(this.config.progBarChar || '▒', 0, 1);
this.config.compressThreshold = this.config.compressThreshold || 1440000; // >= 1.44M by default :)
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
async.series( async.series(
[ [
(callback) => this.prepViewController('main', FormIds.main, mciData.menu, callback), callback =>
(callback) => this.prepareList(callback), this.prepViewController(
'main',
FormIds.main,
mciData.menu,
callback
),
callback => this.prepareList(callback),
], ],
err => { err => {
if(err) { if (err) {
if('NORESULTS' === err.reasonCode) { if ('NORESULTS' === err.reasonCode) {
return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'fileBaseExportListNoResults'); return this.gotoMenu(
this.menuConfig.config.noResultsMenu ||
'fileBaseExportListNoResults'
);
} }
return this.prevMenu(); return this.prevMenu();
@ -108,16 +122,18 @@ exports.getModule = class FileBaseListExport extends MenuModule {
const self = this; const self = this;
const statusView = self.viewControllers.main.getView(MciViewIds.main.status); const statusView = self.viewControllers.main.getView(MciViewIds.main.status);
const updateStatus = (status) => { const updateStatus = status => {
if(statusView) { if (statusView) {
statusView.setText(status); statusView.setText(status);
} }
}; };
const progBarView = self.viewControllers.main.getView(MciViewIds.main.progressBar); const progBarView = self.viewControllers.main.getView(
MciViewIds.main.progressBar
);
const updateProgressBar = (curr, total) => { const updateProgressBar = (curr, total) => {
if(progBarView) { if (progBarView) {
const prog = Math.floor( (curr / total) * progBarView.dimens.width ); const prog = Math.floor((curr / total) * progBarView.dimens.width);
progBarView.setText(self.config.progBarChar.repeat(prog)); progBarView.setText(self.config.progBarChar.repeat(prog));
} }
}; };
@ -125,17 +141,21 @@ exports.getModule = class FileBaseListExport extends MenuModule {
let cancel = false; let cancel = false;
const exportListProgress = (state, progNext) => { const exportListProgress = (state, progNext) => {
switch(state.step) { switch (state.step) {
case 'preparing' : case 'preparing':
case 'gathering' : case 'gathering':
updateStatus(state.status); updateStatus(state.status);
break; break;
case 'file' : case 'file':
updateStatus(state.status); updateStatus(state.status);
updateProgressBar(state.current, state.total); updateProgressBar(state.current, state.total);
self.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state.fileInfo); self.updateCustomViewTextsWithFilter(
'main',
MciViewIds.main.customRangeStart,
state.fileInfo
);
break; break;
default : default:
break; break;
} }
@ -143,7 +163,7 @@ exports.getModule = class FileBaseListExport extends MenuModule {
}; };
const keyPressHandler = (ch, key) => { const keyPressHandler = (ch, key) => {
if('escape' === key.name) { if ('escape' === key.name) {
cancel = true; cancel = true;
self.client.removeListener('key press', keyPressHandler); self.client.removeListener('key press', keyPressHandler);
} }
@ -158,17 +178,27 @@ exports.getModule = class FileBaseListExport extends MenuModule {
self.client.on('key press', keyPressHandler); self.client.on('key press', keyPressHandler);
const filterCriteria = Object.assign({}, self.config.filterCriteria); const filterCriteria = Object.assign({}, self.config.filterCriteria);
if(!filterCriteria.areaTag) { if (!filterCriteria.areaTag) {
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(self.client); filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(
self.client
);
} }
const opts = { const opts = {
templateEncoding : self.config.templateEncoding, templateEncoding: self.config.templateEncoding,
headerTemplate : _.get(self.config, 'templates.header', 'file_list_header.asc'), headerTemplate: _.get(
entryTemplate : _.get(self.config, 'templates.entry', 'file_list_entry.asc'), self.config,
tsFormat : self.config.tsFormat, 'templates.header',
descWidth : self.config.descWidth, 'file_list_header.asc'
progress : exportListProgress, ),
entryTemplate: _.get(
self.config,
'templates.entry',
'file_list_entry.asc'
),
tsFormat: self.config.tsFormat,
descWidth: self.config.descWidth,
progress: exportListProgress,
}; };
exportFileList(filterCriteria, opts, (err, listBody) => { exportFileList(filterCriteria, opts, (err, listBody) => {
@ -178,47 +208,65 @@ exports.getModule = class FileBaseListExport extends MenuModule {
function persistList(listBody, callback) { function persistList(listBody, callback) {
updateStatus('Persisting list'); updateStatus('Persisting list');
const sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); const sysTempDownloadArea = FileArea.getFileAreaByTag(
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea); FileArea.WellKnownAreaTags.TempDownloads
);
const sysTempDownloadDir =
FileArea.getAreaDefaultStorageDirectory(sysTempDownloadArea);
fse.mkdirs(sysTempDownloadDir, err => { fse.mkdirs(sysTempDownloadDir, err => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
const outputFileName = paths.join( const outputFileName = paths.join(
sysTempDownloadDir, sysTempDownloadDir,
`file_list_${UUIDv4().substr(-8)}_${moment().format('YYYY-MM-DD')}.txt` `file_list_${UUIDv4().substr(-8)}_${moment().format(
'YYYY-MM-DD'
)}.txt`
); );
fs.writeFile(outputFileName, listBody, 'utf8', err => { fs.writeFile(outputFileName, listBody, 'utf8', err => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
self.getSizeAndCompressIfMeetsSizeThreshold(outputFileName, (err, finalOutputFileName, fileSize) => { self.getSizeAndCompressIfMeetsSizeThreshold(
return callback(err, finalOutputFileName, fileSize, sysTempDownloadArea); outputFileName,
}); (err, finalOutputFileName, fileSize) => {
return callback(
err,
finalOutputFileName,
fileSize,
sysTempDownloadArea
);
}
);
}); });
}); });
}, },
function persistFileEntry(outputFileName, fileSize, sysTempDownloadArea, callback) { function persistFileEntry(
outputFileName,
fileSize,
sysTempDownloadArea,
callback
) {
const newEntry = new FileEntry({ const newEntry = new FileEntry({
areaTag : sysTempDownloadArea.areaTag, areaTag: sysTempDownloadArea.areaTag,
fileName : paths.basename(outputFileName), fileName: paths.basename(outputFileName),
storageTag : sysTempDownloadArea.storageTags[0], storageTag: sysTempDownloadArea.storageTags[0],
meta : { meta: {
upload_by_username : self.client.user.username, upload_by_username: self.client.user.username,
upload_by_user_id : self.client.user.userId, upload_by_user_id: self.client.user.userId,
byte_size : fileSize, byte_size: fileSize,
session_temp_dl : 1, // download is valid until session is over session_temp_dl: 1, // download is valid until session is over
} },
}); });
newEntry.desc = 'File List Export'; newEntry.desc = 'File List Export';
newEntry.persist(err => { newEntry.persist(err => {
if(!err) { if (!err) {
// queue it! // queue it!
DownloadQueue.get(self.client).addTemporaryDownload(newEntry); DownloadQueue.get(self.client).addTemporaryDownload(newEntry);
} }
@ -232,7 +280,7 @@ exports.getModule = class FileBaseListExport extends MenuModule {
updateStatus('Exported list has been added to your download queue'); updateStatus('Exported list has been added to your download queue');
return callback(null); return callback(null);
} },
], ],
err => { err => {
self.client.removeListener('key press', keyPressHandler); self.client.removeListener('key press', keyPressHandler);
@ -243,11 +291,11 @@ exports.getModule = class FileBaseListExport extends MenuModule {
getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) { getSizeAndCompressIfMeetsSizeThreshold(filePath, cb) {
fse.stat(filePath, (err, stats) => { fse.stat(filePath, (err, stats) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(stats.size < this.config.compressThreshold) { if (stats.size < this.config.compressThreshold) {
// small enough, keep orig // small enough, keep orig
return cb(null, filePath, stats.size); return cb(null, filePath, stats.size);
} }
@ -256,13 +304,13 @@ exports.getModule = class FileBaseListExport extends MenuModule {
const zipFile = new yazl.ZipFile(); const zipFile = new yazl.ZipFile();
zipFile.addFile(filePath, paths.basename(filePath)); zipFile.addFile(filePath, paths.basename(filePath));
zipFile.end( () => { zipFile.end(() => {
const outZipFile = fs.createWriteStream(zipFilePath); const outZipFile = fs.createWriteStream(zipFilePath);
zipFile.outputStream.pipe(outZipFile); zipFile.outputStream.pipe(outZipFile);
zipFile.outputStream.on('finish', () => { zipFile.outputStream.on('finish', () => {
// delete the original // delete the original
fse.unlink(filePath, err => { fse.unlink(filePath, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -275,4 +323,4 @@ exports.getModule = class FileBaseListExport extends MenuModule {
}); });
}); });
} }
}; };

View File

@ -2,74 +2,79 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const FileAreaWeb = require('./file_area_web.js'); const FileAreaWeb = require('./file_area_web.js');
const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled; const ErrNotEnabled = require('./enig_error.js').ErrorReasons.NotEnabled;
const Config = require('./config.js').get; const Config = require('./config.js').get;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Base Download Web Queue Manager', name: 'File Base Download Web Queue Manager',
desc : 'Module for interacting with web backed download queue/batch', desc: 'Module for interacting with web backed download queue/batch',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const FormIds = { const FormIds = {
queueManager : 0 queueManager: 0,
}; };
const MciViewIds = { const MciViewIds = {
queueManager : { queueManager: {
queue : 1, queue: 1,
navMenu : 2, navMenu: 2,
customRangeStart : 10, customRangeStart: 10,
} },
}; };
exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule { exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
this.menuMethods = { this.menuMethods = {
removeItem : (formData, extraArgs, cb) => { removeItem: (formData, extraArgs, cb) => {
const selectedItem = this.dlQueue.items[formData.value.queueItem]; const selectedItem = this.dlQueue.items[formData.value.queueItem];
if(!selectedItem) { if (!selectedItem) {
return cb(null); return cb(null);
} }
this.dlQueue.removeItems(selectedItem.fileId); this.dlQueue.removeItems(selectedItem.fileId);
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); return this.removeItemsFromDownloadQueueView(
formData.value.queueItem,
cb
);
}, },
clearQueue : (formData, extraArgs, cb) => { clearQueue: (formData, extraArgs, cb) => {
this.dlQueue.clear(); this.dlQueue.clear();
// :TODO: broken: does not redraw menu properly - needs fixed! // :TODO: broken: does not redraw menu properly - needs fixed!
return this.removeItemsFromDownloadQueueView('all', cb); return this.removeItemsFromDownloadQueueView('all', cb);
}, },
getBatchLink : (formData, extraArgs, cb) => { getBatchLink: (formData, extraArgs, cb) => {
return this.generateAndDisplayBatchLink(cb); return this.generateAndDisplayBatchLink(cb);
} },
}; };
} }
initSequence() { initSequence() {
if(0 === this.dlQueue.items.length) { if (0 === this.dlQueue.items.length) {
return this.gotoMenu(this.menuConfig.config.emptyQueueMenu || 'fileBaseDownloadManagerEmptyQueue'); return this.gotoMenu(
this.menuConfig.config.emptyQueueMenu ||
'fileBaseDownloadManagerEmptyQueue'
);
} }
const self = this; const self = this;
@ -81,7 +86,7 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
}, },
function display(callback) { function display(callback) {
return self.displayQueueManagerPage(false, callback); return self.displayQueueManagerPage(false, callback);
} },
], ],
() => { () => {
return self.finishedLoading(); return self.finishedLoading();
@ -90,12 +95,14 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
} }
removeItemsFromDownloadQueueView(itemIndex, cb) { removeItemsFromDownloadQueueView(itemIndex, cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(
if(!queueView) { MciViewIds.queueManager.queue
);
if (!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
if('all' === itemIndex) { if ('all' === itemIndex) {
queueView.setItems([]); queueView.setItems([]);
queueView.setFocusItems([]); queueView.setFocusItems([]);
} else { } else {
@ -109,14 +116,17 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
displayFileInfoForFileEntry(fileEntry) { displayFileInfoForFileEntry(fileEntry) {
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'queueManager', 'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry, MciViewIds.queueManager.customRangeStart,
{ filter : [ '{webDlLink}', '{webDlExpire}', '{fileName}' ] } // :TODO: Others.... fileEntry,
{ filter: ['{webDlLink}', '{webDlExpire}', '{fileName}'] } // :TODO: Others....
); );
} }
updateDownloadQueueView(cb) { updateDownloadQueueView(cb) {
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); const queueView = this.viewControllers.queueManager.getView(
if(!queueView) { MciViewIds.queueManager.queue
);
if (!queueView) {
return cb(Errors.DoesNotExist('Queue view does not exist')); return cb(Errors.DoesNotExist('Queue view does not exist'));
} }
@ -140,26 +150,28 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
this.client, this.client,
this.dlQueue.items, this.dlQueue.items,
{ {
expireTime : expireTime expireTime: expireTime,
}, },
(err, webBatchDlLink) => { (err, webBatchDlLink) => {
// :TODO: handle not enabled -> display such // :TODO: handle not enabled -> display such
if(err) { if (err) {
return cb(err); return cb(err);
} }
const webDlExpireTimeFormat = this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat =
this.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm';
const formatObj = { const formatObj = {
webBatchDlLink : ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink, webBatchDlLink:
webBatchDlExpire : expireTime.format(webDlExpireTimeFormat), ansi.vtxHyperlink(this.client, webBatchDlLink) + webBatchDlLink,
webBatchDlExpire: expireTime.format(webDlExpireTimeFormat),
}; };
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'queueManager', 'queueManager',
MciViewIds.queueManager.customRangeStart, MciViewIds.queueManager.customRangeStart,
formatObj, formatObj,
{ filter : Object.keys(formatObj).map(k => '{' + k + '}' ) } { filter: Object.keys(formatObj).map(k => '{' + k + '}') }
); );
return cb(null); return cb(null);
@ -173,54 +185,82 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
async.series( async.series(
[ [
function prepArtAndViewController(callback) { function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('queueManager', { clearScreen : clearScreen }, callback); return self.displayArtAndPrepViewController(
'queueManager',
{ clearScreen: clearScreen },
callback
);
}, },
function prepareQueueDownloadLinks(callback) { function prepareQueueDownloadLinks(callback) {
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat =
self.menuConfig.config.webDlExpireTimeFormat ||
'YYYY-MMM-DD @ h:mm';
const config = Config(); const config = Config();
async.each(self.dlQueue.items, (fileEntry, nextFileEntry) => { async.each(
FileAreaWeb.getExistingTempDownloadServeItem(self.client, fileEntry, (err, serveItem) => { self.dlQueue.items,
if(err) { (fileEntry, nextFileEntry) => {
if(ErrNotEnabled === err.reasonCode) { FileAreaWeb.getExistingTempDownloadServeItem(
return nextFileEntry(err); // we should have caught this prior self.client,
} fileEntry,
(err, serveItem) => {
const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); if (err) {
if (ErrNotEnabled === err.reasonCode) {
FileAreaWeb.createAndServeTempDownload( return nextFileEntry(err); // we should have caught this prior
self.client,
fileEntry,
{ expireTime : expireTime },
(err, url) => {
if(err) {
return nextFileEntry(err);
} }
fileEntry.webDlLinkRaw = url; const expireTime = moment().add(
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; config.fileBase.web.expireMinutes,
fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); 'minutes'
);
FileAreaWeb.createAndServeTempDownload(
self.client,
fileEntry,
{ expireTime: expireTime },
(err, url) => {
if (err) {
return nextFileEntry(err);
}
fileEntry.webDlLinkRaw = url;
fileEntry.webDlLink =
ansi.vtxHyperlink(self.client, url) +
url;
fileEntry.webDlExpire =
expireTime.format(
webDlExpireTimeFormat
);
return nextFileEntry(null);
}
);
} else {
fileEntry.webDlLinkRaw = serveItem.url;
fileEntry.webDlLink =
ansi.vtxHyperlink(
self.client,
serveItem.url
) + serveItem.url;
fileEntry.webDlExpire = moment(
serveItem.expireTimestamp
).format(webDlExpireTimeFormat);
return nextFileEntry(null); return nextFileEntry(null);
} }
); }
} else { );
fileEntry.webDlLinkRaw = serveItem.url; },
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; err => {
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); return callback(err);
return nextFileEntry(null); }
} );
});
}, err => {
return callback(err);
});
}, },
function populateViews(callback) { function populateViews(callback) {
return self.updateDownloadQueueView(callback); return self.updateDownloadQueueView(callback);
} },
], ],
err => { err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
} }
@ -228,42 +268,45 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
const self = this; const self = this;
const config = this.menuConfig.config; const config = this.menuConfig.config;
async.waterfall( async.waterfall(
[ [
function readyAndDisplayArt(callback) { function readyAndDisplayArt(callback) {
if(options.clearScreen) { if (options.clearScreen) {
self.client.term.rawWrite(ansi.resetScreen()); self.client.term.rawWrite(ansi.resetScreen());
} }
theme.displayThemedAsset( theme.displayThemedAsset(
config.art[name], config.art[name],
self.client, self.client,
{ font : self.menuConfig.font, trailingLF : false }, { font: self.menuConfig.font, trailingLF: false },
(err, artData) => { (err, artData) => {
return callback(err, artData); return callback(err, artData);
} }
); );
}, },
function prepeareViewController(artData, callback) { function prepeareViewController(artData, callback) {
if(_.isUndefined(self.viewControllers[name])) { if (_.isUndefined(self.viewControllers[name])) {
const vcOpts = { const vcOpts = {
client : self.client, client: self.client,
formId : FormIds[name], formId: FormIds[name],
}; };
if(!_.isUndefined(options.noInput)) { if (!_.isUndefined(options.noInput)) {
vcOpts.noInput = options.noInput; vcOpts.noInput = options.noInput;
} }
const vc = self.addViewController(name, new ViewController(vcOpts)); const vc = self.addViewController(
name,
new ViewController(vcOpts)
);
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu: self,
mciMap : artData.mciMap, mciMap: artData.mciMap,
formId : FormIds[name], formId: FormIds[name],
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -271,7 +314,6 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
self.viewControllers[name].setFocus(true); self.viewControllers[name].setFocus(true);
return callback(null); return callback(null);
}, },
], ],
err => { err => {

View File

@ -1,57 +1,60 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const fileDb = require('./database.js').dbs.file; const fileDb = require('./database.js').dbs.file;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const { const { getISOTimestampString, sanitizeString } = require('./database.js');
getISOTimestampString, const Config = require('./config.js').get;
sanitizeString
} = require('./database.js');
const Config = require('./config.js').get;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const paths = require('path'); const paths = require('path');
const fse = require('fs-extra'); const fse = require('fs-extra');
const { unlink, readFile } = require('graceful-fs'); const { unlink, readFile } = require('graceful-fs');
const crypto = require('crypto'); const crypto = require('crypto');
const moment = require('moment'); const moment = require('moment');
const FILE_TABLE_MEMBERS = [ const FILE_TABLE_MEMBERS = [
'file_id', 'area_tag', 'file_sha256', 'file_name', 'storage_tag', 'file_id',
'desc', 'desc_long', 'upload_timestamp' 'area_tag',
'file_sha256',
'file_name',
'storage_tag',
'desc',
'desc_long',
'upload_timestamp',
]; ];
const FILE_WELL_KNOWN_META = { const FILE_WELL_KNOWN_META = {
// name -> *read* converter, if any // name -> *read* converter, if any
upload_by_username : null, upload_by_username: null,
upload_by_user_id : (u) => parseInt(u) || 0, upload_by_user_id: u => parseInt(u) || 0,
file_md5 : null, file_md5: null,
file_sha1 : null, file_sha1: null,
file_crc32 : null, file_crc32: null,
est_release_year : (y) => parseInt(y) || new Date().getFullYear(), est_release_year: y => parseInt(y) || new Date().getFullYear(),
dl_count : (d) => parseInt(d) || 0, dl_count: d => parseInt(d) || 0,
byte_size : (b) => parseInt(b) || 0, byte_size: b => parseInt(b) || 0,
archive_type : null, archive_type: null,
short_file_name : null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import short_file_name: null, // e.g. DOS 8.3 filename, avail in some scenarios such as TIC import
tic_origin : null, // TIC "Origin" tic_origin: null, // TIC "Origin"
tic_desc : null, // TIC "Desc" tic_desc: null, // TIC "Desc"
tic_ldesc : null, // TIC "Ldesc" joined by '\n' tic_ldesc: null, // TIC "Ldesc" joined by '\n'
session_temp_dl : (v) => parseInt(v) ? true : false, session_temp_dl: v => (parseInt(v) ? true : false),
desc_sauce : (s) => JSON.parse(s) || {}, desc_sauce: s => JSON.parse(s) || {},
desc_long_sauce : (s) => JSON.parse(s) || {}, desc_long_sauce: s => JSON.parse(s) || {},
}; };
module.exports = class FileEntry { module.exports = class FileEntry {
constructor(options) { constructor(options) {
options = options || {}; options = options || {};
this.fileId = options.fileId || 0; this.fileId = options.fileId || 0;
this.areaTag = options.areaTag || ''; this.areaTag = options.areaTag || '';
this.meta = Object.assign( { dl_count : 0 }, options.meta); this.meta = Object.assign({ dl_count: 0 }, options.meta);
this.hashTags = options.hashTags || new Set(); this.hashTags = options.hashTags || new Set();
this.fileName = options.fileName; this.fileName = options.fileName;
this.storageTag = options.storageTag; this.storageTag = options.storageTag;
this.fileSha256 = options.fileSha256; this.fileSha256 = options.fileSha256;
} }
@ -64,13 +67,13 @@ module.exports = class FileEntry {
FROM file FROM file
WHERE file_id=? WHERE file_id=?
LIMIT 1;`, LIMIT 1;`,
[ fileId ], [fileId],
(err, file) => { (err, file) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(!file) { if (!file) {
return cb(Errors.DoesNotExist('No file is available by that ID')); return cb(Errors.DoesNotExist('No file is available by that ID'));
} }
@ -100,7 +103,7 @@ module.exports = class FileEntry {
}, },
function loadUserRating(callback) { function loadUserRating(callback) {
return self.loadRating(callback); return self.loadRating(callback);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -109,7 +112,7 @@ module.exports = class FileEntry {
} }
persist(isUpdate, cb) { persist(isUpdate, cb) {
if(!cb && _.isFunction(isUpdate)) { if (!cb && _.isFunction(isUpdate)) {
cb = isUpdate; cb = isUpdate;
isUpdate = false; isUpdate = false;
} }
@ -119,22 +122,30 @@ module.exports = class FileEntry {
async.waterfall( async.waterfall(
[ [
function check(callback) { function check(callback) {
if(isUpdate && !self.fileId) { if (isUpdate && !self.fileId) {
return callback(Errors.Invalid('Cannot update file entry without an existing "fileId" member')); return callback(
Errors.Invalid(
'Cannot update file entry without an existing "fileId" member'
)
);
} }
return callback(null); return callback(null);
}, },
function calcSha256IfNeeded(callback) { function calcSha256IfNeeded(callback) {
if(self.fileSha256) { if (self.fileSha256) {
return callback(null); return callback(null);
} }
if(isUpdate) { if (isUpdate) {
return callback(Errors.MissingParam('fileSha256 property must be set for updates!')); return callback(
Errors.MissingParam(
'fileSha256 property must be set for updates!'
)
);
} }
readFile(self.filePath, (err, data) => { readFile(self.filePath, (err, data) => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
@ -148,11 +159,20 @@ module.exports = class FileEntry {
return fileDb.beginTransaction(callback); return fileDb.beginTransaction(callback);
}, },
function storeEntry(trans, callback) { function storeEntry(trans, callback) {
if(isUpdate) { if (isUpdate) {
trans.run( trans.run(
`REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) `REPLACE INTO file (file_id, area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
VALUES(?, ?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?, ?);`,
[ self.fileId, self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], [
self.fileId,
self.areaTag,
self.fileSha256,
self.fileName,
self.storageTag,
self.desc,
self.descLong,
getISOTimestampString(),
],
err => { err => {
return callback(err, trans); return callback(err, trans);
} }
@ -161,9 +181,18 @@ module.exports = class FileEntry {
trans.run( trans.run(
`REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp) `REPLACE INTO file (area_tag, file_sha256, file_name, storage_tag, desc, desc_long, upload_timestamp)
VALUES(?, ?, ?, ?, ?, ?, ?);`, VALUES(?, ?, ?, ?, ?, ?, ?);`,
[ self.areaTag, self.fileSha256, self.fileName, self.storageTag, self.desc, self.descLong, getISOTimestampString() ], [
function inserted(err) { // use non-arrow func for 'this' scope / lastID self.areaTag,
if(!err) { self.fileSha256,
self.fileName,
self.storageTag,
self.desc,
self.descLong,
getISOTimestampString(),
],
function inserted(err) {
// use non-arrow func for 'this' scope / lastID
if (!err) {
self.fileId = this.lastID; self.fileId = this.lastID;
} }
return callback(err, trans); return callback(err, trans);
@ -172,27 +201,44 @@ module.exports = class FileEntry {
} }
}, },
function storeMeta(trans, callback) { function storeMeta(trans, callback) {
async.each(Object.keys(self.meta), (n, next) => { async.each(
const v = self.meta[n]; Object.keys(self.meta),
return FileEntry.persistMetaValue(self.fileId, n, v, trans, next); (n, next) => {
}, const v = self.meta[n];
err => { return FileEntry.persistMetaValue(
return callback(err, trans); self.fileId,
}); n,
v,
trans,
next
);
},
err => {
return callback(err, trans);
}
);
}, },
function storeHashTags(trans, callback) { function storeHashTags(trans, callback) {
const hashTagsArray = Array.from(self.hashTags); const hashTagsArray = Array.from(self.hashTags);
async.each(hashTagsArray, (hashTag, next) => { async.each(
return FileEntry.persistHashTag(self.fileId, hashTag, trans, next); hashTagsArray,
}, (hashTag, next) => {
err => { return FileEntry.persistHashTag(
return callback(err, trans); self.fileId,
}); hashTag,
} trans,
next
);
},
err => {
return callback(err, trans);
}
);
},
], ],
(err, trans) => { (err, trans) => {
// :TODO: Log orig err // :TODO: Log orig err
if(trans) { if (trans) {
trans[err ? 'rollback' : 'commit'](transErr => { trans[err ? 'rollback' : 'commit'](transErr => {
return cb(transErr ? transErr : err); return cb(transErr ? transErr : err);
}); });
@ -205,10 +251,10 @@ module.exports = class FileEntry {
static getAreaStorageDirectoryByTag(storageTag) { static getAreaStorageDirectoryByTag(storageTag) {
const config = Config(); const config = Config();
const storageLocation = (storageTag && config.fileBase.storageTags[storageTag]); const storageLocation = storageTag && config.fileBase.storageTags[storageTag];
// absolute paths as-is // absolute paths as-is
if(storageLocation && '/' === storageLocation.charAt(0)) { if (storageLocation && '/' === storageLocation.charAt(0)) {
return storageLocation; return storageLocation;
} }
@ -227,7 +273,7 @@ module.exports = class FileEntry {
FROM file FROM file
WHERE file_name = ? WHERE file_name = ?
LIMIT 1;`, LIMIT 1;`,
[ paths.basename(fullPath) ], [paths.basename(fullPath)],
(err, rows) => { (err, rows) => {
return err ? cb(err) : cb(null, rows.count > 0 ? true : false); return err ? cb(err) : cb(null, rows.count > 0 ? true : false);
} }
@ -238,7 +284,7 @@ module.exports = class FileEntry {
return fileDb.run( return fileDb.run(
`REPLACE INTO file_user_rating (file_id, user_id, rating) `REPLACE INTO file_user_rating (file_id, user_id, rating)
VALUES (?, ?, ?);`, VALUES (?, ?, ?);`,
[ fileId, userId, rating ], [fileId, userId, rating],
cb cb
); );
} }
@ -247,13 +293,13 @@ module.exports = class FileEntry {
return fileDb.run( return fileDb.run(
`DELETE FROM file_user_rating `DELETE FROM file_user_rating
WHERE user_id = ?;`, WHERE user_id = ?;`,
[ userId ], [userId],
cb cb
); );
} }
static persistMetaValue(fileId, name, value, transOrDb, cb) { static persistMetaValue(fileId, name, value, transOrDb, cb) {
if(!_.isFunction(cb) && _.isFunction(transOrDb)) { if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb; cb = transOrDb;
transOrDb = fileDb; transOrDb = fileDb;
} }
@ -261,7 +307,7 @@ module.exports = class FileEntry {
return transOrDb.run( return transOrDb.run(
`REPLACE INTO file_meta (file_id, meta_name, meta_value) `REPLACE INTO file_meta (file_id, meta_name, meta_value)
VALUES (?, ?, ?);`, VALUES (?, ?, ?);`,
[ fileId, name, value ], [fileId, name, value],
cb cb
); );
} }
@ -272,9 +318,9 @@ module.exports = class FileEntry {
`UPDATE file_meta `UPDATE file_meta
SET meta_value = meta_value + ? SET meta_value = meta_value + ?
WHERE file_id = ? AND meta_name = ?;`, WHERE file_id = ? AND meta_name = ?;`,
[ incrementBy, fileId, name ], [incrementBy, fileId, name],
err => { err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
} }
@ -286,11 +332,13 @@ module.exports = class FileEntry {
`SELECT meta_name, meta_value `SELECT meta_name, meta_value
FROM file_meta FROM file_meta
WHERE file_id=?;`, WHERE file_id=?;`,
[ this.fileId ], [this.fileId],
(err, meta) => { (err, meta) => {
if(meta) { if (meta) {
const conv = FILE_WELL_KNOWN_META[meta.meta_name]; const conv = FILE_WELL_KNOWN_META[meta.meta_name];
this.meta[meta.meta_name] = conv ? conv(meta.meta_value) : meta.meta_value; this.meta[meta.meta_name] = conv
? conv(meta.meta_value)
: meta.meta_value;
} }
}, },
err => { err => {
@ -300,16 +348,16 @@ module.exports = class FileEntry {
} }
static persistHashTag(fileId, hashTag, transOrDb, cb) { static persistHashTag(fileId, hashTag, transOrDb, cb) {
if(!_.isFunction(cb) && _.isFunction(transOrDb)) { if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb; cb = transOrDb;
transOrDb = fileDb; transOrDb = fileDb;
} }
transOrDb.serialize( () => { transOrDb.serialize(() => {
transOrDb.run( transOrDb.run(
`INSERT OR IGNORE INTO hash_tag (hash_tag) `INSERT OR IGNORE INTO hash_tag (hash_tag)
VALUES (?);`, VALUES (?);`,
[ hashTag ] [hashTag]
); );
transOrDb.run( transOrDb.run(
@ -320,7 +368,7 @@ module.exports = class FileEntry {
WHERE hash_tag = ?), WHERE hash_tag = ?),
? ?
);`, );`,
[ hashTag, fileId ], [hashTag, fileId],
err => { err => {
return cb(err); return cb(err);
} }
@ -337,9 +385,9 @@ module.exports = class FileEntry {
FROM file_hash_tag FROM file_hash_tag
WHERE file_id=? WHERE file_id=?
);`, );`,
[ this.fileId ], [this.fileId],
(err, hashTag) => { (err, hashTag) => {
if(hashTag) { if (hashTag) {
this.hashTags.add(hashTag.hash_tag); this.hashTags.add(hashTag.hash_tag);
} }
}, },
@ -356,9 +404,9 @@ module.exports = class FileEntry {
INNER JOIN file f INNER JOIN file f
ON f.file_id = fur.file_id ON f.file_id = fur.file_id
AND f.file_id = ?`, AND f.file_id = ?`,
[ this.fileId ], [this.fileId],
(err, result) => { (err, result) => {
if(result) { if (result) {
this.userRating = result.avg_rating; this.userRating = result.avg_rating;
} }
return cb(err); return cb(err);
@ -367,16 +415,16 @@ module.exports = class FileEntry {
} }
setHashTags(hashTags) { setHashTags(hashTags) {
if(_.isString(hashTags)) { if (_.isString(hashTags)) {
this.hashTags = new Set(hashTags.split(/[\s,]+/)); this.hashTags = new Set(hashTags.split(/[\s,]+/));
} else if(Array.isArray(hashTags)) { } else if (Array.isArray(hashTags)) {
this.hashTags = new Set(hashTags); this.hashTags = new Set(hashTags);
} else if(hashTags instanceof Set) { } else if (hashTags instanceof Set) {
this.hashTags = hashTags; this.hashTags = hashTags;
} }
} }
static get WellKnownMetaValues() { static get WellKnownMetaValues() {
return Object.keys(FILE_WELL_KNOWN_META); return Object.keys(FILE_WELL_KNOWN_META);
} }
@ -386,17 +434,17 @@ module.exports = class FileEntry {
`SELECT file_id `SELECT file_id
FROM file FROM file
WHERE file_sha256 LIKE "${sha}%" WHERE file_sha256 LIKE "${sha}%"
LIMIT 2;`, // limit 2 such that we can find if there are dupes LIMIT 2;`, // limit 2 such that we can find if there are dupes
(err, fileIdRows) => { (err, fileIdRows) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(!fileIdRows || 0 === fileIdRows.length) { if (!fileIdRows || 0 === fileIdRows.length) {
return cb(Errors.DoesNotExist('No matches')); return cb(Errors.DoesNotExist('No matches'));
} }
if(fileIdRows.length > 1) { if (fileIdRows.length > 1) {
return cb(Errors.Invalid('SHA is ambiguous')); return cb(Errors.Invalid('SHA is ambiguous'));
} }
@ -413,17 +461,17 @@ module.exports = class FileEntry {
static findByFullPath(fullPath, cb) { static findByFullPath(fullPath, cb) {
// first, basic by-filename lookup. // first, basic by-filename lookup.
FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => { FileEntry.findByFileNameWildcard(paths.basename(fullPath), (err, entries) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(!entries || !entries.length || entries.length > 1) { if (!entries || !entries.length || entries.length > 1) {
return cb(Errors.DoesNotExist('No matches')); return cb(Errors.DoesNotExist('No matches'));
} }
// ensure the *full* path has not changed // ensure the *full* path has not changed
// :TODO: if FS is case-insensitive, we probably want a better check here // :TODO: if FS is case-insensitive, we probably want a better check here
const possibleMatch = entries[0]; const possibleMatch = entries[0];
if(possibleMatch.fullPath === fullPath) { if (possibleMatch.fullPath === fullPath) {
return cb(null, possibleMatch); return cb(null, possibleMatch);
} }
@ -441,27 +489,30 @@ module.exports = class FileEntry {
WHERE file_name LIKE "${wc}" WHERE file_name LIKE "${wc}"
`, `,
(err, fileIdRows) => { (err, fileIdRows) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(!fileIdRows || 0 === fileIdRows.length) { if (!fileIdRows || 0 === fileIdRows.length) {
return cb(Errors.DoesNotExist('No matches')); return cb(Errors.DoesNotExist('No matches'));
} }
const entries = []; const entries = [];
async.each(fileIdRows, (row, nextRow) => { async.each(
const fileEntry = new FileEntry(); fileIdRows,
fileEntry.load(row.file_id, err => { (row, nextRow) => {
if(!err) { const fileEntry = new FileEntry();
entries.push(fileEntry); fileEntry.load(row.file_id, err => {
} if (!err) {
return nextRow(err); entries.push(fileEntry);
}); }
}, return nextRow(err);
err => { });
return cb(err, entries); },
}); err => {
return cb(err, entries);
}
);
} }
); );
} }
@ -484,12 +535,12 @@ module.exports = class FileEntry {
let sqlOrderBy; let sqlOrderBy;
const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC'; const sqlOrderDir = 'ascending' === filter.order ? 'ASC' : 'DESC';
if(moment.isMoment(filter.newerThanTimestamp)) { if (moment.isMoment(filter.newerThanTimestamp)) {
filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp);
} }
function getOrderByWithCast(ob) { function getOrderByWithCast(ob) {
if( [ 'dl_count', 'est_release_year', 'byte_size' ].indexOf(filter.sort) > -1 ) { if (['dl_count', 'est_release_year', 'byte_size'].indexOf(filter.sort) > -1) {
return `ORDER BY CAST(${ob} AS INTEGER)`; return `ORDER BY CAST(${ob} AS INTEGER)`;
} }
@ -497,7 +548,7 @@ module.exports = class FileEntry {
} }
function appendWhereClause(clause) { function appendWhereClause(clause) {
if(sqlWhere) { if (sqlWhere) {
sqlWhere += ' AND '; sqlWhere += ' AND ';
} else { } else {
sqlWhere += ' WHERE '; sqlWhere += ' WHERE ';
@ -505,20 +556,21 @@ module.exports = class FileEntry {
sqlWhere += clause; sqlWhere += clause;
} }
if(filter.sort && filter.sort.length > 0) { if (filter.sort && filter.sort.length > 0) {
if(Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) { // sorting via a meta value? if (Object.keys(FILE_WELL_KNOWN_META).indexOf(filter.sort) > -1) {
sql = // sorting via a meta value?
`SELECT DISTINCT f.file_id sql = `SELECT DISTINCT f.file_id
FROM file f, file_meta m`; FROM file f, file_meta m`;
appendWhereClause(`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`); appendWhereClause(
`f.file_id = m.file_id AND m.meta_name = "${filter.sort}"`
);
sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`; sqlOrderBy = `${getOrderByWithCast('m.meta_value')} ${sqlOrderDir}`;
} else { } else {
// additional special treatment for user ratings: we need to average them // additional special treatment for user ratings: we need to average them
if('user_rating' === filter.sort) { if ('user_rating' === filter.sort) {
sql = sql = `SELECT DISTINCT f.file_id,
`SELECT DISTINCT f.file_id,
(SELECT IFNULL(AVG(rating), 0) rating (SELECT IFNULL(AVG(rating), 0) rating
FROM file_user_rating FROM file_user_rating
WHERE file_id = f.file_id) WHERE file_id = f.file_id)
@ -527,23 +579,22 @@ module.exports = class FileEntry {
sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`; sqlOrderBy = `ORDER BY avg_rating ${sqlOrderDir}`;
} else { } else {
sql = sql = `SELECT DISTINCT f.file_id
`SELECT DISTINCT f.file_id
FROM file f`; FROM file f`;
sqlOrderBy = getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir; sqlOrderBy =
getOrderByWithCast(`f.${filter.sort}`) + ' ' + sqlOrderDir;
} }
} }
} else { } else {
sql = sql = `SELECT DISTINCT f.file_id
`SELECT DISTINCT f.file_id
FROM file f`; FROM file f`;
sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`; sqlOrderBy = `${getOrderByWithCast('f.file_id')} ${sqlOrderDir}`;
} }
if(filter.areaTag && filter.areaTag.length > 0) { if (filter.areaTag && filter.areaTag.length > 0) {
if(Array.isArray(filter.areaTag)) { if (Array.isArray(filter.areaTag)) {
const areaList = filter.areaTag.map(t => `"${t}"`).join(', '); const areaList = filter.areaTag.map(t => `"${t}"`).join(', ');
appendWhereClause(`f.area_tag IN(${areaList})`); appendWhereClause(`f.area_tag IN(${areaList})`);
} else { } else {
@ -551,10 +602,9 @@ module.exports = class FileEntry {
} }
} }
if(filter.metaPairs && filter.metaPairs.length > 0) { if (filter.metaPairs && filter.metaPairs.length > 0) {
filter.metaPairs.forEach(mp => { filter.metaPairs.forEach(mp => {
if(mp.wildcards) { if (mp.wildcards) {
// convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html // convert any * -> % and ? -> _ for SQLite syntax - see https://www.sqlite.org/lang_expr.html
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
appendWhereClause( appendWhereClause(
@ -576,11 +626,11 @@ module.exports = class FileEntry {
}); });
} }
if(filter.storageTag && filter.storageTag.length > 0) { if (filter.storageTag && filter.storageTag.length > 0) {
appendWhereClause(`f.storage_tag="${filter.storageTag}"`); appendWhereClause(`f.storage_tag="${filter.storageTag}"`);
} }
if(filter.terms && filter.terms.length > 0) { if (filter.terms && filter.terms.length > 0) {
const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms); const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms);
if ('fts_match' === queryType) { if ('fts_match' === queryType) {
@ -606,9 +656,14 @@ module.exports = class FileEntry {
filter.tags = filter.tags.toString(); filter.tags = filter.tags.toString();
} }
if(filter.tags && filter.tags.length > 0) { if (filter.tags && filter.tags.length > 0) {
// build list of quoted tags; filter.tags comes in as a space and/or comma separated values // build list of quoted tags; filter.tags comes in as a space and/or comma separated values
const tags = filter.tags.replace(/,/g, ' ').replace(/\s{2,}/g, ' ').split(' ').map( tag => `"${sanitizeString(tag)}"` ).join(','); const tags = filter.tags
.replace(/,/g, ' ')
.replace(/\s{2,}/g, ' ')
.split(' ')
.map(tag => `"${sanitizeString(tag)}"`)
.join(',');
appendWhereClause( appendWhereClause(
`f.file_id IN ( `f.file_id IN (
@ -623,35 +678,43 @@ module.exports = class FileEntry {
); );
} }
if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { if (
appendWhereClause(`DATETIME(f.upload_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); _.isString(filter.newerThanTimestamp) &&
filter.newerThanTimestamp.length > 0
) {
appendWhereClause(
`DATETIME(f.upload_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`
);
} }
if(_.isNumber(filter.newerThanFileId)) { if (_.isNumber(filter.newerThanFileId)) {
appendWhereClause(`f.file_id > ${filter.newerThanFileId}`); appendWhereClause(`f.file_id > ${filter.newerThanFileId}`);
} }
sql += `${sqlWhere} ${sqlOrderBy}`; sql += `${sqlWhere} ${sqlOrderBy}`;
if(_.isNumber(filter.limit)) { if (_.isNumber(filter.limit)) {
sql += ` LIMIT ${filter.limit}`; sql += ` LIMIT ${filter.limit}`;
} }
sql += ';'; sql += ';';
fileDb.all(sql, (err, rows) => { fileDb.all(sql, (err, rows) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(!rows || 0 === rows.length) { if (!rows || 0 === rows.length) {
return cb(null, []); // no matches return cb(null, []); // no matches
} }
return cb(null, rows.map(r => r.file_id)); return cb(
null,
rows.map(r => r.file_id)
);
}); });
} }
static removeEntry(srcFileEntry, options, cb) { static removeEntry(srcFileEntry, options, cb) {
if(!_.isFunction(cb) && _.isFunction(options)) { if (!_.isFunction(cb) && _.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
@ -662,21 +725,21 @@ module.exports = class FileEntry {
fileDb.run( fileDb.run(
`DELETE FROM file `DELETE FROM file
WHERE file_id = ?;`, WHERE file_id = ?;`,
[ srcFileEntry.fileId ], [srcFileEntry.fileId],
err => { err => {
return callback(err); return callback(err);
} }
); );
}, },
function optionallyRemovePhysicalFile(callback) { function optionallyRemovePhysicalFile(callback) {
if(true !== options.removePhysFile) { if (true !== options.removePhysFile) {
return callback(null); return callback(null);
} }
unlink(srcFileEntry.filePath, err => { unlink(srcFileEntry.filePath, err => {
return callback(err); return callback(err);
}); });
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -685,25 +748,25 @@ module.exports = class FileEntry {
} }
static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) { static moveEntry(srcFileEntry, destAreaTag, destStorageTag, destFileName, cb) {
if(!cb && _.isFunction(destFileName)) { if (!cb && _.isFunction(destFileName)) {
cb = destFileName; cb = destFileName;
destFileName = srcFileEntry.fileName; destFileName = srcFileEntry.fileName;
} }
const srcPath = srcFileEntry.filePath; const srcPath = srcFileEntry.filePath;
const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag); const dstDir = FileEntry.getAreaStorageDirectoryByTag(destStorageTag);
if(!dstDir) { if (!dstDir) {
return cb(Errors.Invalid('Invalid storage tag')); return cb(Errors.Invalid('Invalid storage tag'));
} }
const dstPath = paths.join(dstDir, destFileName); const dstPath = paths.join(dstDir, destFileName);
async.series( async.series(
[ [
function movePhysFile(callback) { function movePhysFile(callback) {
if(srcPath === dstPath) { if (srcPath === dstPath) {
return callback(null); // don't need to move file, but may change areas return callback(null); // don't need to move file, but may change areas
} }
fse.move(srcPath, dstPath, err => { fse.move(srcPath, dstPath, err => {
@ -715,12 +778,12 @@ module.exports = class FileEntry {
`UPDATE file `UPDATE file
SET area_tag = ?, file_name = ?, storage_tag = ? SET area_tag = ?, file_name = ?, storage_tag = ?
WHERE file_id = ?;`, WHERE file_id = ?;`,
[ destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId ], [destAreaTag, destFileName, destStorageTag, srcFileEntry.fileId],
err => { err => {
return callback(err); return callback(err);
} }
); );
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -735,7 +798,7 @@ module.exports = class FileEntry {
// No wildcards? // No wildcards?
const hasSingleCharWC = terms.indexOf('?') > -1; const hasSingleCharWC = terms.indexOf('?') > -1;
if (terms.indexOf('*') === -1 && !hasSingleCharWC) { if (terms.indexOf('*') === -1 && !hasSingleCharWC) {
return [ terms, 'fts_match' ]; return [terms, 'fts_match'];
} }
const prepareLike = () => { const prepareLike = () => {
@ -746,7 +809,7 @@ module.exports = class FileEntry {
// Any ? wildcards? // Any ? wildcards?
if (hasSingleCharWC) { if (hasSingleCharWC) {
return [ prepareLike(terms), 'like' ]; return [prepareLike(terms), 'like'];
} }
const split = terms.replace(/\s+/g, ' ').split(' '); const split = terms.replace(/\s+/g, ' ').split(' ');
@ -764,9 +827,9 @@ module.exports = class FileEntry {
}); });
if (useLike) { if (useLike) {
return [ prepareLike(terms), 'like' ]; return [prepareLike(terms), 'like'];
} }
return [ terms, 'fts_match' ]; return [terms, 'fts_match'];
} }
}; };

View File

@ -2,30 +2,30 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const DownloadQueue = require('./download_queue.js'); const DownloadQueue = require('./download_queue.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const FileEntry = require('./file_entry.js'); const FileEntry = require('./file_entry.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const Events = require('./events.js'); const Events = require('./events.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const SysProps = require('./system_property.js'); const SysProps = require('./system_property.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const pty = require('node-pty'); const pty = require('node-pty');
const temptmp = require('temptmp').createTrackedSession('transfer_file'); const temptmp = require('temptmp').createTrackedSession('transfer_file');
const paths = require('path'); const paths = require('path');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const fse = require('fs-extra'); const fse = require('fs-extra');
// some consts // some consts
const SYSTEM_EOL = require('os').EOL; const SYSTEM_EOL = require('os').EOL;
const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc. const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
/* /*
Notes Notes
@ -44,9 +44,9 @@ const TEMP_SUFFIX = 'enigtf-'; // temp CWD/etc.
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Transfer file', name: 'Transfer file',
desc : 'Sends or receives a file(s)', desc: 'Sends or receives a file(s)',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class TransferFileModule extends MenuModule { exports.getModule = class TransferFileModule extends MenuModule {
@ -59,56 +59,58 @@ exports.getModule = class TransferFileModule extends MenuModule {
// Most options can be set via extraArgs or config block // Most options can be set via extraArgs or config block
// //
const config = Config(); const config = Config();
if(options.extraArgs) { if (options.extraArgs) {
if(options.extraArgs.protocol) { if (options.extraArgs.protocol) {
this.protocolConfig = config.fileTransferProtocols[options.extraArgs.protocol]; this.protocolConfig =
config.fileTransferProtocols[options.extraArgs.protocol];
} }
if(options.extraArgs.direction) { if (options.extraArgs.direction) {
this.direction = options.extraArgs.direction; this.direction = options.extraArgs.direction;
} }
if(options.extraArgs.sendQueue) { if (options.extraArgs.sendQueue) {
this.sendQueue = options.extraArgs.sendQueue; this.sendQueue = options.extraArgs.sendQueue;
} }
if(options.extraArgs.recvFileName) { if (options.extraArgs.recvFileName) {
this.recvFileName = options.extraArgs.recvFileName; this.recvFileName = options.extraArgs.recvFileName;
} }
if(options.extraArgs.recvDirectory) { if (options.extraArgs.recvDirectory) {
this.recvDirectory = options.extraArgs.recvDirectory; this.recvDirectory = options.extraArgs.recvDirectory;
} }
} else { } else {
if(this.config.protocol) { if (this.config.protocol) {
this.protocolConfig = config.fileTransferProtocols[this.config.protocol]; this.protocolConfig = config.fileTransferProtocols[this.config.protocol];
} }
if(this.config.direction) { if (this.config.direction) {
this.direction = this.config.direction; this.direction = this.config.direction;
} }
if(this.config.sendQueue) { if (this.config.sendQueue) {
this.sendQueue = this.config.sendQueue; this.sendQueue = this.config.sendQueue;
} }
if(this.config.recvFileName) { if (this.config.recvFileName) {
this.recvFileName = this.config.recvFileName; this.recvFileName = this.config.recvFileName;
} }
if(this.config.recvDirectory) { if (this.config.recvDirectory) {
this.recvDirectory = this.config.recvDirectory; this.recvDirectory = this.config.recvDirectory;
} }
} }
this.protocolConfig = this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something* this.protocolConfig =
this.direction = this.direction || 'send'; this.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
this.sendQueue = this.sendQueue || []; this.direction = this.direction || 'send';
this.sendQueue = this.sendQueue || [];
// Ensure sendQueue is an array of objects that contain at least a 'path' member // Ensure sendQueue is an array of objects that contain at least a 'path' member
this.sendQueue = this.sendQueue.map(item => { this.sendQueue = this.sendQueue.map(item => {
if(_.isString(item)) { if (_.isString(item)) {
return { path : item }; return { path: item };
} else { } else {
return item; return item;
} }
@ -118,11 +120,11 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
isSending() { isSending() {
return ('send' === this.direction); return 'send' === this.direction;
} }
restorePipeAfterExternalProc() { restorePipeAfterExternalProc() {
if(!this.pipeRestored) { if (!this.pipeRestored) {
this.pipeRestored = true; this.pipeRestored = true;
this.client.restoreDataHandler(); this.client.restoreDataHandler();
@ -134,17 +136,22 @@ exports.getModule = class TransferFileModule extends MenuModule {
// :TODO: Look into this further // :TODO: Look into this further
const allFiles = this.sendQueue.map(f => f.path); const allFiles = this.sendQueue.map(f => f.path);
this.executeExternalProtocolHandlerForSend(allFiles, err => { this.executeExternalProtocolHandlerForSend(allFiles, err => {
if(err) { if (err) {
this.client.log.warn( { files : allFiles, error : err.message }, 'Error sending file(s)' ); this.client.log.warn(
{ files: allFiles, error: err.message },
'Error sending file(s)'
);
} else { } else {
const sentFiles = []; const sentFiles = [];
this.sendQueue.forEach(f => { this.sendQueue.forEach(f => {
f.sent = true; f.sent = true;
sentFiles.push(f.path); sentFiles.push(f.path);
}); });
this.client.log.info( { sentFiles : sentFiles }, `Successfully sent ${sentFiles.length} file(s)` ); this.client.log.info(
{ sentFiles: sentFiles },
`Successfully sent ${sentFiles.length} file(s)`
);
} }
return cb(err); return cb(err);
}); });
@ -196,29 +203,32 @@ exports.getModule = class TransferFileModule extends MenuModule {
// Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc. // Move |src| -> |dst| renaming to file(1).ext, file(2).ext, etc.
// in the case of collisions. // in the case of collisions.
// //
const dstPath = paths.dirname(dst); const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst); const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt); const dstFileSuffix = paths.basename(dst, dstFileExt);
let renameIndex = 0; let renameIndex = 0;
let movedOk = false; let movedOk = false;
let tryDstPath; let tryDstPath;
async.until( async.until(
(callback) => callback(null, movedOk), // until moved OK callback => callback(null, movedOk), // until moved OK
(cb) => { cb => {
if(0 === renameIndex) { if (0 === renameIndex) {
// try originally supplied path first // try originally supplied path first
tryDstPath = dst; tryDstPath = dst;
} else { } else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); tryDstPath = paths.join(
dstPath,
`${dstFileSuffix}(${renameIndex})${dstFileExt}`
);
} }
fse.move(src, tryDstPath, err => { fse.move(src, tryDstPath, err => {
if(err) { if (err) {
if('EEXIST' === err.code) { if ('EEXIST' === err.code) {
renameIndex += 1; renameIndex += 1;
return cb(null); // keep trying return cb(null); // keep trying
} }
return cb(err); return cb(err);
@ -236,25 +246,27 @@ exports.getModule = class TransferFileModule extends MenuModule {
recvFiles(cb) { recvFiles(cb) {
this.executeExternalProtocolHandlerForRecv(err => { this.executeExternalProtocolHandlerForRecv(err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
this.recvFilePaths = []; this.recvFilePaths = [];
if(this.recvFileName) { if (this.recvFileName) {
// //
// file name specified - we expect a single file in |this.recvDirectory| // file name specified - we expect a single file in |this.recvDirectory|
// by the name of |this.recvFileName| // by the name of |this.recvFileName|
// //
const recvFullPath = paths.join(this.recvDirectory, this.recvFileName); const recvFullPath = paths.join(this.recvDirectory, this.recvFileName);
fs.stat(recvFullPath, (err, stats) => { fs.stat(recvFullPath, (err, stats) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(!stats.isFile()) { if (!stats.isFile()) {
return cb(Errors.Invalid('Expected file entry in recv directory')); return cb(
Errors.Invalid('Expected file entry in recv directory')
);
} }
this.recvFilePaths.push(recvFullPath); this.recvFilePaths.push(recvFullPath);
@ -265,83 +277,96 @@ exports.getModule = class TransferFileModule extends MenuModule {
// Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already // Blind Upload (recv): files in |this.recvDirectory| should be named appropriately already
// //
fs.readdir(this.recvDirectory, (err, files) => { fs.readdir(this.recvDirectory, (err, files) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
// stat each to grab files only // stat each to grab files only
async.each(files, (fileName, nextFile) => { async.each(
const recvFullPath = paths.join(this.recvDirectory, fileName); files,
(fileName, nextFile) => {
const recvFullPath = paths.join(this.recvDirectory, fileName);
fs.stat(recvFullPath, (err, stats) => { fs.stat(recvFullPath, (err, stats) => {
if(err) { if (err) {
this.client.log.warn('Failed to stat file', { path : recvFullPath } ); this.client.log.warn('Failed to stat file', {
return nextFile(null); // just try the next one path: recvFullPath,
} });
return nextFile(null); // just try the next one
}
if(stats.isFile()) { if (stats.isFile()) {
this.recvFilePaths.push(recvFullPath); this.recvFilePaths.push(recvFullPath);
} }
return nextFile(null); return nextFile(null);
}); });
}, () => { },
return cb(null); () => {
}); return cb(null);
}
);
}); });
} }
}); });
} }
pathWithTerminatingSeparator(path) { pathWithTerminatingSeparator(path) {
if(path && paths.sep !== path.charAt(path.length - 1)) { if (path && paths.sep !== path.charAt(path.length - 1)) {
path = path + paths.sep; path = path + paths.sep;
} }
return path; return path;
} }
prepAndBuildSendArgs(filePaths, cb) { prepAndBuildSendArgs(filePaths, cb) {
const externalArgs = this.protocolConfig.external['sendArgs']; const externalArgs = this.protocolConfig.external['sendArgs'];
async.waterfall( async.waterfall(
[ [
function getTempFileListPath(callback) { function getTempFileListPath(callback) {
const hasFileList = externalArgs.find(ea => (ea.indexOf('{fileListPath}') > -1) ); const hasFileList = externalArgs.find(
if(!hasFileList) { ea => ea.indexOf('{fileListPath}') > -1
);
if (!hasFileList) {
return callback(null, null); return callback(null, null);
} }
temptmp.open( { prefix : TEMP_SUFFIX, suffix : '.txt' }, (err, tempFileInfo) => { temptmp.open(
if(err) { { prefix: TEMP_SUFFIX, suffix: '.txt' },
return callback(err); // failed to create it (err, tempFileInfo) => {
} if (err) {
return callback(err); // failed to create it
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => {
if(err) {
return callback(err);
} }
fs.close(tempFileInfo.fd, err => {
return callback(err, tempFileInfo.path); fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => {
if (err) {
return callback(err);
}
fs.close(tempFileInfo.fd, err => {
return callback(err, tempFileInfo.path);
});
}); });
}); }
}); );
}, },
function createArgs(tempFileListPath, callback) { function createArgs(tempFileListPath, callback) {
// initial args: ignore {filePaths} as we must break that into it's own sep array items // initial args: ignore {filePaths} as we must break that into it's own sep array items
const args = externalArgs.map(arg => { const args = externalArgs.map(arg => {
return '{filePaths}' === arg ? arg : stringFormat(arg, { return '{filePaths}' === arg
fileListPath : tempFileListPath || '', ? arg
}); : stringFormat(arg, {
fileListPath: tempFileListPath || '',
});
}); });
const filePathsPos = args.indexOf('{filePaths}'); const filePathsPos = args.indexOf('{filePaths}');
if(filePathsPos > -1) { if (filePathsPos > -1) {
// replace {filePaths} with 0:n individual entries in |args| // replace {filePaths} with 0:n individual entries in |args|
args.splice.apply( args, [ filePathsPos, 1 ].concat(filePaths) ); args.splice.apply(args, [filePathsPos, 1].concat(filePaths));
} }
return callback(null, args); return callback(null, args);
} },
], ],
(err, args) => { (err, args) => {
return cb(err, args); return cb(err, args);
@ -350,47 +375,52 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
prepAndBuildRecvArgs(cb) { prepAndBuildRecvArgs(cb) {
const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs'; const argsKey = this.recvFileName ? 'recvArgsNonBatch' : 'recvArgs';
const externalArgs = this.protocolConfig.external[argsKey]; const externalArgs = this.protocolConfig.external[argsKey];
const args = externalArgs.map(arg => stringFormat(arg, { const args = externalArgs.map(arg =>
uploadDir : this.recvDirectory, stringFormat(arg, {
fileName : this.recvFileName || '', uploadDir: this.recvDirectory,
})); fileName: this.recvFileName || '',
})
);
return cb(null, args); return cb(null, args);
} }
executeExternalProtocolHandler(args, cb) { executeExternalProtocolHandler(args, cb) {
const external = this.protocolConfig.external; const external = this.protocolConfig.external;
const cmd = external[`${this.direction}Cmd`]; const cmd = external[`${this.direction}Cmd`];
// support for handlers that need IACs taken care of over Telnet/etc. // support for handlers that need IACs taken care of over Telnet/etc.
const processIACs = const processIACs = external.processIACs || external.escapeTelnet; // deprecated name
external.processIACs ||
external.escapeTelnet; // deprecated name
// :TODO: we should only do this when over Telnet (or derived, such as WebSockets)? // :TODO: we should only do this when over Telnet (or derived, such as WebSockets)?
const IAC = Buffer.from([255]); const IAC = Buffer.from([255]);
const EscapedIAC = Buffer.from([255, 255]); const EscapedIAC = Buffer.from([255, 255]);
this.client.log.debug( this.client.log.debug(
{ cmd : cmd, args : args, tempDir : this.recvDirectory, direction : this.direction }, {
cmd: cmd,
args: args,
tempDir: this.recvDirectory,
direction: this.direction,
},
'Executing external protocol' 'Executing external protocol'
); );
const spawnOpts = { const spawnOpts = {
cols : this.client.term.termWidth, cols: this.client.term.termWidth,
rows : this.client.term.termHeight, rows: this.client.term.termHeight,
cwd : this.recvDirectory, cwd: this.recvDirectory,
encoding : null, // don't bork our data! encoding: null, // don't bork our data!
}; };
const externalProc = pty.spawn(cmd, args, spawnOpts); const externalProc = pty.spawn(cmd, args, spawnOpts);
let dataHits = 0; let dataHits = 0;
const updateActivity = () => { const updateActivity = () => {
if (0 === (dataHits++ % 4)) { if (0 === dataHits++ % 4) {
this.client.explicitActivityTimeUpdate(); this.client.explicitActivityTimeUpdate();
} }
}; };
@ -399,7 +429,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
updateActivity(); updateActivity();
// needed for things like sz/rz // needed for things like sz/rz
if(processIACs) { if (processIACs) {
let iacPos = data.indexOf(EscapedIAC); let iacPos = data.indexOf(EscapedIAC);
if (-1 === iacPos) { if (-1 === iacPos) {
return externalProc.write(data); return externalProc.write(data);
@ -430,7 +460,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
updateActivity(); updateActivity();
// needed for things like sz/rz // needed for things like sz/rz
if(processIACs) { if (processIACs) {
let iacPos = data.indexOf(IAC); let iacPos = data.indexOf(IAC);
if (-1 === iacPos) { if (-1 === iacPos) {
return this.client.term.rawWrite(data); return this.client.term.rawWrite(data);
@ -459,23 +489,33 @@ exports.getModule = class TransferFileModule extends MenuModule {
return this.restorePipeAfterExternalProc(); return this.restorePipeAfterExternalProc();
}); });
externalProc.once('exit', (exitCode) => { externalProc.once('exit', exitCode => {
this.client.log.debug( { cmd : cmd, args : args, exitCode : exitCode }, 'Process exited' ); this.client.log.debug(
{ cmd: cmd, args: args, exitCode: exitCode },
'Process exited'
);
this.restorePipeAfterExternalProc(); this.restorePipeAfterExternalProc();
externalProc.removeAllListeners(); externalProc.removeAllListeners();
return cb(exitCode ? Errors.ExternalProcess(`Process exited with exit code ${exitCode}`, 'EBADEXIT') : null); return cb(
exitCode
? Errors.ExternalProcess(
`Process exited with exit code ${exitCode}`,
'EBADEXIT'
)
: null
);
}); });
} }
executeExternalProtocolHandlerForSend(filePaths, cb) { executeExternalProtocolHandlerForSend(filePaths, cb) {
if(!Array.isArray(filePaths)) { if (!Array.isArray(filePaths)) {
filePaths = [ filePaths ]; filePaths = [filePaths];
} }
this.prepAndBuildSendArgs(filePaths, (err, args) => { this.prepAndBuildSendArgs(filePaths, (err, args) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -486,8 +526,8 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
executeExternalProtocolHandlerForRecv(cb) { executeExternalProtocolHandlerForRecv(cb) {
this.prepAndBuildRecvArgs( (err, args) => { this.prepAndBuildRecvArgs((err, args) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -498,85 +538,115 @@ exports.getModule = class TransferFileModule extends MenuModule {
} }
getMenuResult() { getMenuResult() {
if(this.isSending()) { if (this.isSending()) {
return { sentFileIds : this.sentFileIds }; return { sentFileIds: this.sentFileIds };
} else { } else {
return { recvFilePaths : this.recvFilePaths }; return { recvFilePaths: this.recvFilePaths };
} }
} }
updateSendStats(cb) { updateSendStats(cb) {
let downloadBytes = 0; let downloadBytes = 0;
let downloadCount = 0; let downloadCount = 0;
let fileIds = []; let fileIds = [];
async.each(this.sendQueue, (queueItem, next) => { async.each(
if(!queueItem.sent) { this.sendQueue,
return next(null); (queueItem, next) => {
} if (!queueItem.sent) {
return next(null);
if(queueItem.fileId) {
fileIds.push(queueItem.fileId);
}
if(_.isNumber(queueItem.byteSize)) {
downloadCount += 1;
downloadBytes += queueItem.byteSize;
return next(null);
}
// we just have a path - figure it out
fs.stat(queueItem.path, (err, stats) => {
if(err) {
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' );
} else {
downloadCount += 1;
downloadBytes += stats.size;
} }
return next(null); if (queueItem.fileId) {
}); fileIds.push(queueItem.fileId);
}, () => { }
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalCount, downloadCount);
StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalBytes, downloadBytes);
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount); if (_.isNumber(queueItem.byteSize)) {
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes); downloadCount += 1;
downloadBytes += queueItem.byteSize;
return next(null);
}
fileIds.forEach(fileId => { // we just have a path - figure it out
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1); fs.stat(queueItem.path, (err, stats) => {
}); if (err) {
this.client.log.warn(
{ error: err.message, path: queueItem.path },
'File stat failed'
);
} else {
downloadCount += 1;
downloadBytes += stats.size;
}
return cb(null); return next(null);
}); });
},
() => {
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks
StatLog.incrementUserStat(
this.client.user,
UserProps.FileDlTotalCount,
downloadCount
);
StatLog.incrementUserStat(
this.client.user,
UserProps.FileDlTotalBytes,
downloadBytes
);
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount);
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes);
fileIds.forEach(fileId => {
FileEntry.incrementAndPersistMetaValue(fileId, 'dl_count', 1);
});
return cb(null);
}
);
} }
updateRecvStats(cb) { updateRecvStats(cb) {
let uploadBytes = 0; let uploadBytes = 0;
let uploadCount = 0; let uploadCount = 0;
async.each(this.recvFilePaths, (filePath, next) => { async.each(
// we just have a path - figure it out this.recvFilePaths,
fs.stat(filePath, (err, stats) => { (filePath, next) => {
if(err) { // we just have a path - figure it out
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); fs.stat(filePath, (err, stats) => {
} else { if (err) {
uploadCount += 1; this.client.log.warn(
uploadBytes += stats.size; { error: err.message, path: filePath },
} 'File stat failed'
);
} else {
uploadCount += 1;
uploadBytes += stats.size;
}
return next(null); return next(null);
}); });
}, () => { },
StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalCount, uploadCount); () => {
StatLog.incrementUserStat(this.client.user, UserProps.FileUlTotalBytes, uploadBytes); StatLog.incrementUserStat(
this.client.user,
UserProps.FileUlTotalCount,
uploadCount
);
StatLog.incrementUserStat(
this.client.user,
UserProps.FileUlTotalBytes,
uploadBytes
);
StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount); StatLog.incrementSystemStat(SysProps.FileUlTotalCount, uploadCount);
StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes); StatLog.incrementSystemStat(SysProps.FileUlTotalBytes, uploadBytes);
return cb(null); return cb(null);
}); }
);
} }
initSequence() { initSequence() {
@ -587,41 +657,38 @@ exports.getModule = class TransferFileModule extends MenuModule {
async.series( async.series(
[ [
function validateConfig(callback) { function validateConfig(callback) {
if(self.isSending()) { if (self.isSending()) {
if(!Array.isArray(self.sendQueue)) { if (!Array.isArray(self.sendQueue)) {
self.sendQueue = [ self.sendQueue ]; self.sendQueue = [self.sendQueue];
} }
} }
return callback(null); return callback(null);
}, },
function transferFiles(callback) { function transferFiles(callback) {
if(self.isSending()) { if (self.isSending()) {
self.sendFiles( err => { self.sendFiles(err => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
const sentFileIds = []; const sentFileIds = [];
self.sendQueue.forEach(queueItem => { self.sendQueue.forEach(queueItem => {
if(queueItem.sent && queueItem.fileId) { if (queueItem.sent && queueItem.fileId) {
sentFileIds.push(queueItem.fileId); sentFileIds.push(queueItem.fileId);
} }
}); });
if(sentFileIds.length > 0) { if (sentFileIds.length > 0) {
// remove items we sent from the D/L queue // remove items we sent from the D/L queue
const dlQueue = new DownloadQueue(self.client); const dlQueue = new DownloadQueue(self.client);
const dlFileEntries = dlQueue.removeItems(sentFileIds); const dlFileEntries = dlQueue.removeItems(sentFileIds);
// fire event for downloaded entries // fire event for downloaded entries
Events.emit( Events.emit(Events.getSystemEvents().UserDownload, {
Events.getSystemEvents().UserDownload, user: self.client.user,
{ files: dlFileEntries,
user : self.client.user, });
files : dlFileEntries
}
);
self.sentFileIds = sentFileIds; self.sentFileIds = sentFileIds;
} }
@ -629,29 +696,32 @@ exports.getModule = class TransferFileModule extends MenuModule {
return callback(null); return callback(null);
}); });
} else { } else {
self.recvFiles( err => { self.recvFiles(err => {
return callback(err); return callback(err);
}); });
} }
}, },
function cleanupTempFiles(callback) { function cleanupTempFiles(callback) {
temptmp.cleanup( paths => { temptmp.cleanup(paths => {
Log.debug( { paths : paths, sessionId : temptmp.sessionId }, 'Temporary files cleaned up' ); Log.debug(
{ paths: paths, sessionId: temptmp.sessionId },
'Temporary files cleaned up'
);
}); });
return callback(null); return callback(null);
}, },
function updateUserAndSystemStats(callback) { function updateUserAndSystemStats(callback) {
if(self.isSending()) { if (self.isSending()) {
return self.updateSendStats(callback); return self.updateSendStats(callback);
} else { } else {
return self.updateRecvStats(callback); return self.updateRecvStats(callback);
} }
} },
], ],
err => { err => {
if(err) { if (err) {
self.client.log.warn( { error : err.message }, 'File transfer error'); self.client.log.warn({ error: err.message }, 'File transfer error');
} }
return self.prevMenu(); return self.prevMenu();

View File

@ -2,84 +2,95 @@
'use strict'; 'use strict';
// enigma-bbs // enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File transfer protocol selection', name: 'File transfer protocol selection',
desc : 'Select protocol / method for file transfer', desc: 'Select protocol / method for file transfer',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
protList : 1, protList: 1,
}; };
exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = this.menuConfig.config || {}; this.config = this.menuConfig.config || {};
if(options.extraArgs) { if (options.extraArgs) {
if(options.extraArgs.direction) { if (options.extraArgs.direction) {
this.config.direction = options.extraArgs.direction; this.config.direction = options.extraArgs.direction;
} }
} }
this.config.direction = this.config.direction || 'send'; this.config.direction = this.config.direction || 'send';
this.extraArgs = options.extraArgs; this.extraArgs = options.extraArgs;
if(_.has(options, 'lastMenuResult.sentFileIds')) { if (_.has(options, 'lastMenuResult.sentFileIds')) {
this.sentFileIds = options.lastMenuResult.sentFileIds; this.sentFileIds = options.lastMenuResult.sentFileIds;
} }
if(_.has(options, 'lastMenuResult.recvFilePaths')) { if (_.has(options, 'lastMenuResult.recvFilePaths')) {
this.recvFilePaths = options.lastMenuResult.recvFilePaths; this.recvFilePaths = options.lastMenuResult.recvFilePaths;
} }
this.fallbackOnly = options.lastMenuResult ? true : false; this.fallbackOnly = options.lastMenuResult ? true : false;
this.loadAvailProtocols(); this.loadAvailProtocols();
this.menuMethods = { this.menuMethods = {
selectProtocol : (formData, extraArgs, cb) => { selectProtocol: (formData, extraArgs, cb) => {
const protocol = this.protocols[formData.value.protocol]; const protocol = this.protocols[formData.value.protocol];
const finalExtraArgs = this.extraArgs || {}; const finalExtraArgs = this.extraArgs || {};
Object.assign(finalExtraArgs, { protocol : protocol.protocol, direction : this.config.direction }, extraArgs ); Object.assign(
finalExtraArgs,
{ protocol: protocol.protocol, direction: this.config.direction },
extraArgs
);
const modOpts = { const modOpts = {
extraArgs : finalExtraArgs, extraArgs: finalExtraArgs,
}; };
if('send' === this.config.direction) { if ('send' === this.config.direction) {
return this.gotoMenu(this.config.downloadFilesMenu || 'sendFilesToUser', modOpts, cb); return this.gotoMenu(
this.config.downloadFilesMenu || 'sendFilesToUser',
modOpts,
cb
);
} else { } else {
return this.gotoMenu(this.config.uploadFilesMenu || 'recvFilesFromUser', modOpts, cb); return this.gotoMenu(
this.config.uploadFilesMenu || 'recvFilesFromUser',
modOpts,
cb
);
} }
}, },
}; };
} }
getMenuResult() { getMenuResult() {
if(this.sentFileIds) { if (this.sentFileIds) {
return { sentFileIds : this.sentFileIds }; return { sentFileIds: this.sentFileIds };
} }
if(this.recvFilePaths) { if (this.recvFilePaths) {
return { recvFilePaths : this.recvFilePaths }; return { recvFilePaths: this.recvFilePaths };
} }
} }
initSequence() { initSequence() {
if(this.sentFileIds || this.recvFilePaths) { if (this.sentFileIds || this.recvFilePaths) {
// nothing to do here; move along (we're just falling through) // nothing to do here; move along (we're just falling through)
this.prevMenu(); this.prevMenu();
} else { } else {
@ -89,19 +100,21 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = (self.viewControllers.allViews = new ViewController({
client: self.client,
}));
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu: self,
mciMap : mciData.menu mciMap: mciData.menu,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -113,7 +126,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
protListView.redraw(); protListView.redraw();
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -125,28 +138,32 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
loadAvailProtocols() { loadAvailProtocols() {
this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => { this.protocols = _.map(Config().fileTransferProtocols, (protInfo, protocol) => {
return { return {
text : protInfo.name, // standard text: protInfo.name, // standard
protocol : protocol, protocol: protocol,
name : protInfo.name, name: protInfo.name,
hasBatch : _.has(protInfo, 'external.recvArgs'), hasBatch: _.has(protInfo, 'external.recvArgs'),
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'), hasNonBatch: _.has(protInfo, 'external.recvArgsNonBatch'),
sort : protInfo.sort, sort: protInfo.sort,
}; };
}); });
// Filter out batch vs non-batch only protocols // Filter out batch vs non-batch only protocols
if(this.extraArgs.recvFileName) { // non-batch aka non-blind if (this.extraArgs.recvFileName) {
this.protocols = this.protocols.filter( prot => prot.hasNonBatch ); // non-batch aka non-blind
this.protocols = this.protocols.filter(prot => prot.hasNonBatch);
} else { } else {
this.protocols = this.protocols.filter( prot => prot.hasBatch ); this.protocols = this.protocols.filter(prot => prot.hasBatch);
} }
// natural sort taking explicit orders into consideration // natural sort taking explicit orders into consideration
this.protocols.sort( (a, b) => { this.protocols.sort((a, b) => {
if(_.isNumber(a.sort) && _.isNumber(b.sort)) { if (_.isNumber(a.sort) && _.isNumber(b.sort)) {
return a.sort - b.sort; return a.sort - b.sort;
} else { } else {
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } ); return a.name.localeCompare(b.name, {
sensitivity: false,
numeric: true,
});
} }
}); });
} }

View File

@ -2,58 +2,61 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const EnigAssert = require('./enigma_assert.js'); const EnigAssert = require('./enigma_assert.js');
// deps // deps
const fse = require('fs-extra'); const fse = require('fs-extra');
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling; exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling;
exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling; exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling;
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator; exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) { function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
operation = operation || 'copy'; operation = operation || 'copy';
const dstPath = paths.dirname(dst); const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst); const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt); const dstFileSuffix = paths.basename(dst, dstFileExt);
EnigAssert('move' === operation || 'copy' === operation); EnigAssert('move' === operation || 'copy' === operation);
let renameIndex = 0; let renameIndex = 0;
let opOk = false; let opOk = false;
let tryDstPath; let tryDstPath;
function tryOperation(src, dst, callback) { function tryOperation(src, dst, callback) {
if('move' === operation) { if ('move' === operation) {
fse.move(src, tryDstPath, err => { fse.move(src, tryDstPath, err => {
return callback(err); return callback(err);
}); });
} else if('copy' === operation) { } else if ('copy' === operation) {
fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => { fse.copy(src, tryDstPath, { overwrite: false, errorOnExist: true }, err => {
return callback(err); return callback(err);
}); });
} }
} }
async.until( async.until(
(callback) => callback(null, opOk), // until moved OK callback => callback(null, opOk), // until moved OK
(cb) => { cb => {
if(0 === renameIndex) { if (0 === renameIndex) {
// try originally supplied path first // try originally supplied path first
tryDstPath = dst; tryDstPath = dst;
} else { } else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`); tryDstPath = paths.join(
dstPath,
`${dstFileSuffix}(${renameIndex})${dstFileExt}`
);
} }
tryOperation(src, tryDstPath, err => { tryOperation(src, tryDstPath, err => {
if(err) { if (err) {
// for some reason fs-extra copy doesn't pass err.code // for some reason fs-extra copy doesn't pass err.code
// :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST // :TODO: this is dangerous: submit a PR to fs-extra to set EEXIST
if('EEXIST' === err.code || 'dest already exists.' === err.message) { if ('EEXIST' === err.code || 'dest already exists.' === err.message) {
renameIndex += 1; renameIndex += 1;
return cb(null); // keep trying return cb(null); // keep trying
} }
return cb(err); return cb(err);
@ -82,7 +85,7 @@ function copyFileWithCollisionHandling(src, dst, cb) {
} }
function pathWithTerminatingSeparator(path) { function pathWithTerminatingSeparator(path) {
if(path && paths.sep !== path.charAt(path.length - 1)) { if (path && paths.sep !== path.charAt(path.length - 1)) {
path = path + paths.sep; path = path + paths.sep;
} }
return path; return path;

View File

@ -1,12 +1,12 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const moment = require('moment'); const moment = require('moment');
// Descriptions found in the wild that mean "no description" /facepalm. // Descriptions found in the wild that mean "no description" /facepalm.
const IgnoredDescriptions = [ const IgnoredDescriptions = [
@ -25,14 +25,14 @@ module.exports = class FilesBBSFile {
getDescription(fileName) { getDescription(fileName) {
const entry = this.get(fileName); const entry = this.get(fileName);
if(entry) { if (entry) {
return entry.desc; return entry.desc;
} }
} }
static createFromFile(path, cb) { static createFromFile(path, cb) {
fs.readFile(path, (err, descData) => { fs.readFile(path, (err, descData) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
@ -40,7 +40,7 @@ module.exports = class FilesBBSFile {
const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g); const lines = iconv.decode(descData, 'cp437').split(/\r?\n/g);
const filesBbs = new FilesBBSFile(); const filesBbs = new FilesBBSFile();
const isBadDescription = (desc) => { const isBadDescription = desc => {
return IgnoredDescriptions.find(d => desc.startsWith(d)) ? true : false; return IgnoredDescriptions.find(d => desc.startsWith(d)) ? true : false;
}; };
@ -59,9 +59,7 @@ module.exports = class FilesBBSFile {
const detectDecoder = () => { const detectDecoder = () => {
// helpers // helpers
const regExpTestUpTo = (n, re) => { const regExpTestUpTo = (n, re) => {
return lines return lines.slice(0, n).some(l => re.test(l));
.slice(0, n)
.some(l => re.test(l));
}; };
// //
@ -70,36 +68,37 @@ module.exports = class FilesBBSFile {
const decoders = [ const decoders = [
{ {
// I've been told this is what Syncrhonet uses // I've been told this is what Syncrhonet uses
lineRegExp : /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/, lineRegExp:
detect : function() { /^([^ ]{1,12})\s{1,11}([0-3][0-9]\/[0-3][0-9]\/[1789][0-9]) ([^\r\n]+)$/,
detect: function () {
return regExpTestUpTo(10, this.lineRegExp); return regExpTestUpTo(10, this.lineRegExp);
}, },
extract : function() { extract: function () {
for(let i = 0; i < lines.length; ++i) { for (let i = 0; i < lines.length; ++i) {
let line = lines[i]; let line = lines[i];
const hdr = line.match(this.lineRegExp); const hdr = line.match(this.lineRegExp);
if(!hdr) { if (!hdr) {
continue; continue;
} }
const long = []; const long = [];
for(let j = i + 1; j < lines.length; ++j) { for (let j = i + 1; j < lines.length; ++j) {
line = lines[j]; line = lines[j];
if(!line.startsWith(' ')) { if (!line.startsWith(' ')) {
break; break;
} }
long.push(line.trim()); long.push(line.trim());
++i; ++i;
} }
const desc = long.join('\r\n') || hdr[3] || ''; const desc = long.join('\r\n') || hdr[3] || '';
const fileName = hdr[1]; const fileName = hdr[1];
const timestamp = moment(hdr[2], 'MM/DD/YY'); const timestamp = moment(hdr[2], 'MM/DD/YY');
if(isBadDescription(desc) || !timestamp.isValid()) { if (isBadDescription(desc) || !timestamp.isValid()) {
continue; continue;
} }
filesBbs.entries.set(fileName, { timestamp, desc } ); filesBbs.entries.set(fileName, { timestamp, desc });
} }
} },
}, },
{ {
@ -107,37 +106,41 @@ module.exports = class FilesBBSFile {
// Examples: // Examples:
// - Night Owl CD #7, 1992 // - Night Owl CD #7, 1992
// //
lineRegExp : /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/, lineRegExp: /^([^\s]{1,12})\s{2,14}\[0\]\s\s([^\r\n]+)$/,
detect : function() { detect: function () {
return regExpTestUpTo(10, this.lineRegExp); return regExpTestUpTo(10, this.lineRegExp);
}, },
extract : function() { extract: function () {
for(let i = 0; i < lines.length; ++i) { for (let i = 0; i < lines.length; ++i) {
let line = lines[i]; let line = lines[i];
const hdr = line.match(this.lineRegExp); const hdr = line.match(this.lineRegExp);
if(!hdr) { if (!hdr) {
continue; continue;
} }
const long = [ hdr[2].trim() ]; const long = [hdr[2].trim()];
for(let j = i + 1; j < lines.length; ++j) { for (let j = i + 1; j < lines.length; ++j) {
line = lines[j]; line = lines[j];
// -------------------------------------------------v 32 // -------------------------------------------------v 32
if(!line.startsWith(' | ')) { if (
!line.startsWith(
' | '
)
) {
break; break;
} }
long.push(line.substr(33)); long.push(line.substr(33));
++i; ++i;
} }
const desc = long.join('\r\n'); const desc = long.join('\r\n');
const fileName = hdr[1]; const fileName = hdr[1];
if(isBadDescription(desc)) { if (isBadDescription(desc)) {
continue; continue;
} }
filesBbs.entries.set(fileName, { desc } ); filesBbs.entries.set(fileName, { desc });
} }
} },
}, },
{ {
@ -148,36 +151,36 @@ module.exports = class FilesBBSFile {
// Examples // Examples
// - GUS archive @ dk.toastednet.org // - GUS archive @ dk.toastednet.org
// //
lineRegExp : /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/, lineRegExp: /^([^\s]{1,12})\s+\[00\]\s([^\r\n]+)$/,
detect : function() { detect: function () {
return regExpTestUpTo(10, this.lineRegExp); return regExpTestUpTo(10, this.lineRegExp);
}, },
extract : function() { extract: function () {
for(let i = 0; i < lines.length; ++i) { for (let i = 0; i < lines.length; ++i) {
let line = lines[i]; let line = lines[i];
const hdr = line.match(this.lineRegExp); const hdr = line.match(this.lineRegExp);
if(!hdr) { if (!hdr) {
continue; continue;
} }
const long = [ hdr[2].trimRight() ]; const long = [hdr[2].trimRight()];
for(let j = i + 1; j < lines.length; ++j) { for (let j = i + 1; j < lines.length; ++j) {
line = lines[j]; line = lines[j];
if(!line.startsWith('\t\t ')) { if (!line.startsWith('\t\t ')) {
break; break;
} }
long.push(line.substr(4)); long.push(line.substr(4));
++i; ++i;
} }
const desc = long.join('\r\n'); const desc = long.join('\r\n');
const fileName = hdr[1]; const fileName = hdr[1];
if(isBadDescription(desc)) { if (isBadDescription(desc)) {
continue; continue;
} }
filesBbs.entries.set(fileName, { desc } ); filesBbs.entries.set(fileName, { desc });
} }
} },
}, },
{ {
@ -187,41 +190,46 @@ module.exports = class FilesBBSFile {
// Examples: // Examples:
// - Expanding Your BBS CD by David Wolfe, 1995 // - Expanding Your BBS CD by David Wolfe, 1995
// //
lineRegExp : /^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/, lineRegExp:
detect : function() { /^([^ ]{1,12})\s{1,20}([0-9]+)\s\s([0-3][0-9]-[0-3][0-9]-[1789][0-9])\s\s([^\r\n]+)$/,
detect: function () {
return regExpTestUpTo(10, this.lineRegExp); return regExpTestUpTo(10, this.lineRegExp);
}, },
extract : function() { extract: function () {
for(let i = 0; i < lines.length; ++i) { for (let i = 0; i < lines.length; ++i) {
let line = lines[i]; let line = lines[i];
const hdr = line.match(this.lineRegExp); const hdr = line.match(this.lineRegExp);
if(!hdr) { if (!hdr) {
continue; continue;
} }
const firstDescLine = hdr[4].trimRight(); const firstDescLine = hdr[4].trimRight();
const long = [ firstDescLine ]; const long = [firstDescLine];
for(let j = i + 1; j < lines.length; ++j) { for (let j = i + 1; j < lines.length; ++j) {
line = lines[j]; line = lines[j];
if(!line.startsWith(' '.repeat(34))) { if (!line.startsWith(' '.repeat(34))) {
break; break;
} }
long.push(line.substr(34).trimRight()); long.push(line.substr(34).trimRight());
++i; ++i;
} }
const desc = long.join('\r\n'); const desc = long.join('\r\n');
const fileName = hdr[1]; const fileName = hdr[1];
const size = parseInt(hdr[2]); const size = parseInt(hdr[2]);
const timestamp = moment(hdr[3], 'MM-DD-YY'); const timestamp = moment(hdr[3], 'MM-DD-YY');
if(isBadDescription(desc) || isNaN(size) || !timestamp.isValid()) { if (
isBadDescription(desc) ||
isNaN(size) ||
!timestamp.isValid()
) {
continue; continue;
} }
filesBbs.entries.set(fileName, { desc, size, timestamp }); filesBbs.entries.set(fileName, { desc, size, timestamp });
} }
} },
}, },
{ {
@ -235,25 +243,25 @@ module.exports = class FilesBBSFile {
// //
// May contain headers, but we'll just skip 'em. // May contain headers, but we'll just skip 'em.
// //
lineRegExp : /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/, lineRegExp: /^([^ ]{1,12})\s{1,11}([^\r\n]+)$/,
detect : function() { detect: function () {
return regExpTestUpTo(10, this.lineRegExp); return regExpTestUpTo(10, this.lineRegExp);
}, },
extract : function() { extract: function () {
lines.forEach(line => { lines.forEach(line => {
const hdr = line.match(this.lineRegExp); const hdr = line.match(this.lineRegExp);
if(!hdr) { if (!hdr) {
return; // forEach return; // forEach
} }
const fileName = hdr[1].trim(); const fileName = hdr[1].trim();
const desc = hdr[2].trim(); const desc = hdr[2].trim();
if(desc && !isBadDescription(desc)) { if (desc && !isBadDescription(desc)) {
filesBbs.entries.set(fileName, { desc } ); filesBbs.entries.set(fileName, { desc });
} }
}); });
} },
}, },
{ {
@ -261,31 +269,32 @@ module.exports = class FilesBBSFile {
// Examples: // Examples:
// - AMINET CD's & similar // - AMINET CD's & similar
// //
lineRegExp : /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/, lineRegExp: /^(.{1,22}) ([0-9]+)K ([^\r\n]+)$/,
detect : function() { detect: function () {
return regExpTestUpTo(10, this.lineRegExp); return regExpTestUpTo(10, this.lineRegExp);
}, },
extract : function() { extract: function () {
lines.forEach(line => { lines.forEach(line => {
const hdr = line.match(this.tester); const hdr = line.match(this.tester);
if(!hdr) { if (!hdr) {
return; // forEach return; // forEach
} }
const fileName = hdr[1].trim(); const fileName = hdr[1].trim();
let size = parseInt(hdr[2]); let size = parseInt(hdr[2]);
const desc = hdr[3].trim(); const desc = hdr[3].trim();
if(isNaN(size)) { if (isNaN(size)) {
return; // forEach return; // forEach
} }
size *= 1024; // K->bytes. size *= 1024; // K->bytes.
if(desc) { // omit empty entries if (desc) {
filesBbs.entries.set(fileName, { size, desc } ); // omit empty entries
filesBbs.entries.set(fileName, { size, desc });
} }
}); });
} },
}, },
]; ];
@ -294,18 +303,18 @@ module.exports = class FilesBBSFile {
}; };
const decoder = detectDecoder(); const decoder = detectDecoder();
if(!decoder) { if (!decoder) {
return cb(Errors.Invalid('Invalid or unrecognized FILES.BBS format')); return cb(Errors.Invalid('Invalid or unrecognized FILES.BBS format'));
} }
decoder.extract(decoder); decoder.extract(decoder);
return cb( return cb(
filesBbs.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized FILES.BBS format'), filesBbs.entries.size > 0
? null
: Errors.Invalid('Invalid or unrecognized FILES.BBS format'),
filesBbs filesBbs
); );
}); });
} }
}; };

View File

@ -3,36 +3,39 @@
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const _ = require('lodash'); const _ = require('lodash');
// FNV-1a based on work here: https://github.com/wiedi/node-fnv // FNV-1a based on work here: https://github.com/wiedi/node-fnv
module.exports = class FNV1a { module.exports = class FNV1a {
constructor(data) { constructor(data) {
this.hash = 0x811c9dc5; this.hash = 0x811c9dc5;
if(!_.isUndefined(data)) { if (!_.isUndefined(data)) {
this.update(data); this.update(data);
} }
} }
update(data) { update(data) {
if(_.isNumber(data)) { if (_.isNumber(data)) {
data = data.toString(); data = data.toString();
} }
if(_.isString(data)) { if (_.isString(data)) {
data = Buffer.from(data); data = Buffer.from(data);
} }
if(!Buffer.isBuffer(data)) { if (!Buffer.isBuffer(data)) {
throw Errors.Invalid('data must be String or Buffer!'); throw Errors.Invalid('data must be String or Buffer!');
} }
for(let b of data) { for (let b of data) {
this.hash = this.hash ^ b; this.hash = this.hash ^ b;
this.hash += this.hash +=
(this.hash << 24) + (this.hash << 8) + (this.hash << 7) + (this.hash << 24) +
(this.hash << 4) + (this.hash << 1); (this.hash << 8) +
(this.hash << 7) +
(this.hash << 4) +
(this.hash << 1);
} }
return this; return this;
@ -49,4 +52,3 @@ module.exports = class FNV1a {
return this.hash & 0xffffffff; return this.hash & 0xffffffff;
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,20 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const _ = require('lodash'); const _ = require('lodash');
const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i; const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-.]+)?$/i;
const FTN_PATTERN_REGEXP = /^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i; const FTN_PATTERN_REGEXP =
/^([0-9*]+:)?([0-9*]+)(\/[0-9*]+)?(\.[0-9*]+)?(@[a-z0-9\-.*]+)?$/i;
module.exports = class Address { module.exports = class Address {
constructor(addr) { constructor(addr) {
if(addr) { if (addr) {
if(_.isObject(addr)) { if (_.isObject(addr)) {
Object.assign(this, addr); Object.assign(this, addr);
} else if(_.isString(addr)) { } else if (_.isString(addr)) {
const temp = Address.fromString(addr); const temp = Address.fromString(addr);
if(temp) { if (temp) {
Object.assign(this, temp); Object.assign(this, temp);
} }
} }
@ -30,7 +31,7 @@ module.exports = class Address {
} }
isEqual(other) { isEqual(other) {
if(_.isString(other)) { if (_.isString(other)) {
other = Address.fromString(other); other = Address.fromString(other);
} }
@ -45,46 +46,46 @@ module.exports = class Address {
getMatchAddr(pattern) { getMatchAddr(pattern) {
const m = FTN_PATTERN_REGEXP.exec(pattern); const m = FTN_PATTERN_REGEXP.exec(pattern);
if(m) { if (m) {
let addr = { }; let addr = {};
if(m[1]) { if (m[1]) {
addr.zone = m[1].slice(0, -1); addr.zone = m[1].slice(0, -1);
if('*' !== addr.zone) { if ('*' !== addr.zone) {
addr.zone = parseInt(addr.zone); addr.zone = parseInt(addr.zone);
} }
} else { } else {
addr.zone = '*'; addr.zone = '*';
} }
if(m[2]) { if (m[2]) {
addr.net = m[2]; addr.net = m[2];
if('*' !== addr.net) { if ('*' !== addr.net) {
addr.net = parseInt(addr.net); addr.net = parseInt(addr.net);
} }
} else { } else {
addr.net = '*'; addr.net = '*';
} }
if(m[3]) { if (m[3]) {
addr.node = m[3].substr(1); addr.node = m[3].substr(1);
if('*' !== addr.node) { if ('*' !== addr.node) {
addr.node = parseInt(addr.node); addr.node = parseInt(addr.node);
} }
} else { } else {
addr.node = '*'; addr.node = '*';
} }
if(m[4]) { if (m[4]) {
addr.point = m[4].substr(1); addr.point = m[4].substr(1);
if('*' !== addr.point) { if ('*' !== addr.point) {
addr.point = parseInt(addr.point); addr.point = parseInt(addr.point);
} }
} else { } else {
addr.point = '*'; addr.point = '*';
} }
if(m[5]) { if (m[5]) {
addr.domain = m[5].substr(1); addr.domain = m[5].substr(1);
} else { } else {
addr.domain = '*'; addr.domain = '*';
@ -118,7 +119,7 @@ module.exports = class Address {
isPatternMatch(pattern) { isPatternMatch(pattern) {
const addr = this.getMatchAddr(pattern); const addr = this.getMatchAddr(pattern);
if(addr) { if (addr) {
return ( return (
('*' === addr.net || this.net === addr.net) && ('*' === addr.net || this.net === addr.net) &&
('*' === addr.node || this.node === addr.node) && ('*' === addr.node || this.node === addr.node) &&
@ -134,25 +135,25 @@ module.exports = class Address {
static fromString(addrStr) { static fromString(addrStr) {
const m = FTN_ADDRESS_REGEXP.exec(addrStr); const m = FTN_ADDRESS_REGEXP.exec(addrStr);
if(m) { if (m) {
// start with a 2D // start with a 2D
let addr = { let addr = {
net : parseInt(m[2]), net: parseInt(m[2]),
node : parseInt(m[3].substr(1)), node: parseInt(m[3].substr(1)),
}; };
// 3D: Addition of zone if present // 3D: Addition of zone if present
if(m[1]) { if (m[1]) {
addr.zone = parseInt(m[1].slice(0, -1)); addr.zone = parseInt(m[1].slice(0, -1));
} }
// 4D if optional point is present // 4D if optional point is present
if(m[4]) { if (m[4]) {
addr.point = parseInt(m[4].substr(1)); addr.point = parseInt(m[4].substr(1));
} }
// 5D with @domain // 5D with @domain
if(m[5]) { if (m[5]) {
addr.domain = m[5].substr(1); addr.domain = m[5].substr(1);
} }
@ -168,16 +169,16 @@ module.exports = class Address {
// allow for e.g. '4D' or 5 // allow for e.g. '4D' or 5
const dim = parseInt(dimensions.toString()[0]); const dim = parseInt(dimensions.toString()[0]);
if(dim >= 3) { if (dim >= 3) {
addrStr += `/${this.node}`; addrStr += `/${this.node}`;
} }
// missing & .0 are equiv for point // missing & .0 are equiv for point
if(dim >= 4 && this.point) { if (dim >= 4 && this.point) {
addrStr += `.${this.point}`; addrStr += `.${this.point}`;
} }
if(5 === dim && this.domain) { if (5 === dim && this.domain) {
addrStr += `@${this.domain.toLowerCase()}`; addrStr += `@${this.domain.toLowerCase()}`;
} }
@ -185,19 +186,19 @@ module.exports = class Address {
} }
static getComparator() { static getComparator() {
return function(left, right) { return function (left, right) {
let c = (left.zone || 0) - (right.zone || 0); let c = (left.zone || 0) - (right.zone || 0);
if(0 !== c) { if (0 !== c) {
return c; return c;
} }
c = (left.net || 0) - (right.net || 0); c = (left.net || 0) - (right.net || 0);
if(0 !== c) { if (0 !== c) {
return c; return c;
} }
c = (left.node || 0) - (right.node || 0); c = (left.node || 0) - (right.node || 0);
if(0 !== c) { if (0 !== c) {
return c; return c;
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,40 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Address = require('./ftn_address.js'); const Address = require('./ftn_address.js');
const FNV1a = require('./fnv1a.js'); const FNV1a = require('./fnv1a.js');
const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion; const getCleanEnigmaVersion = require('./misc_util.js').getCleanEnigmaVersion;
const _ = require('lodash'); const _ = require('lodash');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const moment = require('moment'); const moment = require('moment');
const os = require('os'); const os = require('os');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
// :TODO: Remove "Ftn" from most of these -- it's implied in the module // :TODO: Remove "Ftn" from most of these -- it's implied in the module
exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer;
exports.getMessageSerialNumber = getMessageSerialNumber; exports.getMessageSerialNumber = getMessageSerialNumber;
exports.getDateFromFtnDateTime = getDateFromFtnDateTime; exports.getDateFromFtnDateTime = getDateFromFtnDateTime;
exports.getDateTimeString = getDateTimeString; exports.getDateTimeString = getDateTimeString;
exports.getMessageIdentifier = getMessageIdentifier; exports.getMessageIdentifier = getMessageIdentifier;
exports.getProductIdentifier = getProductIdentifier; exports.getProductIdentifier = getProductIdentifier;
exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset;
exports.getOrigin = getOrigin; exports.getOrigin = getOrigin;
exports.getTearLine = getTearLine; exports.getTearLine = getTearLine;
exports.getVia = getVia; exports.getVia = getVia;
exports.getIntl = getIntl; exports.getIntl = getIntl;
exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList;
exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList; exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList;
exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries;
exports.getUpdatedPathEntries = getUpdatedPathEntries; exports.getUpdatedPathEntries = getUpdatedPathEntries;
exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding; exports.getCharacterSetIdentifierByEncoding = getCharacterSetIdentifierByEncoding;
exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier; exports.getEncodingFromCharacterSetIdentifier = getEncodingFromCharacterSetIdentifier;
exports.getQuotePrefix = getQuotePrefix; exports.getQuotePrefix = getQuotePrefix;
// //
// Namespace for RFC-4122 name based UUIDs generated from // Namespace for RFC-4122 name based UUIDs generated from
@ -45,9 +45,9 @@ exports.getQuotePrefix = getQuotePrefix;
// See list here: https://github.com/Mithgol/node-fidonet-jam // See list here: https://github.com/Mithgol/node-fidonet-jam
function stringToNullPaddedBuffer(s, bufLen) { function stringToNullPaddedBuffer(s, bufLen) {
let buffer = Buffer.alloc(bufLen); let buffer = Buffer.alloc(bufLen);
let enc = iconv.encode(s, 'CP437').slice(0, bufLen); let enc = iconv.encode(s, 'CP437').slice(0, bufLen);
for(let i = 0; i < enc.length; ++i) { for (let i = 0; i < enc.length; ++i) {
buffer[i] = enc[i]; buffer[i] = enc[i];
} }
return buffer; return buffer;
@ -65,7 +65,7 @@ function getDateFromFtnDateTime(dateTime) {
// "27 Feb 15 00:00:03" // "27 Feb 15 00:00:03"
// //
// :TODO: Use moment.js here // :TODO: Use moment.js here
return moment(Date.parse(dateTime)); // Date.parse() allows funky formats return moment(Date.parse(dateTime)); // Date.parse() allows funky formats
} }
function getDateTimeString(m) { function getDateTimeString(m) {
@ -85,7 +85,7 @@ function getDateTimeString(m) {
// MM = "00" | .. | "59" // MM = "00" | .. | "59"
// SS = "00" | .. | "59" // SS = "00" | .. | "59"
// //
if(!moment.isMoment(m)) { if (!moment.isMoment(m)) {
m = moment(m); m = moment(m);
} }
@ -93,8 +93,8 @@ function getDateTimeString(m) {
} }
function getMessageSerialNumber(messageId) { function getMessageSerialNumber(messageId) {
const msSinceEnigmaEpoc = (Date.now() - Date.UTC(2016, 1, 1)); const msSinceEnigmaEpoc = Date.now() - Date.UTC(2016, 1, 1);
const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16); const hash = Math.abs(new FNV1a(msSinceEnigmaEpoc + messageId).value).toString(16);
return `00000000${hash}`.substr(-8); return `00000000${hash}`.substr(-8);
} }
@ -143,10 +143,13 @@ function getMessageSerialNumber(messageId) {
// //
function getMessageIdentifier(message, address, isNetMail = false) { function getMessageIdentifier(message, address, isNetMail = false) {
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
return isNetMail ? return isNetMail
`${addrStr} ${getMessageSerialNumber(message.messageId)}` : ? `${addrStr} ${getMessageSerialNumber(message.messageId)}`
`${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message.messageId)}` : `${
; message.messageId
}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(
message.messageId
)}`;
} }
// //
@ -158,7 +161,7 @@ function getMessageIdentifier(message, address, isNetMail = false) {
// //
function getProductIdentifier() { function getProductIdentifier() {
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }
@ -181,9 +184,12 @@ function getQuotePrefix(name) {
let initials; let initials;
const parts = name.split(' '); const parts = name.split(' ');
if(parts.length > 1) { if (parts.length > 1) {
// First & Last initials - (Bryan Ashby -> BA) // First & Last initials - (Bryan Ashby -> BA)
initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(0, 1)}`.toUpperCase(); initials = `${parts[0].slice(0, 1)}${parts[parts.length - 1].slice(
0,
1
)}`.toUpperCase();
} else { } else {
// Just use the first two - (NuSkooler -> Nu) // Just use the first two - (NuSkooler -> Nu)
initials = _.capitalize(name.slice(0, 2)); initials = _.capitalize(name.slice(0, 2));
@ -198,17 +204,19 @@ function getQuotePrefix(name) {
// //
function getOrigin(address) { function getOrigin(address) {
const config = Config(); const config = Config();
const origin = _.has(config, 'messageNetworks.originLine') ? const origin = _.has(config, 'messageNetworks.originLine')
config.messageNetworks.originLine : ? config.messageNetworks.originLine
config.general.boardName; : config.general.boardName;
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
return ` * Origin: ${origin} (${addrStr})`; return ` * Origin: ${origin} (${addrStr})`;
} }
function getTearLine() { function getTearLine() {
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `--- ENiGMA 1/2 v${packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `--- ENiGMA 1/2 v${
packageJson.version
} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }
// //
@ -222,9 +230,9 @@ function getVia(address) {
^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] ^AVia: <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone]
<Program Name> <Version> [Serial Number]<CR> <Program Name> <Version> [Serial Number]<CR>
*/ */
const addrStr = new Address(address).toString('5D'); const addrStr = new Address(address).toString('5D');
const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC'); const dateTime = moment().utc().format('YYYYMMDD.HHmmSS.SSSS.UTC');
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`; return `${addrStr} @${dateTime} ENiGMA1/2 ${version}`;
} }
@ -247,10 +255,10 @@ function getAbbreviatedNetNodeList(netNodes) {
let abbrList = ''; let abbrList = '';
let currNet; let currNet;
netNodes.forEach(netNode => { netNodes.forEach(netNode => {
if(_.isString(netNode)) { if (_.isString(netNode)) {
netNode = Address.fromString(netNode); netNode = Address.fromString(netNode);
} }
if(currNet !== netNode.net) { if (currNet !== netNode.net) {
abbrList += `${netNode.net}/`; abbrList += `${netNode.net}/`;
currNet = netNode.net; currNet = netNode.net;
} }
@ -268,12 +276,12 @@ function parseAbbreviatedNetNodeList(netNodes) {
let net; let net;
let m; let m;
let results = []; let results = [];
while(null !== (m = re.exec(netNodes))) { while (null !== (m = re.exec(netNodes))) {
if(m[1] && m[2]) { if (m[1] && m[2]) {
net = parseInt(m[1]); net = parseInt(m[1]);
results.push(new Address( { net : net, node : parseInt(m[2]) } )); results.push(new Address({ net: net, node: parseInt(m[2]) }));
} else if(net) { } else if (net) {
results.push(new Address( { net : net, node : parseInt(m[3]) } )); results.push(new Address({ net: net, node: parseInt(m[3]) }));
} }
} }
@ -316,11 +324,11 @@ function getUpdatedSeenByEntries(existingEntries, additions) {
programs." programs."
*/ */
existingEntries = existingEntries || []; existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) { if (!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ]; existingEntries = [existingEntries];
} }
if(!_.isString(additions)) { if (!_.isString(additions)) {
additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions)); additions = parseAbbreviatedNetNodeList(getAbbreviatedNetNodeList(additions));
} }
@ -338,12 +346,13 @@ function getUpdatedPathEntries(existingEntries, localAddress) {
// :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line
existingEntries = existingEntries || []; existingEntries = existingEntries || [];
if(!_.isArray(existingEntries)) { if (!_.isArray(existingEntries)) {
existingEntries = [ existingEntries ]; existingEntries = [existingEntries];
} }
existingEntries.push(getAbbreviatedNetNodeList( existingEntries.push(
parseAbbreviatedNetNodeList(localAddress))); getAbbreviatedNetNodeList(parseAbbreviatedNetNodeList(localAddress))
);
return existingEntries; return existingEntries;
} }
@ -354,69 +363,68 @@ function getUpdatedPathEntries(existingEntries, localAddress) {
// //
const ENCODING_TO_FTS_5003_001_CHARS = { const ENCODING_TO_FTS_5003_001_CHARS = {
// level 1 - generally should not be used // level 1 - generally should not be used
ascii : [ 'ASCII', 1 ], ascii: ['ASCII', 1],
'us-ascii' : [ 'ASCII', 1 ], 'us-ascii': ['ASCII', 1],
// level 2 - 8 bit, ASCII based // level 2 - 8 bit, ASCII based
cp437 : [ 'CP437', 2 ], cp437: ['CP437', 2],
cp850 : [ 'CP850', 2 ], cp850: ['CP850', 2],
// level 3 - reserved // level 3 - reserved
// level 4 // level 4
utf8 : [ 'UTF-8', 4 ], utf8: ['UTF-8', 4],
'utf-8' : [ 'UTF-8', 4 ], 'utf-8': ['UTF-8', 4],
}; };
function getCharacterSetIdentifierByEncoding(encodingName) { function getCharacterSetIdentifierByEncoding(encodingName) {
const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()]; const value = ENCODING_TO_FTS_5003_001_CHARS[encodingName.toLowerCase()];
return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase(); return value ? `${value[0]} ${value[1]}` : encodingName.toUpperCase();
} }
const CHRSToEncodingTable = { const CHRSToEncodingTable = {
Level1 : { Level1: {
'ASCII' : 'ascii', // ISO-646-1 ASCII: 'ascii', // ISO-646-1
'DUTCH' : 'ascii', // ISO-646 DUTCH: 'ascii', // ISO-646
'FINNISH' : 'ascii', // ISO-646-10 FINNISH: 'ascii', // ISO-646-10
'FRENCH' : 'ascii', // ISO-646 FRENCH: 'ascii', // ISO-646
'CANADIAN' : 'ascii', // ISO-646 CANADIAN: 'ascii', // ISO-646
'GERMAN' : 'ascii', // ISO-646 GERMAN: 'ascii', // ISO-646
'ITALIAN' : 'ascii', // ISO-646 ITALIAN: 'ascii', // ISO-646
'NORWEIG' : 'ascii', // ISO-646 NORWEIG: 'ascii', // ISO-646
'PORTU' : 'ascii', // ISO-646 PORTU: 'ascii', // ISO-646
'SPANISH' : 'iso-656', SPANISH: 'iso-656',
'SWEDISH' : 'ascii', // ISO-646-10 SWEDISH: 'ascii', // ISO-646-10
'SWISS' : 'ascii', // ISO-646 SWISS: 'ascii', // ISO-646
'UK' : 'ascii', // ISO-646 UK: 'ascii', // ISO-646
'ISO-10' : 'ascii', // ISO-646-10 'ISO-10': 'ascii', // ISO-646-10
}, },
Level2 : { Level2: {
'CP437' : 'cp437', CP437: 'cp437',
'CP850' : 'cp850', CP850: 'cp850',
'CP852' : 'cp852', CP852: 'cp852',
'CP866' : 'cp866', CP866: 'cp866',
'CP848' : 'cp848', CP848: 'cp848',
'CP1250' : 'cp1250', CP1250: 'cp1250',
'CP1251' : 'cp1251', CP1251: 'cp1251',
'CP1252' : 'cp1252', CP1252: 'cp1252',
'CP10000' : 'macroman', CP10000: 'macroman',
'LATIN-1' : 'iso-8859-1', 'LATIN-1': 'iso-8859-1',
'LATIN-2' : 'iso-8859-2', 'LATIN-2': 'iso-8859-2',
'LATIN-5' : 'iso-8859-9', 'LATIN-5': 'iso-8859-9',
'LATIN-9' : 'iso-8859-15', 'LATIN-9': 'iso-8859-15',
}, },
Level4 : { Level4: {
'UTF-8' : 'utf8', 'UTF-8': 'utf8',
}, },
DeprecatedMisc : { DeprecatedMisc: {
'IBMPC' : 'cp1250', // :TODO: validate IBMPC: 'cp1250', // :TODO: validate
'+7_FIDO' : 'cp866', '+7_FIDO': 'cp866',
'+7' : 'cp866', '+7': 'cp866',
'MAC' : 'macroman', // :TODO: validate MAC: 'macroman', // :TODO: validate
} },
}; };
// Given 1:N CHRS kludge IDs, try to pick the best encoding we can // Given 1:N CHRS kludge IDs, try to pick the best encoding we can
@ -424,7 +432,7 @@ const CHRSToEncodingTable = {
// http://www.unicode.org/L2/L1999/99325-N.htm // http://www.unicode.org/L2/L1999/99325-N.htm
function getEncodingFromCharacterSetIdentifier(chrs) { function getEncodingFromCharacterSetIdentifier(chrs) {
if (!Array.isArray(chrs)) { if (!Array.isArray(chrs)) {
chrs = [ chrs ]; chrs = [chrs];
} }
const encLevel = (ident, table, level) => { const encLevel = (ident, table, level) => {
@ -448,7 +456,7 @@ function getEncodingFromCharacterSetIdentifier(chrs) {
} }
}); });
mapping.sort( (l, r) => { mapping.sort((l, r) => {
return l.level - r.level; return l.level - r.level;
}); });

View File

@ -15,497 +15,513 @@ const _ = require('lodash');
exports.FullMenuView = FullMenuView; exports.FullMenuView = FullMenuView;
function FullMenuView(options) { function FullMenuView(options) {
options.cursor = options.cursor || 'hide'; options.cursor = options.cursor || 'hide';
options.justify = options.justify || 'left'; options.justify = options.justify || 'left';
MenuView.call(this, options);
MenuView.call(this, options); // Initialize paging
this.pages = [];
this.currentPage = 0;
this.initDefaultWidth();
// Initialize paging // we want page up/page down by default
this.pages = []; if (!_.isObject(options.specialKeyMap)) {
this.currentPage = 0; Object.assign(this.specialKeyMap, {
'page up': ['page up'],
this.initDefaultWidth(); 'page down': ['page down'],
});
// we want page up/page down by default
if (!_.isObject(options.specialKeyMap)) {
Object.assign(this.specialKeyMap, {
'page up': ['page up'],
'page down': ['page down'],
});
}
this.autoAdjustHeightIfEnabled = () => {
if (this.autoAdjustHeight) {
this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing);
this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row);
} }
this.positionCacheExpired = true; this.autoAdjustHeightIfEnabled = () => {
}; if (this.autoAdjustHeight) {
this.dimens.height =
this.autoAdjustHeightIfEnabled(); this.items.length * (this.itemSpacing + 1) - this.itemSpacing;
this.dimens.height = Math.min(
this.clearPage = () => { this.dimens.height,
let width = this.dimens.width; this.client.term.termHeight - this.position.row
if (this.oldDimens) { );
if (this.oldDimens.width > width) {
width = this.oldDimens.width;
}
delete this.oldDimens;
}
for (let i = 0; i < this.dimens.height; i++) {
const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`;
this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`);
}
}
this.cachePositions = () => {
if (this.positionCacheExpired) {
// first, clear the page
this.clearPage();
this.autoAdjustHeightIfEnabled();
this.pages = []; // reset
// Calculate number of items visible per column
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
// handle case where one can fit at the end
if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) {
this.itemsPerRow++;
}
// Final check to make sure we don't try to display more than we have
if (this.itemsPerRow > this.items.length) {
this.itemsPerRow = this.items.length;
}
let col = this.position.col;
let row = this.position.row;
const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar);
let itemInRow = 0;
let itemInCol = 0;
let pageStart = 0;
for (let i = 0; i < this.items.length; ++i) {
itemInRow++;
this.items[i].row = row;
this.items[i].col = col;
this.items[i].itemInRow = itemInRow;
row += this.itemSpacing + 1;
// have to calculate the max length on the last entry
if (i == this.items.length - 1) {
let maxLength = 0;
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
const itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column
// skip for column 0, we need at least one
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
// save previous page
this.pages.push({ start: pageStart, end: i - itemInRow });
// fix the last column processed
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != col) {
break;
}
this.items[i - j].col = this.position.col;
pageStart = i - j;
}
}
// Since this is the last page, save the current page as well
this.pages.push({ start: pageStart, end: i });
}
// also handle going to next column
else if (itemInRow == this.itemsPerRow) {
itemInRow = 0;
// restart row for next column
row = this.position.row;
let maxLength = 0;
for (let j = 0; j < this.itemsPerRow; j++) {
// TODO: handle complex items
let itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (let j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column in the current page
// skip for first column, we need at least one
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
// save previous page
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
// restart page start for next page
pageStart = i - this.itemsPerRow + 1;
// reset
col = this.position.col;
itemInRow = 0;
// fix the last column processed
for (let j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].col = col;
}
}
// increment the column
col += maxLength + spacer.length;
itemInCol++;
} }
this.positionCacheExpired = true;
};
// Set the current page if the current item is focused. this.autoAdjustHeightIfEnabled();
if (this.focusedItemIndex === i) {
this.currentPage = this.pages.length; this.clearPage = () => {
let width = this.dimens.width;
if (this.oldDimens) {
if (this.oldDimens.width > width) {
width = this.oldDimens.width;
}
delete this.oldDimens;
} }
}
}
this.positionCacheExpired = false; for (let i = 0; i < this.dimens.height; i++) {
}; const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`;
this.client.term.write(
`${ansi.goto(
this.position.row + i,
this.position.col
)}${this.getSGR()}${text}`
);
}
};
this.drawItem = (index) => { this.cachePositions = () => {
const item = this.items[index]; if (this.positionCacheExpired) {
if (!item) { // first, clear the page
return; this.clearPage();
}
const cached = this.getRenderCacheItem(index, item.focused); this.autoAdjustHeightIfEnabled();
if (cached) {
return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
}
let text; this.pages = []; // reset
let sgr;
if (item.focused && this.hasFocusItems()) {
const focusItem = this.focusItems[index];
text = focusItem ? focusItem.text : item.text;
sgr = '';
} else if (this.complexItems) {
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
} else {
text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle);
sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
}
let renderLength = strUtil.renderStringLength(text); // Calculate number of items visible per column
if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) { this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; // handle case where one can fit at the end
} if (this.dimens.height > this.itemsPerRow * (this.itemSpacing + 1)) {
this.itemsPerRow++;
}
let padLength = Math.min(item.fixedLength + 1, this.dimens.width); // Final check to make sure we don't try to display more than we have
if (this.itemsPerRow > this.items.length) {
this.itemsPerRow = this.items.length;
}
text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; let col = this.position.col;
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); let row = this.position.row;
this.setRenderCacheItem(index, text, item.focused); const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar);
};
let itemInRow = 0;
let itemInCol = 0;
let pageStart = 0;
for (let i = 0; i < this.items.length; ++i) {
itemInRow++;
this.items[i].row = row;
this.items[i].col = col;
this.items[i].itemInRow = itemInRow;
row += this.itemSpacing + 1;
// have to calculate the max length on the last entry
if (i == this.items.length - 1) {
let maxLength = 0;
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
const itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column
// skip for column 0, we need at least one
if (itemInCol != 0 && col + maxLength > this.dimens.width) {
// save previous page
this.pages.push({ start: pageStart, end: i - itemInRow });
// fix the last column processed
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != col) {
break;
}
this.items[i - j].col = this.position.col;
pageStart = i - j;
}
}
// Since this is the last page, save the current page as well
this.pages.push({ start: pageStart, end: i });
}
// also handle going to next column
else if (itemInRow == this.itemsPerRow) {
itemInRow = 0;
// restart row for next column
row = this.position.row;
let maxLength = 0;
for (let j = 0; j < this.itemsPerRow; j++) {
// TODO: handle complex items
let itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (let j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column in the current page
// skip for first column, we need at least one
if (itemInCol != 0 && col + maxLength > this.dimens.width) {
// save previous page
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
// restart page start for next page
pageStart = i - this.itemsPerRow + 1;
// reset
col = this.position.col;
itemInRow = 0;
// fix the last column processed
for (let j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].col = col;
}
}
// increment the column
col += maxLength + spacer.length;
itemInCol++;
}
// Set the current page if the current item is focused.
if (this.focusedItemIndex === i) {
this.currentPage = this.pages.length;
}
}
}
this.positionCacheExpired = false;
};
this.drawItem = index => {
const item = this.items[index];
if (!item) {
return;
}
const cached = this.getRenderCacheItem(index, item.focused);
if (cached) {
return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
}
let text;
let sgr;
if (item.focused && this.hasFocusItems()) {
const focusItem = this.focusItems[index];
text = focusItem ? focusItem.text : item.text;
sgr = '';
} else if (this.complexItems) {
text = pipeToAnsi(
formatString(
item.focused && this.focusItemFormat
? this.focusItemFormat
: this.itemFormat,
item
)
);
sgr = this.focusItemFormat
? ''
: index === this.focusedItemIndex
? this.getFocusSGR()
: this.getSGR();
} else {
text = strUtil.stylizeString(
item.text,
item.focused ? this.focusTextStyle : this.textStyle
);
sgr = index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR();
}
let renderLength = strUtil.renderStringLength(text);
if (this.hasTextOverflow() && item.col + renderLength > this.dimens.width) {
text =
strUtil.renderSubstr(
text,
0,
this.dimens.width - (item.col + this.textOverflow.length)
) + this.textOverflow;
}
let padLength = Math.min(item.fixedLength + 1, this.dimens.width);
text = `${sgr}${strUtil.pad(
text,
padLength,
this.fillChar,
this.justify
)}${this.getSGR()}`;
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
this.setRenderCacheItem(index, text, item.focused);
};
} }
util.inherits(FullMenuView, MenuView); util.inherits(FullMenuView, MenuView);
FullMenuView.prototype.redraw = function() { FullMenuView.prototype.redraw = function () {
FullMenuView.super_.prototype.redraw.call(this); FullMenuView.super_.prototype.redraw.call(this);
this.cachePositions(); this.cachePositions();
if (this.items.length) { if (this.items.length) {
for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) { for (
this.items[i].focused = this.focusedItemIndex === i; let i = this.pages[this.currentPage].start;
this.drawItem(i); i <= this.pages[this.currentPage].end;
++i
) {
this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i);
}
} }
}
}; };
FullMenuView.prototype.setHeight = function(height) { FullMenuView.prototype.setHeight = function (height) {
this.oldDimens = Object.assign({}, this.dimens);
FullMenuView.super_.prototype.setHeight.call(this, height);
this.positionCacheExpired = true;
this.autoAdjustHeight = false;
};
FullMenuView.prototype.setWidth = function(width) {
this.oldDimens = Object.assign({}, this.dimens);
FullMenuView.super_.prototype.setWidth.call(this, width);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setTextOverflow = function(overflow) {
FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
this.positionCacheExpired = true;
}
FullMenuView.prototype.setPosition = function(pos) {
FullMenuView.super_.prototype.setPosition.call(this, pos);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setFocus = function(focused) {
FullMenuView.super_.prototype.setFocus.call(this, focused);
this.positionCacheExpired = true;
this.autoAdjustHeight = false;
this.redraw();
};
FullMenuView.prototype.setFocusItemIndex = function(index) {
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
};
FullMenuView.prototype.onKeyPress = function(ch, key) {
if (key) {
if (this.isKeyMapped('up', key.name)) {
this.focusPrevious();
} else if (this.isKeyMapped('down', key.name)) {
this.focusNext();
} else if (this.isKeyMapped('left', key.name)) {
this.focusPreviousColumn();
} else if (this.isKeyMapped('right', key.name)) {
this.focusNextColumn();
} else if (this.isKeyMapped('page up', key.name)) {
this.focusPreviousPageItem();
} else if (this.isKeyMapped('page down', key.name)) {
this.focusNextPageItem();
} else if (this.isKeyMapped('home', key.name)) {
this.focusFirst();
} else if (this.isKeyMapped('end', key.name)) {
this.focusLast();
}
}
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
};
FullMenuView.prototype.getData = function() {
const item = this.getItem(this.focusedItemIndex);
return _.isString(item.data) ? item.data : this.focusedItemIndex;
};
FullMenuView.prototype.setItems = function(items) {
// if we have items already, save off their drawing area so we don't leave fragments at redraw
if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens); this.oldDimens = Object.assign({}, this.dimens);
}
FullMenuView.super_.prototype.setItems.call(this, items); FullMenuView.super_.prototype.setHeight.call(this, height);
this.positionCacheExpired = true; this.positionCacheExpired = true;
this.autoAdjustHeight = false;
}; };
FullMenuView.prototype.removeItem = function(index) { FullMenuView.prototype.setWidth = function (width) {
if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens); this.oldDimens = Object.assign({}, this.dimens);
}
FullMenuView.super_.prototype.removeItem.call(this, index); FullMenuView.super_.prototype.setWidth.call(this, width);
this.positionCacheExpired = true;
this.positionCacheExpired = true;
}; };
FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.setTextOverflow = function (overflow) {
if (this.items.length - 1 === this.focusedItemIndex) { FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
this.clearPage();
this.focusedItemIndex = 0;
this.currentPage = 0;
}
else {
this.focusedItemIndex++;
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage();
this.currentPage++;
}
}
this.redraw(); this.positionCacheExpired = true;
FullMenuView.super_.prototype.focusNext.call(this);
}; };
FullMenuView.prototype.focusPrevious = function() { FullMenuView.prototype.setPosition = function (pos) {
if (0 === this.focusedItemIndex) { FullMenuView.super_.prototype.setPosition.call(this, pos);
this.clearPage();
this.focusedItemIndex = this.items.length - 1;
this.currentPage = this.pages.length - 1;
}
else {
this.focusedItemIndex--;
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage();
this.currentPage--;
}
}
this.redraw(); this.positionCacheExpired = true;
FullMenuView.super_.prototype.focusPrevious.call(this);
}; };
FullMenuView.prototype.focusPreviousColumn = function() { FullMenuView.prototype.setFocus = function (focused) {
FullMenuView.super_.prototype.setFocus.call(this, focused);
this.positionCacheExpired = true;
this.autoAdjustHeight = false;
const currentRow = this.items[this.focusedItemIndex].itemInRow; this.redraw();
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
if (this.focusedItemIndex < 0) {
this.clearPage();
const lastItemRow = this.items[this.items.length - 1].itemInRow;
if (lastItemRow > currentRow) {
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
}
else {
// can't go to same column, so go to last item
this.focusedItemIndex = this.items.length - 1;
}
// set to last page
this.currentPage = this.pages.length - 1;
}
else {
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage();
this.currentPage--;
}
}
this.redraw();
// TODO: This isn't specific to Previous, may want to replace in the future
FullMenuView.super_.prototype.focusPrevious.call(this);
}; };
FullMenuView.prototype.focusNextColumn = function() { FullMenuView.prototype.setFocusItemIndex = function (index) {
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
};
const currentRow = this.items[this.focusedItemIndex].itemInRow; FullMenuView.prototype.onKeyPress = function (ch, key) {
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; if (key) {
if (this.focusedItemIndex > this.items.length - 1) { if (this.isKeyMapped('up', key.name)) {
this.focusedItemIndex = currentRow - 1; this.focusPrevious();
this.currentPage = 0; } else if (this.isKeyMapped('down', key.name)) {
this.clearPage(); this.focusNext();
} } else if (this.isKeyMapped('left', key.name)) {
else if (this.focusedItemIndex > this.pages[this.currentPage].end) { this.focusPreviousColumn();
} else if (this.isKeyMapped('right', key.name)) {
this.focusNextColumn();
} else if (this.isKeyMapped('page up', key.name)) {
this.focusPreviousPageItem();
} else if (this.isKeyMapped('page down', key.name)) {
this.focusNextPageItem();
} else if (this.isKeyMapped('home', key.name)) {
this.focusFirst();
} else if (this.isKeyMapped('end', key.name)) {
this.focusLast();
}
}
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
};
FullMenuView.prototype.getData = function () {
const item = this.getItem(this.focusedItemIndex);
return _.isString(item.data) ? item.data : this.focusedItemIndex;
};
FullMenuView.prototype.setItems = function (items) {
// if we have items already, save off their drawing area so we don't leave fragments at redraw
if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens);
}
FullMenuView.super_.prototype.setItems.call(this, items);
this.positionCacheExpired = true;
};
FullMenuView.prototype.removeItem = function (index) {
if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens);
}
FullMenuView.super_.prototype.removeItem.call(this, index);
this.positionCacheExpired = true;
};
FullMenuView.prototype.focusNext = function () {
if (this.items.length - 1 === this.focusedItemIndex) {
this.clearPage();
this.focusedItemIndex = 0;
this.currentPage = 0;
} else {
this.focusedItemIndex++;
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage();
this.currentPage++;
}
}
this.redraw();
FullMenuView.super_.prototype.focusNext.call(this);
};
FullMenuView.prototype.focusPrevious = function () {
if (0 === this.focusedItemIndex) {
this.clearPage();
this.focusedItemIndex = this.items.length - 1;
this.currentPage = this.pages.length - 1;
} else {
this.focusedItemIndex--;
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage();
this.currentPage--;
}
}
this.redraw();
FullMenuView.super_.prototype.focusPrevious.call(this);
};
FullMenuView.prototype.focusPreviousColumn = function () {
const currentRow = this.items[this.focusedItemIndex].itemInRow;
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
if (this.focusedItemIndex < 0) {
this.clearPage();
const lastItemRow = this.items[this.items.length - 1].itemInRow;
if (lastItemRow > currentRow) {
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
} else {
// can't go to same column, so go to last item
this.focusedItemIndex = this.items.length - 1;
}
// set to last page
this.currentPage = this.pages.length - 1;
} else {
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage();
this.currentPage--;
}
}
this.redraw();
// TODO: This isn't specific to Previous, may want to replace in the future
FullMenuView.super_.prototype.focusPrevious.call(this);
};
FullMenuView.prototype.focusNextColumn = function () {
const currentRow = this.items[this.focusedItemIndex].itemInRow;
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow;
if (this.focusedItemIndex > this.items.length - 1) {
this.focusedItemIndex = currentRow - 1;
this.currentPage = 0;
this.clearPage();
} else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage();
this.currentPage++;
}
this.redraw();
// TODO: This isn't specific to Next, may want to replace in the future
FullMenuView.super_.prototype.focusNext.call(this);
};
FullMenuView.prototype.focusPreviousPageItem = function () {
// handle first page
if (this.currentPage == 0) {
// Do nothing, page up shouldn't go down on last page
return;
}
this.currentPage--;
this.focusedItemIndex = this.pages[this.currentPage].start;
this.clearPage(); this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
};
FullMenuView.prototype.focusNextPageItem = function () {
// handle last page
if (this.currentPage == this.pages.length - 1) {
// Do nothing, page up shouldn't go down on last page
return;
}
this.currentPage++; this.currentPage++;
} this.focusedItemIndex = this.pages[this.currentPage].start;
this.clearPage();
this.redraw(); this.redraw();
// TODO: This isn't specific to Next, may want to replace in the future return FullMenuView.super_.prototype.focusNextPageItem.call(this);
FullMenuView.super_.prototype.focusNext.call(this);
}; };
FullMenuView.prototype.focusPreviousPageItem = function() { FullMenuView.prototype.focusFirst = function () {
this.currentPage = 0;
this.focusedItemIndex = 0;
this.clearPage();
// handle first page this.redraw();
if (this.currentPage == 0) { return FullMenuView.super_.prototype.focusFirst.call(this);
// Do nothing, page up shouldn't go down on last page
return;
}
this.currentPage--;
this.focusedItemIndex = this.pages[this.currentPage].start;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
}; };
FullMenuView.prototype.focusNextPageItem = function() { FullMenuView.prototype.focusLast = function () {
this.currentPage = this.pages.length - 1;
this.focusedItemIndex = this.pages[this.currentPage].end;
this.clearPage();
// handle last page this.redraw();
if (this.currentPage == this.pages.length - 1) { return FullMenuView.super_.prototype.focusLast.call(this);
// Do nothing, page up shouldn't go down on last page
return;
}
this.currentPage++;
this.focusedItemIndex = this.pages[this.currentPage].start;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusNextPageItem.call(this);
}; };
FullMenuView.prototype.focusFirst = function() { FullMenuView.prototype.setFocusItems = function (items) {
FullMenuView.super_.prototype.setFocusItems.call(this, items);
this.currentPage = 0; this.positionCacheExpired = true;
this.focusedItemIndex = 0;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusFirst.call(this);
}; };
FullMenuView.prototype.focusLast = function() { FullMenuView.prototype.setItemSpacing = function (itemSpacing) {
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing);
this.currentPage = this.pages.length - 1; this.positionCacheExpired = true;
this.focusedItemIndex = this.pages[this.currentPage].end;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusLast.call(this);
}; };
FullMenuView.prototype.setFocusItems = function(items) { FullMenuView.prototype.setJustify = function (justify) {
FullMenuView.super_.prototype.setFocusItems.call(this, items); FullMenuView.super_.prototype.setJustify.call(this, justify);
this.positionCacheExpired = true;
this.positionCacheExpired = true;
}; };
FullMenuView.prototype.setItemSpacing = function(itemSpacing) { FullMenuView.prototype.setItemHorizSpacing = function (itemHorizSpacing) {
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
this.positionCacheExpired = true; this.positionCacheExpired = true;
};
FullMenuView.prototype.setJustify = function(justify) {
FullMenuView.super_.prototype.setJustify.call(this, justify);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
this.positionCacheExpired = true;
}; };

View File

@ -1,23 +1,23 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const MenuView = require('./menu_view.js').MenuView; const MenuView = require('./menu_view.js').MenuView;
const strUtil = require('./string_util.js'); const strUtil = require('./string_util.js');
const formatString = require('./string_format'); const formatString = require('./string_format');
const { pipeToAnsi } = require('./color_codes.js'); const { pipeToAnsi } = require('./color_codes.js');
const { goto } = require('./ansi_term.js'); const { goto } = require('./ansi_term.js');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.HorizontalMenuView = HorizontalMenuView; exports.HorizontalMenuView = HorizontalMenuView;
// :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView) // :TODO: Update this to allow scrolling if number of items cannot fit in width (similar to VerticalMenuView)
function HorizontalMenuView(options) { function HorizontalMenuView(options) {
options.cursor = options.cursor || 'hide'; options.cursor = options.cursor || 'hide';
if(!_.isNumber(options.itemSpacing)) { if (!_.isNumber(options.itemSpacing)) {
options.itemSpacing = 1; options.itemSpacing = 1;
} }
@ -27,16 +27,16 @@ function HorizontalMenuView(options) {
var self = this; var self = this;
this.getSpacer = function() { this.getSpacer = function () {
return new Array(self.itemSpacing + 1).join(' '); return new Array(self.itemSpacing + 1).join(' ');
}; };
this.cachePositions = function() { this.cachePositions = function () {
if(this.positionCacheExpired) { if (this.positionCacheExpired) {
var col = self.position.col; var col = self.position.col;
var spacer = self.getSpacer(); var spacer = self.getSpacer();
for(var i = 0; i < self.items.length; ++i) { for (var i = 0; i < self.items.length; ++i) {
self.items[i].col = col; self.items[i].col = col;
col += spacer.length + self.items[i].text.length + spacer.length; col += spacer.length + self.items[i].text.length + spacer.length;
} }
@ -45,75 +45,94 @@ function HorizontalMenuView(options) {
this.positionCacheExpired = false; this.positionCacheExpired = false;
}; };
this.drawItem = function(index) { this.drawItem = function (index) {
assert(!this.positionCacheExpired); assert(!this.positionCacheExpired);
const item = self.items[index]; const item = self.items[index];
if(!item) { if (!item) {
return; return;
} }
let text; let text;
let sgr; let sgr;
if(item.focused && self.hasFocusItems()) { if (item.focused && self.hasFocusItems()) {
const focusItem = self.focusItems[index]; const focusItem = self.focusItems[index];
text = focusItem ? focusItem.text : item.text; text = focusItem ? focusItem.text : item.text;
sgr = ''; sgr = '';
} else if(this.complexItems) { } else if (this.complexItems) {
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); text = pipeToAnsi(
sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); formatString(
item.focused && this.focusItemFormat
? this.focusItemFormat
: this.itemFormat,
item
)
);
sgr = this.focusItemFormat
? ''
: index === self.focusedItemIndex
? self.getFocusSGR()
: self.getSGR();
} else { } else {
text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); text = strUtil.stylizeString(
sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); item.text,
item.focused ? self.focusTextStyle : self.textStyle
);
sgr = index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR();
} }
const drawWidth = strUtil.renderStringLength(text) + (self.getSpacer().length * 2); const drawWidth = strUtil.renderStringLength(text) + self.getSpacer().length * 2;
self.client.term.write( self.client.term.write(
`${goto(self.position.row, item.col)}${sgr}${strUtil.pad(text, drawWidth, self.fillChar, 'center')}` `${goto(self.position.row, item.col)}${sgr}${strUtil.pad(
text,
drawWidth,
self.fillChar,
'center'
)}`
); );
}; };
} }
require('util').inherits(HorizontalMenuView, MenuView); require('util').inherits(HorizontalMenuView, MenuView);
HorizontalMenuView.prototype.setHeight = function(height) { HorizontalMenuView.prototype.setHeight = function (height) {
height = parseInt(height, 10); height = parseInt(height, 10);
assert(1 === height); // nothing else allowed here assert(1 === height); // nothing else allowed here
HorizontalMenuView.super_.prototype.setHeight(this, height); HorizontalMenuView.super_.prototype.setHeight(this, height);
}; };
HorizontalMenuView.prototype.redraw = function() { HorizontalMenuView.prototype.redraw = function () {
HorizontalMenuView.super_.prototype.redraw.call(this); HorizontalMenuView.super_.prototype.redraw.call(this);
this.cachePositions(); this.cachePositions();
for(var i = 0; i < this.items.length; ++i) { for (var i = 0; i < this.items.length; ++i) {
this.items[i].focused = this.focusedItemIndex === i; this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i); this.drawItem(i);
} }
}; };
HorizontalMenuView.prototype.setPosition = function(pos) { HorizontalMenuView.prototype.setPosition = function (pos) {
HorizontalMenuView.super_.prototype.setPosition.call(this, pos); HorizontalMenuView.super_.prototype.setPosition.call(this, pos);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
HorizontalMenuView.prototype.setFocus = function(focused) { HorizontalMenuView.prototype.setFocus = function (focused) {
HorizontalMenuView.super_.prototype.setFocus.call(this, focused); HorizontalMenuView.super_.prototype.setFocus.call(this, focused);
this.redraw(); this.redraw();
}; };
HorizontalMenuView.prototype.setItems = function(items) { HorizontalMenuView.prototype.setItems = function (items) {
HorizontalMenuView.super_.prototype.setItems.call(this, items); HorizontalMenuView.super_.prototype.setItems.call(this, items);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
HorizontalMenuView.prototype.focusNext = function() { HorizontalMenuView.prototype.focusNext = function () {
if(this.items.length - 1 === this.focusedItemIndex) { if (this.items.length - 1 === this.focusedItemIndex) {
this.focusedItemIndex = 0; this.focusedItemIndex = 0;
} else { } else {
this.focusedItemIndex++; this.focusedItemIndex++;
@ -125,9 +144,8 @@ HorizontalMenuView.prototype.focusNext = function() {
HorizontalMenuView.super_.prototype.focusNext.call(this); HorizontalMenuView.super_.prototype.focusNext.call(this);
}; };
HorizontalMenuView.prototype.focusPrevious = function() { HorizontalMenuView.prototype.focusPrevious = function () {
if (0 === this.focusedItemIndex) {
if(0 === this.focusedItemIndex) {
this.focusedItemIndex = this.items.length - 1; this.focusedItemIndex = this.items.length - 1;
} else { } else {
this.focusedItemIndex--; this.focusedItemIndex--;
@ -139,11 +157,11 @@ HorizontalMenuView.prototype.focusPrevious = function() {
HorizontalMenuView.super_.prototype.focusPrevious.call(this); HorizontalMenuView.super_.prototype.focusPrevious.call(this);
}; };
HorizontalMenuView.prototype.onKeyPress = function(ch, key) { HorizontalMenuView.prototype.onKeyPress = function (ch, key) {
if(key) { if (key) {
if(this.isKeyMapped('left', key.name)) { if (this.isKeyMapped('left', key.name)) {
this.focusPrevious(); this.focusPrevious();
} else if(this.isKeyMapped('right', key.name)) { } else if (this.isKeyMapped('right', key.name)) {
this.focusNext(); this.focusNext();
} }
} }
@ -151,7 +169,7 @@ HorizontalMenuView.prototype.onKeyPress = function(ch, key) {
HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key); HorizontalMenuView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
HorizontalMenuView.prototype.getData = function() { HorizontalMenuView.prototype.getData = function () {
const item = this.getItem(this.focusedItemIndex); const item = this.getItem(this.focusedItemIndex);
return _.isString(item.data) ? item.data : this.focusedItemIndex; return _.isString(item.data) ? item.data : this.focusedItemIndex;
}; };

View File

@ -1,12 +1,12 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const View = require('./view.js').View; const View = require('./view.js').View;
const valueWithDefault = require('./misc_util.js').valueWithDefault; const valueWithDefault = require('./misc_util.js').valueWithDefault;
const isPrintable = require('./string_util.js').isPrintable; const isPrintable = require('./string_util.js').isPrintable;
const stylizeString = require('./string_util.js').stylizeString; const stylizeString = require('./string_util.js').stylizeString;
const _ = require('lodash'); const _ = require('lodash');
module.exports = class KeyEntryView extends View { module.exports = class KeyEntryView extends View {
constructor(options) { constructor(options) {
@ -15,12 +15,12 @@ module.exports = class KeyEntryView extends View {
super(options); super(options);
this.eatTabKey = options.eatTabKey || true; this.eatTabKey = options.eatTabKey || true;
this.caseInsensitive = options.caseInsensitive || true; this.caseInsensitive = options.caseInsensitive || true;
if(Array.isArray(options.keys)) { if (Array.isArray(options.keys)) {
if(this.caseInsensitive) { if (this.caseInsensitive) {
this.keys = options.keys.map( k => k.toUpperCase() ); this.keys = options.keys.map(k => k.toUpperCase());
} else { } else {
this.keys = options.keys; this.keys = options.keys;
} }
@ -30,18 +30,22 @@ module.exports = class KeyEntryView extends View {
onKeyPress(ch, key) { onKeyPress(ch, key) {
const drawKey = ch; const drawKey = ch;
if(ch && this.caseInsensitive) { if (ch && this.caseInsensitive) {
ch = ch.toUpperCase(); ch = ch.toUpperCase();
} }
if(drawKey && isPrintable(drawKey) && (!this.keys || this.keys.indexOf(ch) > -1)) { if (
this.redraw(); // sets position drawKey &&
isPrintable(drawKey) &&
(!this.keys || this.keys.indexOf(ch) > -1)
) {
this.redraw(); // sets position
this.client.term.write(stylizeString(ch, this.textStyle)); this.client.term.write(stylizeString(ch, this.textStyle));
} }
this.keyEntered = ch || key.name; this.keyEntered = ch || key.name;
if(key && 'tab' === key.name && !this.eatTabKey) { if (key && 'tab' === key.name && !this.eatTabKey) {
return this.emit('action', 'next', key); return this.emit('action', 'next', key);
} }
@ -50,21 +54,21 @@ module.exports = class KeyEntryView extends View {
} }
setPropertyValue(propName, propValue) { setPropertyValue(propName, propValue) {
switch(propName) { switch (propName) {
case 'eatTabKey' : case 'eatTabKey':
if(_.isBoolean(propValue)) { if (_.isBoolean(propValue)) {
this.eatTabKey = propValue; this.eatTabKey = propValue;
} }
break; break;
case 'caseInsensitive' : case 'caseInsensitive':
if(_.isBoolean(propValue)) { if (_.isBoolean(propValue)) {
this.caseInsensitive = propValue; this.caseInsensitive = propValue;
} }
break; break;
case 'keys' : case 'keys':
if(Array.isArray(propValue)) { if (Array.isArray(propValue)) {
this.keys = propValue; this.keys = propValue;
} }
break; break;
@ -73,5 +77,7 @@ module.exports = class KeyEntryView extends View {
super.setPropertyValue(propName, propValue); super.setPropertyValue(propName, propValue);
} }
getData() { return this.keyEntered; } getData() {
}; return this.keyEntered;
}
};

View File

@ -2,74 +2,90 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const User = require('./user.js'); const User = require('./user.js');
const sysDb = require('./database.js').dbs.system; const sysDb = require('./database.js').dbs.system;
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const SysLogKeys = require('./system_log.js'); const SysLogKeys = require('./system_log.js');
// deps // deps
const moment = require('moment'); const moment = require('moment');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Last Callers', name: 'Last Callers',
desc : 'Last callers to the system', desc: 'Last callers to the system',
author : 'NuSkooler', author: 'NuSkooler',
packageName : 'codes.l33t.enigma.lastcallers' packageName: 'codes.l33t.enigma.lastcallers',
}; };
const MciViewIds = { const MciViewIds = {
callerList : 1, callerList: 1,
}; };
exports.getModule = class LastCallersModule extends MenuModule { exports.getModule = class LastCallersModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {}); this.actionIndicators = _.get(options, 'menuConfig.config.actionIndicators', {});
this.actionIndicatorDefault = _.get(options, 'menuConfig.config.actionIndicatorDefault', '-'); this.actionIndicatorDefault = _.get(
options,
'menuConfig.config.actionIndicatorDefault',
'-'
);
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
async.waterfall( async.waterfall(
[ [
(callback) => { callback => {
this.prepViewController('callers', 0, mciData.menu, err => { this.prepViewController('callers', 0, mciData.menu, err => {
return callback(err); return callback(err);
}); });
}, },
(callback) => { callback => {
this.fetchHistory( (err, loginHistory) => { this.fetchHistory((err, loginHistory) => {
return callback(err, loginHistory); return callback(err, loginHistory);
}); });
}, },
(loginHistory, callback) => { (loginHistory, callback) => {
this.loadUserForHistoryItems(loginHistory, (err, updatedHistory) => { this.loadUserForHistoryItems(
return callback(err, updatedHistory); loginHistory,
}); (err, updatedHistory) => {
return callback(err, updatedHistory);
}
);
}, },
(loginHistory, callback) => { (loginHistory, callback) => {
const callersView = this.viewControllers.callers.getView(MciViewIds.callerList); const callersView = this.viewControllers.callers.getView(
if(!callersView) { MciViewIds.callerList
return cb(Errors.MissingMci(`Missing caller list MCI ${MciViewIds.callerList}`)); );
if (!callersView) {
return cb(
Errors.MissingMci(
`Missing caller list MCI ${MciViewIds.callerList}`
)
);
} }
callersView.setItems(loginHistory); callersView.setItems(loginHistory);
callersView.redraw(); callersView.redraw();
return callback(null); return callback(null);
} },
], ],
err => { err => {
if(err) { if (err) {
this.client.log.warn( { error : err.message }, 'Error loading last callers'); this.client.log.warn(
{ error: err.message },
'Error loading last callers'
);
} }
return cb(err); return cb(err);
} }
@ -79,65 +95,74 @@ exports.getModule = class LastCallersModule extends MenuModule {
getCollapse(conf) { getCollapse(conf) {
let collapse = _.get(this, conf); let collapse = _.get(this, conf);
collapse = collapse && collapse.match(/^([0-9]+)\s*(minutes?|seconds?|hours?|days?|months?)$/); collapse =
if(collapse) { collapse &&
collapse.match(/^([0-9]+)\s*(minutes?|seconds?|hours?|days?|months?)$/);
if (collapse) {
return moment.duration(parseInt(collapse[1]), collapse[2]); return moment.duration(parseInt(collapse[1]), collapse[2]);
} }
} }
fetchHistory(cb) { fetchHistory(cb) {
const callersView = this.viewControllers.callers.getView(MciViewIds.callerList); const callersView = this.viewControllers.callers.getView(MciViewIds.callerList);
if(!callersView || 0 === callersView.dimens.height) { if (!callersView || 0 === callersView.dimens.height) {
return cb(null); return cb(null);
} }
StatLog.getSystemLogEntries( StatLog.getSystemLogEntries(
SysLogKeys.UserLoginHistory, SysLogKeys.UserLoginHistory,
StatLog.Order.TimestampDesc, StatLog.Order.TimestampDesc,
200, // max items to fetch - we need more than max displayed for filtering/etc. 200, // max items to fetch - we need more than max displayed for filtering/etc.
(err, loginHistory) => { (err, loginHistory) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const dateTimeFormat = _.get( const dateTimeFormat = _.get(
this, 'menuConfig.config.dateTimeFormat', this.client.currentTheme.helpers.getDateFormat('short')); this,
'menuConfig.config.dateTimeFormat',
this.client.currentTheme.helpers.getDateFormat('short')
);
loginHistory = loginHistory.map(item => { loginHistory = loginHistory.map(item => {
try { try {
const historyItem = JSON.parse(item.log_value); const historyItem = JSON.parse(item.log_value);
if(_.isObject(historyItem)) { if (_.isObject(historyItem)) {
item.userId = historyItem.userId; item.userId = historyItem.userId;
item.sessionId = historyItem.sessionId; item.sessionId = historyItem.sessionId;
} else { } else {
item.userId = historyItem; // older format item.userId = historyItem; // older format
item.sessionId = '-none-'; item.sessionId = '-none-';
} }
} catch(e) { } catch (e) {
return null; // we'll filter this out return null; // we'll filter this out
} }
item.timestamp = moment(item.timestamp); item.timestamp = moment(item.timestamp);
return Object.assign( return Object.assign(item, {
item, ts: moment(item.timestamp).format(dateTimeFormat),
{ });
ts : moment(item.timestamp).format(dateTimeFormat)
}
);
}); });
const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide'); const hideSysOp = _.get(this, 'menuConfig.config.sysop.hide');
const sysOpCollapse = this.getCollapse('menuConfig.config.sysop.collapse'); const sysOpCollapse = this.getCollapse(
'menuConfig.config.sysop.collapse'
);
const collapseList = (withUserId, minAge) => { const collapseList = (withUserId, minAge) => {
let lastUserId; let lastUserId;
let lastTimestamp; let lastTimestamp;
loginHistory = loginHistory.filter(item => { loginHistory = loginHistory.filter(item => {
const secApart = lastTimestamp ? moment.duration(lastTimestamp.diff(item.timestamp)).asSeconds() : 0; const secApart = lastTimestamp
const collapse = (null === withUserId ? true : withUserId === item.userId) && ? moment
(lastUserId === item.userId) && .duration(lastTimestamp.diff(item.timestamp))
(secApart < minAge); .asSeconds()
: 0;
const collapse =
(null === withUserId ? true : withUserId === item.userId) &&
lastUserId === item.userId &&
secApart < minAge;
lastUserId = item.userId; lastUserId = item.userId;
lastTimestamp = item.timestamp; lastTimestamp = item.timestamp;
@ -146,20 +171,22 @@ exports.getModule = class LastCallersModule extends MenuModule {
}); });
}; };
if(hideSysOp) { if (hideSysOp) {
loginHistory = loginHistory.filter(item => false === User.isRootUserId(item.userId)); loginHistory = loginHistory.filter(
} else if(sysOpCollapse) { item => false === User.isRootUserId(item.userId)
);
} else if (sysOpCollapse) {
collapseList(User.RootUserID, sysOpCollapse.asSeconds()); collapseList(User.RootUserID, sysOpCollapse.asSeconds());
} }
const userCollapse = this.getCollapse('menuConfig.config.user.collapse'); const userCollapse = this.getCollapse('menuConfig.config.user.collapse');
if(userCollapse) { if (userCollapse) {
collapseList(null, userCollapse.asSeconds()); collapseList(null, userCollapse.asSeconds());
} }
return cb( return cb(
null, null,
loginHistory.slice(0, callersView.dimens.height) // trim the fat loginHistory.slice(0, callersView.dimens.height) // trim the fat
); );
} }
); );
@ -167,57 +194,70 @@ exports.getModule = class LastCallersModule extends MenuModule {
loadUserForHistoryItems(loginHistory, cb) { loadUserForHistoryItems(loginHistory, cb) {
const getPropOpts = { const getPropOpts = {
names : [ UserProps.RealName, UserProps.Location, UserProps.Affiliations ] names: [UserProps.RealName, UserProps.Location, UserProps.Affiliations],
}; };
const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k); const actionIndicatorNames = _.map(this.actionIndicators, (v, k) => k);
let indicatorSumsSql; let indicatorSumsSql;
if(actionIndicatorNames.length > 0) { if (actionIndicatorNames.length > 0) {
indicatorSumsSql = actionIndicatorNames.map(i => { indicatorSumsSql = actionIndicatorNames.map(i => {
return `SUM(CASE WHEN log_name='${_.snakeCase(i)}' THEN 1 ELSE 0 END) AS ${i}`; return `SUM(CASE WHEN log_name='${_.snakeCase(
i
)}' THEN 1 ELSE 0 END) AS ${i}`;
}); });
} }
async.map(loginHistory, (item, nextHistoryItem) => { async.map(
User.getUserName(item.userId, (err, userName) => { loginHistory,
if(err) { (item, nextHistoryItem) => {
return nextHistoryItem(null, null); User.getUserName(item.userId, (err, userName) => {
} if (err) {
return nextHistoryItem(null, null);
item.userName = item.text = userName;
User.loadProperties(item.userId, getPropOpts, (err, props) => {
item.location = (props && props[UserProps.Location]) || '';
item.affiliation = item.affils = (props && props[UserProps.Affiliations]) || '';
item.realName = (props && props[UserProps.RealName]) || '';
if(!indicatorSumsSql) {
return nextHistoryItem(null, item);
} }
sysDb.get( item.userName = item.text = userName;
`SELECT ${indicatorSumsSql.join(', ')}
User.loadProperties(item.userId, getPropOpts, (err, props) => {
item.location = (props && props[UserProps.Location]) || '';
item.affiliation = item.affils =
(props && props[UserProps.Affiliations]) || '';
item.realName = (props && props[UserProps.RealName]) || '';
if (!indicatorSumsSql) {
return nextHistoryItem(null, item);
}
sysDb.get(
`SELECT ${indicatorSumsSql.join(', ')}
FROM user_event_log FROM user_event_log
WHERE user_id=? AND session_id=? WHERE user_id=? AND session_id=?
LIMIT 1;`, LIMIT 1;`,
[ item.userId, item.sessionId ], [item.userId, item.sessionId],
(err, results) => { (err, results) => {
if(_.isObject(results)) { if (_.isObject(results)) {
item.actions = ''; item.actions = '';
Object.keys(results).forEach(n => { Object.keys(results).forEach(n => {
const indicator = results[n] > 0 ? this.actionIndicators[n] || this.actionIndicatorDefault : this.actionIndicatorDefault; const indicator =
item[n] = indicator; results[n] > 0
item.actions += indicator; ? this.actionIndicators[n] ||
}); this.actionIndicatorDefault
: this.actionIndicatorDefault;
item[n] = indicator;
item.actions += indicator;
});
}
return nextHistoryItem(null, item);
} }
return nextHistoryItem(null, item); );
} });
);
}); });
}); },
}, (err, mapped) => {
(err, mapped) => { return cb(
return cb(err, mapped.filter(item => item)); // remove deleted err,
}); mapped.filter(item => item)
); // remove deleted
}
);
} }
}; };

View File

@ -2,16 +2,16 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const logger = require('./logger.js'); const logger = require('./logger.js');
// deps // deps
const async = require('async'); const async = require('async');
const listeningServers = {}; // packageName -> info const listeningServers = {}; // packageName -> info
exports.startup = startup; exports.startup = startup;
exports.shutdown = shutdown; exports.shutdown = shutdown;
exports.getServer = getServer; exports.getServer = getServer;
function startup(cb) { function startup(cb) {
return startListening(cb); return startListening(cb);
@ -28,36 +28,44 @@ function getServer(packageName) {
function startListening(cb) { function startListening(cb) {
const moduleUtil = require('./module_util.js'); // late load so we get Config const moduleUtil = require('./module_util.js'); // late load so we get Config
async.each( [ 'login', 'content', 'chat' ], (category, next) => { async.each(
moduleUtil.loadModulesForCategory(`${category}Servers`, (module, nextModule) => { ['login', 'content', 'chat'],
const moduleInst = new module.getModule(); (category, next) => {
try { moduleUtil.loadModulesForCategory(
moduleInst.createServer(err => { `${category}Servers`,
if(err) { (module, nextModule) => {
return nextModule(err); const moduleInst = new module.getModule();
try {
moduleInst.createServer(err => {
if (err) {
return nextModule(err);
}
moduleInst.listen(err => {
if (err) {
return nextModule(err);
}
listeningServers[module.moduleInfo.packageName] = {
instance: moduleInst,
info: module.moduleInfo,
};
return nextModule(null);
});
});
} catch (e) {
logger.log.error(e, 'Exception caught creating server!');
return nextModule(e);
} }
},
moduleInst.listen( err => { err => {
if(err) { return next(err);
return nextModule(err); }
} );
},
listeningServers[module.moduleInfo.packageName] = { err => {
instance : moduleInst, return cb(err);
info : module.moduleInfo, }
}; );
return nextModule(null);
});
});
} catch(e) {
logger.log.error(e, 'Exception caught creating server!');
return nextModule(e);
}
}, err => {
return next(err);
});
}, err => {
return cb(err);
});
} }

View File

@ -2,54 +2,56 @@
'use strict'; 'use strict';
// deps // deps
const bunyan = require('bunyan'); const bunyan = require('bunyan');
const paths = require('path'); const paths = require('path');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const _ = require('lodash'); const _ = require('lodash');
module.exports = class Log { module.exports = class Log {
static init() { static init() {
const Config = require('./config.js').get(); const Config = require('./config.js').get();
const logPath = Config.paths.logs; const logPath = Config.paths.logs;
const err = this.checkLogPath(logPath); const err = this.checkLogPath(logPath);
if(err) { if (err) {
console.error(err.message); // eslint-disable-line no-console console.error(err.message); // eslint-disable-line no-console
return process.exit(); return process.exit();
} }
const logStreams = []; const logStreams = [];
if(_.isObject(Config.logging.rotatingFile)) { if (_.isObject(Config.logging.rotatingFile)) {
Config.logging.rotatingFile.path = paths.join(logPath, Config.logging.rotatingFile.fileName); Config.logging.rotatingFile.path = paths.join(
logPath,
Config.logging.rotatingFile.fileName
);
logStreams.push(Config.logging.rotatingFile); logStreams.push(Config.logging.rotatingFile);
} }
const serializers = { const serializers = {
err : bunyan.stdSerializers.err, // handle 'err' fields with stack/etc. err: bunyan.stdSerializers.err, // handle 'err' fields with stack/etc.
}; };
// try to remove sensitive info by default, e.g. 'password' fields // try to remove sensitive info by default, e.g. 'password' fields
[ 'formData', 'formValue' ].forEach(keyName => { ['formData', 'formValue'].forEach(keyName => {
serializers[keyName] = (fd) => Log.hideSensitive(fd); serializers[keyName] = fd => Log.hideSensitive(fd);
}); });
this.log = bunyan.createLogger({ this.log = bunyan.createLogger({
name : 'ENiGMA½ BBS', name: 'ENiGMA½ BBS',
streams : logStreams, streams: logStreams,
serializers : serializers, serializers: serializers,
}); });
} }
static checkLogPath(logPath) { static checkLogPath(logPath) {
try { try {
if(!fs.statSync(logPath).isDirectory()) { if (!fs.statSync(logPath).isDirectory()) {
return new Error(`${logPath} is not a directory`); return new Error(`${logPath} is not a directory`);
} }
return null; return null;
} catch(e) { } catch (e) {
if('ENOENT' === e.code) { if ('ENOENT' === e.code) {
return new Error(`${logPath} does not exist`); return new Error(`${logPath} does not exist`);
} }
return e; return e;
@ -62,11 +64,14 @@ module.exports = class Log {
// Use a regexp -- we don't know how nested fields we want to seek and destroy may be // Use a regexp -- we don't know how nested fields we want to seek and destroy may be
// //
return JSON.parse( return JSON.parse(
JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => { JSON.stringify(obj).replace(
return `"${valueName}":"********"`; /"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/,
}) (match, valueName) => {
return `"${valueName}":"********"`;
}
)
); );
} catch(e) { } catch (e) {
// be safe and return empty obj! // be safe and return empty obj!
return {}; return {};
} }

View File

@ -2,14 +2,14 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config').get; const Config = require('./config').get;
const logger = require('./logger.js'); const logger = require('./logger.js');
const ServerModule = require('./server_module.js').ServerModule; const ServerModule = require('./server_module.js').ServerModule;
const clientConns = require('./client_connections.js'); const clientConns = require('./client_connections.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
module.exports = class LoginServerModule extends ServerModule { module.exports = class LoginServerModule extends ServerModule {
constructor() { constructor() {
@ -19,7 +19,7 @@ module.exports = class LoginServerModule extends ServerModule {
// :TODO: we need to max connections -- e.g. from config 'maxConnections' // :TODO: we need to max connections -- e.g. from config 'maxConnections'
prepareClient(client, cb) { prepareClient(client, cb) {
if(client.user.isAuthenticated()) { if (client.user.isAuthenticated()) {
return cb(null); return cb(null);
} }
@ -29,7 +29,7 @@ module.exports = class LoginServerModule extends ServerModule {
// Choose initial theme before we have user context // Choose initial theme before we have user context
// //
const preLoginTheme = _.get(Config(), 'theme.preLogin'); const preLoginTheme = _.get(Config(), 'theme.preLogin');
if('*' === preLoginTheme) { if ('*' === preLoginTheme) {
client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || ''; client.user.properties[UserProps.ThemeId] = theme.getRandomTheme() || '';
} else { } else {
client.user.properties[UserProps.ThemeId] = preLoginTheme; client.user.properties[UserProps.ThemeId] = preLoginTheme;
@ -41,24 +41,25 @@ module.exports = class LoginServerModule extends ServerModule {
handleNewClient(client, clientSock, modInfo) { handleNewClient(client, clientSock, modInfo) {
clientSock.on('error', err => { clientSock.on('error', err => {
logger.log.warn({ modInfo, error : err.message }, 'Client socket error'); logger.log.warn({ modInfo, error: err.message }, 'Client socket error');
}); });
// //
// Start tracking the client. A session ID aka client ID // Start tracking the client. A session ID aka client ID
// will be established in addNewClient() below. // will be established in addNewClient() below.
// //
if(_.isUndefined(client.session)) { if (_.isUndefined(client.session)) {
client.session = {}; client.session = {};
} }
client.session.serverName = modInfo.name; client.session.serverName = modInfo.name;
client.session.isSecure = _.isBoolean(client.isSecure) ? client.isSecure : (modInfo.isSecure || false); client.session.isSecure = _.isBoolean(client.isSecure)
? client.isSecure
: modInfo.isSecure || false;
clientConns.addNewClient(client, clientSock); clientConns.addNewClient(client, clientSock);
client.on('ready', readyOptions => { client.on('ready', readyOptions => {
client.startIdleMonitor(); client.startIdleMonitor();
// Go to module -- use default error handler // Go to module -- use default error handler
@ -72,12 +73,15 @@ module.exports = class LoginServerModule extends ServerModule {
}); });
client.on('error', err => { client.on('error', err => {
logger.log.info({ nodeId : client.node, error : err.message }, 'Connection error'); logger.log.info(
{ nodeId: client.node, error: err.message },
'Connection error'
);
}); });
client.on('close', err => { client.on('close', err => {
const logFunc = err ? logger.log.info : logger.log.debug; const logFunc = err ? logger.log.info : logger.log.debug;
logFunc( { nodeId : client.node }, 'Connection closed'); logFunc({ nodeId: client.node }, 'Connection closed');
clientConns.removeClient(client); clientConns.removeClient(client);
}); });
@ -86,7 +90,7 @@ module.exports = class LoginServerModule extends ServerModule {
client.log.info('User idle timeout expired'); client.log.info('User idle timeout expired');
client.menuStack.goto('idleLogoff', err => { client.menuStack.goto('idleLogoff', err => {
if(err) { if (err) {
// likely just doesn't exist // likely just doesn't exist
client.term.write('\nIdle timeout expired. Goodbye!\n'); client.term.write('\nIdle timeout expired. Goodbye!\n');
client.end(); client.end();

View File

@ -1,9 +1,9 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var events = require('events'); var events = require('events');
var assert = require('assert'); var assert = require('assert');
var _ = require('lodash'); var _ = require('lodash');
module.exports = MailPacket; module.exports = MailPacket;
@ -16,7 +16,7 @@ function MailPacket(options) {
require('util').inherits(MailPacket, events.EventEmitter); require('util').inherits(MailPacket, events.EventEmitter);
MailPacket.prototype.read = function(options) { MailPacket.prototype.read = function (options) {
// //
// options.packetPath | opts.packetBuffer: supplies a path-to-file // options.packetPath | opts.packetBuffer: supplies a path-to-file
// or a buffer containing packet data // or a buffer containing packet data
@ -26,11 +26,11 @@ MailPacket.prototype.read = function(options) {
assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer)); assert(_.isString(options.packetPath) || Buffer.isBuffer(options.packetBuffer));
}; };
MailPacket.prototype.write = function(options) { MailPacket.prototype.write = function (options) {
// //
// options.messages[]: array of message(s) to create packets from // options.messages[]: array of message(s) to create packets from
// //
// emits 'packet' event per packet constructed // emits 'packet' event per packet constructed
// //
assert(_.isArray(options.messages)); assert(_.isArray(options.messages));
}; };

View File

@ -1,12 +1,13 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const Address = require('./ftn_address.js'); const Address = require('./ftn_address.js');
const Message = require('./message.js'); const Message = require('./message.js');
exports.getAddressedToInfo = getAddressedToInfo; exports.getAddressedToInfo = getAddressedToInfo;
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
/* /*
Input Output Input Output
@ -26,56 +27,72 @@ function getAddressedToInfo(input) {
const firstAtPos = input.indexOf('@'); const firstAtPos = input.indexOf('@');
if(firstAtPos < 0) { if (firstAtPos < 0) {
let addr = Address.fromString(input); let addr = Address.fromString(input);
if(Address.isValidAddress(addr)) { if (Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : input }; return { flavor: Message.AddressFlavor.FTN, remote: input };
} }
const lessThanPos = input.indexOf('<'); const lessThanPos = input.indexOf('<');
if(lessThanPos < 0) { if (lessThanPos < 0) {
return { name : input, flavor : Message.AddressFlavor.Local }; return { name: input, flavor: Message.AddressFlavor.Local };
} }
const greaterThanPos = input.indexOf('>'); const greaterThanPos = input.indexOf('>');
if(greaterThanPos < lessThanPos) { if (greaterThanPos < lessThanPos) {
return { name : input, flavor : Message.AddressFlavor.Local }; return { name: input, flavor: Message.AddressFlavor.Local };
} }
addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos)); addr = Address.fromString(input.slice(lessThanPos + 1, greaterThanPos));
if(Address.isValidAddress(addr)) { if (Address.isValidAddress(addr)) {
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; return {
name: input.slice(0, lessThanPos).trim(),
flavor: Message.AddressFlavor.FTN,
remote: addr.toString(),
};
} }
return { name : input, flavor : Message.AddressFlavor.Local }; return { name: input, flavor: Message.AddressFlavor.Local };
} }
const lessThanPos = input.indexOf('<'); const lessThanPos = input.indexOf('<');
const greaterThanPos = input.indexOf('>'); const greaterThanPos = input.indexOf('>');
if(lessThanPos > 0 && greaterThanPos > lessThanPos) { if (lessThanPos > 0 && greaterThanPos > lessThanPos) {
const addr = input.slice(lessThanPos + 1, greaterThanPos); const addr = input.slice(lessThanPos + 1, greaterThanPos);
const m = addr.match(EMAIL_REGEX); const m = addr.match(EMAIL_REGEX);
if(m) { if (m) {
return { name : input.slice(0, lessThanPos).trim(), flavor : Message.AddressFlavor.Email, remote : addr }; return {
name: input.slice(0, lessThanPos).trim(),
flavor: Message.AddressFlavor.Email,
remote: addr,
};
} }
return { name : input, flavor : Message.AddressFlavor.Local }; return { name: input, flavor: Message.AddressFlavor.Local };
} }
let m = input.match(EMAIL_REGEX); let m = input.match(EMAIL_REGEX);
if(m) { if (m) {
return { name : input.slice(0, firstAtPos), flavor : Message.AddressFlavor.Email, remote : input }; return {
name: input.slice(0, firstAtPos),
flavor: Message.AddressFlavor.Email,
remote: input,
};
} }
let addr = Address.fromString(input); // 5D? let addr = Address.fromString(input); // 5D?
if(Address.isValidAddress(addr)) { if (Address.isValidAddress(addr)) {
return { flavor : Message.AddressFlavor.FTN, remote : addr.toString() } ; return { flavor: Message.AddressFlavor.FTN, remote: addr.toString() };
} }
addr = Address.fromString(input.slice(firstAtPos + 1).trim()); addr = Address.fromString(input.slice(firstAtPos + 1).trim());
if(Address.isValidAddress(addr)) { if (Address.isValidAddress(addr)) {
return { name : input.slice(0, firstAtPos).trim(), flavor : Message.AddressFlavor.FTN, remote : addr.toString() }; return {
name: input.slice(0, firstAtPos).trim(),
flavor: Message.AddressFlavor.FTN,
remote: addr.toString(),
};
} }
return { name : input, flavor : Message.AddressFlavor.Local }; return { name: input, flavor: Message.AddressFlavor.Local };
} }

View File

@ -1,16 +1,16 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var TextView = require('./text_view.js').TextView; var TextView = require('./text_view.js').TextView;
var miscUtil = require('./misc_util.js'); var miscUtil = require('./misc_util.js');
var strUtil = require('./string_util.js'); var strUtil = require('./string_util.js');
var ansi = require('./ansi_term.js'); var ansi = require('./ansi_term.js');
//var util = require('util'); //var util = require('util');
var assert = require('assert'); var assert = require('assert');
var _ = require('lodash'); var _ = require('lodash');
exports.MaskEditTextView = MaskEditTextView; exports.MaskEditTextView = MaskEditTextView;
// ##/##/#### <--styleSGR2 if fillChar // ##/##/#### <--styleSGR2 if fillChar
// ^- styleSGR1 // ^- styleSGR1
@ -29,59 +29,71 @@ exports.MaskEditTextView = MaskEditTextView;
// * There exists some sort of condition that allows pattern position to get out of sync // * There exists some sort of condition that allows pattern position to get out of sync
function MaskEditTextView(options) { function MaskEditTextView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false; options.resizable = false;
TextView.call(this, options); TextView.call(this, options);
this.initDefaultWidth(); this.initDefaultWidth();
this.cursorPos = { x : 0 }; this.cursorPos = { x: 0 };
this.patternArrayPos = 0; this.patternArrayPos = 0;
var self = this; var self = this;
this.maskPattern = options.maskPattern || ''; this.maskPattern = options.maskPattern || '';
this.clientBackspace = function() { this.clientBackspace = function () {
var fillCharSGR = this.getStyleSGR(3) || this.getSGR(); var fillCharSGR = this.getStyleSGR(3) || this.getSGR();
this.client.term.write('\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR()); this.client.term.write(
'\b' + fillCharSGR + this.fillChar + '\b' + this.getFocusSGR()
);
}; };
this.drawText = function(s) { this.drawText = function (s) {
var textToDraw = strUtil.stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); var textToDraw = strUtil.stylizeString(
s,
this.hasFocus ? this.focusTextStyle : this.textStyle
);
assert(textToDraw.length <= self.patternArray.length); assert(textToDraw.length <= self.patternArray.length);
// draw out the text we have so far // draw out the text we have so far
var i = 0; var i = 0;
var t = 0; var t = 0;
while(i < self.patternArray.length) { while (i < self.patternArray.length) {
if(_.isRegExp(self.patternArray[i])) { if (_.isRegExp(self.patternArray[i])) {
if(t < textToDraw.length) { if (t < textToDraw.length) {
self.client.term.write((self.hasFocus ? self.getFocusSGR() : self.getSGR()) + textToDraw[t]); self.client.term.write(
(self.hasFocus ? self.getFocusSGR() : self.getSGR()) +
textToDraw[t]
);
t++; t++;
} else { } else {
self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar); self.client.term.write((self.getStyleSGR(3) || '') + self.fillChar);
} }
} else { } else {
var styleSgr = this.hasFocus ? (self.getStyleSGR(2) || '') : (self.getStyleSGR(1) || ''); var styleSgr = this.hasFocus
? self.getStyleSGR(2) || ''
: self.getStyleSGR(1) || '';
self.client.term.write(styleSgr + self.maskPattern[i]); self.client.term.write(styleSgr + self.maskPattern[i]);
} }
i++; i++;
} }
}; };
this.buildPattern = function() { this.buildPattern = function () {
self.patternArray = []; self.patternArray = [];
self.maxLength = 0; self.maxLength = 0;
for(var i = 0; i < self.maskPattern.length; i++) { for (var i = 0; i < self.maskPattern.length; i++) {
// :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark! // :TODO: support escaped characters, e.g. \#. Also allow \\ for a '\' mark!
if(self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) { if (self.maskPattern[i] in MaskEditTextView.maskPatternCharacterRegEx) {
self.patternArray.push(MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]); self.patternArray.push(
MaskEditTextView.maskPatternCharacterRegEx[self.maskPattern[i]]
);
++self.maxLength; ++self.maxLength;
} else { } else {
self.patternArray.push(self.maskPattern[i]); self.patternArray.push(self.maskPattern[i]);
@ -89,53 +101,58 @@ function MaskEditTextView(options) {
} }
}; };
this.getEndOfTextColumn = function() { this.getEndOfTextColumn = function () {
return this.position.col + this.patternArrayPos; return this.position.col + this.patternArrayPos;
}; };
this.buildPattern(); this.buildPattern();
} }
require('util').inherits(MaskEditTextView, TextView); require('util').inherits(MaskEditTextView, TextView);
MaskEditTextView.maskPatternCharacterRegEx = { MaskEditTextView.maskPatternCharacterRegEx = {
'#' : /[0-9]/, // Numeric '#': /[0-9]/, // Numeric
'A' : /[a-zA-Z]/, // Alpha A: /[a-zA-Z]/, // Alpha
'@' : /[0-9a-zA-Z]/, // Alphanumeric '@': /[0-9a-zA-Z]/, // Alphanumeric
'&' : /[\w\d\s]/, // Any "printable" 32-126, 128-255 '&': /[\w\d\s]/, // Any "printable" 32-126, 128-255
}; };
MaskEditTextView.prototype.setText = function(text) { MaskEditTextView.prototype.setText = function (text) {
MaskEditTextView.super_.prototype.setText.call(this, text); MaskEditTextView.super_.prototype.setText.call(this, text);
if(this.patternArray) { // :TODO: This is a hack - see TextView ctor note about setText() if (this.patternArray) {
// :TODO: This is a hack - see TextView ctor note about setText()
this.patternArrayPos = this.patternArray.length; this.patternArrayPos = this.patternArray.length;
} }
}; };
MaskEditTextView.prototype.setMaskPattern = function(pattern) { MaskEditTextView.prototype.setMaskPattern = function (pattern) {
this.dimens.width = pattern.length; this.dimens.width = pattern.length;
this.maskPattern = pattern; this.maskPattern = pattern;
this.buildPattern(); this.buildPattern();
}; };
MaskEditTextView.prototype.onKeyPress = function(ch, key) { MaskEditTextView.prototype.onKeyPress = function (ch, key) {
if(key) { if (key) {
if(this.isKeyMapped('backspace', key.name)) { if (this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) { if (this.text.length > 0) {
this.patternArrayPos--; this.patternArrayPos--;
assert(this.patternArrayPos >= 0); assert(this.patternArrayPos >= 0);
if(_.isRegExp(this.patternArray[this.patternArrayPos])) { if (_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1); this.text = this.text.substr(0, this.text.length - 1);
this.clientBackspace(); this.clientBackspace();
} else { } else {
while(this.patternArrayPos >= 0) { while (this.patternArrayPos >= 0) {
if(_.isRegExp(this.patternArray[this.patternArrayPos])) { if (_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1); this.text = this.text.substr(0, this.text.length - 1);
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); this.client.term.write(
ansi.goto(
this.position.row,
this.getEndOfTextColumn() + 1
)
);
this.clientBackspace(); this.clientBackspace();
break; break;
} }
@ -145,62 +162,67 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
} }
return; return;
} else if(this.isKeyMapped('clearLine', key.name)) { } else if (this.isKeyMapped('clearLine', key.name)) {
this.text = ''; this.text = '';
this.patternArrayPos = 0; this.patternArrayPos = 0;
this.setFocus(true); // redraw + adjust cursor this.setFocus(true); // redraw + adjust cursor
return; return;
} }
} }
if(ch && strUtil.isPrintable(ch)) { if (ch && strUtil.isPrintable(ch)) {
if(this.text.length < this.maxLength) { if (this.text.length < this.maxLength) {
ch = strUtil.stylizeString(ch, this.textStyle); ch = strUtil.stylizeString(ch, this.textStyle);
if(!ch.match(this.patternArray[this.patternArrayPos])) { if (!ch.match(this.patternArray[this.patternArrayPos])) {
return; return;
} }
this.text += ch; this.text += ch;
this.patternArrayPos++; this.patternArrayPos++;
while(this.patternArrayPos < this.patternArray.length && while (
!_.isRegExp(this.patternArray[this.patternArrayPos])) this.patternArrayPos < this.patternArray.length &&
{ !_.isRegExp(this.patternArray[this.patternArrayPos])
) {
this.patternArrayPos++; this.patternArrayPos++;
} }
this.redraw(); this.redraw();
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn())); this.client.term.write(
ansi.goto(this.position.row, this.getEndOfTextColumn())
);
} }
} }
MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key); MaskEditTextView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
MaskEditTextView.prototype.setPropertyValue = function(propName, value) { MaskEditTextView.prototype.setPropertyValue = function (propName, value) {
switch(propName) { switch (propName) {
case 'maskPattern' : this.setMaskPattern(value); break; case 'maskPattern':
this.setMaskPattern(value);
break;
} }
MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value); MaskEditTextView.super_.prototype.setPropertyValue.call(this, propName, value);
}; };
MaskEditTextView.prototype.getData = function() { MaskEditTextView.prototype.getData = function () {
var rawData = MaskEditTextView.super_.prototype.getData.call(this); var rawData = MaskEditTextView.super_.prototype.getData.call(this);
if(!rawData || 0 === rawData.length) { if (!rawData || 0 === rawData.length) {
return rawData; return rawData;
} }
var data = ''; var data = '';
assert(rawData.length <= this.patternArray.length); assert(rawData.length <= this.patternArray.length);
var p = 0; var p = 0;
for(var i = 0; i < this.patternArray.length; ++i) { for (var i = 0; i < this.patternArray.length; ++i) {
if(_.isRegExp(this.patternArray[i])) { if (_.isRegExp(this.patternArray[i])) {
data += rawData[p++]; data += rawData[p++];
} else { } else {
data += this.patternArray[i]; data += this.patternArray[i];

View File

@ -9,7 +9,7 @@ const { Errors } = require('./enig_error');
// //
// Number to 32bit MBF // Number to 32bit MBF
const numToMbf32 = (v) => { const numToMbf32 = v => {
const mbf = Buffer.alloc(4); const mbf = Buffer.alloc(4);
if (0 === v) { if (0 === v) {
@ -19,8 +19,8 @@ const numToMbf32 = (v) => {
const ieee = Buffer.alloc(4); const ieee = Buffer.alloc(4);
ieee.writeFloatLE(v, 0); ieee.writeFloatLE(v, 0);
const sign = ieee[3] & 0x80; const sign = ieee[3] & 0x80;
let exp = (ieee[3] << 1) | (ieee[2] >> 7); let exp = (ieee[3] << 1) | (ieee[2] >> 7);
if (exp === 0xfe) { if (exp === 0xfe) {
throw Errors.Invalid(`${v} cannot be converted to mbf`); throw Errors.Invalid(`${v} cannot be converted to mbf`);
@ -36,14 +36,14 @@ const numToMbf32 = (v) => {
return mbf; return mbf;
}; };
const mbf32ToNum = (mbf) => { const mbf32ToNum = mbf => {
if (0 === mbf[3]) { if (0 === mbf[3]) {
return 0.0; return 0.0;
} }
const ieee = Buffer.alloc(4); const ieee = Buffer.alloc(4);
const sign = mbf[2] & 0x80; const sign = mbf[2] & 0x80;
const exp = mbf[3] - 2; const exp = mbf[3] - 2;
ieee[3] = sign | (exp >> 1); ieee[3] = sign | (exp >> 1);
ieee[2] = (exp << 7) | (mbf[2] & 0x7f); ieee[2] = (exp << 7) | (mbf[2] & 0x7f);

View File

@ -2,33 +2,45 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const TextView = require('./text_view.js').TextView; const TextView = require('./text_view.js').TextView;
const View = require('./view.js').View; const View = require('./view.js').View;
const EditTextView = require('./edit_text_view.js').EditTextView; const EditTextView = require('./edit_text_view.js').EditTextView;
const ButtonView = require('./button_view.js').ButtonView; const ButtonView = require('./button_view.js').ButtonView;
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
const FullMenuView = require('./full_menu_view.js').FullMenuView; const FullMenuView = require('./full_menu_view.js').FullMenuView;
const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
const KeyEntryView = require('./key_entry_view.js'); const KeyEntryView = require('./key_entry_view.js');
const MultiLineEditTextView = require('./multi_line_edit_text_view.js').MultiLineEditTextView; const MultiLineEditTextView =
require('./multi_line_edit_text_view.js').MultiLineEditTextView;
const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue; const getPredefinedMCIValue = require('./predefined_mci.js').getPredefinedMCIValue;
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
// deps // deps
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.MCIViewFactory = MCIViewFactory; exports.MCIViewFactory = MCIViewFactory;
function MCIViewFactory(client) { function MCIViewFactory(client) {
this.client = client; this.client = client;
} }
MCIViewFactory.UserViewCodes = [ MCIViewFactory.UserViewCodes = [
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE', 'TL',
'ET',
'ME',
'MT',
'PL',
'BT',
'VM',
'HM',
'FM',
'SM',
'TM',
'KE',
// //
// XY is a special MCI code that allows finding positions // XY is a special MCI code that allows finding positions
@ -38,34 +50,32 @@ MCIViewFactory.UserViewCodes = [
'XY', 'XY',
]; ];
MCIViewFactory.MovementCodes = [ MCIViewFactory.MovementCodes = ['CF', 'CB', 'CU', 'CD'];
'CF', 'CB', 'CU', 'CD',
];
MCIViewFactory.prototype.createFromMCI = function(mci) { MCIViewFactory.prototype.createFromMCI = function (mci) {
assert(mci.code); assert(mci.code);
assert(mci.id > 0); assert(mci.id > 0);
assert(mci.position); assert(mci.position);
var view; var view;
var options = { var options = {
client : this.client, client: this.client,
id : mci.id, id: mci.id,
ansiSGR : mci.SGR, ansiSGR: mci.SGR,
ansiFocusSGR : mci.focusSGR, ansiFocusSGR: mci.focusSGR,
position : { row : mci.position[0], col : mci.position[1] }, position: { row: mci.position[0], col: mci.position[1] },
}; };
// :TODO: These should use setPropertyValue()! // :TODO: These should use setPropertyValue()!
function setOption(pos, name) { function setOption(pos, name) {
if(mci.args.length > pos && mci.args[pos].length > 0) { if (mci.args.length > pos && mci.args[pos].length > 0) {
options[name] = mci.args[pos]; options[name] = mci.args[pos];
} }
} }
function setWidth(pos) { function setWidth(pos) {
if(mci.args.length > pos && mci.args[pos].length > 0) { if (mci.args.length > pos && mci.args[pos].length > 0) {
if(!_.isObject(options.dimens)) { if (!_.isObject(options.dimens)) {
options.dimens = {}; options.dimens = {};
} }
options.dimens.width = parseInt(mci.args[pos], 10); options.dimens.width = parseInt(mci.args[pos], 10);
@ -73,7 +83,11 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
} }
function setFocusOption(pos, name) { function setFocusOption(pos, name) {
if(mci.focusArgs && mci.focusArgs.length > pos && mci.focusArgs[pos].length > 0) { if (
mci.focusArgs &&
mci.focusArgs.length > pos &&
mci.focusArgs[pos].length > 0
) {
options[name] = mci.focusArgs[pos]; options[name] = mci.focusArgs[pos];
} }
} }
@ -81,46 +95,46 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
// //
// Note: Keep this in sync with UserViewCodes above! // Note: Keep this in sync with UserViewCodes above!
// //
switch(mci.code) { switch (mci.code) {
// Text Label (Text View) // Text Label (Text View)
case 'TL' : case 'TL':
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setOption(1, 'justify'); setOption(1, 'justify');
setWidth(2); setWidth(2);
view = new TextView(options); view = new TextView(options);
break; break;
// Edit Text // Edit Text
case 'ET' : case 'ET':
setWidth(0); setWidth(0);
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new EditTextView(options); view = new EditTextView(options);
break; break;
// Masked Edit Text // Masked Edit Text
case 'ME' : case 'ME':
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new MaskEditTextView(options); view = new MaskEditTextView(options);
break; break;
// Multi Line Edit Text // Multi Line Edit Text
case 'MT' : case 'MT':
// :TODO: apply params // :TODO: apply params
view = new MultiLineEditTextView(options); view = new MultiLineEditTextView(options);
break; break;
// Pre-defined Label (Text View) // Pre-defined Label (Text View)
// :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove // :TODO: Currently no real point of PL -- @method replaces this pretty much... probably remove
case 'PL' : case 'PL':
if(mci.args.length > 0) { if (mci.args.length > 0) {
options.text = getPredefinedMCIValue(this.client, mci.args[0]); options.text = getPredefinedMCIValue(this.client, mci.args[0]);
if(options.text) { if (options.text) {
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setOption(2, 'justify'); setOption(2, 'justify');
setWidth(3); setWidth(3);
@ -130,10 +144,10 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
} }
break; break;
// Button // Button
case 'BT' : case 'BT':
if(mci.args.length > 0) { if (mci.args.length > 0) {
options.dimens = { width : parseInt(mci.args[0], 10) }; options.dimens = { width: parseInt(mci.args[0], 10) };
} }
setOption(1, 'textStyle'); setOption(1, 'textStyle');
@ -144,78 +158,78 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
view = new ButtonView(options); view = new ButtonView(options);
break; break;
// Vertial Menu // Vertial Menu
case 'VM' : case 'VM':
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'justify'); setOption(1, 'justify');
setOption(2, 'textStyle'); setOption(2, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new VerticalMenuView(options); view = new VerticalMenuView(options);
break; break;
// Horizontal Menu // Horizontal Menu
case 'HM' : case 'HM':
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new HorizontalMenuView(options); view = new HorizontalMenuView(options);
break; break;
// Full Menu // Full Menu
case 'FM' : case 'FM':
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'itemHorizSpacing'); setOption(1, 'itemHorizSpacing');
setOption(2, 'justify'); setOption(2, 'justify');
setOption(3, 'textStyle'); setOption(3, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new FullMenuView(options); view = new FullMenuView(options);
break; break;
case 'SM' : case 'SM':
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setOption(1, 'justify'); setOption(1, 'justify');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new SpinnerMenuView(options); view = new SpinnerMenuView(options);
break; break;
case 'TM' : case 'TM':
if(mci.args.length > 0) { if (mci.args.length > 0) {
var styleSG1 = { fg : parseInt(mci.args[0], 10) }; var styleSG1 = { fg: parseInt(mci.args[0], 10) };
if(mci.args.length > 1) { if (mci.args.length > 1) {
styleSG1.bg = parseInt(mci.args[1], 10); styleSG1.bg = parseInt(mci.args[1], 10);
} }
options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true); options.styleSG1 = ansi.getSGRFromGraphicRendition(styleSG1, true);
} }
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
view = new ToggleMenuView(options); view = new ToggleMenuView(options);
break; break;
case 'KE' : case 'KE':
view = new KeyEntryView(options); view = new KeyEntryView(options);
break; break;
case 'XY' : case 'XY':
view = new View(options); view = new View(options);
break; break;
default : default:
if(!MCIViewFactory.MovementCodes.includes(mci.code)) { if (!MCIViewFactory.MovementCodes.includes(mci.code)) {
options.text = getPredefinedMCIValue(this.client, mci.code); options.text = getPredefinedMCIValue(this.client, mci.code);
if(_.isString(options.text)) { if (_.isString(options.text)) {
setWidth(0); setWidth(0);
setOption(1, 'textStyle'); setOption(1, 'textStyle');
setOption(2, 'justify'); setOption(2, 'justify');
view = new TextView(options); view = new TextView(options);
} }
@ -223,7 +237,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
break; break;
} }
if(view) { if (view) {
view.mciCode = mci.code; view.mciCode = mci.code;
} }

View File

@ -1,47 +1,51 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const PluginModule = require('./plugin_module.js').PluginModule; const PluginModule = require('./plugin_module.js').PluginModule;
const theme = require('./theme.js'); const theme = require('./theme.js');
const ansi = require('./ansi_term.js'); const ansi = require('./ansi_term.js');
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const menuUtil = require('./menu_util.js'); const menuUtil = require('./menu_util.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
const stringFormat = require('../core/string_format.js'); const stringFormat = require('../core/string_format.js');
const MultiLineEditTextView = require('../core/multi_line_edit_text_view.js').MultiLineEditTextView; const MultiLineEditTextView =
const Errors = require('../core/enig_error.js').Errors; 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 { getPredefinedMCIValue } = require('../core/predefined_mci.js');
// deps // deps
const async = require('async'); const async = require('async');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
const iconvDecode = require('iconv-lite').decode; const iconvDecode = require('iconv-lite').decode;
exports.MenuModule = class MenuModule extends PluginModule { exports.MenuModule = class MenuModule extends PluginModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.menuName = options.menuName; this.menuName = options.menuName;
this.menuConfig = options.menuConfig; this.menuConfig = options.menuConfig;
this.client = options.client; this.client = options.client;
this.menuMethods = {}; // methods called from @method's this.menuMethods = {}; // methods called from @method's
this.menuConfig.config = this.menuConfig.config || {}; this.menuConfig.config = this.menuConfig.config || {};
this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls); this.cls = _.get(this.menuConfig.config, 'cls', Config().menus.cls);
this.viewControllers = {}; this.viewControllers = {};
this.interrupt = (_.get(this.menuConfig.config, 'interrupt', MenuModule.InterruptTypes.Queued)).toLowerCase(); this.interrupt = _.get(
this.menuConfig.config,
'interrupt',
MenuModule.InterruptTypes.Queued
).toLowerCase();
if(MenuModule.InterruptTypes.Realtime === this.interrupt) { if (MenuModule.InterruptTypes.Realtime === this.interrupt) {
this.realTimeInterrupt = 'blocked'; this.realTimeInterrupt = 'blocked';
} }
} }
static get InterruptTypes() { static get InterruptTypes() {
return { return {
Never : 'never', Never: 'never',
Queued : 'queued', Queued: 'queued',
Realtime : 'realtime', Realtime: 'realtime',
}; };
} }
@ -54,13 +58,16 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
initSequence() { initSequence() {
const self = this; const self = this;
const mciData = {}; const mciData = {};
let pausePosition = {row: 0, column: 0}; let pausePosition = { row: 0, column: 0 };
const hasArt = () => { const hasArt = () => {
return _.isString(self.menuConfig.art) || return (
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs')); _.isString(self.menuConfig.art) ||
(Array.isArray(self.menuConfig.art) &&
_.has(self.menuConfig.art[0], 'acs'))
);
}; };
async.waterfall( async.waterfall(
@ -72,7 +79,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
return self.beforeArt(callback); return self.beforeArt(callback);
}, },
function displayMenuArt(callback) { function displayMenuArt(callback) {
if(!hasArt()) { if (!hasArt()) {
return callback(null, null); return callback(null, null);
} }
@ -80,32 +87,39 @@ exports.MenuModule = class MenuModule extends PluginModule {
self.menuConfig.art, self.menuConfig.art,
self.menuConfig.config, self.menuConfig.config,
(err, artData) => { (err, artData) => {
if(err) { if (err) {
self.client.log.trace('Could not display art', { art : self.menuConfig.art, reason : err.message } ); self.client.log.trace('Could not display art', {
art: self.menuConfig.art,
reason: err.message,
});
} else { } else {
mciData.menu = artData.mciMap; mciData.menu = artData.mciMap;
} }
if(artData) { if (artData) {
pausePosition.row = artData.height + 1; pausePosition.row = artData.height + 1;
} }
return callback(null, artData); // any errors are non-fatal return callback(null, artData); // any errors are non-fatal
} }
); );
}, },
function displayPromptArt(artData, callback) { function displayPromptArt(artData, callback) {
if(!_.isString(self.menuConfig.prompt)) { if (!_.isString(self.menuConfig.prompt)) {
return callback(null); return callback(null);
} }
if(!_.isObject(self.menuConfig.promptConfig)) { if (!_.isObject(self.menuConfig.promptConfig)) {
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found')); return callback(
Errors.MissingConfig(
'Prompt specified but no "promptConfig" block found'
)
);
} }
const options = Object.assign({}, self.menuConfig.config); const options = Object.assign({}, self.menuConfig.config);
if(_.isNumber(artData?.height)) { if (_.isNumber(artData?.height)) {
options.startRow = artData.height + 1; options.startRow = artData.height + 1;
} }
@ -113,12 +127,12 @@ exports.MenuModule = class MenuModule extends PluginModule {
self.menuConfig.promptConfig.art, self.menuConfig.promptConfig.art,
options, options,
(err, artData) => { (err, artData) => {
if(artData) { if (artData) {
mciData.prompt = artData.mciMap; mciData.prompt = artData.mciMap;
pausePosition.row = artData.height + 1; pausePosition.row = artData.height + 1;
} }
return callback(err); // pass err here; prompts *must* have art return callback(err); // pass err here; prompts *must* have art
} }
); );
}, },
@ -126,11 +140,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
return self.mciReady(mciData, callback); return self.mciReady(mciData, callback);
}, },
function displayPauseIfRequested(callback) { function displayPauseIfRequested(callback) {
if(!self.shouldPause()) { if (!self.shouldPause()) {
return callback(null, null); return callback(null, null);
} }
if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) { if (
self.client.term.termHeight > 0 &&
pausePosition.row > self.client.termHeight
) {
// If this scrolled, the prompt will go to the bottom of the screen // If this scrolled, the prompt will go to the bottom of the screen
pausePosition.row = self.client.termHeight; pausePosition.row = self.client.termHeight;
} }
@ -141,25 +158,31 @@ exports.MenuModule = class MenuModule extends PluginModule {
self.finishedLoading(); self.finishedLoading();
self.realTimeInterrupt = 'allowed'; self.realTimeInterrupt = 'allowed';
return self.autoNextMenu(callback); return self.autoNextMenu(callback);
} },
], ],
err => { err => {
if(err) { if (err) {
self.client.log.warn('Error during init sequence', { error : err.message } ); self.client.log.warn('Error during init sequence', {
error: err.message,
});
return self.prevMenu( () => { /* dummy */ } ); return self.prevMenu(() => {
/* dummy */
});
} }
} }
); );
} }
beforeArt(cb) { beforeArt(cb) {
if(_.isNumber(this.menuConfig.config.baudRate)) { if (_.isNumber(this.menuConfig.config.baudRate)) {
// :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here // :TODO: some terminals not supporting cterm style emulated baud rate end up displaying a broken ESC sequence or a single "r" here
this.client.term.rawWrite(ansi.setEmulatedBaudRate(this.menuConfig.config.baudRate)); this.client.term.rawWrite(
ansi.setEmulatedBaudRate(this.menuConfig.config.baudRate)
);
} }
if(this.cls) { if (this.cls) {
this.client.term.rawWrite(ansi.resetScreen()); this.client.term.rawWrite(ansi.resetScreen());
} }
@ -176,14 +199,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
displayQueuedInterruptions(cb) { displayQueuedInterruptions(cb) {
if(MenuModule.InterruptTypes.Never === this.interrupt) { if (MenuModule.InterruptTypes.Never === this.interrupt) {
return cb(null); return cb(null);
} }
let opts = { cls : true }; // clear screen for first message let opts = { cls: true }; // clear screen for first message
async.whilst( async.whilst(
(callback) => callback(null, this.client.interruptQueue.hasItems()), callback => callback(null, this.client.interruptQueue.hasItems()),
next => { next => {
this.client.interruptQueue.displayNext(opts, err => { this.client.interruptQueue.displayNext(opts, err => {
opts = {}; opts = {};
@ -197,7 +220,10 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
attemptInterruptNow(interruptItem, cb) { attemptInterruptNow(interruptItem, cb) {
if(this.realTimeInterrupt !== 'allowed' || MenuModule.InterruptTypes.Realtime !== this.interrupt) { if (
this.realTimeInterrupt !== 'allowed' ||
MenuModule.InterruptTypes.Realtime !== this.interrupt
) {
return cb(null, false); // don't eat up the item; queue for later return cb(null, false); // don't eat up the item; queue for later
} }
@ -212,15 +238,16 @@ exports.MenuModule = class MenuModule extends PluginModule {
}; };
this.client.interruptQueue.displayWithItem( this.client.interruptQueue.displayWithItem(
Object.assign({}, interruptItem, { cls : true }), Object.assign({}, interruptItem, { cls: true }),
err => { err => {
if(err) { if (err) {
return done(err, false); return done(err, false);
} }
this.reload(err => { this.reload(err => {
return done(err, err ? false : true); return done(err, err ? false : true);
}); });
}); }
);
} }
getSaveState() { getSaveState() {
@ -237,17 +264,17 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
nextMenu(cb) { nextMenu(cb) {
if(!this.haveNext()) { if (!this.haveNext()) {
return this.prevMenu(cb); // no next, go to prev return this.prevMenu(cb); // no next, go to prev
} }
this.displayQueuedInterruptions( () => { this.displayQueuedInterruptions(() => {
return this.client.menuStack.next(cb); return this.client.menuStack.next(cb);
}); });
} }
prevMenu(cb) { prevMenu(cb) {
this.displayQueuedInterruptions( () => { this.displayQueuedInterruptions(() => {
return this.client.menuStack.prev(cb); return this.client.menuStack.prev(cb);
}); });
} }
@ -258,8 +285,8 @@ exports.MenuModule = class MenuModule extends PluginModule {
gotoMenuOrPrev(name, options, cb) { gotoMenuOrPrev(name, options, cb) {
this.client.menuStack.goto(name, options, err => { this.client.menuStack.goto(name, options, err => {
if(!err) { if (!err) {
if(cb) { if (cb) {
return cb(null); return cb(null);
} }
} }
@ -269,7 +296,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
gotoMenuOrShowMessage(name, message, options, cb) { gotoMenuOrShowMessage(name, message, options, cb) {
if(!cb && _.isFunction(options)) { if (!cb && _.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
@ -277,18 +304,18 @@ exports.MenuModule = class MenuModule extends PluginModule {
options = options || { clearScreen: true }; options = options || { clearScreen: true };
this.gotoMenu(name, options, err => { this.gotoMenu(name, options, err => {
if(err) { if (err) {
if(options.clearScreen) { if (options.clearScreen) {
this.client.term.rawWrite(ansi.resetScreen()); this.client.term.rawWrite(ansi.resetScreen());
} }
this.client.term.write(`${message}\n`); this.client.term.write(`${message}\n`);
return this.pausePrompt( () => { return this.pausePrompt(() => {
return this.prevMenu(cb); return this.prevMenu(cb);
}); });
} }
if(cb) { if (cb) {
return cb(null); return cb(null);
} }
}); });
@ -301,33 +328,39 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
prevMenuOnTimeout(timeout, cb) { prevMenuOnTimeout(timeout, cb) {
setTimeout( () => { setTimeout(() => {
return this.prevMenu(cb); return this.prevMenu(cb);
}, timeout); }, timeout);
} }
addViewController(name, vc) { addViewController(name, vc) {
assert(!this.viewControllers[name], `ViewController by the name of "${name}" already exists!`); assert(
!this.viewControllers[name],
`ViewController by the name of "${name}" already exists!`
);
this.viewControllers[name] = vc; this.viewControllers[name] = vc;
return vc; return vc;
} }
removeViewController(name) { removeViewController(name) {
if(this.viewControllers[name]) { if (this.viewControllers[name]) {
this.viewControllers[name].detachClientEvents(); this.viewControllers[name].detachClientEvents();
delete this.viewControllers[name]; delete this.viewControllers[name];
} }
} }
detachViewControllers() { detachViewControllers() {
Object.keys(this.viewControllers).forEach( name => { Object.keys(this.viewControllers).forEach(name => {
this.viewControllers[name].detachClientEvents(); this.viewControllers[name].detachClientEvents();
}); });
} }
shouldPause() { shouldPause() {
return ('end' === this.menuConfig.config.pause || true === this.menuConfig.config.pause); return (
'end' === this.menuConfig.config.pause ||
true === this.menuConfig.config.pause
);
} }
hasNextTimeout() { hasNextTimeout() {
@ -335,13 +368,13 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
haveNext() { haveNext() {
return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next)); return _.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next);
} }
autoNextMenu(cb) { autoNextMenu(cb) {
const gotoNextMenu = () => { const gotoNextMenu = () => {
if(this.haveNext()) { if (this.haveNext()) {
this.displayQueuedInterruptions( () => { this.displayQueuedInterruptions(() => {
return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb); return menuUtil.handleNext(this.client, this.menuConfig.next, {}, cb);
}); });
} else { } else {
@ -349,9 +382,12 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
}; };
if(_.has(this.menuConfig, 'runtime.autoNext') && true === this.menuConfig.runtime.autoNext) { if (
if(this.hasNextTimeout()) { _.has(this.menuConfig, 'runtime.autoNext') &&
setTimeout( () => { true === this.menuConfig.runtime.autoNext
) {
if (this.hasNextTimeout()) {
setTimeout(() => {
return gotoNextMenu(); return gotoNextMenu();
}, this.menuConfig.config.nextTimeout); }, this.menuConfig.config.nextTimeout);
} else { } else {
@ -374,20 +410,23 @@ exports.MenuModule = class MenuModule extends PluginModule {
function addViewControllers(callback) { function addViewControllers(callback) {
_.forEach(mciData, (mciMap, name) => { _.forEach(mciData, (mciMap, name) => {
assert('menu' === name || 'prompt' === name); assert('menu' === name || 'prompt' === name);
self.addViewController(name, new ViewController( { client : self.client } ) ); self.addViewController(
name,
new ViewController({ client: self.client })
);
}); });
return callback(null); return callback(null);
}, },
function createMenu(callback) { function createMenu(callback) {
if(!self.viewControllers.menu) { if (!self.viewControllers.menu) {
return callback(null); return callback(null);
} }
const menuLoadOpts = { const menuLoadOpts = {
mciMap : mciData.menu, mciMap: mciData.menu,
callingMenu : self, callingMenu: self,
withoutForm : _.isObject(mciData.prompt), withoutForm: _.isObject(mciData.prompt),
}; };
self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => { self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, err => {
@ -395,19 +434,22 @@ exports.MenuModule = class MenuModule extends PluginModule {
}); });
}, },
function createPrompt(callback) { function createPrompt(callback) {
if(!self.viewControllers.prompt) { if (!self.viewControllers.prompt) {
return callback(null); return callback(null);
} }
const promptLoadOpts = { const promptLoadOpts = {
callingMenu : self, callingMenu: self,
mciMap : mciData.prompt, mciMap: mciData.prompt,
}; };
self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, err => { self.viewControllers.prompt.loadFromPromptConfig(
return callback(err); promptLoadOpts,
}); err => {
} return callback(err);
}
);
},
], ],
err => { err => {
return cb(err); return cb(err);
@ -416,28 +458,27 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
displayAsset(nameOrData, options, cb) { displayAsset(nameOrData, options, cb) {
if(_.isFunction(options)) { if (_.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
if(options.clearScreen) { if (options.clearScreen) {
this.client.term.rawWrite(ansi.resetScreen()); this.client.term.rawWrite(ansi.resetScreen());
} }
options = Object.assign( { client : this.client, font : this.menuConfig.config.font }, options ); options = Object.assign(
{ client: this.client, font: this.menuConfig.config.font },
options
);
if(Buffer.isBuffer(nameOrData)) { if (Buffer.isBuffer(nameOrData)) {
const data = iconvDecode(nameOrData, options.encoding || 'cp437'); const data = iconvDecode(nameOrData, options.encoding || 'cp437');
return theme.displayPreparedArt( return theme.displayPreparedArt(options, { data }, (err, artData) => {
options, if (cb) {
{ data }, return cb(err, artData);
(err, artData) => {
if(cb) {
return cb(err, artData);
}
} }
); });
} }
return theme.displayThemedAsset( return theme.displayThemedAsset(
@ -445,7 +486,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
this.client, this.client,
options, options,
(err, artData) => { (err, artData) => {
if(cb) { if (cb) {
return cb(err, artData); return cb(err, artData);
} }
} }
@ -454,18 +495,18 @@ exports.MenuModule = class MenuModule extends PluginModule {
prepViewController(name, formId, mciMap, cb) { prepViewController(name, formId, mciMap, cb) {
const needsCreated = _.isUndefined(this.viewControllers[name]); const needsCreated = _.isUndefined(this.viewControllers[name]);
if(needsCreated) { if (needsCreated) {
const vcOpts = { const vcOpts = {
client : this.client, client: this.client,
formId : formId, formId: formId,
}; };
const vc = this.addViewController(name, new ViewController(vcOpts)); const vc = this.addViewController(name, new ViewController(vcOpts));
const loadOpts = { const loadOpts = {
callingMenu : this, callingMenu: this,
mciMap : mciMap, mciMap: mciMap,
formId : formId, formId: formId,
}; };
return vc.loadFromMenuConfig(loadOpts, err => { return vc.loadFromMenuConfig(loadOpts, err => {
@ -479,21 +520,17 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
prepViewControllerWithArt(name, formId, options, cb) { prepViewControllerWithArt(name, formId, options, cb) {
this.displayAsset( this.displayAsset(this.menuConfig.config.art[name], options, (err, artData) => {
this.menuConfig.config.art[name], if (err) {
options, return cb(err);
(err, artData) => {
if(err) {
return cb(err);
}
return this.prepViewController(name, formId, artData.mciMap, cb);
} }
);
return this.prepViewController(name, formId, artData.mciMap, cb);
});
} }
optionalMoveToPosition(position) { optionalMoveToPosition(position) {
if(position) { if (position) {
position.x = position.row || position.x || 1; position.x = position.row || position.x || 1;
position.y = position.col || position.y || 1; position.y = position.col || position.y || 1;
@ -502,47 +539,53 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
pausePrompt(position, cb) { pausePrompt(position, cb) {
if(!cb && _.isFunction(position)) { if (!cb && _.isFunction(position)) {
cb = position; cb = position;
position = null; position = null;
} }
this.optionalMoveToPosition(position); this.optionalMoveToPosition(position);
return theme.displayThemedPause(this.client, {position}, cb); return theme.displayThemedPause(this.client, { position }, cb);
} }
promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) { promptForInput(
if(!cb && _.isFunction(options)) { { formName, formId, promptName, prevFormName, position } = {},
options,
cb
) {
if (!cb && _.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
options.viewController = this.addViewController( options.viewController = this.addViewController(
formName, formName,
new ViewController( { client : this.client, formId } ) new ViewController({ client: this.client, formId })
); );
options.trailingLF = _.get(options, 'trailingLF', false); options.trailingLF = _.get(options, 'trailingLF', false);
let prevVc; let prevVc;
if(prevFormName) { if (prevFormName) {
prevVc = this.viewControllers[prevFormName]; prevVc = this.viewControllers[prevFormName];
if(prevVc) { if (prevVc) {
prevVc.setFocus(false); prevVc.setFocus(false);
} }
} }
//let artHeight; //let artHeight;
options.submitNotify = () => { options.submitNotify = () => {
if(prevVc) { if (prevVc) {
prevVc.setFocus(true); prevVc.setFocus(true);
} }
this.removeViewController(formName); this.removeViewController(formName);
if(options.clearAtSubmit) { if (options.clearAtSubmit) {
this.optionalMoveToPosition(position); this.optionalMoveToPosition(position);
if(options.clearWidth) { if (options.clearWidth) {
this.client.term.rawWrite(`${ansi.reset()}${' '.repeat(options.clearWidth)}`); this.client.term.rawWrite(
`${ansi.reset()}${' '.repeat(options.clearWidth)}`
);
} else { } else {
// :TODO: handle multi-rows via artHeight // :TODO: handle multi-rows via artHeight
this.client.term.rawWrite(ansi.eraseLine()); this.client.term.rawWrite(ansi.eraseLine());
@ -565,11 +608,11 @@ exports.MenuModule = class MenuModule extends PluginModule {
setViewText(formName, mciId, text, appendMultiLine) { setViewText(formName, mciId, text, appendMultiLine) {
const view = this.getView(formName, mciId); const view = this.getView(formName, mciId);
if(!view) { if (!view) {
return; return;
} }
if(appendMultiLine && (view instanceof MultiLineEditTextView)) { if (appendMultiLine && view instanceof MultiLineEditTextView) {
view.addText(text); view.addText(text);
} else { } else {
view.setText(text); view.setText(text);
@ -586,17 +629,26 @@ exports.MenuModule = class MenuModule extends PluginModule {
let textView; let textView;
let customMciId = startId; let customMciId = startId;
const config = this.menuConfig.config; const config = this.menuConfig.config;
const endId = options.endId || 99; // we'll fail to get a view before 99 const endId = options.endId || 99; // we'll fail to get a view before 99
while(customMciId <= endId && (textView = this.viewControllers[formName].getView(customMciId)) ) { while (
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" customMciId <= endId &&
const format = config[key]; (textView = this.viewControllers[formName].getView(customMciId))
) {
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
const format = config[key];
if(format && (!options.filter || options.filter.find(f => format.indexOf(f) > - 1))) { if (
format &&
(!options.filter || options.filter.find(f => format.indexOf(f) > -1))
) {
const text = stringFormat(format, fmtObj); const text = stringFormat(format, fmtObj);
if(options.appendMultiLine && (textView instanceof MultiLineEditTextView)) { if (
options.appendMultiLine &&
textView instanceof MultiLineEditTextView
) {
textView.addText(text); textView.addText(text);
} else { } else {
textView.setText(text); textView.setText(text);
@ -608,10 +660,10 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
refreshPredefinedMciViewsByCode(formName, mciCodes) { refreshPredefinedMciViewsByCode(formName, mciCodes) {
const form = _.get(this, [ 'viewControllers', formName] ); const form = _.get(this, ['viewControllers', formName]);
if(form) { if (form) {
form.getViewsByMciCode(mciCodes).forEach(v => { form.getViewsByMciCode(mciCodes).forEach(v => {
if(!v.setText) { if (!v.setText) {
return; return;
} }
@ -621,15 +673,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
} }
validateMCIByViewIds(formName, viewIds, cb) { validateMCIByViewIds(formName, viewIds, cb) {
if(!Array.isArray(viewIds)) { if (!Array.isArray(viewIds)) {
viewIds = [ viewIds ]; viewIds = [viewIds];
} }
const form = _.get(this, [ 'viewControllers', formName ] ); const form = _.get(this, ['viewControllers', formName]);
if(!form) { if (!form) {
return cb(Errors.DoesNotExist(`Form does not exist: ${formName}`)); return cb(Errors.DoesNotExist(`Form does not exist: ${formName}`));
} }
for(let i = 0; i < viewIds.length; ++i) { for (let i = 0; i < viewIds.length; ++i) {
if(!form.hasView(viewIds[i])) { if (!form.hasView(viewIds[i])) {
return cb(Errors.MissingMci(`Missing MCI ${viewIds[i]}`)); return cb(Errors.MissingMci(`Missing MCI ${viewIds[i]}`));
} }
} }
@ -641,7 +693,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
// fields is expected to be { key : type || validator(key, config) } // fields is expected to be { key : type || validator(key, config) }
// where |type| is 'string', 'array', object', 'number' // where |type| is 'string', 'array', object', 'number'
// //
if(!_.isObject(fields)) { if (!_.isObject(fields)) {
return cb(Errors.Invalid('Invalid validator!')); return cb(Errors.Invalid('Invalid validator!'));
} }
@ -649,10 +701,10 @@ exports.MenuModule = class MenuModule extends PluginModule {
let firstBadKey; let firstBadKey;
let badReason; let badReason;
const good = _.every(fields, (type, key) => { const good = _.every(fields, (type, key) => {
if(_.isFunction(type)) { if (_.isFunction(type)) {
if(!type(key, config)) { if (!type(key, config)) {
firstBadKey = key; firstBadKey = key;
badReason = 'Validate failure'; badReason = 'Validate failure';
return false; return false;
} }
return true; return true;
@ -660,30 +712,44 @@ exports.MenuModule = class MenuModule extends PluginModule {
const c = config[key]; const c = config[key];
let typeOk; let typeOk;
if(_.isUndefined(c)) { if (_.isUndefined(c)) {
typeOk = false; typeOk = false;
badReason = `Missing "${key}", expected ${type}`; badReason = `Missing "${key}", expected ${type}`;
} else { } else {
switch(type) { switch (type) {
case 'string' : typeOk = _.isString(c); break; case 'string':
case 'object' : typeOk = _.isObject(c); break; typeOk = _.isString(c);
case 'array' : typeOk = Array.isArray(c); break; break;
case 'number' : typeOk = !isNaN(parseInt(c)); break; case 'object':
default : typeOk = _.isObject(c);
break;
case 'array':
typeOk = Array.isArray(c);
break;
case 'number':
typeOk = !isNaN(parseInt(c));
break;
default:
typeOk = false; typeOk = false;
badReason = `Don't know how to validate ${type}`; badReason = `Don't know how to validate ${type}`;
break; break;
} }
} }
if(!typeOk) { if (!typeOk) {
firstBadKey = key; firstBadKey = key;
if(!badReason) { if (!badReason) {
badReason = `Expected ${type}`; badReason = `Expected ${type}`;
} }
} }
return typeOk; return typeOk;
}); });
return cb(good ? null : Errors.Invalid(`Invalid or missing config option "${firstBadKey}" (${badReason})`)); return cb(
good
? null
: Errors.Invalid(
`Invalid or missing config option "${firstBadKey}" (${badReason})`
)
);
} }
}; };

View File

@ -2,25 +2,20 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const loadMenu = require('./menu_util.js').loadMenu; const loadMenu = require('./menu_util.js').loadMenu;
const { const { Errors, ErrorReasons } = require('./enig_error.js');
Errors, const { getResolvedSpec } = require('./menu_util.js');
ErrorReasons
} = require('./enig_error.js');
const {
getResolvedSpec
} = require('./menu_util.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
// :TODO: Stack is backwards.... top should be most recent! :) // :TODO: Stack is backwards.... top should be most recent! :)
module.exports = class MenuStack { module.exports = class MenuStack {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.stack = []; this.stack = [];
} }
push(moduleInfo) { push(moduleInfo) {
@ -32,13 +27,13 @@ module.exports = class MenuStack {
} }
peekPrev() { peekPrev() {
if(this.stackSize > 1) { if (this.stackSize > 1) {
return this.stack[this.stack.length - 2]; return this.stack[this.stack.length - 2];
} }
} }
top() { top() {
if(this.stackSize > 0) { if (this.stackSize > 0) {
return this.stack[this.stack.length - 1]; return this.stack[this.stack.length - 1];
} }
} }
@ -55,47 +50,61 @@ module.exports = class MenuStack {
next(cb) { next(cb) {
const currentModuleInfo = this.top(); const currentModuleInfo = this.top();
const menuConfig = currentModuleInfo.instance.menuConfig; const menuConfig = currentModuleInfo.instance.menuConfig;
const nextMenu = getResolvedSpec(this.client, menuConfig.next, 'next'); const nextMenu = getResolvedSpec(this.client, menuConfig.next, 'next');
if(!nextMenu) { if (!nextMenu) {
return cb(Array.isArray(menuConfig.next) ? return cb(
Errors.MenuStack('No matching condition for "next"', ErrorReasons.NoConditionMatch) : Array.isArray(menuConfig.next)
Errors.MenuStack('Invalid or missing "next" member in menu config', ErrorReasons.InvalidNextMenu) ? Errors.MenuStack(
'No matching condition for "next"',
ErrorReasons.NoConditionMatch
)
: Errors.MenuStack(
'Invalid or missing "next" member in menu config',
ErrorReasons.InvalidNextMenu
)
); );
} }
if(nextMenu === currentModuleInfo.name) { if (nextMenu === currentModuleInfo.name) {
return cb(Errors.MenuStack('Menu config "next" specifies current menu', ErrorReasons.AlreadyThere)); return cb(
Errors.MenuStack(
'Menu config "next" specifies current menu',
ErrorReasons.AlreadyThere
)
);
} }
this.goto(nextMenu, { }, cb); this.goto(nextMenu, {}, cb);
} }
prev(cb) { prev(cb) {
const menuResult = this.top().instance.getMenuResult(); const menuResult = this.top().instance.getMenuResult();
// :TODO: leave() should really take a cb... // :TODO: leave() should really take a cb...
this.pop().instance.leave(); // leave & remove current this.pop().instance.leave(); // leave & remove current
const previousModuleInfo = this.pop(); // get previous const previousModuleInfo = this.pop(); // get previous
if(previousModuleInfo) { if (previousModuleInfo) {
const opts = { const opts = {
extraArgs : previousModuleInfo.extraArgs, extraArgs: previousModuleInfo.extraArgs,
savedState : previousModuleInfo.savedState, savedState: previousModuleInfo.savedState,
lastMenuResult : menuResult, lastMenuResult: menuResult,
}; };
return this.goto(previousModuleInfo.name, opts, cb); return this.goto(previousModuleInfo.name, opts, cb);
} }
return cb(Errors.MenuStack('No previous menu available', ErrorReasons.NoPreviousMenu)); return cb(
Errors.MenuStack('No previous menu available', ErrorReasons.NoPreviousMenu)
);
} }
goto(name, options, cb) { goto(name, options, cb) {
const currentModuleInfo = this.top(); const currentModuleInfo = this.top();
if(!cb && _.isFunction(options)) { if (!cb && _.isFunction(options)) {
cb = options; cb = options;
options = {}; options = {};
} }
@ -103,19 +112,24 @@ module.exports = class MenuStack {
options = options || {}; options = options || {};
const self = this; const self = this;
if(currentModuleInfo && name === currentModuleInfo.name) { if (currentModuleInfo && name === currentModuleInfo.name) {
if(cb) { if (cb) {
cb(Errors.MenuStack('Already at supplied menu', ErrorReasons.AlreadyThere)); cb(
Errors.MenuStack(
'Already at supplied menu',
ErrorReasons.AlreadyThere
)
);
} }
return; return;
} }
const loadOpts = { const loadOpts = {
name : name, name: name,
client : self.client, client: self.client,
}; };
if(currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) { if (currentModuleInfo && currentModuleInfo.menuFlags.includes('forwardArgs')) {
loadOpts.extraArgs = currentModuleInfo.extraArgs; loadOpts.extraArgs = currentModuleInfo.extraArgs;
} else { } else {
loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value'); loadOpts.extraArgs = options.extraArgs || _.get(options, 'formData.value');
@ -123,15 +137,15 @@ module.exports = class MenuStack {
loadOpts.lastMenuResult = options.lastMenuResult; loadOpts.lastMenuResult = options.lastMenuResult;
loadMenu(loadOpts, (err, modInst) => { loadMenu(loadOpts, (err, modInst) => {
if(err) { if (err) {
// :TODO: probably should just require a cb... // :TODO: probably should just require a cb...
const errCb = cb || self.client.defaultHandlerMissingMod(); const errCb = cb || self.client.defaultHandlerMissingMod();
errCb(err); errCb(err);
} else { } else {
self.client.log.debug( { menuName : name }, 'Goto menu module'); self.client.log.debug({ menuName: name }, 'Goto menu module');
if(!this.client.acs.hasMenuModuleAccess(modInst)) { if (!this.client.acs.hasMenuModuleAccess(modInst)) {
if(cb) { if (cb) {
return cb(Errors.AccessDenied('No access to this menu')); return cb(Errors.AccessDenied('No access to this menu'));
} }
return; return;
@ -141,12 +155,15 @@ module.exports = class MenuStack {
// Handle deprecated 'options' block by merging to config and warning user. // Handle deprecated 'options' block by merging to config and warning user.
// :TODO: Remove in 0.0.10+ // :TODO: Remove in 0.0.10+
// //
if(modInst.menuConfig.options) { if (modInst.menuConfig.options) {
self.client.log.warn( self.client.log.warn(
{ options : modInst.menuConfig.options }, { options: modInst.menuConfig.options },
'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions' 'Use of "options" is deprecated. Move relevant members to "config" block! Support will be fully removed in future versions'
); );
Object.assign(modInst.menuConfig.config || {}, modInst.menuConfig.options); Object.assign(
modInst.menuConfig.config || {},
modInst.menuConfig.options
);
delete modInst.menuConfig.options; delete modInst.menuConfig.options;
} }
@ -155,57 +172,63 @@ module.exports = class MenuStack {
// anything supplied in code. // anything supplied in code.
// //
let menuFlags; let menuFlags;
if(0 === modInst.menuConfig.config.menuFlags.length) { if (0 === modInst.menuConfig.config.menuFlags.length) {
menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : []; menuFlags = Array.isArray(options.menuFlags) ? options.menuFlags : [];
} else { } else {
menuFlags = modInst.menuConfig.config.menuFlags; menuFlags = modInst.menuConfig.config.menuFlags;
// in code we can ask to merge in // in code we can ask to merge in
if(Array.isArray(options.menuFlags) && options.menuFlags.includes('mergeFlags')) { if (
Array.isArray(options.menuFlags) &&
options.menuFlags.includes('mergeFlags')
) {
menuFlags = _.uniq(menuFlags.concat(options.menuFlags)); menuFlags = _.uniq(menuFlags.concat(options.menuFlags));
} }
} }
if(currentModuleInfo) { if (currentModuleInfo) {
// save stack state // save stack state
currentModuleInfo.savedState = currentModuleInfo.instance.getSaveState(); currentModuleInfo.savedState =
currentModuleInfo.instance.getSaveState();
currentModuleInfo.instance.leave(); currentModuleInfo.instance.leave();
if(currentModuleInfo.menuFlags.includes('noHistory')) { if (currentModuleInfo.menuFlags.includes('noHistory')) {
this.pop(); this.pop();
} }
if(menuFlags.includes('popParent')) { if (menuFlags.includes('popParent')) {
this.pop().instance.leave(); // leave & remove current this.pop().instance.leave(); // leave & remove current
} }
} }
self.push({ self.push({
name : name, name: name,
instance : modInst, instance: modInst,
extraArgs : loadOpts.extraArgs, extraArgs: loadOpts.extraArgs,
menuFlags : menuFlags, menuFlags: menuFlags,
}); });
// restore previous state if requested // restore previous state if requested
if(options.savedState) { if (options.savedState) {
modInst.restoreSavedState(options.savedState); modInst.restoreSavedState(options.savedState);
} }
const stackEntries = self.stack.map(stackEntry => { const stackEntries = self.stack.map(stackEntry => {
let name = stackEntry.name; let name = stackEntry.name;
if(stackEntry.instance.menuConfig.config.menuFlags.length > 0) { if (stackEntry.instance.menuConfig.config.menuFlags.length > 0) {
name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(', ')})`; name += ` (${stackEntry.instance.menuConfig.config.menuFlags.join(
', '
)})`;
} }
return name; return name;
}); });
self.client.log.trace( { stack : stackEntries }, 'Updated menu stack' ); self.client.log.trace({ stack: stackEntries }, 'Updated menu stack');
modInst.enter(); modInst.enter();
if(cb) { if (cb) {
cb(null); cb(null);
} }
} }

View File

@ -2,29 +2,29 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const moduleUtil = require('./module_util.js'); const moduleUtil = require('./module_util.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const asset = require('./asset.js'); const asset = require('./asset.js');
const { MCIViewFactory } = require('./mci_view_factory.js'); const { MCIViewFactory } = require('./mci_view_factory.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
// deps // deps
const paths = require('path'); const paths = require('path');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.loadMenu = loadMenu; exports.loadMenu = loadMenu;
exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap; exports.getFormConfigByIDAndMap = getFormConfigByIDAndMap;
exports.handleAction = handleAction; exports.handleAction = handleAction;
exports.getResolvedSpec = getResolvedSpec; exports.getResolvedSpec = getResolvedSpec;
exports.handleNext = handleNext; exports.handleNext = handleNext;
function getMenuConfig(client, name, cb) { function getMenuConfig(client, name, cb) {
async.waterfall( async.waterfall(
[ [
function locateMenuConfig(callback) { function locateMenuConfig(callback) {
const menuConfig = _.get(client.currentTheme, [ 'menus', name ]); const menuConfig = _.get(client.currentTheme, ['menus', name]);
if (menuConfig) { if (menuConfig) {
return callback(null, menuConfig); return callback(null, menuConfig);
} }
@ -32,15 +32,18 @@ function getMenuConfig(client, name, cb) {
return callback(Errors.DoesNotExist(`No menu entry for "${name}"`)); return callback(Errors.DoesNotExist(`No menu entry for "${name}"`));
}, },
function locatePromptConfig(menuConfig, callback) { function locatePromptConfig(menuConfig, callback) {
if(_.isString(menuConfig.prompt)) { if (_.isString(menuConfig.prompt)) {
if(_.has(client.currentTheme, [ 'prompts', menuConfig.prompt ])) { if (_.has(client.currentTheme, ['prompts', menuConfig.prompt])) {
menuConfig.promptConfig = client.currentTheme.prompts[menuConfig.prompt]; menuConfig.promptConfig =
client.currentTheme.prompts[menuConfig.prompt];
return callback(null, menuConfig); return callback(null, menuConfig);
} }
return callback(Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`)); return callback(
Errors.DoesNotExist(`No prompt entry for "${menuConfig.prompt}"`)
);
} }
return callback(null, menuConfig); return callback(null, menuConfig);
} },
], ],
(err, menuConfig) => { (err, menuConfig) => {
return cb(err, menuConfig); return cb(err, menuConfig);
@ -50,7 +53,7 @@ function getMenuConfig(client, name, cb) {
// :TODO: name/client should not be part of options - they are required always // :TODO: name/client should not be part of options - they are required always
function loadMenu(options, cb) { function loadMenu(options, cb) {
if(!_.isString(options.name) || !_.isObject(options.client)) { if (!_.isString(options.name) || !_.isObject(options.client)) {
return cb(Errors.MissingParam('Missing required options')); return cb(Errors.MissingParam('Missing required options'));
} }
@ -62,27 +65,30 @@ function loadMenu(options, cb) {
}); });
}, },
function loadMenuModule(menuConfig, callback) { function loadMenuModule(menuConfig, callback) {
menuConfig.config = menuConfig.config || {}; menuConfig.config = menuConfig.config || {};
menuConfig.config.menuFlags = menuConfig.config.menuFlags || []; menuConfig.config.menuFlags = menuConfig.config.menuFlags || [];
if(!Array.isArray(menuConfig.config.menuFlags)) { if (!Array.isArray(menuConfig.config.menuFlags)) {
menuConfig.config.menuFlags = [ menuConfig.config.menuFlags ]; menuConfig.config.menuFlags = [menuConfig.config.menuFlags];
} }
const modAsset = asset.getModuleAsset(menuConfig.module); const modAsset = asset.getModuleAsset(menuConfig.module);
const modSupplied = null !== modAsset; const modSupplied = null !== modAsset;
const modLoadOpts = { const modLoadOpts = {
name : modSupplied ? modAsset.asset : 'standard_menu', name: modSupplied ? modAsset.asset : 'standard_menu',
path : (!modSupplied || 'systemModule' === modAsset.type) ? __dirname : Config().paths.mods, path:
category : (!modSupplied || 'systemModule' === modAsset.type) ? null : 'mods', !modSupplied || 'systemModule' === modAsset.type
? __dirname
: Config().paths.mods,
category:
!modSupplied || 'systemModule' === modAsset.type ? null : 'mods',
}; };
moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => { moduleUtil.loadModuleEx(modLoadOpts, (err, mod) => {
const modData = { const modData = {
name : modLoadOpts.name, name: modLoadOpts.name,
config : menuConfig, config: menuConfig,
mod : mod, mod: mod,
}; };
return callback(err, modData); return callback(err, modData);
@ -90,24 +96,30 @@ function loadMenu(options, cb) {
}, },
function createModuleInstance(modData, callback) { function createModuleInstance(modData, callback) {
Log.trace( Log.trace(
{ moduleName : modData.name, extraArgs : options.extraArgs, config : modData.config, info : modData.mod.modInfo }, {
'Creating menu module instance'); moduleName: modData.name,
extraArgs: options.extraArgs,
config: modData.config,
info: modData.mod.modInfo,
},
'Creating menu module instance'
);
let moduleInstance; let moduleInstance;
try { try {
moduleInstance = new modData.mod.getModule({ moduleInstance = new modData.mod.getModule({
menuName : options.name, menuName: options.name,
menuConfig : modData.config, menuConfig: modData.config,
extraArgs : options.extraArgs, extraArgs: options.extraArgs,
client : options.client, client: options.client,
lastMenuResult : options.lastMenuResult, lastMenuResult: options.lastMenuResult,
}); });
} catch(e) { } catch (e) {
return callback(e); return callback(e);
} }
return callback(null, moduleInstance); return callback(null, moduleInstance);
} },
], ],
(err, modInst) => { (err, modInst) => {
return cb(err, modInst); return cb(err, modInst);
@ -116,82 +128,99 @@ function loadMenu(options, cb) {
} }
function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) { function getFormConfigByIDAndMap(menuConfig, formId, mciMap, cb) {
if(!_.isObject(menuConfig.form)) { if (!_.isObject(menuConfig.form)) {
return cb(Errors.MissingParam('Invalid or missing "form" member for menu')); return cb(Errors.MissingParam('Invalid or missing "form" member for menu'));
} }
if(!_.isObject(menuConfig.form[formId])) { if (!_.isObject(menuConfig.form[formId])) {
return cb(Errors.DoesNotExist(`No form found for formId ${formId}`)); return cb(Errors.DoesNotExist(`No form found for formId ${formId}`));
} }
const formForId = menuConfig.form[formId]; const formForId = menuConfig.form[formId];
const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), (mci) => { const mciReqKey = _.filter(_.map(_.sortBy(mciMap, 'code'), 'code'), mci => {
return MCIViewFactory.UserViewCodes.indexOf(mci) > -1; return MCIViewFactory.UserViewCodes.indexOf(mci) > -1;
}).join(''); }).join('');
Log.trace( { mciKey : mciReqKey }, 'Looking for MCI configuration key'); Log.trace({ mciKey: mciReqKey }, 'Looking for MCI configuration key');
// //
// Exact, explicit match? // Exact, explicit match?
// //
if(_.isObject(formForId[mciReqKey])) { if (_.isObject(formForId[mciReqKey])) {
Log.trace( { mciKey : mciReqKey }, 'Using exact configuration key match'); Log.trace({ mciKey: mciReqKey }, 'Using exact configuration key match');
return cb(null, formForId[mciReqKey]); return cb(null, formForId[mciReqKey]);
} }
// //
// Generic match // Generic match
// //
if(_.has(formForId, 'mci') || _.has(formForId, 'submit')) { if (_.has(formForId, 'mci') || _.has(formForId, 'submit')) {
Log.trace('Using generic configuration'); Log.trace('Using generic configuration');
return cb(null, formForId); return cb(null, formForId);
} }
return cb(Errors.DoesNotExist(`No matching form configuration found for key "${mciReqKey}"`)); return cb(
Errors.DoesNotExist(`No matching form configuration found for key "${mciReqKey}"`)
);
} }
// :TODO: Most of this should be moved elsewhere .... DRY... // :TODO: Most of this should be moved elsewhere .... DRY...
function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) { function callModuleMenuMethod(client, asset, path, formData, extraArgs, cb) {
if('' === paths.extname(path)) { if ('' === paths.extname(path)) {
path += '.js'; path += '.js';
} }
try { try {
client.log.trace( client.log.trace(
{ path : path, methodName : asset.asset, formData : formData, extraArgs : extraArgs }, {
'Calling menu method'); path: path,
methodName: asset.asset,
formData: formData,
extraArgs: extraArgs,
},
'Calling menu method'
);
const methodMod = require(path); const methodMod = require(path);
return methodMod[asset.asset](client.currentMenuModule, formData || { }, extraArgs, cb); return methodMod[asset.asset](
} catch(e) { client.currentMenuModule,
client.log.error( { error : e.toString(), methodName : asset.asset }, 'Failed to execute asset method'); formData || {},
extraArgs,
cb
);
} catch (e) {
client.log.error(
{ error: e.toString(), methodName: asset.asset },
'Failed to execute asset method'
);
return cb(e); return cb(e);
} }
} }
function handleAction(client, formData, conf, cb) { function handleAction(client, formData, conf, cb) {
if(!_.isObject(conf)) { if (!_.isObject(conf)) {
return cb(Errors.MissingParam('Missing config')); return cb(Errors.MissingParam('Missing config'));
} }
const action = getResolvedSpec(client, conf.action, 'action'); // random/conditionals/etc. const action = getResolvedSpec(client, conf.action, 'action'); // random/conditionals/etc.
const actionAsset = asset.parseAsset(action); const actionAsset = asset.parseAsset(action);
if(!_.isObject(actionAsset)) { if (!_.isObject(actionAsset)) {
return cb(Errors.Invalid('Unable to parse "conf.action"')); return cb(Errors.Invalid('Unable to parse "conf.action"'));
} }
switch(actionAsset.type) { switch (actionAsset.type) {
case 'method' : case 'method':
case 'systemMethod' : case 'systemMethod':
if(_.isString(actionAsset.location)) { if (_.isString(actionAsset.location)) {
return callModuleMenuMethod( return callModuleMenuMethod(
client, client,
actionAsset, actionAsset,
paths.join(Config().paths.mods, actionAsset.location), paths.join(Config().paths.mods, actionAsset.location),
formData, formData,
conf.extraArgs, conf.extraArgs,
cb); cb
} else if('systemMethod' === actionAsset.type) { );
} else if ('systemMethod' === actionAsset.type) {
// :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. () // :TODO: Need to pass optional args here -- conf.extraArgs and args between e.g. ()
// :TODO: Probably better as system_method.js // :TODO: Probably better as system_method.js
return callModuleMenuMethod( return callModuleMenuMethod(
@ -200,21 +229,30 @@ function handleAction(client, formData, conf, cb) {
paths.join(__dirname, 'system_menu_method.js'), paths.join(__dirname, 'system_menu_method.js'),
formData, formData,
conf.extraArgs, conf.extraArgs,
cb); cb
);
} else { } else {
// local to current module // local to current module
const currentModule = client.currentMenuModule; const currentModule = client.currentMenuModule;
if(_.isFunction(currentModule.menuMethods[actionAsset.asset])) { if (_.isFunction(currentModule.menuMethods[actionAsset.asset])) {
return currentModule.menuMethods[actionAsset.asset](formData, conf.extraArgs, cb); return currentModule.menuMethods[actionAsset.asset](
formData,
conf.extraArgs,
cb
);
} }
const err = Errors.DoesNotExist('Method does not exist'); const err = Errors.DoesNotExist('Method does not exist');
client.log.warn( { method : actionAsset.asset }, err.message); client.log.warn({ method: actionAsset.asset }, err.message);
return cb(err); return cb(err);
} }
case 'menu' : case 'menu':
return client.currentMenuModule.gotoMenu(actionAsset.asset, { formData : formData, extraArgs : conf.extraArgs }, cb ); return client.currentMenuModule.gotoMenu(
actionAsset.asset,
{ formData: formData, extraArgs: conf.extraArgs },
cb
);
} }
} }
@ -237,15 +275,15 @@ function getResolvedSpec(client, spec, memberName) {
// (3) Simple array of strings. A random selection will be made: // (3) Simple array of strings. A random selection will be made:
// next: [ "foo", "baz", "fizzbang" ] // next: [ "foo", "baz", "fizzbang" ]
// //
if(!Array.isArray(spec)) { if (!Array.isArray(spec)) {
return spec; // (1) simple string, as-is return spec; // (1) simple string, as-is
} }
if(_.isObject(spec[0])) { if (_.isObject(spec[0])) {
return client.acs.getConditionalValue(spec, memberName); // (2) ACS conditionals return client.acs.getConditionalValue(spec, memberName); // (2) ACS conditionals
} }
return spec[Math.floor(Math.random() * spec.length)]; // (3) random return spec[Math.floor(Math.random() * spec.length)]; // (3) random
} }
function handleNext(client, nextSpec, conf, cb) { function handleNext(client, nextSpec, conf, cb) {
@ -257,32 +295,54 @@ function handleNext(client, nextSpec, conf, cb) {
const extraArgs = conf.extraArgs || {}; const extraArgs = conf.extraArgs || {};
// :TODO: DRY this with handleAction() // :TODO: DRY this with handleAction()
switch(nextAsset.type) { switch (nextAsset.type) {
case 'method' : case 'method':
case 'systemMethod' : case 'systemMethod':
if(_.isString(nextAsset.location)) { if (_.isString(nextAsset.location)) {
return callModuleMenuMethod(client, nextAsset, paths.join(Config().paths.mods, nextAsset.location), {}, extraArgs, cb); return callModuleMenuMethod(
} else if('systemMethod' === nextAsset.type) { client,
nextAsset,
paths.join(Config().paths.mods, nextAsset.location),
{},
extraArgs,
cb
);
} else if ('systemMethod' === nextAsset.type) {
// :TODO: see other notes about system_menu_method.js here // :TODO: see other notes about system_menu_method.js here
return callModuleMenuMethod(client, nextAsset, paths.join(__dirname, 'system_menu_method.js'), {}, extraArgs, cb); return callModuleMenuMethod(
client,
nextAsset,
paths.join(__dirname, 'system_menu_method.js'),
{},
extraArgs,
cb
);
} else { } else {
// local to current module // local to current module
const currentModule = client.currentMenuModule; const currentModule = client.currentMenuModule;
if(_.isFunction(currentModule.menuMethods[nextAsset.asset])) { if (_.isFunction(currentModule.menuMethods[nextAsset.asset])) {
const formData = {}; // we don't have any const formData = {}; // we don't have any
return currentModule.menuMethods[nextAsset.asset]( formData, extraArgs, cb ); return currentModule.menuMethods[nextAsset.asset](
formData,
extraArgs,
cb
);
} }
const err = Errors.DoesNotExist('Method does not exist'); const err = Errors.DoesNotExist('Method does not exist');
client.log.warn( { method : nextAsset.asset }, err.message); client.log.warn({ method: nextAsset.asset }, err.message);
return cb(err); return cb(err);
} }
case 'menu' : case 'menu':
return client.currentMenuModule.gotoMenu(nextAsset.asset, { extraArgs : extraArgs }, cb ); return client.currentMenuModule.gotoMenu(
nextAsset.asset,
{ extraArgs: extraArgs },
cb
);
} }
const err = Errors.Invalid('Invalid asset type for "next"'); const err = Errors.Invalid('Invalid asset type for "next"');
client.log.error( { nextSpec : nextSpec }, err.message); client.log.error({ nextSpec: nextSpec }, err.message);
return cb(err); return cb(err);
} }

View File

@ -2,16 +2,16 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const View = require('./view.js').View; const View = require('./view.js').View;
const miscUtil = require('./misc_util.js'); const miscUtil = require('./misc_util.js');
const pipeToAnsi = require('./color_codes.js').pipeToAnsi; const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
// deps // deps
const util = require('util'); const util = require('util');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
exports.MenuView = MenuView; exports.MenuView = MenuView;
function MenuView(options) { function MenuView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
@ -23,7 +23,7 @@ function MenuView(options) {
const self = this; const self = this;
if(options.items) { if (options.items) {
this.setItems(options.items); this.setItems(options.items);
} else { } else {
this.items = []; this.items = [];
@ -31,54 +31,61 @@ function MenuView(options) {
this.renderCache = {}; this.renderCache = {};
this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(
options.caseInsensitiveHotKeys,
true
);
this.setHotKeys(options.hotKeys); this.setHotKeys(options.hotKeys);
this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = options.focusedItemIndex || 0;
this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; this.focusedItemIndex =
this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing)
? options.itemHorizSpacing
: 0;
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
this.focusPrefix = options.focusPrefix || ''; this.focusPrefix = options.focusPrefix || '';
this.focusSuffix = options.focusSuffix || ''; this.focusSuffix = options.focusSuffix || '';
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
this.hasFocusItems = function() { this.hasFocusItems = function () {
return !_.isUndefined(self.focusItems); return !_.isUndefined(self.focusItems);
}; };
this.getHotKeyItemIndex = function(ch) { this.getHotKeyItemIndex = function (ch) {
if(ch && self.hotKeys) { if (ch && self.hotKeys) {
const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; const keyIndex =
if(_.isNumber(keyIndex)) { self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
if (_.isNumber(keyIndex)) {
return keyIndex; return keyIndex;
} }
} }
return -1; return -1;
}; };
this.emitIndexUpdate = function() { this.emitIndexUpdate = function () {
self.emit('index update', self.focusedItemIndex); self.emit('index update', self.focusedItemIndex);
}; };
} }
util.inherits(MenuView, View); util.inherits(MenuView, View);
MenuView.prototype.setTextOverflow = function(overflow) { MenuView.prototype.setTextOverflow = function (overflow) {
this.textOverflow = overflow; this.textOverflow = overflow;
this.invalidateRenderCache(); this.invalidateRenderCache();
} };
MenuView.prototype.hasTextOverflow = function() { MenuView.prototype.hasTextOverflow = function () {
return this.textOverflow != undefined; return this.textOverflow != undefined;
} };
MenuView.prototype.setItems = function(items) { MenuView.prototype.setItems = function (items) {
if(Array.isArray(items)) { if (Array.isArray(items)) {
this.sorted = false; this.sorted = false;
this.renderCache = {}; this.renderCache = {};
@ -97,7 +104,7 @@ MenuView.prototype.setItems = function(items) {
let stringItem; let stringItem;
this.items = items.map(item => { this.items = items.map(item => {
stringItem = _.isString(item); stringItem = _.isString(item);
if(stringItem) { if (stringItem) {
text = item; text = item;
} else { } else {
text = item.text || ''; text = item.text || '';
@ -105,10 +112,10 @@ MenuView.prototype.setItems = function(items) {
} }
text = this.disablePipe ? text : pipeToAnsi(text, this.client); text = this.disablePipe ? text : pipeToAnsi(text, this.client);
return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
}); });
if(this.complexItems) { if (this.complexItems) {
this.itemFormat = this.itemFormat || '{text}'; this.itemFormat = this.itemFormat || '{text}';
} }
@ -116,58 +123,58 @@ MenuView.prototype.setItems = function(items) {
} }
}; };
MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { MenuView.prototype.getRenderCacheItem = function (index, focusItem = false) {
const item = this.renderCache[index]; const item = this.renderCache[index];
return item && item[focusItem ? 'focus' : 'standard']; return item && item[focusItem ? 'focus' : 'standard'];
}; };
MenuView.prototype.removeRenderCacheItem = function(index) { MenuView.prototype.removeRenderCacheItem = function (index) {
delete this.renderCache[index]; delete this.renderCache[index];
}; };
MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { MenuView.prototype.setRenderCacheItem = function (index, rendered, focusItem = false) {
this.renderCache[index] = this.renderCache[index] || {}; this.renderCache[index] = this.renderCache[index] || {};
this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered;
}; };
MenuView.prototype.invalidateRenderCache = function() { MenuView.prototype.invalidateRenderCache = function () {
this.renderCache = {}; this.renderCache = {};
}; };
MenuView.prototype.setSort = function(sort) { MenuView.prototype.setSort = function (sort) {
if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) {
return; return;
} }
const key = true === sort ? 'text' : sort; const key = true === sort ? 'text' : sort;
if('text' !== sort && !this.complexItems) { if ('text' !== sort && !this.complexItems) {
return; // need a valid sort key return; // need a valid sort key
} }
this.items.sort( (a, b) => { this.items.sort((a, b) => {
const a1 = a[key]; const a1 = a[key];
const b1 = b[key]; const b1 = b[key];
if(!a1) { if (!a1) {
return -1; return -1;
} }
if(!b1) { if (!b1) {
return 1; return 1;
} }
return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); return a1.localeCompare(b1, { sensitivity: false, numeric: true });
}); });
this.sorted = true; this.sorted = true;
}; };
MenuView.prototype.removeItem = function(index) { MenuView.prototype.removeItem = function (index) {
this.sorted = false; this.sorted = false;
this.items.splice(index, 1); this.items.splice(index, 1);
if(this.focusItems) { if (this.focusItems) {
this.focusItems.splice(index, 1); this.focusItems.splice(index, 1);
} }
if(this.focusedItemIndex >= index) { if (this.focusedItemIndex >= index) {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
} }
@ -176,62 +183,62 @@ MenuView.prototype.removeItem = function(index) {
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
MenuView.prototype.getCount = function() { MenuView.prototype.getCount = function () {
return this.items.length; return this.items.length;
}; };
MenuView.prototype.getItems = function() { MenuView.prototype.getItems = function () {
if(this.complexItems) { if (this.complexItems) {
return this.items; return this.items;
} }
return this.items.map( item => { return this.items.map(item => {
return item.text; return item.text;
}); });
}; };
MenuView.prototype.getItem = function(index) { MenuView.prototype.getItem = function (index) {
if(this.complexItems) { if (this.complexItems) {
return this.items[index]; return this.items[index];
} }
return this.items[index].text; return this.items[index].text;
}; };
MenuView.prototype.focusNext = function() { MenuView.prototype.focusNext = function () {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusPrevious = function() { MenuView.prototype.focusPrevious = function () {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusNextPageItem = function() { MenuView.prototype.focusNextPageItem = function () {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusPreviousPageItem = function() { MenuView.prototype.focusPreviousPageItem = function () {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusFirst = function() { MenuView.prototype.focusFirst = function () {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.focusLast = function() { MenuView.prototype.focusLast = function () {
this.emitIndexUpdate(); this.emitIndexUpdate();
}; };
MenuView.prototype.setFocusItemIndex = function(index) { MenuView.prototype.setFocusItemIndex = function (index) {
this.focusedItemIndex = index; this.focusedItemIndex = index;
}; };
MenuView.prototype.onKeyPress = function(ch, key) { MenuView.prototype.onKeyPress = function (ch, key) {
const itemIndex = this.getHotKeyItemIndex(ch); const itemIndex = this.getHotKeyItemIndex(ch);
if(itemIndex >= 0) { if (itemIndex >= 0) {
this.setFocusItemIndex(itemIndex); this.setFocusItemIndex(itemIndex);
if(true === this.hotKeySubmit) { if (true === this.hotKeySubmit) {
this.emit('action', 'accept'); this.emit('action', 'accept');
} }
} }
@ -239,79 +246,99 @@ MenuView.prototype.onKeyPress = function(ch, key) {
MenuView.super_.prototype.onKeyPress.call(this, ch, key); MenuView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
MenuView.prototype.setFocusItems = function(items) { MenuView.prototype.setFocusItems = function (items) {
const self = this; const self = this;
if(items) { if (items) {
this.focusItems = []; this.focusItems = [];
items.forEach( itemText => { items.forEach(itemText => {
this.focusItems.push( this.focusItems.push({
{ text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client),
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) });
}
);
}); });
} }
}; };
MenuView.prototype.setItemSpacing = function(itemSpacing) { MenuView.prototype.setItemSpacing = function (itemSpacing) {
itemSpacing = parseInt(itemSpacing); itemSpacing = parseInt(itemSpacing);
assert(_.isNumber(itemSpacing)); assert(_.isNumber(itemSpacing));
this.itemSpacing = itemSpacing; this.itemSpacing = itemSpacing;
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { MenuView.prototype.setItemHorizSpacing = function (itemHorizSpacing) {
itemHorizSpacing = parseInt(itemHorizSpacing); itemHorizSpacing = parseInt(itemHorizSpacing);
assert(_.isNumber(itemHorizSpacing)); assert(_.isNumber(itemHorizSpacing));
this.itemHorizSpacing = itemHorizSpacing; this.itemHorizSpacing = itemHorizSpacing;
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.prototype.setPropertyValue = function (propName, value) {
switch(propName) { switch (propName) {
case 'itemSpacing' : this.setItemSpacing(value); break; case 'itemSpacing':
case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; this.setItemSpacing(value);
case 'items' : this.setItems(value); break; break;
case 'focusItems' : this.setFocusItems(value); break; case 'itemHorizSpacing':
case 'hotKeys' : this.setHotKeys(value); break; this.setItemHorizSpacing(value);
case 'textOverflow' : this.setTextOverflow(value); break; break;
case 'hotKeySubmit' : this.hotKeySubmit = value; break; case 'items':
case 'justify' : this.setJustify(value); break; this.setItems(value);
case 'fillChar' : this.setFillChar(value); break; break;
case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'focusItems':
this.setFocusItems(value);
break;
case 'hotKeys':
this.setHotKeys(value);
break;
case 'textOverflow':
this.setTextOverflow(value);
break;
case 'hotKeySubmit':
this.hotKeySubmit = value;
break;
case 'justify':
this.setJustify(value);
break;
case 'fillChar':
this.setFillChar(value);
break;
case 'focusItemIndex':
this.focusedItemIndex = value;
break;
case 'itemFormat' : case 'itemFormat':
case 'focusItemFormat' : case 'focusItemFormat':
this[propName] = value; this[propName] = value;
// if there is a cache currently, invalidate it // if there is a cache currently, invalidate it
this.invalidateRenderCache(); this.invalidateRenderCache();
break; break;
case 'sort' : this.setSort(value); break; case 'sort':
this.setSort(value);
break;
} }
MenuView.super_.prototype.setPropertyValue.call(this, propName, value); MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
}; };
MenuView.prototype.setFillChar = function(fillChar) { MenuView.prototype.setFillChar = function (fillChar) {
this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1);
this.invalidateRenderCache(); this.invalidateRenderCache();
} };
MenuView.prototype.setJustify = function(justify) { MenuView.prototype.setJustify = function (justify) {
this.justify = justify; this.justify = justify;
this.invalidateRenderCache(); this.invalidateRenderCache();
this.positionCacheExpired = true; this.positionCacheExpired = true;
} };
MenuView.prototype.setHotKeys = function(hotKeys) { MenuView.prototype.setHotKeys = function (hotKeys) {
if(_.isObject(hotKeys)) { if (_.isObject(hotKeys)) {
if(this.caseInsensitiveHotKeys) { if (this.caseInsensitiveHotKeys) {
this.hotKeys = {}; this.hotKeys = {};
for(var key in hotKeys) { for (var key in hotKeys) {
this.hotKeys[key.toLowerCase()] = hotKeys[key]; this.hotKeys[key.toLowerCase()] = hotKeys[key];
} }
} else { } else {
@ -319,4 +346,3 @@ MenuView.prototype.setHotKeys = function(hotKeys) {
} }
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@ -2,71 +2,80 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const msgDb = require('./database.js').dbs.message; const msgDb = require('./database.js').dbs.message;
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Message = require('./message.js'); const Message = require('./message.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const msgNetRecord = require('./msg_network.js').recordMessage; const msgNetRecord = require('./msg_network.js').recordMessage;
const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs; const sortAreasOrConfs = require('./conf_area_util.js').sortAreasOrConfs;
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const SysProps = require('./system_property.js'); const SysProps = require('./system_property.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const moment = require('moment'); const moment = require('moment');
exports.startup = startup; exports.startup = startup;
exports.shutdown = shutdown; exports.shutdown = shutdown;
exports.getAvailableMessageConferences = getAvailableMessageConferences; exports.getAvailableMessageConferences = getAvailableMessageConferences;
exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences; exports.getSortedAvailMessageConferences = getSortedAvailMessageConferences;
exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag; exports.getAvailableMessageAreasByConfTag = getAvailableMessageAreasByConfTag;
exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag; exports.getSortedAvailMessageAreasByConfTag = getSortedAvailMessageAreasByConfTag;
exports.getAllAvailableMessageAreaTags = getAllAvailableMessageAreaTags; exports.getAllAvailableMessageAreaTags = getAllAvailableMessageAreaTags;
exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag; exports.getDefaultMessageConferenceTag = getDefaultMessageConferenceTag;
exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag; exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags; exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags;
exports.getMessageConferenceByTag = getMessageConferenceByTag; exports.getMessageConferenceByTag = getMessageConferenceByTag;
exports.getMessageAreaByTag = getMessageAreaByTag; exports.getMessageAreaByTag = getMessageAreaByTag;
exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag; exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag;
exports.changeMessageConference = changeMessageConference; exports.changeMessageConference = changeMessageConference;
exports.changeMessageArea = changeMessageArea; exports.changeMessageArea = changeMessageArea;
exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead;
exports.hasMessageConfAndAreaWrite = hasMessageConfAndAreaWrite; exports.hasMessageConfAndAreaWrite = hasMessageConfAndAreaWrite;
exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS; exports.filterMessageAreaTagsByReadACS = filterMessageAreaTagsByReadACS;
exports.filterMessageListByReadACS = filterMessageListByReadACS; exports.filterMessageListByReadACS = filterMessageListByReadACS;
exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea; exports.tempChangeMessageConfAndArea = tempChangeMessageConfAndArea;
exports.getMessageListForArea = getMessageListForArea; exports.getMessageListForArea = getMessageListForArea;
exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser; exports.getNewMessageCountInAreaForUser = getNewMessageCountInAreaForUser;
exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser; exports.getNewMessagesInAreaForUser = getNewMessagesInAreaForUser;
exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea; exports.getMessageIdNewerThanTimestampByArea = getMessageIdNewerThanTimestampByArea;
exports.getMessageAreaLastReadId = getMessageAreaLastReadId; exports.getMessageAreaLastReadId = getMessageAreaLastReadId;
exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId; exports.updateMessageAreaLastReadId = updateMessageAreaLastReadId;
exports.persistMessage = persistMessage; exports.persistMessage = persistMessage;
exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent; exports.trimMessageAreasScheduledEvent = trimMessageAreasScheduledEvent;
function startup(cb) { function startup(cb) {
// by default, private messages are NOT included // by default, private messages are NOT included
async.series( async.series(
[ [
(callback) => { callback => {
Message.findMessages( { resultType : 'count' }, (err, count) => { Message.findMessages({ resultType: 'count' }, (err, count) => {
if(count) { if (count) {
StatLog.setNonPersistentSystemStat(SysProps.MessageTotalCount, count); StatLog.setNonPersistentSystemStat(
SysProps.MessageTotalCount,
count
);
} }
return callback(err); return callback(err);
}); });
}, },
(callback) => { callback => {
Message.findMessages( { resultType : 'count', date : moment() }, (err, count) => { Message.findMessages(
if(count) { { resultType: 'count', date: moment() },
StatLog.setNonPersistentSystemStat(SysProps.MessagesToday, count); (err, count) => {
if (count) {
StatLog.setNonPersistentSystemStat(
SysProps.MessagesToday,
count
);
}
return callback(err);
} }
return callback(err); );
}); },
}
], ],
err => { err => {
return cb(err); return cb(err);
@ -79,13 +88,13 @@ function shutdown(cb) {
} }
function getAvailableMessageConferences(client, options) { function getAvailableMessageConferences(client, options) {
options = options || { includeSystemInternal : false }; options = options || { includeSystemInternal: false };
assert(client || true === options.noClient); assert(client || true === options.noClient);
// perform ACS check per conf & omit system_internal if desired // perform ACS check per conf & omit system_internal if desired
return _.omitBy(Config().messageConferences, (conf, confTag) => { return _.omitBy(Config().messageConferences, (conf, confTag) => {
if(!options.includeSystemInternal && 'system_internal' === confTag) { if (!options.includeSystemInternal && 'system_internal' === confTag) {
return true; return true;
} }
@ -96,8 +105,8 @@ function getAvailableMessageConferences(client, options) {
function getSortedAvailMessageConferences(client, options) { function getSortedAvailMessageConferences(client, options) {
const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => { const confs = _.map(getAvailableMessageConferences(client, options), (v, k) => {
return { return {
confTag : k, confTag: k,
conf : v, conf: v,
}; };
}); });
@ -113,10 +122,10 @@ function getAvailableMessageAreasByConfTag(confTag, options) {
// :TODO: confTag === "" then find default // :TODO: confTag === "" then find default
const config = Config(); const config = Config();
if(_.has(config.messageConferences, [ confTag, 'areas' ])) { if (_.has(config.messageConferences, [confTag, 'areas'])) {
const areas = config.messageConferences[confTag].areas; const areas = config.messageConferences[confTag].areas;
if(!options.client || true === options.noAcsCheck) { if (!options.client || true === options.noAcsCheck) {
// everything - no ACS checks // everything - no ACS checks
return areas; return areas;
} else { } else {
@ -130,9 +139,9 @@ function getAvailableMessageAreasByConfTag(confTag, options) {
function getSortedAvailMessageAreasByConfTag(confTag, options) { function getSortedAvailMessageAreasByConfTag(confTag, options) {
const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => { const areas = _.map(getAvailableMessageAreasByConfTag(confTag, options), (v, k) => {
return { return {
areaTag : k, areaTag: k,
area : v, area: v,
}; };
}); });
@ -145,11 +154,13 @@ function getAllAvailableMessageAreaTags(client, options) {
const areaTags = []; const areaTags = [];
// mask over older messy APIs for now // mask over older messy APIs for now
const confOpts = Object.assign({}, options, { noClient : client ? false : true }); const confOpts = Object.assign({}, options, { noClient: client ? false : true });
const areaOpts = Object.assign({}, options, { client }); const areaOpts = Object.assign({}, options, { client });
Object.keys(getAvailableMessageConferences(client, confOpts)).forEach(confTag => { Object.keys(getAvailableMessageConferences(client, confOpts)).forEach(confTag => {
areaTags.push(...Object.keys(getAvailableMessageAreasByConfTag(confTag, areaOpts))); areaTags.push(
...Object.keys(getAvailableMessageAreasByConfTag(confTag, areaOpts))
);
}); });
return areaTags; return areaTags;
@ -170,16 +181,19 @@ function getDefaultMessageConferenceTag(client, disableAcsCheck) {
// //
const config = Config(); const config = Config();
let defaultConf = _.findKey(config.messageConferences, o => o.default); let defaultConf = _.findKey(config.messageConferences, o => o.default);
if(defaultConf) { if (defaultConf) {
const conf = config.messageConferences[defaultConf]; const conf = config.messageConferences[defaultConf];
if(true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) { if (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)) {
return defaultConf; return defaultConf;
} }
} }
// just use anything we can // just use anything we can
defaultConf = _.findKey(config.messageConferences, (conf, confTag) => { defaultConf = _.findKey(config.messageConferences, (conf, confTag) => {
return 'system_internal' !== confTag && (true === disableAcsCheck || client.acs.hasMessageConfRead(conf)); return (
'system_internal' !== confTag &&
(true === disableAcsCheck || client.acs.hasMessageConfRead(conf))
);
}); });
return defaultConf; return defaultConf;
@ -196,21 +210,21 @@ function getDefaultMessageAreaTagByConfTag(client, confTag, disableAcsCheck) {
confTag = confTag || getDefaultMessageConferenceTag(client); confTag = confTag || getDefaultMessageConferenceTag(client);
const config = Config(); const config = Config();
if(confTag && _.has(config.messageConferences, [ confTag, 'areas' ])) { if (confTag && _.has(config.messageConferences, [confTag, 'areas'])) {
const areaPool = config.messageConferences[confTag].areas; const areaPool = config.messageConferences[confTag].areas;
let defaultArea = _.findKey(areaPool, o => o.default); let defaultArea = _.findKey(areaPool, o => o.default);
if(defaultArea) { if (defaultArea) {
const area = areaPool[defaultArea]; const area = areaPool[defaultArea];
if(true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) { if (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)) {
return defaultArea; return defaultArea;
} }
} }
defaultArea = _.findKey(areaPool, (area, areaTag) => { defaultArea = _.findKey(areaPool, (area, areaTag) => {
if(Message.isPrivateAreaTag(areaTag)) { if (Message.isPrivateAreaTag(areaTag)) {
return false; return false;
} }
return (true === disableAcsCheck || client.acs.hasMessageAreaRead(area)); return true === disableAcsCheck || client.acs.hasMessageAreaRead(area);
}); });
return defaultArea; return defaultArea;
@ -229,26 +243,29 @@ function getSuitableMessageConfAndAreaTags(client) {
// if we fail to find something. // if we fail to find something.
// //
let confTag = getDefaultMessageConferenceTag(client); let confTag = getDefaultMessageConferenceTag(client);
if(!confTag) { if (!confTag) {
return ['', '']; // can't have an area without a conf return ['', '']; // can't have an area without a conf
} }
let areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); let areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
if(!areaTag) { if (!areaTag) {
// OK, perhaps *any* area in *any* conf? // OK, perhaps *any* area in *any* conf?
_.forEach(Config().messageConferences, (conf, ct) => { _.forEach(Config().messageConferences, (conf, ct) => {
if(!client.acs.hasMessageConfRead(conf)) { if (!client.acs.hasMessageConfRead(conf)) {
return; return;
} }
_.forEach(conf.areas, (area, at) => { _.forEach(conf.areas, (area, at) => {
if(!_.includes(Message.WellKnownAreaTags, at) && client.acs.hasMessageAreaRead(area)) { if (
!_.includes(Message.WellKnownAreaTags, at) &&
client.acs.hasMessageAreaRead(area)
) {
confTag = ct; confTag = ct;
areaTag = at; areaTag = at;
return false; // stop inner iteration return false; // stop inner iteration
} }
}); });
if(areaTag) { if (areaTag) {
return false; // stop iteration return false; // stop iteration
} }
}); });
} }
@ -262,8 +279,8 @@ function getMessageConferenceByTag(confTag) {
function getMessageConfTagByAreaTag(areaTag) { function getMessageConfTagByAreaTag(areaTag) {
const confs = Config().messageConferences; const confs = Config().messageConferences;
return Object.keys(confs).find( (confTag) => { return Object.keys(confs).find(confTag => {
return _.has(confs, [ confTag, 'areas', areaTag]); return _.has(confs, [confTag, 'areas', areaTag]);
}); });
} }
@ -271,12 +288,12 @@ function getMessageAreaByTag(areaTag, optionalConfTag) {
const confs = Config().messageConferences; const confs = Config().messageConferences;
// :TODO: this could be cached // :TODO: this could be cached
if(_.isString(optionalConfTag)) { if (_.isString(optionalConfTag)) {
if(_.has(confs, [ optionalConfTag, 'areas', areaTag ])) { if (_.has(confs, [optionalConfTag, 'areas', areaTag])) {
return Object.assign( return Object.assign(
{ {
areaTag, areaTag,
confTag : optionalConfTag, confTag: optionalConfTag,
}, },
confs[optionalConfTag].areas[areaTag] confs[optionalConfTag].areas[areaTag]
); );
@ -287,9 +304,9 @@ function getMessageAreaByTag(areaTag, optionalConfTag) {
// //
let area; let area;
_.forEach(confs, (conf, confTag) => { _.forEach(confs, (conf, confTag) => {
if(_.has(conf, [ 'areas', areaTag ])) { if (_.has(conf, ['areas', areaTag])) {
area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]); area = Object.assign({ areaTag, confTag }, conf.areas[areaTag]);
return false; // stop iteration return false; // stop iteration
} }
}); });
@ -303,33 +320,38 @@ function changeMessageConference(client, confTag, cb) {
function getConf(callback) { function getConf(callback) {
const conf = getMessageConferenceByTag(confTag); const conf = getMessageConferenceByTag(confTag);
if(conf) { if (conf) {
callback(null, conf); callback(null, conf);
} else { } else {
callback(new Error('Invalid message conference tag')); callback(new Error('Invalid message conference tag'));
} }
}, },
function getDefaultAreaInConf(conf, callback) { function getDefaultAreaInConf(conf, callback) {
const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag); const areaTag = getDefaultMessageAreaTagByConfTag(client, confTag);
const area = getMessageAreaByTag(areaTag, confTag); const area = getMessageAreaByTag(areaTag, confTag);
if(area) { if (area) {
callback(null, conf, { areaTag : areaTag, area : area } ); callback(null, conf, { areaTag: areaTag, area: area });
} else { } else {
callback(new Error('No available areas for this user in conference')); callback(new Error('No available areas for this user in conference'));
} }
}, },
function validateAccess(conf, areaInfo, callback) { function validateAccess(conf, areaInfo, callback) {
if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(areaInfo.area)) { if (
return callback(new Error('Access denied to message area and/or conference')); !client.acs.hasMessageConfRead(conf) ||
!client.acs.hasMessageAreaRead(areaInfo.area)
) {
return callback(
new Error('Access denied to message area and/or conference')
);
} else { } else {
return callback(null, conf, areaInfo); return callback(null, conf, areaInfo);
} }
}, },
function changeConferenceAndArea(conf, areaInfo, callback) { function changeConferenceAndArea(conf, areaInfo, callback) {
const newProps = { const newProps = {
[ UserProps.MessageConfTag ] : confTag, [UserProps.MessageConfTag]: confTag,
[ UserProps.MessageAreaTag ] : areaInfo.areaTag, [UserProps.MessageAreaTag]: areaInfo.areaTag,
}; };
client.user.persistProperties(newProps, err => { client.user.persistProperties(newProps, err => {
callback(err, conf, areaInfo); callback(err, conf, areaInfo);
@ -337,10 +359,16 @@ function changeMessageConference(client, confTag, cb) {
}, },
], ],
function complete(err, conf, areaInfo) { function complete(err, conf, areaInfo) {
if(!err) { if (!err) {
client.log.info( { confTag : confTag, confName : conf.name, areaTag : areaInfo.areaTag }, 'Current message conference changed'); client.log.info(
{ confTag: confTag, confName: conf.name, areaTag: areaInfo.areaTag },
'Current message conference changed'
);
} else { } else {
client.log.warn( { confTag : confTag, error : err.message }, 'Could not change message conference'); client.log.warn(
{ confTag: confTag, error: err.message },
'Could not change message conference'
);
} }
cb(err); cb(err);
} }
@ -348,7 +376,7 @@ function changeMessageConference(client, confTag, cb) {
} }
function changeMessageAreaWithOptions(client, areaTag, options, cb) { function changeMessageAreaWithOptions(client, areaTag, options, cb) {
options = options || {}; // :TODO: this is currently pointless... cb is required... options = options || {}; // :TODO: this is currently pointless... cb is required...
async.waterfall( async.waterfall(
[ [
@ -360,28 +388,38 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
// //
// Need at least *read* to access the area // Need at least *read* to access the area
// //
if(!client.acs.hasMessageAreaRead(area)) { if (!client.acs.hasMessageAreaRead(area)) {
return callback(new Error('Access denied to message area')); return callback(new Error('Access denied to message area'));
} else { } else {
return callback(null, area); return callback(null, area);
} }
}, },
function changeArea(area, callback) { function changeArea(area, callback) {
if(true === options.persist) { if (true === options.persist) {
client.user.persistProperty(UserProps.MessageAreaTag, areaTag, function persisted(err) { client.user.persistProperty(
return callback(err, area); UserProps.MessageAreaTag,
}); areaTag,
function persisted(err) {
return callback(err, area);
}
);
} else { } else {
client.user.properties[UserProps.MessageAreaTag] = areaTag; client.user.properties[UserProps.MessageAreaTag] = areaTag;
return callback(null, area); return callback(null, area);
} }
} },
], ],
function complete(err, area) { function complete(err, area) {
if(!err) { if (!err) {
client.log.info( { areaTag : areaTag, area : area }, 'Current message area changed'); client.log.info(
{ areaTag: areaTag, area: area },
'Current message area changed'
);
} else { } else {
client.log.warn( { areaTag : areaTag, area : area, error : err.message }, 'Could not change message area'); client.log.warn(
{ areaTag: areaTag, area: area, error: err.message },
'Could not change message area'
);
} }
return cb(err); return cb(err);
@ -396,16 +434,16 @@ function changeMessageAreaWithOptions(client, areaTag, options, cb) {
// This is useful for example when doing a new scan // This is useful for example when doing a new scan
// //
function tempChangeMessageConfAndArea(client, areaTag) { function tempChangeMessageConfAndArea(client, areaTag) {
const area = getMessageAreaByTag(areaTag); const area = getMessageAreaByTag(areaTag);
const confTag = getMessageConfTagByAreaTag(areaTag); const confTag = getMessageConfTagByAreaTag(areaTag);
if(!area || !confTag) { if (!area || !confTag) {
return false; return false;
} }
const conf = getMessageConferenceByTag(confTag); const conf = getMessageConferenceByTag(confTag);
if(!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) { if (!client.acs.hasMessageConfRead(conf) || !client.acs.hasMessageAreaRead(area)) {
return false; return false;
} }
@ -416,31 +454,35 @@ function tempChangeMessageConfAndArea(client, areaTag) {
} }
function changeMessageArea(client, areaTag, cb) { function changeMessageArea(client, areaTag, cb) {
changeMessageAreaWithOptions(client, areaTag, { persist : true }, cb); changeMessageAreaWithOptions(client, areaTag, { persist: true }, cb);
} }
function hasMessageConfAndAreaRead(client, areaOrTag) { function hasMessageConfAndAreaRead(client, areaOrTag) {
if(_.isString(areaOrTag)) { if (_.isString(areaOrTag)) {
areaOrTag = getMessageAreaByTag(areaOrTag) || {}; areaOrTag = getMessageAreaByTag(areaOrTag) || {};
} }
const conf = getMessageConferenceByTag(areaOrTag.confTag); const conf = getMessageConferenceByTag(areaOrTag.confTag);
return client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag); return (
client.acs.hasMessageConfRead(conf) && client.acs.hasMessageAreaRead(areaOrTag)
);
} }
function hasMessageConfAndAreaWrite(client, areaOrTag) { function hasMessageConfAndAreaWrite(client, areaOrTag) {
if(_.isString(areaOrTag)) { if (_.isString(areaOrTag)) {
areaOrTag = getMessageAreaByTag(areaOrTag) || {}; areaOrTag = getMessageAreaByTag(areaOrTag) || {};
} }
const conf = getMessageConferenceByTag(areaOrTag.confTag); const conf = getMessageConferenceByTag(areaOrTag.confTag);
return client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag); return (
client.acs.hasMessageConfWrite(conf) && client.acs.hasMessageAreaWrite(areaOrTag)
);
} }
function filterMessageAreaTagsByReadACS(client, areaTags) { function filterMessageAreaTagsByReadACS(client, areaTags) {
if(!Array.isArray(areaTags)) { if (!Array.isArray(areaTags)) {
areaTags = [ areaTags ]; areaTags = [areaTags];
} }
return areaTags.filter( areaTag => { return areaTags.filter(areaTag => {
const area = getMessageAreaByTag(areaTag); const area = getMessageAreaByTag(areaTag);
return hasMessageConfAndAreaRead(client, area); return hasMessageConfAndAreaRead(client, area);
}); });
@ -453,14 +495,14 @@ function filterMessageListByReadACS(client, messageList) {
// //
// Keep a cache around for quick lookup. // Keep a cache around for quick lookup.
const acsCache = new Map(); // areaTag:boolean const acsCache = new Map(); // areaTag:boolean
return messageList.filter(msg => { return messageList.filter(msg => {
let cached = acsCache.get(msg.areaTag); let cached = acsCache.get(msg.areaTag);
if(false === cached) { if (false === cached) {
return false; return false;
} }
if(true === cached) { if (true === cached) {
return true; return true;
} }
cached = hasMessageConfAndAreaRead(client, msg.areaTag); cached = hasMessageConfAndAreaRead(client, msg.areaTag);
@ -475,11 +517,11 @@ function getNewMessageCountInAreaForUser(userId, areaTag, cb) {
const filter = { const filter = {
areaTag, areaTag,
newerThanMessageId : lastMessageId, newerThanMessageId: lastMessageId,
resultType : 'count', resultType: 'count',
}; };
if(Message.isPrivateAreaTag(areaTag)) { if (Message.isPrivateAreaTag(areaTag)) {
filter.privateTagUserId = userId; filter.privateTagUserId = userId;
} }
@ -495,13 +537,13 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) {
const filter = { const filter = {
areaTag, areaTag,
resultType : 'messageList', resultType: 'messageList',
newerThanMessageId : lastMessageId, newerThanMessageId: lastMessageId,
sort : 'messageId', sort: 'messageId',
order : 'ascending', order: 'ascending',
}; };
if(Message.isPrivateAreaTag(areaTag)) { if (Message.isPrivateAreaTag(areaTag)) {
filter.privateTagUserId = userId; filter.privateTagUserId = userId;
} }
@ -509,27 +551,26 @@ function getNewMessagesInAreaForUser(userId, areaTag, cb) {
}); });
} }
function getMessageListForArea(client, areaTag, filter, cb) function getMessageListForArea(client, areaTag, filter, cb) {
{ if (!cb && _.isFunction(filter)) {
if(!cb && _.isFunction(filter)) {
cb = filter; cb = filter;
filter = { filter = {
areaTag, areaTag,
resultType : 'messageList', resultType: 'messageList',
sort : 'messageId', sort: 'messageId',
order : 'ascending' order: 'ascending',
}; };
} else { } else {
Object.assign(filter, { areaTag } ); Object.assign(filter, { areaTag });
} }
if(client) { if (client) {
if(!hasMessageConfAndAreaRead(client, areaTag)) { if (!hasMessageConfAndAreaRead(client, areaTag)) {
return cb(null, []); return cb(null, []);
} }
} }
if(Message.isPrivateAreaTag(areaTag)) { if (Message.isPrivateAreaTag(areaTag)) {
filter.privateTagUserId = client ? client.user.userId : 'INVALID_USER_ID'; filter.privateTagUserId = client ? client.user.userId : 'INVALID_USER_ID';
} }
@ -541,12 +582,12 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
{ {
areaTag, areaTag,
newerThanTimestamp, newerThanTimestamp,
sort : 'modTimestamp', sort: 'modTimestamp',
order : 'ascending', order: 'ascending',
limit : 1, limit: 1,
}, },
(err, id) => { (err, id) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
return cb(null, id ? id[0] : null); return cb(null, id ? id[0] : null);
@ -556,10 +597,10 @@ function getMessageIdNewerThanTimestampByArea(areaTag, newerThanTimestamp, cb) {
function getMessageAreaLastReadId(userId, areaTag, cb) { function getMessageAreaLastReadId(userId, areaTag, cb) {
msgDb.get( msgDb.get(
'SELECT message_id ' + 'SELECT message_id ' +
'FROM user_message_area_last_read ' + 'FROM user_message_area_last_read ' +
'WHERE user_id = ? AND area_tag = ?;', 'WHERE user_id = ? AND area_tag = ?;',
[ userId, areaTag.toLowerCase() ], [userId, areaTag.toLowerCase()],
function complete(err, row) { function complete(err, row) {
cb(err, row ? row.message_id : 0); cb(err, row ? row.message_id : 0);
} }
@ -567,7 +608,7 @@ function getMessageAreaLastReadId(userId, areaTag, cb) {
} }
function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) { function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb) {
if(!cb && _.isFunction(allowOlder)) { if (!cb && _.isFunction(allowOlder)) {
cb = allowOlder; cb = allowOlder;
allowOlder = false; allowOlder = false;
} }
@ -582,30 +623,37 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb)
}); });
}, },
function update(lastId, callback) { function update(lastId, callback) {
if(allowOlder || messageId > lastId) { if (allowOlder || messageId > lastId) {
msgDb.run( msgDb.run(
'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' + 'REPLACE INTO user_message_area_last_read (user_id, area_tag, message_id) ' +
'VALUES (?, ?, ?);', 'VALUES (?, ?, ?);',
[ userId, areaTag, messageId ], [userId, areaTag, messageId],
function written(err) { function written(err) {
callback(err, true); // true=didUpdate callback(err, true); // true=didUpdate
} }
); );
} else { } else {
callback(null); callback(null);
} }
} },
], ],
function complete(err, didUpdate) { function complete(err, didUpdate) {
if(err) { if (err) {
Log.debug( Log.debug(
{ error : err.toString(), userId : userId, areaTag : areaTag, messageId : messageId }, {
'Failed updating area last read ID'); error: err.toString(),
userId: userId,
areaTag: areaTag,
messageId: messageId,
},
'Failed updating area last read ID'
);
} else { } else {
if(true === didUpdate) { if (true === didUpdate) {
Log.trace( Log.trace(
{ userId : userId, areaTag : areaTag, messageId : messageId }, { userId: userId, areaTag: areaTag, messageId: messageId },
'Area last read ID updated'); 'Area last read ID updated'
);
} }
} }
cb(err); cb(err);
@ -621,7 +669,7 @@ function persistMessage(message, cb) {
}, },
function recordToMessageNetworks(callback) { function recordToMessageNetworks(callback) {
return msgNetRecord(message, callback); return msgNetRecord(message, callback);
} },
], ],
cb cb
); );
@ -629,9 +677,8 @@ function persistMessage(message, cb) {
// method exposed for event scheduler // method exposed for event scheduler
function trimMessageAreasScheduledEvent(args, cb) { function trimMessageAreasScheduledEvent(args, cb) {
function trimMessageAreaByMaxMessages(areaInfo, cb) { function trimMessageAreaByMaxMessages(areaInfo, cb) {
if(0 === areaInfo.maxMessages) { if (0 === areaInfo.maxMessages) {
return cb(null); return cb(null);
} }
@ -644,12 +691,19 @@ function trimMessageAreasScheduledEvent(args, cb) {
ORDER BY message_id DESC ORDER BY message_id DESC
LIMIT -1 OFFSET ${areaInfo.maxMessages} LIMIT -1 OFFSET ${areaInfo.maxMessages}
);`, );`,
[ areaInfo.areaTag.toLowerCase() ], [areaInfo.areaTag.toLowerCase()],
function result(err) { // no arrow func; need this function result(err) {
if(err) { // no arrow func; need this
Log.error( { areaInfo : areaInfo, error : err.message, type : 'maxMessages' }, 'Error trimming message area'); if (err) {
Log.error(
{ areaInfo: areaInfo, error: err.message, type: 'maxMessages' },
'Error trimming message area'
);
} else { } else {
Log.debug( { areaInfo : areaInfo, type : 'maxMessages', count : this.changes }, 'Area trimmed successfully'); Log.debug(
{ areaInfo: areaInfo, type: 'maxMessages', count: this.changes },
'Area trimmed successfully'
);
} }
return cb(err); return cb(err);
} }
@ -657,19 +711,26 @@ function trimMessageAreasScheduledEvent(args, cb) {
} }
function trimMessageAreaByMaxAgeDays(areaInfo, cb) { function trimMessageAreaByMaxAgeDays(areaInfo, cb) {
if(0 === areaInfo.maxAgeDays) { if (0 === areaInfo.maxAgeDays) {
return cb(null); return cb(null);
} }
msgDb.run( msgDb.run(
`DELETE FROM message `DELETE FROM message
WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`, WHERE area_tag = ? AND modified_timestamp < date('now', '-${areaInfo.maxAgeDays} days');`,
[ areaInfo.areaTag ], [areaInfo.areaTag],
function result(err) { // no arrow func; need this function result(err) {
if(err) { // no arrow func; need this
Log.warn( { areaInfo : areaInfo, error : err.message, type : 'maxAgeDays' }, 'Error trimming message area'); if (err) {
Log.warn(
{ areaInfo: areaInfo, error: err.message, type: 'maxAgeDays' },
'Error trimming message area'
);
} else { } else {
Log.debug( { areaInfo : areaInfo, type : 'maxAgeDays', count : this.changes }, 'Area trimmed successfully'); Log.debug(
{ areaInfo: areaInfo, type: 'maxAgeDays', count: this.changes },
'Area trimmed successfully'
);
} }
return cb(err); return cb(err);
} }
@ -688,12 +749,12 @@ function trimMessageAreasScheduledEvent(args, cb) {
`SELECT DISTINCT area_tag `SELECT DISTINCT area_tag
FROM message;`, FROM message;`,
(err, row) => { (err, row) => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
// We treat private mail special // We treat private mail special
if(!Message.isPrivateAreaTag(row.area_tag)) { if (!Message.isPrivateAreaTag(row.area_tag)) {
areaTags.push(row.area_tag); areaTags.push(row.area_tag);
} }
}, },
@ -708,21 +769,20 @@ function trimMessageAreasScheduledEvent(args, cb) {
// determine maxMessages & maxAgeDays per area // determine maxMessages & maxAgeDays per area
const config = Config(); const config = Config();
areaTags.forEach(areaTag => { areaTags.forEach(areaTag => {
let maxMessages = config.messageAreaDefaults.maxMessages; let maxMessages = config.messageAreaDefaults.maxMessages;
let maxAgeDays = config.messageAreaDefaults.maxAgeDays; let maxAgeDays = config.messageAreaDefaults.maxAgeDays;
const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here const area = getMessageAreaByTag(areaTag); // note: we don't know the conf here
if(area) { if (area) {
maxMessages = area.maxMessages || maxMessages; maxMessages = area.maxMessages || maxMessages;
maxAgeDays = area.maxAgeDays || maxAgeDays; maxAgeDays = area.maxAgeDays || maxAgeDays;
} }
areaInfos.push( { areaInfos.push({
areaTag : areaTag, areaTag: areaTag,
maxMessages : maxMessages, maxMessages: maxMessages,
maxAgeDays : maxAgeDays, maxAgeDays: maxAgeDays,
} ); });
}); });
return callback(null, areaInfos); return callback(null, areaInfos);
@ -732,7 +792,7 @@ function trimMessageAreasScheduledEvent(args, cb) {
areaInfos, areaInfos,
(areaInfo, next) => { (areaInfo, next) => {
trimMessageAreaByMaxMessages(areaInfo, err => { trimMessageAreaByMaxMessages(areaInfo, err => {
if(err) { if (err) {
return next(err); return next(err);
} }
@ -773,20 +833,27 @@ function trimMessageAreasScheduledEvent(args, cb) {
(mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}') (mmf.meta_category='System' AND mmf.meta_name='${Message.SystemMetaNames.ExternalFlavor}')
WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days') WHERE m.area_tag='${Message.WellKnownAreaTags.Private}' AND DATETIME('now') > DATETIME(m.modified_timestamp, '+${maxExternalSentAgeDays} days')
);`, );`,
function results(err) { // no arrow func; need this function results(err) {
if(err) { // no arrow func; need this
Log.warn( { error : err.message }, 'Error trimming private externally sent messages'); if (err) {
Log.warn(
{ error: err.message },
'Error trimming private externally sent messages'
);
} else { } else {
Log.debug( { count : this.changes }, 'Private externally sent messages trimmed successfully'); Log.debug(
{ count: this.changes },
'Private externally sent messages trimmed successfully'
);
} }
} }
); );
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
} }
); );
} }

View File

@ -22,44 +22,51 @@ const _ = require('lodash');
const fse = require('fs-extra'); const fse = require('fs-extra');
const temptmp = require('temptmp'); const temptmp = require('temptmp');
const paths = require('path'); const paths = require('path');
const { v4 : UUIDv4 } = require('uuid'); const { v4: UUIDv4 } = require('uuid');
const moment = require('moment'); const moment = require('moment');
const FormIds = { const FormIds = {
main : 0, main: 0,
}; };
const MciViewIds = { const MciViewIds = {
main : { main: {
status : 1, status: 1,
progressBar : 2, progressBar: 2,
customRangeStart : 10, customRangeStart: 10,
} },
}; };
const UserProperties = { const UserProperties = {
ExportOptions : 'qwk_export_options', ExportOptions: 'qwk_export_options',
ExportAreas : 'qwk_export_msg_areas', ExportAreas: 'qwk_export_msg_areas',
}; };
exports.moduleInfo = { exports.moduleInfo = {
name : 'QWK Export', name: 'QWK Export',
desc : 'Exports a QWK Packet for download', desc: 'Exports a QWK Packet for download',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class MessageBaseQWKExport extends MenuModule { exports.getModule = class MessageBaseQWKExport extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config = Object.assign(
{},
_.get(options, 'menuConfig.config'),
options.extraArgs
);
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); this.config.progBarChar = renderSubstr(this.config.progBarChar || '▒', 0, 1);
this.config.bbsID = this.config.bbsID || _.get(Config(), 'messageNetworks.qwk.bbsID', 'ENIGMA'); this.config.bbsID =
this.config.bbsID || _.get(Config(), 'messageNetworks.qwk.bbsID', 'ENIGMA');
this.tempName = `${UUIDv4().substr(-8).toUpperCase()}.QWK`; this.tempName = `${UUIDv4().substr(-8).toUpperCase()}.QWK`;
this.sysTempDownloadArea = FileArea.getFileAreaByTag(FileArea.WellKnownAreaTags.TempDownloads); this.sysTempDownloadArea = FileArea.getFileAreaByTag(
FileArea.WellKnownAreaTags.TempDownloads
);
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
@ -70,27 +77,38 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
async.waterfall( async.waterfall(
[ [
(callback) => { callback => {
this.prepViewController('main', FormIds.main, mciData.menu, err => { this.prepViewController(
return callback(err); 'main',
}); FormIds.main,
}, mciData.menu,
(callback) => { err => {
this.temptmp = temptmp.createTrackedSession('qwkuserexp');
this.temptmp.mkdir({ prefix : 'enigqwkwriter-'}, (err, tempDir) => {
if (err) {
return callback(err); return callback(err);
} }
);
},
callback => {
this.temptmp = temptmp.createTrackedSession('qwkuserexp');
this.temptmp.mkdir(
{ prefix: 'enigqwkwriter-' },
(err, tempDir) => {
if (err) {
return callback(err);
}
this.tempPacketDir = tempDir; this.tempPacketDir = tempDir;
const sysTempDownloadDir = FileArea.getAreaDefaultStorageDirectory(this.sysTempDownloadArea); const sysTempDownloadDir =
FileArea.getAreaDefaultStorageDirectory(
this.sysTempDownloadArea
);
// ensure dir exists // ensure dir exists
fse.mkdirs(sysTempDownloadDir, err => { fse.mkdirs(sysTempDownloadDir, err => {
return callback(err, sysTempDownloadDir); return callback(err, sysTempDownloadDir);
}); });
}); }
);
}, },
(sysTempDownloadDir, callback) => { (sysTempDownloadDir, callback) => {
this._performExport(sysTempDownloadDir, err => { this._performExport(sysTempDownloadDir, err => {
@ -104,7 +122,10 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
if (err) { if (err) {
// :TODO: doesn't do anything currently: // :TODO: doesn't do anything currently:
if ('NORESULTS' === err.reasonCode) { if ('NORESULTS' === err.reasonCode) {
return this.gotoMenu(this.menuConfig.config.noResultsMenu || 'qwkExportNoResults'); return this.gotoMenu(
this.menuConfig.config.noResultsMenu ||
'qwkExportNoResults'
);
} }
return this.prevMenu(); return this.prevMenu();
@ -123,12 +144,12 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
let qwkOptions = this.client.user.getProperty(UserProperties.ExportOptions); let qwkOptions = this.client.user.getProperty(UserProperties.ExportOptions);
try { try {
qwkOptions = JSON.parse(qwkOptions); qwkOptions = JSON.parse(qwkOptions);
} catch(e) { } catch (e) {
qwkOptions = { qwkOptions = {
enableQWKE : true, enableQWKE: true,
enableHeadersExtension : true, enableHeadersExtension: true,
enableAtKludges : true, enableAtKludges: true,
archiveFormat : 'application/zip', archiveFormat: 'application/zip',
}; };
} }
return qwkOptions; return qwkOptions;
@ -143,7 +164,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
} }
return exportArea; return exportArea;
}); });
} catch(e) { } catch (e) {
// default to all public and private without 'since' // default to all public and private without 'since'
qwkExportAreas = getAllAvailableMessageAreaTags(this.client).map(areaTag => { qwkExportAreas = getAllAvailableMessageAreaTags(this.client).map(areaTag => {
return { areaTag }; return { areaTag };
@ -151,7 +172,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
// Include user's private area // Include user's private area
qwkExportAreas.push({ qwkExportAreas.push({
areaTag : Message.WellKnownAreaTags.Private, areaTag: Message.WellKnownAreaTags.Private,
}); });
} }
@ -160,16 +181,18 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
_performExport(sysTempDownloadDir, cb) { _performExport(sysTempDownloadDir, cb) {
const statusView = this.viewControllers.main.getView(MciViewIds.main.status); const statusView = this.viewControllers.main.getView(MciViewIds.main.status);
const updateStatus = (status) => { const updateStatus = status => {
if (statusView) { if (statusView) {
statusView.setText(status); statusView.setText(status);
} }
}; };
const progBarView = this.viewControllers.main.getView(MciViewIds.main.progressBar); const progBarView = this.viewControllers.main.getView(
MciViewIds.main.progressBar
);
const updateProgressBar = (curr, total) => { const updateProgressBar = (curr, total) => {
if (progBarView) { if (progBarView) {
const prog = Math.floor( (curr / total) * progBarView.dimens.width ); const prog = Math.floor((curr / total) * progBarView.dimens.width);
progBarView.setText(this.config.progBarChar.repeat(prog)); progBarView.setText(this.config.progBarChar.repeat(prog));
} }
}; };
@ -181,19 +204,27 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
// we can produce a TON of updates; only update progress at most every 3/4s // we can produce a TON of updates; only update progress at most every 3/4s
if (Date.now() - lastProgUpdate > 750) { if (Date.now() - lastProgUpdate > 750) {
switch (state.step) { switch (state.step) {
case 'next_area' : case 'next_area':
updateStatus(state.status); updateStatus(state.status);
updateProgressBar(0, 0); updateProgressBar(0, 0);
this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state); this.updateCustomViewTextsWithFilter(
'main',
MciViewIds.main.customRangeStart,
state
);
break; break;
case 'message' : case 'message':
updateStatus(state.status); updateStatus(state.status);
updateProgressBar(state.current, state.total); updateProgressBar(state.current, state.total);
this.updateCustomViewTextsWithFilter('main', MciViewIds.main.customRangeStart, state); this.updateCustomViewTextsWithFilter(
'main',
MciViewIds.main.customRangeStart,
state
);
break; break;
default : default:
break; break;
} }
lastProgUpdate = Date.now(); lastProgUpdate = Date.now();
@ -203,7 +234,7 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
}; };
const keyPressHandler = (ch, key) => { const keyPressHandler = (ch, key) => {
if('escape' === key.name) { if ('escape' === key.name) {
cancel = true; cancel = true;
this.client.removeListener('key press', keyPressHandler); this.client.removeListener('key press', keyPressHandler);
} }
@ -217,54 +248,59 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
} }
let current = 1; let current = 1;
async.eachSeries(messageIds, (messageId, nextMessageId) => { async.eachSeries(
const message = new Message(); messageIds,
message.load({ messageId }, err => { (messageId, nextMessageId) => {
if (err) { const message = new Message();
return nextMessageId(err); message.load({ messageId }, err => {
}
const progress = {
message,
step : 'message',
total : ++totalExported,
areaCurrent : current,
areaCount : messageIds.length,
status : `${_.truncate(message.subject, { length : 25 })} (${current} / ${messageIds.length})`,
};
progressHandler(progress, err => {
if (err) { if (err) {
return nextMessageId(err); return nextMessageId(err);
} }
packetWriter.appendMessage(message); const progress = {
current += 1; message,
step: 'message',
total: ++totalExported,
areaCurrent: current,
areaCount: messageIds.length,
status: `${_.truncate(message.subject, {
length: 25,
})} (${current} / ${messageIds.length})`,
};
return nextMessageId(null); progressHandler(progress, err => {
if (err) {
return nextMessageId(err);
}
packetWriter.appendMessage(message);
current += 1;
return nextMessageId(null);
});
}); });
}); },
}, err => {
err => { return cb(err);
return cb(err); }
}); );
}); });
}; };
const packetWriter = new QWKPacketWriter( const packetWriter = new QWKPacketWriter(
Object.assign(this._getUserQWKExportOptions(), { Object.assign(this._getUserQWKExportOptions(), {
user : this.client.user, user: this.client.user,
bbsID : this.config.bbsID, bbsID: this.config.bbsID,
}) })
); );
packetWriter.on('warning', warning => { packetWriter.on('warning', warning => {
this.client.log.warn( { warning }, 'QWK packet writer warning'); this.client.log.warn({ warning }, 'QWK packet writer warning');
}); });
async.waterfall( async.waterfall(
[ [
(callback) => { callback => {
// don't count idle monitor while processing // don't count idle monitor while processing
this.client.stopIdleMonitor(); this.client.stopIdleMonitor();
@ -276,77 +312,91 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
}); });
packetWriter.once('error', err => { packetWriter.once('error', err => {
this.client.log.error( { error : err.message }, 'QWK packet writer error'); this.client.log.error(
{ error: err.message },
'QWK packet writer error'
);
cancel = true; cancel = true;
}); });
packetWriter.init(); packetWriter.init();
}, },
(callback) => { callback => {
// For each public area -> for each message // For each public area -> for each message
const userExportAreas = this._getUserQWKExportAreas(); const userExportAreas = this._getUserQWKExportAreas();
const publicExportAreas = userExportAreas const publicExportAreas = userExportAreas.filter(exportArea => {
.filter(exportArea => { return exportArea.areaTag !== Message.WellKnownAreaTags.Private;
return exportArea.areaTag !== Message.WellKnownAreaTags.Private; });
}); async.eachSeries(
async.eachSeries(publicExportAreas, (exportArea, nextExportArea) => { publicExportAreas,
const area = getMessageAreaByTag(exportArea.areaTag); (exportArea, nextExportArea) => {
const conf = getMessageConferenceByTag(area.confTag); const area = getMessageAreaByTag(exportArea.areaTag);
if (!area || !conf) { const conf = getMessageConferenceByTag(area.confTag);
// :TODO: remove from user properties - this area does not exist if (!area || !conf) {
this.client.log.warn({ areaTag : exportArea.areaTag }, 'Cannot QWK export area as it does not exist'); // :TODO: remove from user properties - this area does not exist
return nextExportArea(null); this.client.log.warn(
} { areaTag: exportArea.areaTag },
'Cannot QWK export area as it does not exist'
if (!hasMessageConfAndAreaRead(this.client, area)) { );
this.client.log.warn({ areaTag : area.areaTag }, 'Cannot QWK export area due to ACS'); return nextExportArea(null);
return nextExportArea(null);
}
const progress = {
conf,
area,
step : 'next_area',
status : `Gathering in ${conf.name} - ${area.name}...`,
};
progressHandler(progress, err => {
if (err) {
return nextExportArea(err);
} }
const filter = { if (!hasMessageConfAndAreaRead(this.client, area)) {
resultType : 'id', this.client.log.warn(
areaTag : exportArea.areaTag, { areaTag: area.areaTag },
newerThanTimestamp : exportArea.newerThanTimestamp 'Cannot QWK export area due to ACS'
);
return nextExportArea(null);
}
const progress = {
conf,
area,
step: 'next_area',
status: `Gathering in ${conf.name} - ${area.name}...`,
}; };
processMessagesWithFilter(filter, err => { progressHandler(progress, err => {
return nextExportArea(err); if (err) {
return nextExportArea(err);
}
const filter = {
resultType: 'id',
areaTag: exportArea.areaTag,
newerThanTimestamp: exportArea.newerThanTimestamp,
};
processMessagesWithFilter(filter, err => {
return nextExportArea(err);
});
}); });
}); },
}, err => {
err => { return callback(err, userExportAreas);
return callback(err, userExportAreas); }
}); );
}, },
(userExportAreas, callback) => { (userExportAreas, callback) => {
// Private messages to current user if the user has // Private messages to current user if the user has
// elected to export private messages // elected to export private messages
const privateExportArea = userExportAreas.find(exportArea => exportArea.areaTag === Message.WellKnownAreaTags.Private); const privateExportArea = userExportAreas.find(
exportArea =>
exportArea.areaTag === Message.WellKnownAreaTags.Private
);
if (!privateExportArea) { if (!privateExportArea) {
return callback(null); return callback(null);
} }
const filter = { const filter = {
resultType : 'id', resultType: 'id',
privateTagUserId : this.client.user.userId, privateTagUserId: this.client.user.userId,
newerThanTimestamp : privateExportArea.newerThanTimestamp, newerThanTimestamp: privateExportArea.newerThanTimestamp,
}; };
return processMessagesWithFilter(filter, callback); return processMessagesWithFilter(filter, callback);
}, },
(callback) => { callback => {
let packetInfo; let packetInfo;
packetWriter.once('packet', info => { packetWriter.once('packet', info => {
packetInfo = info; packetInfo = info;
@ -370,38 +420,40 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
}, },
(sysDownloadPath, packetInfo, callback) => { (sysDownloadPath, packetInfo, callback) => {
const newEntry = new FileEntry({ const newEntry = new FileEntry({
areaTag : this.sysTempDownloadArea.areaTag, areaTag: this.sysTempDownloadArea.areaTag,
fileName : paths.basename(sysDownloadPath), fileName: paths.basename(sysDownloadPath),
storageTag : this.sysTempDownloadArea.storageTags[0], storageTag: this.sysTempDownloadArea.storageTags[0],
meta : { meta: {
upload_by_username : this.client.user.username, upload_by_username: this.client.user.username,
upload_by_user_id : this.client.user.userId, upload_by_user_id: this.client.user.userId,
byte_size : packetInfo.stats.size, byte_size: packetInfo.stats.size,
session_temp_dl : 1, // download is valid until session is over session_temp_dl: 1, // download is valid until session is over
// :TODO: something like this: allow to override the displayed/downloaded as filename // :TODO: something like this: allow to override the displayed/downloaded as filename
// separate from the actual on disk filename. E.g. we could always download as "ENIGMA.QWK" // separate from the actual on disk filename. E.g. we could always download as "ENIGMA.QWK"
//visible_filename : paths.basename(packetInfo.path), //visible_filename : paths.basename(packetInfo.path),
} },
}); });
newEntry.desc = 'QWK Export'; newEntry.desc = 'QWK Export';
newEntry.persist(err => { newEntry.persist(err => {
if(!err) { if (!err) {
// queue it! // queue it!
DownloadQueue.get(this.client).addTemporaryDownload(newEntry); DownloadQueue.get(this.client).addTemporaryDownload(newEntry);
} }
return callback(err); return callback(err);
}); });
}, },
(callback) => { callback => {
// update user's export area dates; they can always change/reset them again // update user's export area dates; they can always change/reset them again
const updatedUserExportAreas = this._getUserQWKExportAreas().map(exportArea => { const updatedUserExportAreas = this._getUserQWKExportAreas().map(
return Object.assign(exportArea, { exportArea => {
newerThanTimestamp : getISOTimestampString(), return Object.assign(exportArea, {
}); newerThanTimestamp: getISOTimestampString(),
}); });
}
);
return this.client.user.persistProperty( return this.client.user.persistProperty(
UserProperties.ExportAreas, UserProperties.ExportAreas,
@ -425,4 +477,4 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
} }
); );
} }
}; };

View File

@ -2,36 +2,36 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const { const {
getSortedAvailMessageConferences, getSortedAvailMessageConferences,
getAvailableMessageAreasByConfTag, getAvailableMessageAreasByConfTag,
getSortedAvailMessageAreasByConfTag, getSortedAvailMessageAreasByConfTag,
hasMessageConfAndAreaRead, hasMessageConfAndAreaRead,
filterMessageListByReadACS, filterMessageListByReadACS,
} = require('./message_area.js'); } = require('./message_area.js');
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const Message = require('./message.js'); const Message = require('./message.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Base Search', name: 'Message Base Search',
desc : 'Module for quickly searching the message base', desc: 'Module for quickly searching the message base',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
search : { search: {
searchTerms : 1, searchTerms: 1,
search : 2, search: 2,
conf : 3, conf: 3,
area : 4, area: 4,
to : 5, to: 5,
from : 6, from: 6,
advSearch : 7, advSearch: 7,
} },
}; };
exports.getModule = class MessageBaseSearch extends MenuModule { exports.getModule = class MessageBaseSearch extends MenuModule {
@ -39,35 +39,37 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
super(options); super(options);
this.menuMethods = { this.menuMethods = {
search : (formData, extraArgs, cb) => { search: (formData, extraArgs, cb) => {
return this.searchNow(formData, cb); return this.searchNow(formData, cb);
} },
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
this.prepViewController('search', 0, mciData.menu, (err, vc) => { this.prepViewController('search', 0, mciData.menu, (err, vc) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const confView = vc.getView(MciViewIds.search.conf); const confView = vc.getView(MciViewIds.search.conf);
const areaView = vc.getView(MciViewIds.search.area); const areaView = vc.getView(MciViewIds.search.area);
if(!confView || !areaView) { if (!confView || !areaView) {
return cb(Errors.DoesNotExist('Missing one or more required views')); return cb(Errors.DoesNotExist('Missing one or more required views'));
} }
const availConfs = [ { text : '-ALL-', data : '' } ].concat( const availConfs = [{ text: '-ALL-', data: '' }].concat(
getSortedAvailMessageConferences(this.client).map(conf => Object.assign(conf, { text : conf.conf.name, data : conf.confTag } )) || [] getSortedAvailMessageConferences(this.client).map(conf =>
Object.assign(conf, { text: conf.conf.name, data: conf.confTag })
) || []
); );
let availAreas = [ { text : '-ALL-', data : '' } ]; // note: will populate if conf changes from ALL let availAreas = [{ text: '-ALL-', data: '' }]; // note: will populate if conf changes from ALL
confView.setItems(availConfs); confView.setItems(availConfs);
areaView.setItems(availAreas); areaView.setItems(availAreas);
@ -76,9 +78,14 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
areaView.setFocusItemIndex(0); areaView.setFocusItemIndex(0);
confView.on('index update', idx => { confView.on('index update', idx => {
availAreas = [ { text : '-ALL-', data : '' } ].concat( availAreas = [{ text: '-ALL-', data: '' }].concat(
getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, { client : this.client }).map( getSortedAvailMessageAreasByConfTag(availConfs[idx].confTag, {
area => Object.assign(area, { text : area.area.name, data : area.areaTag } ) client: this.client,
}).map(area =>
Object.assign(area, {
text: area.area.name,
data: area.areaTag,
})
) )
); );
areaView.setItems(availAreas); areaView.setItems(availAreas);
@ -92,38 +99,40 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
} }
searchNow(formData, cb) { searchNow(formData, cb) {
const isAdvanced = formData.submitId === MciViewIds.search.advSearch; const isAdvanced = formData.submitId === MciViewIds.search.advSearch;
const value = formData.value; const value = formData.value;
const filter = { const filter = {
resultType : 'messageList', resultType: 'messageList',
sort : 'modTimestamp', sort: 'modTimestamp',
terms : value.searchTerms, terms: value.searchTerms,
//extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ], //extraFields : [ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ],
limit : 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned limit: 2048, // :TODO: best way to handle this? we should probably let the user know if some results are returned
}; };
const returnNoResults = () => { const returnNoResults = () => {
return this.gotoMenu( return this.gotoMenu(
this.menuConfig.config.noResultsMenu || 'messageSearchNoResults', this.menuConfig.config.noResultsMenu || 'messageSearchNoResults',
{ menuFlags : [ 'popParent' ] }, { menuFlags: ['popParent'] },
cb cb
); );
}; };
if(isAdvanced) { if (isAdvanced) {
filter.toUserName = value.toUserName; filter.toUserName = value.toUserName;
filter.fromUserName = value.fromUserName; filter.fromUserName = value.fromUserName;
if(value.confTag && !value.areaTag) { if (value.confTag && !value.areaTag) {
// areaTag may be a string or array of strings // areaTag may be a string or array of strings
// getAvailableMessageAreasByConfTag() returns a obj - we only need tags // getAvailableMessageAreasByConfTag() returns a obj - we only need tags
filter.areaTag = _.map( filter.areaTag = _.map(
getAvailableMessageAreasByConfTag(value.confTag, { client : this.client } ), getAvailableMessageAreasByConfTag(value.confTag, {
client: this.client,
}),
(area, areaTag) => areaTag (area, areaTag) => areaTag
); );
} else if(value.areaTag) { } else if (value.areaTag) {
if(hasMessageConfAndAreaRead(this.client, value.areaTag)) { if (hasMessageConfAndAreaRead(this.client, value.areaTag)) {
filter.areaTag = value.areaTag; // specific conf + area filter.areaTag = value.areaTag; // specific conf + area
} else { } else {
return returnNoResults(); return returnNoResults();
@ -132,26 +141,26 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
} }
Message.findMessages(filter, (err, messageList) => { Message.findMessages(filter, (err, messageList) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
// don't include results without ACS -- if the user searched by // don't include results without ACS -- if the user searched by
// explicit conf/area tag, we should have already filtered (above) // explicit conf/area tag, we should have already filtered (above)
if(!value.confTag && !value.areaTag) { if (!value.confTag && !value.areaTag) {
messageList = filterMessageListByReadACS(this.client, messageList); messageList = filterMessageListByReadACS(this.client, messageList);
} }
if(0 === messageList.length) { if (0 === messageList.length) {
return returnNoResults(); return returnNoResults();
} }
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs: {
messageList, messageList,
noUpdateLastReadId : true noUpdateLastReadId: true,
}, },
menuFlags : [ 'popParent' ], menuFlags: ['popParent'],
}; };
return this.gotoMenu( return this.gotoMenu(

View File

@ -2,31 +2,31 @@
'use strict'; 'use strict';
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const mimeTypes = require('mime-types'); const mimeTypes = require('mime-types');
exports.startup = startup; exports.startup = startup;
exports.resolveMimeType = resolveMimeType; exports.resolveMimeType = resolveMimeType;
function startup(cb) { function startup(cb) {
// //
// Add in types (not yet) supported by mime-db -- and therefor, mime-types // Add in types (not yet) supported by mime-db -- and therefor, mime-types
// //
const ADDITIONAL_EXT_MIMETYPES = { const ADDITIONAL_EXT_MIMETYPES = {
ans : 'text/x-ansi', ans: 'text/x-ansi',
gz : 'application/gzip', // not in mime-types 2.1.15 :( gz: 'application/gzip', // not in mime-types 2.1.15 :(
lzx : 'application/x-lzx', // :TODO: submit to mime-types lzx: 'application/x-lzx', // :TODO: submit to mime-types
}; };
_.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => { _.forEach(ADDITIONAL_EXT_MIMETYPES, (mimeType, ext) => {
// don't override any entries // don't override any entries
if(!_.isString(mimeTypes.types[ext])) { if (!_.isString(mimeTypes.types[ext])) {
mimeTypes[ext] = mimeType; mimeTypes[ext] = mimeType;
} }
if(!mimeTypes.extensions[mimeType]) { if (!mimeTypes.extensions[mimeType]) {
mimeTypes.extensions[mimeType] = [ ext ]; mimeTypes.extensions[mimeType] = [ext];
} }
}); });
@ -34,9 +34,9 @@ function startup(cb) {
} }
function resolveMimeType(query) { function resolveMimeType(query) {
if(mimeTypes.extensions[query]) { if (mimeTypes.extensions[query]) {
return query; // already a mime-type return query; // already a mime-type
} }
return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined return mimeTypes.lookup(query) || undefined; // lookup() returns false; we want undefined
} }

View File

@ -1,8 +1,8 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const SysProps = require('./system_property.js'); const SysProps = require('./system_property.js');
exports.dailyMaintenanceScheduledEvent = dailyMaintenanceScheduledEvent; exports.dailyMaintenanceScheduledEvent = dailyMaintenanceScheduledEvent;
@ -10,7 +10,7 @@ function dailyMaintenanceScheduledEvent(args, cb) {
// //
// Various stats need reset daily // Various stats need reset daily
// //
[ SysProps.LoginsToday, SysProps.MessagesToday ].forEach(prop => { [SysProps.LoginsToday, SysProps.MessagesToday].forEach(prop => {
StatLog.setNonPersistentSystemStat(prop, 0); StatLog.setNonPersistentSystemStat(prop, 0);
}); });

View File

@ -2,18 +2,18 @@
'use strict'; 'use strict';
// deps // deps
const paths = require('path'); const paths = require('path');
const os = require('os'); const os = require('os');
const packageJson = require('../package.json'); const packageJson = require('../package.json');
exports.isProduction = isProduction; exports.isProduction = isProduction;
exports.isDevelopment = isDevelopment; exports.isDevelopment = isDevelopment;
exports.valueWithDefault = valueWithDefault; exports.valueWithDefault = valueWithDefault;
exports.resolvePath = resolvePath; exports.resolvePath = resolvePath;
exports.getCleanEnigmaVersion = getCleanEnigmaVersion; exports.getCleanEnigmaVersion = getCleanEnigmaVersion;
exports.getEnigmaUserAgent = getEnigmaUserAgent; exports.getEnigmaUserAgent = getEnigmaUserAgent;
exports.valueAsArray = valueAsArray; exports.valueAsArray = valueAsArray;
function isProduction() { function isProduction() {
var env = process.env.NODE_ENV || 'dev'; var env = process.env.NODE_ENV || 'dev';
@ -21,17 +21,22 @@ function isProduction() {
} }
function isDevelopment() { function isDevelopment() {
return (!(isProduction())); return !isProduction();
} }
function valueWithDefault(val, defVal) { function valueWithDefault(val, defVal) {
return (typeof val !== 'undefined' ? val : defVal); return typeof val !== 'undefined' ? val : defVal;
} }
function resolvePath(path) { function resolvePath(path) {
if(path.substr(0, 2) === '~/') { if (path.substr(0, 2) === '~/') {
var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH; var mswCombined = process.env.HOMEDRIVE + process.env.HOMEPATH;
path = (process.env.HOME || mswCombined || process.env.HOMEPATH || process.env.HOMEDIR || process.cwd()) + path.substr(1); path =
(process.env.HOME ||
mswCombined ||
process.env.HOMEPATH ||
process.env.HOMEDIR ||
process.cwd()) + path.substr(1);
} }
return paths.resolve(path); return paths.resolve(path);
} }
@ -39,23 +44,22 @@ function resolvePath(path) {
function getCleanEnigmaVersion() { function getCleanEnigmaVersion() {
return packageJson.version return packageJson.version
.replace(/-/g, '.') .replace(/-/g, '.')
.replace(/alpha/,'a') .replace(/alpha/, 'a')
.replace(/beta/,'b') .replace(/beta/, 'b');
;
} }
// See also ftn_util.js getTearLine() & getProductIdentifier() // See also ftn_util.js getTearLine() & getProductIdentifier()
function getEnigmaUserAgent() { function getEnigmaUserAgent() {
// can't have 1/2 or ½ in User-Agent according to RFC 1945 :( // can't have 1/2 or ½ in User-Agent according to RFC 1945 :(
const version = getCleanEnigmaVersion(); const version = getCleanEnigmaVersion();
const nodeVer = process.version.substr(1); // remove 'v' prefix const nodeVer = process.version.substr(1); // remove 'v' prefix
return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; return `ENiGMA-BBS/${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`;
} }
function valueAsArray(value) { function valueAsArray(value) {
if(!value) { if (!value) {
return []; return [];
} }
return Array.isArray(value) ? value : [ value ]; return Array.isArray(value) ? value : [value];
} }

View File

@ -1,36 +1,42 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const messageArea = require('../core/message_area.js'); const messageArea = require('../core/message_area.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
// deps // deps
const { get } = require('lodash'); const { get } = require('lodash');
exports.MessageAreaConfTempSwitcher = Sup => class extends Sup { exports.MessageAreaConfTempSwitcher = Sup =>
class extends Sup {
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
messageAreaTag =
messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
if (!messageAreaTag) {
return; // nothing to do!
}
tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) { if (recordPrevious) {
messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); this.prevMessageConfAndArea = {
if(!messageAreaTag) { confTag: this.client.user.properties[UserProps.MessageConfTag],
return; // nothing to do! areaTag: this.client.user.properties[UserProps.MessageAreaTag],
};
}
if (!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
this.client.log.warn(
{ messageAreaTag: messageArea },
'Failed to perform temporary message area/conf switch'
);
}
} }
if(recordPrevious) { tempMessageConfAndAreaRestore() {
this.prevMessageConfAndArea = { if (this.prevMessageConfAndArea) {
confTag : this.client.user.properties[UserProps.MessageConfTag], this.client.user.properties[UserProps.MessageConfTag] =
areaTag : this.client.user.properties[UserProps.MessageAreaTag], this.prevMessageConfAndArea.confTag;
}; this.client.user.properties[UserProps.MessageAreaTag] =
this.prevMessageConfAndArea.areaTag;
}
} }
};
if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch');
}
}
tempMessageConfAndAreaRestore() {
if(this.prevMessageConfAndArea) {
this.client.user.properties[UserProps.MessageConfTag] = this.prevMessageConfAndArea.confTag;
this.client.user.properties[UserProps.MessageAreaTag] = this.prevMessageConfAndArea.areaTag;
}
}
};

View File

@ -2,37 +2,41 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').get; const Config = require('./config.js').get;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const { const { Errors, ErrorReasons } = require('./enig_error.js');
Errors,
ErrorReasons
} = require('./enig_error.js');
// deps // deps
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const async = require('async'); const async = require('async');
const glob = require('glob'); const glob = require('glob');
// exports // exports
exports.loadModuleEx = loadModuleEx; exports.loadModuleEx = loadModuleEx;
exports.loadModule = loadModule; exports.loadModule = loadModule;
exports.loadModulesForCategory = loadModulesForCategory; exports.loadModulesForCategory = loadModulesForCategory;
exports.getModulePaths = getModulePaths; exports.getModulePaths = getModulePaths;
exports.initializeModules = initializeModules; exports.initializeModules = initializeModules;
function loadModuleEx(options, cb) { function loadModuleEx(options, cb) {
assert(_.isObject(options)); assert(_.isObject(options));
assert(_.isString(options.name)); assert(_.isString(options.name));
assert(_.isString(options.path)); assert(_.isString(options.path));
const modConfig = _.isObject(Config[options.category]) ? Config[options.category][options.name] : null; const modConfig = _.isObject(Config[options.category])
? Config[options.category][options.name]
: null;
if(_.isObject(modConfig) && false === modConfig.enabled) { if (_.isObject(modConfig) && false === modConfig.enabled) {
return cb(Errors.AccessDenied(`Module "${options.name}" is disabled`, ErrorReasons.Disabled)); return cb(
Errors.AccessDenied(
`Module "${options.name}" is disabled`,
ErrorReasons.Disabled
)
);
} }
// //
@ -41,15 +45,15 @@ function loadModuleEx(options, cb) {
// to have their own containing folder, package.json & dependencies, etc. // to have their own containing folder, package.json & dependencies, etc.
// //
let mod; let mod;
let modPath = paths.join(options.path, `${options.name}.js`); // general case first let modPath = paths.join(options.path, `${options.name}.js`); // general case first
try { try {
mod = require(modPath); mod = require(modPath);
} catch(e) { } catch (e) {
if('MODULE_NOT_FOUND' === e.code) { if ('MODULE_NOT_FOUND' === e.code) {
modPath = paths.join(options.path, options.name, `${options.name}.js`); modPath = paths.join(options.path, options.name, `${options.name}.js`);
try { try {
mod = require(modPath); mod = require(modPath);
} catch(e) { } catch (e) {
return cb(e); return cb(e);
} }
} else { } else {
@ -57,12 +61,16 @@ function loadModuleEx(options, cb) {
} }
} }
if(!_.isObject(mod.moduleInfo)) { if (!_.isObject(mod.moduleInfo)) {
return cb(Errors.Invalid(`No exported "moduleInfo" block for module ${modPath}!`)); return cb(
Errors.Invalid(`No exported "moduleInfo" block for module ${modPath}!`)
);
} }
if(!_.isFunction(mod.getModule)) { if (!_.isFunction(mod.getModule)) {
return cb(Errors.Invalid(`No exported "getModule" method for module ${modPath}!`)); return cb(
Errors.Invalid(`No exported "getModule" method for module ${modPath}!`)
);
} }
return cb(null, mod); return cb(null, mod);
@ -71,19 +79,25 @@ function loadModuleEx(options, cb) {
function loadModule(name, category, cb) { function loadModule(name, category, cb) {
const path = Config().paths[category]; const path = Config().paths[category];
if(!_.isString(path)) { if (!_.isString(path)) {
return cb(Errors.DoesNotExist(`Not sure where to look for module "${name}" of category "${category}"`)); return cb(
Errors.DoesNotExist(
`Not sure where to look for module "${name}" of category "${category}"`
)
);
} }
loadModuleEx( { name : name, path : path, category : category }, function loaded(err, mod) { loadModuleEx(
return cb(err, mod); { name: name, path: path, category: category },
}); function loaded(err, mod) {
return cb(err, mod);
}
);
} }
function loadModulesForCategory(category, iterator, complete) { function loadModulesForCategory(category, iterator, complete) {
fs.readdir(Config().paths[category], (err, files) => { fs.readdir(Config().paths[category], (err, files) => {
if(err) { if (err) {
return iterator(err); return iterator(err);
} }
@ -91,23 +105,27 @@ function loadModulesForCategory(category, iterator, complete) {
return '.js' === paths.extname(file); return '.js' === paths.extname(file);
}); });
async.each(jsModules, (file, next) => { async.each(
loadModule(paths.basename(file, '.js'), category, (err, mod) => { jsModules,
if(err) { (file, next) => {
if(ErrorReasons.Disabled === err.reasonCode) { loadModule(paths.basename(file, '.js'), category, (err, mod) => {
Log.debug(err.message); if (err) {
} else { if (ErrorReasons.Disabled === err.reasonCode) {
Log.info( { err : err }, 'Failed loading module'); Log.debug(err.message);
} else {
Log.info({ err: err }, 'Failed loading module');
}
return next(null); // continue no matter what
} }
return next(null); // continue no matter what return iterator(mod, next);
});
},
err => {
if (complete) {
return complete(err);
} }
return iterator(mod, next);
});
}, err => {
if(complete) {
return complete(err);
} }
}); );
}); });
} }
@ -127,48 +145,63 @@ function initializeModules(cb) {
const modulePaths = getModulePaths().concat(__dirname); const modulePaths = getModulePaths().concat(__dirname);
async.each(modulePaths, (modulePath, nextPath) => { async.each(
glob('*{.js,/*.js}', { cwd : modulePath }, (err, files) => { modulePaths,
if(err) { (modulePath, nextPath) => {
return nextPath(err); 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 ourPath = paths.join(__dirname, __filename);
const mod = require(fullModulePath);
if(_.isFunction(mod.moduleInitialize)) { async.each(
const initInfo = { files,
events : Events, (moduleName, nextModule) => {
}; const fullModulePath = paths.join(modulePath, moduleName);
if (ourPath === fullModulePath) {
mod.moduleInitialize(initInfo, err => {
if(err) {
Log.warn( { error : err.message, modulePath : fullModulePath }, 'Error during "moduleInitialize"');
}
return nextModule(null); return nextModule(null);
}); }
} else {
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.message, fullModulePath },
'Exception during "moduleInitialize"'
);
return nextModule(null);
}
},
err => {
return nextPath(err);
} }
} catch(e) { );
Log.warn( { error : e.message, fullModulePath }, 'Exception during "moduleInitialize"');
return nextModule(null);
}
},
err => {
return nextPath(err);
}); });
}); },
}, err => {
err => { return cb(err);
return cb(err); }
}); );
} }

View File

@ -2,27 +2,24 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const { const { pipeToAnsi, stripMciColorCodes } = require('./color_codes.js');
pipeToAnsi, const stringFormat = require('./string_format.js');
stripMciColorCodes const StringUtil = require('./string_util.js');
} = require('./color_codes.js'); const Config = require('./config.js').get;
const stringFormat = require('./string_format.js');
const StringUtil = require('./string_util.js');
const Config = require('./config.js').get;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const async = require('async'); const async = require('async');
const net = require('net'); const net = require('net');
const moment = require('moment'); const moment = require('moment');
exports.moduleInfo = { exports.moduleInfo = {
name : 'MRC Client', name: 'MRC Client',
desc : 'Connects to an MRC chat server', desc: 'Connects to an MRC chat server',
author : 'RiPuk', author: 'RiPuk',
packageName : 'codes.l33t.enigma.mrc.client', packageName: 'codes.l33t.enigma.mrc.client',
// Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were // Whilst this module was put together by me (RiPuk), it should be noted that a lot of the ideas (and even some code snippets) were
// borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together. // borrowed from the Synchronet implementation of MRC by echicken. So...thanks, your code was very helpful in putting this together.
@ -30,24 +27,22 @@ exports.moduleInfo = {
}; };
const FormIds = { const FormIds = {
mrcChat : 0, mrcChat: 0,
}; };
const MciViewIds = { const MciViewIds = {
mrcChat : { mrcChat: {
chatLog : 1, chatLog: 1,
inputArea : 2, inputArea: 2,
roomName : 3, roomName: 3,
roomTopic : 4, roomTopic: 4,
mrcUsers : 5, mrcUsers: 5,
mrcBbses : 6, mrcBbses: 6,
customRangeStart : 20, // 20+ = customs customRangeStart: 20, // 20+ = customs
} },
}; };
// TODO: this is a bit shit, could maybe do it with an ansi instead // TODO: this is a bit shit, could maybe do it with an ansi instead
const helpText = ` const helpText = `
|15General Chat|08: |15General Chat|08:
@ -66,13 +61,14 @@ const helpText = `
|03/|11rainbow |03<your message> |08- |07Crazy rainbow text |03/|11rainbow |03<your message> |08- |07Crazy rainbow text
`; `;
exports.getModule = class mrcModule extends MenuModule { exports.getModule = class mrcModule extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.log = Log.child( { module : 'MRC' } ); this.log = Log.child({ module: 'MRC' });
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), { extraArgs : options.extraArgs }); this.config = Object.assign({}, _.get(options, 'menuConfig.config'), {
extraArgs: options.extraArgs,
});
this.config.maxScrollbackLines = this.config.maxScrollbackLines || 500; this.config.maxScrollbackLines = this.config.maxScrollbackLines || 500;
@ -82,27 +78,27 @@ exports.getModule = class mrcModule extends MenuModule {
room: '', room: '',
room_topic: '', room_topic: '',
nicks: [], nicks: [],
lastSentMsg : {}, // used for latency est. lastSentMsg: {}, // used for latency est.
}; };
this.customFormatObj = { this.customFormatObj = {
roomName : '', roomName: '',
roomTopic : '', roomTopic: '',
roomUserCount : 0, roomUserCount: 0,
userCount : 0, userCount: 0,
boardCount : 0, boardCount: 0,
roomCount : 0, roomCount: 0,
latencyMs : 0, latencyMs: 0,
activityLevel : 0, activityLevel: 0,
activityLevelIndicator : ' ', activityLevelIndicator: ' ',
}; };
this.menuMethods = { this.menuMethods = {
sendChatMessage: (formData, extraArgs, cb) => {
sendChatMessage : (formData, extraArgs, cb) => { const inputAreaView = this.viewControllers.mrcChat.getView(
MciViewIds.mrcChat.inputArea
const inputAreaView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.inputArea); );
const inputData = inputAreaView.getData(); const inputData = inputAreaView.getData();
this.processOutgoingMessage(inputData); this.processOutgoingMessage(inputData);
inputAreaView.clearText(); inputAreaView.clearText();
@ -110,13 +106,23 @@ exports.getModule = class mrcModule extends MenuModule {
return cb(null); return cb(null);
}, },
movementKeyPressed : (formData, extraArgs, cb) => { movementKeyPressed: (formData, extraArgs, cb) => {
const bodyView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); const bodyView = this.viewControllers.mrcChat.getView(
switch(formData.key.name) { MciViewIds.mrcChat.chatLog
case 'down arrow' : bodyView.scrollDocumentUp(); break; );
case 'up arrow' : bodyView.scrollDocumentDown(); break; switch (formData.key.name) {
case 'page up' : bodyView.keyPressPageUp(); break; case 'down arrow':
case 'page down' : bodyView.keyPressPageDown(); break; bodyView.scrollDocumentUp();
break;
case 'up arrow':
bodyView.scrollDocumentDown();
break;
case 'page up':
bodyView.keyPressPageUp();
break;
case 'page down':
bodyView.keyPressPageDown();
break;
} }
this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea); this.viewControllers.mrcChat.switchFocus(MciViewIds.mrcChat.inputArea);
@ -124,35 +130,48 @@ exports.getModule = class mrcModule extends MenuModule {
return cb(null); return cb(null);
}, },
quit : (formData, extraArgs, cb) => { quit: (formData, extraArgs, cb) => {
return this.prevMenu(cb); return this.prevMenu(cb);
}, },
clearMessages : (formData, extraArgs, cb) => { clearMessages: (formData, extraArgs, cb) => {
this.clearMessages(); this.clearMessages();
return cb(null); return cb(null);
} },
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
async.series( async.series(
[ [
(callback) => { callback => {
return this.prepViewController('mrcChat', FormIds.mrcChat, mciData.menu, callback); return this.prepViewController(
'mrcChat',
FormIds.mrcChat,
mciData.menu,
callback
);
}, },
(callback) => { callback => {
return this.validateMCIByViewIds('mrcChat', [ MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea ], callback); return this.validateMCIByViewIds(
'mrcChat',
[MciViewIds.mrcChat.chatLog, MciViewIds.mrcChat.inputArea],
callback
);
}, },
(callback) => { callback => {
const connectOpts = { const connectOpts = {
port : _.get(Config(), 'chatServers.mrc.multiplexerPort', 5000), port: _.get(
host : 'localhost', Config(),
'chatServers.mrc.multiplexerPort',
5000
),
host: 'localhost',
}; };
// connect to multiplexer // connect to multiplexer
@ -167,18 +186,28 @@ exports.getModule = class mrcModule extends MenuModule {
this.clientConnect(); this.clientConnect();
// send register to central MRC and get stats every 60s // send register to central MRC and get stats every 60s
this.heartbeat = setInterval( () => { this.heartbeat = setInterval(() => {
this.sendHeartbeat(); this.sendHeartbeat();
this.sendServerMessage('STATS'); this.sendServerMessage('STATS');
}, 60000); }, 60000);
// override idle logout seconds if configured // override idle logout seconds if configured
const idleLogoutSeconds = parseInt(this.config.idleLogoutSeconds); const idleLogoutSeconds = parseInt(
if(0 === idleLogoutSeconds) { this.config.idleLogoutSeconds
this.log.debug('Temporary disable idle monitor due to config'); );
if (0 === idleLogoutSeconds) {
this.log.debug(
'Temporary disable idle monitor due to config'
);
this.client.stopIdleMonitor(); this.client.stopIdleMonitor();
} else if (!isNaN(idleLogoutSeconds) && idleLogoutSeconds >= 60) { } else if (
this.log.debug( { idleLogoutSeconds }, 'Temporary override idle logout seconds due to config'); !isNaN(idleLogoutSeconds) &&
idleLogoutSeconds >= 60
) {
this.log.debug(
{ idleLogoutSeconds },
'Temporary override idle logout seconds due to config'
);
this.client.overrideIdleLogoutSeconds(idleLogoutSeconds); this.client.overrideIdleLogoutSeconds(idleLogoutSeconds);
} }
}); });
@ -190,7 +219,10 @@ exports.getModule = class mrcModule extends MenuModule {
}); });
this.state.socket.once('error', err => { this.state.socket.once('error', err => {
this.log.warn( { error : err.message }, 'MRC multiplexer socket error' ); this.log.warn(
{ error: err.message },
'MRC multiplexer socket error'
);
this.state.socket.destroy(); this.state.socket.destroy();
delete this.state.socket; delete this.state.socket;
@ -198,8 +230,8 @@ exports.getModule = class mrcModule extends MenuModule {
return callback(err); return callback(err);
}); });
return(callback); return callback;
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -222,7 +254,7 @@ exports.getModule = class mrcModule extends MenuModule {
quitServer() { quitServer() {
clearInterval(this.heartbeat); clearInterval(this.heartbeat);
if(this.state.socket) { if (this.state.socket) {
this.sendServerMessage('LOGOFF'); this.sendServerMessage('LOGOFF');
this.state.socket.destroy(); this.state.socket.destroy();
delete this.state.socket; delete this.state.socket;
@ -233,12 +265,14 @@ exports.getModule = class mrcModule extends MenuModule {
* Adds a message to the chat log on screen * Adds a message to the chat log on screen
*/ */
addMessageToChatLog(message) { addMessageToChatLog(message) {
if(!Array.isArray(message)) { if (!Array.isArray(message)) {
message = [ message ]; message = [message];
} }
message.forEach(msg => { message.forEach(msg => {
const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); const chatLogView = this.viewControllers.mrcChat.getView(
MciViewIds.mrcChat.chatLog
);
const messageLength = stripMciColorCodes(msg).length; const messageLength = stripMciColorCodes(msg).length;
const chatWidth = chatLogView.dimens.width; const chatWidth = chatLogView.dimens.width;
let padAmount = 0; let padAmount = 0;
@ -255,7 +289,7 @@ exports.getModule = class mrcModule extends MenuModule {
const padding = ' |00' + ' '.repeat(padAmount); const padding = ' |00' + ' '.repeat(padAmount);
chatLogView.addText(pipeToAnsi(msg + padding)); chatLogView.addText(pipeToAnsi(msg + padding));
if(chatLogView.getLineCount() > this.config.maxScrollbackLines) { if (chatLogView.getLineCount() > this.config.maxScrollbackLines) {
chatLogView.deleteLine(0); chatLogView.deleteLine(0);
} }
}); });
@ -265,8 +299,7 @@ exports.getModule = class mrcModule extends MenuModule {
* Processes data received from the MRC multiplexer * Processes data received from the MRC multiplexer
*/ */
processReceivedMessage(blob) { processReceivedMessage(blob) {
blob.split('\n').forEach( message => { blob.split('\n').forEach(message => {
try { try {
message = JSON.parse(message); message = JSON.parse(message);
} catch (e) { } catch (e) {
@ -285,8 +318,8 @@ exports.getModule = class mrcModule extends MenuModule {
this.setText(MciViewIds.mrcChat.roomName, `#${params[1]}`); this.setText(MciViewIds.mrcChat.roomName, `#${params[1]}`);
this.setText(MciViewIds.mrcChat.roomTopic, params[2]); this.setText(MciViewIds.mrcChat.roomTopic, params[2]);
this.customFormatObj.roomName = params[1]; this.customFormatObj.roomName = params[1];
this.customFormatObj.roomTopic = params[2]; this.customFormatObj.roomTopic = params[2];
this.updateCustomViews(); this.updateCustomViews();
this.state.room = params[1]; this.state.room = params[1];
@ -300,22 +333,19 @@ exports.getModule = class mrcModule extends MenuModule {
break; break;
case 'STATS': { case 'STATS': {
const [ const [boardCount, roomCount, userCount, activityLevel] =
params[1].split(' ').map(v => parseInt(v));
const activityLevelIndicator =
this.getActivityLevelIndicator(activityLevel);
Object.assign(this.customFormatObj, {
boardCount, boardCount,
roomCount, roomCount,
userCount, userCount,
activityLevel activityLevel,
] = params[1].split(' ').map(v => parseInt(v)); activityLevelIndicator,
});
const activityLevelIndicator = this.getActivityLevelIndicator(activityLevel);
Object.assign(
this.customFormatObj,
{
boardCount, roomCount, userCount,
activityLevel, activityLevelIndicator
}
);
this.setText(MciViewIds.mrcChat.mrcUsers, userCount); this.setText(MciViewIds.mrcChat.mrcUsers, userCount);
this.setText(MciViewIds.mrcChat.mrcBbses, boardCount); this.setText(MciViewIds.mrcChat.mrcBbses, boardCount);
@ -328,18 +358,22 @@ exports.getModule = class mrcModule extends MenuModule {
this.addMessageToChatLog(message.body); this.addMessageToChatLog(message.body);
break; break;
} }
} else { } else {
if(message.body === this.state.lastSentMsg.msg) { if (message.body === this.state.lastSentMsg.msg) {
this.customFormatObj.latencyMs = this.customFormatObj.latencyMs = moment
moment.duration(moment().diff(this.state.lastSentMsg.time)).asMilliseconds(); .duration(moment().diff(this.state.lastSentMsg.time))
.asMilliseconds();
delete this.state.lastSentMsg.msg; delete this.state.lastSentMsg.msg;
} }
if (message.to_room == this.state.room) { if (message.to_room == this.state.room) {
// if we're here then we want to show it to the user // if we're here then we want to show it to the user
const currentTime = moment().format(this.client.currentTheme.helpers.getTimeFormat()); const currentTime = moment().format(
this.addMessageToChatLog('|08' + currentTime + '|00 ' + message.body + '|00'); this.client.currentTheme.helpers.getTimeFormat()
);
this.addMessageToChatLog(
'|08' + currentTime + '|00 ' + message.body + '|00'
);
} }
} }
@ -349,8 +383,8 @@ exports.getModule = class mrcModule extends MenuModule {
getActivityLevelIndicator(level) { getActivityLevelIndicator(level) {
let indicators = this.config.activityLevelIndicators; let indicators = this.config.activityLevelIndicators;
if(!Array.isArray(indicators) || indicators.length < level + 1) { if (!Array.isArray(indicators) || indicators.length < level + 1) {
indicators = [ ' ', '░', '▒', '▓' ]; indicators = [' ', '░', '▒', '▓'];
} }
return indicators[level]; return indicators[level];
} }
@ -382,9 +416,9 @@ exports.getModule = class mrcModule extends MenuModule {
// else just format and send // else just format and send
const textFormatObj = { const textFormatObj = {
fromUserName : this.state.alias, fromUserName: this.state.alias,
toUserName : to_user, toUserName: to_user,
message : message message: message,
}; };
const messageFormat = const messageFormat =
@ -406,15 +440,19 @@ exports.getModule = class mrcModule extends MenuModule {
try { try {
this.state.lastSentMsg = { this.state.lastSentMsg = {
msg : formattedMessage, msg: formattedMessage,
time : moment(), time: moment(),
}; };
this.sendMessageToMultiplexer(to_user || '', '', this.state.room, formattedMessage); this.sendMessageToMultiplexer(
} catch(e) { to_user || '',
this.client.log.warn( { error : e.message }, 'MRC error'); '',
this.state.room,
formattedMessage
);
} catch (e) {
this.client.log.warn({ error: e.message }, 'MRC error');
} }
} }
} }
/** /**
@ -432,24 +470,35 @@ exports.getModule = class mrcModule extends MenuModule {
case 'rainbow': { case 'rainbow': {
// this is brutal, but i love it // this is brutal, but i love it
const line = message.replace(/^\/rainbow\s/, '').split(' ').reduce(function (a, c) { const line = message
const cc = Math.floor((Math.random() * 31) + 1).toString().padStart(2, '0'); .replace(/^\/rainbow\s/, '')
a += `|${cc}${c}|00 `; .split(' ')
return a; .reduce(function (a, c) {
}, '').substr(0, 140).replace(/\\s\|\d*$/, ''); const cc = Math.floor(Math.random() * 31 + 1)
.toString()
.padStart(2, '0');
a += `|${cc}${c}|00 `;
return a;
}, '')
.substr(0, 140)
.replace(/\\s\|\d*$/, '');
this.processOutgoingMessage(line); this.processOutgoingMessage(line);
break; break;
} }
case 'l33t': case 'l33t':
this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), 'l33t')); this.processOutgoingMessage(
StringUtil.stylizeString(message.substr(6), 'l33t')
);
break; break;
case 'kewl': { case 'kewl': {
const text_modes = Array('f','v','V','i','M'); const text_modes = Array('f', 'v', 'V', 'i', 'M');
const mode = text_modes[Math.floor(Math.random() * text_modes.length)]; const mode = text_modes[Math.floor(Math.random() * text_modes.length)];
this.processOutgoingMessage(StringUtil.stylizeString(message.substr(6), mode)); this.processOutgoingMessage(
StringUtil.stylizeString(message.substr(6), mode)
);
break; break;
} }
@ -470,7 +519,9 @@ exports.getModule = class mrcModule extends MenuModule {
break; break;
case 'topic': case 'topic':
this.sendServerMessage(`NEWTOPIC:${this.state.room}:${message.substr(7)}`); this.sendServerMessage(
`NEWTOPIC:${this.state.room}:${message.substr(7)}`
);
break; break;
case 'info': case 'info':
@ -489,7 +540,7 @@ exports.getModule = class mrcModule extends MenuModule {
this.sendServerMessage('LIST'); this.sendServerMessage('LIST');
break; break;
case 'quit' : case 'quit':
return this.prevMenu(); return this.prevMenu();
case 'clear': case 'clear':
@ -501,7 +552,6 @@ exports.getModule = class mrcModule extends MenuModule {
break; break;
default: default:
break; break;
} }
@ -511,7 +561,9 @@ exports.getModule = class mrcModule extends MenuModule {
} }
clearMessages() { clearMessages() {
const chatLogView = this.viewControllers.mrcChat.getView(MciViewIds.mrcChat.chatLog); const chatLogView = this.viewControllers.mrcChat.getView(
MciViewIds.mrcChat.chatLog
);
chatLogView.setText(''); chatLogView.setText('');
} }
@ -519,17 +571,16 @@ exports.getModule = class mrcModule extends MenuModule {
* Creates a json object, stringifies it and sends it to the MRC multiplexer * Creates a json object, stringifies it and sends it to the MRC multiplexer
*/ */
sendMessageToMultiplexer(to_user, to_site, to_room, body) { sendMessageToMultiplexer(to_user, to_site, to_room, body) {
const message = { const message = {
to_user, to_user,
to_site, to_site,
to_room, to_room,
body, body,
from_user : this.state.alias, from_user: this.state.alias,
from_room : this.state.room, from_room: this.state.room,
}; };
if(this.state.socket) { if (this.state.socket) {
this.state.socket.write(JSON.stringify(message) + '\n'); this.state.socket.write(JSON.stringify(message) + '\n');
} }
} }
@ -570,7 +621,3 @@ exports.getModule = class mrcModule extends MenuModule {
this.sendHeartbeat(); this.sendHeartbeat();
} }
}; };

View File

@ -2,27 +2,27 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area List', name: 'Message Area List',
desc : 'Module for listing / choosing message areas', desc: 'Module for listing / choosing message areas',
author : 'NuSkooler', author: 'NuSkooler',
}; };
// :TODO: Obv/2 others can show # of messages in area // :TODO: Obv/2 others can show # of messages in area
const MciViewIds = { const MciViewIds = {
areaList : 1, areaList: 1,
areaDesc : 2, // area desc updated @ index update areaDesc: 2, // area desc updated @ index update
customRangeStart : 10, // updated @ index update customRangeStart: 10, // updated @ index update
}; };
exports.getModule = class MessageAreaListModule extends MenuModule { exports.getModule = class MessageAreaListModule extends MenuModule {
@ -32,25 +32,32 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
this.initList(); this.initList();
this.menuMethods = { this.menuMethods = {
changeArea : (formData, extraArgs, cb) => { changeArea: (formData, extraArgs, cb) => {
if(1 === formData.submitId) { if (1 === formData.submitId) {
const area = this.messageAreas[formData.value.area]; const area = this.messageAreas[formData.value.area];
messageArea.changeMessageArea(this.client, area.areaTag, err => { messageArea.changeMessageArea(this.client, area.areaTag, err => {
if(err) { if (err) {
this.client.term.pipeWrite(`\n|00Cannot change area: ${err.message}\n`); this.client.term.pipeWrite(
`\n|00Cannot change area: ${err.message}\n`
);
return this.prevMenuOnTimeout(1000, cb); return this.prevMenuOnTimeout(1000, cb);
} }
if(area.hasArt) { if (area.hasArt) {
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs: {
areaTag : area.areaTag, areaTag: area.areaTag,
}, },
menuFlags : [ 'popParent', 'noHistory' ] menuFlags: ['popParent', 'noHistory'],
}; };
return this.gotoMenu(this.menuConfig.config.changeAreaPreArtMenu || 'changeMessageAreaPreArt', menuOpts, cb); return this.gotoMenu(
this.menuConfig.config.changeAreaPreArtMenu ||
'changeMessageAreaPreArt',
menuOpts,
cb
);
} }
return this.prevMenu(cb); return this.prevMenu(cb);
@ -58,25 +65,31 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
} else { } else {
return cb(null); return cb(null);
} }
} },
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
async.series( async.series(
[ [
(next) => { next => {
return this.prepViewController('areaList', 0, mciData.menu, next); return this.prepViewController('areaList', 0, mciData.menu, next);
}, },
(next) => { next => {
const areaListView = this.viewControllers.areaList.getView(MciViewIds.areaList); const areaListView = this.viewControllers.areaList.getView(
if(!areaListView) { MciViewIds.areaList
return cb(Errors.MissingMci(`Missing area list MCI ${MciViewIds.areaList}`)); );
if (!areaListView) {
return cb(
Errors.MissingMci(
`Missing area list MCI ${MciViewIds.areaList}`
)
);
} }
areaListView.on('index update', idx => { areaListView.on('index update', idx => {
@ -87,11 +100,14 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
areaListView.redraw(); areaListView.redraw();
this.selectionIndexUpdate(0); this.selectionIndexUpdate(0);
return next(null); return next(null);
} },
], ],
err => { err => {
if(err) { if (err) {
this.client.log.error( { error : err.message }, 'Failed loading message area list'); this.client.log.error(
{ error: err.message },
'Failed loading message area list'
);
} }
return cb(err); return cb(err);
} }
@ -101,27 +117,33 @@ exports.getModule = class MessageAreaListModule extends MenuModule {
selectionIndexUpdate(idx) { selectionIndexUpdate(idx) {
const area = this.messageAreas[idx]; const area = this.messageAreas[idx];
if(!area) { if (!area) {
return; return;
} }
this.setViewText('areaList', MciViewIds.areaDesc, area.desc); this.setViewText('areaList', MciViewIds.areaDesc, area.desc);
this.updateCustomViewTextsWithFilter('areaList', MciViewIds.customRangeStart, area); this.updateCustomViewTextsWithFilter(
'areaList',
MciViewIds.customRangeStart,
area
);
} }
initList() { initList() {
let index = 1; let index = 1;
this.messageAreas = messageArea.getSortedAvailMessageAreasByConfTag( this.messageAreas = messageArea
this.client.user.properties[UserProps.MessageConfTag], .getSortedAvailMessageAreasByConfTag(
{ client : this.client } this.client.user.properties[UserProps.MessageConfTag],
).map(area => { { client: this.client }
return { )
index : index++, .map(area => {
areaTag : area.areaTag, return {
name : area.area.name, index: index++,
text : area.area.name, // standard areaTag: area.areaTag,
desc : area.area.desc, name: area.area.name,
hasArt : _.isString(area.area.art), text: area.area.name, // standard
}; desc: area.area.desc,
}); hasArt: _.isString(area.area.art),
};
});
} }
}; };

View File

@ -1,19 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
const persistMessage = require('./message_area.js').persistMessage; const persistMessage = require('./message_area.js').persistMessage;
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const { const { hasMessageConfAndAreaWrite } = require('./message_area.js');
hasMessageConfAndAreaWrite,
} = require('./message_area.js');
const async = require('async'); const async = require('async');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area Post', name: 'Message Area Post',
desc : 'Module for posting a new message to an area', desc: 'Module for posting a new message to an area',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule { exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
@ -25,8 +23,7 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
// we're posting, so always start with 'edit' mode // we're posting, so always start with 'edit' mode
this.editorMode = 'edit'; this.editorMode = 'edit';
this.menuMethods.editModeMenuSave = function(formData, extraArgs, cb) { this.menuMethods.editModeMenuSave = function (formData, extraArgs, cb) {
var msg; var msg;
async.series( async.series(
[ [
@ -41,15 +38,19 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
}, },
function updateStats(callback) { function updateStats(callback) {
self.updateUserAndSystemStats(callback); self.updateUserAndSystemStats(callback);
} },
], ],
function complete(err) { function complete(err) {
if(err) { if (err) {
// :TODO:... sooooo now what? // :TODO:... sooooo now what?
} else { } else {
// note: not logging 'from' here as it's part of client.log.xxxx() // note: not logging 'from' here as it's part of client.log.xxxx()
self.client.log.info( self.client.log.info(
{ to : msg.toUserName, subject : msg.subject, uuid : msg.messageUuid }, {
to: msg.toUserName,
subject: msg.subject,
uuid: msg.messageUuid,
},
'Message persisted' 'Message persisted'
); );
} }
@ -62,14 +63,13 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
enter() { enter() {
this.messageAreaTag = this.messageAreaTag =
this.messageAreaTag || this.messageAreaTag || this.client.user.getProperty(UserProps.MessageAreaTag);
this.client.user.getProperty(UserProps.MessageAreaTag);
super.enter(); super.enter();
} }
initSequence() { initSequence() {
if(!hasMessageConfAndAreaWrite(this.client, this.messageAreaTag)) { if (!hasMessageConfAndAreaWrite(this.client, this.messageAreaTag)) {
const noAcsMenu = const noAcsMenu =
this.menuConfig.config.messageBasePostMessageNoAccess || this.menuConfig.config.messageBasePostMessageNoAccess ||
'messageBasePostMessageNoAccess'; 'messageBasePostMessageNoAccess';
@ -82,4 +82,4 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
super.initSequence(); super.initSequence();
} }
}; };

View File

@ -1,14 +1,14 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; var FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
exports.getModule = AreaReplyFSEModule; exports.getModule = AreaReplyFSEModule;
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area Reply', name: 'Message Area Reply',
desc : 'Module for replying to an area message', desc: 'Module for replying to an area message',
author : 'NuSkooler', author: 'NuSkooler',
}; };
function AreaReplyFSEModule(options) { function AreaReplyFSEModule(options) {

View File

@ -2,36 +2,36 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule; const FullScreenEditorModule = require('./fse.js').FullScreenEditorModule;
const Message = require('./message.js'); const Message = require('./message.js');
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Area View', name: 'Message Area View',
desc : 'Module for viewing an area message', desc: 'Module for viewing an area message',
author : 'NuSkooler', author: 'NuSkooler',
}; };
exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule { exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
constructor(options) { constructor(options) {
super(options); super(options);
this.editorType = 'area'; this.editorType = 'area';
this.editorMode = 'view'; this.editorMode = 'view';
if(_.isObject(options.extraArgs)) { if (_.isObject(options.extraArgs)) {
this.messageList = options.extraArgs.messageList; this.messageList = options.extraArgs.messageList;
this.messageIndex = options.extraArgs.messageIndex; this.messageIndex = options.extraArgs.messageIndex;
this.lastMessageNextExit = options.extraArgs.lastMessageNextExit; this.lastMessageNextExit = options.extraArgs.lastMessageNextExit;
} }
this.messageList = this.messageList || []; this.messageList = this.messageList || [];
this.messageIndex = this.messageIndex || 0; this.messageIndex = this.messageIndex || 0;
this.messageTotal = this.messageList.length; this.messageTotal = this.messageList.length;
if(this.messageList.length > 0) { if (this.messageList.length > 0) {
this.messageAreaTag = this.messageList[this.messageIndex].areaTag; this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
} }
@ -39,18 +39,21 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
// assign *additional* menuMethods // assign *additional* menuMethods
Object.assign(this.menuMethods, { Object.assign(this.menuMethods, {
nextMessage : (formData, extraArgs, cb) => { nextMessage: (formData, extraArgs, cb) => {
if(self.messageIndex + 1 < self.messageList.length) { if (self.messageIndex + 1 < self.messageList.length) {
self.messageIndex++; self.messageIndex++;
this.messageAreaTag = this.messageList[this.messageIndex].areaTag; this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); return self.loadMessageByUuid(
self.messageList[self.messageIndex].messageUuid,
cb
);
} }
// auto-exit if no more to go? // auto-exit if no more to go?
if(self.lastMessageNextExit) { if (self.lastMessageNextExit) {
self.lastMessageReached = true; self.lastMessageReached = true;
return self.prevMenu(cb); return self.prevMenu(cb);
} }
@ -58,28 +61,39 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
return cb(null); return cb(null);
}, },
prevMessage : (formData, extraArgs, cb) => { prevMessage: (formData, extraArgs, cb) => {
if(self.messageIndex > 0) { if (self.messageIndex > 0) {
self.messageIndex--; self.messageIndex--;
this.messageAreaTag = this.messageList[this.messageIndex].areaTag; this.messageAreaTag = this.messageList[this.messageIndex].areaTag;
this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with this.tempMessageConfAndAreaSwitch(this.messageAreaTag, false); // false=don't record prev; we want what we entered the module with
return self.loadMessageByUuid(self.messageList[self.messageIndex].messageUuid, cb); return self.loadMessageByUuid(
self.messageList[self.messageIndex].messageUuid,
cb
);
} }
return cb(null); return cb(null);
}, },
movementKeyPressed : (formData, extraArgs, cb) => { movementKeyPressed: (formData, extraArgs, cb) => {
const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic # const bodyView = self.viewControllers.body.getView(1); // :TODO: use const here vs magic #
// :TODO: Create methods for up/down vs using keyPressXXXXX // :TODO: Create methods for up/down vs using keyPressXXXXX
switch(formData.key.name) { switch (formData.key.name) {
case 'down arrow' : bodyView.scrollDocumentUp(); break; case 'down arrow':
case 'up arrow' : bodyView.scrollDocumentDown(); break; bodyView.scrollDocumentUp();
case 'page up' : bodyView.keyPressPageUp(); break; break;
case 'page down' : bodyView.keyPressPageDown(); break; case 'up arrow':
bodyView.scrollDocumentDown();
break;
case 'page up':
bodyView.keyPressPageUp();
break;
case 'page down':
bodyView.keyPressPageDown();
break;
} }
// :TODO: need to stop down/page down if doing so would push the last // :TODO: need to stop down/page down if doing so would push the last
@ -88,13 +102,13 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
return cb(null); return cb(null);
}, },
replyMessage : (formData, extraArgs, cb) => { replyMessage: (formData, extraArgs, cb) => {
if(_.isString(extraArgs.menu)) { if (_.isString(extraArgs.menu)) {
const modOpts = { const modOpts = {
extraArgs : { extraArgs: {
messageAreaTag : self.messageAreaTag, messageAreaTag: self.messageAreaTag,
replyToMessage : self.message, replyToMessage: self.message,
} },
}; };
return self.gotoMenu(extraArgs.menu, modOpts, cb); return self.gotoMenu(extraArgs.menu, modOpts, cb);
@ -108,10 +122,10 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
loadMessageByUuid(uuid, cb) { loadMessageByUuid(uuid, cb) {
const msg = new Message(); const msg = new Message();
msg.load( { uuid : uuid, user : this.client.user }, () => { msg.load({ uuid: uuid, user: this.client.user }, () => {
this.setMessage(msg); this.setMessage(msg);
if(cb) { if (cb) {
return cb(null); return cb(null);
} }
}); });
@ -123,22 +137,22 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
getSaveState() { getSaveState() {
return { return {
messageList : this.messageList, messageList: this.messageList,
messageIndex : this.messageIndex, messageIndex: this.messageIndex,
messageTotal : this.messageList.length, messageTotal: this.messageList.length,
}; };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
this.messageList = savedState.messageList; this.messageList = savedState.messageList;
this.messageIndex = savedState.messageIndex; this.messageIndex = savedState.messageIndex;
this.messageTotal = savedState.messageTotal; this.messageTotal = savedState.messageTotal;
} }
getMenuResult() { getMenuResult() {
return { return {
messageIndex : this.messageIndex, messageIndex: this.messageIndex,
lastMessageReached : this.lastMessageReached, lastMessageReached: this.lastMessageReached,
}; };
} }
}; };

View File

@ -2,24 +2,24 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const { MenuModule } = require('./menu_module.js'); const { MenuModule } = require('./menu_module.js');
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const { Errors } = require('./enig_error.js'); const { Errors } = require('./enig_error.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message Conference List', name: 'Message Conference List',
desc : 'Module for listing / choosing message conferences', desc: 'Module for listing / choosing message conferences',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const MciViewIds = { const MciViewIds = {
confList : 1, confList: 1,
confDesc : 2, // description updated @ index update confDesc: 2, // description updated @ index update
customRangeStart : 10, // updated @ index update customRangeStart: 10, // updated @ index update
}; };
exports.getModule = class MessageConfListModule extends MenuModule { exports.getModule = class MessageConfListModule extends MenuModule {
@ -29,51 +29,68 @@ exports.getModule = class MessageConfListModule extends MenuModule {
this.initList(); this.initList();
this.menuMethods = { this.menuMethods = {
changeConference : (formData, extraArgs, cb) => { changeConference: (formData, extraArgs, cb) => {
if(1 === formData.submitId) { if (1 === formData.submitId) {
const conf = this.messageConfs[formData.value.conf]; const conf = this.messageConfs[formData.value.conf];
messageArea.changeMessageConference(this.client, conf.confTag, err => { messageArea.changeMessageConference(
if(err) { this.client,
this.client.term.pipeWrite(`\n|00Cannot change conference: ${err.message}\n`); conf.confTag,
return this.prevMenuOnTimeout(1000, cb); err => {
if (err) {
this.client.term.pipeWrite(
`\n|00Cannot change conference: ${err.message}\n`
);
return this.prevMenuOnTimeout(1000, cb);
}
if (conf.hasArt) {
const menuOpts = {
extraArgs: {
confTag: conf.confTag,
},
menuFlags: ['popParent', 'noHistory'],
};
return this.gotoMenu(
this.menuConfig.config.changeConfPreArtMenu ||
'changeMessageConfPreArt',
menuOpts,
cb
);
}
return this.prevMenu(cb);
} }
);
if(conf.hasArt) {
const menuOpts = {
extraArgs : {
confTag : conf.confTag,
},
menuFlags : [ 'popParent', 'noHistory' ]
};
return this.gotoMenu(this.menuConfig.config.changeConfPreArtMenu || 'changeMessageConfPreArt', menuOpts, cb);
}
return this.prevMenu(cb);
});
} else { } else {
return cb(null); return cb(null);
} }
} },
}; };
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
async.series( async.series(
[ [
(next) => { next => {
return this.prepViewController('confList', 0, mciData.menu, next); return this.prepViewController('confList', 0, mciData.menu, next);
}, },
(next) => { next => {
const confListView = this.viewControllers.confList.getView(MciViewIds.confList); const confListView = this.viewControllers.confList.getView(
if(!confListView) { MciViewIds.confList
return next(Errors.MissingMci(`Missing conf list MCI ${MciViewIds.confList}`)); );
if (!confListView) {
return next(
Errors.MissingMci(
`Missing conf list MCI ${MciViewIds.confList}`
)
);
} }
confListView.on('index update', idx => { confListView.on('index update', idx => {
@ -84,11 +101,14 @@ exports.getModule = class MessageConfListModule extends MenuModule {
confListView.redraw(); confListView.redraw();
this.selectionIndexUpdate(0); this.selectionIndexUpdate(0);
return next(null); return next(null);
} },
], ],
err => { err => {
if(err) { if (err) {
this.client.log.error( { error : err.message }, 'Failed loading message conference list'); this.client.log.error(
{ error: err.message },
'Failed loading message conference list'
);
} }
} }
); );
@ -97,26 +117,31 @@ exports.getModule = class MessageConfListModule extends MenuModule {
selectionIndexUpdate(idx) { selectionIndexUpdate(idx) {
const conf = this.messageConfs[idx]; const conf = this.messageConfs[idx];
if(!conf) { if (!conf) {
return; return;
} }
this.setViewText('confList', MciViewIds.confDesc, conf.desc); this.setViewText('confList', MciViewIds.confDesc, conf.desc);
this.updateCustomViewTextsWithFilter('confList', MciViewIds.customRangeStart, conf); this.updateCustomViewTextsWithFilter(
'confList',
MciViewIds.customRangeStart,
conf
);
} }
initList() initList() {
{
let index = 1; let index = 1;
this.messageConfs = messageArea.getSortedAvailMessageConferences(this.client).map(conf => { this.messageConfs = messageArea
return { .getSortedAvailMessageConferences(this.client)
index : index++, .map(conf => {
confTag : conf.confTag, return {
name : conf.conf.name, index: index++,
text : conf.conf.name, confTag: conf.confTag,
desc : conf.conf.desc, name: conf.conf.name,
areaCount : Object.keys(conf.conf.areas || {}).length, text: conf.conf.name,
hasArt : _.isString(conf.conf.art), desc: conf.conf.desc,
}; areaCount: Object.keys(conf.conf.areas || {}).length,
}); hasArt: _.isString(conf.conf.art),
};
});
} }
}; };

View File

@ -2,18 +2,19 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const MenuModule = require('./menu_module.js').MenuModule; const MenuModule = require('./menu_module.js').MenuModule;
const ViewController = require('./view_controller.js').ViewController; const ViewController = require('./view_controller.js').ViewController;
const messageArea = require('./message_area.js'); const messageArea = require('./message_area.js');
const MessageAreaConfTempSwitcher = require('./mod_mixins.js').MessageAreaConfTempSwitcher; const MessageAreaConfTempSwitcher =
const Errors = require('./enig_error.js').Errors; require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const Message = require('./message.js'); const Errors = require('./enig_error.js').Errors;
const UserProps = require('./user_property.js'); const Message = require('./message.js');
const UserProps = require('./user_property.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
/* /*
Available itemFormat/focusItemFormat members for |msgList| Available itemFormat/focusItemFormat members for |msgList|
@ -26,54 +27,71 @@ const moment = require('moment');
newIndicator : New mark/indicator (config.newIndicator) newIndicator : New mark/indicator (config.newIndicator)
*/ */
exports.moduleInfo = { exports.moduleInfo = {
name : 'Message List', name: 'Message List',
desc : 'Module for listing/browsing available messages', desc: 'Module for listing/browsing available messages',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const FormIds = { const FormIds = {
allViews : 0, allViews: 0,
delPrompt : 1, delPrompt: 1,
}; };
const MciViewIds = { const MciViewIds = {
allViews : { allViews: {
msgList : 1, // VM1 - see above msgList: 1, // VM1 - see above
delPromptXy : 2, // %XY2, e.g: delete confirmation delPromptXy: 2, // %XY2, e.g: delete confirmation
customRangeStart : 10, // Everything |msgList| has plus { msgNumSelected, msgNumTotal } customRangeStart: 10, // Everything |msgList| has plus { msgNumSelected, msgNumTotal }
}, },
delPrompt: { delPrompt: {
prompt : 1, prompt: 1,
} },
}; };
exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(MenuModule) { exports.getModule = class MessageListModule extends (
MessageAreaConfTempSwitcher(MenuModule)
) {
constructor(options) { constructor(options) {
super(options); super(options);
// :TODO: consider this pattern in base MenuModule - clean up code all over // :TODO: consider this pattern in base MenuModule - clean up code all over
this.config = Object.assign({}, _.get(options, 'menuConfig.config'), options.extraArgs); this.config = Object.assign(
{},
_.get(options, 'menuConfig.config'),
options.extraArgs
);
this.lastMessageReachedExit = _.get(options, 'lastMenuResult.lastMessageReached', false); this.lastMessageReachedExit = _.get(
options,
'lastMenuResult.lastMessageReached',
false
);
this.menuMethods = { this.menuMethods = {
selectMessage : (formData, extraArgs, cb) => { selectMessage: (formData, extraArgs, cb) => {
if(MciViewIds.allViews.msgList === formData.submitId) { if (MciViewIds.allViews.msgList === formData.submitId) {
// 'messageIndex' or older deprecated 'message' member // 'messageIndex' or older deprecated 'message' member
this.initialFocusIndex = _.get(formData, 'value.messageIndex', formData.value.message); this.initialFocusIndex = _.get(
formData,
'value.messageIndex',
formData.value.message
);
const modOpts = { const modOpts = {
extraArgs : { extraArgs: {
messageAreaTag : this.getSelectedAreaTag(this.initialFocusIndex), messageAreaTag: this.getSelectedAreaTag(
messageList : this.config.messageList, this.initialFocusIndex
messageIndex : this.initialFocusIndex, ),
lastMessageNextExit : true, messageList: this.config.messageList,
} messageIndex: this.initialFocusIndex,
lastMessageNextExit: true,
},
}; };
if(_.isBoolean(this.config.noUpdateLastReadId)) { if (_.isBoolean(this.config.noUpdateLastReadId)) {
modOpts.extraArgs.noUpdateLastReadId = this.config.noUpdateLastReadId; modOpts.extraArgs.noUpdateLastReadId =
this.config.noUpdateLastReadId;
} }
// //
@ -81,72 +99,98 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
// due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189 // due to the size of |messageList|. See https://github.com/trentm/node-bunyan/issues/189
// //
const self = this; const self = this;
modOpts.extraArgs.toJSON = function() { modOpts.extraArgs.toJSON = function () {
const logMsgList = (self.config.messageList.length <= 4) ? const logMsgList =
self.config.messageList : self.config.messageList.length <= 4
self.config.messageList.slice(0, 2).concat(self.config.messageList.slice(-2)); ? self.config.messageList
: self.config.messageList
.slice(0, 2)
.concat(self.config.messageList.slice(-2));
return { return {
// note |this| is scope of toJSON()! // note |this| is scope of toJSON()!
messageAreaTag : this.messageAreaTag, messageAreaTag: this.messageAreaTag,
apprevMessageList : logMsgList, apprevMessageList: logMsgList,
messageCount : this.messageList.length, messageCount: this.messageList.length,
messageIndex : this.messageIndex, messageIndex: this.messageIndex,
}; };
}; };
return this.gotoMenu(this.config.menuViewPost || 'messageAreaViewPost', modOpts, cb); return this.gotoMenu(
this.config.menuViewPost || 'messageAreaViewPost',
modOpts,
cb
);
} else { } else {
return cb(null); return cb(null);
} }
}, },
fullExit : (formData, extraArgs, cb) => { fullExit: (formData, extraArgs, cb) => {
this.menuResult = { fullExit : true }; this.menuResult = { fullExit: true };
return this.prevMenu(cb); return this.prevMenu(cb);
}, },
deleteSelected : (formData, extraArgs, cb) => { deleteSelected: (formData, extraArgs, cb) => {
if(MciViewIds.allViews.msgList != formData.submitId) { if (MciViewIds.allViews.msgList != formData.submitId) {
return cb(null); return cb(null);
} }
// newer 'messageIndex' or older deprecated value // newer 'messageIndex' or older deprecated value
const messageIndex = _.get(formData, 'value.messageIndex', formData.value.message); const messageIndex = _.get(
formData,
'value.messageIndex',
formData.value.message
);
return this.promptDeleteMessageConfirm(messageIndex, cb); return this.promptDeleteMessageConfirm(messageIndex, cb);
}, },
deleteMessageYes : (formData, extraArgs, cb) => { deleteMessageYes: (formData, extraArgs, cb) => {
const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); const msgListView = this.viewControllers.allViews.getView(
MciViewIds.allViews.msgList
);
this.enableMessageListIndexUpdates(msgListView); this.enableMessageListIndexUpdates(msgListView);
if(this.selectedMessageForDelete) { if (this.selectedMessageForDelete) {
this.selectedMessageForDelete.deleteMessage(this.client.user, err => { this.selectedMessageForDelete.deleteMessage(this.client.user, err => {
if(err) { if (err) {
this.client.log.error(`Failed to delete message: ${this.selectedMessageForDelete.messageUuid}`); this.client.log.error(
`Failed to delete message: ${this.selectedMessageForDelete.messageUuid}`
);
} else { } else {
this.client.log.info(`User deleted message: ${this.selectedMessageForDelete.messageUuid}`); this.client.log.info(
this.config.messageList.splice(msgListView.focusedItemIndex, 1); `User deleted message: ${this.selectedMessageForDelete.messageUuid}`
this.updateMessageNumbersAfterDelete(msgListView.focusedItemIndex); );
this.config.messageList.splice(
msgListView.focusedItemIndex,
1
);
this.updateMessageNumbersAfterDelete(
msgListView.focusedItemIndex
);
msgListView.setItems(this.config.messageList); msgListView.setItems(this.config.messageList);
} }
this.selectedMessageForDelete = null; this.selectedMessageForDelete = null;
msgListView.redraw(); msgListView.redraw();
this.populateCustomLabelsForSelected(msgListView.focusedItemIndex); this.populateCustomLabelsForSelected(
msgListView.focusedItemIndex
);
return cb(null); return cb(null);
}); });
} else { } else {
return cb(null); return cb(null);
} }
}, },
deleteMessageNo : (formData, extraArgs, cb) => { deleteMessageNo: (formData, extraArgs, cb) => {
const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); const msgListView = this.viewControllers.allViews.getView(
MciViewIds.allViews.msgList
);
this.enableMessageListIndexUpdates(msgListView); this.enableMessageListIndexUpdates(msgListView);
return cb(null); return cb(null);
}, },
markAllRead : (formData, extraArgs, cb) => { markAllRead: (formData, extraArgs, cb) => {
if(this.config.noUpdateLastReadId) { if (this.config.noUpdateLastReadId) {
return cb(null); return cb(null);
} }
return this.markAllMessagesAsRead(cb); return this.markAllMessagesAsRead(cb);
} },
}; };
} }
@ -155,7 +199,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
} }
enter() { enter() {
if(this.lastMessageReachedExit) { if (this.lastMessageReachedExit) {
return this.prevMenu(); return this.prevMenu();
} }
@ -167,11 +211,12 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
// each item is expected to contain |areaTag|, so we use that // each item is expected to contain |areaTag|, so we use that
// instead in those cases. // instead in those cases.
// //
if(!Array.isArray(this.config.messageList)) { if (!Array.isArray(this.config.messageList)) {
if(this.config.messageAreaTag) { if (this.config.messageAreaTag) {
this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag); this.tempMessageConfAndAreaSwitch(this.config.messageAreaTag);
} else { } else {
this.config.messageAreaTag = this.client.user.properties[UserProps.MessageAreaTag]; this.config.messageAreaTag =
this.client.user.properties[UserProps.MessageAreaTag];
} }
} }
} }
@ -184,30 +229,36 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
populateCustomLabelsForSelected(selectedIndex) { populateCustomLabelsForSelected(selectedIndex) {
const formatObj = Object.assign( const formatObj = Object.assign(
{ {
msgNumSelected : (selectedIndex + 1), msgNumSelected: selectedIndex + 1,
msgNumTotal : this.config.messageList.length, msgNumTotal: this.config.messageList.length,
}, },
this.config.messageList[selectedIndex] // plus, all the selected message props this.config.messageList[selectedIndex] // plus, all the selected message props
); );
return this.updateCustomViewTextsWithFilter('allViews', MciViewIds.allViews.customRangeStart, formatObj); return this.updateCustomViewTextsWithFilter(
'allViews',
MciViewIds.allViews.customRangeStart,
formatObj
);
} }
mciReady(mciData, cb) { mciReady(mciData, cb) {
super.mciReady(mciData, err => { super.mciReady(mciData, err => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const self = this; const self = this;
const vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = (self.viewControllers.allViews = new ViewController({
client: self.client,
}));
let configProvidedMessageList = false; let configProvidedMessageList = false;
async.series( async.series(
[ [
function loadFromConfig(callback) { function loadFromConfig(callback) {
const loadOpts = { const loadOpts = {
callingMenu : self, callingMenu: self,
mciMap : mciData.menu mciMap: mciData.menu,
}; };
return vc.loadFromMenuConfig(loadOpts, callback); return vc.loadFromMenuConfig(loadOpts, callback);
@ -216,49 +267,70 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
// //
// Config can supply messages else we'll need to populate the list now // Config can supply messages else we'll need to populate the list now
// //
if(_.isArray(self.config.messageList)) { if (_.isArray(self.config.messageList)) {
configProvidedMessageList = true; configProvidedMessageList = true;
return callback(0 === self.config.messageList.length ? new Error('No messages in area') : null); return callback(
0 === self.config.messageList.length
? new Error('No messages in area')
: null
);
} }
messageArea.getMessageListForArea(self.client, self.config.messageAreaTag, function msgs(err, msgList) { messageArea.getMessageListForArea(
if(!msgList || 0 === msgList.length) { self.client,
return callback(new Error('No messages in area')); self.config.messageAreaTag,
} function msgs(err, msgList) {
if (!msgList || 0 === msgList.length) {
return callback(new Error('No messages in area'));
}
self.config.messageList = msgList; self.config.messageList = msgList;
return callback(err); return callback(err);
}); }
);
}, },
function getLastReadMessageId(callback) { function getLastReadMessageId(callback) {
// messageList entries can contain |isNew| if they want to be considered new // messageList entries can contain |isNew| if they want to be considered new
if(configProvidedMessageList) { if (configProvidedMessageList) {
self.lastReadId = 0; self.lastReadId = 0;
return callback(null); return callback(null);
} }
messageArea.getMessageAreaLastReadId(self.client.user.userId, self.config.messageAreaTag, function lastRead(err, lastReadId) { messageArea.getMessageAreaLastReadId(
self.lastReadId = lastReadId || 0; self.client.user.userId,
return callback(null); // ignore any errors, e.g. missing value self.config.messageAreaTag,
}); function lastRead(err, lastReadId) {
self.lastReadId = lastReadId || 0;
return callback(null); // ignore any errors, e.g. missing value
}
);
}, },
function updateMessageListObjects(callback) { function updateMessageListObjects(callback) {
const dateTimeFormat = self.menuConfig.config.dateTimeFormat || self.client.currentTheme.helpers.getDateTimeFormat(); const dateTimeFormat =
const newIndicator = self.menuConfig.config.newIndicator || '*'; self.menuConfig.config.dateTimeFormat ||
const regIndicator = ' '.repeat(newIndicator.length); // fill with space to avoid draw issues self.client.currentTheme.helpers.getDateTimeFormat();
const newIndicator = self.menuConfig.config.newIndicator || '*';
const regIndicator = ' '.repeat(newIndicator.length); // fill with space to avoid draw issues
let msgNum = 1; let msgNum = 1;
self.config.messageList.forEach( (listItem, index) => { self.config.messageList.forEach((listItem, index) => {
listItem.msgNum = msgNum++; listItem.msgNum = msgNum++;
listItem.ts = moment(listItem.modTimestamp).format(dateTimeFormat); listItem.ts = moment(listItem.modTimestamp).format(
const isNew = _.isBoolean(listItem.isNew) ? listItem.isNew : listItem.messageId > self.lastReadId; dateTimeFormat
listItem.newIndicator = isNew ? newIndicator : regIndicator; );
const isNew = _.isBoolean(listItem.isNew)
? listItem.isNew
: listItem.messageId > self.lastReadId;
listItem.newIndicator = isNew ? newIndicator : regIndicator;
if(_.isUndefined(self.initialFocusIndex) && listItem.messageId > self.lastReadId) { if (
_.isUndefined(self.initialFocusIndex) &&
listItem.messageId > self.lastReadId
) {
self.initialFocusIndex = index; self.initialFocusIndex = index;
} }
listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text listItem.text = `${listItem.msgNum} - ${listItem.subject} from ${listItem.fromUserName}`; // default text
}); });
return callback(null); return callback(null);
}, },
@ -267,7 +339,7 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
msgListView.setItems(self.config.messageList); msgListView.setItems(self.config.messageList);
self.enableMessageListIndexUpdates(msgListView); self.enableMessageListIndexUpdates(msgListView);
if(self.initialFocusIndex > 0) { if (self.initialFocusIndex > 0) {
// note: causes redraw() // note: causes redraw()
msgListView.setFocusItemIndex(self.initialFocusIndex); msgListView.setFocusItemIndex(self.initialFocusIndex);
} else { } else {
@ -279,8 +351,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
}, },
], ],
err => { err => {
if(err) { if (err) {
self.client.log.error( { error : err.message }, 'Error loading message list'); self.client.log.error(
{ error: err.message },
'Error loading message list'
);
} }
return cb(err); return cb(err);
} }
@ -289,11 +364,11 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
} }
getSaveState() { getSaveState() {
return { initialFocusIndex : this.initialFocusIndex }; return { initialFocusIndex: this.initialFocusIndex };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
if(savedState) { if (savedState) {
this.initialFocusIndex = savedState.initialFocusIndex; this.initialFocusIndex = savedState.initialFocusIndex;
} }
} }
@ -303,12 +378,12 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
} }
enableMessageListIndexUpdates(msgListView) { enableMessageListIndexUpdates(msgListView) {
msgListView.on('index update', idx => this.populateCustomLabelsForSelected(idx) ); msgListView.on('index update', idx => this.populateCustomLabelsForSelected(idx));
} }
markAllMessagesAsRead(cb) { markAllMessagesAsRead(cb) {
if(!this.config.messageList || this.config.messageList.length === 0) { if (!this.config.messageList || this.config.messageList.length === 0) {
return cb(null); // nothing to do. return cb(null); // nothing to do.
} }
// //
@ -320,8 +395,8 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
const areaHighestIds = {}; const areaHighestIds = {};
this.config.messageList.forEach(msg => { this.config.messageList.forEach(msg => {
const highestId = areaHighestIds[msg.areaTag]; const highestId = areaHighestIds[msg.areaTag];
if(highestId) { if (highestId) {
if(msg.messageId > highestId) { if (msg.messageId > highestId) {
areaHighestIds[msg.areaTag] = msg.messageId; areaHighestIds[msg.areaTag] = msg.messageId;
} }
} else { } else {
@ -329,38 +404,52 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
} }
}); });
const regIndicator = ' '.repeat( (this.menuConfig.config.newIndicator || '*').length ); const regIndicator = ' '.repeat(
async.forEachOf(areaHighestIds, (highestId, areaTag, nextArea) => { (this.menuConfig.config.newIndicator || '*').length
messageArea.updateMessageAreaLastReadId( );
this.client.user.userId, async.forEachOf(
areaTag, areaHighestIds,
highestId, (highestId, areaTag, nextArea) => {
err => { messageArea.updateMessageAreaLastReadId(
if(err) { this.client.user.userId,
this.client.log.warn( { error : err.message }, 'Failed marking area as read'); areaTag,
} else { highestId,
// update newIndicator on messages err => {
this.config.messageList.forEach(msg => { if (err) {
if(areaTag === msg.areaTag) { this.client.log.warn(
msg.newIndicator = regIndicator; { error: err.message },
} 'Failed marking area as read'
}); );
const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); } else {
msgListView.setItems(this.config.messageList); // update newIndicator on messages
msgListView.redraw(); this.config.messageList.forEach(msg => {
this.client.log.info( { highestId, areaTag }, 'User marked area as read'); if (areaTag === msg.areaTag) {
msg.newIndicator = regIndicator;
}
});
const msgListView = this.viewControllers.allViews.getView(
MciViewIds.allViews.msgList
);
msgListView.setItems(this.config.messageList);
msgListView.redraw();
this.client.log.info(
{ highestId, areaTag },
'User marked area as read'
);
}
return nextArea(null); // always continue
} }
return nextArea(null); // always continue );
} },
); () => {
}, () => { return cb(null);
return cb(null); }
}); );
} }
updateMessageNumbersAfterDelete(startIndex) { updateMessageNumbersAfterDelete(startIndex) {
// all index -= 1 from this point on. // all index -= 1 from this point on.
for(let i = startIndex; i < this.config.messageList.length; ++i) { for (let i = startIndex; i < this.config.messageList.length; ++i) {
const msgItem = this.config.messageList[i]; const msgItem = this.config.messageList[i];
msgItem.msgNum -= 1; msgItem.msgNum -= 1;
msgItem.text = `${msgItem.msgNum} - ${msgItem.subject} from ${msgItem.fromUserName}`; // default text msgItem.text = `${msgItem.msgNum} - ${msgItem.subject} from ${msgItem.fromUserName}`; // default text
@ -369,21 +458,25 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
promptDeleteMessageConfirm(messageIndex, cb) { promptDeleteMessageConfirm(messageIndex, cb) {
const messageInfo = this.config.messageList[messageIndex]; const messageInfo = this.config.messageList[messageIndex];
if(!_.isObject(messageInfo)) { if (!_.isObject(messageInfo)) {
return cb(Errors.Invalid(`Invalid message index: ${messageIndex}`)); return cb(Errors.Invalid(`Invalid message index: ${messageIndex}`));
} }
// :TODO: create static userHasDeleteRights() that takes id || uuid that doesn't require full msg load // :TODO: create static userHasDeleteRights() that takes id || uuid that doesn't require full msg load
this.selectedMessageForDelete = new Message(); this.selectedMessageForDelete = new Message();
this.selectedMessageForDelete.load( { uuid : messageInfo.messageUuid }, err => { this.selectedMessageForDelete.load({ uuid: messageInfo.messageUuid }, err => {
if(err) { if (err) {
this.selectedMessageForDelete = null; this.selectedMessageForDelete = null;
return cb(err); return cb(err);
} }
if(!this.selectedMessageForDelete.userHasDeleteRights(this.client.user)) { if (!this.selectedMessageForDelete.userHasDeleteRights(this.client.user)) {
this.selectedMessageForDelete = null; this.selectedMessageForDelete = null;
return cb(Errors.AccessDenied('User does not have rights to delete this message')); return cb(
Errors.AccessDenied(
'User does not have rights to delete this message'
)
);
} }
// user has rights to delete -- prompt/confirm then proceed // user has rights to delete -- prompt/confirm then proceed
@ -392,25 +485,33 @@ exports.getModule = class MessageListModule extends MessageAreaConfTempSwitcher(
} }
promptConfirmDelete(cb) { promptConfirmDelete(cb) {
const promptXyView = this.viewControllers.allViews.getView(MciViewIds.allViews.delPromptXy); const promptXyView = this.viewControllers.allViews.getView(
if(!promptXyView) { MciViewIds.allViews.delPromptXy
return cb(Errors.MissingMci(`Missing prompt XY${MciViewIds.allViews.delPromptXy} MCI`)); );
if (!promptXyView) {
return cb(
Errors.MissingMci(
`Missing prompt XY${MciViewIds.allViews.delPromptXy} MCI`
)
);
} }
const promptOpts = { const promptOpts = {
clearAtSubmit : true, clearAtSubmit: true,
}; };
if(promptXyView.dimens.width) { if (promptXyView.dimens.width) {
promptOpts.clearWidth = promptXyView.dimens.width; promptOpts.clearWidth = promptXyView.dimens.width;
} }
return this.promptForInput( return this.promptForInput(
{ {
formName : 'delPrompt', formName: 'delPrompt',
formId : FormIds.delPrompt, formId: FormIds.delPrompt,
promptName : this.config.deleteMessageFromListPrompt || 'deleteMessageFromListPrompt', promptName:
prevFormName : 'allViews', this.config.deleteMessageFromListPrompt ||
position : promptXyView.position, 'deleteMessageFromListPrompt',
prevFormName: 'allViews',
position: promptXyView.position,
}, },
promptOpts, promptOpts,
err => { err => {

View File

@ -2,14 +2,14 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const loadModulesForCategory = require('./module_util.js').loadModulesForCategory; const loadModulesForCategory = require('./module_util.js').loadModulesForCategory;
// standard/deps // standard/deps
const async = require('async'); const async = require('async');
exports.startup = startup; exports.startup = startup;
exports.shutdown = shutdown; exports.shutdown = shutdown;
exports.recordMessage = recordMessage; exports.recordMessage = recordMessage;
let msgNetworkModules = []; let msgNetworkModules = [];
@ -17,19 +17,23 @@ function startup(cb) {
async.series( async.series(
[ [
function loadModules(callback) { function loadModules(callback) {
loadModulesForCategory('scannerTossers', (module, nextModule) => { loadModulesForCategory(
const modInst = new module.getModule(); 'scannerTossers',
(module, nextModule) => {
const modInst = new module.getModule();
modInst.startup(err => { modInst.startup(err => {
if(!err) { if (!err) {
msgNetworkModules.push(modInst); msgNetworkModules.push(modInst);
} }
}); });
return nextModule(null); return nextModule(null);
}, err => { },
callback(err); err => {
}); callback(err);
} }
);
},
], ],
cb cb
); );
@ -39,7 +43,7 @@ function shutdown(cb) {
async.each( async.each(
msgNetworkModules, msgNetworkModules,
(msgNetModule, next) => { (msgNetModule, next) => {
msgNetModule.shutdown( () => { msgNetModule.shutdown(() => {
return next(); return next();
}); });
}, },
@ -56,10 +60,14 @@ function recordMessage(message, cb) {
// a chance to do something with |message|. Any or all can // a chance to do something with |message|. Any or all can
// choose to ignore it. // choose to ignore it.
// //
async.each(msgNetworkModules, (modInst, next) => { async.each(
modInst.record(message); msgNetworkModules,
next(); (modInst, next) => {
}, err => { modInst.record(message);
cb(err); next();
}); },
} err => {
cb(err);
}
);
}

Some files were not shown because too many files have changed in this diff Show More