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,31 +3,20 @@
"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

@ -7,10 +7,7 @@ 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,
trackDoorRunEnd
} = require('./door_util.js');
const Log = require('./logger').log; const Log = require('./logger').log;
// deps // deps
@ -23,9 +20,9 @@ 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,13 +179,13 @@ 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) {
@ -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;
} }

View File

@ -7,23 +7,13 @@ 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
} = require('./database.js');
const UserInterruptQueue = require('./user_interrupt_queue.js'); const UserInterruptQueue = require('./user_interrupt_queue.js');
const { const { getConnectionByUserId } = require('./client_connections.js');
getConnectionByUserId
} = require('./client_connections.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const { const { Errors, ErrorReasons } = require('./enig_error.js');
Errors,
ErrorReasons
} = require('./enig_error.js');
const { getThemeArt } = require('./theme.js'); const { getThemeArt } = require('./theme.js');
const { const { pipeToAnsi, stripMciColorCodes } = require('./color_codes.js');
pipeToAnsi,
stripMciColorCodes
} = require('./color_codes.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const StatLog = require('./stat_log.js'); const StatLog = require('./stat_log.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
@ -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,7 +180,7 @@ 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
@ -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,29 +275,40 @@ 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,
userStatEvent => {
if (
[
UserProps.AchievementTotalCount,
UserProps.AchievementTotalPoints,
].includes(userStatEvent.statName)
) {
return; return;
} }
if(!_.isNumber(userStatEvent.statValue) && !_.isNumber(userStatEvent.statIncrementBy)) { if (
!_.isNumber(userStatEvent.statValue) &&
!_.isNumber(userStatEvent.statIncrementBy)
) {
return; return;
} }
// :TODO: Make this code generic - find + return factory created object // :TODO: Make this code generic - find + return factory created object
const achievementTags = Object.keys(_.pickBy( const achievementTags = Object.keys(
_.pickBy(
_.get(this.config.get(), 'achievements', {}), _.get(this.config.get(), 'achievements', {}),
achievement => { achievement => {
if(false === achievement.enabled) { if (false === achievement.enabled) {
return false; return false;
} }
const acceptedTypes = [ const acceptedTypes = [
@ -301,48 +316,78 @@ class Achievements {
Achievement.Types.UserStatInc, Achievement.Types.UserStatInc,
Achievement.Types.UserStatIncNewVal, Achievement.Types.UserStatIncNewVal,
]; ];
return acceptedTypes.includes(achievement.type) && achievement.statName === userStatEvent.statName; return (
acceptedTypes.includes(achievement.type) &&
achievement.statName === userStatEvent.statName
);
} }
)); )
);
if(0 === achievementTags.length) { if (0 === achievementTags.length) {
return; return;
} }
async.eachSeries(achievementTags, (achievementTag, nextAchievementTag) => { async.eachSeries(
const achievement = Achievement.factory(this.getAchievementByTag(achievementTag)); achievementTags,
if(!achievement) { (achievementTag, nextAchievementTag) => {
const achievement = Achievement.factory(
this.getAchievementByTag(achievementTag)
);
if (!achievement) {
return nextAchievementTag(null); return nextAchievementTag(null);
} }
const statValue = parseInt( const statValue = parseInt(
[ Achievement.Types.UserStatSet, Achievement.Types.UserStatIncNewVal ].includes(achievement.data.type) ? [
userStatEvent.statValue : Achievement.Types.UserStatSet,
userStatEvent.statIncrementBy Achievement.Types.UserStatIncNewVal,
].includes(achievement.data.type)
? userStatEvent.statValue
: userStatEvent.statIncrementBy
); );
if(isNaN(statValue)) { if (isNaN(statValue)) {
return nextAchievementTag(null); return nextAchievementTag(null);
} }
const [ details, matchField, matchValue ] = achievement.getMatchDetails(statValue); const [details, matchField, matchValue] =
if(!details) { achievement.getMatchDetails(statValue);
if (!details) {
return nextAchievementTag(null); return nextAchievementTag(null);
} }
async.waterfall( async.waterfall(
[ [
(callback) => { callback => {
this.loadAchievementHitCount(userStatEvent.user, achievementTag, matchField, (err, count) => { this.loadAchievementHitCount(
if(err) { userStatEvent.user,
achievementTag,
matchField,
(err, count) => {
if (err) {
return callback(err); return callback(err);
} }
return callback(count > 0 ? Errors.General('Achievement already acquired', ErrorReasons.TooMany) : null); return callback(
}); count > 0
? Errors.General(
'Achievement already acquired',
ErrorReasons.TooMany
)
: null
);
}
);
}, },
(callback) => { callback => {
const client = getConnectionByUserId(userStatEvent.user.userId); const client = getConnectionByUserId(
if(!client) { userStatEvent.user.userId
return callback(Errors.UnexpectedState('Failed to get client for user ID')); );
if (!client) {
return callback(
Errors.UnexpectedState(
'Failed to get client for user ID'
)
);
} }
const info = { const info = {
@ -352,21 +397,26 @@ class Achievements {
client, client,
matchField, // match - may be in odd format matchField, // match - may be in odd format
matchValue, // actual value matchValue, // actual value
achievedValue : matchField, // achievement value met achievedValue: matchField, // achievement value met
user : userStatEvent.user, user: userStatEvent.user,
timestamp : moment(), timestamp: moment(),
}; };
const achievementsInfo = [ info ]; const achievementsInfo = [info];
return callback(null, achievementsInfo, info); return callback(null, achievementsInfo, info);
}, },
(achievementsInfo, basicInfo, callback) => { (achievementsInfo, basicInfo, callback) => {
if(true !== achievement.data.retroactive) { if (true !== achievement.data.retroactive) {
return callback(null, achievementsInfo); return callback(null, achievementsInfo);
} }
const index = achievement.matchKeys.findIndex(v => v < matchField); const index = achievement.matchKeys.findIndex(
if(-1 === index || !Array.isArray(achievement.matchKeys)) { v => v < matchField
);
if (
-1 === index ||
!Array.isArray(achievement.matchKeys)
) {
return callback(null, achievementsInfo); return callback(null, achievementsInfo);
} }
@ -375,60 +425,78 @@ class Achievements {
// ^---- we met here // ^---- we met here
// ^------------^ retroactive range // ^------------^ retroactive range
// //
async.eachSeries(achievement.matchKeys.slice(index), (k, nextKey) => { async.eachSeries(
const [ det, fld, val ] = achievement.getMatchDetails(k); achievement.matchKeys.slice(index),
if(!det) { (k, nextKey) => {
const [det, fld, val] =
achievement.getMatchDetails(k);
if (!det) {
return nextKey(null); return nextKey(null);
} }
this.loadAchievementHitCount(userStatEvent.user, achievementTag, fld, (err, count) => { this.loadAchievementHitCount(
if(!err || count && 0 === count) { userStatEvent.user,
achievementsInfo.push(Object.assign( achievementTag,
{}, fld,
basicInfo, (err, count) => {
{ if (!err || (count && 0 === count)) {
details : det, achievementsInfo.push(
matchField : fld, Object.assign({}, basicInfo, {
achievedValue : fld, details: det,
matchValue : val, matchField: fld,
} achievedValue: fld,
)); matchValue: val,
})
);
} }
return nextKey(null); return nextKey(null);
}); }
);
}, },
() => { () => {
return callback(null, achievementsInfo); return callback(null, achievementsInfo);
}); }
);
}, },
(achievementsInfo, callback) => { (achievementsInfo, callback) => {
// reverse achievementsInfo so we display smallest > largest // reverse achievementsInfo so we display smallest > largest
achievementsInfo.reverse(); achievementsInfo.reverse();
async.eachSeries(achievementsInfo, (achInfo, nextAchInfo) => { async.eachSeries(
return this.recordAndDisplayAchievement(achInfo, err => { achievementsInfo,
(achInfo, nextAchInfo) => {
return this.recordAndDisplayAchievement(
achInfo,
err => {
return nextAchInfo(err); return nextAchInfo(err);
}); }
);
}, },
err => { err => {
return callback(err); return callback(err);
});
} }
);
},
], ],
err => { err => {
if(err && ErrorReasons.TooMany !== err.reasonCode) { if (err && ErrorReasons.TooMany !== err.reasonCode) {
Log.warn( { error : err.message, userStatEvent }, 'Error handling achievement for user stat event'); 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(
info.client.currentTheme,
'achievements.defaults',
{}
);
const textTypeSgr = themeDefaults[`${textType}SGR`] || defaultSgr; 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}`;
@ -483,7 +555,7 @@ class Achievements {
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,15 +579,17 @@ 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(
itemTypes,
(itemType, nextItemType) => {
async.waterfall( async.waterfall(
[ [
(callback) => { callback => {
getArt(`${itemType}Header`, headerArt => { getArt(`${itemType}Header`, headerArt => {
return callback(null, headerArt); return callback(null, headerArt);
}); });
@ -529,29 +603,45 @@ class Achievements {
const itemText = 'global' === itemType ? globalText : text; const itemText = 'global' === itemType ? globalText : text;
interruptItems[itemType] = { interruptItems[itemType] = {
title, title,
achievText : itemText, achievText: itemText,
text : `${title}\r\n${itemText}`, text: `${title}\r\n${itemText}`,
pause : true, pause: true,
}; };
if(headerArt || footerArt) { if (headerArt || footerArt) {
const themeDefaults = _.get(info.client.currentTheme, 'achievements.defaults', {}); const themeDefaults = _.get(
info.client.currentTheme,
'achievements.defaults',
{}
);
const defaultContentsFormat = '{title}\r\n{message}'; const defaultContentsFormat = '{title}\r\n{message}';
const contentsFormat = 'global' === itemType ? const contentsFormat =
themeDefaults.globalFormat || defaultContentsFormat : 'global' === itemType
themeDefaults.format || defaultContentsFormat; ? themeDefaults.globalFormat ||
defaultContentsFormat
: themeDefaults.format || defaultContentsFormat;
const formatObj = Object.assign(this.getFormatObject(info), { const formatObj = Object.assign(
title : this.getFormattedTextFor(info, 'title', ''), // ''=defaultSgr this.getFormatObject(info),
message : itemText, {
}); title: this.getFormattedTextFor(
info,
'title',
''
), // ''=defaultSgr
message: itemText,
}
);
const contents = pipeToAnsi(stringFormat(contentsFormat, formatObj)); const contents = pipeToAnsi(
stringFormat(contentsFormat, formatObj)
);
interruptItems[itemType].contents = interruptItems[itemType].contents = `${
`${headerArt || ''}\r\n${contents}\r\n${footerArt || ''}`; headerArt || ''
}\r\n${contents}\r\n${footerArt || ''}`;
} }
return callback(null); return callback(null);
} },
], ],
err => { err => {
return nextItemType(err); return nextItemType(err);
@ -560,14 +650,15 @@ class Achievements {
}, },
err => { err => {
return cb(err, interruptItems); 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)); const achievement = Achievement.factory(
if(!achievement) { achievementsInstance.getAchievementByTag(row.achievement_tag)
);
if (!achievement) {
return; return;
} }
const earnedInfo = { const earnedInfo = {
achievementTag : row.achievement_tag, achievementTag: row.achievement_tag,
type : achievement.data.type, type: achievement.data.type,
retroactive : achievement.data.retroactive, retroactive: achievement.data.retroactive,
title : row.title, title: row.title,
text : row.text, text: row.text,
points : row.points, points: row.points,
timestamp : moment(row.timestamp), timestamp: moment(row.timestamp),
}; };
switch(earnedInfo.type) { switch (earnedInfo.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]:
earnedInfo.statName = achievement.data.statName; earnedInfo.statName = achievement.data.statName;
break; break;
} }
return earnedInfo; return earnedInfo;
}).filter(a => a); // remove any empty records (ie: no achievement.hjson entry exists anymore). })
.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

@ -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,31 +76,34 @@ 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 {
@ -108,7 +111,7 @@ class ACS {
} }
}); });
if(matchCond) { if (matchCond) {
return matchCond[memberName]; return matchCond[memberName];
} }
} }

View File

@ -4,10 +4,12 @@
* http://pegjs.org/ * http://pegjs.org/
*/ */
"use strict"; 'use strict';
function peg$subclass(child, parent) { function peg$subclass(child, parent) {
function ctor() { this.constructor = child; } function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype; ctor.prototype = parent.prototype;
child.prototype = new ctor(); child.prototype = new ctor();
} }
@ -17,45 +19,48 @@ function peg$SyntaxError(message, expected, found, location) {
this.expected = expected; this.expected = expected;
this.found = found; this.found = found;
this.location = location; this.location = location;
this.name = "SyntaxError"; this.name = 'SyntaxError';
if (typeof Error.captureStackTrace === "function") { if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, peg$SyntaxError); Error.captureStackTrace(this, peg$SyntaxError);
} }
} }
peg$subclass(peg$SyntaxError, Error); peg$subclass(peg$SyntaxError, Error);
peg$SyntaxError.buildMessage = function(expected, found) { peg$SyntaxError.buildMessage = function (expected, found) {
var DESCRIBE_EXPECTATION_FNS = { var DESCRIBE_EXPECTATION_FNS = {
literal: function(expectation) { literal: function (expectation) {
return "\"" + literalEscape(expectation.text) + "\""; return '"' + literalEscape(expectation.text) + '"';
}, },
"class": function(expectation) { class: function (expectation) {
var escapedParts = "", var escapedParts = '',
i; i;
for (i = 0; i < expectation.parts.length; i++) { for (i = 0; i < expectation.parts.length; i++) {
escapedParts += expectation.parts[i] instanceof Array escapedParts +=
? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1]) expectation.parts[i] instanceof Array
? classEscape(expectation.parts[i][0]) +
'-' +
classEscape(expectation.parts[i][1])
: classEscape(expectation.parts[i]); : classEscape(expectation.parts[i]);
} }
return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]"; return '[' + (expectation.inverted ? '^' : '') + escapedParts + ']';
}, },
any: function(expectation) { any: function (expectation) {
return "any character"; return 'any character';
}, },
end: function(expectation) { end: function (expectation) {
return "end of input"; return 'end of input';
}, },
other: function(expectation) { other: function (expectation) {
return expectation.description; return expectation.description;
} },
}; };
function hex(ch) { function hex(ch) {
@ -70,8 +75,12 @@ peg$SyntaxError.buildMessage = function(expected, found) {
.replace(/\t/g, '\\t') .replace(/\t/g, '\\t')
.replace(/\n/g, '\\n') .replace(/\n/g, '\\n')
.replace(/\r/g, '\\r') .replace(/\r/g, '\\r')
.replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) .replace(/[\x00-\x0F]/g, function (ch) {
.replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); return '\\x0' + hex(ch);
})
.replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) {
return '\\x' + hex(ch);
});
} }
function classEscape(s) { function classEscape(s) {
@ -84,8 +93,12 @@ peg$SyntaxError.buildMessage = function(expected, found) {
.replace(/\t/g, '\\t') .replace(/\t/g, '\\t')
.replace(/\n/g, '\\n') .replace(/\n/g, '\\n')
.replace(/\r/g, '\\r') .replace(/\r/g, '\\r')
.replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) .replace(/[\x00-\x0F]/g, function (ch) {
.replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); return '\\x0' + hex(ch);
})
.replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) {
return '\\x' + hex(ch);
});
} }
function describeExpectation(expectation) { function describeExpectation(expectation) {
@ -94,7 +107,8 @@ peg$SyntaxError.buildMessage = function(expected, found) {
function describeExpected(expected) { function describeExpected(expected) {
var descriptions = new Array(expected.length), var descriptions = new Array(expected.length),
i, j; i,
j;
for (i = 0; i < expected.length; i++) { for (i = 0; i < expected.length; i++) {
descriptions[i] = describeExpectation(expected[i]); descriptions[i] = describeExpectation(expected[i]);
@ -117,78 +131,110 @@ peg$SyntaxError.buildMessage = function(expected, found) {
return descriptions[0]; return descriptions[0];
case 2: case 2:
return descriptions[0] + " or " + descriptions[1]; return descriptions[0] + ' or ' + descriptions[1];
default: default:
return descriptions.slice(0, -1).join(", ") return (
+ ", or " descriptions.slice(0, -1).join(', ') +
+ descriptions[descriptions.length - 1]; ', or ' +
descriptions[descriptions.length - 1]
);
} }
} }
function describeFound(found) { function describeFound(found) {
return found ? "\"" + literalEscape(found) + "\"" : "end of input"; return found ? '"' + literalEscape(found) + '"' : 'end of input';
} }
return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; return (
'Expected ' +
describeExpected(expected) +
' but ' +
describeFound(found) +
' found.'
);
}; };
function peg$parse(input, options) { function peg$parse(input, options) {
options = options !== void 0 ? options : {}; options = options !== void 0 ? options : {};
var peg$FAILED = {}, var peg$FAILED = {},
peg$startRuleFunctions = { start: peg$parsestart }, peg$startRuleFunctions = { start: peg$parsestart },
peg$startRuleFunction = peg$parsestart, peg$startRuleFunction = peg$parsestart,
peg$c0 = '|',
peg$c0 = "|", peg$c1 = peg$literalExpectation('|', false),
peg$c1 = peg$literalExpectation("|", false), peg$c2 = '&',
peg$c2 = "&", peg$c3 = peg$literalExpectation('&', false),
peg$c3 = peg$literalExpectation("&", false), peg$c4 = '!',
peg$c4 = "!", peg$c5 = peg$literalExpectation('!', false),
peg$c5 = peg$literalExpectation("!", false), peg$c6 = '(',
peg$c6 = "(", peg$c7 = peg$literalExpectation('(', false),
peg$c7 = peg$literalExpectation("(", false), peg$c8 = ')',
peg$c8 = ")", peg$c9 = peg$literalExpectation(')', false),
peg$c9 = peg$literalExpectation(")", false), peg$c10 = function (left, right) {
peg$c10 = function(left, right) { return left || right; }, return left || right;
peg$c11 = function(left, right) { return left && right; }, },
peg$c12 = function(value) { return !value; }, peg$c11 = function (left, right) {
peg$c13 = function(value) { return value; }, return left && right;
peg$c14 = ",", },
peg$c15 = peg$literalExpectation(",", false), peg$c12 = function (value) {
peg$c16 = " ", return !value;
peg$c17 = peg$literalExpectation(" ", false), },
peg$c18 = "[", peg$c13 = function (value) {
peg$c19 = peg$literalExpectation("[", false), return value;
peg$c20 = "]", },
peg$c21 = peg$literalExpectation("]", false), peg$c14 = ',',
peg$c22 = function(acs, a) { return checkAccess(acs, a); }, peg$c15 = peg$literalExpectation(',', false),
peg$c16 = ' ',
peg$c17 = peg$literalExpectation(' ', false),
peg$c18 = '[',
peg$c19 = peg$literalExpectation('[', false),
peg$c20 = ']',
peg$c21 = peg$literalExpectation(']', false),
peg$c22 = function (acs, a) {
return checkAccess(acs, a);
},
peg$c23 = /^[A-Z]/, peg$c23 = /^[A-Z]/,
peg$c24 = peg$classExpectation([["A", "Z"]], false, false), peg$c24 = peg$classExpectation([['A', 'Z']], false, false),
peg$c25 = function(c) { return c.join(''); }, peg$c25 = function (c) {
return c.join('');
},
peg$c26 = /^[A-Za-z0-9\-_+]/, peg$c26 = /^[A-Za-z0-9\-_+]/,
peg$c27 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "-", "_", "+"], false, false), peg$c27 = peg$classExpectation(
peg$c28 = function(a) { return a.join('') }, [['A', 'Z'], ['a', 'z'], ['0', '9'], '-', '_', '+'],
peg$c29 = function(v) { return v; }, false,
peg$c30 = function(start, last) { return start.concat(last); }, false
peg$c31 = function(l) { return l; }, ),
peg$c28 = function (a) {
return a.join('');
},
peg$c29 = function (v) {
return v;
},
peg$c30 = function (start, last) {
return start.concat(last);
},
peg$c31 = function (l) {
return l;
},
peg$c32 = /^[0-9]/, peg$c32 = /^[0-9]/,
peg$c33 = peg$classExpectation([["0", "9"]], false, false), peg$c33 = peg$classExpectation([['0', '9']], false, false),
peg$c34 = function(d) { return parseInt(d.join(''), 10); }, peg$c34 = function (d) {
return parseInt(d.join(''), 10);
},
peg$currPos = 0, peg$currPos = 0,
peg$savedPos = 0, peg$savedPos = 0,
peg$posDetailsCache = [{ line: 1, column: 1 }], peg$posDetailsCache = [{ line: 1, column: 1 }],
peg$maxFailPos = 0, peg$maxFailPos = 0,
peg$maxFailExpected = [], peg$maxFailExpected = [],
peg$silentFails = 0, peg$silentFails = 0,
peg$result; peg$result;
if ("startRule" in options) { if ('startRule' in options) {
if (!(options.startRule in peg$startRuleFunctions)) { if (!(options.startRule in peg$startRuleFunctions)) {
throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); throw new Error(
'Can\'t start parsing from rule "' + options.startRule + '".'
);
} }
peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
@ -203,7 +249,10 @@ function peg$parse(input, options) {
} }
function expected(description, location) { function expected(description, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) location =
location !== void 0
? location
: peg$computeLocation(peg$savedPos, peg$currPos);
throw peg$buildStructuredError( throw peg$buildStructuredError(
[peg$otherExpectation(description)], [peg$otherExpectation(description)],
@ -213,33 +262,42 @@ function peg$parse(input, options) {
} }
function error(message, location) { function error(message, location) {
location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) location =
location !== void 0
? location
: peg$computeLocation(peg$savedPos, peg$currPos);
throw peg$buildSimpleError(message, location); throw peg$buildSimpleError(message, location);
} }
function peg$literalExpectation(text, ignoreCase) { function peg$literalExpectation(text, ignoreCase) {
return { type: "literal", text: text, ignoreCase: ignoreCase }; return { type: 'literal', text: text, ignoreCase: ignoreCase };
} }
function peg$classExpectation(parts, inverted, ignoreCase) { function peg$classExpectation(parts, inverted, ignoreCase) {
return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; return {
type: 'class',
parts: parts,
inverted: inverted,
ignoreCase: ignoreCase,
};
} }
function peg$anyExpectation() { function peg$anyExpectation() {
return { type: "any" }; return { type: 'any' };
} }
function peg$endExpectation() { function peg$endExpectation() {
return { type: "end" }; return { type: 'end' };
} }
function peg$otherExpectation(description) { function peg$otherExpectation(description) {
return { type: "other", description: description }; return { type: 'other', description: description };
} }
function peg$computePosDetails(pos) { function peg$computePosDetails(pos) {
var details = peg$posDetailsCache[pos], p; var details = peg$posDetailsCache[pos],
p;
if (details) { if (details) {
return details; return details;
@ -252,7 +310,7 @@ function peg$parse(input, options) {
details = peg$posDetailsCache[p]; details = peg$posDetailsCache[p];
details = { details = {
line: details.line, line: details.line,
column: details.column column: details.column,
}; };
while (p < pos) { while (p < pos) {
@ -279,18 +337,20 @@ function peg$parse(input, options) {
start: { start: {
offset: startPos, offset: startPos,
line: startPosDetails.line, line: startPosDetails.line,
column: startPosDetails.column column: startPosDetails.column,
}, },
end: { end: {
offset: endPos, offset: endPos,
line: endPosDetails.line, line: endPosDetails.line,
column: endPosDetails.column column: endPosDetails.column,
} },
}; };
} }
function peg$fail(expected) { function peg$fail(expected) {
if (peg$currPos < peg$maxFailPos) { return; } if (peg$currPos < peg$maxFailPos) {
return;
}
if (peg$currPos > peg$maxFailPos) { if (peg$currPos > peg$maxFailPos) {
peg$maxFailPos = peg$currPos; peg$maxFailPos = peg$currPos;
@ -329,7 +389,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c1); } if (peg$silentFails === 0) {
peg$fail(peg$c1);
}
} }
return s0; return s0;
@ -343,7 +405,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c3); } if (peg$silentFails === 0) {
peg$fail(peg$c3);
}
} }
return s0; return s0;
@ -357,7 +421,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c5); } if (peg$silentFails === 0) {
peg$fail(peg$c5);
}
} }
return s0; return s0;
@ -371,7 +437,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c7); } if (peg$silentFails === 0) {
peg$fail(peg$c7);
}
} }
return s0; return s0;
@ -385,7 +453,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c9); } if (peg$silentFails === 0) {
peg$fail(peg$c9);
}
} }
return s0; return s0;
@ -524,7 +594,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c15); } if (peg$silentFails === 0) {
peg$fail(peg$c15);
}
} }
return s0; return s0;
@ -538,7 +610,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c17); } if (peg$silentFails === 0) {
peg$fail(peg$c17);
}
} }
return s0; return s0;
@ -565,7 +639,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c19); } if (peg$silentFails === 0) {
peg$fail(peg$c19);
}
} }
return s0; return s0;
@ -579,7 +655,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s0 = peg$FAILED; s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c21); } if (peg$silentFails === 0) {
peg$fail(peg$c21);
}
} }
return s0; return s0;
@ -618,7 +696,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c24); } if (peg$silentFails === 0) {
peg$fail(peg$c24);
}
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
if (peg$c23.test(input.charAt(peg$currPos))) { if (peg$c23.test(input.charAt(peg$currPos))) {
@ -626,7 +706,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s3 = peg$FAILED; s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c24); } if (peg$silentFails === 0) {
peg$fail(peg$c24);
}
} }
if (s3 !== peg$FAILED) { if (s3 !== peg$FAILED) {
s2 = [s2, s3]; s2 = [s2, s3];
@ -658,7 +740,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c27); } if (peg$silentFails === 0) {
peg$fail(peg$c27);
}
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) {
@ -668,7 +752,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c27); } if (peg$silentFails === 0) {
peg$fail(peg$c27);
}
} }
} }
} else { } else {
@ -804,7 +890,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c33); } if (peg$silentFails === 0) {
peg$fail(peg$c33);
}
} }
if (s2 !== peg$FAILED) { if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) {
@ -814,7 +902,9 @@ function peg$parse(input, options) {
peg$currPos++; peg$currPos++;
} else { } else {
s2 = peg$FAILED; s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$c33); } if (peg$silentFails === 0) {
peg$fail(peg$c33);
}
} }
} }
} else { } else {
@ -843,7 +933,6 @@ function peg$parse(input, options) {
return s0; return s0;
} }
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const User = require('./user.js'); const User = require('./user.js');
@ -857,223 +946,249 @@ function peg$parse(input, options) {
function checkAccess(acsCode, value) { function checkAccess(acsCode, value) {
try { try {
return { return {
LC : function isLocalConnection() { LC: function isLocalConnection() {
return client && client.isLocal(); return client && client.isLocal();
}, },
AG : function ageGreaterOrEqualThan() { AG: function ageGreaterOrEqualThan() {
return !isNaN(value) && user && user.getAge() >= value; return !isNaN(value) && user && user.getAge() >= value;
}, },
AS : function accountStatus() { AS: function accountStatus() {
if(!user) { if (!user) {
return false; return false;
} }
if(!Array.isArray(value)) { if (!Array.isArray(value)) {
value = [ value ]; value = [value];
} }
const userAccountStatus = user.getPropertyAsNumber(UserProps.AccountStatus); const userAccountStatus = user.getPropertyAsNumber(
UserProps.AccountStatus
);
return value.map(n => parseInt(n, 10)).includes(userAccountStatus); return value.map(n => parseInt(n, 10)).includes(userAccountStatus);
}, },
EC : function isEncoding() { EC: function isEncoding() {
const encoding = _.get(client, 'term.outputEncoding', '').toLowerCase(); const encoding = _.get(
switch(value) { client,
case 0 : return 'cp437' === encoding; 'term.outputEncoding',
case 1 : return 'utf-8' === encoding; ''
default : return false; ).toLowerCase();
} switch (value) {
}, case 0:
GM : function isOneOfGroups() { return 'cp437' === encoding;
if(!user) { case 1:
return 'utf-8' === encoding;
default:
return false; return false;
} }
if(!Array.isArray(value)) { },
GM: function isOneOfGroups() {
if (!user) {
return false;
}
if (!Array.isArray(value)) {
return false; return false;
} }
return value.some(groupName => user.isGroupMember(groupName)); return value.some(groupName => user.isGroupMember(groupName));
}, },
NN : function isNode() { NN: function isNode() {
if(!client) { if (!client) {
return false; return false;
} }
if(!Array.isArray(value)) { if (!Array.isArray(value)) {
value = [ value ]; value = [value];
} }
return value.map(n => parseInt(n, 10)).includes(client.node); return value.map(n => parseInt(n, 10)).includes(client.node);
}, },
NP : function numberOfPosts() { NP: function numberOfPosts() {
if(!user) { if (!user) {
return false; return false;
} }
const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
return !isNaN(value) && postCount >= value; return !isNaN(value) && postCount >= value;
}, },
NC : function numberOfCalls() { NC: function numberOfCalls() {
if(!user) { if (!user) {
return false; return false;
} }
const loginCount = user.getPropertyAsNumber(UserProps.LoginCount); const loginCount = user.getPropertyAsNumber(UserProps.LoginCount);
return !isNaN(value) && loginCount >= value; return !isNaN(value) && loginCount >= value;
}, },
AA : function accountAge() { AA: function accountAge() {
if(!user) { if (!user) {
return false; return false;
} }
const accountCreated = moment(user.getProperty(UserProps.AccountCreated)); const accountCreated = moment(
user.getProperty(UserProps.AccountCreated)
);
const now = moment(); const now = moment();
const daysOld = accountCreated.diff(moment(), 'days'); const daysOld = accountCreated.diff(moment(), 'days');
return !isNaN(value) && return (
!isNaN(value) &&
accountCreated.isValid() && accountCreated.isValid() &&
now.isAfter(accountCreated) && now.isAfter(accountCreated) &&
daysOld >= value; daysOld >= value
);
}, },
BU : function bytesUploaded() { BU: function bytesUploaded() {
if(!user) { if (!user) {
return false; return false;
} }
const bytesUp = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; const bytesUp =
user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
return !isNaN(value) && bytesUp >= value; return !isNaN(value) && bytesUp >= value;
}, },
UP : function uploads() { UP: function uploads() {
if(!user) { if (!user) {
return false; return false;
} }
const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; const uls = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
return !isNaN(value) && uls >= value; return !isNaN(value) && uls >= value;
}, },
BD : function bytesDownloaded() { BD: function bytesDownloaded() {
if(!user) { if (!user) {
return false; return false;
} }
const bytesDown = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; const bytesDown =
user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
return !isNaN(value) && bytesDown >= value; return !isNaN(value) && bytesDown >= value;
}, },
DL : function downloads() { DL: function downloads() {
if(!user) { if (!user) {
return false; return false;
} }
const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; const dls = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
return !isNaN(value) && dls >= value; return !isNaN(value) && dls >= value;
}, },
NR : function uploadDownloadRatioGreaterThan() { NR: function uploadDownloadRatioGreaterThan() {
if(!user) { if (!user) {
return false; return false;
} }
const ulCount = user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0; const ulCount =
const dlCount = user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0; user.getPropertyAsNumber(UserProps.FileUlTotalCount) || 0;
const dlCount =
user.getPropertyAsNumber(UserProps.FileDlTotalCount) || 0;
const ratio = ~~((ulCount / dlCount) * 100); const ratio = ~~((ulCount / dlCount) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },
KR : function uploadDownloadByteRatioGreaterThan() { KR: function uploadDownloadByteRatioGreaterThan() {
if(!user) { if (!user) {
return false; return false;
} }
const ulBytes = user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0; const ulBytes =
const dlBytes = user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0; user.getPropertyAsNumber(UserProps.FileUlTotalBytes) || 0;
const dlBytes =
user.getPropertyAsNumber(UserProps.FileDlTotalBytes) || 0;
const ratio = ~~((ulBytes / dlBytes) * 100); const ratio = ~~((ulBytes / dlBytes) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },
PC : function postCallRatio() { PC: function postCallRatio() {
if(!user) { if (!user) {
return false; return false;
} }
const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0; const postCount = user.getPropertyAsNumber(UserProps.PostCount) || 0;
const loginCount = user.getPropertyAsNumber(UserProps.LoginCount) || 0; const loginCount =
user.getPropertyAsNumber(UserProps.LoginCount) || 0;
const ratio = ~~((postCount / loginCount) * 100); const ratio = ~~((postCount / loginCount) * 100);
return !isNaN(value) && ratio >= value; return !isNaN(value) && ratio >= value;
}, },
SC : function isSecureConnection() { SC: function isSecureConnection() {
return _.get(client, 'session.isSecure', false); return _.get(client, 'session.isSecure', false);
}, },
AF : function currentAuthFactor() { AF: function currentAuthFactor() {
if(!user) { if (!user) {
return false; return false;
} }
return !isNaN(value) && user.authFactor >= value; return !isNaN(value) && user.authFactor >= value;
}, },
AR : function authFactorRequired() { AR: function authFactorRequired() {
if(!user) { if (!user) {
return false; return false;
} }
switch(value) { switch (value) {
case 1 : return true; case 1:
case 2 : return user.getProperty(UserProps.AuthFactor2OTP) ? true : false; return true;
default : return false; case 2:
return user.getProperty(UserProps.AuthFactor2OTP)
? true
: false;
default:
return false;
} }
}, },
ML : function minutesLeft() { ML: function minutesLeft() {
// :TODO: implement me! // :TODO: implement me!
return false; return false;
}, },
TH : function termHeight() { TH: function termHeight() {
return !isNaN(value) && _.get(client, 'term.termHeight', 0) >= value; return !isNaN(value) && _.get(client, 'term.termHeight', 0) >= value;
}, },
TM : function isOneOfThemes() { TM: function isOneOfThemes() {
if(!Array.isArray(value)) { if (!Array.isArray(value)) {
return false; return false;
} }
return value.includes(_.get(client, 'currentTheme.name')); return value.includes(_.get(client, 'currentTheme.name'));
}, },
TT : function isOneOfTermTypes() { TT: function isOneOfTermTypes() {
if(!Array.isArray(value)) { if (!Array.isArray(value)) {
return false; return false;
} }
return value.includes(_.get(client, 'term.termType')); return value.includes(_.get(client, 'term.termType'));
}, },
TW : function termWidth() { TW: function termWidth() {
return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value; return !isNaN(value) && _.get(client, 'term.termWidth', 0) >= value;
}, },
ID : function isUserId() { ID: function isUserId() {
if(!user) { if (!user) {
return false; return false;
} }
if(!Array.isArray(value)) { if (!Array.isArray(value)) {
value = [ value ]; value = [value];
} }
return value.map(n => parseInt(n, 10)).includes(user.userId); return value.map(n => parseInt(n, 10)).includes(user.userId);
}, },
WD : function isOneOfDayOfWeek() { WD: function isOneOfDayOfWeek() {
if(!Array.isArray(value)) { if (!Array.isArray(value)) {
value = [ value ]; value = [value];
} }
return value.map(n => parseInt(n, 10)).includes(new Date().getDay()); return value.map(n => parseInt(n, 10)).includes(new Date().getDay());
}, },
MM : function isMinutesPastMidnight() { MM: function isMinutesPastMidnight() {
const now = moment(); const now = moment();
const midnight = now.clone().startOf('day') const midnight = now.clone().startOf('day');
const minutesPastMidnight = now.diff(midnight, 'minutes'); const minutesPastMidnight = now.diff(midnight, 'minutes');
return !isNaN(value) && minutesPastMidnight >= value; return !isNaN(value) && minutesPastMidnight >= value;
}, },
AC : function achievementCount() { AC: function achievementCount() {
if(!user) { if (!user) {
return false; return false;
} }
const count = user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0; const count =
user.getPropertyAsNumber(UserProps.AchievementTotalCount) || 0;
return !isNan(value) && points >= value; return !isNan(value) && points >= value;
}, },
AP : function achievementPoints() { AP: function achievementPoints() {
if(!user) { if (!user) {
return false; return false;
} }
const points = user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0; const points =
user.getPropertyAsNumber(UserProps.AchievementTotalPoints) || 0;
return !isNan(value) && points >= value; return !isNan(value) && points >= value;
}, },
PV : function userPropValue() { PV: function userPropValue() {
if (!user || !Array.isArray(value) || value.length !== 2) { if (!user || !Array.isArray(value) || value.length !== 2) {
return false; return false;
} }
const [propName, propValue] = value; const [propName, propValue] = value;
const actualPropValue = user.getProperty(propName); const actualPropValue = user.getProperty(propName);
return actualPropValue === propValue; return actualPropValue === propValue;
} },
}[acsCode](value); }[acsCode](value);
} catch (e) { } catch (e) {
const logger = _.get(client, 'log', Log); const logger = _.get(client, 'log', Log);
logger.warn( { acsCode : acsCode, value : value }, 'Invalid ACS string!'); logger.warn({ acsCode: acsCode, value: value }, 'Invalid ACS string!');
return false; return false;
} }
} }
peg$result = peg$startRuleFunction(); peg$result = peg$startRuleFunction();
if (peg$result !== peg$FAILED && peg$currPos === input.length) { if (peg$result !== peg$FAILED && peg$currPos === input.length) {
@ -1095,5 +1210,5 @@ function peg$parse(input, options) {
module.exports = { module.exports = {
SyntaxError: peg$SyntaxError, SyntaxError: peg$SyntaxError,
parse: peg$parse parse: peg$parse,
}; };

View File

@ -24,26 +24,24 @@ function ANSIEscapeParser(options) {
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;
@ -54,14 +52,14 @@ function ANSIEscapeParser(options) {
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,14 +68,13 @@ 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);
}; };
@ -88,11 +85,11 @@ function ANSIEscapeParser(options) {
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,8 +113,8 @@ 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;
@ -138,7 +135,7 @@ 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;
@ -146,7 +143,7 @@ function ANSIEscapeParser(options) {
} }
const rem = text.slice(start); const rem = text.slice(start);
if(rem) { if (rem) {
self.emit('literal', rem); self.emit('literal', rem);
} }
} }
@ -164,15 +161,15 @@ function ANSIEscapeParser(options) {
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);
} }
@ -246,15 +247,15 @@ function ANSIEscapeParser(options) {
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));
} }
@ -266,27 +267,27 @@ function ANSIEscapeParser(options) {
//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,37 +344,37 @@ 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];
@ -383,29 +384,29 @@ function ANSIEscapeParser(options) {
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,35 +422,38 @@ 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;
} }
} }
@ -461,9 +465,9 @@ function ANSIEscapeParser(options) {
// :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

@ -4,16 +4,13 @@
// 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, '');
} }
@ -27,29 +24,35 @@ module.exports = function ansiPrep(input, options, cb) {
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;
} }
@ -62,17 +65,20 @@ 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,7 +92,7 @@ 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 {
@ -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:
// //
@ -167,19 +181,19 @@ 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
@ -189,8 +203,8 @@ module.exports = function ansiPrep(input, options, cb) {
} }
} }
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

@ -73,18 +73,18 @@ exports.vtxHyperlink = vtxHyperlink;
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);
} }
} }
@ -417,22 +417,22 @@ 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,7 +452,7 @@ function resetScreen() {
} }
function normal() { function normal() {
return sgr( [ 'normal', 'reset' ] ); return sgr(['normal', 'reset']);
} }
function goHome() { function goHome() {
@ -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,
115200: 11,
}[rate] || 0; }[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

@ -12,9 +12,9 @@ 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 {
@ -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,15 +63,22 @@ 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',
fwdPort,
self.config.host,
self.config.rloginPort,
(err, stream) => {
if (err) {
return sshClient.end(); return sshClient.end();
} }
@ -81,13 +90,17 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
// we need to filter I/O for escape/de-escaping zmodem and the like // we need to filter I/O for escape/de-escaping zmodem and the like
self.client.setTemporaryDirectDataHandler(data => { self.client.setTemporaryDirectDataHandler(data => {
const tmp = data.toString('binary').replace(/\xff{2}/g, '\xff'); // de-escape const tmp = data
.toString('binary')
.replace(/\xff{2}/g, '\xff'); // de-escape
stream.write(Buffer.from(tmp, 'binary')); stream.write(Buffer.from(tmp, 'binary'));
}); });
needRestore = true; needRestore = true;
stream.on('data', data => { stream.on('data', data => {
const tmp = data.toString('binary').replace(/\xff/g, '\xff\xff'); // escape const tmp = data
.toString('binary')
.replace(/\xff/g, '\xff\xff'); // escape
self.client.term.rawWrite(Buffer.from(tmp, 'binary')); self.client.term.rawWrite(Buffer.from(tmp, 'binary'));
}); });
@ -95,41 +108,46 @@ exports.getModule = class ArchaicNETModule extends MenuModule {
restorePipe(); restorePipe();
return sshClient.end(); 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

@ -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 archConfig = config.archives.archivers[archKey];
const archiver = new Archiver(archConfig); const archiver = new Archiver(archConfig);
if(!archiver.ok()) { 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

@ -28,20 +28,20 @@ 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;
} }
} }
@ -50,8 +50,8 @@ 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;
} }
@ -71,7 +71,7 @@ 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);
} }
@ -84,30 +84,33 @@ function getArtFromPath(path, options, cb) {
// :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];
@ -141,51 +144,53 @@ function getArt(name, options, cb) {
// :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 = {};
@ -279,33 +287,32 @@ function display(client, art, options, cb) {
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

@ -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

@ -10,52 +10,60 @@ 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

@ -30,11 +30,12 @@ exports.main = main;
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,10 +86,12 @@ 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
} }
@ -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(
paths.join(__dirname, '../misc/startup_banner.asc'),
'utf8',
(err, banner) => {
console.info(FULL_COPYRIGHT); console.info(FULL_COPYRIGHT);
if(!err) { if (!err) {
console.info(banner); 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,23 +153,25 @@ 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 {
@ -166,13 +179,13 @@ function shutdownSystem() {
} }
}, },
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,23 +199,32 @@ 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(
Object.keys(Config.paths),
function entry(pathKey, next) {
mkdirs(Config.paths[pathKey], function dirCreated(err) { mkdirs(Config.paths[pathKey], function dirCreated(err) {
if(err) { if (err) {
console.error('Could not create path: ' + Config.paths[pathKey] + ': ' + err.toString()); console.error(
'Could not create path: ' +
Config.paths[pathKey] +
': ' +
err.toString()
);
} }
return next(err); return next(err);
}); });
}, function dirCreationComplete(err) { },
function dirCreationComplete(err) {
return callback(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! ****'
); );
@ -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(
User.RootUserID,
propLoadOpts,
(err, opProps) => {
return next(err, opUserName, 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

@ -4,10 +4,7 @@
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');
@ -42,9 +39,9 @@ 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 {
@ -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,
null,
function resp(err, body) {
if (err) {
callback(err); callback(err);
} else { } else {
token = body.trim(); token = body.trim();
self.client.log.trace( { token : token }, 'BBSLink token'); self.client.log.trace(
{ token: token },
'BBSLink token'
);
callback(null); 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(
'/auth.php?key=' + randomKey,
headers,
function resp(err, body) {
var status = body.trim(); 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(
'Connection ended. Terminating BBSLink connection'
);
clientTerminated = true; clientTerminated = true;
bridgeConnection.end(); 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

@ -4,10 +4,7 @@
// 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');
@ -22,52 +19,52 @@ 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(
self.entries,
(entry, next) => {
User.getUserName(entry.submitterUserId, (err, username) => { User.getUserName(entry.submitterUserId, (err, username) => {
if(username) { if (username) {
entry.submitter = username; entry.submitter = username;
} else { } else {
entry.submitter = 'N/A'; entry.submitter = 'N/A';
} }
return next(); 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

@ -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

@ -60,22 +60,27 @@ 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(
'(?:\u001b+)(O|N|\\[|\\[\\[)(?:' +
[
'(\\d+)(?:;(\\d+))?([~^$])', '(\\d+)(?:;(\\d+))?([~^$])',
'(?:M([@ #!a`])(.)(.))', // mouse stuff '(?:M([@ #!a`])(.)(.))', // mouse stuff
'(?:1;)?(\\d+)?([a-zA-Z@])' '(?:1;)?(\\d+)?([a-zA-Z@])',
].join('|') + ')'); ].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_FUNCTION_KEYCODE_ANYWHERE.source,
RE_META_KEYCODE_ANYWHERE.source, RE_META_KEYCODE_ANYWHERE.source,
RE_DSR_RESPONSE_ANYWHERE.source, RE_DSR_RESPONSE_ANYWHERE.source,
RE_DEV_ATTR_RESPONSE_ANYWHERE.source, RE_DEV_ATTR_RESPONSE_ANYWHERE.source,
/\u001b./.source // eslint-disable-line no-control-regex /\u001b./.source, // eslint-disable-line no-control-regex
].join('|')); ].join('|')
);
function Client(/*input, output*/) { function Client(/*input, output*/) {
stream.call(this); stream.call(this);
@ -83,59 +88,61 @@ function Client(/*input, output*/) {
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,134 +180,136 @@ 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);
@ -310,39 +319,39 @@ function Client(/*input, output*/) {
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,7 +360,7 @@ 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';
@ -359,22 +368,22 @@ function Client(/*input, output*/) {
} 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))) {
@ -382,10 +391,12 @@ function Client(/*input, output*/) {
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;
@ -398,14 +409,14 @@ function Client(/*input, output*/) {
} }
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,
{
user : this.user,
statName : UserProps.MinutesOnlineTotalCount,
statIncrementBy : 1,
statValue : minOnline
}
); );
if (0 === minOnline % 5) {
Events.emit(Events.getSystemEvents().UserStatIncrement, {
user: this.user,
statName: UserProps.MinutesOnlineTotalCount,
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

@ -23,13 +23,12 @@ 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,22 +36,25 @@ 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(
moment(ac.user.properties[UserProps.LastLoginTs]),
'minutes'
);
entry.timeOn = moment.duration(diff, 'minutes'); entry.timeOn = moment.duration(diff, 'minutes');
} }
return entry; return entry;
@ -72,34 +74,37 @@ function addNewClient(client, clientSock) {
} }
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

@ -9,7 +9,6 @@ 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) {
@ -38,26 +37,26 @@ function ClientTerminal(output) {
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

@ -23,78 +23,82 @@ 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
} }
@ -102,18 +106,18 @@ function renegadeToAnsi(s, client) {
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,7 +148,8 @@ 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 = '';
@ -153,17 +158,17 @@ function controlCodesToAnsi(s, client) {
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

@ -5,19 +5,16 @@
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 {
@ -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

@ -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

@ -8,8 +8,7 @@ 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
} }
@ -18,27 +17,34 @@ module.exports = new class ConfigCache
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,16 +14,14 @@ 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;
@ -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,14 +211,16 @@ 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(
includePaths,
(includePath, nextIncludePath) => {
this._loadConfigFile(includePath, (err, includedConfig) => { this._loadConfigFile(includePath, (err, includedConfig) => {
if (err) { if (err) {
return nextIncludePath(err); return nextIncludePath(err);
@ -223,15 +231,14 @@ module.exports = class ConfigLoader {
}); });
}, },
err => { err => {
this.configPaths = [ this.baseConfigPath, ...includePaths ]; this.configPaths = [this.baseConfigPath, ...includePaths];
return cb(err, config); return cb(err, config);
}); }
);
} }
_resolveAtSpecs(config) { _resolveAtSpecs(config) {
return mapValuesDeep( return mapValuesDeep(config, value => {
config,
value => {
if (_.isString(value) && '@' === value.charAt(0)) { if (_.isString(value) && '@' === value.charAt(0)) {
if (value.startsWith('@reference:')) { if (value.startsWith('@reference:')) {
const refPath = value.slice(11); const refPath = value.slice(11);
@ -242,7 +249,6 @@ module.exports = class ConfigLoader {
} }
return value; return value;
} });
);
} }
}; };

View File

@ -15,13 +15,13 @@ 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
// //
@ -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,17 +102,21 @@ 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
@ -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,10 +193,11 @@ 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;
@ -193,9 +205,9 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
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,17 +256,20 @@ 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;
@ -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 {
@ -55,14 +55,14 @@ exports.CRC32 = class CRC32 {
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];
} }
} }
@ -70,22 +70,22 @@ exports.CRC32 = class CRC32 {
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

@ -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) => {
dbs[dbName] = sqlite3Trans.wrap(
new sqlite3.Database(getDatabasePath(dbName), err => {
if (err) {
return cb(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 => { );
},
err => {
return cb(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

@ -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,13 +35,17 @@ 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,
(entryData, nextLine) => {
// //
// We allow quoted (long) filenames or non-quoted filenames. // We allow quoted (long) filenames or non-quoted filenames.
// FILENAME<SPC>DESC<0x04><program data><CR/LF> // FILENAME<SPC>DESC<0x04><program data><CR/LF>
// //
const parts = entryData.match(/^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/); // eslint-disable-line no-control-regex const parts = entryData.match(
if(!parts) { /^(?:(?:"([^"]+)" )|(?:([^ ]+) ))([^\x04]+)\x04(.)[^\r\n]*$/
); // eslint-disable-line no-control-regex
if (!parts) {
return nextLine(null); return nextLine(null);
} }
@ -54,24 +58,25 @@ module.exports = class DescriptIonFile {
// //
const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n'); const desc = parts[3].replace(/\\r\\n|\\n|[^@]@n/g, '\r\n');
descIonFile.entries.set( descIonFile.entries.set(fileName, {
fileName, desc: desc,
{ programId: parts[4],
desc : desc, programData: parts[5],
programId : parts[4], });
programData : parts[5],
}
);
return nextLine(null); return nextLine(null);
}, },
() => { () => {
return cb( return cb(
descIonFile.entries.size > 0 ? null : Errors.Invalid('Invalid or unrecognized DESCRIPT.ION format'), descIonFile.entries.size > 0
? null
: Errors.Invalid(
'Invalid or unrecognized DESCRIPT.ION format'
),
descIonFile descIonFile
); );
}); }
);
}); });
} }
}; };

