First pass formatting with Prettier
* Added .prettierrc.json * Added .prettierignore * Formatted
This commit is contained in:
parent
eecfb33ad5
commit
4881c2123a
|
@ -3,33 +3,22 @@
|
|||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
art
|
||||
config
|
||||
db
|
||||
docs
|
||||
drop
|
||||
gopher
|
||||
logs
|
||||
misc
|
||||
www
|
||||
mkdocs.yml
|
||||
*.md
|
||||
.github
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
57
core/acs.js
57
core/acs.js
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
2117
core/acs_parser.js
2117
core/acs_parser.js
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}\\`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
187
core/art.js
187
core/art.js
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
188
core/bbs.js
188
core/bbs.js
|
@ -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);
|
||||
|
|
190
core/bbs_link.js
190
core/bbs_link.js
|
@ -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) {
|
||||
|
|
310
core/bbs_list.js
310
core/bbs_list.js
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
488
core/client.js
488
core/client.js
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
100
core/connect.js
100
core/connect.js
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
48
core/crc.js
48
core/crc.js
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
127
core/database.js
127
core/database.js
|
@ -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);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
96
core/door.js
96
core/door.js
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,20 +23,24 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
284
core/dropfile.js
284
core/dropfile.js
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
183
core/exodus.js
183
core/exodus.js
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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,41 +515,36 @@ 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);
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FileAreaWebAccess();
|
||||
module.exports = new FileAreaWebAccess();
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -275,4 +323,4 @@ exports.getModule = class FileBaseListExport extends MenuModule {
|
|||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
2364
core/fse.js
2364
core/fse.js
File diff suppressed because it is too large
Load Diff
|
@ -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
222
core/ftn_util.js
222
core/ftn_util.js
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,11 +26,11 @@ 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
|
||||
//
|
||||
// emits 'packet' event per packet constructed
|
||||
//
|
||||
assert(_.isArray(options.messages));
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
14
core/mbf.js
14
core/mbf.js
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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})`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
790
core/message.js
790
core/message.js
File diff suppressed because it is too large
Load Diff
|
@ -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,20 +833,27 @@ 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -425,4 +477,4 @@ exports.getModule = class MessageBaseQWKExport extends MenuModule {
|
|||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
313
core/mrc.js
313
core/mrc.js
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
@ -82,4 +82,4 @@ exports.getModule = class AreaPostFSEModule extends FullScreenEditorModule {
|
|||
|
||||
super.initSequence();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
403
core/msg_list.js
403
core/msg_list.js
|
@ -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 => {
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue