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,
"node": true
},
"extends": [
"eslint:recommended"
],
"extends": ["eslint:recommended"],
"rules": {
"indent": [
"error",
4,
{
"SwitchCase" : 1
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"comma-dangle": 0,
"no-trailing-spaces" :"warn"
"no-trailing-spaces": "warn"
},
"parserOptions": {
"ecmaVersion": 2020

12
.prettierignore Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,24 +2,23 @@
'use strict';
// ENiGMA½
var Log = require('./logger.js').log;
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
const Config = require('./config.js').get;
var iconv = require('iconv-lite');
var assert = require('assert');
var _ = require('lodash');
var Log = require('./logger.js').log;
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
const Config = require('./config.js').get;
var iconv = require('iconv-lite');
var assert = require('assert');
var _ = require('lodash');
exports.ClientTerminal = ClientTerminal;
exports.ClientTerminal = ClientTerminal;
function ClientTerminal(output) {
this.output = output;
this.output = output;
var outputEncoding = 'cp437';
assert(iconv.encodingExists(outputEncoding));
// convert line feeds such as \n -> \r\n
this.convertLF = true;
this.convertLF = true;
this.syncTermFontsEnabled = false;
@ -27,37 +26,37 @@ function ClientTerminal(output) {
// Some terminal we handle specially
// They can also be found in this.env{}
//
var termType = 'unknown';
var termHeight = 0;
var termWidth = 0;
var termClient = 'unknown';
var termType = 'unknown';
var termHeight = 0;
var termWidth = 0;
var termClient = 'unknown';
this.currentSyncFont = 'not_set';
this.currentSyncFont = 'not_set';
// Raw values set by e.g. telnet NAWS, ENVIRONMENT, etc.
this.env = {};
this.env = {};
Object.defineProperty(this, 'outputEncoding', {
get : function() {
get: function () {
return outputEncoding;
},
set : function(enc) {
if(iconv.encodingExists(enc)) {
set: function (enc) {
if (iconv.encodingExists(enc)) {
outputEncoding = enc;
} else {
Log.warn({ encoding : enc }, 'Unknown encoding');
Log.warn({ encoding: enc }, 'Unknown encoding');
}
}
},
});
Object.defineProperty(this, 'termType', {
get : function() {
get: function () {
return termType;
},
set : function(ttype) {
set: function (ttype) {
termType = ttype.toLowerCase();
if(this.isANSI()) {
if (this.isANSI()) {
this.outputEncoding = 'cp437';
} else {
// :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'
// 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', {
get : function() {
get: function () {
return termWidth;
},
set : function(width) {
if(width > 0) {
set: function (width) {
if (width > 0) {
termWidth = width;
}
}
},
});
Object.defineProperty(this, 'termHeight', {
get : function() {
get: function () {
return termHeight;
},
set : function(height) {
if(height > 0) {
set: function (height) {
if (height > 0) {
termHeight = height;
}
}
},
});
Object.defineProperty(this, 'termClient', {
get : function() {
get: function () {
return termClient;
},
set : function(tc) {
set: function (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;
};
ClientTerminal.prototype.isNixTerm = function() {
ClientTerminal.prototype.isNixTerm = function () {
//
// Standard *nix type terminals
//
if(this.termType.startsWith('xterm')) {
if (this.termType.startsWith('xterm')) {
return true;
}
@ -122,7 +124,7 @@ ClientTerminal.prototype.isNixTerm = function() {
return utf8TermList.includes(this.termType);
};
ClientTerminal.prototype.isANSI = function() {
ClientTerminal.prototype.isANSI = function () {
//
// 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)
ClientTerminal.prototype.write = function(s, convertLineFeeds, cb) {
ClientTerminal.prototype.write = function (s, convertLineFeeds, cb) {
this.rawWrite(this.encode(s, convertLineFeeds), cb);
};
ClientTerminal.prototype.rawWrite = function(s, cb) {
if(this.output && this.output.writable) {
ClientTerminal.prototype.rawWrite = function (s, cb) {
if (this.output && this.output.writable) {
this.output.write(s, err => {
if(cb) {
if (cb) {
return cb(err);
}
if(err) {
Log.warn( { error : err.message }, 'Failed writing to socket');
if (err) {
Log.warn({ error: err.message }, 'Failed writing to socket');
}
});
}
};
ClientTerminal.prototype.pipeWrite = function(s, cb) {
this.write(renegadeToAnsi(s, this), null, cb); // null = use default for |convertLineFeeds|
ClientTerminal.prototype.pipeWrite = function (s, cb) {
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;
if(convertLineFeeds && _.isString(s)) {
if (convertLineFeeds && _.isString(s)) {
s = s.replace(/\n/g, '\r\n');
}
return iconv.encode(s, this.outputEncoding);
};

View File

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

View File

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

View File

@ -2,9 +2,9 @@
'use strict';
// deps
const _ = require('lodash');
const _ = require('lodash');
exports.sortAreasOrConfs = sortAreasOrConfs;
exports.sortAreasOrConfs = sortAreasOrConfs;
//
// Method for sorting message, file, etc. areas and confs
@ -19,12 +19,12 @@ function sortAreasOrConfs(areasOrConfs, type) {
entryA = type ? a[type] : a;
entryB = type ? b[type] : b;
if(_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
if (_.isNumber(entryA.sort) && _.isNumber(entryB.sort)) {
return entryA.sort - entryB.sort;
} else {
const keyA = entryA.sort ? entryA.sort.toString() : entryA.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',
];
const replaceKeys = [
'args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch',
];
const replaceKeys = ['args', 'sendArgs', 'recvArgs', 'recvArgsNonBatch'];
const configOptions = Object.assign({}, options, {
defaultConfig : DefaultConfig,
defaultsCustomizer : (defaultVal, configVal, key, path) => {
defaultConfig: DefaultConfig,
defaultsCustomizer: (defaultVal, configVal, key, path) => {
if (Array.isArray(defaultVal) && Array.isArray(configVal)) {
if (replacePaths.includes(path) || replaceKeys.includes(key)) {
// full replacement using user config value
@ -42,7 +40,7 @@ exports.Config = class Config extends ConfigLoader {
}
}
},
onReload : err => {
onReload: err => {
if (!err) {
const Events = require('./events.js');
Events.emit(Events.getSystemEvents().ConfigChanged);

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2,26 +2,26 @@
'use strict';
// ENiGMA½
const ansi = require('./ansi_term.js');
const Events = require('./events.js');
const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js');
const ansi = require('./ansi_term.js');
const Events = require('./events.js');
const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js');
// deps
const async = require('async');
const async = require('async');
exports.connectEntry = connectEntry;
exports.connectEntry = connectEntry;
const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
let giveUpTimer;
const done = function(err) {
const done = function (err) {
client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer);
return cb(err);
};
const cprListener = (pos) => {
const cprListener = pos => {
cprHandler(pos);
return done(null);
};
@ -29,10 +29,10 @@ const withCursorPositionReport = (client, cprHandler, failMessage, cb) => {
client.once('cursor position report', cprListener);
// give up after 2s
giveUpTimer = setTimeout( () => {
giveUpTimer = setTimeout(() => {
return done(Errors.General(failMessage));
}, 2000);
}
};
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
// 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.
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
//
if(h > 1 || w > 1) {
return client.log.warn( { height : h, width : w }, 'Ignoring ANSI home position CPR due to unexpected values');
if (h > 1 || w > 1) {
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
//
@ -70,7 +73,7 @@ function ansiDiscoverHomePosition(client, cb) {
cb
);
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
client.term.write(`${ansi.goHome()}${ansi.queryPos()}`); // go home, query pos
}
function ansiAttemptDetectUTF8(client, cb) {
@ -87,7 +90,7 @@ function ansiAttemptDetectUTF8(client, cb) {
// "*nix" terminal -- that is, xterm, etc.
// 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);
}
@ -99,20 +102,24 @@ function ansiAttemptDetectUTF8(client, cb) {
pos => {
initialPosition = pos;
withCursorPositionReport(client,
withCursorPositionReport(
client,
pos => {
const [_, w] = pos;
const len = w - initialPosition[1];
if(!isNaN(len) && len >= ASCIIPortion.length + 6) { // CP437 displays 3 chars each Unicode skull
client.log.info('Terminal identified as UTF-8 but does not appear to be. Overriding to "ansi".');
if (!isNaN(len) && len >= ASCIIPortion.length + 6) {
// 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');
}
},
'Detect UTF-8 stage 2 timed out',
cb,
cb
);
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
client.term.rawWrite(`\u9760${ASCIIPortion}\u9760`); // Unicode skulls on each side
client.term.rawWrite(ansi.queryPos());
},
'Detect UTF-8 stage 1 timed out',
@ -150,7 +157,9 @@ const ansiQuerySyncTermFontSupport = (client, cb) => {
const [_, w] = pos;
if (w === 1) {
// 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;
}
},
@ -158,11 +167,13 @@ const ansiQuerySyncTermFontSupport = (client, 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) {
if(client.term.termHeight > 0 || client.term.termWidth > 0) {
if (client.term.termHeight > 0 || client.term.termWidth > 0) {
return cb(null);
}
@ -172,7 +183,7 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
//
// 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;
}
@ -182,20 +193,21 @@ function ansiQueryTermSizeIfNeeded(client, cb) {
// 999x999 values we asked to move to.
//
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(
{ height : h, width : w },
'Ignoring ANSI CPR screen size query response due to non-sane values');
{ height: h, width: w },
'Ignoring ANSI CPR screen size query response due to non-sane values'
);
}
client.term.termHeight = h;
client.term.termWidth = w;
client.term.termHeight = h;
client.term.termWidth = w;
client.log.debug(
{
termWidth : client.term.termWidth,
termHeight : client.term.termHeight,
source : 'ANSI CPR'
termWidth: client.term.termWidth,
termHeight: client.term.termHeight,
source: 'ANSI CPR',
},
'Window size updated'
);
@ -226,8 +238,7 @@ function displayBanner(term) {
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00`
);
|00`);
}
function connectEntry(client, nextMenu) {
@ -245,20 +256,23 @@ function connectEntry(client, nextMenu) {
},
function queryTermSizeByNonStandardAnsi(callback) {
ansiQueryTermSizeIfNeeded(client, err => {
if(err) {
if (err) {
//
// 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.
// Default to DOS size 80x25.
//
// :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.termWidth = 80;
term.termWidth = 80;
}
}
@ -268,7 +282,7 @@ function connectEntry(client, nextMenu) {
function checkUtf8IfNeeded(callback) {
return ansiAttemptDetectUTF8(client, callback);
},
function querySyncTERMFontSupport(callback) {
function querySyncTERMFontSupport(callback) {
return ansiQuerySyncTermFontSupport(client, callback);
},
],
@ -281,9 +295,9 @@ function connectEntry(client, nextMenu) {
displayBanner(term);
// fire event
Events.emit(Events.getSystemEvents().TermDetected, { client : client } );
Events.emit(Events.getSystemEvents().TermDetected, { client: client });
setTimeout( () => {
setTimeout(() => {
return client.menuStack.goto(nextMenu);
}, 500);
}

View File

@ -1,47 +1,265 @@
const CP437UnicodeTable = [
'\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'
'\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',
];
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 isCP437Encodable = (s) => {
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 isCP437Encodable = s => {
if (!s.length) {
return true;
}

View File

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

View File

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

View File

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

View File

@ -1,28 +1,28 @@
/* jslint node: true */
'use strict';
const stringFormat = require('./string_format.js');
const { Errors } = require('./enig_error.js');
const Events = require('./events');
const stringFormat = require('./string_format.js');
const { Errors } = require('./enig_error.js');
const Events = require('./events');
// deps
const pty = require('node-pty');
const decode = require('iconv-lite').decode;
const createServer = require('net').createServer;
const paths = require('path');
const _ = require('lodash');
const pty = require('node-pty');
const decode = require('iconv-lite').decode;
const createServer = require('net').createServer;
const paths = require('path');
const _ = require('lodash');
module.exports = class Door {
constructor(client) {
this.client = client;
this.restored = false;
this.client = client;
this.restored = false;
}
prepare(ioType, cb) {
this.io = ioType;
// we currently only have to do any real setup for 'socket'
if('socket' !== ioType) {
if ('socket' !== ioType) {
return cb(null);
}
@ -32,13 +32,16 @@ module.exports = class Door {
});
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);
});
this.sockServer.getConnections( (err, count) => {
this.sockServer.getConnections((err, count) => {
// We expect only one connection from our DOOR/emulator/etc.
if(!err && count <= 1) {
if (!err && count <= 1) {
this.client.term.output.pipe(conn);
conn.on('data', this.doorDataHandler.bind(this));
}
@ -53,39 +56,39 @@ module.exports = class Door {
run(exeInfo, cb) {
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'));
}
const cwd = exeInfo.cwd || paths.dirname(exeInfo.cmd);
const formatObj = {
dropFile : exeInfo.dropFile,
dropFilePath : exeInfo.dropFilePath,
node : exeInfo.node.toString(),
srvPort : this.sockServer ? this.sockServer.address().port.toString() : '-1',
userId : this.client.user.userId.toString(),
userName : this.client.user.getSanitizedName(),
userNameRaw : this.client.user.username,
cwd : cwd,
dropFile: exeInfo.dropFile,
dropFilePath: exeInfo.dropFilePath,
node: exeInfo.node.toString(),
srvPort: this.sockServer ? this.sockServer.address().port.toString() : '-1',
userId: this.client.user.userId.toString(),
userName: this.client.user.getSanitizedName(),
userNameRaw: this.client.user.username,
cwd: cwd,
};
const args = exeInfo.args.map( arg => stringFormat(arg, formatObj) );
const args = exeInfo.args.map(arg => stringFormat(arg, formatObj));
this.client.log.info(
{ cmd : exeInfo.cmd, args, io : this.io },
{ cmd: exeInfo.cmd, args, io: this.io },
'Executing external door process'
);
try {
this.doorPty = pty.spawn(exeInfo.cmd, args, {
cols : this.client.term.termWidth,
rows : this.client.term.termHeight,
cwd : cwd,
env : exeInfo.env,
encoding : null, // we want to handle all encoding ourself
cols: this.client.term.termWidth,
rows: this.client.term.termHeight,
cwd: cwd,
env: exeInfo.env,
encoding: null, // we want to handle all encoding ourself
});
} catch(e) {
} catch (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.
//
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(
{ pid : this.doorPty.pid },
{ pid: this.doorPty.pid },
'User has disconnected; Killing door process.'
);
this.doorPty.kill();
@ -103,10 +109,11 @@ module.exports = class Door {
});
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.term.output.pipe(this.doorPty);
@ -116,22 +123,25 @@ module.exports = class Door {
this.doorPty.once('close', () => {
return this.restoreIo(this.doorPty);
});
} else if('socket' === this.io) {
} else if ('socket' === this.io) {
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'
);
}
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();
}
// we may not get a close
if('stdio' === this.io) {
if ('stdio' === this.io) {
this.restoreIo(this.doorPty);
}
@ -147,13 +157,13 @@ module.exports = class Door {
}
restoreIo(piped) {
if(!this.restored) {
if(this.doorPty) {
if (!this.restored) {
if (this.doorPty) {
this.doorPty.kill();
}
const output = this.client.term.output;
if(output) {
if (output) {
output.unpipe(piped);
output.resume();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -2,57 +2,59 @@
'use strict';
// ENiGMA½
const TextView = require('./text_view.js').TextView;
const miscUtil = require('./misc_util.js');
const strUtil = require('./string_util.js');
const TextView = require('./text_view.js').TextView;
const miscUtil = require('./misc_util.js');
const strUtil = require('./string_util.js');
const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT;
// deps
const _ = require('lodash');
const _ = require('lodash');
exports.EditTextView = EditTextView;
exports.EditTextView = EditTextView;
const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, {
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) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false;
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false;
if(!_.isObject(options.specialKeyMap)) {
if (!_.isObject(options.specialKeyMap)) {
options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP;
}
TextView.call(this, options);
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);
if(this.text.length >= this.dimens.width) {
if (this.text.length >= this.dimens.width) {
this.redraw();
} else {
this.cursorPos.col -= 1;
if(this.cursorPos.col >= 0) {
if (this.cursorPos.col >= 0) {
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);
EditTextView.prototype.onKeyPress = function(ch, key) {
if(key) {
if(this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) {
EditTextView.prototype.onKeyPress = function (ch, key) {
if (key) {
if (this.isKeyMapped('backspace', key.name)) {
if (this.text.length > 0) {
this.clientBackspace();
}
@ -63,29 +65,29 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
if (this.text.length > 0 && this.cursorPos.col === this.text.length) {
this.clientBackspace();
}
} else if(this.isKeyMapped('clearLine', key.name)) {
this.text = '';
this.cursorPos.col = 0;
this.setFocus(true); // resetting focus will redraw & adjust cursor
} else if (this.isKeyMapped('clearLine', key.name)) {
this.text = '';
this.cursorPos.col = 0;
this.setFocus(true); // resetting focus will redraw & adjust cursor
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
}
}
if(ch && strUtil.isPrintable(ch)) {
if(this.text.length < this.maxLength) {
if (ch && strUtil.isPrintable(ch)) {
if (this.text.length < this.maxLength) {
ch = strUtil.stylizeString(ch, this.textStyle);
this.text += ch;
if(this.text.length > this.dimens.width) {
if (this.text.length > this.dimens.width) {
// no shortcuts - redraw the view
this.redraw();
} else {
this.cursorPos.col += 1;
if(_.isString(this.textMaskChar)) {
if(this.textMaskChar.length > 0) {
if (_.isString(this.textMaskChar)) {
if (this.textMaskChar.length > 0) {
this.client.term.write(this.textMaskChar);
}
} else {
@ -98,10 +100,10 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
};
EditTextView.prototype.setText = function(text) {
EditTextView.prototype.setText = function (text) {
// draw & set |text|
EditTextView.super_.prototype.setText.call(this, text);
// adjust local cursor tracking
this.cursorPos = { row : 0, col : text.length };
this.cursorPos = { row: 0, col: text.length };
};

View File

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

View File

@ -5,53 +5,65 @@ class EnigError extends Error {
constructor(message, code, reason, reasonCode) {
super(message);
this.name = this.constructor.name;
this.message = message;
this.code = code;
this.reason = reason;
this.name = this.constructor.name;
this.message = message;
this.code = code;
this.reason = reason;
this.reasonCode = reasonCode;
if(this.reason) {
if (this.reason) {
this.message += `: ${this.reason}`;
}
if(typeof Error.captureStackTrace === 'function') {
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = (new Error(message)).stack;
this.stack = new Error(message).stack;
}
}
}
exports.EnigError = EnigError;
exports.EnigError = EnigError;
exports.Errors = {
General : (reason, reasonCode) => new EnigError('An error occurred', -33000, reason, reasonCode),
MenuStack : (reason, reasonCode) => new EnigError('Menu stack error', -33001, reason, reasonCode),
DoesNotExist : (reason, reasonCode) => new EnigError('Object does not exist', -33002, reason, reasonCode),
AccessDenied : (reason, reasonCode) => new EnigError('Access denied', -32003, reason, reasonCode),
Invalid : (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
MissingConfig : (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),
General: (reason, reasonCode) =>
new EnigError('An error occurred', -33000, reason, reasonCode),
MenuStack: (reason, reasonCode) =>
new EnigError('Menu stack error', -33001, reason, reasonCode),
DoesNotExist: (reason, reasonCode) =>
new EnigError('Object does not exist', -33002, reason, reasonCode),
AccessDenied: (reason, reasonCode) =>
new EnigError('Access denied', -32003, reason, reasonCode),
Invalid: (reason, reasonCode) => new EnigError('Invalid', -32004, reason, reasonCode),
ExternalProcess: (reason, reasonCode) =>
new EnigError('External process error', -32005, reason, reasonCode),
MissingConfig: (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 = {
AlreadyThere : 'ALREADYTHERE',
InvalidNextMenu : 'BADNEXT',
NoPreviousMenu : 'NOPREV',
NoConditionMatch : 'NOCONDMATCH',
NotEnabled : 'NOTENABLED',
AlreadyLoggedIn : 'ALREADYLOGGEDIN',
TooMany : 'TOOMANY',
Disabled : 'DISABLED',
Inactive : 'INACTIVE',
Locked : 'LOCKED',
NotAllowed : 'NOTALLOWED',
Invalid2FA : 'INVALID2FA',
AlreadyThere: 'ALREADYTHERE',
InvalidNextMenu: 'BADNEXT',
NoPreviousMenu: 'NOPREV',
NoConditionMatch: 'NOCONDMATCH',
NotEnabled: 'NOTENABLED',
AlreadyLoggedIn: 'ALREADYLOGGEDIN',
TooMany: 'TOOMANY',
Disabled: 'DISABLED',
Inactive: 'INACTIVE',
Locked: 'LOCKED',
NotAllowed: 'NOTALLOWED',
Invalid2FA: 'INVALID2FA',
};

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,22 @@
'use strict';
// enigma-bbs
const MenuModule = require('./menu_module.js').MenuModule;
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
const StatLog = require('./stat_log.js');
const SysProps = require('./system_property.js');
const MenuModule = require('./menu_module.js').MenuModule;
const { getSortedAvailableFileAreas } = require('./file_base_area.js');
const StatLog = require('./stat_log.js');
const SysProps = require('./system_property.js');
// deps
const async = require('async');
const async = require('async');
exports.moduleInfo = {
name : 'File Area Selector',
desc : 'Select from available file areas',
author : 'NuSkooler',
name: 'File Area Selector',
desc: 'Select from available file areas',
author: 'NuSkooler',
};
const MciViewIds = {
areaList : 1,
areaList: 1,
};
exports.getModule = class FileAreaSelectModule extends MenuModule {
@ -25,26 +25,31 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
super(options);
this.menuMethods = {
selectArea : (formData, extraArgs, cb) => {
selectArea: (formData, extraArgs, cb) => {
const filterCriteria = {
areaTag : formData.value.areaTag,
areaTag: formData.value.areaTag,
};
const menuOpts = {
extraArgs : {
filterCriteria : filterCriteria,
extraArgs: {
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) {
super.mciReady(mciData, err => {
if(err) {
if (err) {
return cb(err);
}
@ -53,7 +58,9 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
async.waterfall(
[
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
const availAreas = getSortedAvailableFileAreas(self.client);
@ -66,18 +73,30 @@ exports.getModule = class FileAreaSelectModule extends MenuModule {
return callback(null, availAreas);
},
function prepView(availAreas, callback) {
self.prepViewController('allViews', 0, mciData.menu, (err, vc) => {
if(err) {
return callback(err);
self.prepViewController(
'allViews',
0,
mciData.menu,
(err, vc) => {
if (err) {
return callback(err);
}
const areaListView = vc.getView(MciViewIds.areaList);
areaListView.setItems(
availAreas.map(area =>
Object.assign(area, {
text: area.name,
data: area.areaTag,
})
)
);
areaListView.redraw();
return callback(null);
}
const areaListView = vc.getView(MciViewIds.areaList);
areaListView.setItems(availAreas.map(area => Object.assign(area, { text : area.name, data : area.areaTag } )));
areaListView.redraw();
return callback(null);
});
}
);
},
],
err => {
return cb(err);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,58 +2,61 @@
'use strict';
// ENiGMA½
const EnigAssert = require('./enigma_assert.js');
const EnigAssert = require('./enigma_assert.js');
// deps
const fse = require('fs-extra');
const paths = require('path');
const async = require('async');
const fse = require('fs-extra');
const paths = require('path');
const async = require('async');
exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling;
exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling;
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
exports.moveFileWithCollisionHandling = moveFileWithCollisionHandling;
exports.copyFileWithCollisionHandling = copyFileWithCollisionHandling;
exports.pathWithTerminatingSeparator = pathWithTerminatingSeparator;
function moveOrCopyFileWithCollisionHandling(src, dst, operation, cb) {
operation = operation || 'copy';
const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst);
operation = operation || 'copy';
const dstPath = paths.dirname(dst);
const dstFileExt = paths.extname(dst);
const dstFileSuffix = paths.basename(dst, dstFileExt);
EnigAssert('move' === operation || 'copy' === operation);
let renameIndex = 0;
let opOk = false;
let renameIndex = 0;
let opOk = false;
let tryDstPath;
function tryOperation(src, dst, callback) {
if('move' === operation) {
if ('move' === operation) {
fse.move(src, tryDstPath, err => {
return callback(err);
});
} else if('copy' === operation) {
fse.copy(src, tryDstPath, { overwrite : false, errorOnExist : true }, err => {
} else if ('copy' === operation) {
fse.copy(src, tryDstPath, { overwrite: false, errorOnExist: true }, err => {
return callback(err);
});
}
}
async.until(
(callback) => callback(null, opOk), // until moved OK
(cb) => {
if(0 === renameIndex) {
callback => callback(null, opOk), // until moved OK
cb => {
if (0 === renameIndex) {
// try originally supplied path first
tryDstPath = dst;
} else {
tryDstPath = paths.join(dstPath, `${dstFileSuffix}(${renameIndex})${dstFileExt}`);
tryDstPath = paths.join(
dstPath,
`${dstFileSuffix}(${renameIndex})${dstFileExt}`
);
}
tryOperation(src, tryDstPath, err => {
if(err) {
if (err) {
// for some reason fs-extra copy doesn't pass err.code
// :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;
return cb(null); // keep trying
return cb(null); // keep trying
}
return cb(err);
@ -82,7 +85,7 @@ function copyFileWithCollisionHandling(src, dst, cb) {
}
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;
}
return path;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,54 +2,56 @@
'use strict';
// deps
const bunyan = require('bunyan');
const paths = require('path');
const fs = require('graceful-fs');
const _ = require('lodash');
const bunyan = require('bunyan');
const paths = require('path');
const fs = require('graceful-fs');
const _ = require('lodash');
module.exports = class Log {
static init() {
const Config = require('./config.js').get();
const logPath = Config.paths.logs;
const Config = require('./config.js').get();
const logPath = Config.paths.logs;
const err = this.checkLogPath(logPath);
if(err) {
if (err) {
console.error(err.message); // eslint-disable-line no-console
return process.exit();
}
const logStreams = [];
if(_.isObject(Config.logging.rotatingFile)) {
Config.logging.rotatingFile.path = paths.join(logPath, Config.logging.rotatingFile.fileName);
if (_.isObject(Config.logging.rotatingFile)) {
Config.logging.rotatingFile.path = paths.join(
logPath,
Config.logging.rotatingFile.fileName
);
logStreams.push(Config.logging.rotatingFile);
}
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
[ 'formData', 'formValue' ].forEach(keyName => {
serializers[keyName] = (fd) => Log.hideSensitive(fd);
['formData', 'formValue'].forEach(keyName => {
serializers[keyName] = fd => Log.hideSensitive(fd);
});
this.log = bunyan.createLogger({
name : 'ENiGMA½ BBS',
streams : logStreams,
serializers : serializers,
name: 'ENiGMA½ BBS',
streams: logStreams,
serializers: serializers,
});
}
static checkLogPath(logPath) {
try {
if(!fs.statSync(logPath).isDirectory()) {
if (!fs.statSync(logPath).isDirectory()) {
return new Error(`${logPath} is not a directory`);
}
return null;
} catch(e) {
if('ENOENT' === e.code) {
} catch (e) {
if ('ENOENT' === e.code) {
return new Error(`${logPath} does not exist`);
}
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
//
return JSON.parse(
JSON.stringify(obj).replace(/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/, (match, valueName) => {
return `"${valueName}":"********"`;
})
JSON.stringify(obj).replace(
/"(password|passwordConfirm|key|authCode)"\s?:\s?"([^"]+)"/,
(match, valueName) => {
return `"${valueName}":"********"`;
}
)
);
} catch(e) {
} catch (e) {
// be safe and return empty obj!
return {};
}

View File

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

View File

@ -1,9 +1,9 @@
/* jslint node: true */
'use strict';
var events = require('events');
var assert = require('assert');
var _ = require('lodash');
var events = require('events');
var assert = require('assert');
var _ = require('lodash');
module.exports = MailPacket;
@ -16,7 +16,7 @@ function MailPacket(options) {
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
// or a buffer containing packet data
@ -26,7 +26,7 @@ MailPacket.prototype.read = function(options) {
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
//

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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