View File

@ -22,7 +22,7 @@ module.exports = class Door {
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

@ -5,19 +5,16 @@
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 {
@ -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,15 +72,24 @@ 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); doorTracking = trackDoorRunBegin(self.client);
@ -112,11 +118,14 @@ exports.getModule = class DoorPartyModule extends MenuModule {
restorePipe(); restorePipe();
sshClient.end(); 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

@ -23,18 +23,22 @@ 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

@ -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,52 +35,64 @@ 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'
);
} }
} }
@ -88,13 +102,19 @@ module.exports = class DownloadQueue {
// 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

@ -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(
client,
{ fileType = 'DORINFO', baseDir = Config().paths.dropFiles } = {}
) {
this.client = client; this.client = client;
this.fileType = fileType.toUpperCase(); this.fileType = fileType.toUpperCase();
this.baseDir = baseDir; 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));
@ -91,13 +94,18 @@ module.exports = class DropFile {
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" 'COM1:', // "Comm Port - COM0: = LOCAL MODE"
'57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!) '57600', // "Baud Rate - 300 to 38400" (Note: set as 57600 instead!)
'8', // "Parity - 7 or 8" '8', // "Parity - 7 or 8"
@ -108,7 +116,7 @@ module.exports = class DropFile {
'Y', // "Page Bell - Y=On N=Off (Default to Y)" 'Y', // "Page Bell - Y=On N=Off (Default to Y)"
'Y', // "Caller Alarm - Y=On N=Off (Default to Y)" 'Y', // "Caller Alarm - Y=On N=Off (Default to Y)"
fullName, // "User Full Name" fullName, // "User Full Name"
prop[UserProps.Location]|| 'Anywhere', // "Calling From" prop[UserProps.Location] || 'Anywhere', // "Calling From"
'123-456-7890', // "Home Phone" '123-456-7890', // "Home Phone"
'123-456-7890', // "Work/Data Phone" '123-456-7890', // "Work/Data Phone"
'NOPE', // "Password" (Note: this is never given out or even stored plaintext) 'NOPE', // "Password" (Note: this is never given out or even stored plaintext)
@ -152,7 +160,9 @@ module.exports = class DropFile {
prop[UserProps.UserComment] || 'None', // "User Comment" prop[UserProps.UserComment] || 'None', // "User Comment"
'0', // "Total Doors Opened" '0', // "Total Doors Opened"
'0', // "Total Messages Left" '0', // "Total Messages Left"
].join('\r\n') + '\r\n', 'cp437'); ].join('\r\n') + '\r\n',
'cp437'
);
} }
getDoor32Buffer() { getDoor32Buffer() {
@ -163,14 +173,15 @@ 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(), commType.toString(),
'-1', '-1',
'115200', '115200',
@ -182,7 +193,9 @@ module.exports = class DropFile {
'546', // :TODO: Minutes left! '546', // :TODO: Minutes left!
'1', // ANSI '1', // ANSI
this.client.node.toString(), this.client.node.toString(),
].join('\r\n') + '\r\n', 'cp437'); ].join('\r\n') + '\r\n',
'cp437'
);
} }
getDoorInfoDefBuffer() { getDoorInfoDefBuffer() {
@ -194,12 +207,15 @@ 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(
StatLog.getSystemStat(SysProps.SysOpUsername)
)[0];
const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0]; const userName = /[^\s]*/.exec(this.client.user.getSanitizedName())[0];
const secLevel = this.client.user.getLegacySecurityLevel().toString(); const secLevel = this.client.user.getLegacySecurityLevel().toString();
const location = this.client.user.properties[UserProps.Location]; const location = this.client.user.properties[UserProps.Location];
return iconv.encode( [ return iconv.encode(
[
Config().general.boardName, // "The name of the system." Config().general.boardName, // "The name of the system."
opUserName, // "The sysop's name up to the first space." opUserName, // "The sysop's name up to the first space."
opUserName, // "The sysop's name following the first space." opUserName, // "The sysop's name following the first space."
@ -212,13 +228,15 @@ module.exports = class DropFile {
'1', // "The number "0" if TTY, or "1" if ANSI." '1', // "The number "0" if TTY, or "1" if ANSI."
secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops." secLevel, // "The number 5 for problem users, 30 for regular users, 80 for Aides, and 100 for Sysops."
'546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software." '546', // "The number of minutes left in the current user's account, limited to 546 to keep from overflowing other software."
'-1' // "The number "-1" if using an external serial driver or "0" if using internal serial routines." '-1', // "The number "-1" if using an external serial driver or "0" if using internal serial routines."
].join('\r\n') + '\r\n', 'cp437'); ].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

@ -14,7 +14,7 @@ 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) {
@ -23,36 +23,38 @@ function EditTextView(options) {
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,7 +65,7 @@ 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
@ -72,20 +74,20 @@ EditTextView.prototype.onKeyPress = function(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

@ -14,14 +14,14 @@ 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

@ -11,14 +11,14 @@ class EnigError extends Error {
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;
} }
} }
} }
@ -26,32 +26,44 @@ class EnigError extends Error {
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

@ -8,11 +8,11 @@ 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

@ -20,9 +20,9 @@ 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]+)?$/;
@ -33,17 +33,21 @@ class ScheduledEvent {
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(
later.setInterval(() => {
self.performAction(schedEvent, 'Schedule'); self.performAction(schedEvent, 'Schedule');
}, schedEvent.schedule.sched)); }, 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

@ -8,7 +8,7 @@ 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...
@ -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

@ -7,13 +7,8 @@ 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');
@ -55,9 +50,9 @@ 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 {
@ -66,17 +61,17 @@ exports.getModule = class ExodusModule extends MenuModule {
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;
@ -84,12 +79,18 @@ exports.getModule = class ExodusModule extends MenuModule {
[ [
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);
} }
@ -99,30 +100,33 @@ 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
.createHash('sha256')
.update(`${self.config.key}${now}`)
.digest('hex');
const token = `${sha256}|${now}`; 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,50 +160,56 @@ 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);
@ -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

@ -4,7 +4,8 @@
// 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 =
require('./file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('./file_base_filter.js'); const FileBaseFilters = require('./file_base_filter.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
@ -13,26 +14,26 @@ const UserProps = require('./user_property.js');
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 {
@ -46,40 +47,43 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
// 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,17 +91,17 @@ 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);
} }
@ -107,25 +111,29 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
// 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,12 +142,14 @@ 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 {
@ -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(
mciId => {
this.setFocusItemIndex(mciId, 0); 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;
} }
@ -306,7 +337,7 @@ exports.getModule = class FileAreaFilterEdit extends MenuModule {
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,7 +358,7 @@ 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);

View File

@ -27,50 +27,49 @@ const moment = require('moment');
const paths = require('path'); const paths = require('path');
exports.moduleInfo = { exports.moduleInfo = {
name : 'File Area List', name: 'File Area List',
desc : 'Lists contents of file an file area', desc: 'Lists contents of file an file area',
author : 'NuSkooler', author: 'NuSkooler',
}; };
const FormIds = { const FormIds = {
browse : 0, browse: 0,
details : 1, details: 1,
detailsGeneral : 2, detailsGeneral: 2,
detailsNfo : 3, detailsNfo: 3,
detailsFileList : 4, detailsFileList: 4,
}; };
const MciViewIds = { const MciViewIds = {
browse : { browse: {
desc : 1, desc: 1,
navMenu : 2, navMenu: 2,
customRangeStart : 10, // 10+ = customs customRangeStart: 10, // 10+ = customs
}, },
details : { details: {
navMenu : 1, navMenu: 1,
infoXyTop : 2, // %XY starting position for info area infoXyTop: 2, // %XY starting position for info area
infoXyBottom : 3, infoXyBottom: 3,
customRangeStart : 10, // 10+ = customs customRangeStart: 10, // 10+ = customs
}, },
detailsGeneral : { detailsGeneral: {
customRangeStart : 10, // 10+ = customs customRangeStart: 10, // 10+ = customs
}, },
detailsNfo : { detailsNfo: {
nfo : 1, nfo: 1,
customRangeStart : 10, // 10+ = customs customRangeStart: 10, // 10+ = customs
}, },
detailsFileList : { detailsFileList: {
fileList : 1, fileList: 1,
customRangeStart : 10, // 10+ = customs customRangeStart: 10, // 10+ = customs
}, },
}; };
exports.getModule = class FileAreaList extends MenuModule { exports.getModule = class FileAreaList extends MenuModule {
constructor(options) { constructor(options) {
super(options); super(options);
@ -78,41 +77,41 @@ exports.getModule = class FileAreaList extends MenuModule {
this.fileList = _.get(options, 'extraArgs.fileList'); this.fileList = _.get(options, 'extraArgs.fileList');
this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true); this.lastFileNextExit = _.get(options, 'extraArgs.lastFileNextExit', true);
if(this.fileList) { if (this.fileList) {
// we'll need to adjust position as well! // we'll need to adjust position as well!
this.fileListPosition = 0; this.fileListPosition = 0;
} }
this.dlQueue = new DownloadQueue(this.client); this.dlQueue = new DownloadQueue(this.client);
if(!this.filterCriteria) { if (!this.filterCriteria) {
this.filterCriteria = FileBaseFilters.getActiveFilter(this.client); this.filterCriteria = FileBaseFilters.getActiveFilter(this.client);
} }
if(_.isString(this.filterCriteria)) { if (_.isString(this.filterCriteria)) {
this.filterCriteria = JSON.parse(this.filterCriteria); this.filterCriteria = JSON.parse(this.filterCriteria);
} }
if(_.has(options, 'lastMenuResult.value')) { if (_.has(options, 'lastMenuResult.value')) {
this.lastMenuResultValue = options.lastMenuResult.value; this.lastMenuResultValue = options.lastMenuResult.value;
} }
this.menuMethods = { this.menuMethods = {
nextFile : (formData, extraArgs, cb) => { nextFile: (formData, extraArgs, cb) => {
if(this.fileListPosition + 1 < this.fileList.length) { if (this.fileListPosition + 1 < this.fileList.length) {
this.fileListPosition += 1; this.fileListPosition += 1;
return this.displayBrowsePage(true, cb); // true=clerarScreen return this.displayBrowsePage(true, cb); // true=clerarScreen
} }
if(this.lastFileNextExit) { if (this.lastFileNextExit) {
return this.prevMenu(cb); return this.prevMenu(cb);
} }
return cb(null); return cb(null);
}, },
prevFile : (formData, extraArgs, cb) => { prevFile: (formData, extraArgs, cb) => {
if(this.fileListPosition > 0) { if (this.fileListPosition > 0) {
--this.fileListPosition; --this.fileListPosition;
return this.displayBrowsePage(true, cb); // true=clearScreen return this.displayBrowsePage(true, cb); // true=clearScreen
@ -120,32 +119,32 @@ exports.getModule = class FileAreaList extends MenuModule {
return cb(null); return cb(null);
}, },
viewDetails : (formData, extraArgs, cb) => { viewDetails: (formData, extraArgs, cb) => {
this.viewControllers.browse.setFocus(false); this.viewControllers.browse.setFocus(false);
return this.displayDetailsPage(cb); return this.displayDetailsPage(cb);
}, },
detailsQuit : (formData, extraArgs, cb) => { detailsQuit: (formData, extraArgs, cb) => {
[ 'detailsNfo', 'detailsFileList', 'details' ].forEach(n => { ['detailsNfo', 'detailsFileList', 'details'].forEach(n => {
const vc = this.viewControllers[n]; const vc = this.viewControllers[n];
if(vc) { if (vc) {
vc.detachClientEvents(); vc.detachClientEvents();
} }
}); });
return this.displayBrowsePage(true, cb); // true=clearScreen return this.displayBrowsePage(true, cb); // true=clearScreen
}, },
toggleQueue : (formData, extraArgs, cb) => { toggleQueue: (formData, extraArgs, cb) => {
this.dlQueue.toggle(this.currentFileEntry); this.dlQueue.toggle(this.currentFileEntry);
this.updateQueueIndicator(); this.updateQueueIndicator();
return cb(null); return cb(null);
}, },
showWebDownloadLink : (formData, extraArgs, cb) => { showWebDownloadLink: (formData, extraArgs, cb) => {
return this.fetchAndDisplayWebDownloadLink(cb); return this.fetchAndDisplayWebDownloadLink(cb);
}, },
displayHelp : (formData, extraArgs, cb) => { displayHelp: (formData, extraArgs, cb) => {
return this.displayHelpPage(cb); return this.displayHelpPage(cb);
}, },
movementKeyPressed : (formData, extraArgs, cb) => { movementKeyPressed: (formData, extraArgs, cb) => {
return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb); return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb);
}, },
}; };
@ -161,31 +160,39 @@ exports.getModule = class FileAreaList extends MenuModule {
getSaveState() { getSaveState() {
return { return {
fileList : this.fileList, fileList: this.fileList,
fileListPosition : this.fileListPosition, fileListPosition: this.fileListPosition,
}; };
} }
restoreSavedState(savedState) { restoreSavedState(savedState) {
if(savedState) { if (savedState) {
this.fileList = savedState.fileList; this.fileList = savedState.fileList;
this.fileListPosition = savedState.fileListPosition; this.fileListPosition = savedState.fileListPosition;
} }
} }
updateFileEntryWithMenuResult(cb) { updateFileEntryWithMenuResult(cb) {
if(!this.lastMenuResultValue) { if (!this.lastMenuResultValue) {
return cb(null); return cb(null);
} }
if(_.isNumber(this.lastMenuResultValue.rating)) { if (_.isNumber(this.lastMenuResultValue.rating)) {
const fileId = this.fileList[this.fileListPosition]; const fileId = this.fileList[this.fileListPosition];
FileEntry.persistUserRating(fileId, this.client.user.userId, this.lastMenuResultValue.rating, err => { FileEntry.persistUserRating(
if(err) { fileId,
this.client.log.warn( { error : err.message, fileId : fileId }, 'Failed to persist file rating' ); this.client.user.userId,
this.lastMenuResultValue.rating,
err => {
if (err) {
this.client.log.warn(
{ error: err.message, fileId: fileId },
'Failed to persist file rating'
);
} }
return cb(null); return cb(null);
}); }
);
} else { } else {
return cb(null); return cb(null);
} }
@ -204,12 +211,15 @@ exports.getModule = class FileAreaList extends MenuModule {
}, },
function display(callback) { function display(callback) {
return self.displayBrowsePage(false, err => { return self.displayBrowsePage(false, err => {
if(err) { if (err) {
self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults'); self.gotoMenu(
self.menuConfig.config.noResultsMenu ||
'fileBaseListEntriesNoResults'
);
} }
return callback(err); return callback(err);
}); });
} },
], ],
() => { () => {
self.finishedLoading(); self.finishedLoading();
@ -221,28 +231,34 @@ exports.getModule = class FileAreaList extends MenuModule {
const config = this.menuConfig.config; const config = this.menuConfig.config;
const currEntry = this.currentFileEntry; const currEntry = this.currentFileEntry;
const uploadTimestampFormat = config.uploadTimestampFormat || this.client.currentTheme.helpers.getDateFormat('short'); const uploadTimestampFormat =
config.uploadTimestampFormat ||
this.client.currentTheme.helpers.getDateFormat('short');
const area = FileArea.getFileAreaByTag(currEntry.areaTag); const area = FileArea.getFileAreaByTag(currEntry.areaTag);
const hashTagsSep = config.hashTagsSep || ', '; const hashTagsSep = config.hashTagsSep || ', ';
const isQueuedIndicator = config.isQueuedIndicator || 'Y'; const isQueuedIndicator = config.isQueuedIndicator || 'Y';
const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N'; const isNotQueuedIndicator = config.isNotQueuedIndicator || 'N';
const entryInfo = currEntry.entryInfo = { const entryInfo = (currEntry.entryInfo = {
fileId : currEntry.fileId, fileId: currEntry.fileId,
areaTag : currEntry.areaTag, areaTag: currEntry.areaTag,
areaName : _.get(area, 'name') || 'N/A', areaName: _.get(area, 'name') || 'N/A',
areaDesc : _.get(area, 'desc') || 'N/A', areaDesc: _.get(area, 'desc') || 'N/A',
fileSha256 : currEntry.fileSha256, fileSha256: currEntry.fileSha256,
fileName : currEntry.fileName, fileName: currEntry.fileName,
desc : currEntry.desc || '', desc: currEntry.desc || '',
descLong : currEntry.descLong || '', descLong: currEntry.descLong || '',
userRating : currEntry.userRating, userRating: currEntry.userRating,
uploadTimestamp : moment(currEntry.uploadTimestamp).format(uploadTimestampFormat), uploadTimestamp: moment(currEntry.uploadTimestamp).format(
hashTags : Array.from(currEntry.hashTags).join(hashTagsSep), uploadTimestampFormat
isQueued : this.dlQueue.isQueued(currEntry) ? isQueuedIndicator : isNotQueuedIndicator, ),
webDlLink : '', // :TODO: fetch web any existing web d/l link hashTags: Array.from(currEntry.hashTags).join(hashTagsSep),
webDlExpire : '', // :TODO: fetch web d/l link expire time isQueued: this.dlQueue.isQueued(currEntry)
}; ? isQueuedIndicator
: isNotQueuedIndicator,
webDlLink: '', // :TODO: fetch web any existing web d/l link
webDlExpire: '', // :TODO: fetch web d/l link expire time
});
// //
// We need the entry object to contain meta keys even if they are empty as // We need the entry object to contain meta keys even if they are empty as
@ -250,19 +266,23 @@ exports.getModule = class FileAreaList extends MenuModule {
// //
const metaValues = FileEntry.WellKnownMetaValues; const metaValues = FileEntry.WellKnownMetaValues;
metaValues.forEach(name => { metaValues.forEach(name => {
const value = !_.isUndefined(currEntry.meta[name]) ? currEntry.meta[name] : 'N/A'; const value = !_.isUndefined(currEntry.meta[name])
? currEntry.meta[name]
: 'N/A';
entryInfo[_.camelCase(name)] = value; entryInfo[_.camelCase(name)] = value;
}); });
if(entryInfo.archiveType) { if (entryInfo.archiveType) {
const mimeType = resolveMimeType(entryInfo.archiveType); const mimeType = resolveMimeType(entryInfo.archiveType);
let desc; let desc;
if(mimeType) { if (mimeType) {
let fileType = _.get(Config(), [ 'fileTypes', mimeType ] ); let fileType = _.get(Config(), ['fileTypes', mimeType]);
if(Array.isArray(fileType)) { if (Array.isArray(fileType)) {
// further refine by extention // further refine by extention
fileType = fileType.find(ft => paths.extname(currEntry.fileName) === ft.ext); fileType = fileType.find(
ft => paths.extname(currEntry.fileName) === ft.ext
);
} }
desc = fileType && fileType.desc; desc = fileType && fileType.desc;
} }
@ -271,7 +291,8 @@ exports.getModule = class FileAreaList extends MenuModule {
entryInfo.archiveTypeDesc = 'N/A'; entryInfo.archiveTypeDesc = 'N/A';
} }
entryInfo.uploadByUsername = entryInfo.uploadByUserName = entryInfo.uploadByUsername || 'N/A'; // may be imported entryInfo.uploadByUsername = entryInfo.uploadByUserName =
entryInfo.uploadByUsername || 'N/A'; // may be imported
entryInfo.hashTags = entryInfo.hashTags || '(none)'; entryInfo.hashTags = entryInfo.hashTags || '(none)';
// create a rating string, e.g. "**---" // create a rating string, e.g. "**---"
@ -279,31 +300,48 @@ exports.getModule = class FileAreaList extends MenuModule {
const userRatingUnticked = config.userRatingUnticked || ''; const userRatingUnticked = config.userRatingUnticked || '';
entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe! entryInfo.userRating = ~~Math.round(entryInfo.userRating) || 0; // be safe!
entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating); entryInfo.userRatingString = userRatingTicked.repeat(entryInfo.userRating);
if(entryInfo.userRating < 5) { if (entryInfo.userRating < 5) {
entryInfo.userRatingString += userRatingUnticked.repeat( (5 - entryInfo.userRating) ); entryInfo.userRatingString += userRatingUnticked.repeat(
5 - entryInfo.userRating
);
} }
FileAreaWeb.getExistingTempDownloadServeItem(this.client, this.currentFileEntry, (err, serveItem) => { FileAreaWeb.getExistingTempDownloadServeItem(
if(err) { this.client,
this.currentFileEntry,
(err, serveItem) => {
if (err) {
entryInfo.webDlExpire = ''; entryInfo.webDlExpire = '';
if(ErrNotEnabled === err.reasonCode) { if (ErrNotEnabled === err.reasonCode) {
entryInfo.webDlExpire = config.webDlLinkNoWebserver || 'Web server is not enabled'; entryInfo.webDlExpire =
config.webDlLinkNoWebserver || 'Web server is not enabled';
} else { } else {
entryInfo.webDlLink = config.webDlLinkNeedsGenerated || 'Not yet generated'; entryInfo.webDlLink =
config.webDlLinkNeedsGenerated || 'Not yet generated';
} }
} else { } else {
const webDlExpireTimeFormat = config.webDlExpireTimeFormat || this.client.currentTheme.helpers.getDateTimeFormat('short'); const webDlExpireTimeFormat =
config.webDlExpireTimeFormat ||
this.client.currentTheme.helpers.getDateTimeFormat('short');
entryInfo.webDlLink = ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url; entryInfo.webDlLink =
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); ansi.vtxHyperlink(this.client, serveItem.url) + serveItem.url;
entryInfo.webDlExpire = moment(serveItem.expireTimestamp).format(
webDlExpireTimeFormat
);
} }
return cb(null); return cb(null);
}); }
);
} }
populateCustomLabels(category, startId) { populateCustomLabels(category, startId) {
return this.updateCustomViewTextsWithFilter(category, startId, this.currentFileEntry.entryInfo); return this.updateCustomViewTextsWithFilter(
category,
startId,
this.currentFileEntry.entryInfo
);
} }
displayArtAndPrepViewController(name, options, cb) { displayArtAndPrepViewController(name, options, cb) {
@ -313,47 +351,54 @@ exports.getModule = class FileAreaList extends MenuModule {
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)
);
if('details' === name) { if ('details' === name) {
try { try {
self.detailsInfoArea = { self.detailsInfoArea = {
top : artData.mciMap.XY2.position, top: artData.mciMap.XY2.position,
bottom : artData.mciMap.XY3.position, bottom: artData.mciMap.XY3.position,
}; };
} catch(e) { } catch (e) {
return callback(Errors.DoesNotExist('Missing XY2 and XY3 position indicators!')); return callback(
Errors.DoesNotExist(
'Missing XY2 and XY3 position indicators!'
)
);
} }
} }
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);
@ -361,7 +406,6 @@ exports.getModule = class FileAreaList extends MenuModule {
self.viewControllers[name].setFocus(true); self.viewControllers[name].setFocus(true);
return callback(null); return callback(null);
}, },
], ],
err => { err => {
@ -376,35 +420,46 @@ exports.getModule = class FileAreaList extends MenuModule {
async.series( async.series(
[ [
function fetchEntryData(callback) { function fetchEntryData(callback) {
if(self.fileList) { if (self.fileList) {
return callback(null); return callback(null);
} }
return self.loadFileIds(false, callback); // false=do not force return self.loadFileIds(false, callback); // false=do not force
}, },
function checkEmptyResults(callback) { function checkEmptyResults(callback) {
if(0 === self.fileList.length) { if (0 === self.fileList.length) {
return callback(Errors.General('No results for criteria', 'NORESULTS')); return callback(
Errors.General('No results for criteria', 'NORESULTS')
);
} }
return callback(null); return callback(null);
}, },
function prepArtAndViewController(callback) { function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('browse', { clearScreen : clearScreen }, callback); return self.displayArtAndPrepViewController(
'browse',
{ clearScreen: clearScreen },
callback
);
}, },
function loadCurrentFileInfo(callback) { function loadCurrentFileInfo(callback) {
self.currentFileEntry = new FileEntry(); self.currentFileEntry = new FileEntry();
self.currentFileEntry.load( self.fileList[ self.fileListPosition ], err => { self.currentFileEntry.load(
if(err) { self.fileList[self.fileListPosition],
err => {
if (err) {
return callback(err); return callback(err);
} }
return self.populateCurrentEntryInfo(callback); return self.populateCurrentEntryInfo(callback);
}); }
);
}, },
function populateDesc(callback) { function populateDesc(callback) {
if(_.isString(self.currentFileEntry.desc)) { if (_.isString(self.currentFileEntry.desc)) {
const descView = self.viewControllers.browse.getView(MciViewIds.browse.desc); const descView = self.viewControllers.browse.getView(
if(descView) { MciViewIds.browse.desc
);
if (descView) {
// //
// For descriptions we want to support as many color code systems // For descriptions we want to support as many color code systems
// as we can for coverage of what is found in the while (e.g. Renegade // as we can for coverage of what is found in the while (e.g. Renegade
@ -415,18 +470,24 @@ exports.getModule = class FileAreaList extends MenuModule {
// it as text. // it as text.
// //
const desc = controlCodesToAnsi(self.currentFileEntry.desc); const desc = controlCodesToAnsi(self.currentFileEntry.desc);
if(desc.length != self.currentFileEntry.desc.length || isAnsi(desc)) { if (
desc.length != self.currentFileEntry.desc.length ||
isAnsi(desc)
) {
const opts = { const opts = {
prepped : false, prepped: false,
forceLineTerm : true forceLineTerm: true,
}; };
// //
// if SAUCE states a term width, honor it else we may see // if SAUCE states a term width, honor it else we may see
// display corruption // display corruption
// //
const sauceTermWidth = _.get(self.currentFileEntry.meta, 'desc_sauce.Character.characterWidth'); const sauceTermWidth = _.get(
if(_.isNumber(sauceTermWidth)) { self.currentFileEntry.meta,
'desc_sauce.Character.characterWidth'
);
if (_.isNumber(sauceTermWidth)) {
opts.termWidth = sauceTermWidth; opts.termWidth = sauceTermWidth;
} }
@ -444,12 +505,15 @@ exports.getModule = class FileAreaList extends MenuModule {
}, },
function populateAdditionalViews(callback) { function populateAdditionalViews(callback) {
self.updateQueueIndicator(); self.updateQueueIndicator();
self.populateCustomLabels('browse', MciViewIds.browse.customRangeStart); self.populateCustomLabels(
'browse',
MciViewIds.browse.customRangeStart
);
return callback(null); return callback(null);
} },
], ],
err => { err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
} }
@ -462,33 +526,42 @@ exports.getModule = class FileAreaList extends MenuModule {
async.series( async.series(
[ [
function prepArtAndViewController(callback) { function prepArtAndViewController(callback) {
return self.displayArtAndPrepViewController('details', { clearScreen : true }, callback); return self.displayArtAndPrepViewController(
'details',
{ clearScreen: true },
callback
);
}, },
function populateViews(callback) { function populateViews(callback) {
self.populateCustomLabels('details', MciViewIds.details.customRangeStart); self.populateCustomLabels(
'details',
MciViewIds.details.customRangeStart
);
return callback(null); return callback(null);
}, },
function prepSection(callback) { function prepSection(callback) {
return self.displayDetailsSection('general', false, callback); return self.displayDetailsSection('general', false, callback);
}, },
function listenNavChanges(callback) { function listenNavChanges(callback) {
const navMenu = self.viewControllers.details.getView(MciViewIds.details.navMenu); const navMenu = self.viewControllers.details.getView(
MciViewIds.details.navMenu
);
navMenu.setFocusItemIndex(0); navMenu.setFocusItemIndex(0);
navMenu.on('index update', index => { navMenu.on('index update', index => {
const sectionName = { const sectionName = {
0 : 'general', 0: 'general',
1 : 'nfo', 1: 'nfo',
2 : 'fileList', 2: 'fileList',
}[index]; }[index];
if(sectionName) { if (sectionName) {
self.displayDetailsSection(sectionName, true); self.displayDetailsSection(sectionName, true);
} }
}); });
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -497,15 +570,11 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
displayHelpPage(cb) { displayHelpPage(cb) {
this.displayAsset( this.displayAsset(this.menuConfig.config.art.help, { clearScreen: true }, () => {
this.menuConfig.config.art.help, this.client.waitForKeyPress(() => {
{ clearScreen : true },
() => {
this.client.waitForKeyPress( () => {
return this.displayBrowsePage(true, cb); return this.displayBrowsePage(true, cb);
}); });
} });
);
} }
_handleMovementKeyPress(keyName, cb) { _handleMovementKeyPress(keyName, cb) {
@ -515,10 +584,18 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
switch (keyName) { switch (keyName) {
case 'down arrow' : descView.scrollDocumentUp(); break; case 'down arrow':
case 'up arrow' : descView.scrollDocumentDown(); break; descView.scrollDocumentUp();
case 'page up' : descView.keyPressPageUp(); break; break;
case 'page down' : descView.keyPressPageDown(); break; case 'up arrow':
descView.scrollDocumentDown();
break;
case 'page up':
descView.keyPressPageUp();
break;
case 'page down':
descView.keyPressPageDown();
break;
} }
this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu); this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu);
@ -531,28 +608,34 @@ exports.getModule = class FileAreaList extends MenuModule {
async.series( async.series(
[ [
function generateLinkIfNeeded(callback) { function generateLinkIfNeeded(callback) {
if (self.currentFileEntry.webDlExpireTime < moment()) {
if(self.currentFileEntry.webDlExpireTime < moment()) {
return callback(null); return callback(null);
} }
const expireTime = moment().add(Config().fileBase.web.expireMinutes, 'minutes'); const expireTime = moment().add(
Config().fileBase.web.expireMinutes,
'minutes'
);
FileAreaWeb.createAndServeTempDownload( FileAreaWeb.createAndServeTempDownload(
self.client, self.client,
self.currentFileEntry, self.currentFileEntry,
{ expireTime : expireTime }, { expireTime: expireTime },
(err, url) => { (err, url) => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
self.currentFileEntry.webDlExpireTime = expireTime; self.currentFileEntry.webDlExpireTime = expireTime;
const webDlExpireTimeFormat = self.menuConfig.config.webDlExpireTimeFormat || 'YYYY-MMM-DD @ h:mm'; const webDlExpireTimeFormat =
self.menuConfig.config.webDlExpireTimeFormat ||
'YYYY-MMM-DD @ h:mm';
self.currentFileEntry.entryInfo.webDlLink = ansi.vtxHyperlink(self.client, url) + url; self.currentFileEntry.entryInfo.webDlLink =
self.currentFileEntry.entryInfo.webDlExpire = expireTime.format(webDlExpireTimeFormat); ansi.vtxHyperlink(self.client, url) + url;
self.currentFileEntry.entryInfo.webDlExpire =
expireTime.format(webDlExpireTimeFormat);
return callback(null); return callback(null);
} }
@ -561,11 +644,12 @@ exports.getModule = class FileAreaList extends MenuModule {
function updateActiveViews(callback) { function updateActiveViews(callback) {
self.updateCustomViewTextsWithFilter( self.updateCustomViewTextsWithFilter(
'browse', 'browse',
MciViewIds.browse.customRangeStart, self.currentFileEntry.entryInfo, MciViewIds.browse.customRangeStart,
{ filter : [ '{webDlLink}', '{webDlExpire}' ] } self.currentFileEntry.entryInfo,
{ filter: ['{webDlLink}', '{webDlExpire}'] }
); );
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -578,69 +662,82 @@ exports.getModule = class FileAreaList extends MenuModule {
const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N'; const isNotQueuedIndicator = this.menuConfig.config.isNotQueuedIndicator || 'N';
this.currentFileEntry.entryInfo.isQueued = stringFormat( this.currentFileEntry.entryInfo.isQueued = stringFormat(
this.dlQueue.isQueued(this.currentFileEntry) ? this.dlQueue.isQueued(this.currentFileEntry)
isQueuedIndicator : ? isQueuedIndicator
isNotQueuedIndicator : isNotQueuedIndicator
); );
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'browse', 'browse',
MciViewIds.browse.customRangeStart, MciViewIds.browse.customRangeStart,
this.currentFileEntry.entryInfo, this.currentFileEntry.entryInfo,
{ filter : [ '{isQueued}' ] } { filter: ['{isQueued}'] }
); );
} }
cacheArchiveEntries(cb) { cacheArchiveEntries(cb) {
// check cache // check cache
if(this.currentFileEntry.archiveEntries) { if (this.currentFileEntry.archiveEntries) {
return cb(null, 'cache'); return cb(null, 'cache');
} }
const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag); const areaInfo = FileArea.getFileAreaByTag(this.currentFileEntry.areaTag);
if(!areaInfo) { if (!areaInfo) {
return cb(Errors.Invalid('Invalid area tag')); return cb(Errors.Invalid('Invalid area tag'));
} }
const filePath = this.currentFileEntry.filePath; const filePath = this.currentFileEntry.filePath;
const archiveUtil = ArchiveUtil.getInstance(); const archiveUtil = ArchiveUtil.getInstance();
archiveUtil.listEntries(filePath, this.currentFileEntry.entryInfo.archiveType, (err, entries) => { archiveUtil.listEntries(
if(err) { filePath,
this.currentFileEntry.entryInfo.archiveType,
(err, entries) => {
if (err) {
return cb(err); return cb(err);
} }
// assign and add standard "text" member for itemFormat // assign and add standard "text" member for itemFormat
this.currentFileEntry.archiveEntries = entries.map(e => Object.assign(e, { text : `${e.fileName} (${e.byteSize})` } )); this.currentFileEntry.archiveEntries = entries.map(e =>
Object.assign(e, { text: `${e.fileName} (${e.byteSize})` })
);
return cb(null, 're-cached'); return cb(null, 're-cached');
}); }
);
} }
setFileListNoListing(text) { setFileListNoListing(text) {
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); const fileListView = this.viewControllers.detailsFileList.getView(
if(fileListView) { MciViewIds.detailsFileList.fileList
);
if (fileListView) {
fileListView.complexItems = false; fileListView.complexItems = false;
fileListView.setItems( [ text ] ); fileListView.setItems([text]);
fileListView.redraw(); fileListView.redraw();
} }
} }
populateFileListing() { populateFileListing() {
const fileListView = this.viewControllers.detailsFileList.getView(MciViewIds.detailsFileList.fileList); const fileListView = this.viewControllers.detailsFileList.getView(
MciViewIds.detailsFileList.fileList
);
if(this.currentFileEntry.entryInfo.archiveType) { if (this.currentFileEntry.entryInfo.archiveType) {
this.cacheArchiveEntries( (err, cacheStatus) => { this.cacheArchiveEntries((err, cacheStatus) => {
if(err) { if (err) {
return this.setFileListNoListing('Failed to get file listing'); return this.setFileListNoListing('Failed to get file listing');
} }
if('re-cached' === cacheStatus) { if ('re-cached' === cacheStatus) {
fileListView.setItems(this.currentFileEntry.archiveEntries); fileListView.setItems(this.currentFileEntry.archiveEntries);
fileListView.redraw(); fileListView.redraw();
} }
}); });
} else { } else {
const notAnArchiveFileName = stringFormat(this.menuConfig.config.notAnArchiveFormat || 'Not an archive', { fileName : this.currentFileEntry.fileName } ); const notAnArchiveFileName = stringFormat(
this.menuConfig.config.notAnArchiveFormat || 'Not an archive',
{ fileName: this.currentFileEntry.fileName }
);
this.setFileListNoListing(notAnArchiveFileName); this.setFileListNoListing(notAnArchiveFileName);
} }
} }
@ -652,78 +749,87 @@ exports.getModule = class FileAreaList extends MenuModule {
async.series( async.series(
[ [
function detachPrevious(callback) { function detachPrevious(callback) {
if(self.lastDetailsViewController) { if (self.lastDetailsViewController) {
self.lastDetailsViewController.detachClientEvents(); self.lastDetailsViewController.detachClientEvents();
} }
return callback(null); return callback(null);
}, },
function prepArtAndViewController(callback) { function prepArtAndViewController(callback) {
function gotoTopPos() { function gotoTopPos() {
self.client.term.rawWrite(ansi.goto(self.detailsInfoArea.top[0], 1)); self.client.term.rawWrite(
ansi.goto(self.detailsInfoArea.top[0], 1)
);
} }
gotoTopPos(); gotoTopPos();
if(clearArea) { if (clearArea) {
self.client.term.rawWrite(ansi.reset()); self.client.term.rawWrite(ansi.reset());
let pos = self.detailsInfoArea.top[0]; let pos = self.detailsInfoArea.top[0];
const bottom = self.detailsInfoArea.bottom[0]; const bottom = self.detailsInfoArea.bottom[0];
while(pos++ <= bottom) { while (pos++ <= bottom) {
self.client.term.rawWrite(ansi.eraseLine() + ansi.down()); self.client.term.rawWrite(ansi.eraseLine() + ansi.down());
} }
gotoTopPos(); gotoTopPos();
} }
return self.displayArtAndPrepViewController(name, { clearScreen : false, noInput : true }, callback); return self.displayArtAndPrepViewController(
name,
{ clearScreen: false, noInput: true },
callback
);
}, },
function populateViews(callback) { function populateViews(callback) {
self.lastDetailsViewController = self.viewControllers[name]; self.lastDetailsViewController = self.viewControllers[name];
switch(sectionName) { switch (sectionName) {
case 'nfo' : case 'nfo':
{ {
const nfoView = self.viewControllers.detailsNfo.getView(MciViewIds.detailsNfo.nfo); const nfoView = self.viewControllers.detailsNfo.getView(
if(!nfoView) { MciViewIds.detailsNfo.nfo
);
if (!nfoView) {
return callback(null); return callback(null);
} }
if(isAnsi(self.currentFileEntry.entryInfo.descLong)) { if (isAnsi(self.currentFileEntry.entryInfo.descLong)) {
nfoView.setAnsi( nfoView.setAnsi(
self.currentFileEntry.entryInfo.descLong, self.currentFileEntry.entryInfo.descLong,
{ {
prepped : false, prepped: false,
forceLineTerm : true, forceLineTerm: true,
}, },
() => { () => {
return callback(null); return callback(null);
} }
); );
} else { } else {
nfoView.setText(self.currentFileEntry.entryInfo.descLong); nfoView.setText(
self.currentFileEntry.entryInfo.descLong
);
return callback(null); return callback(null);
} }
} }
break; break;
case 'fileList' : case 'fileList':
self.populateFileListing(); self.populateFileListing();
return callback(null); return callback(null);
default : default:
return callback(null); return callback(null);
} }
}, },
function setLabels(callback) { function setLabels(callback) {
self.populateCustomLabels(name, MciViewIds[name].customRangeStart); self.populateCustomLabels(name, MciViewIds[name].customRangeStart);
return callback(null); return callback(null);
} },
], ],
err => { err => {
if(cb) { if (cb) {
return cb(err); return cb(err);
} }
} }
@ -731,11 +837,15 @@ exports.getModule = class FileAreaList extends MenuModule {
} }
loadFileIds(force, cb) { loadFileIds(force, cb) {
if(force || (_.isUndefined(this.fileList) || _.isUndefined(this.fileListPosition))) { if (
force ||
_.isUndefined(this.fileList) ||
_.isUndefined(this.fileListPosition)
) {
this.fileListPosition = 0; this.fileListPosition = 0;
const filterCriteria = Object.assign({}, this.filterCriteria); const filterCriteria = Object.assign({}, this.filterCriteria);
if(!filterCriteria.areaTag) { if (!filterCriteria.areaTag) {
filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client); filterCriteria.areaTag = FileArea.getAvailableFileAreaTags(this.client);
} }

View File

@ -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,7 +245,7 @@ class FileAreaWebAccess {
} }
createAndServeTempDownload(client, fileEntry, options, cb) { createAndServeTempDownload(client, fileEntry, options, cb) {
if(!this.isEnabled()) { if (!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
@ -243,7 +259,7 @@ class FileAreaWebAccess {
} }
createAndServeTempBatchDownload(client, fileEntries, options, cb) { createAndServeTempBatchDownload(client, fileEntries, options, cb) {
if(!this.isEnabled()) { if (!this.isEnabled()) {
return cb(notEnabledError()); return cb(notEnabledError());
} }
@ -252,32 +268,36 @@ class FileAreaWebAccess {
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(
fileEntries,
(entry, nextEntry) => {
trans.run( trans.run(
`INSERT INTO file_web_serve_batch (hash_id, file_id) `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 => {
trans[err ? 'rollback' : 'commit'](() => {
return cb(err, url); 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,38 +398,63 @@ 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(
fileIds,
(fileId, nextFileId) => {
const fileEntry = new FileEntry(); const fileEntry = new FileEntry();
fileEntry.load(fileId, err => { fileEntry.load(fileId, err => {
return nextFileId(err, fileEntry); return nextFileId(err, fileEntry);
}); });
}, (err, fileEntries) => { },
if(err) { (err, fileEntries) => {
return callback(Errors.DoesNotExist('Could not load file IDs for batch')); 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 => {
@ -413,14 +462,16 @@ class FileAreaWebAccess {
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,11 +515,10 @@ 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);
} }
@ -481,17 +537,13 @@ class FileAreaWebAccess {
return callback(null, user); return callback(null, user);
}, },
function sendEvent(user, callback) { function sendEvent(user, callback) {
Events.emit( Events.emit(Events.getSystemEvents().UserDownload, {
Events.getSystemEvents().UserDownload, user: user,
{ files: fileEntries,
user : user, });
files : fileEntries,
}
);
return callback(null); return callback(null);
} },
] ]);
);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,13 +11,13 @@ const SysProps = require('./system_property.js');
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',
0,
mciData.menu,
(err, vc) => {
if (err) {
return callback(err); return callback(err);
} }
const areaListView = vc.getView(MciViewIds.areaList); const areaListView = vc.getView(MciViewIds.areaList);
areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } ))); areaListView.setItems(
availAreas.map(area =>
Object.assign(area, {
text: area.name,
data: area.areaTag,
})
)
);
areaListView.redraw(); areaListView.redraw();
return callback(null); return callback(null);
});
} }
);
},
], ],
err => { err => {
return cb(err); return cb(err);

View File

@ -16,77 +16,87 @@ 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,12 +136,20 @@ 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;
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(
webDlExpireTimeFormat
);
} else { } else {
fileEntry.webDlLink = ''; fileEntry.webDlLink = '';
fileEntry.webDlExpire = ''; fileEntry.webDlExpire = '';
@ -137,15 +157,19 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
this.updateCustomViewTextsWithFilter( this.updateCustomViewTextsWithFilter(
'queueManager', 'queueManager',
MciViewIds.queueManager.customRangeStart, fileEntry, MciViewIds.queueManager.customRangeStart,
{ filter : [ '{webDlLink}', '{webDlExpire}' ] } 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);
} }
} }
@ -189,36 +217,39 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
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

@ -5,7 +5,7 @@ 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;
} }
@ -115,38 +128,40 @@ module.exports = class FileBaseFilters {
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

@ -7,10 +7,7 @@ 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,
isAnsi,
} = require('./string_util.js');
const AnsiPrep = require('./ansi_prep.js'); const AnsiPrep = require('./ansi_prep.js');
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
@ -27,68 +24,76 @@ exports.updateFileBaseDescFilesScheduledEvent = updateFileBaseDescFilesSchedul
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.entryTemplate || 'descript_ion_export_entry_template.asc';
options.tsFormat = options.tsFormat || 'YYYY-MM-DD'; options.tsFormat = options.tsFormat || 'YYYY-MM-DD';
options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec options.descWidth = options.descWidth || 45; // FILE_ID.DIZ spec
options.escapeDesc = _.get(options, 'escapeDesc', false); // escape \r and \n in desc? 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 => {
return progCb(null);
} }
; : progCb => {
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,
(template, nextTemplate) => {
if (!template.name && !template.req) {
return nextTemplate(null, Buffer.from([])); 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)
? template.name
: paths.join(config.paths.misc, template.name);
fs.readFile(template.name, (err, data) => { fs.readFile(template.name, (err, data) => {
return nextTemplate(err, data); return nextTemplate(err, data);
}); });
}, (err, templates) => { },
if(err) { (err, templates) => {
if (err) {
return callback(Errors.General(err.message)); return callback(Errors.General(err.message));
} }
// decode + ensure DOS style CRLF // decode + ensure DOS style CRLF
templates = templates.map(tmp => iconv.decode(tmp, options.templateEncoding).replace(/\r?\n/g, '\r\n') ); 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 // Look for the first {fileDesc} (if any) in 'entry' template & find indentation requirements
let descIndent = 0; let descIndent = 0;
if(!options.escapeDesc) { if (!options.escapeDesc) {
splitTextAtTerms(templates[1]).some(line => { splitTextAtTerms(templates[1]).some(line => {
const pos = line.indexOf('{fileDesc}'); const pos = line.indexOf('{fileDesc}');
if(pos > -1) { if (pos > -1) {
descIndent = pos; descIndent = pos;
return true; // found it! return true; // found it!
} }
@ -97,56 +102,79 @@ function exportFileList(filterCriteria, options, cb) {
} }
return callback(null, templates[0], templates[1], descIndent); 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(
fileIds,
(fileId, nextFileId) => {
const fileInfo = new FileEntry(); const fileInfo = new FileEntry();
current += 1; 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; totals.bytes += fileInfo.meta.byte_size;
const appendFileInfo = () => { const appendFileInfo = () => {
if(options.escapeDesc) { if (options.escapeDesc) {
formatObj.fileDesc = formatObj.fileDesc.replace(/\r?\n/g, options.escapeDesc); formatObj.fileDesc = formatObj.fileDesc.replace(
/\r?\n/g,
options.escapeDesc
);
} }
if(options.maxDescLen) { if (options.maxDescLen) {
formatObj.fileDesc = formatObj.fileDesc.slice(0, options.maxDescLen); formatObj.fileDesc = formatObj.fileDesc.slice(
0,
options.maxDescLen
);
} }
listBody += stringFormat(entryTemplate, formatObj); listBody += stringFormat(entryTemplate, formatObj);
@ -169,50 +197,70 @@ function exportFileList(filterCriteria, options, cb) {
formatObj.fileName = fileInfo.fileName; formatObj.fileName = fileInfo.fileName;
formatObj.fileSize = fileInfo.meta.byte_size; formatObj.fileSize = fileInfo.meta.byte_size;
formatObj.fileDesc = fileInfo.desc || ''; formatObj.fileDesc = fileInfo.desc || '';
formatObj.fileDescShort = formatObj.fileDesc.slice(0, options.descWidth); formatObj.fileDescShort = formatObj.fileDesc.slice(
0,
options.descWidth
);
formatObj.fileSha256 = fileInfo.fileSha256; formatObj.fileSha256 = fileInfo.fileSha256;
formatObj.fileCrc32 = fileInfo.meta.file_crc32; formatObj.fileCrc32 = fileInfo.meta.file_crc32;
formatObj.fileMd5 = fileInfo.meta.file_md5; formatObj.fileMd5 = fileInfo.meta.file_md5;
formatObj.fileSha1 = fileInfo.meta.file_sha1; formatObj.fileSha1 = fileInfo.meta.file_sha1;
formatObj.uploadBy = fileInfo.meta.upload_by_username || 'N/A'; formatObj.uploadBy =
formatObj.fileUploadTs = moment(fileInfo.uploadTimestamp).format(options.tsFormat); fileInfo.meta.upload_by_username || 'N/A';
formatObj.fileHashTags = fileInfo.hashTags.size > 0 ? Array.from(fileInfo.hashTags).join(', ') : '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.currentFile = current;
formatObj.progress = Math.floor( (current / fileIds.length) * 100 ); formatObj.progress = Math.floor(
(current / fileIds.length) * 100
);
if(isAnsi(fileInfo.desc)) { if (isAnsi(fileInfo.desc)) {
AnsiPrep( AnsiPrep(
fileInfo.desc, fileInfo.desc,
{ {
cols : Math.min(options.descWidth, 79 - descIndent), cols: Math.min(
forceLineTerm : true, // ensure each line is term'd options.descWidth,
asciiMode : true, // export to ASCII 79 - descIndent
fillLines : false, // don't fill up to |cols| ),
indent : 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) => { (err, desc) => {
if(desc) { if (desc) {
formatObj.fileDesc = desc; formatObj.fileDesc = desc;
} }
return appendFileInfo(); return appendFileInfo();
} }
); );
} else { } else {
const indentSpc = descIndent > 0 ? ' '.repeat(descIndent) : ''; const indentSpc =
formatObj.fileDesc = splitTextAtTerms(formatObj.fileDesc).join(`\r\n${indentSpc}`) + '\r\n'; descIndent > 0 ? ' '.repeat(descIndent) : '';
formatObj.fileDesc =
splitTextAtTerms(formatObj.fileDesc).join(
`\r\n${indentSpc}`
) + '\r\n';
return appendFileInfo(); return appendFileInfo();
} }
}); });
}, err => { },
err => {
return callback(err, listBody, headerTemplate, totals); 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';
@ -222,15 +270,15 @@ function exportFileList(filterCriteria, options, cb) {
} }
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;
@ -240,11 +288,12 @@ function exportFileList(filterCriteria, options, cb) {
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);
} }
); );
@ -263,39 +312,56 @@ function updateFileBaseDescFilesScheduledEvent(args, cb) {
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(
areas,
(area, nextArea) => {
const storageLocations = FileArea.getAreaStorageLocations(area); const storageLocations = FileArea.getAreaStorageLocations(area);
async.each(storageLocations, (storageLoc, nextStorageLoc) => { async.each(
storageLocations,
(storageLoc, nextStorageLoc) => {
const filterCriteria = { const filterCriteria = {
areaTag : area.areaTag, areaTag: area.areaTag,
storageTag : storageLoc.storageTag, 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(descIonPath, iconv.encode(listBody, 'cp437'), err => { fs.writeFile(
if(err) { descIonPath,
Log.warn( { error : err.message, path : descIonPath }, 'Failed (re)creating DESCRIPT.ION'); iconv.encode(listBody, 'cp437'),
err => {
if (err) {
Log.warn(
{ error: err.message, path: descIonPath },
'Failed (re)creating DESCRIPT.ION'
);
} else { } else {
Log.debug( { path : descIonPath }, '(Re)generated DESCRIPT.ION'); Log.debug(
{ path: descIonPath },
'(Re)generated DESCRIPT.ION'
);
} }
return nextStorageLoc(null); return nextStorageLoc(null);
}
);
}); });
}); },
}, () => { () => {
return nextArea(null); return nextArea(null);
}); }
}, () => { );
},
() => {
return cb(null); return cb(null);
}); }
);
} }

View File

@ -4,28 +4,29 @@
// 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 =
require('./file_base_area.js').getSortedAvailableFileAreas;
const FileBaseFilters = require('./file_base_filter.js'); 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;
@ -97,11 +106,11 @@ exports.getModule = class FileBaseSearch extends MenuModule {
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

@ -17,7 +17,7 @@ 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');
/* /*
@ -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.tsFormat ||
this.client.currentTheme.helpers.getDateTimeFormat('short');
this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ this.config.descWidth = this.config.descWidth || 45; // ie FILE_ID.DIZ
this.config.progBarChar = renderSubstr( (this.config.progBarChar || '▒'), 0, 1); this.config.progBarChar = renderSubstr(this.config.progBarChar || '▒', 0, 1);
this.config.compressThreshold = this.config.compressThreshold || (1440000); // >= 1.44M by default :) 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);
} }

View File

@ -18,58 +18,63 @@ 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(
self.client,
fileEntry,
(err, serveItem) => {
if (err) {
if (ErrNotEnabled === err.reasonCode) {
return nextFileEntry(err); // we should have caught this prior return nextFileEntry(err); // we should have caught this prior
} }
const expireTime = moment().add(config.fileBase.web.expireMinutes, 'minutes'); const expireTime = moment().add(
config.fileBase.web.expireMinutes,
'minutes'
);
FileAreaWeb.createAndServeTempDownload( FileAreaWeb.createAndServeTempDownload(
self.client, self.client,
fileEntry, fileEntry,
{ expireTime : expireTime }, { expireTime: expireTime },
(err, url) => { (err, url) => {
if(err) { if (err) {
return nextFileEntry(err); return nextFileEntry(err);
} }
fileEntry.webDlLinkRaw = url; fileEntry.webDlLinkRaw = url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, url) + url; fileEntry.webDlLink =
fileEntry.webDlExpire = expireTime.format(webDlExpireTimeFormat); ansi.vtxHyperlink(self.client, url) +
url;
fileEntry.webDlExpire =
expireTime.format(
webDlExpireTimeFormat
);
return nextFileEntry(null); return nextFileEntry(null);
} }
); );
} else { } else {
fileEntry.webDlLinkRaw = serveItem.url; fileEntry.webDlLinkRaw = serveItem.url;
fileEntry.webDlLink = ansi.vtxHyperlink(self.client, serveItem.url) + serveItem.url; fileEntry.webDlLink =
fileEntry.webDlExpire = moment(serveItem.expireTimestamp).format(webDlExpireTimeFormat); ansi.vtxHyperlink(
self.client,
serveItem.url
) + serveItem.url;
fileEntry.webDlExpire = moment(
serveItem.expireTimestamp
).format(webDlExpireTimeFormat);
return nextFileEntry(null); return nextFileEntry(null);
} }
}); }
}, err => { );
},
err => {
return callback(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);
} }
} }
@ -234,36 +274,39 @@ exports.getModule = class FileBaseWebDownloadQueueManager extends MenuModule {
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

@ -3,10 +3,7 @@
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,
sanitizeString
} = require('./database.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
// deps // deps
@ -19,28 +16,34 @@ 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 {
@ -49,7 +52,7 @@ module.exports = class FileEntry {
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;
@ -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(
Object.keys(self.meta),
(n, next) => {
const v = self.meta[n]; const v = self.meta[n];
return FileEntry.persistMetaValue(self.fileId, n, v, trans, next); return FileEntry.persistMetaValue(
self.fileId,
n,
v,
trans,
next
);
}, },
err => { err => {
return callback(err, trans); 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) => {
return FileEntry.persistHashTag(
self.fileId,
hashTag,
trans,
next
);
}, },
err => { err => {
return callback(err, trans); 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,11 +415,11 @@ 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;
} }
} }
@ -388,15 +436,15 @@ module.exports = class FileEntry {
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,19 +489,21 @@ 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(
fileIdRows,
(row, nextRow) => {
const fileEntry = new FileEntry(); const fileEntry = new FileEntry();
fileEntry.load(row.file_id, err => { fileEntry.load(row.file_id, err => {
if(!err) { if (!err) {
entries.push(fileEntry); entries.push(fileEntry);
} }
return nextRow(err); return nextRow(err);
@ -461,7 +511,8 @@ module.exports = class FileEntry {
}, },
err => { err => {
return cb(err, entries); 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,7 +748,7 @@ 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;
} }
@ -693,7 +756,7 @@ module.exports = class FileEntry {
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'));
} }
@ -702,7 +765,7 @@ module.exports = class FileEntry {
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
} }
@ -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

@ -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.protocolConfig || config.fileTransferProtocols.zmodem8kSz; // try for *something*
this.direction = this.direction || 'send'; this.direction = this.direction || 'send';
this.sendQueue = this.sendQueue || []; 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);
}); });
@ -205,18 +212,21 @@ exports.getModule = class TransferFileModule extends MenuModule {
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
} }
@ -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,36 +277,42 @@ 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(
files,
(fileName, nextFile) => {
const recvFullPath = paths.join(this.recvDirectory, fileName); 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', {
path: recvFullPath,
});
return nextFile(null); // just try the next one 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;
@ -306,42 +324,49 @@ exports.getModule = class TransferFileModule extends MenuModule {
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' },
(err, tempFileInfo) => {
if (err) {
return callback(err); // failed to create it return callback(err); // failed to create it
} }
fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => { fs.write(tempFileInfo.fd, filePaths.join(SYSTEM_EOL), err => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
fs.close(tempFileInfo.fd, err => { fs.close(tempFileInfo.fd, err => {
return callback(err, tempFileInfo.path); 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);
@ -352,10 +377,12 @@ 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);
} }
@ -365,9 +392,7 @@ exports.getModule = class TransferFileModule extends MenuModule {
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)?
@ -375,22 +400,27 @@ exports.getModule = class TransferFileModule extends MenuModule {
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,10 +538,10 @@ 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 };
} }
} }
@ -510,16 +550,18 @@ exports.getModule = class TransferFileModule extends MenuModule {
let downloadCount = 0; let downloadCount = 0;
let fileIds = []; let fileIds = [];
async.each(this.sendQueue, (queueItem, next) => { async.each(
if(!queueItem.sent) { this.sendQueue,
(queueItem, next) => {
if (!queueItem.sent) {
return next(null); return next(null);
} }
if(queueItem.fileId) { if (queueItem.fileId) {
fileIds.push(queueItem.fileId); fileIds.push(queueItem.fileId);
} }
if(_.isNumber(queueItem.byteSize)) { if (_.isNumber(queueItem.byteSize)) {
downloadCount += 1; downloadCount += 1;
downloadBytes += queueItem.byteSize; downloadBytes += queueItem.byteSize;
return next(null); return next(null);
@ -527,8 +569,11 @@ exports.getModule = class TransferFileModule extends MenuModule {
// we just have a path - figure it out // we just have a path - figure it out
fs.stat(queueItem.path, (err, stats) => { fs.stat(queueItem.path, (err, stats) => {
if(err) { if (err) {
this.client.log.warn( { error : err.message, path : queueItem.path }, 'File stat failed' ); this.client.log.warn(
{ error: err.message, path: queueItem.path },
'File stat failed'
);
} else { } else {
downloadCount += 1; downloadCount += 1;
downloadBytes += stats.size; downloadBytes += stats.size;
@ -536,10 +581,19 @@ exports.getModule = class TransferFileModule extends MenuModule {
return next(null); return next(null);
}); });
}, () => { },
() => {
// All stats/meta currently updated via fire & forget - if this is ever a issue, we can wait for callbacks // 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(
StatLog.incrementUserStat(this.client.user, UserProps.FileDlTotalBytes, downloadBytes); this.client.user,
UserProps.FileDlTotalCount,
downloadCount
);
StatLog.incrementUserStat(
this.client.user,
UserProps.FileDlTotalBytes,
downloadBytes
);
StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount); StatLog.incrementSystemStat(SysProps.FileDlTotalCount, downloadCount);
StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes); StatLog.incrementSystemStat(SysProps.FileDlTotalBytes, downloadBytes);
@ -549,18 +603,24 @@ exports.getModule = class TransferFileModule extends MenuModule {
}); });
return cb(null); 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(
this.recvFilePaths,
(filePath, next) => {
// we just have a path - figure it out // we just have a path - figure it out
fs.stat(filePath, (err, stats) => { fs.stat(filePath, (err, stats) => {
if(err) { if (err) {
this.client.log.warn( { error : err.message, path : filePath }, 'File stat failed' ); this.client.log.warn(
{ error: err.message, path: filePath },
'File stat failed'
);
} else { } else {
uploadCount += 1; uploadCount += 1;
uploadBytes += stats.size; uploadBytes += stats.size;
@ -568,15 +628,25 @@ exports.getModule = class TransferFileModule extends MenuModule {
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

@ -11,24 +11,23 @@ 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;
} }
} }
@ -37,11 +36,11 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
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;
} }
@ -50,36 +49,48 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
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

@ -26,32 +26,35 @@ function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
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
} }
@ -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

@ -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,21 +68,22 @@ 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());
@ -94,35 +93,39 @@ module.exports = class FilesBBSFile {
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 });
}
} }
}, },
},
{ {
// //
// 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));
@ -131,14 +134,14 @@ module.exports = class FilesBBSFile {
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,21 +151,21 @@ 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));
@ -171,14 +174,14 @@ module.exports = class FilesBBSFile {
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,23 +190,24 @@ 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());
@ -215,13 +219,17 @@ module.exports = class FilesBBSFile {
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,14 +269,14 @@ 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
} }
@ -276,16 +284,17 @@ module.exports = class FilesBBSFile {
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

@ -10,29 +10,32 @@ 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

@ -4,16 +4,17 @@
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

@ -47,7 +47,7 @@ exports.getQuotePrefix = getQuotePrefix;
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;
@ -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,7 +93,7 @@ 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
)}`;
} }
// //
@ -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,9 +204,9 @@ 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})`;
@ -208,7 +214,9 @@ function getOrigin(address) {
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})`;
} }
// //
@ -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

@ -18,10 +18,8 @@ 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 // Initialize paging
this.pages = []; this.pages = [];
this.currentPage = 0; this.currentPage = 0;
@ -38,8 +36,12 @@ function FullMenuView(options) {
this.autoAdjustHeightIfEnabled = () => { this.autoAdjustHeightIfEnabled = () => {
if (this.autoAdjustHeight) { if (this.autoAdjustHeight) {
this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); this.dimens.height =
this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); 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.positionCacheExpired = true;
@ -58,16 +60,20 @@ function FullMenuView(options) {
for (let i = 0; i < this.dimens.height; i++) { for (let i = 0; i < this.dimens.height; i++) {
const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; 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.client.term.write(
} `${ansi.goto(
this.position.row + i,
this.position.col
)}${this.getSGR()}${text}`
);
} }
};
this.cachePositions = () => { this.cachePositions = () => {
if (this.positionCacheExpired) { if (this.positionCacheExpired) {
// first, clear the page // first, clear the page
this.clearPage(); this.clearPage();
this.autoAdjustHeightIfEnabled(); this.autoAdjustHeightIfEnabled();
this.pages = []; // reset this.pages = []; // reset
@ -75,7 +81,7 @@ function FullMenuView(options) {
// Calculate number of items visible per column // Calculate number of items visible per column
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
// handle case where one can fit at the end // handle case where one can fit at the end
if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { if (this.dimens.height > this.itemsPerRow * (this.itemSpacing + 1)) {
this.itemsPerRow++; this.itemsPerRow++;
} }
@ -122,10 +128,9 @@ function FullMenuView(options) {
this.items[i - j].fixedLength = maxLength; this.items[i - j].fixedLength = maxLength;
} }
// Check if we have room for this column // Check if we have room for this column
// skip for column 0, we need at least one // skip for column 0, we need at least one
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { if (itemInCol != 0 && col + maxLength > this.dimens.width) {
// save previous page // save previous page
this.pages.push({ start: pageStart, end: i - itemInRow }); this.pages.push({ start: pageStart, end: i - itemInRow });
@ -137,12 +142,10 @@ function FullMenuView(options) {
this.items[i - j].col = this.position.col; this.items[i - j].col = this.position.col;
pageStart = i - j; pageStart = i - j;
} }
} }
// Since this is the last page, save the current page as well // Since this is the last page, save the current page as well
this.pages.push({ start: pageStart, end: i }); this.pages.push({ start: pageStart, end: i });
} }
// also handle going to next column // also handle going to next column
else if (itemInRow == this.itemsPerRow) { else if (itemInRow == this.itemsPerRow) {
@ -166,7 +169,7 @@ function FullMenuView(options) {
// Check if we have room for this column in the current page // Check if we have room for this column in the current page
// skip for first column, we need at least one // skip for first column, we need at least one
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { if (itemInCol != 0 && col + maxLength > this.dimens.width) {
// save previous page // save previous page
this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
@ -181,7 +184,6 @@ function FullMenuView(options) {
for (let j = 0; j < this.itemsPerRow; j++) { for (let j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].col = col; this.items[i - j].col = col;
} }
} }
// increment the column // increment the column
@ -189,7 +191,6 @@ function FullMenuView(options) {
itemInCol++; itemInCol++;
} }
// Set the current page if the current item is focused. // Set the current page if the current item is focused.
if (this.focusedItemIndex === i) { if (this.focusedItemIndex === i) {
this.currentPage = this.pages.length; this.currentPage = this.pages.length;
@ -200,7 +201,7 @@ function FullMenuView(options) {
this.positionCacheExpired = false; this.positionCacheExpired = false;
}; };
this.drawItem = (index) => { this.drawItem = index => {
const item = this.items[index]; const item = this.items[index];
if (!item) { if (!item) {
return; return;
@ -218,21 +219,45 @@ function FullMenuView(options) {
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 === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); formatString(
item.focused && this.focusItemFormat
? this.focusItemFormat
: this.itemFormat,
item
)
);
sgr = this.focusItemFormat
? ''
: index === this.focusedItemIndex
? this.getFocusSGR()
: this.getSGR();
} else { } else {
text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle); text = strUtil.stylizeString(
sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); item.text,
item.focused ? this.focusTextStyle : this.textStyle
);
sgr = index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR();
} }
let renderLength = strUtil.renderStringLength(text); let renderLength = strUtil.renderStringLength(text);
if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) { if (this.hasTextOverflow() && item.col + renderLength > this.dimens.width) {
text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; 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); let padLength = Math.min(item.fixedLength + 1, this.dimens.width);
text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; text = `${sgr}${strUtil.pad(
text,
padLength,
this.fillChar,
this.justify
)}${this.getSGR()}`;
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
this.setRenderCacheItem(index, text, item.focused); this.setRenderCacheItem(index, text, item.focused);
}; };
@ -240,20 +265,24 @@ function FullMenuView(options) {
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 (
let i = this.pages[this.currentPage].start;
i <= this.pages[this.currentPage].end;
++i
) {
this.items[i].focused = this.focusedItemIndex === i; this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i); this.drawItem(i);
} }
} }
}; };
FullMenuView.prototype.setHeight = function(height) { FullMenuView.prototype.setHeight = function (height) {
this.oldDimens = Object.assign({}, this.dimens); this.oldDimens = Object.assign({}, this.dimens);
FullMenuView.super_.prototype.setHeight.call(this, height); FullMenuView.super_.prototype.setHeight.call(this, height);
@ -262,7 +291,7 @@ FullMenuView.prototype.setHeight = function(height) {
this.autoAdjustHeight = false; this.autoAdjustHeight = false;
}; };
FullMenuView.prototype.setWidth = function(width) { FullMenuView.prototype.setWidth = function (width) {
this.oldDimens = Object.assign({}, this.dimens); this.oldDimens = Object.assign({}, this.dimens);
FullMenuView.super_.prototype.setWidth.call(this, width); FullMenuView.super_.prototype.setWidth.call(this, width);
@ -270,20 +299,19 @@ FullMenuView.prototype.setWidth = function(width) {
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
FullMenuView.prototype.setTextOverflow = function(overflow) { FullMenuView.prototype.setTextOverflow = function (overflow) {
FullMenuView.super_.prototype.setTextOverflow.call(this, overflow); FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
this.positionCacheExpired = true; this.positionCacheExpired = true;
};
} FullMenuView.prototype.setPosition = function (pos) {
FullMenuView.prototype.setPosition = function(pos) {
FullMenuView.super_.prototype.setPosition.call(this, pos); FullMenuView.super_.prototype.setPosition.call(this, pos);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
FullMenuView.prototype.setFocus = function(focused) { FullMenuView.prototype.setFocus = function (focused) {
FullMenuView.super_.prototype.setFocus.call(this, focused); FullMenuView.super_.prototype.setFocus.call(this, focused);
this.positionCacheExpired = true; this.positionCacheExpired = true;
this.autoAdjustHeight = false; this.autoAdjustHeight = false;
@ -291,11 +319,11 @@ FullMenuView.prototype.setFocus = function(focused) {
this.redraw(); this.redraw();
}; };
FullMenuView.prototype.setFocusItemIndex = function(index) { FullMenuView.prototype.setFocusItemIndex = function (index) {
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
}; };
FullMenuView.prototype.onKeyPress = function(ch, key) { FullMenuView.prototype.onKeyPress = function (ch, key) {
if (key) { if (key) {
if (this.isKeyMapped('up', key.name)) { if (this.isKeyMapped('up', key.name)) {
this.focusPrevious(); this.focusPrevious();
@ -319,12 +347,12 @@ FullMenuView.prototype.onKeyPress = function(ch, key) {
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key); FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
}; };
FullMenuView.prototype.getData = function() { FullMenuView.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;
}; };
FullMenuView.prototype.setItems = function(items) { FullMenuView.prototype.setItems = function (items) {
// if we have items already, save off their drawing area so we don't leave fragments at redraw // if we have items already, save off their drawing area so we don't leave fragments at redraw
if (this.items && this.items.length) { if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens); this.oldDimens = Object.assign({}, this.dimens);
@ -335,7 +363,7 @@ FullMenuView.prototype.setItems = function(items) {
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
FullMenuView.prototype.removeItem = function(index) { FullMenuView.prototype.removeItem = function (index) {
if (this.items && this.items.length) { if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens); this.oldDimens = Object.assign({}, this.dimens);
} }
@ -344,13 +372,12 @@ FullMenuView.prototype.removeItem = function(index) {
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.focusNext = function () {
if (this.items.length - 1 === this.focusedItemIndex) { if (this.items.length - 1 === this.focusedItemIndex) {
this.clearPage(); this.clearPage();
this.focusedItemIndex = 0; this.focusedItemIndex = 0;
this.currentPage = 0; this.currentPage = 0;
} } else {
else {
this.focusedItemIndex++; this.focusedItemIndex++;
if (this.focusedItemIndex > this.pages[this.currentPage].end) { if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage(); this.clearPage();
@ -363,13 +390,12 @@ FullMenuView.prototype.focusNext = function() {
FullMenuView.super_.prototype.focusNext.call(this); FullMenuView.super_.prototype.focusNext.call(this);
}; };
FullMenuView.prototype.focusPrevious = function() { FullMenuView.prototype.focusPrevious = function () {
if (0 === this.focusedItemIndex) { if (0 === this.focusedItemIndex) {
this.clearPage(); this.clearPage();
this.focusedItemIndex = this.items.length - 1; this.focusedItemIndex = this.items.length - 1;
this.currentPage = this.pages.length - 1; this.currentPage = this.pages.length - 1;
} } else {
else {
this.focusedItemIndex--; this.focusedItemIndex--;
if (this.focusedItemIndex < this.pages[this.currentPage].start) { if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage(); this.clearPage();
@ -382,8 +408,7 @@ FullMenuView.prototype.focusPrevious = function() {
FullMenuView.super_.prototype.focusPrevious.call(this); FullMenuView.super_.prototype.focusPrevious.call(this);
}; };
FullMenuView.prototype.focusPreviousColumn = function() { FullMenuView.prototype.focusPreviousColumn = function () {
const currentRow = this.items[this.focusedItemIndex].itemInRow; const currentRow = this.items[this.focusedItemIndex].itemInRow;
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
if (this.focusedItemIndex < 0) { if (this.focusedItemIndex < 0) {
@ -391,15 +416,13 @@ FullMenuView.prototype.focusPreviousColumn = function() {
const lastItemRow = this.items[this.items.length - 1].itemInRow; const lastItemRow = this.items[this.items.length - 1].itemInRow;
if (lastItemRow > currentRow) { if (lastItemRow > currentRow) {
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
} } else {
else {
// can't go to same column, so go to last item // can't go to same column, so go to last item
this.focusedItemIndex = this.items.length - 1; this.focusedItemIndex = this.items.length - 1;
} }
// set to last page // set to last page
this.currentPage = this.pages.length - 1; this.currentPage = this.pages.length - 1;
} } else {
else {
if (this.focusedItemIndex < this.pages[this.currentPage].start) { if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage(); this.clearPage();
this.currentPage--; this.currentPage--;
@ -412,16 +435,14 @@ FullMenuView.prototype.focusPreviousColumn = function() {
FullMenuView.super_.prototype.focusPrevious.call(this); FullMenuView.super_.prototype.focusPrevious.call(this);
}; };
FullMenuView.prototype.focusNextColumn = function() { FullMenuView.prototype.focusNextColumn = function () {
const currentRow = this.items[this.focusedItemIndex].itemInRow; const currentRow = this.items[this.focusedItemIndex].itemInRow;
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow;
if (this.focusedItemIndex > this.items.length - 1) { if (this.focusedItemIndex > this.items.length - 1) {
this.focusedItemIndex = currentRow - 1; this.focusedItemIndex = currentRow - 1;
this.currentPage = 0; this.currentPage = 0;
this.clearPage(); this.clearPage();
} } else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage(); this.clearPage();
this.currentPage++; this.currentPage++;
} }
@ -432,8 +453,7 @@ FullMenuView.prototype.focusNextColumn = function() {
FullMenuView.super_.prototype.focusNext.call(this); FullMenuView.super_.prototype.focusNext.call(this);
}; };
FullMenuView.prototype.focusPreviousPageItem = function() { FullMenuView.prototype.focusPreviousPageItem = function () {
// handle first page // handle first page
if (this.currentPage == 0) { if (this.currentPage == 0) {
// Do nothing, page up shouldn't go down on last page // Do nothing, page up shouldn't go down on last page
@ -449,8 +469,7 @@ FullMenuView.prototype.focusPreviousPageItem = function() {
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
}; };
FullMenuView.prototype.focusNextPageItem = function() { FullMenuView.prototype.focusNextPageItem = function () {
// handle last page // handle last page
if (this.currentPage == this.pages.length - 1) { if (this.currentPage == this.pages.length - 1) {
// Do nothing, page up shouldn't go down on last page // Do nothing, page up shouldn't go down on last page
@ -466,8 +485,7 @@ FullMenuView.prototype.focusNextPageItem = function() {
return FullMenuView.super_.prototype.focusNextPageItem.call(this); return FullMenuView.super_.prototype.focusNextPageItem.call(this);
}; };
FullMenuView.prototype.focusFirst = function() { FullMenuView.prototype.focusFirst = function () {
this.currentPage = 0; this.currentPage = 0;
this.focusedItemIndex = 0; this.focusedItemIndex = 0;
this.clearPage(); this.clearPage();
@ -476,8 +494,7 @@ FullMenuView.prototype.focusFirst = function() {
return FullMenuView.super_.prototype.focusFirst.call(this); return FullMenuView.super_.prototype.focusFirst.call(this);
}; };
FullMenuView.prototype.focusLast = function() { FullMenuView.prototype.focusLast = function () {
this.currentPage = this.pages.length - 1; this.currentPage = this.pages.length - 1;
this.focusedItemIndex = this.pages[this.currentPage].end; this.focusedItemIndex = this.pages[this.currentPage].end;
this.clearPage(); this.clearPage();
@ -486,25 +503,24 @@ FullMenuView.prototype.focusLast = function() {
return FullMenuView.super_.prototype.focusLast.call(this); return FullMenuView.super_.prototype.focusLast.call(this);
}; };
FullMenuView.prototype.setFocusItems = function(items) { FullMenuView.prototype.setFocusItems = function (items) {
FullMenuView.super_.prototype.setFocusItems.call(this, items); FullMenuView.super_.prototype.setFocusItems.call(this, items);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
FullMenuView.prototype.setItemSpacing = function(itemSpacing) { FullMenuView.prototype.setItemSpacing = function (itemSpacing) {
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
FullMenuView.prototype.setJustify = function(justify) { FullMenuView.prototype.setJustify = function (justify) {
FullMenuView.super_.prototype.setJustify.call(this, justify); FullMenuView.super_.prototype.setJustify.call(this, justify);
this.positionCacheExpired = true; this.positionCacheExpired = true;
}; };
FullMenuView.prototype.setItemHorizSpacing = function (itemHorizSpacing) {
FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
this.positionCacheExpired = true; this.positionCacheExpired = true;

View File

@ -17,7 +17,7 @@ exports.HorizontalMenuView = HorizontalMenuView;
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

@ -18,9 +18,9 @@ module.exports = class KeyEntryView extends View {
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 (
drawKey &&
isPrintable(drawKey) &&
(!this.keys || this.keys.indexOf(ch) > -1)
) {
this.redraw(); // sets position 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

@ -16,14 +16,14 @@ 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 {
@ -31,45 +31,61 @@ exports.getModule = class LastCallersModule extends MenuModule {
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(
loginHistory,
(err, updatedHistory) => {
return callback(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,15 +95,17 @@ 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);
} }
@ -96,48 +114,55 @@ exports.getModule = class LastCallersModule extends MenuModule {
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,14 +171,16 @@ 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());
} }
@ -167,20 +194,24 @@ 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(
loginHistory,
(item, nextHistoryItem) => {
User.getUserName(item.userId, (err, userName) => { User.getUserName(item.userId, (err, userName) => {
if(err) { if (err) {
return nextHistoryItem(null, null); return nextHistoryItem(null, null);
} }
@ -188,10 +219,11 @@ exports.getModule = class LastCallersModule extends MenuModule {
User.loadProperties(item.userId, getPropOpts, (err, props) => { User.loadProperties(item.userId, getPropOpts, (err, props) => {
item.location = (props && props[UserProps.Location]) || ''; item.location = (props && props[UserProps.Location]) || '';
item.affiliation = item.affils = (props && props[UserProps.Affiliations]) || ''; item.affiliation = item.affils =
(props && props[UserProps.Affiliations]) || '';
item.realName = (props && props[UserProps.RealName]) || ''; item.realName = (props && props[UserProps.RealName]) || '';
if(!indicatorSumsSql) { if (!indicatorSumsSql) {
return nextHistoryItem(null, item); return nextHistoryItem(null, item);
} }
@ -200,12 +232,16 @@ exports.getModule = class LastCallersModule extends MenuModule {
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 =
results[n] > 0
? this.actionIndicators[n] ||
this.actionIndicatorDefault
: this.actionIndicatorDefault;
item[n] = indicator; item[n] = indicator;
item.actions += indicator; item.actions += indicator;
}); });
@ -217,7 +253,11 @@ exports.getModule = class LastCallersModule extends MenuModule {
}); });
}, },
(err, mapped) => { (err, mapped) => {
return cb(err, mapped.filter(item => item)); // remove deleted return cb(
}); err,
mapped.filter(item => item)
); // remove deleted
}
);
} }
}; };

View File

@ -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'],
(category, next) => {
moduleUtil.loadModulesForCategory(
`${category}Servers`,
(module, nextModule) => {
const moduleInst = new module.getModule(); const moduleInst = new module.getModule();
try { try {
moduleInst.createServer(err => { moduleInst.createServer(err => {
if(err) { if (err) {
return nextModule(err); return nextModule(err);
} }
moduleInst.listen( err => { moduleInst.listen(err => {
if(err) { if (err) {
return nextModule(err); return nextModule(err);
} }
listeningServers[module.moduleInfo.packageName] = { listeningServers[module.moduleInfo.packageName] = {
instance : moduleInst, instance: moduleInst,
info : module.moduleInfo, info: module.moduleInfo,
}; };
return nextModule(null); return nextModule(null);
}); });
}); });
} catch(e) { } catch (e) {
logger.log.error(e, 'Exception caught creating server!'); logger.log.error(e, 'Exception caught creating server!');
return nextModule(e); return nextModule(e);
} }
}, err => { },
err => {
return next(err); return next(err);
}); }
}, err => { );
},
err => {
return cb(err); return cb(err);
}); }
);
} }

View File

@ -8,48 +8,50 @@ 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(
/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/,
(match, valueName) => {
return `"${valueName}":"********"`; return `"${valueName}":"********"`;
}) }
)
); );
} catch(e) { } catch (e) {
// be safe and return empty obj! // be safe and return empty obj!
return {}; return {};
} }

View File

@ -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

@ -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,7 +26,7 @@ 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
// //

View File

@ -6,7 +6,8 @@ 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

@ -38,50 +38,62 @@ function MaskEditTextView(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,7 +162,7 @@ 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
@ -154,43 +171,48 @@ MaskEditTextView.prototype.onKeyPress = function(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);
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;
} }
@ -199,8 +221,8 @@ MaskEditTextView.prototype.getData = function() {
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) {
@ -36,7 +36,7 @@ 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;
} }

View File

@ -13,7 +13,8 @@ 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');
@ -28,7 +29,18 @@ function MCIViewFactory(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,9 +95,9 @@ 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);
@ -92,7 +106,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
break; break;
// Edit Text // Edit Text
case 'ET' : case 'ET':
setWidth(0); setWidth(0);
setOption(1, 'textStyle'); setOption(1, 'textStyle');
@ -102,7 +116,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
break; break;
// Masked Edit Text // Masked Edit Text
case 'ME' : case 'ME':
setOption(0, 'textStyle'); setOption(0, 'textStyle');
setFocusOption(0, 'focusTextStyle'); setFocusOption(0, 'focusTextStyle');
@ -110,17 +124,17 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
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);
@ -131,9 +145,9 @@ 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');
@ -145,7 +159,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
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');
@ -156,7 +170,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
break; break;
// Horizontal Menu // Horizontal Menu
case 'HM' : case 'HM':
setOption(0, 'itemSpacing'); setOption(0, 'itemSpacing');
setOption(1, 'textStyle'); setOption(1, 'textStyle');
@ -166,7 +180,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
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');
@ -177,7 +191,7 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
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');
@ -186,10 +200,10 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
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);
@ -200,18 +214,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
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');
@ -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

@ -8,7 +8,8 @@ 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 =
require('../core/multi_line_edit_text_view.js').MultiLineEditTextView;
const Errors = require('../core/enig_error.js').Errors; const Errors = require('../core/enig_error.js').Errors;
const { getPredefinedMCIValue } = require('../core/predefined_mci.js'); const { getPredefinedMCIValue } = require('../core/predefined_mci.js');
@ -19,7 +20,6 @@ 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);
@ -30,18 +30,22 @@ exports.MenuModule = class MenuModule extends PluginModule {
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',
}; };
} }
@ -56,11 +60,14 @@ 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,13 +87,16 @@ 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;
} }
@ -95,17 +105,21 @@ exports.MenuModule = class MenuModule extends PluginModule {
); );
}, },
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,7 +127,7 @@ 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;
} }
@ -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(
promptLoadOpts,
err => {
return callback(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 },
(err, artData) => {
if(cb) {
return cb(err, artData); 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,
(err, artData) => {
if(err) {
return cb(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);
@ -589,14 +632,23 @@ exports.MenuModule = class MenuModule extends PluginModule {
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 (
customMciId <= endId &&
(textView = this.viewControllers[formName].getView(customMciId))
) {
const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10" const key = `${formName}InfoFormat${customMciId}`; // e.g. "mainInfoFormat10"
const format = config[key]; 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,8 +701,8 @@ 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;
@ -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

@ -3,13 +3,8 @@
// 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');
@ -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];
} }
} }
@ -57,18 +52,30 @@ module.exports = class MenuStack {
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) {
@ -79,23 +86,25 @@ module.exports = class MenuStack {
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

@ -24,7 +24,7 @@ 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,11 +275,11 @@ 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
} }
@ -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

@ -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,15 +31,21 @@ 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 || '';
@ -47,38 +53,39 @@ function MenuView(options) {
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,22 +246,20 @@ 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));
@ -262,7 +267,7 @@ MenuView.prototype.setItemSpacing = function(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));
@ -270,48 +275,70 @@ MenuView.prototype.setItemHorizSpacing = function(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) {
} }
} }
}; };

View File

@ -7,17 +7,16 @@ const ftnUtil = require('./ftn_util.js');
const createNamedUUID = require('./uuid_util.js').createNamedUUID; const createNamedUUID = require('./uuid_util.js').createNamedUUID;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const ANSI = require('./ansi_term.js'); const ANSI = require('./ansi_term.js');
const { const { sanitizeString, getISOTimestampString } = require('./database.js');
sanitizeString,
getISOTimestampString } = require('./database.js');
const { isCP437Encodable } = require('./cp437util'); const { isCP437Encodable } = require('./cp437util');
const { containsNonLatinCodepoints } = require('./string_util'); const { containsNonLatinCodepoints } = require('./string_util');
const { const {
isAnsi, isFormattedLine, isAnsi,
isFormattedLine,
splitTextAtTerms, splitTextAtTerms,
renderSubstr renderSubstr,
} = require('./string_util.js'); } = require('./string_util.js');
const ansiPrep = require('./ansi_prep.js'); const ansiPrep = require('./ansi_prep.js');
@ -30,83 +29,84 @@ const assert = require('assert');
const moment = require('moment'); const moment = require('moment');
const iconvEncode = require('iconv-lite').encode; const iconvEncode = require('iconv-lite').encode;
const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse('154506df-1df8-46b9-98f8-ebb5815baaf8'); const ENIGMA_MESSAGE_UUID_NAMESPACE = uuidParse.parse(
'154506df-1df8-46b9-98f8-ebb5815baaf8'
);
const WELL_KNOWN_AREA_TAGS = { const WELL_KNOWN_AREA_TAGS = {
Invalid : '', Invalid: '',
Private : 'private_mail', Private: 'private_mail',
Bulletin : 'local_bulletin', Bulletin: 'local_bulletin',
}; };
const SYSTEM_META_NAMES = { const SYSTEM_META_NAMES = {
LocalToUserID : 'local_to_user_id', LocalToUserID: 'local_to_user_id',
LocalFromUserID : 'local_from_user_id', LocalFromUserID: 'local_from_user_id',
StateFlags0 : 'state_flags0', // See Message.StateFlags0 StateFlags0: 'state_flags0', // See Message.StateFlags0
ExplicitEncoding : 'explicit_encoding', // Explicitly set encoding when exporting/etc. ExplicitEncoding: 'explicit_encoding', // Explicitly set encoding when exporting/etc.
ExternalFlavor : 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor ExternalFlavor: 'external_flavor', // "Flavor" of message - imported from or to be exported to. See Message.AddressFlavor
RemoteToUser : 'remote_to_user', // Opaque value depends on external system, e.g. FTN address RemoteToUser: 'remote_to_user', // Opaque value depends on external system, e.g. FTN address
RemoteFromUser : 'remote_from_user', // Opaque value depends on external system, e.g. FTN address RemoteFromUser: 'remote_from_user', // Opaque value depends on external system, e.g. FTN address
}; };
// Types for Message.SystemMetaNames.ExternalFlavor meta // Types for Message.SystemMetaNames.ExternalFlavor meta
const ADDRESS_FLAVOR = { const ADDRESS_FLAVOR = {
Local : 'local', // local / non-remote addressing Local: 'local', // local / non-remote addressing
FTN : 'ftn', // FTN style FTN: 'ftn', // FTN style
Email : 'email', // From email Email: 'email', // From email
QWK : 'qwk', // QWK packet QWK: 'qwk', // QWK packet
}; };
const STATE_FLAGS0 = { const STATE_FLAGS0 = {
None : 0x00000000, None: 0x00000000,
Imported : 0x00000001, // imported from foreign system Imported: 0x00000001, // imported from foreign system
Exported : 0x00000002, // exported to foreign system Exported: 0x00000002, // exported to foreign system
}; };
// :TODO: these should really live elsewhere... // :TODO: these should really live elsewhere...
const FTN_PROPERTY_NAMES = { const FTN_PROPERTY_NAMES = {
// packet header oriented // packet header oriented
FtnOrigNode : 'ftn_orig_node', FtnOrigNode: 'ftn_orig_node',
FtnDestNode : 'ftn_dest_node', FtnDestNode: 'ftn_dest_node',
// :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping // :TODO: rename these to ftn_*_net vs network - ensure things won't break, may need mapping
FtnOrigNetwork : 'ftn_orig_network', FtnOrigNetwork: 'ftn_orig_network',
FtnDestNetwork : 'ftn_dest_network', FtnDestNetwork: 'ftn_dest_network',
FtnAttrFlags : 'ftn_attr_flags', FtnAttrFlags: 'ftn_attr_flags',
FtnCost : 'ftn_cost', FtnCost: 'ftn_cost',
FtnOrigZone : 'ftn_orig_zone', FtnOrigZone: 'ftn_orig_zone',
FtnDestZone : 'ftn_dest_zone', FtnDestZone: 'ftn_dest_zone',
FtnOrigPoint : 'ftn_orig_point', FtnOrigPoint: 'ftn_orig_point',
FtnDestPoint : 'ftn_dest_point', FtnDestPoint: 'ftn_dest_point',
// message header oriented // message header oriented
FtnMsgOrigNode : 'ftn_msg_orig_node', FtnMsgOrigNode: 'ftn_msg_orig_node',
FtnMsgDestNode : 'ftn_msg_dest_node', FtnMsgDestNode: 'ftn_msg_dest_node',
FtnMsgOrigNet : 'ftn_msg_orig_net', FtnMsgOrigNet: 'ftn_msg_orig_net',
FtnMsgDestNet : 'ftn_msg_dest_net', FtnMsgDestNet: 'ftn_msg_dest_net',
FtnAttribute : 'ftn_attribute', FtnAttribute: 'ftn_attribute',
FtnTearLine : 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001 FtnTearLine: 'ftn_tear_line', // http://ftsc.org/docs/fts-0004.001
FtnOrigin : 'ftn_origin', // http://ftsc.org/docs/fts-0004.001 FtnOrigin: 'ftn_origin', // http://ftsc.org/docs/fts-0004.001
FtnArea : 'ftn_area', // http://ftsc.org/docs/fts-0004.001 FtnArea: 'ftn_area', // http://ftsc.org/docs/fts-0004.001
FtnSeenBy : 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001 FtnSeenBy: 'ftn_seen_by', // http://ftsc.org/docs/fts-0004.001
}; };
const QWKPropertyNames = { const QWKPropertyNames = {
MessageNumber : 'qwk_msg_num', MessageNumber: 'qwk_msg_num',
MessageStatus : 'qwk_msg_status', // See http://wiki.synchro.net/ref:qwk for a decent list MessageStatus: 'qwk_msg_status', // See http://wiki.synchro.net/ref:qwk for a decent list
ConferenceNumber : 'qwk_conf_num', ConferenceNumber: 'qwk_conf_num',
InReplyToNum : 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available InReplyToNum: 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available
}; };
// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! // :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)!
const MESSAGE_ROW_MAP = { const MESSAGE_ROW_MAP = {
reply_to_message_id : 'replyToMsgId', reply_to_message_id: 'replyToMsgId',
modified_timestamp : 'modTimestamp' modified_timestamp: 'modTimestamp',
}; };
module.exports = class Message { module.exports = class Message {
constructor( constructor({
{
messageId = 0, messageId = 0,
areaTag = Message.WellKnownAreaTags.Invalid, areaTag = Message.WellKnownAreaTags.Invalid,
uuid, uuid,
@ -118,9 +118,7 @@ module.exports = class Message {
modTimestamp = moment(), modTimestamp = moment(),
meta, meta,
hashTags = [], hashTags = [],
} = { } } = {}) {
)
{
this.messageId = messageId; this.messageId = messageId;
this.areaTag = areaTag; this.areaTag = areaTag;
this.messageUuid = uuid; this.messageUuid = uuid;
@ -130,23 +128,26 @@ module.exports = class Message {
this.subject = subject; this.subject = subject;
this.message = message; this.message = message;
if(_.isDate(modTimestamp) || _.isString(modTimestamp)) { if (_.isDate(modTimestamp) || _.isString(modTimestamp)) {
modTimestamp = moment(modTimestamp); modTimestamp = moment(modTimestamp);
} }
this.modTimestamp = modTimestamp || moment(); this.modTimestamp = modTimestamp || moment();
this.meta = {}; this.meta = {};
_.defaultsDeep(this.meta, { System : {} }, meta); _.defaultsDeep(this.meta, { System: {} }, meta);
this.hashTags = hashTags; this.hashTags = hashTags;
} }
get uuid() { // deprecated, will be removed in the near future get uuid() {
// deprecated, will be removed in the near future
return this.messageUuid; return this.messageUuid;
} }
isValid() { return true; } // :TODO: obviously useless; look into this or remove it isValid() {
return true;
} // :TODO: obviously useless; look into this or remove it
static isPrivateAreaTag(areaTag) { static isPrivateAreaTag(areaTag) {
return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private; return areaTag.toLowerCase() === Message.WellKnownAreaTags.Private;
@ -161,17 +162,21 @@ module.exports = class Message {
} }
isCP437Encodable() { isCP437Encodable() {
return isCP437Encodable(this.toUserName) && return (
isCP437Encodable(this.toUserName) &&
isCP437Encodable(this.fromUserName) && isCP437Encodable(this.fromUserName) &&
isCP437Encodable(this.subject) && isCP437Encodable(this.subject) &&
isCP437Encodable(this.message); isCP437Encodable(this.message)
);
} }
containsNonLatinCodepoints() { containsNonLatinCodepoints() {
return containsNonLatinCodepoints(this.toUserName) || return (
containsNonLatinCodepoints(this.toUserName) ||
containsNonLatinCodepoints(this.fromUserName) || containsNonLatinCodepoints(this.fromUserName) ||
containsNonLatinCodepoints(this.subject) || containsNonLatinCodepoints(this.subject) ||
containsNonLatinCodepoints(this.message); containsNonLatinCodepoints(this.message)
);
} }
/* /*
@ -196,7 +201,9 @@ module.exports = class Message {
*/ */
userHasDeleteRights(user) { userHasDeleteRights(user) {
const messageLocalUserId = parseInt(this.meta.System[Message.SystemMetaNames.LocalToUserID]); const messageLocalUserId = parseInt(
this.meta.System[Message.SystemMetaNames.LocalToUserID]
);
return (this.isPrivate() && user.userId === messageLocalUserId) || user.isSysOp(); return (this.isPrivate() && user.userId === messageLocalUserId) || user.isSysOp();
} }
@ -250,16 +257,24 @@ module.exports = class Message {
assert(_.isString(subject)); assert(_.isString(subject));
assert(_.isString(body)); assert(_.isString(body));
if(!moment.isMoment(modTimestamp)) { if (!moment.isMoment(modTimestamp)) {
modTimestamp = moment(modTimestamp); modTimestamp = moment(modTimestamp);
} }
areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437'); areaTag = iconvEncode(areaTag.toUpperCase(), 'CP437');
modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437'); modTimestamp = iconvEncode(modTimestamp.format('DD MMM YY HH:mm:ss'), 'CP437');
subject = iconvEncode(subject.toUpperCase().trim(), 'CP437'); subject = iconvEncode(subject.toUpperCase().trim(), 'CP437');
body = iconvEncode(body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(), 'CP437'); body = iconvEncode(
body.replace(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g, '').trim(),
'CP437'
);
return uuidParse.unparse(createNamedUUID(ENIGMA_MESSAGE_UUID_NAMESPACE, Buffer.concat( [ areaTag, modTimestamp, subject, body ] ))); return uuidParse.unparse(
createNamedUUID(
ENIGMA_MESSAGE_UUID_NAMESPACE,
Buffer.concat([areaTag, modTimestamp, subject, body])
)
);
} }
static getMessageFromRow(row) { static getMessageFromRow(row) {
@ -312,27 +327,36 @@ module.exports = class Message {
filter.extraFields = filter.extraFields || []; filter.extraFields = filter.extraFields || [];
filter.operator = filter.operator || 'AND'; filter.operator = filter.operator || 'AND';
if('messageList' === filter.resultType) { if ('messageList' === filter.resultType) {
filter.extraFields = _.uniq(filter.extraFields.concat( filter.extraFields = _.uniq(
[ 'area_tag', 'message_uuid', 'reply_to_message_id', 'to_user_name', 'from_user_name', 'subject', 'modified_timestamp' ] filter.extraFields.concat([
)); 'area_tag',
'message_uuid',
'reply_to_message_id',
'to_user_name',
'from_user_name',
'subject',
'modified_timestamp',
])
);
} }
const field = 'uuid' === filter.resultType ? 'message_uuid' : 'message_id'; const field = 'uuid' === filter.resultType ? 'message_uuid' : 'message_id';
if(moment.isMoment(filter.newerThanTimestamp)) { if (moment.isMoment(filter.newerThanTimestamp)) {
filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp); filter.newerThanTimestamp = getISOTimestampString(filter.newerThanTimestamp);
} }
let sql; let sql;
if('count' === filter.resultType) { if ('count' === filter.resultType) {
sql = sql = `SELECT COUNT() AS count
`SELECT COUNT() AS count
FROM message m`; FROM message m`;
} else { } else {
sql = sql = `SELECT DISTINCT m.${field}${
`SELECT DISTINCT m.${field}${filter.extraFields.length > 0 ? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ') : ''} filter.extraFields.length > 0
? ', ' + filter.extraFields.map(f => `m.${f}`).join(', ')
: ''
}
FROM message m`; FROM message m`;
} }
@ -341,7 +365,7 @@ module.exports = class Message {
let sqlWhere = ''; let sqlWhere = '';
function appendWhereClause(clause, op) { function appendWhereClause(clause, op) {
if(sqlWhere) { if (sqlWhere) {
sqlWhere += ` ${op || filter.operator} `; sqlWhere += ` ${op || filter.operator} `;
} else { } else {
sqlWhere += ' WHERE '; sqlWhere += ' WHERE ';
@ -350,40 +374,41 @@ module.exports = class Message {
} }
// currently only avail sort // currently only avail sort
if('modTimestamp' === filter.sort) { if ('modTimestamp' === filter.sort) {
sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`; sqlOrderBy = `ORDER BY m.modified_timestamp ${sqlOrderDir}`;
} else { } else {
sqlOrderBy = `ORDER BY m.message_id ${sqlOrderDir}`; sqlOrderBy = `ORDER BY m.message_id ${sqlOrderDir}`;
} }
if(Array.isArray(filter.ids)) { if (Array.isArray(filter.ids)) {
appendWhereClause(`m.message_id IN (${filter.ids.join(', ')})`); appendWhereClause(`m.message_id IN (${filter.ids.join(', ')})`);
} }
if(Array.isArray(filter.uuids)) { if (Array.isArray(filter.uuids)) {
const uuidList = filter.uuids.map(u => `"${u}"`).join(', '); const uuidList = filter.uuids.map(u => `"${u}"`).join(', ');
appendWhereClause(`m.message_id IN (${uuidList})`); appendWhereClause(`m.message_id IN (${uuidList})`);
} }
if (_.isNumber(filter.privateTagUserId)) {
if(_.isNumber(filter.privateTagUserId)) {
appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`); appendWhereClause(`m.area_tag = "${Message.WellKnownAreaTags.Private}"`);
appendWhereClause( appendWhereClause(
`m.message_id IN ( `m.message_id IN (
SELECT message_id SELECT message_id
FROM message_meta FROM message_meta
WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId} WHERE meta_category = "System" AND meta_name = "${Message.SystemMetaNames.LocalToUserID}" AND meta_value = ${filter.privateTagUserId}
)`); )`
);
} else { } else {
if(filter.areaTag && filter.areaTag.length > 0) { if (filter.areaTag && filter.areaTag.length > 0) {
if (!Array.isArray(filter.areaTag)) { if (!Array.isArray(filter.areaTag)) {
filter.areaTag = [ filter.areaTag ]; filter.areaTag = [filter.areaTag];
} }
const areaList = filter.areaTag const areaList = filter.areaTag
.filter(t => t !== Message.WellKnownAreaTags.Private) .filter(t => t !== Message.WellKnownAreaTags.Private)
.map(t => `"${t}"`).join(', '); .map(t => `"${t}"`)
if(areaList.length > 0) { .join(', ');
if (areaList.length > 0) {
appendWhereClause(`m.area_tag IN(${areaList})`); appendWhereClause(`m.area_tag IN(${areaList})`);
} else { } else {
// nothing to do; no areas remain // nothing to do; no areas remain
@ -391,42 +416,59 @@ module.exports = class Message {
} }
} else { } else {
// explicit exclude of Private // explicit exclude of Private
appendWhereClause(`m.area_tag != "${Message.WellKnownAreaTags.Private}"`, 'AND'); appendWhereClause(
`m.area_tag != "${Message.WellKnownAreaTags.Private}"`,
'AND'
);
} }
} }
if(_.isNumber(filter.replyToMessageId)) { if (_.isNumber(filter.replyToMessageId)) {
appendWhereClause(`m.reply_to_message_id=${filter.replyToMessageId}`); appendWhereClause(`m.reply_to_message_id=${filter.replyToMessageId}`);
} }
[ 'toUserName', 'fromUserName' ].forEach(field => { ['toUserName', 'fromUserName'].forEach(field => {
let val = filter[field]; let val = filter[field];
if(!val) { if (!val) {
return; // next item return; // next item
} }
if(_.isString(val)) { if (_.isString(val)) {
val = [ val ]; val = [val];
} }
if(Array.isArray(val)) { if (Array.isArray(val)) {
val = '(' + val.map(v => { val =
'(' +
val
.map(v => {
return `m.${_.snakeCase(field)} LIKE "${sanitizeString(v)}"`; return `m.${_.snakeCase(field)} LIKE "${sanitizeString(v)}"`;
}).join(' OR ') + ')'; })
.join(' OR ') +
')';
appendWhereClause(val); appendWhereClause(val);
} }
}); });
if(_.isString(filter.newerThanTimestamp) && filter.newerThanTimestamp.length > 0) { if (
_.isString(filter.newerThanTimestamp) &&
filter.newerThanTimestamp.length > 0
) {
// :TODO: should be using "localtime" here? // :TODO: should be using "localtime" here?
appendWhereClause(`DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`); appendWhereClause(
} else if(moment.isMoment(filter.date)) { `DATETIME(m.modified_timestamp) > DATETIME("${filter.newerThanTimestamp}", "+1 seconds")`
appendWhereClause(`DATE(m.modified_timestamp, "localtime") = DATE("${filter.date.format('YYYY-MM-DD')}")`); );
} else if (moment.isMoment(filter.date)) {
appendWhereClause(
`DATE(m.modified_timestamp, "localtime") = DATE("${filter.date.format(
'YYYY-MM-DD'
)}")`
);
} }
if(_.isNumber(filter.newerThanMessageId)) { if (_.isNumber(filter.newerThanMessageId)) {
appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`); appendWhereClause(`m.message_id > ${filter.newerThanMessageId}`);
} }
if(filter.terms && filter.terms.length > 0) { if (filter.terms && filter.terms.length > 0) {
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
appendWhereClause( appendWhereClause(
`m.message_id IN ( `m.message_id IN (
@ -437,10 +479,14 @@ module.exports = class Message {
); );
} }
if(Array.isArray(filter.metaTuples)) { if (Array.isArray(filter.metaTuples)) {
let sub = []; let sub = [];
filter.metaTuples.forEach(mt => { filter.metaTuples.forEach(mt => {
sub.push(`(meta_category = "${mt.category}" AND meta_name = "${mt.name}" AND meta_value = "${sanitizeString(mt.value)}")`); sub.push(
`(meta_category = "${mt.category}" AND meta_name = "${
mt.name
}" AND meta_value = "${sanitizeString(mt.value)}")`
);
}); });
sub = sub.join(` ${filter.operator} `); sub = sub.join(` ${filter.operator} `);
appendWhereClause( appendWhereClause(
@ -454,13 +500,13 @@ module.exports = class Message {
sql += `${sqlWhere} ${sqlOrderBy}`; sql += `${sqlWhere} ${sqlOrderBy}`;
if(_.isNumber(filter.limit)) { if (_.isNumber(filter.limit)) {
sql += ` LIMIT ${filter.limit}`; sql += ` LIMIT ${filter.limit}`;
} }
sql += ';'; sql += ';';
if('count' === filter.resultType) { if ('count' === filter.resultType) {
msgDb.get(sql, (err, row) => { msgDb.get(sql, (err, row) => {
return cb(err, row ? row.count : 0); return cb(err, row ? row.count : 0);
}); });
@ -468,15 +514,22 @@ module.exports = class Message {
const matches = []; const matches = [];
const extra = filter.extraFields.length > 0; const extra = filter.extraFields.length > 0;
const rowConv = 'messageList' === filter.resultType ? Message.getMessageFromRow : row => row; const rowConv =
'messageList' === filter.resultType
? Message.getMessageFromRow
: row => row;
msgDb.each(sql, (err, row) => { msgDb.each(
if(_.isObject(row)) { sql,
(err, row) => {
if (_.isObject(row)) {
matches.push(extra ? rowConv(row) : row[field]); matches.push(extra ? rowConv(row) : row[field]);
} }
}, err => { },
err => {
return cb(err, matches); return cb(err, matches);
}); }
);
} }
} }
@ -487,13 +540,13 @@ module.exports = class Message {
FROM message FROM message
WHERE message_uuid = ? WHERE message_uuid = ?
LIMIT 1;`, LIMIT 1;`,
[ uuid ], [uuid],
(err, row) => { (err, row) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
const success = (row && row.message_id); const success = row && row.message_id;
return cb( return cb(
success ? null : Errors.DoesNotExist(`No message for UUID ${uuid}`), success ? null : Errors.DoesNotExist(`No message for UUID ${uuid}`),
success ? row.message_id : null success ? row.message_id : null
@ -508,37 +561,42 @@ module.exports = class Message {
`SELECT message_id `SELECT message_id
FROM message_meta FROM message_meta
WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`, WHERE meta_category = ? AND meta_name = ? AND meta_value = ?;`,
[ category, name, value ], [category, name, value],
(err, rows) => { (err, rows) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
return cb(null, rows.map(r => parseInt(r.message_id))); // return array of ID(s) return cb(
null,
rows.map(r => parseInt(r.message_id))
); // return array of ID(s)
} }
); );
} }
static getMetaValuesByMessageId(messageId, category, name, cb) { static getMetaValuesByMessageId(messageId, category, name, cb) {
const sql = const sql = `SELECT meta_value
`SELECT meta_value
FROM message_meta FROM message_meta
WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`; WHERE message_id = ? AND meta_category = ? AND meta_name = ?;`;
msgDb.all(sql, [ messageId, category, name ], (err, rows) => { msgDb.all(sql, [messageId, category, name], (err, rows) => {
if(err) { if (err) {
return cb(err); return cb(err);
} }
if(0 === rows.length) { if (0 === rows.length) {
return cb(Errors.DoesNotExist('No value for category/name')); return cb(Errors.DoesNotExist('No value for category/name'));
} }
// single values are returned without an array // single values are returned without an array
if(1 === rows.length) { if (1 === rows.length) {
return cb(null, rows[0].meta_value); return cb(null, rows[0].meta_value);
} }
return cb(null, rows.map(r => r.meta_value)); // map to array of values only return cb(
null,
rows.map(r => r.meta_value)
); // map to array of values only
}); });
} }
@ -551,10 +609,15 @@ module.exports = class Message {
}); });
}, },
function getMetaValues(messageId, callback) { function getMetaValues(messageId, callback) {
Message.getMetaValuesByMessageId(messageId, category, name, (err, values) => { Message.getMetaValuesByMessageId(
messageId,
category,
name,
(err, values) => {
return callback(err, values); return callback(err, values);
});
} }
);
},
], ],
(err, values) => { (err, values) => {
return cb(err, values); return cb(err, values);
@ -575,30 +638,36 @@ module.exports = class Message {
} }
} }
*/ */
const sql = const sql = `SELECT meta_category, meta_name, meta_value
`SELECT meta_category, meta_name, meta_value
FROM message_meta FROM message_meta
WHERE message_id = ?;`; WHERE message_id = ?;`;
const self = this; // :TODO: not required - arrow functions below: const self = this; // :TODO: not required - arrow functions below:
msgDb.each(sql, [ this.messageId ], (err, row) => { msgDb.each(
if(!(row.meta_category in self.meta)) { sql,
self.meta[row.meta_category] = { }; [this.messageId],
(err, row) => {
if (!(row.meta_category in self.meta)) {
self.meta[row.meta_category] = {};
self.meta[row.meta_category][row.meta_name] = row.meta_value; self.meta[row.meta_category][row.meta_name] = row.meta_value;
} else { } else {
if(!(row.meta_name in self.meta[row.meta_category])) { if (!(row.meta_name in self.meta[row.meta_category])) {
self.meta[row.meta_category][row.meta_name] = row.meta_value; self.meta[row.meta_category][row.meta_name] = row.meta_value;
} else { } else {
if(_.isString(self.meta[row.meta_category][row.meta_name])) { if (_.isString(self.meta[row.meta_category][row.meta_name])) {
self.meta[row.meta_category][row.meta_name] = [ self.meta[row.meta_category][row.meta_name] ]; self.meta[row.meta_category][row.meta_name] = [
self.meta[row.meta_category][row.meta_name],
];
} }
self.meta[row.meta_category][row.meta_name].push(row.meta_value); self.meta[row.meta_category][row.meta_name].push(row.meta_value);
} }
} }
}, err => { },
err => {
return cb(err); return cb(err);
}); }
);
} }
load(loadWith, cb) { load(loadWith, cb) {
@ -616,14 +685,16 @@ module.exports = class Message {
FROM message FROM message
WHERE ${whereField} = ? WHERE ${whereField} = ?
LIMIT 1;`, LIMIT 1;`,
[ loadWith.uuid || loadWith.messageId ], [loadWith.uuid || loadWith.messageId],
(err, msgRow) => { (err, msgRow) => {
if(err) { if (err) {
return callback(err); return callback(err);
} }
if(!msgRow) { if (!msgRow) {
return callback(Errors.DoesNotExist('Message (no longer) available')); return callback(
Errors.DoesNotExist('Message (no longer) available')
);
} }
self.messageId = msgRow.message_id; self.messageId = msgRow.message_id;
@ -636,7 +707,9 @@ module.exports = class Message {
self.message = msgRow.message; self.message = msgRow.message;
// We use parseZone() to *preserve* the time zone information // We use parseZone() to *preserve* the time zone information
self.modTimestamp = moment.parseZone(msgRow.modified_timestamp); self.modTimestamp = moment.parseZone(
msgRow.modified_timestamp
);
return callback(err); return callback(err);
} }
@ -650,7 +723,7 @@ module.exports = class Message {
function loadHashTags(callback) { function loadHashTags(callback) {
// :TODO: // :TODO:
return callback(null); return callback(null);
} },
], ],
err => { err => {
return cb(err); return cb(err);
@ -659,32 +732,37 @@ module.exports = class Message {
} }
persistMetaValue(category, name, value, transOrDb, cb) { persistMetaValue(category, name, value, transOrDb, cb) {
if(!_.isFunction(cb) && _.isFunction(transOrDb)) { if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb; cb = transOrDb;
transOrDb = msgDb; transOrDb = msgDb;
} }
const metaStmt = transOrDb.prepare( const metaStmt = transOrDb.prepare(
`INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value) `INSERT INTO message_meta (message_id, meta_category, meta_name, meta_value)
VALUES (?, ?, ?, ?);`); VALUES (?, ?, ?, ?);`
);
if(!_.isArray(value)) { if (!_.isArray(value)) {
value = [ value ]; value = [value];
} }
const self = this; const self = this;
async.each(value, (v, next) => { async.each(
value,
(v, next) => {
metaStmt.run(self.messageId, category, name, v, err => { metaStmt.run(self.messageId, category, name, v, err => {
return next(err); return next(err);
}); });
}, err => { },
err => {
return cb(err); return cb(err);
}); }
);
} }
persist(cb) { persist(cb) {
if(!this.isValid()) { if (!this.isValid()) {
return cb(Errors.Invalid('Cannot persist invalid message!')); return cb(Errors.Invalid('Cannot persist invalid message!'));
} }
@ -697,7 +775,7 @@ module.exports = class Message {
}, },
function storeMessage(trans, callback) { function storeMessage(trans, callback) {
// generate a UUID for this message if required (general case) // generate a UUID for this message if required (general case)
if(!self.messageUuid) { if (!self.messageUuid) {
self.messageUuid = Message.createMessageUUID( self.messageUuid = Message.createMessageUUID(
self.areaTag, self.areaTag,
self.modTimestamp, self.modTimestamp,
@ -710,11 +788,18 @@ module.exports = class Message {
`INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp) `INSERT INTO message (area_tag, message_uuid, reply_to_message_id, to_user_name, from_user_name, subject, message, modified_timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?);`, VALUES (?, ?, ?, ?, ?, ?, ?, ?);`,
[ [
self.areaTag, self.messageUuid, self.replyToMsgId, self.toUserName, self.areaTag,
self.fromUserName, self.subject, self.message, getISOTimestampString(self.modTimestamp) self.messageUuid,
self.replyToMsgId,
self.toUserName,
self.fromUserName,
self.subject,
self.message,
getISOTimestampString(self.modTimestamp),
], ],
function inserted(err) { // use non-arrow function for 'this' scope function inserted(err) {
if(!err) { // use non-arrow function for 'this' scope
if (!err) {
self.messageId = this.lastID; self.messageId = this.lastID;
} }
@ -723,7 +808,7 @@ module.exports = class Message {
); );
}, },
function storeMeta(trans, callback) { function storeMeta(trans, callback) {
if(!self.meta) { if (!self.meta) {
return callback(null, trans); return callback(null, trans);
} }
/* /*
@ -738,26 +823,39 @@ module.exports = class Message {
} }
} }
*/ */
async.each(Object.keys(self.meta), (category, nextCat) => { async.each(
async.each(Object.keys(self.meta[category]), (name, nextName) => { Object.keys(self.meta),
self.persistMetaValue(category, name, self.meta[category][name], trans, err => { (category, nextCat) => {
async.each(
Object.keys(self.meta[category]),
(name, nextName) => {
self.persistMetaValue(
category,
name,
self.meta[category][name],
trans,
err => {
return nextName(err); return nextName(err);
}); }
}, err => { );
},
err => {
return nextCat(err); return nextCat(err);
}); }
);
}, err => { },
err => {
return callback(err, trans); return callback(err, trans);
}); }
);
}, },
function storeHashTags(trans, callback) { function storeHashTags(trans, callback) {
// :TODO: hash tag support // :TODO: hash tag support
return callback(null, trans); return callback(null, trans);
} },
], ],
(err, trans) => { (err, trans) => {
if(trans) { if (trans) {
trans[err ? 'rollback' : 'commit'](transErr => { trans[err ? 'rollback' : 'commit'](transErr => {
return cb(err ? err : transErr, self.messageId); return cb(err ? err : transErr, self.messageId);
}); });
@ -769,14 +867,16 @@ module.exports = class Message {
} }
deleteMessage(requestingUser, cb) { deleteMessage(requestingUser, cb) {
if(!this.userHasDeleteRights(requestingUser)) { if (!this.userHasDeleteRights(requestingUser)) {
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')
);
} }
msgDb.run( msgDb.run(
`DELETE FROM message `DELETE FROM message
WHERE message_uuid = ?;`, WHERE message_uuid = ?;`,
[ this.messageUuid ], [this.messageUuid],
err => { err => {
return cb(err); return cb(err);
} }
@ -796,14 +896,18 @@ module.exports = class Message {
} }
getQuoteLines(options, cb) { getQuoteLines(options, cb) {
if(!options.termWidth || !options.termHeight || !options.cols) { if (!options.termWidth || !options.termHeight || !options.cols) {
return cb(Errors.MissingParam()); return cb(Errors.MissingParam());
} }
options.startCol = options.startCol || 1; options.startCol = options.startCol || 1;
options.includePrefix = _.get(options, 'includePrefix', true); options.includePrefix = _.get(options, 'includePrefix', true);
options.ansiResetSgr = options.ansiResetSgr || ANSI.getSGRFromGraphicRendition( { fg : 39, bg : 49 }, true); options.ansiResetSgr =
options.ansiFocusPrefixSgr = options.ansiFocusPrefixSgr || ANSI.getSGRFromGraphicRendition( { intensity : 'bold', fg : 39, bg : 49 } ); options.ansiResetSgr ||
ANSI.getSGRFromGraphicRendition({ fg: 39, bg: 49 }, true);
options.ansiFocusPrefixSgr =
options.ansiFocusPrefixSgr ||
ANSI.getSGRFromGraphicRendition({ intensity: 'bold', fg: 39, bg: 49 });
options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting options.isAnsi = options.isAnsi || isAnsi(this.message); // :TODO: If this.isAnsi, use that setting
/* /*
@ -817,19 +921,23 @@ module.exports = class Message {
Ot> Nu> right after doing so, don't ya think? yeah I think so Ot> Nu> right after doing so, don't ya think? yeah I think so
*/ */
const quotePrefix = options.includePrefix ? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName') : ''; const quotePrefix = options.includePrefix
? this.getFTNQuotePrefix(options.prefixSource || 'fromUserName')
: '';
function getWrapped(text, extraPrefix) { function getWrapped(text, extraPrefix) {
extraPrefix = extraPrefix ? ` ${extraPrefix}` : ''; extraPrefix = extraPrefix ? ` ${extraPrefix}` : '';
const wrapOpts = { const wrapOpts = {
width : options.cols - (quotePrefix.length + extraPrefix.length), width: options.cols - (quotePrefix.length + extraPrefix.length),
tabHandling : 'expand', tabHandling: 'expand',
tabWidth : 4, tabWidth: 4,
}; };
return wordWrapText(text, wrapOpts).wrapped.map( (w, i) => { return wordWrapText(text, wrapOpts).wrapped.map((w, i) => {
return i === 0 ? `${quotePrefix}${w}` : `${quotePrefix}${extraPrefix}${w}`; return i === 0
? `${quotePrefix}${w}`
: `${quotePrefix}${extraPrefix}${w}`;
}); });
} }
@ -838,7 +946,7 @@ module.exports = class Message {
let newLen; let newLen;
const total = line.length + quotePrefix.length; const total = line.length + quotePrefix.length;
if(total > options.cols) { if (total > options.cols) {
newLen = options.cols - total; newLen = options.cols - total;
} else { } else {
newLen = total; newLen = total;
@ -847,16 +955,16 @@ module.exports = class Message {
return `${quotePrefix}${line.slice(0, newLen)}`; return `${quotePrefix}${line.slice(0, newLen)}`;
} }
if(options.isAnsi) { if (options.isAnsi) {
ansiPrep( ansiPrep(
this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF this.message.replace(/\r?\n/g, '\r\n'), // normalized LF -> CRLF
{ {
termWidth : options.termWidth, termWidth: options.termWidth,
termHeight : options.termHeight, termHeight: options.termHeight,
cols : options.cols, cols: options.cols,
rows : 'auto', rows: 'auto',
startCol : options.startCol, startCol: options.startCol,
forceLineTerm : true, forceLineTerm: true,
}, },
(err, prepped) => { (err, prepped) => {
prepped = prepped || this.message; prepped = prepped || this.message;
@ -876,8 +984,17 @@ module.exports = class Message {
split.forEach(l => { split.forEach(l => {
quoteLines.push(`${lastSgr}${l}`); quoteLines.push(`${lastSgr}${l}`);
focusQuoteLines.push(`${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(l, 1, l.length - 1)}`); focusQuoteLines.push(
lastSgr = (l.match(/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/) || [])[0] || ''; // eslint-disable-line no-control-regex `${options.ansiFocusPrefixSgr}>${lastSgr}${renderSubstr(
l,
1,
l.length - 1
)}`
);
lastSgr =
(l.match(
/(?:\x1b\x5b)[?=;0-9]*m(?!.*(?:\x1b\x5b)[?=;0-9]*m)/
) || [])[0] || ''; // eslint-disable-line no-control-regex
}); });
quoteLines[quoteLines.length - 1] += options.ansiResetSgr; quoteLines[quoteLines.length - 1] += options.ansiResetSgr;
@ -894,7 +1011,10 @@ module.exports = class Message {
let tearLinePos = Message.getTearLinePosition(input); let tearLinePos = Message.getTearLinePosition(input);
tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string
input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { input
.slice(0, tearLinePos)
.split(/\r\n\r\n|\n\n/)
.forEach(paragraph => {
// //
// For each paragraph, a state machine: // For each paragraph, a state machine:
// - New line - line // - New line - line
@ -908,7 +1028,7 @@ module.exports = class Message {
let buf = ''; let buf = '';
let quoteMatch; let quoteMatch;
if(quoted.length > 0) { if (quoted.length > 0) {
// //
// Preserve paragraph separation. // Preserve paragraph separation.
// //
@ -920,18 +1040,20 @@ module.exports = class Message {
} }
paragraph.split(/\r?\n/).forEach(line => { paragraph.split(/\r?\n/).forEach(line => {
if(0 === line.trim().length) { if (0 === line.trim().length) {
// see blank line notes above // see blank line notes above
return quoted.push(quotePrefix); return quoted.push(quotePrefix);
} }
quoteMatch = line.match(QUOTE_RE); quoteMatch = line.match(QUOTE_RE);
switch(state) { switch (state) {
case 'line' : case 'line':
if(quoteMatch) { if (quoteMatch) {
if(isFormattedLine(line)) { if (isFormattedLine(line)) {
quoted.push(getFormattedLine(line.replace(/\s/, ''))); quoted.push(
getFormattedLine(line.replace(/\s/, ''))
);
} else { } else {
quoted.push(...getWrapped(buf, quoteMatch[1])); quoted.push(...getWrapped(buf, quoteMatch[1]));
state = 'quote_line'; state = 'quote_line';
@ -942,10 +1064,10 @@ module.exports = class Message {
} }
break; break;
case 'quote_line' : case 'quote_line':
if(quoteMatch) { if (quoteMatch) {
const rem = line.slice(quoteMatch[0].length); const rem = line.slice(quoteMatch[0].length);
if(!buf.startsWith(quoteMatch[0])) { if (!buf.startsWith(quoteMatch[0])) {
quoted.push(...getWrapped(buf, quoteMatch[1])); quoted.push(...getWrapped(buf, quoteMatch[1]));
buf = rem; buf = rem;
} else { } else {
@ -958,12 +1080,13 @@ module.exports = class Message {
} }
break; break;
default : default:
if(isFormattedLine(line)) { if (isFormattedLine(line)) {
quoted.push(getFormattedLine(line)); quoted.push(getFormattedLine(line));
} else { } else {
state = quoteMatch ? 'quote_line' : 'line'; state = quoteMatch ? 'quote_line' : 'line';
buf = 'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any buf =
'line' === state ? line : line.replace(/\s/, ''); // trim *first* leading space, if any
} }
break; break;
} }
@ -972,7 +1095,10 @@ module.exports = class Message {
quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null)); quoted.push(...getWrapped(buf, quoteMatch ? quoteMatch[1] : null));
}); });
input.slice(tearLinePos).split(/\r?\n/).forEach(l => { input
.slice(tearLinePos)
.split(/\r?\n/)
.forEach(l => {
quoted.push(...getWrapped(l)); quoted.push(...getWrapped(l));
}); });

View File

@ -51,22 +51,31 @@ 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 {
@ -131,8 +140,8 @@ 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,25 +243,28 @@ 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,7 +304,7 @@ 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,7 +320,7 @@ 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'));
@ -313,23 +330,28 @@ function changeMessageConference(client, confTag, cb) {
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);
} }
@ -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(
UserProps.MessageAreaTag,
areaTag,
function persisted(err) {
return callback(err, area); 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);
@ -399,13 +437,13 @@ 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);
}); });
@ -457,10 +499,10 @@ function filterMessageListByReadACS(client, messageList) {
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);
@ -559,7 +600,7 @@ function getMessageAreaLastReadId(userId, areaTag, cb) {
'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,11 +623,11 @@ 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
} }
@ -594,18 +635,25 @@ function updateMessageAreaLastReadId(userId, areaTag, messageId, allowOlder, cb)
} 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,17 +833,24 @@ 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(
'main',
FormIds.main,
mciData.menu,
err => {
return callback(err); return callback(err);
}); }
);
}, },
(callback) => { callback => {
this.temptmp = temptmp.createTrackedSession('qwkuserexp'); this.temptmp = temptmp.createTrackedSession('qwkuserexp');
this.temptmp.mkdir({ prefix : 'enigqwkwriter-'}, (err, tempDir) => { this.temptmp.mkdir(
{ prefix: 'enigqwkwriter-' },
(err, tempDir) => {
if (err) { if (err) {
return callback(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,7 +248,9 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
} }
let current = 1; let current = 1;
async.eachSeries(messageIds, (messageId, nextMessageId) => { async.eachSeries(
messageIds,
(messageId, nextMessageId) => {
const message = new Message(); const message = new Message();
message.load({ messageId }, err => { message.load({ messageId }, err => {
if (err) { if (err) {
@ -226,11 +259,13 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
const progress = { const progress = {
message, message,
step : 'message', step: 'message',
total : ++totalExported, total: ++totalExported,
areaCurrent : current, areaCurrent: current,
areaCount : messageIds.length, areaCount: messageIds.length,
status : `${_.truncate(message.subject, { length : 25 })} (${current} / ${messageIds.length})`, status: `${_.truncate(message.subject, {
length: 25,
})} (${current} / ${messageIds.length})`,
}; };
progressHandler(progress, err => { progressHandler(progress, err => {
@ -247,24 +282,25 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
}, },
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,39 +312,49 @@ 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(publicExportAreas, (exportArea, nextExportArea) => { async.eachSeries(
publicExportAreas,
(exportArea, nextExportArea) => {
const area = getMessageAreaByTag(exportArea.areaTag); const area = getMessageAreaByTag(exportArea.areaTag);
const conf = getMessageConferenceByTag(area.confTag); const conf = getMessageConferenceByTag(area.confTag);
if (!area || !conf) { if (!area || !conf) {
// :TODO: remove from user properties - this area does not exist // :TODO: remove from user properties - this area does not exist
this.client.log.warn({ areaTag : exportArea.areaTag }, 'Cannot QWK export area as it does not exist'); this.client.log.warn(
{ areaTag: exportArea.areaTag },
'Cannot QWK export area as it does not exist'
);
return nextExportArea(null); return nextExportArea(null);
} }
if (!hasMessageConfAndAreaRead(this.client, area)) { if (!hasMessageConfAndAreaRead(this.client, area)) {
this.client.log.warn({ areaTag : area.areaTag }, 'Cannot QWK export area due to ACS'); this.client.log.warn(
{ areaTag: area.areaTag },
'Cannot QWK export area due to ACS'
);
return nextExportArea(null); return nextExportArea(null);
} }
const progress = { const progress = {
conf, conf,
area, area,
step : 'next_area', step: 'next_area',
status : `Gathering in ${conf.name} - ${area.name}...`, status: `Gathering in ${conf.name} - ${area.name}...`,
}; };
progressHandler(progress, err => { progressHandler(progress, err => {
@ -317,9 +363,9 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
} }
const filter = { const filter = {
resultType : 'id', resultType: 'id',
areaTag : exportArea.areaTag, areaTag: exportArea.areaTag,
newerThanTimestamp : exportArea.newerThanTimestamp newerThanTimestamp: exportArea.newerThanTimestamp,
}; };
processMessagesWithFilter(filter, err => { processMessagesWithFilter(filter, err => {
@ -329,24 +375,28 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
}, },
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(
exportArea => {
return Object.assign(exportArea, { return Object.assign(exportArea, {
newerThanTimestamp : getISOTimestampString(), newerThanTimestamp: getISOTimestampString(),
});
}); });
}
);
return this.client.user.persistProperty( return this.client.user.persistProperty(
UserProperties.ExportAreas, UserProperties.ExportAreas,

View File

@ -17,21 +17,21 @@ const Message = require('./message.js');
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);
@ -96,34 +103,36 @@ exports.getModule = class MessageBaseSearch extends MenuModule {
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

@ -14,19 +14,19 @@ 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,7 +34,7 @@ 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
} }

View File

@ -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

@ -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,9 +44,8 @@ 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()
@ -54,8 +58,8 @@ function getEnigmaUserAgent() {
} }
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

@ -7,30 +7,36 @@ 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) { tempMessageConfAndAreaSwitch(messageAreaTag, recordPrevious = true) {
messageAreaTag = messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag); messageAreaTag =
if(!messageAreaTag) { messageAreaTag || get(this, 'config.messageAreaTag', this.messageAreaTag);
if (!messageAreaTag) {
return; // nothing to do! return; // nothing to do!
} }
if(recordPrevious) { if (recordPrevious) {
this.prevMessageConfAndArea = { this.prevMessageConfAndArea = {
confTag : this.client.user.properties[UserProps.MessageConfTag], confTag: this.client.user.properties[UserProps.MessageConfTag],
areaTag : this.client.user.properties[UserProps.MessageAreaTag], areaTag: this.client.user.properties[UserProps.MessageAreaTag],
}; };
} }
if(!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) { if (!messageArea.tempChangeMessageConfAndArea(this.client, messageAreaTag)) {
this.client.log.warn( { messageAreaTag : messageArea }, 'Failed to perform temporary message area/conf switch'); this.client.log.warn(
{ messageAreaTag: messageArea },
'Failed to perform temporary message area/conf switch'
);
} }
} }
tempMessageConfAndAreaRestore() { tempMessageConfAndAreaRestore() {
if(this.prevMessageConfAndArea) { if (this.prevMessageConfAndArea) {
this.client.user.properties[UserProps.MessageConfTag] = this.prevMessageConfAndArea.confTag; this.client.user.properties[UserProps.MessageConfTag] =
this.client.user.properties[UserProps.MessageAreaTag] = this.prevMessageConfAndArea.areaTag; this.prevMessageConfAndArea.confTag;
this.client.user.properties[UserProps.MessageAreaTag] =
this.prevMessageConfAndArea.areaTag;
} }
} }
}; };

View File

@ -4,10 +4,7 @@
// 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');
@ -29,10 +26,17 @@ function loadModuleEx(options, cb) {
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
)
);
} }
// //
@ -44,12 +48,12 @@ function loadModuleEx(options, cb) {
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(
{ name: name, path: path, category: category },
function loaded(err, mod) {
return cb(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(
jsModules,
(file, next) => {
loadModule(paths.basename(file, '.js'), category, (err, mod) => { loadModule(paths.basename(file, '.js'), category, (err, mod) => {
if(err) { if (err) {
if(ErrorReasons.Disabled === err.reasonCode) { if (ErrorReasons.Disabled === err.reasonCode) {
Log.debug(err.message); Log.debug(err.message);
} else { } else {
Log.info( { err : err }, 'Failed loading module'); 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); return iterator(mod, next);
}); });
}, err => { },
if(complete) { err => {
if (complete) {
return complete(err); 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) => {
glob('*{.js,/*.js}', { cwd: modulePath }, (err, files) => {
if (err) {
return nextPath(err); return nextPath(err);
} }
const ourPath = paths.join(__dirname, __filename); const ourPath = paths.join(__dirname, __filename);
async.each(files, (moduleName, nextModule) => { async.each(
files,
(moduleName, nextModule) => {
const fullModulePath = paths.join(modulePath, moduleName); const fullModulePath = paths.join(modulePath, moduleName);
if(ourPath === fullModulePath) { if (ourPath === fullModulePath) {
return nextModule(null); return nextModule(null);
} }
try { try {
const mod = require(fullModulePath); const mod = require(fullModulePath);
if(_.isFunction(mod.moduleInitialize)) { if (_.isFunction(mod.moduleInitialize)) {
const initInfo = { const initInfo = {
events : Events, events: Events,
}; };
mod.moduleInitialize(initInfo, err => { mod.moduleInitialize(initInfo, err => {
if(err) { if (err) {
Log.warn( { error : err.message, modulePath : fullModulePath }, 'Error during "moduleInitialize"'); Log.warn(
{
error: err.message,
modulePath: fullModulePath,
},
'Error during "moduleInitialize"'
);
} }
return nextModule(null); return nextModule(null);
}); });
} else { } else {
return nextModule(null); return nextModule(null);
} }
} catch(e) { } catch (e) {
Log.warn( { error : e.message, fullModulePath }, 'Exception during "moduleInitialize"'); Log.warn(
{ error: e.message, fullModulePath },
'Exception during "moduleInitialize"'
);
return nextModule(null); return nextModule(null);
} }
}, },
err => { err => {
return nextPath(err); return nextPath(err);
}); }
);
}); });
}, },
err => { err => {
return cb(err); return cb(err);
}); }
);
} }

View File

@ -4,10 +4,7 @@
// 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,
stripMciColorCodes
} = require('./color_codes.js');
const stringFormat = require('./string_format.js'); const stringFormat = require('./string_format.js');
const StringUtil = require('./string_util.js'); const StringUtil = require('./string_util.js');
const Config = require('./config.js').get; const Config = require('./config.js').get;
@ -19,10 +16,10 @@ 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,26 +78,26 @@ 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);
@ -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) {
@ -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/, '')
.split(' ')
.reduce(function (a, c) {
const cc = Math.floor(Math.random() * 31 + 1)
.toString()
.padStart(2, '0');
a += `|${cc}${c}|00 `; a += `|${cc}${c}|00 `;
return a; return a;
}, '').substr(0, 140).replace(/\\s\|\d*$/, ''); }, '')
.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

@ -12,17 +12,17 @@ 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,26 +117,32 @@ 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
.getSortedAvailMessageAreasByConfTag(
this.client.user.properties[UserProps.MessageConfTag], this.client.user.properties[UserProps.MessageConfTag],
{ client : this.client } { client: this.client }
).map(area => { )
.map(area => {
return { return {
index : index++, index: index++,
areaTag : area.areaTag, areaTag: area.areaTag,
name : area.area.name, name: area.area.name,
text : area.area.name, // standard text: area.area.name, // standard
desc : area.area.desc, desc: area.area.desc,
hasArt : _.isString(area.area.art), hasArt: _.isString(area.area.art),
}; };
}); });
} }

View File

@ -4,16 +4,14 @@
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';

View File

@ -6,9 +6,9 @@ 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

@ -9,9 +9,9 @@ const Message = require('./message.js');
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 {
@ -21,7 +21,7 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
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;
@ -31,7 +31,7 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
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,9 +137,9 @@ 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,
}; };
} }
@ -137,8 +151,8 @@ exports.getModule = class AreaViewFSEModule extends FullScreenEditorModule {
getMenuResult() { getMenuResult() {
return { return {
messageIndex : this.messageIndex, messageIndex: this.messageIndex,
lastMessageReached : this.lastMessageReached, lastMessageReached: this.lastMessageReached,
}; };
} }
}; };

View File

@ -11,15 +11,15 @@ 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,
err => {
if (err) {
this.client.term.pipeWrite(
`\n|00Cannot change conference: ${err.message}\n`
);
return this.prevMenuOnTimeout(1000, cb); return this.prevMenuOnTimeout(1000, cb);
} }
if(conf.hasArt) { if (conf.hasArt) {
const menuOpts = { const menuOpts = {
extraArgs : { extraArgs: {
confTag : conf.confTag, confTag: conf.confTag,
}, },
menuFlags : [ 'popParent', 'noHistory' ] menuFlags: ['popParent', 'noHistory'],
}; };
return this.gotoMenu(this.menuConfig.config.changeConfPreArtMenu || 'changeMessageConfPreArt', menuOpts, cb); return this.gotoMenu(
this.menuConfig.config.changeConfPreArtMenu ||
'changeMessageConfPreArt',
menuOpts,
cb
);
} }
return this.prevMenu(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,25 +117,30 @@ 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
.getSortedAvailMessageConferences(this.client)
.map(conf => {
return { return {
index : index++, index: index++,
confTag : conf.confTag, confTag: conf.confTag,
name : conf.conf.name, name: conf.conf.name,
text : conf.conf.name, text: conf.conf.name,
desc : conf.conf.desc, desc: conf.conf.desc,
areaCount : Object.keys(conf.conf.areas || {}).length, areaCount: Object.keys(conf.conf.areas || {}).length,
hasArt : _.isString(conf.conf.art), hasArt: _.isString(conf.conf.art),
}; };
}); });
} }

View File

@ -5,7 +5,8 @@
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 =
require('./mod_mixins.js').MessageAreaConfTempSwitcher;
const Errors = require('./enig_error.js').Errors; const Errors = require('./enig_error.js').Errors;
const Message = require('./message.js'); const Message = require('./message.js');
const UserProps = require('./user_property.js'); const UserProps = require('./user_property.js');
@ -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,45 +267,66 @@ 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,
self.config.messageAreaTag,
function msgs(err, msgList) {
if (!msgList || 0 === msgList.length) {
return callback(new Error('No messages in area')); 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.client.user.userId,
self.config.messageAreaTag,
function lastRead(err, lastReadId) {
self.lastReadId = lastReadId || 0; self.lastReadId = lastReadId || 0;
return callback(null); // ignore any errors, e.g. missing value 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 =
self.menuConfig.config.dateTimeFormat ||
self.client.currentTheme.helpers.getDateTimeFormat();
const newIndicator = self.menuConfig.config.newIndicator || '*'; const newIndicator = self.menuConfig.config.newIndicator || '*';
const regIndicator = ' '.repeat(newIndicator.length); // fill with space to avoid draw issues 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
);
const isNew = _.isBoolean(listItem.isNew)
? listItem.isNew
: listItem.messageId > self.lastReadId;
listItem.newIndicator = isNew ? newIndicator : regIndicator; 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;
} }
@ -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,11 +378,11 @@ 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
);
async.forEachOf(
areaHighestIds,
(highestId, areaTag, nextArea) => {
messageArea.updateMessageAreaLastReadId( messageArea.updateMessageAreaLastReadId(
this.client.user.userId, this.client.user.userId,
areaTag, areaTag,
highestId, highestId,
err => { err => {
if(err) { if (err) {
this.client.log.warn( { error : err.message }, 'Failed marking area as read'); this.client.log.warn(
{ error: err.message },
'Failed marking area as read'
);
} else { } else {
// update newIndicator on messages // update newIndicator on messages
this.config.messageList.forEach(msg => { this.config.messageList.forEach(msg => {
if(areaTag === msg.areaTag) { if (areaTag === msg.areaTag) {
msg.newIndicator = regIndicator; msg.newIndicator = regIndicator;
} }
}); });
const msgListView = this.viewControllers.allViews.getView(MciViewIds.allViews.msgList); const msgListView = this.viewControllers.allViews.getView(
MciViewIds.allViews.msgList
);
msgListView.setItems(this.config.messageList); msgListView.setItems(this.config.messageList);
msgListView.redraw(); msgListView.redraw();
this.client.log.info( { highestId, areaTag }, 'User marked area as read'); 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

@ -17,19 +17,23 @@ function startup(cb) {
async.series( async.series(
[ [
function loadModules(callback) { function loadModules(callback) {
loadModulesForCategory('scannerTossers', (module, nextModule) => { loadModulesForCategory(
'scannerTossers',
(module, nextModule) => {
const modInst = new module.getModule(); 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 => { },
err => {
callback(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(
msgNetworkModules,
(modInst, next) => {
modInst.record(message); modInst.record(message);
next(); next();
}, err => { },
err => {
cb(err); cb(err);
}); }
);
} }

View File

@ -12,13 +12,12 @@ function MessageScanTossModule() {
require('util').inherits(MessageScanTossModule, PluginModule); require('util').inherits(MessageScanTossModule, PluginModule);
MessageScanTossModule.prototype.startup = function(cb) { MessageScanTossModule.prototype.startup = function (cb) {
return cb(null); return cb(null);
}; };
MessageScanTossModule.prototype.shutdown = function(cb) { MessageScanTossModule.prototype.shutdown = function (cb) {
return cb(null); return cb(null);
}; };
MessageScanTossModule.prototype.record = function(/*message*/) { MessageScanTossModule.prototype.record = function (/*message*/) {};
};

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